mirror of
https://github.com/docker/cli.git
synced 2026-11-12 15:53:52 -04:00
Compare commits
268
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c32057333e | ||
|
|
a7abd65c2c | ||
|
|
3d49d403c7 | ||
|
|
9a594cbe33 | ||
|
|
dd25cd2d2b | ||
|
|
6ff84b8076 | ||
|
|
09b43d5ed9 | ||
|
|
a34ca56d30 | ||
|
|
6ce1dd5384 | ||
|
|
3768d28545 | ||
|
|
eb94fd0d81 | ||
|
|
2d468ad289 | ||
|
|
069e27c180 | ||
|
|
72d8f66a68 | ||
|
|
1dba0fa8ec | ||
|
|
5e5ccaa829 | ||
|
|
ac722bb368 | ||
|
|
abf372a871 | ||
|
|
416c061000 | ||
|
|
5adf6be6d8 | ||
|
|
1d72a32b64 | ||
|
|
fcad2c6584 | ||
|
|
3e271deeca | ||
|
|
867baaff66 | ||
|
|
cfc99a548f | ||
|
|
58e6338654 | ||
|
|
f18b630f22 | ||
|
|
8239b55d3e | ||
|
|
8f7c53974d | ||
|
|
1da2df7800 | ||
|
|
6131eb9dbc | ||
|
|
7dd9a687f1 | ||
|
|
7ff18d219f | ||
|
|
77a921c5c8 | ||
|
|
7a9b855cca | ||
|
|
0f15753db9 | ||
|
|
fd1f5bfad0 | ||
|
|
631220fddb | ||
|
|
e1ba8df80f | ||
|
|
fcfe5f32af | ||
|
|
e7f68af778 | ||
|
|
5c94bb0209 | ||
|
|
672b1497b9 | ||
|
|
5aa844d97b | ||
|
|
71c12cf295 | ||
|
|
9d193ea872 | ||
|
|
4f35b26d40 | ||
|
|
eefb7634f9 | ||
|
|
4d6486dd68 | ||
|
|
ddaded1b1c | ||
|
|
1f3e30e1d2 | ||
|
|
3edca7e89d | ||
|
|
e505b13caf | ||
|
|
9c25cb58dc | ||
|
|
26f825f5de | ||
|
|
e0ba392319 | ||
|
|
15258b687e | ||
|
|
000d6dd870 | ||
|
|
6ecab75472 | ||
|
|
570d237c3a | ||
|
|
92955ed461 | ||
|
|
fa6e4acd43 | ||
|
|
8f67e817fa | ||
|
|
7e32d44867 | ||
|
|
e26416f371 | ||
|
|
35b5ac3b88 | ||
|
|
68f76364d2 | ||
|
|
8a62233e06 | ||
|
|
240022fbd1 | ||
|
|
4a1dd71c8c | ||
|
|
67df4e8ca7 | ||
|
|
b0dca399b1 | ||
|
|
006486f65d | ||
|
|
eea509a890 | ||
|
|
6f09a927b0 | ||
|
|
86dd4c7172 | ||
|
|
1fe3a6f334 | ||
|
|
03b983e175 | ||
|
|
99e34836ce | ||
|
|
32213b8eab | ||
|
|
9010f6b088 | ||
|
|
eff14affdf | ||
|
|
aff002a7ca | ||
|
|
d1567c200d | ||
|
|
ed23fd6148 | ||
|
|
0dbe22b5e7 | ||
|
|
398104f939 | ||
|
|
d170759bcd | ||
|
|
73624c4bde | ||
|
|
974dcfd654 | ||
|
|
0d34fe7fb2 | ||
|
|
3b389dd273 | ||
|
|
28f1b586f6 | ||
|
|
dcec47bf2b | ||
|
|
b298f8f2cd | ||
|
|
8cbad756d9 | ||
|
|
93772a590d | ||
|
|
fec554c357 | ||
|
|
be6f8b2b69 | ||
|
|
67474fb865 | ||
|
|
5840a39af4 | ||
|
|
e2949a1333 | ||
|
|
e7f258feaf | ||
|
|
9835110f8b | ||
|
|
cb6a1dfe6f | ||
|
|
3eee043fec | ||
|
|
d07bce7fd2 | ||
|
|
63e163cf3c | ||
|
|
42e07e50fb | ||
|
|
6ef0757c0e | ||
|
|
d741c64884 | ||
|
|
dccbbfc4c7 | ||
|
|
c0581e8a38 | ||
|
|
0cfc19830b | ||
|
|
ae7cd7e16a | ||
|
|
9e8d522f77 | ||
|
|
3882e674e4 | ||
|
|
10483a1bfe | ||
|
|
de94fd5ecf | ||
|
|
9cc20a29ce | ||
|
|
c092f42b29 | ||
|
|
30c9ef4dc5 | ||
|
|
7f62da4ce8 | ||
|
|
3ef3f29a03 | ||
|
|
d652eeefee | ||
|
|
f7146743f7 | ||
|
|
19a4a3ade2 | ||
|
|
ca26349ad1 | ||
|
|
49d789f800 | ||
|
|
865d9bce65 | ||
|
|
01c822f924 | ||
|
|
42f0a62edc | ||
|
|
7a54b7bea2 | ||
|
|
551ac7493d | ||
|
|
60933212ff | ||
|
|
def48b36de | ||
|
|
a483dfd10b | ||
|
|
dd7238df1e | ||
|
|
5df4fd8b46 | ||
|
|
e7f7256b25 | ||
|
|
90c59dcdf4 | ||
|
|
710a4042d1 | ||
|
|
44a7d9537f | ||
|
|
a98af2b396 | ||
|
|
254f611800 | ||
|
|
a8d4f0d392 | ||
|
|
6f353c8000 | ||
|
|
63b205048f | ||
|
|
33f7513d5f | ||
|
|
c76773b197 | ||
|
|
ef23cbc431 | ||
|
|
c769f20797 | ||
|
|
fcab2d0f9a | ||
|
|
802f174889 | ||
|
|
498743aed2 | ||
|
|
fde09d1d87 | ||
|
|
bc4487a59e | ||
|
|
f332962161 | ||
|
|
3cda7c30c6 | ||
|
|
6c0f9022c8 | ||
|
|
776388cc19 | ||
|
|
8e00eb4a69 | ||
|
|
91b7318833 | ||
|
|
17357a7783 | ||
|
|
2cf68537d7 | ||
|
|
4a42917081 | ||
|
|
f914316a97 | ||
|
|
9dfe5a29ee | ||
|
|
2c1fde2239 | ||
|
|
99124087e0 | ||
|
|
86e79b55ed | ||
|
|
ada43812d0 | ||
|
|
ced769fc12 | ||
|
|
c648e0b065 | ||
|
|
f480fb1e37 | ||
|
|
ad7ae5a894 | ||
|
|
b6cd3c85b5 | ||
|
|
aa85421ff8 | ||
|
|
6bc4bf4f05 | ||
|
|
8a35f92fff | ||
|
|
2cb152c41d | ||
|
|
623da1997e | ||
|
|
e76242fff2 | ||
|
|
0b3f54066c | ||
|
|
4b4d7e2b48 | ||
|
|
e67e5559d5 | ||
|
|
8b783cc22a | ||
|
|
9d4a21cd79 | ||
|
|
d45284a65d | ||
|
|
fe7eb06263 | ||
|
|
7192601845 | ||
|
|
726dfe92ec | ||
|
|
a8e3b4b852 | ||
|
|
f88c159c7a | ||
|
|
6c052fcda3 | ||
|
|
079e80889d | ||
|
|
ff448ff455 | ||
|
|
476933b68b | ||
|
|
6131fe9e45 | ||
|
|
bbc791ed7d | ||
|
|
dd6f77bbaa | ||
|
|
cb338ec972 | ||
|
|
ffafb38e30 | ||
|
|
a763e51147 | ||
|
|
3c7de64541 | ||
|
|
3d0d576af7 | ||
|
|
2609bd23ff | ||
|
|
5fd2c08502 | ||
|
|
24d608d5f1 | ||
|
|
06ab5992dc | ||
|
|
5cd7710a04 | ||
|
|
683b099613 | ||
|
|
391b2f0fab | ||
|
|
209e9c0c13 | ||
|
|
ce68ce7ae8 | ||
|
|
c28cb35c42 | ||
|
|
948dfa91c9 | ||
|
|
f813c9639f | ||
|
|
97d91f13aa | ||
|
|
6efc4eaccb | ||
|
|
c7cdab58d2 | ||
|
|
d93d78588d | ||
|
|
3e7cbfdee1 | ||
|
|
8e38271f23 | ||
|
|
569dd73db1 | ||
|
|
f6643207a2 | ||
|
|
f381e08425 | ||
|
|
18f20a5537 | ||
|
|
d3a36fc38c | ||
|
|
59bb07f2e4 | ||
|
|
80f27987f4 | ||
|
|
6a8406e602 | ||
|
|
c2c122fb65 | ||
|
|
40a48e4154 | ||
|
|
a43c9f3440 | ||
|
|
114e17ac4b | ||
|
|
e2c402118c | ||
|
|
d07453890c | ||
|
|
288b6c79fe | ||
|
|
fbab8cd2be | ||
|
|
b898a46135 | ||
|
|
90a72a5894 | ||
|
|
4c63110a92 | ||
|
|
b61b5a9878 | ||
|
|
84fe451ec7 | ||
|
|
71615c2df1 | ||
|
|
a1acc9af91 | ||
|
|
95066ff3a2 | ||
|
|
0dbf70fad2 | ||
|
|
e0b8e19687 | ||
|
|
98e874dac7 | ||
|
|
92164b0306 | ||
|
|
5af8077eeb | ||
|
|
d352c504a8 | ||
|
|
28c74b759b | ||
|
|
57a502772b | ||
|
|
14ac8db968 | ||
|
|
1ab7665be8 | ||
|
|
1810e922ac | ||
|
|
5051d82a17 | ||
|
|
7f4e3ead75 | ||
|
|
a5ee5b1dfc | ||
|
|
27b19a6acf | ||
|
|
ab4ef4aed4 | ||
|
|
14aac2c232 | ||
|
|
0cd15abfde | ||
|
|
168f1b55e2 | ||
|
|
53ed25d9b6 |
@@ -0,0 +1,19 @@
|
|||||||
|
# This is a dummy CircleCI config file to avoid GitHub status failures reported
|
||||||
|
# on branches that don't use CircleCI. This file should be deleted when all
|
||||||
|
# branches are no longer dependent on CircleCI.
|
||||||
|
version: 2
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
dummy:
|
||||||
|
docker:
|
||||||
|
- image: busybox
|
||||||
|
steps:
|
||||||
|
- run:
|
||||||
|
name: "dummy"
|
||||||
|
command: echo "dummy job"
|
||||||
|
|
||||||
|
workflows:
|
||||||
|
version: 2
|
||||||
|
ci:
|
||||||
|
jobs:
|
||||||
|
- dummy
|
||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
/build/
|
/build/
|
||||||
/cmd/docker/winresources/versioninfo.json
|
/cli/winresources/versioninfo.json
|
||||||
/cmd/docker/winresources/*.syso
|
/cli/winresources/*.syso
|
||||||
/man/man*/
|
/man/man*/
|
||||||
/man/vendor/
|
/man/vendor/
|
||||||
/man/go.sum
|
/man/go.sum
|
||||||
|
|||||||
@@ -1,14 +1,3 @@
|
|||||||
* text=auto
|
|
||||||
|
|
||||||
Dockerfile* linguist-language=Dockerfile
|
Dockerfile* linguist-language=Dockerfile
|
||||||
vendor.mod linguist-language=Go-Module
|
vendor.mod linguist-language=Go-Module
|
||||||
vendor.sum linguist-language=Go-Checksums
|
vendor.sum linguist-language=Go-Checksums
|
||||||
|
|
||||||
*.go -text diff=golang
|
|
||||||
|
|
||||||
# scripts directory contains shell scripts
|
|
||||||
# without extensions, so we need to force
|
|
||||||
scripts/** text=auto eol=lf
|
|
||||||
|
|
||||||
# shell scripts should always have LF
|
|
||||||
*.sh text eol=lf
|
|
||||||
|
|||||||
+4
-3
@@ -1,6 +1,7 @@
|
|||||||
# GitHub code owners
|
# GitHub code owners
|
||||||
# See https://github.com/blog/2392-introducing-code-owners
|
# See https://github.com/blog/2392-introducing-code-owners
|
||||||
|
|
||||||
cli/command/stack/** @silvin-lubecki @docker/runtime-owners
|
cli/command/stack/** @silvin-lubecki
|
||||||
contrib/completion/bash/** @albers @docker/runtime-owners
|
contrib/completion/bash/** @albers
|
||||||
docs/** @thaJeztah @docker/runtime-owners
|
contrib/completion/zsh/** @sdurrheimer
|
||||||
|
docs/** @thaJeztah
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
Thank you for taking the time to report a bug!
|
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).
|
If this is a security issue please report it to the [Docker Security team](mailto:security@docker.com).
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: description
|
id: description
|
||||||
attributes:
|
attributes:
|
||||||
label: Description
|
label: Description
|
||||||
description: Give a clear and concise description of the bug
|
description: Please give a clear and concise description of the bug
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
|
|||||||
@@ -1,17 +1,11 @@
|
|||||||
blank_issues_enabled: false
|
blank_issues_enabled: false
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: "🗃️ Docker Sandboxes: report an issue"
|
- name: "Contributing to Docker"
|
||||||
about: "Issues with Docker Sandboxes should be filed in the Docker Desktop feedback tracker (not docker/cli)."
|
|
||||||
url: "https://github.com/docker/desktop-feedback/issues"
|
|
||||||
- name: "🖥️ Docker Desktop: report an issue"
|
|
||||||
about: "General Docker Desktop issues (installation, upgrades, UI, networking on macOS/Windows, WSL2, etc.) should be filed in the Docker Desktop feedback tracker (not docker/cli)."
|
|
||||||
url: "https://github.com/docker/desktop-feedback/issues"
|
|
||||||
- name: "🧑💻 Contributing to Docker"
|
|
||||||
about: "Read guidelines and tips about contributing to Docker."
|
about: "Read guidelines and tips about contributing to Docker."
|
||||||
url: "https://github.com/docker/cli/blob/master/CONTRIBUTING.md"
|
url: "https://github.com/docker/cli/blob/master/CONTRIBUTING.md"
|
||||||
- name: "🔒 Security and Vulnerabilities"
|
- name: "Security and Vulnerabilities"
|
||||||
about: "Report any security issues or vulnerabilities responsibly to the Docker security team. Do not use the public issue tracker."
|
about: "Please report any security issues or vulnerabilities responsibly to the Docker security team. Please do not use the public issue tracker."
|
||||||
url: "https://github.com/moby/moby/security/policy"
|
url: "https://github.com/moby/moby/security/policy"
|
||||||
- name: "💬 General Support"
|
- name: "General Support"
|
||||||
about: "Get the help you need to build, share, and run your Docker applications"
|
about: "Get the help you need to build, share, and run your Docker applications"
|
||||||
url: "https://www.docker.com/support/"
|
url: "https://www.docker.com/support/"
|
||||||
|
|||||||
@@ -2,36 +2,29 @@
|
|||||||
Please make sure you've read and understood our contributing guidelines;
|
Please make sure you've read and understood our contributing guidelines;
|
||||||
https://github.com/docker/cli/blob/master/CONTRIBUTING.md
|
https://github.com/docker/cli/blob/master/CONTRIBUTING.md
|
||||||
|
|
||||||
Make sure commits are signed off (`git commit -s`) with your real name.
|
** Make sure all your commits include a signature generated with `git commit -s` **
|
||||||
|
|
||||||
If the PR relates to an existing issue or PR, mention it at the top:
|
For additional information on our contributing process, read our contributing
|
||||||
|
guide https://docs.docker.com/opensource/code/
|
||||||
|
|
||||||
- Fixes: https://github.com/docker/cli/issues/12345678
|
If this is a bug fix, make sure your description includes "fixes #xxxx", or
|
||||||
- Replaces: https://github.com/docker/cli/pull/12345678
|
"closes #xxxx"
|
||||||
- Follow up to: https://github.com/docker/cli/pull/12345678
|
|
||||||
- Related to: https://github.com/docker/cli/issues/12345678
|
Please provide the following information:
|
||||||
-->
|
-->
|
||||||
|
|
||||||
## Summary
|
**- What I did**
|
||||||
|
|
||||||
## Release notes (optional)
|
**- How I did it**
|
||||||
|
|
||||||
|
**- How to verify it**
|
||||||
|
|
||||||
|
**- Description for the changelog**
|
||||||
<!--
|
<!--
|
||||||
One-sentence user-facing release note.
|
Write a short (one line) summary that describes the changes in this
|
||||||
Leave empty if the change is not changelog-worthy.
|
pull request for inclusion in the changelog:
|
||||||
Use present tense and imperative tone.
|
|
||||||
|
|
||||||
Good:
|
|
||||||
- Fix a panic when running `docker top` on a non-running Windows container.
|
|
||||||
- Don't print warnings in `docker info` for broken symlinks in CLI plugin directories.
|
|
||||||
|
|
||||||
Bad:
|
|
||||||
- Refactor test TestFooWithBar
|
|
||||||
- Fixed a panic when running docker top
|
|
||||||
-->
|
-->
|
||||||
|
|
||||||
```markdown changelog
|
|
||||||
|
|
||||||
```
|
**- A picture of a cute animal (not mandatory but encouraged)**
|
||||||
|
|
||||||
## A picture of a cute animal (not mandatory but encouraged)
|
|
||||||
|
|||||||
+1
-10
@@ -5,14 +5,5 @@ updates:
|
|||||||
schedule:
|
schedule:
|
||||||
interval: "daily"
|
interval: "daily"
|
||||||
labels:
|
labels:
|
||||||
- "area/ci"
|
- "area/testing"
|
||||||
- "status/2-code-review"
|
- "status/2-code-review"
|
||||||
cooldown:
|
|
||||||
default-days: 7
|
|
||||||
groups:
|
|
||||||
codeql-actions:
|
|
||||||
patterns:
|
|
||||||
- "github/codeql-action/*"
|
|
||||||
docker-actions:
|
|
||||||
patterns:
|
|
||||||
- "docker/*"
|
|
||||||
|
|||||||
+20
-90
@@ -1,51 +1,28 @@
|
|||||||
name: build
|
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:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
env:
|
|
||||||
VERSION: ${{ github.ref }}
|
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- 'master'
|
- 'master'
|
||||||
- '[0-9]+.[0-9]+'
|
- '[0-9]+.[0-9]+'
|
||||||
- '[0-9]+.x'
|
|
||||||
tags:
|
tags:
|
||||||
- 'v*'
|
- 'v*'
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
prepare:
|
prepare:
|
||||||
runs-on: ubuntu-26.04
|
runs-on: ubuntu-20.04
|
||||||
outputs:
|
outputs:
|
||||||
matrix: ${{ steps.platforms.outputs.matrix }}
|
matrix: ${{ steps.platforms.outputs.matrix }}
|
||||||
steps:
|
steps:
|
||||||
- name: Configure credentials
|
|
||||||
continue-on-error: true
|
|
||||||
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
|
|
||||||
with:
|
|
||||||
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
|
|
||||||
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
|
|
||||||
profile: administrator
|
|
||||||
profile-region: us-east-1
|
|
||||||
-
|
-
|
||||||
name: Checkout
|
name: Checkout
|
||||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
uses: actions/checkout@v3
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
-
|
-
|
||||||
name: Create matrix
|
name: Create matrix
|
||||||
id: platforms
|
id: platforms
|
||||||
@@ -57,7 +34,7 @@ jobs:
|
|||||||
echo ${{ steps.platforms.outputs.matrix }}
|
echo ${{ steps.platforms.outputs.matrix }}
|
||||||
|
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-26.04
|
runs-on: ubuntu-20.04
|
||||||
needs:
|
needs:
|
||||||
- prepare
|
- prepare
|
||||||
strategy:
|
strategy:
|
||||||
@@ -71,20 +48,17 @@ jobs:
|
|||||||
- ""
|
- ""
|
||||||
- glibc
|
- glibc
|
||||||
steps:
|
steps:
|
||||||
- name: Configure credentials
|
-
|
||||||
continue-on-error: true
|
name: Checkout
|
||||||
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
|
fetch-depth: 0
|
||||||
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
|
|
||||||
profile: administrator
|
|
||||||
profile-region: us-east-1
|
|
||||||
-
|
-
|
||||||
name: Set up Docker Buildx
|
name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
|
uses: docker/setup-buildx-action@v2
|
||||||
-
|
-
|
||||||
name: Build
|
name: Build
|
||||||
uses: docker/bake-action@018cb6412ab401ebaa809aa5f85966b74628600f # v7.4.0
|
uses: docker/bake-action@v3
|
||||||
with:
|
with:
|
||||||
targets: ${{ matrix.target }}
|
targets: ${{ matrix.target }}
|
||||||
set: |
|
set: |
|
||||||
@@ -96,69 +70,30 @@ jobs:
|
|||||||
working-directory: ./build
|
working-directory: ./build
|
||||||
run: |
|
run: |
|
||||||
mkdir /tmp/out
|
mkdir /tmp/out
|
||||||
platformPair=${PLATFORM//\//-}
|
platform=${{ matrix.platform }}
|
||||||
|
platformPair=${platform//\//-}
|
||||||
tar -cvzf "/tmp/out/docker-${platformPair}.tar.gz" .
|
tar -cvzf "/tmp/out/docker-${platformPair}.tar.gz" .
|
||||||
if [ -z "${{ matrix.use_glibc }}" ]; then
|
if [ -z "${{ matrix.use_glibc }}" ]; then
|
||||||
echo "ARTIFACT_NAME=${{ matrix.target }}-${platformPair}" >> $GITHUB_ENV
|
echo "ARTIFACT_NAME=${{ matrix.target }}-${platformPair}" >> $GITHUB_ENV
|
||||||
else
|
else
|
||||||
echo "ARTIFACT_NAME=${{ matrix.target }}-${platformPair}-glibc" >> $GITHUB_ENV
|
echo "ARTIFACT_NAME=${{ matrix.target }}-${platformPair}-glibc" >> $GITHUB_ENV
|
||||||
fi
|
fi
|
||||||
env:
|
|
||||||
PLATFORM: ${{ matrix.platform }}
|
|
||||||
-
|
-
|
||||||
name: Upload artifacts
|
name: Upload artifacts
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: ${{ env.ARTIFACT_NAME }}
|
name: ${{ env.ARTIFACT_NAME }}
|
||||||
path: /tmp/out/*
|
path: /tmp/out/*
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
bin-image:
|
|
||||||
if: ${{ github.event_name != 'pull_request' && github.repository == 'docker/cli' }}
|
|
||||||
uses: docker/github-builder/.github/workflows/bake.yml@58cb9f5b71b1836d6f690c1e95effdeb9b98cb8a # v1.17.0
|
|
||||||
permissions:
|
|
||||||
contents: read # same as global permission
|
|
||||||
id-token: write # for signing attestation(s) and authenticating to Docker Hub with GitHub OIDC Token
|
|
||||||
with:
|
|
||||||
setup-qemu: true
|
|
||||||
target: bin-image-cross
|
|
||||||
cache: true
|
|
||||||
cache-scope: bin-image
|
|
||||||
output: image
|
|
||||||
push: true
|
|
||||||
vars: |
|
|
||||||
VERSION=${{ github.ref }}
|
|
||||||
meta-images: |
|
|
||||||
dockereng/cli-bin
|
|
||||||
meta-tags: |
|
|
||||||
type=semver,pattern={{version}}
|
|
||||||
type=ref,event=branch
|
|
||||||
type=ref,event=pr
|
|
||||||
type=semver,pattern={{major}}
|
|
||||||
type=semver,pattern={{major}}.{{minor}}
|
|
||||||
registry-identities: |
|
|
||||||
- type: dockerhub
|
|
||||||
username: dockereng
|
|
||||||
connection_id: ${{ vars.DOCKERHUB_OIDC_CONNECTIONID }}
|
|
||||||
|
|
||||||
prepare-plugins:
|
prepare-plugins:
|
||||||
runs-on: ubuntu-26.04
|
runs-on: ubuntu-20.04
|
||||||
outputs:
|
outputs:
|
||||||
matrix: ${{ steps.platforms.outputs.matrix }}
|
matrix: ${{ steps.platforms.outputs.matrix }}
|
||||||
steps:
|
steps:
|
||||||
- name: Configure credentials
|
|
||||||
continue-on-error: true
|
|
||||||
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
|
|
||||||
with:
|
|
||||||
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
|
|
||||||
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
|
|
||||||
profile: administrator
|
|
||||||
profile-region: us-east-1
|
|
||||||
-
|
-
|
||||||
name: Checkout
|
name: Checkout
|
||||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
uses: actions/checkout@v3
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
-
|
-
|
||||||
name: Create matrix
|
name: Create matrix
|
||||||
id: platforms
|
id: platforms
|
||||||
@@ -170,7 +105,7 @@ jobs:
|
|||||||
echo ${{ steps.platforms.outputs.matrix }}
|
echo ${{ steps.platforms.outputs.matrix }}
|
||||||
|
|
||||||
plugins:
|
plugins:
|
||||||
runs-on: ubuntu-26.04
|
runs-on: ubuntu-20.04
|
||||||
needs:
|
needs:
|
||||||
- prepare-plugins
|
- prepare-plugins
|
||||||
strategy:
|
strategy:
|
||||||
@@ -178,20 +113,15 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
platform: ${{ fromJson(needs.prepare-plugins.outputs.matrix) }}
|
platform: ${{ fromJson(needs.prepare-plugins.outputs.matrix) }}
|
||||||
steps:
|
steps:
|
||||||
- name: Configure credentials
|
-
|
||||||
continue-on-error: true
|
name: Checkout
|
||||||
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
|
uses: actions/checkout@v3
|
||||||
with:
|
|
||||||
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
|
|
||||||
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
|
|
||||||
profile: administrator
|
|
||||||
profile-region: us-east-1
|
|
||||||
-
|
-
|
||||||
name: Set up Docker Buildx
|
name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
|
uses: docker/setup-buildx-action@v2
|
||||||
-
|
-
|
||||||
name: Build
|
name: Build
|
||||||
uses: docker/bake-action@018cb6412ab401ebaa809aa5f85966b74628600f # v7.4.0
|
uses: docker/bake-action@v3
|
||||||
with:
|
with:
|
||||||
targets: plugins-cross
|
targets: plugins-cross
|
||||||
set: |
|
set: |
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
name: codeql
|
||||||
|
|
||||||
|
on:
|
||||||
|
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-20.04
|
||||||
|
env:
|
||||||
|
DISABLE_WARN_OUTSIDE_CONTAINER: '1'
|
||||||
|
|
||||||
|
steps:
|
||||||
|
-
|
||||||
|
name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 2
|
||||||
|
-
|
||||||
|
name: Checkout HEAD on PR
|
||||||
|
if: ${{ github.event_name == 'pull_request' }}
|
||||||
|
run: |
|
||||||
|
git checkout HEAD^2
|
||||||
|
-
|
||||||
|
name: Initialize CodeQL
|
||||||
|
uses: github/codeql-action/init@v2
|
||||||
|
with:
|
||||||
|
languages: go
|
||||||
|
# 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: Autobuild
|
||||||
|
uses: github/codeql-action/autobuild@v2
|
||||||
|
-
|
||||||
|
name: Perform CodeQL Analysis
|
||||||
|
uses: github/codeql-action/analyze@v2
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
name: codeql
|
|
||||||
|
|
||||||
# Default to 'contents: read', which grants actions to read commits.
|
|
||||||
#
|
|
||||||
# If any permission is set, any permission not included in the list is
|
|
||||||
# implicitly set to "none".
|
|
||||||
#
|
|
||||||
# see https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- 'master'
|
|
||||||
- '[0-9]+.[0-9]+'
|
|
||||||
- '[0-9]+.x'
|
|
||||||
tags:
|
|
||||||
- 'v*'
|
|
||||||
pull_request:
|
|
||||||
# The branches below must be a subset of the branches above
|
|
||||||
branches: ["master"]
|
|
||||||
schedule:
|
|
||||||
# ┌───────────── minute (0 - 59)
|
|
||||||
# │ ┌───────────── hour (0 - 23)
|
|
||||||
# │ │ ┌───────────── day of the month (1 - 31)
|
|
||||||
# │ │ │ ┌───────────── month (1 - 12)
|
|
||||||
# │ │ │ │ ┌───────────── day of the week (0 - 6) (Sunday to Saturday)
|
|
||||||
# │ │ │ │ │
|
|
||||||
# │ │ │ │ │
|
|
||||||
# │ │ │ │ │
|
|
||||||
# * * * * *
|
|
||||||
- cron: '0 9 * * 4'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
codeql:
|
|
||||||
runs-on: ubuntu-26.04
|
|
||||||
timeout-minutes: 10
|
|
||||||
env:
|
|
||||||
DISABLE_WARN_OUTSIDE_CONTAINER: '1'
|
|
||||||
permissions:
|
|
||||||
actions: read
|
|
||||||
contents: read
|
|
||||||
security-events: write
|
|
||||||
|
|
||||||
steps:
|
|
||||||
-
|
|
||||||
name: Checkout
|
|
||||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
||||||
with:
|
|
||||||
fetch-depth: 2
|
|
||||||
persist-credentials: false
|
|
||||||
# CodeQL 2.16.4's auto-build added support for multi-module repositories,
|
|
||||||
# and is trying to be smart by searching for modules in every directory,
|
|
||||||
# including vendor directories. If no module is found, it's creating one
|
|
||||||
# which is ... not what we want, so let's give it a "go.mod".
|
|
||||||
# see: https://github.com/docker/cli/pull/4944#issuecomment-2002034698
|
|
||||||
-
|
|
||||||
name: Create go.mod
|
|
||||||
run: |
|
|
||||||
ln -s vendor.mod go.mod
|
|
||||||
ln -s vendor.sum go.sum
|
|
||||||
-
|
|
||||||
name: Update Go
|
|
||||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
|
||||||
with:
|
|
||||||
go-version: "1.26.8"
|
|
||||||
cache: false
|
|
||||||
-
|
|
||||||
name: Initialize CodeQL
|
|
||||||
uses: github/codeql-action/init@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
|
|
||||||
with:
|
|
||||||
languages: go
|
|
||||||
-
|
|
||||||
name: Autobuild
|
|
||||||
uses: github/codeql-action/autobuild@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
|
|
||||||
-
|
|
||||||
name: Perform CodeQL Analysis
|
|
||||||
uses: github/codeql-action/analyze@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
|
|
||||||
with:
|
|
||||||
category: "/language:go"
|
|
||||||
+17
-43
@@ -1,14 +1,5 @@
|
|||||||
name: e2e
|
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:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
@@ -19,72 +10,55 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- 'master'
|
- 'master'
|
||||||
- '[0-9]+.[0-9]+'
|
- '[0-9]+.[0-9]+'
|
||||||
- '[0-9]+.x'
|
|
||||||
tags:
|
tags:
|
||||||
- 'v*'
|
- 'v*'
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
tests:
|
e2e:
|
||||||
runs-on: ubuntu-26.04
|
runs-on: ubuntu-20.04
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
target:
|
target:
|
||||||
- local
|
- non-experimental
|
||||||
|
- experimental
|
||||||
- connhelper-ssh
|
- connhelper-ssh
|
||||||
base:
|
base:
|
||||||
- alpine
|
- alpine
|
||||||
- debian
|
- bullseye
|
||||||
engine-version:
|
engine-version:
|
||||||
- rc # latest rc
|
# - 20.10-dind # FIXME: Fails on 20.10
|
||||||
- 29 # latest
|
- stable-dind # TODO: Use 20.10-dind, stable-dind is deprecated
|
||||||
- 28 # latest - 1
|
include:
|
||||||
- 25 # mirantis lts
|
- target: non-experimental
|
||||||
|
engine-version: 19.03-dind
|
||||||
steps:
|
steps:
|
||||||
- name: Configure credentials
|
|
||||||
continue-on-error: true
|
|
||||||
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
|
|
||||||
with:
|
|
||||||
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
|
|
||||||
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
|
|
||||||
profile: administrator
|
|
||||||
profile-region: us-east-1
|
|
||||||
-
|
-
|
||||||
name: Checkout
|
name: Checkout
|
||||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
uses: actions/checkout@v3
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
-
|
-
|
||||||
name: Update daemon.json
|
name: Update daemon.json
|
||||||
run: |
|
run: |
|
||||||
if [ ! -f /etc/docker/daemon.json ]; then
|
sudo jq '.experimental = true' < /etc/docker/daemon.json > /tmp/docker.json
|
||||||
# ubuntu runners no longer have a default daemon.json present
|
sudo mv /tmp/docker.json /etc/docker/daemon.json
|
||||||
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 cat /etc/docker/daemon.json
|
||||||
sudo service docker restart
|
sudo service docker restart
|
||||||
docker version
|
docker version
|
||||||
docker info
|
docker info
|
||||||
-
|
-
|
||||||
name: Set up Docker Buildx
|
name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
|
uses: docker/setup-buildx-action@v2
|
||||||
-
|
-
|
||||||
name: Run ${{ matrix.target }}
|
name: Run ${{ matrix.target }}
|
||||||
run: |
|
run: |
|
||||||
make -f docker.Makefile test-e2e-${{ matrix.target }}
|
make -f docker.Makefile test-e2e-${{ matrix.target }}
|
||||||
env:
|
env:
|
||||||
BASE_VARIANT: ${{ matrix.base }}
|
BASE_VARIANT: ${{ matrix.base }}
|
||||||
ENGINE_VERSION: ${{ matrix.engine-version }}
|
E2E_ENGINE_VERSION: ${{ matrix.engine-version }}
|
||||||
TESTFLAGS: -coverprofile=/tmp/coverage/coverage.txt
|
TESTFLAGS: -coverprofile=/tmp/coverage/coverage.txt
|
||||||
-
|
-
|
||||||
name: Send to Codecov
|
name: Send to Codecov
|
||||||
uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
|
uses: codecov/codecov-action@v3
|
||||||
with:
|
with:
|
||||||
files: ./build/coverage/coverage.txt
|
file: ./build/coverage/coverage.txt
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
name: PR Review - Trigger
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
types: [ready_for_review, opened, review_requested]
|
|
||||||
pull_request_review_comment:
|
|
||||||
types: [created]
|
|
||||||
|
|
||||||
permissions: {}
|
|
||||||
|
|
||||||
# Deduplicate simultaneous pull_request events for the same fork PR.
|
|
||||||
# When reviewers are requested at the same time, GitHub fires multiple
|
|
||||||
# review_requested events. Without this group each event triggers a
|
|
||||||
# separate review via workflow_run, producing duplicate reviews.
|
|
||||||
concurrency:
|
|
||||||
group: pr-review-trigger-${{ github.event.pull_request.number }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
save-context:
|
|
||||||
if: github.event.pull_request.head.repo.fork
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Configure credentials
|
|
||||||
continue-on-error: true
|
|
||||||
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
|
|
||||||
with:
|
|
||||||
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
|
|
||||||
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
|
|
||||||
profile: administrator
|
|
||||||
profile-region: us-east-1
|
|
||||||
- name: Save event context
|
|
||||||
env:
|
|
||||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
|
||||||
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
|
||||||
COMMENT_JSON: ${{ toJSON(github.event.comment) }}
|
|
||||||
run: |
|
|
||||||
mkdir -p context
|
|
||||||
printf '%s' "${{ github.event_name }}" > context/event_name.txt
|
|
||||||
printf '%s' "$PR_NUMBER" > context/pr_number.txt
|
|
||||||
printf '%s' "$PR_HEAD_SHA" > context/pr_head_sha.txt
|
|
||||||
if [ "${{ github.event_name }}" = "pull_request_review_comment" ]; then
|
|
||||||
printf '%s' "$COMMENT_JSON" > context/comment.json
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Upload context
|
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
||||||
with:
|
|
||||||
name: pr-review-context
|
|
||||||
path: context/
|
|
||||||
retention-days: 1
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
name: PR Review
|
|
||||||
on:
|
|
||||||
issue_comment:
|
|
||||||
types: [created]
|
|
||||||
workflow_run:
|
|
||||||
workflows: ["PR Review - Trigger"]
|
|
||||||
types: [completed]
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
review:
|
|
||||||
uses: docker/docker-agent-action/.github/workflows/review-pr.yml@3654ec391b070e247de2e2e3c1ff2519b6378b3a # v2.0.9
|
|
||||||
permissions:
|
|
||||||
contents: read # Read repository files and PR diffs
|
|
||||||
pull-requests: write # Post review comments
|
|
||||||
issues: write # Create security incident issues if secrets detected
|
|
||||||
checks: write # (Optional) Show review progress as a check run
|
|
||||||
id-token: write # Required for OIDC authentication to AWS Secrets Manager
|
|
||||||
actions: write # Required since v2.0.3 — review-lock cache cleanup and feedback artifacts
|
|
||||||
with:
|
|
||||||
trigger-run-id: ${{ github.event_name == 'workflow_run' && format('{0}', github.event.workflow_run.id) || '' }}
|
|
||||||
@@ -1,157 +0,0 @@
|
|||||||
name: Sync Docker release branch
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ inputs.release_branch }}
|
|
||||||
cancel-in-progress: false
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
release_branch:
|
|
||||||
description: Release branch to sync, for example 29.x
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
tag:
|
|
||||||
description: Tag to sync from, for example v29.6.0
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
dry_run:
|
|
||||||
description: Merge but don't push
|
|
||||||
required: true
|
|
||||||
default: false
|
|
||||||
type: boolean
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
sync-release-branch:
|
|
||||||
runs-on: ubuntu-26.04
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
actions: write
|
|
||||||
outputs:
|
|
||||||
base_sha: ${{ steps.sync.outputs.base_sha }}
|
|
||||||
has_changes: ${{ steps.sync.outputs.has_changes }}
|
|
||||||
temporary_branch: ${{ steps.sync.outputs.temporary_branch }}
|
|
||||||
temporary_sha: ${{ steps.sync.outputs.temporary_sha }}
|
|
||||||
timeout-minutes: 10
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Validate
|
|
||||||
env:
|
|
||||||
BRANCH: ${{ github.ref_name }}
|
|
||||||
RELEASE_BRANCH: ${{ inputs.release_branch }}
|
|
||||||
run: |
|
|
||||||
if [ "$BRANCH" != "master" ]; then
|
|
||||||
echo "::error::This workflow is expected to be run on master, not $BRANCH"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [ "$RELEASE_BRANCH" = "master" ]; then
|
|
||||||
echo "::error::The release branch must not be master"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if ! [[ "$RELEASE_BRANCH" =~ ^[0-9]+\.[x0-9]+$ ]]; then
|
|
||||||
echo "::error::Invalid release branch name: '$RELEASE_BRANCH'. Expected format: 29.x"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Configure git author
|
|
||||||
run: |
|
|
||||||
git config user.name "github-actions[bot]"
|
|
||||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
||||||
|
|
||||||
- name: Sync release branch to tag range
|
|
||||||
id: sync
|
|
||||||
env:
|
|
||||||
DRY_RUN: ${{ inputs.dry_run }}
|
|
||||||
RELEASE_BRANCH: ${{ inputs.release_branch }}
|
|
||||||
RUN_ATTEMPT: ${{ github.run_attempt }}
|
|
||||||
RUN_ID: ${{ github.run_id }}
|
|
||||||
TAG: ${{ inputs.tag }}
|
|
||||||
run: |
|
|
||||||
set -o pipefail
|
|
||||||
base_sha=$(git rev-parse "origin/$RELEASE_BRANCH")
|
|
||||||
temporary_branch="process/sync-release-branch/$RUN_ID-$RUN_ATTEMPT"
|
|
||||||
echo "base_sha=$base_sha" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "temporary_branch=$temporary_branch" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
# Keep the master checkout unchanged so scripts run from the dispatched revision.
|
|
||||||
release_worktree="$RUNNER_TEMP/release-branch"
|
|
||||||
git worktree add --detach "$release_worktree" "$base_sha"
|
|
||||||
cd "$release_worktree"
|
|
||||||
|
|
||||||
tags_file=$(mktemp)
|
|
||||||
"$GITHUB_WORKSPACE/scripts/unmerged-tags" \
|
|
||||||
"origin/$RELEASE_BRANCH" \
|
|
||||||
"$TAG" \
|
|
||||||
> "$tags_file"
|
|
||||||
|
|
||||||
echo >> "$GITHUB_STEP_SUMMARY"
|
|
||||||
echo "## Tags to sync" >> "$GITHUB_STEP_SUMMARY"
|
|
||||||
echo >> "$GITHUB_STEP_SUMMARY"
|
|
||||||
sed 's/^/- /' "$tags_file" >> "$GITHUB_STEP_SUMMARY"
|
|
||||||
|
|
||||||
xargs -r "$GITHUB_WORKSPACE/scripts/sync-branch" < "$tags_file" | tee -a "$GITHUB_STEP_SUMMARY"
|
|
||||||
|
|
||||||
if [[ "$DRY_RUN" == "true" ]]; then
|
|
||||||
echo "has_changes=false" >> "$GITHUB_OUTPUT"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ $(git rev-parse HEAD) == $(git rev-parse "origin/$RELEASE_BRANCH") ]]; then
|
|
||||||
echo "has_changes=false" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "No changes to push"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "has_changes=true" >> "$GITHUB_OUTPUT"
|
|
||||||
git push origin "HEAD:refs/heads/$temporary_branch"
|
|
||||||
echo "temporary_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
push-release-branch:
|
|
||||||
needs: sync-release-branch
|
|
||||||
if: ${{ !inputs.dry_run && needs.sync-release-branch.outputs.has_changes == 'true' }}
|
|
||||||
runs-on: ubuntu-26.04
|
|
||||||
environment: docker-releases
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
actions: write
|
|
||||||
timeout-minutes: 10
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Push release branch
|
|
||||||
env:
|
|
||||||
BASE_SHA: ${{ needs.sync-release-branch.outputs.base_sha }}
|
|
||||||
RELEASE_BRANCH: ${{ inputs.release_branch }}
|
|
||||||
TEMPORARY_BRANCH: ${{ needs.sync-release-branch.outputs.temporary_branch }}
|
|
||||||
TEMPORARY_SHA: ${{ needs.sync-release-branch.outputs.temporary_sha }}
|
|
||||||
run: |
|
|
||||||
git fetch origin "$RELEASE_BRANCH"
|
|
||||||
current_sha=$(git rev-parse "origin/$RELEASE_BRANCH")
|
|
||||||
if [[ "$current_sha" != "$BASE_SHA" ]]; then
|
|
||||||
echo "$RELEASE_BRANCH changed from $BASE_SHA to $current_sha"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
git fetch origin "$TEMPORARY_BRANCH"
|
|
||||||
current_temporary_sha=$(git rev-parse FETCH_HEAD)
|
|
||||||
if [[ "$current_temporary_sha" != "$TEMPORARY_SHA" ]]; then
|
|
||||||
echo "$TEMPORARY_BRANCH changed from $TEMPORARY_SHA to $current_temporary_sha"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
git push origin "FETCH_HEAD:$RELEASE_BRANCH"
|
|
||||||
|
|
||||||
- name: Delete temporary branch
|
|
||||||
env:
|
|
||||||
TEMPORARY_BRANCH: ${{ needs.sync-release-branch.outputs.temporary_branch }}
|
|
||||||
run: git push origin --delete "$TEMPORARY_BRANCH"
|
|
||||||
+23
-47
@@ -1,14 +1,5 @@
|
|||||||
name: test
|
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:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
@@ -19,37 +10,30 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- 'master'
|
- 'master'
|
||||||
- '[0-9]+.[0-9]+'
|
- '[0-9]+.[0-9]+'
|
||||||
- '[0-9]+.x'
|
|
||||||
tags:
|
tags:
|
||||||
- 'v*'
|
- 'v*'
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
ctn:
|
ctn:
|
||||||
runs-on: ubuntu-26.04
|
runs-on: ubuntu-20.04
|
||||||
steps:
|
steps:
|
||||||
- name: Configure credentials
|
-
|
||||||
continue-on-error: true
|
name: Checkout
|
||||||
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
|
uses: actions/checkout@v3
|
||||||
with:
|
|
||||||
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
|
|
||||||
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
|
|
||||||
profile: administrator
|
|
||||||
profile-region: us-east-1
|
|
||||||
-
|
-
|
||||||
name: Set up Docker Buildx
|
name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
|
uses: docker/setup-buildx-action@v2
|
||||||
-
|
-
|
||||||
name: Test
|
name: Test
|
||||||
uses: docker/bake-action@018cb6412ab401ebaa809aa5f85966b74628600f # v7.4.0
|
uses: docker/bake-action@v3
|
||||||
with:
|
with:
|
||||||
targets: test-coverage
|
targets: test-coverage
|
||||||
-
|
-
|
||||||
name: Send to Codecov
|
name: Send to Codecov
|
||||||
uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
|
uses: codecov/codecov-action@v3
|
||||||
with:
|
with:
|
||||||
files: ./build/coverage/coverage.txt
|
file: ./build/coverage/coverage.txt
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
|
||||||
|
|
||||||
host:
|
host:
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
@@ -61,45 +45,37 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
os:
|
os:
|
||||||
- macos-14 # macOS 14 on arm64 (Apple Silicon M1)
|
- macos-13 # macOS 13 on Intel
|
||||||
- macos-15-intel # macOS 15 on Intel
|
- macos-14 # macOS 14 on arm64 (Apple Silicon M1)
|
||||||
- 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
|
# - 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:
|
steps:
|
||||||
- name: Configure credentials
|
-
|
||||||
continue-on-error: true
|
name: Prepare git
|
||||||
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
|
if: matrix.os == 'windows-latest'
|
||||||
with:
|
run: |
|
||||||
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
|
git config --system core.autocrlf false
|
||||||
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
|
git config --system core.eol lf
|
||||||
profile: administrator
|
|
||||||
profile-region: us-east-1
|
|
||||||
-
|
-
|
||||||
name: Checkout
|
name: Checkout
|
||||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
path: ${{ env.GOPATH }}/src/github.com/docker/cli
|
path: ${{ env.GOPATH }}/src/github.com/docker/cli
|
||||||
persist-credentials: false
|
|
||||||
-
|
-
|
||||||
name: Set up Go
|
name: Set up Go
|
||||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
uses: actions/setup-go@v3
|
||||||
with:
|
with:
|
||||||
go-version: "1.26.8"
|
go-version: 1.22.6
|
||||||
cache: false
|
|
||||||
-
|
-
|
||||||
name: Test
|
name: Test
|
||||||
run: |
|
run: |
|
||||||
# run in go modules mode to prevent traversing to nested modules
|
go test -coverprofile=/tmp/coverage.txt $(go list ./... | grep -vE '/vendor/|/e2e/')
|
||||||
ln -s vendor.mod go.mod
|
|
||||||
ln -s vendor.sum go.sum
|
|
||||||
go test -coverprofile=/tmp/coverage.txt $(go list ./... | grep -vE '^github.com/docker/cli/e2e/')
|
|
||||||
go tool cover -func=/tmp/coverage.txt
|
go tool cover -func=/tmp/coverage.txt
|
||||||
working-directory: ${{ env.GOPATH }}/src/github.com/docker/cli
|
working-directory: ${{ env.GOPATH }}/src/github.com/docker/cli
|
||||||
shell: bash
|
shell: bash
|
||||||
-
|
-
|
||||||
name: Send to Codecov
|
name: Send to Codecov
|
||||||
uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
|
uses: codecov/codecov-action@v3
|
||||||
with:
|
with:
|
||||||
files: /tmp/coverage.txt
|
file: /tmp/coverage.txt
|
||||||
working-directory: ${{ env.GOPATH }}/src/github.com/docker/cli
|
working-directory: ${{ env.GOPATH }}/src/github.com/docker/cli
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
version: v0.7.3 # Work around https://github.com/codecov/uploader/issues/1687 -- uploader v0.8.0 for mac is built for arm64, not x86_64
|
||||||
|
|||||||
@@ -1,66 +0,0 @@
|
|||||||
name: validate-milestone
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
pull-requests: read
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
types: [opened, synchronize, milestoned, demilestoned, edited]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
validate-milestone:
|
|
||||||
runs-on: ubuntu-26.04
|
|
||||||
timeout-minutes: 5
|
|
||||||
steps:
|
|
||||||
- name: Configure credentials
|
|
||||||
continue-on-error: true
|
|
||||||
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
|
|
||||||
with:
|
|
||||||
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
|
|
||||||
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
|
|
||||||
profile: administrator
|
|
||||||
profile-region: us-east-1
|
|
||||||
- name: Validate milestone matches VERSION
|
|
||||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
|
||||||
env:
|
|
||||||
MILESTONE: ${{ github.event.pull_request.milestone.title }}
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
const files = await github.paginate(github.rest.pulls.listFiles, {
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
pull_number: context.payload.pull_request.number,
|
|
||||||
});
|
|
||||||
core.info(`Modified files: ${files.map(f => f.filename).join(', ')}`);
|
|
||||||
|
|
||||||
const touchesVersion = files.some(f => f.filename === 'VERSION');
|
|
||||||
core.info(`Touches VERSION: ${touchesVersion}`);
|
|
||||||
|
|
||||||
// Use the PR's version when it bumps the file, base branch otherwise.
|
|
||||||
// It's fine to trust the author in this case, it's not meant to be
|
|
||||||
// a security gate, just a helpful check for maintainers.
|
|
||||||
const ref = touchesVersion
|
|
||||||
? context.payload.pull_request.head.sha
|
|
||||||
: context.payload.pull_request.base.ref;
|
|
||||||
|
|
||||||
core.info(`Base ref: ${ref}`);
|
|
||||||
|
|
||||||
const resp = await github.rest.repos.getContent({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
path: 'VERSION',
|
|
||||||
ref,
|
|
||||||
});
|
|
||||||
const expected = Buffer.from(resp.data.content, resp.data.encoding).toString('utf8').trim();
|
|
||||||
const milestone = process.env.MILESTONE;
|
|
||||||
|
|
||||||
if (!milestone) {
|
|
||||||
core.setFailed(`PR must have a milestone set (expected: ${expected})`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (milestone !== expected) {
|
|
||||||
core.setFailed(`Milestone '${milestone}' does not match VERSION '${expected}'`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
core.info(`Milestone: ${milestone} ✓`);
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
name: validate-pr
|
|
||||||
|
|
||||||
# Default to 'contents: read', which grants actions to read commits.
|
|
||||||
#
|
|
||||||
# If any permission is set, any permission not included in the list is
|
|
||||||
# implicitly set to "none".
|
|
||||||
#
|
|
||||||
# see https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
types: [opened, edited, labeled, unlabeled, synchronize]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
check-labels:
|
|
||||||
runs-on: ubuntu-26.04
|
|
||||||
timeout-minutes: 120 # guardrails timeout for the whole job
|
|
||||||
steps:
|
|
||||||
- name: Configure credentials
|
|
||||||
continue-on-error: true
|
|
||||||
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
|
|
||||||
with:
|
|
||||||
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
|
|
||||||
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
|
|
||||||
profile: administrator
|
|
||||||
profile-region: us-east-1
|
|
||||||
- name: Missing `area/` label
|
|
||||||
if: always() && contains(join(github.event.pull_request.labels.*.name, ','), 'impact/') && !contains(join(github.event.pull_request.labels.*.name, ','), 'area/')
|
|
||||||
run: |
|
|
||||||
echo "::error::Every PR with an 'impact/*' label should also have an 'area/*' label"
|
|
||||||
exit 1
|
|
||||||
- name: Missing `kind/` label
|
|
||||||
if: always() && contains(join(github.event.pull_request.labels.*.name, ','), 'impact/') && !contains(join(github.event.pull_request.labels.*.name, ','), 'kind/')
|
|
||||||
run: |
|
|
||||||
echo "::error::Every PR with an 'impact/*' label should also have a 'kind/*' label"
|
|
||||||
exit 1
|
|
||||||
- name: OK
|
|
||||||
run: exit 0
|
|
||||||
|
|
||||||
check-changelog:
|
|
||||||
runs-on: ubuntu-26.04
|
|
||||||
timeout-minutes: 120 # guardrails timeout for the whole job
|
|
||||||
env:
|
|
||||||
HAS_IMPACT_LABEL: ${{ contains(join(github.event.pull_request.labels.*.name, ','), 'impact/') }}
|
|
||||||
PR_BODY: |
|
|
||||||
${{ github.event.pull_request.body }}
|
|
||||||
steps:
|
|
||||||
- name: Configure credentials
|
|
||||||
continue-on-error: true
|
|
||||||
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
|
|
||||||
with:
|
|
||||||
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
|
|
||||||
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
|
|
||||||
profile: administrator
|
|
||||||
profile-region: us-east-1
|
|
||||||
- name: Check changelog description
|
|
||||||
run: |
|
|
||||||
# Extract the `markdown changelog` note code block
|
|
||||||
block=$(echo -n "$PR_BODY" | tr -d '\r' | awk '/^```markdown changelog$/{flag=1;next}/^```$/{flag=0}flag')
|
|
||||||
|
|
||||||
# Strip empty lines
|
|
||||||
desc=$(echo "$block" | awk NF)
|
|
||||||
|
|
||||||
if [ "$HAS_IMPACT_LABEL" = "true" ]; then
|
|
||||||
if [ -z "$desc" ]; then
|
|
||||||
echo "::error::Changelog section is empty. Please provide a description for the changelog."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
len=$(echo -n "$desc" | wc -c)
|
|
||||||
if [[ $len -le 6 ]]; then
|
|
||||||
echo "::error::Description looks too short: $desc"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
if [ -n "$desc" ]; then
|
|
||||||
echo "::error::PR has a changelog description, but no changelog label"
|
|
||||||
echo "::error::Please add the relevant 'impact/' label to the PR or remove the changelog description"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "This PR will be included in the release notes with the following note:"
|
|
||||||
echo "$desc"
|
|
||||||
|
|
||||||
check-pr-branch:
|
|
||||||
runs-on: ubuntu-26.04
|
|
||||||
timeout-minutes: 120 # guardrails timeout for the whole job
|
|
||||||
env:
|
|
||||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
|
||||||
steps:
|
|
||||||
- name: Configure credentials
|
|
||||||
continue-on-error: true
|
|
||||||
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
|
|
||||||
with:
|
|
||||||
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
|
|
||||||
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
|
|
||||||
profile: administrator
|
|
||||||
profile-region: us-east-1
|
|
||||||
# Backports or PR that target a release branch directly should mention the target branch in the title, for example:
|
|
||||||
# [X.Y backport] Some change that needs backporting to X.Y
|
|
||||||
# [X.Y] Change directly targeting the X.Y branch
|
|
||||||
- name: Check release branch
|
|
||||||
id: title_branch
|
|
||||||
run: |
|
|
||||||
# get the intended major version prefix ("[27.1 backport]" -> "27.") from the PR title.
|
|
||||||
[[ "$PR_TITLE" =~ ^\[([0-9]*\.)[^]]*\] ]] && branch="${BASH_REMATCH[1]}"
|
|
||||||
|
|
||||||
# get major version prefix from the release branch ("27.x -> "27.")
|
|
||||||
[[ "$GITHUB_BASE_REF" =~ ^([0-9]*\.) ]] && target_branch="${BASH_REMATCH[1]}" || target_branch="$GITHUB_BASE_REF"
|
|
||||||
|
|
||||||
if [[ "$target_branch" != "$branch" ]] && ! [[ "$GITHUB_BASE_REF" == "master" && "$branch" == "" ]]; then
|
|
||||||
echo "::error::PR is opened against the $GITHUB_BASE_REF branch, but its title suggests otherwise."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
@@ -1,14 +1,5 @@
|
|||||||
name: validate
|
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:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
@@ -19,14 +10,13 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- 'master'
|
- 'master'
|
||||||
- '[0-9]+.[0-9]+'
|
- '[0-9]+.[0-9]+'
|
||||||
- '[0-9]+.x'
|
|
||||||
tags:
|
tags:
|
||||||
- 'v*'
|
- 'v*'
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
validate:
|
validate:
|
||||||
runs-on: ubuntu-26.04
|
runs-on: ubuntu-20.04
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -36,37 +26,22 @@ jobs:
|
|||||||
- validate-vendor
|
- validate-vendor
|
||||||
- update-authors # ensure authors update target runs fine
|
- update-authors # ensure authors update target runs fine
|
||||||
steps:
|
steps:
|
||||||
- name: Configure credentials
|
-
|
||||||
continue-on-error: true
|
name: Checkout
|
||||||
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
|
uses: actions/checkout@v3
|
||||||
with:
|
|
||||||
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
|
|
||||||
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
|
|
||||||
profile: administrator
|
|
||||||
profile-region: us-east-1
|
|
||||||
-
|
-
|
||||||
name: Run
|
name: Run
|
||||||
uses: docker/bake-action@018cb6412ab401ebaa809aa5f85966b74628600f # v7.4.0
|
uses: docker/bake-action@v3
|
||||||
with:
|
with:
|
||||||
targets: ${{ matrix.target }}
|
targets: ${{ matrix.target }}
|
||||||
|
|
||||||
# check that the generated Markdown and the checked-in files match
|
# check that the generated Markdown and the checked-in files match
|
||||||
validate-md:
|
validate-md:
|
||||||
runs-on: ubuntu-26.04
|
runs-on: ubuntu-20.04
|
||||||
steps:
|
steps:
|
||||||
- name: Configure credentials
|
|
||||||
continue-on-error: true
|
|
||||||
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
|
|
||||||
with:
|
|
||||||
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
|
|
||||||
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
|
|
||||||
profile: administrator
|
|
||||||
profile-region: us-east-1
|
|
||||||
-
|
-
|
||||||
name: Checkout
|
name: Checkout
|
||||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
uses: actions/checkout@v3
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
-
|
-
|
||||||
name: Generate
|
name: Generate
|
||||||
shell: 'script --return --quiet --command "bash {0}"'
|
shell: 'script --return --quiet --command "bash {0}"'
|
||||||
@@ -82,7 +57,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
validate-make:
|
validate-make:
|
||||||
runs-on: ubuntu-26.04
|
runs-on: ubuntu-20.04
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -90,54 +65,11 @@ jobs:
|
|||||||
- yamldocs # ensure yamldocs target runs fine
|
- yamldocs # ensure yamldocs target runs fine
|
||||||
- manpages # ensure manpages target runs fine
|
- manpages # ensure manpages target runs fine
|
||||||
steps:
|
steps:
|
||||||
- name: Configure credentials
|
|
||||||
continue-on-error: true
|
|
||||||
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
|
|
||||||
with:
|
|
||||||
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
|
|
||||||
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
|
|
||||||
profile: administrator
|
|
||||||
profile-region: us-east-1
|
|
||||||
-
|
-
|
||||||
name: Checkout
|
name: Checkout
|
||||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
uses: actions/checkout@v3
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
-
|
-
|
||||||
name: Run
|
name: Run
|
||||||
shell: 'script --return --quiet --command "bash {0}"'
|
shell: 'script --return --quiet --command "bash {0}"'
|
||||||
run: |
|
run: |
|
||||||
make -f docker.Makefile ${{ matrix.target }}
|
make -f docker.Makefile ${{ matrix.target }}
|
||||||
|
|
||||||
validate-gocompat:
|
|
||||||
runs-on: ubuntu-26.04
|
|
||||||
env:
|
|
||||||
GOPATH: ${{ github.workspace }}
|
|
||||||
GO111MODULE: off
|
|
||||||
steps:
|
|
||||||
- name: Configure credentials
|
|
||||||
continue-on-error: true
|
|
||||||
uses: tracebit-com/tracebit-community-action@27de2ed8de16af5270d32003a4c8bfc3c54e0a20 # v1.2.0
|
|
||||||
with:
|
|
||||||
customer-id: ${{ secrets.TRACEBIT_CUSTOMER_ID }}
|
|
||||||
api-token: ${{ secrets.TRACEBIT_API_TOKEN }}
|
|
||||||
profile: administrator
|
|
||||||
profile-region: us-east-1
|
|
||||||
-
|
|
||||||
name: Checkout
|
|
||||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
||||||
with:
|
|
||||||
path: src/github.com/docker/cli
|
|
||||||
persist-credentials: false
|
|
||||||
-
|
|
||||||
name: Set up Go
|
|
||||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
|
||||||
with:
|
|
||||||
go-version: "1.26.8"
|
|
||||||
cache: false
|
|
||||||
-
|
|
||||||
name: Run gocompat check
|
|
||||||
shell: 'script --return --quiet --command "bash {0}"'
|
|
||||||
working-directory: ${{ github.workspace }}/src/github.com/docker/cli
|
|
||||||
run: |
|
|
||||||
make -C ./internal/gocompat verify
|
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
name: zizmor
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- 'main'
|
|
||||||
- 'master'
|
|
||||||
- '[0-9]+.[0-9]+'
|
|
||||||
- '[0-9]+.x'
|
|
||||||
tags:
|
|
||||||
- 'v*'
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
run:
|
|
||||||
uses: crazy-max/.github/.github/workflows/zizmor.yml@f91220cba053bd54299ba645a599ee8ed436f222 # v1.11.0
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
security-events: write
|
|
||||||
with:
|
|
||||||
min-severity: medium
|
|
||||||
min-confidence: medium
|
|
||||||
persona: pedantic
|
|
||||||
+3
-3
@@ -1,5 +1,5 @@
|
|||||||
# if you want to ignore files created by your editor/tools,
|
# if you want to ignore files created by your editor/tools,
|
||||||
# consider a global .gitignore https://help.github.com/articles/ignoring-files
|
# please consider a global .gitignore https://help.github.com/articles/ignoring-files
|
||||||
*.exe
|
*.exe
|
||||||
*.exe~
|
*.exe~
|
||||||
*.orig
|
*.orig
|
||||||
@@ -8,8 +8,8 @@
|
|||||||
Thumbs.db
|
Thumbs.db
|
||||||
.editorconfig
|
.editorconfig
|
||||||
/build/
|
/build/
|
||||||
/cmd/docker/winresources/versioninfo.json
|
/cli/winresources/versioninfo.json
|
||||||
/cmd/docker/winresources/*.syso
|
/cli/winresources/*.syso
|
||||||
profile.out
|
profile.out
|
||||||
|
|
||||||
# top-level go.mod is not meant to be checked in
|
# top-level go.mod is not meant to be checked in
|
||||||
|
|||||||
+106
-206
@@ -1,237 +1,137 @@
|
|||||||
version: "2"
|
|
||||||
|
|
||||||
run:
|
|
||||||
# prevent golangci-lint from deducting the go version to lint for through go.mod,
|
|
||||||
# which causes it to fallback to go1.17 semantics.
|
|
||||||
#
|
|
||||||
# TODO(thaJeztah): update "usetesting" settings to enable go1.24 features once our minimum version is go1.24
|
|
||||||
go: "1.26.8"
|
|
||||||
|
|
||||||
timeout: 5m
|
|
||||||
|
|
||||||
issues:
|
|
||||||
# Maximum issues count per one linter. Set to 0 to disable. Default is 50.
|
|
||||||
max-issues-per-linter: 0
|
|
||||||
|
|
||||||
# Maximum count of issues with the same text. Set to 0 to disable. Default is 3.
|
|
||||||
max-same-issues: 0
|
|
||||||
|
|
||||||
formatters:
|
|
||||||
enable:
|
|
||||||
- gofumpt # Detects whether code was gofumpt-ed.
|
|
||||||
- goimports
|
|
||||||
|
|
||||||
exclusions:
|
|
||||||
generated: strict
|
|
||||||
|
|
||||||
linters:
|
linters:
|
||||||
enable:
|
enable:
|
||||||
- asasalint # Detects "[]any" used as argument for variadic "func(...any)".
|
|
||||||
- bodyclose
|
- bodyclose
|
||||||
- copyloopvar # Detects places where loop variables are copied.
|
|
||||||
- depguard
|
- depguard
|
||||||
- dogsled # Detects assignments with too many blank identifiers.
|
- dogsled
|
||||||
- dupword # Detects duplicate words.
|
|
||||||
- durationcheck # Detect cases where two time.Duration values are being multiplied in possibly erroneous ways.
|
|
||||||
- errcheck
|
|
||||||
- errchkjson # Detects unsupported types passed to json encoding functions and reports if checks for the returned error can be omitted.
|
|
||||||
- exhaustive # Detects missing options in enum switch statements.
|
|
||||||
- exptostd # Detects functions from golang.org/x/exp/ that can be replaced by std functions.
|
|
||||||
- fatcontext # Detects nested contexts in loops and function literals.
|
|
||||||
- forbidigo
|
|
||||||
- gocheckcompilerdirectives # Detects invalid go compiler directive comments (//go:).
|
|
||||||
- gocritic # Metalinter; detects bugs, performance, and styling issues.
|
|
||||||
- gocyclo
|
- gocyclo
|
||||||
- gosec # Detects security problems.
|
- gofumpt
|
||||||
|
- goimports
|
||||||
|
- gosec
|
||||||
|
- gosimple
|
||||||
- govet
|
- govet
|
||||||
- iface # Detects incorrect use of interfaces. Currently only used for "identical" interfaces in the same package.
|
|
||||||
- importas # Enforces consistent import aliases.
|
|
||||||
- ineffassign
|
- ineffassign
|
||||||
- makezero # Finds slice declarations with non-zero initial length.
|
- lll
|
||||||
- mirror # Detects wrong mirror patterns of bytes/strings usage.
|
- megacheck
|
||||||
- misspell # Detects commonly misspelled English words in comments.
|
- misspell
|
||||||
- nakedret # Detects uses of naked returns.
|
- nakedret
|
||||||
- nilnesserr # Detects returning nil errors. It combines the features of nilness and nilerr,
|
- revive
|
||||||
- nosprintfhostport # Detects misuse of Sprintf to construct a host with port in a URL.
|
|
||||||
- nolintlint # Detects ill-formed or insufficient nolint directives.
|
|
||||||
- perfsprint # Detects fmt.Sprintf uses that can be replaced with a faster alternative.
|
|
||||||
- prealloc # Detects slice declarations that could potentially be pre-allocated.
|
|
||||||
- predeclared # Detects code that shadows one of Go's predeclared identifiers
|
|
||||||
- reassign # Detects reassigning a top-level variable in another package.
|
|
||||||
- revive # Metalinter; drop-in replacement for golint.
|
|
||||||
- spancheck # Detects mistakes with OpenTelemetry/Census spans.
|
|
||||||
- staticcheck
|
- staticcheck
|
||||||
- thelper # Detects test helpers without t.Helper().
|
- typecheck
|
||||||
- tparallel # Detects inappropriate usage of t.Parallel().
|
- unconvert
|
||||||
- unconvert # Detects unnecessary type conversions.
|
|
||||||
- unparam
|
- unparam
|
||||||
- unused
|
- unused
|
||||||
- usestdlibvars # Detects the possibility to use variables/constants from the Go standard library.
|
|
||||||
- usetesting # Reports uses of functions with replacement inside the testing package.
|
|
||||||
- wastedassign # Detects wasted assignment statements.
|
|
||||||
|
|
||||||
disable:
|
disable:
|
||||||
- errcheck
|
- errcheck
|
||||||
|
|
||||||
settings:
|
run:
|
||||||
depguard:
|
timeout: 5m
|
||||||
rules:
|
|
||||||
main:
|
|
||||||
deny:
|
|
||||||
- pkg: "github.com/containerd/containerd/errdefs"
|
|
||||||
desc: The containerd errdefs package was migrated to a separate module. Use github.com/containerd/errdefs instead.
|
|
||||||
- pkg: "github.com/containerd/containerd/log"
|
|
||||||
desc: The containerd log package was migrated to a separate module. Use github.com/containerd/log instead.
|
|
||||||
- pkg: "github.com/containerd/containerd/pkg/userns"
|
|
||||||
desc: Use github.com/moby/sys/userns instead.
|
|
||||||
- pkg: "github.com/containerd/containerd/platforms"
|
|
||||||
desc: The containerd platforms package was migrated to a separate module. Use github.com/containerd/platforms instead.
|
|
||||||
- pkg: "github.com/docker/docker/errdefs"
|
|
||||||
desc: Use github.com/containerd/errdefs instead.
|
|
||||||
- pkg: "github.com/docker/docker/pkg/system"
|
|
||||||
desc: This package should not be used unless strictly necessary.
|
|
||||||
- pkg: "github.com/docker/distribution/uuid"
|
|
||||||
desc: Use github.com/google/uuid instead.
|
|
||||||
- pkg: "io/ioutil"
|
|
||||||
desc: The io/ioutil package has been deprecated, see https://go.dev/doc/go1.16#ioutil
|
|
||||||
- pkg: "gopkg.in/yaml.v3"
|
|
||||||
desc: Use go.yaml.in/yaml/v3 instead.
|
|
||||||
|
|
||||||
forbidigo:
|
linters-settings:
|
||||||
forbid:
|
depguard:
|
||||||
- pkg: ^regexp$
|
rules:
|
||||||
pattern: ^regexp\.MustCompile
|
main:
|
||||||
msg: Use internal/lazyregexp.New instead.
|
deny:
|
||||||
|
- pkg: io/ioutil
|
||||||
|
desc: The io/ioutil package has been deprecated, see https://go.dev/doc/go1.16#ioutil
|
||||||
|
gocyclo:
|
||||||
|
min-complexity: 16
|
||||||
|
lll:
|
||||||
|
line-length: 200
|
||||||
|
nakedret:
|
||||||
|
command: nakedret
|
||||||
|
pattern: ^(?P<path>.*?\\.go):(?P<line>\\d+)\\s*(?P<message>.*)$
|
||||||
|
revive:
|
||||||
|
rules:
|
||||||
|
- name: unused-parameter
|
||||||
|
severity: warning
|
||||||
|
disabled: true
|
||||||
|
|
||||||
gocyclo:
|
issues:
|
||||||
min-complexity: 16
|
# The default exclusion rules are a bit too permissive, so copying the relevant ones below
|
||||||
|
exclude-use-default: false
|
||||||
|
|
||||||
gosec:
|
exclude:
|
||||||
excludes:
|
- parameter .* always receives
|
||||||
- G104 # G104: Errors unhandled; (TODO: reduce unhandled errors, or explicitly ignore)
|
|
||||||
- G115 # G115: integer overflow conversion; (TODO: verify these: https://github.com/docker/cli/issues/5584)
|
|
||||||
- G117 # G117: Exported struct field matches secret pattern (false positives for legitimate field names)
|
|
||||||
- G118 # G118: Goroutine uses context.Background/TODO while request-scoped context is available (TODO: evaluate these)
|
|
||||||
- G122 # G122: Filesystem operation in filepath.Walk/WalkDir callback uses race-prone path (TODO: evaluate these)
|
|
||||||
- G306 # G306: Expect WriteFile permissions to be 0600 or less (too restrictive; also flags "0o644" permissions)
|
|
||||||
- G307 # G307: Deferring unsafe method "*os.File" on type "Close" (also EXC0008); (TODO: evaluate these and fix where needed: G307: Deferring unsafe method "*os.File" on type "Close")
|
|
||||||
- G702 # G702: Command injection via taint analysis (TODO: evaluate these)
|
|
||||||
- G703 # G703: Path traversal via taint analysis (TODO: evaluate these)
|
|
||||||
- G704 # G704: SSRF via taint analysis (TODO: evaluate these)
|
|
||||||
- G705 # G705: XSS via taint analysis (TODO: evaluate these)
|
|
||||||
|
|
||||||
govet:
|
exclude-files:
|
||||||
enable:
|
- cli/compose/schema/bindata.go
|
||||||
- shadow
|
- .*generated.*
|
||||||
settings:
|
|
||||||
shadow:
|
|
||||||
strict: true
|
|
||||||
|
|
||||||
lll:
|
exclude-rules:
|
||||||
line-length: 200
|
# We prefer to use an "exclude-list" so that new "default" exclusions are not
|
||||||
|
|
||||||
importas:
|
|
||||||
# Do not allow unaliased imports of aliased packages.
|
|
||||||
no-unaliased: true
|
|
||||||
|
|
||||||
alias:
|
|
||||||
# Should no longer be aliased, because we no longer allow moby/docker errdefs.
|
|
||||||
- pkg: "github.com/docker/docker/errdefs"
|
|
||||||
alias: ""
|
|
||||||
- pkg: github.com/opencontainers/image-spec/specs-go/v1
|
|
||||||
alias: ocispec
|
|
||||||
# Enforce that gotest.tools/v3/assert/cmp is always aliased as "is"
|
|
||||||
- pkg: gotest.tools/v3/assert/cmp
|
|
||||||
alias: is
|
|
||||||
|
|
||||||
nakedret:
|
|
||||||
# Disallow naked returns if func has more lines of code than this setting.
|
|
||||||
# Default: 30
|
|
||||||
max-func-lines: 0
|
|
||||||
|
|
||||||
staticcheck:
|
|
||||||
checks:
|
|
||||||
- all
|
|
||||||
- -QF1008 # Omit embedded fields from selector expression; https://staticcheck.dev/docs/checks/#QF1008
|
|
||||||
|
|
||||||
revive:
|
|
||||||
rules:
|
|
||||||
- name: empty-block # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#empty-block
|
|
||||||
- name: empty-lines # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#empty-lines
|
|
||||||
- name: import-shadowing # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#import-shadowing
|
|
||||||
- name: line-length-limit # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#line-length-limit
|
|
||||||
arguments: [200]
|
|
||||||
- name: unused-receiver # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unused-receiver
|
|
||||||
- name: use-any # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#use-any
|
|
||||||
- name: use-errors-new # https://github.com/mgechev/revive/blob/HEAD/RULES_DESCRIPTIONS.md#use-errors-new
|
|
||||||
|
|
||||||
exclusions:
|
|
||||||
# We prefer to use an "linters.exclusions.rules" so that new "default" exclusions are not
|
|
||||||
# automatically inherited. We can decide whether or not to follow upstream
|
# automatically inherited. We can decide whether or not to follow upstream
|
||||||
# defaults when updating golang-ci-lint versions.
|
# defaults when updating golang-ci-lint versions.
|
||||||
# Unfortunately, this means we have to copy the whole exclusion pattern, as
|
# Unfortunately, this means we have to copy the whole exclusion pattern, as
|
||||||
# (unlike the "include" option), the "exclude" option does not take exclusion
|
# (unlike the "include" option), the "exclude" option does not take exclusion
|
||||||
# ID's.
|
# ID's.
|
||||||
#
|
#
|
||||||
# These exclusion patterns are copied from the default excludes at:
|
# These exclusion patterns are copied from the default excluses at:
|
||||||
# https://github.com/golangci/golangci-lint/blob/v1.61.0/pkg/config/issues.go#L11-L104
|
# https://github.com/golangci/golangci-lint/blob/v1.44.0/pkg/config/issues.go#L10-L104
|
||||||
#
|
|
||||||
# The default list of exclusions can be found at:
|
|
||||||
# https://golangci-lint.run/usage/false-positives/#default-exclusions
|
|
||||||
generated: strict
|
|
||||||
|
|
||||||
rules:
|
# EXC0001
|
||||||
# EXC0003
|
- text: "Error return value of .((os\\.)?std(out|err)\\..*|.*Close|.*Flush|os\\.Remove(All)?|.*print(f|ln)?|os\\.(Un)?Setenv). is not checked"
|
||||||
- text: "func name will be used as test\\.Test.* by other packages, and that stutters; consider calling this"
|
linters:
|
||||||
linters:
|
- errcheck
|
||||||
- revive
|
# 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
|
# G113 Potential uncontrolled memory consumption in Rat.SetString (CVE-2022-23772)
|
||||||
- text: "Subprocess launch(ed with variable|ing should be audited)"
|
# only affects gp < 1.16.14. and go < 1.17.7
|
||||||
linters:
|
- text: "(G113)"
|
||||||
- gosec
|
linters:
|
||||||
|
- gosec
|
||||||
|
|
||||||
# EXC0009
|
# Looks like the match in "EXC0007" above doesn't catch this one
|
||||||
- text: "(Expect directory permissions to be 0750 or less|Expect file permissions to be 0600 or less)"
|
# TODO: consider upstreaming this to golangci-lint's default exclusion rules
|
||||||
linters:
|
- text: "G204: Subprocess launched with a potential tainted input or cmd arguments"
|
||||||
- gosec
|
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
|
||||||
|
|
||||||
# EXC0010
|
# TODO: make sure all packages have a description. Currently, there's 67 packages without.
|
||||||
- text: "Potential file inclusion via variable"
|
- text: "package-comments: should have a package comment"
|
||||||
linters:
|
linters:
|
||||||
- gosec
|
- revive
|
||||||
|
# FIXME temporarily suppress these (see https://github.com/gotestyourself/gotest.tools/issues/272)
|
||||||
|
- text: "SA1019: (assert|cmp|is)\\.ErrorType is deprecated"
|
||||||
|
linters:
|
||||||
|
- staticcheck
|
||||||
|
# Exclude some linters from running on tests files.
|
||||||
|
- path: _test\.go
|
||||||
|
linters:
|
||||||
|
- errcheck
|
||||||
|
- gosec
|
||||||
|
|
||||||
# TODO: make sure all packages have a description. Currently, there's 67 packages without.
|
# Maximum issues count per one linter. Set to 0 to disable. Default is 50.
|
||||||
- text: "package-comments: should have a package comment"
|
max-issues-per-linter: 0
|
||||||
linters:
|
|
||||||
- revive
|
|
||||||
|
|
||||||
# Exclude some linters from running on tests files.
|
# Maximum count of issues with the same text. Set to 0 to disable. Default is 3.
|
||||||
- path: _test\.go
|
max-same-issues: 0
|
||||||
linters:
|
|
||||||
- errcheck
|
|
||||||
- gosec
|
|
||||||
|
|
||||||
- text: "ST1000: at least one file in a package should have a package comment"
|
|
||||||
linters:
|
|
||||||
- staticcheck
|
|
||||||
|
|
||||||
# Allow "err" and "ok" vars to shadow existing declarations, otherwise we get too many false positives.
|
|
||||||
- text: '^shadow: declaration of "(err|ok)" shadows declaration'
|
|
||||||
linters:
|
|
||||||
- govet
|
|
||||||
|
|
||||||
# Ignore for cli/command/formatter/tabwriter, which is forked from go stdlib, so we want to align with it.
|
|
||||||
- text: '^(ST1020|ST1022): comment on exported'
|
|
||||||
path: "cli/command/formatter/tabwriter"
|
|
||||||
linters:
|
|
||||||
- staticcheck
|
|
||||||
|
|
||||||
# TODO(thaJeztah): remove once https://github.com/leighmcculloch/gocheckcompilerdirectives/issues/7 is fixed.
|
|
||||||
- text: "compiler directive unrecognized: //go:fix"
|
|
||||||
linters:
|
|
||||||
- gocheckcompilerdirectives
|
|
||||||
|
|
||||||
# Log a warning if an exclusion rule is unused.
|
|
||||||
# Default: false
|
|
||||||
warn-unused: true
|
|
||||||
|
|||||||
@@ -22,11 +22,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>
|
||||||
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.akihiro@lab.ntt.co.jp>
|
||||||
Akihiro Suda <akihiro.suda.cz@hco.ntt.co.jp> <suda.kyoto@gmail.com>
|
Akihiro Suda <akihiro.suda.cz@hco.ntt.co.jp> <suda.kyoto@gmail.com>
|
||||||
Alano Terblanche <alano.terblanche@docker.com>
|
|
||||||
Alano Terblanche <alano.terblanche@docker.com> <18033717+Benehiko@users.noreply.github.com>
|
|
||||||
Albin Kerouanton <albinker@gmail.com>
|
|
||||||
Albin Kerouanton <albinker@gmail.com> <557933+akerouanton@users.noreply.github.com>
|
|
||||||
Albin Kerouanton <albinker@gmail.com> <albin@akerouanton.name>
|
|
||||||
Aleksa Sarai <asarai@suse.de>
|
Aleksa Sarai <asarai@suse.de>
|
||||||
Aleksa Sarai <asarai@suse.de> <asarai@suse.com>
|
Aleksa Sarai <asarai@suse.de> <asarai@suse.com>
|
||||||
Aleksa Sarai <asarai@suse.de> <cyphar@cyphar.com>
|
Aleksa Sarai <asarai@suse.de> <cyphar@cyphar.com>
|
||||||
@@ -34,17 +29,14 @@ Aleksandrs Fadins <aleks@s-ko.net>
|
|||||||
Alessandro Boch <aboch@tetrationanalytics.com> <aboch@docker.com>
|
Alessandro Boch <aboch@tetrationanalytics.com> <aboch@docker.com>
|
||||||
Alex Chen <alexchenunix@gmail.com> <root@localhost.localdomain>
|
Alex Chen <alexchenunix@gmail.com> <root@localhost.localdomain>
|
||||||
Alex Ellis <alexellis2@gmail.com>
|
Alex Ellis <alexellis2@gmail.com>
|
||||||
Alexander Chneerov <achneerov@gmail.com>
|
|
||||||
Alexander Larsson <alexl@redhat.com> <alexander.larsson@gmail.com>
|
Alexander Larsson <alexl@redhat.com> <alexander.larsson@gmail.com>
|
||||||
Alexander Morozov <lk4d4math@gmail.com>
|
Alexander Morozov <lk4d4math@gmail.com>
|
||||||
Alexander Morozov <lk4d4math@gmail.com> <lk4d4@docker.com>
|
Alexander Morozov <lk4d4math@gmail.com> <lk4d4@docker.com>
|
||||||
Alexandre Beslic <alexandre.beslic@gmail.com> <abronan@docker.com>
|
Alexandre Beslic <alexandre.beslic@gmail.com> <abronan@docker.com>
|
||||||
Alexandre Levavasseur <alexandre+oss@13x.fr>
|
|
||||||
Alexis Couvreur <alexiscouvreur.pro@gmail.com>
|
Alexis Couvreur <alexiscouvreur.pro@gmail.com>
|
||||||
Alicia Lauerman <alicia@eta.im> <allydevour@me.com>
|
Alicia Lauerman <alicia@eta.im> <allydevour@me.com>
|
||||||
Allen Sun <allensun.shl@alibaba-inc.com> <allen.sun@daocloud.io>
|
Allen Sun <allensun.shl@alibaba-inc.com> <allen.sun@daocloud.io>
|
||||||
Allen Sun <allensun.shl@alibaba-inc.com> <shlallen1990@gmail.com>
|
Allen Sun <allensun.shl@alibaba-inc.com> <shlallen1990@gmail.com>
|
||||||
Allie Sadler <allie.sadler@docker.com>
|
|
||||||
Andrew Weiss <andrew.weiss@docker.com> <andrew.weiss@microsoft.com>
|
Andrew Weiss <andrew.weiss@docker.com> <andrew.weiss@microsoft.com>
|
||||||
Andrew Weiss <andrew.weiss@docker.com> <andrew.weiss@outlook.com>
|
Andrew Weiss <andrew.weiss@docker.com> <andrew.weiss@outlook.com>
|
||||||
André Martins <aanm90@gmail.com> <martins@noironetworks.com>
|
André Martins <aanm90@gmail.com> <martins@noironetworks.com>
|
||||||
@@ -65,14 +57,8 @@ Arko Dasgupta <arko@tetrate.io> <arko.dasgupta@docker.com>
|
|||||||
Arko Dasgupta <arko@tetrate.io> <arkodg@users.noreply.github.com>
|
Arko Dasgupta <arko@tetrate.io> <arkodg@users.noreply.github.com>
|
||||||
Arnaud Porterie <icecrime@gmail.com>
|
Arnaud Porterie <icecrime@gmail.com>
|
||||||
Arnaud Porterie <icecrime@gmail.com> <arnaud.porterie@docker.com>
|
Arnaud Porterie <icecrime@gmail.com> <arnaud.porterie@docker.com>
|
||||||
Arthur Flageul <arthur.flageul@gmail.com>
|
|
||||||
Arthur Flageul <arthur.flageul@gmail.com> <arthur.flageul@docker.com>
|
|
||||||
Arthur Gautier <baloo@gandi.net> <superbaloo+registrations.github@superbaloo.net>
|
Arthur Gautier <baloo@gandi.net> <superbaloo+registrations.github@superbaloo.net>
|
||||||
Arthur Peka <arthur.peka@outlook.com> <arthrp@users.noreply.github.com>
|
Arthur Peka <arthur.peka@outlook.com> <arthrp@users.noreply.github.com>
|
||||||
Austin Vazquez <austin.vazquez@docker.com>
|
|
||||||
Austin Vazquez <austin.vazquez@docker.com> <55906459+austinvazquez@users.noreply.github.com>
|
|
||||||
Austin Vazquez <austin.vazquez@docker.com> <austin.vazquez.dev@gmail.com>
|
|
||||||
Austin Vazquez <austin.vazquez@docker.com> <macedonv@amazon.com>
|
|
||||||
Avi Miller <avi.miller@oracle.com> <avi.miller@gmail.com>
|
Avi Miller <avi.miller@oracle.com> <avi.miller@gmail.com>
|
||||||
Ben Bonnefoy <frenchben@docker.com>
|
Ben Bonnefoy <frenchben@docker.com>
|
||||||
Ben Golub <ben.golub@dotcloud.com>
|
Ben Golub <ben.golub@dotcloud.com>
|
||||||
@@ -86,9 +72,6 @@ Bill Wang <ozbillwang@gmail.com> <SydOps@users.noreply.github.com>
|
|||||||
Bin Liu <liubin0329@gmail.com>
|
Bin Liu <liubin0329@gmail.com>
|
||||||
Bin Liu <liubin0329@gmail.com> <liubin0329@users.noreply.github.com>
|
Bin Liu <liubin0329@gmail.com> <liubin0329@users.noreply.github.com>
|
||||||
Bingshen Wang <bingshen.wbs@alibaba-inc.com>
|
Bingshen Wang <bingshen.wbs@alibaba-inc.com>
|
||||||
Bjorn Neergaard <bjorn.neergaard@docker.com>
|
|
||||||
Bjorn Neergaard <bjorn.neergaard@docker.com> <bjorn@neersighted.com>
|
|
||||||
Bjorn Neergaard <bjorn.neergaard@docker.com> <bneergaard@mirantis.com>
|
|
||||||
Boaz Shuster <ripcurld.github@gmail.com>
|
Boaz Shuster <ripcurld.github@gmail.com>
|
||||||
Brad Baker <brad@brad.fi>
|
Brad Baker <brad@brad.fi>
|
||||||
Brad Baker <brad@brad.fi> <88946291+brdbkr@users.noreply.github.com>
|
Brad Baker <brad@brad.fi> <88946291+brdbkr@users.noreply.github.com>
|
||||||
@@ -98,8 +81,6 @@ Brent Salisbury <brent.salisbury@docker.com> <brent@docker.com>
|
|||||||
Brian Goff <cpuguy83@gmail.com>
|
Brian Goff <cpuguy83@gmail.com>
|
||||||
Brian Goff <cpuguy83@gmail.com> <bgoff@cpuguy83-mbp.home>
|
Brian Goff <cpuguy83@gmail.com> <bgoff@cpuguy83-mbp.home>
|
||||||
Brian Goff <cpuguy83@gmail.com> <bgoff@cpuguy83-mbp.local>
|
Brian Goff <cpuguy83@gmail.com> <bgoff@cpuguy83-mbp.local>
|
||||||
Brian Tracy <brian.tracy33@gmail.com>
|
|
||||||
Calvin Liu <flycalvin@qq.com>
|
|
||||||
Carlos de Paula <me@carlosedp.com>
|
Carlos de Paula <me@carlosedp.com>
|
||||||
Chad Faragher <wyckster@hotmail.com>
|
Chad Faragher <wyckster@hotmail.com>
|
||||||
Chander Govindarajan <chandergovind@gmail.com>
|
Chander Govindarajan <chandergovind@gmail.com>
|
||||||
@@ -110,8 +91,6 @@ Chen Chuanliang <chen.chuanliang@zte.com.cn>
|
|||||||
Chen Mingjie <chenmingjie0828@163.com>
|
Chen Mingjie <chenmingjie0828@163.com>
|
||||||
Chen Qiu <cheney-90@hotmail.com>
|
Chen Qiu <cheney-90@hotmail.com>
|
||||||
Chen Qiu <cheney-90@hotmail.com> <21321229@zju.edu.cn>
|
Chen Qiu <cheney-90@hotmail.com> <21321229@zju.edu.cn>
|
||||||
Chris Chinchilla <chris@chrischinchilla.com>
|
|
||||||
Chris Chinchilla <chris@chrischinchilla.com> <chris.ward@docker.com>
|
|
||||||
Chris Dias <cdias@microsoft.com>
|
Chris Dias <cdias@microsoft.com>
|
||||||
Chris McKinnel <chris.mckinnel@tangentlabs.co.uk>
|
Chris McKinnel <chris.mckinnel@tangentlabs.co.uk>
|
||||||
Christopher Biscardi <biscarch@sketcht.com>
|
Christopher Biscardi <biscarch@sketcht.com>
|
||||||
@@ -122,7 +101,6 @@ Chun Chen <ramichen@tencent.com> <chenchun.feed@gmail.com>
|
|||||||
Comical Derskeal <27731088+derskeal@users.noreply.github.com>
|
Comical Derskeal <27731088+derskeal@users.noreply.github.com>
|
||||||
Corbin Coleman <corbin.coleman@docker.com>
|
Corbin Coleman <corbin.coleman@docker.com>
|
||||||
Cory Bennet <cbennett@netflix.com>
|
Cory Bennet <cbennett@netflix.com>
|
||||||
Craig Osterhout <craig.osterhout@docker.com>
|
|
||||||
Cristian Staretu <cristian.staretu@gmail.com>
|
Cristian Staretu <cristian.staretu@gmail.com>
|
||||||
Cristian Staretu <cristian.staretu@gmail.com> <unclejack@users.noreply.github.com>
|
Cristian Staretu <cristian.staretu@gmail.com> <unclejack@users.noreply.github.com>
|
||||||
Cristian Staretu <cristian.staretu@gmail.com> <unclejacksons@gmail.com>
|
Cristian Staretu <cristian.staretu@gmail.com> <unclejacksons@gmail.com>
|
||||||
@@ -132,7 +110,6 @@ Daehyeok Mun <daehyeok@gmail.com> <daehyeok@daehyeok-ui-MacBook-Air.local>
|
|||||||
Daehyeok Mun <daehyeok@gmail.com> <daehyeok@daehyeokui-MacBook-Air.local>
|
Daehyeok Mun <daehyeok@gmail.com> <daehyeok@daehyeokui-MacBook-Air.local>
|
||||||
Daisuke Ito <itodaisuke00@gmail.com>
|
Daisuke Ito <itodaisuke00@gmail.com>
|
||||||
Dan Feldman <danf@jfrog.com>
|
Dan Feldman <danf@jfrog.com>
|
||||||
Danial Gharib <danial.mail.gh@gmail.com>
|
|
||||||
Daniel Dao <dqminh@cloudflare.com>
|
Daniel Dao <dqminh@cloudflare.com>
|
||||||
Daniel Dao <dqminh@cloudflare.com> <dqminh89@gmail.com>
|
Daniel Dao <dqminh@cloudflare.com> <dqminh89@gmail.com>
|
||||||
Daniel Garcia <daniel@danielgarcia.info>
|
Daniel Garcia <daniel@danielgarcia.info>
|
||||||
@@ -154,10 +131,6 @@ Dave Henderson <dhenderson@gmail.com> <Dave.Henderson@ca.ibm.com>
|
|||||||
Dave Tucker <dt@docker.com> <dave@dtucker.co.uk>
|
Dave Tucker <dt@docker.com> <dave@dtucker.co.uk>
|
||||||
David Alvarez <david.alvarez@flyeralarm.com>
|
David Alvarez <david.alvarez@flyeralarm.com>
|
||||||
David Alvarez <david.alvarez@flyeralarm.com> <busilezas@gmail.com>
|
David Alvarez <david.alvarez@flyeralarm.com> <busilezas@gmail.com>
|
||||||
David Dooling <david.dooling@docker.com>
|
|
||||||
David Dooling <david.dooling@docker.com> <dooling@gmail.com>
|
|
||||||
David Karlsson <david.karlsson@docker.com>
|
|
||||||
David Karlsson <david.karlsson@docker.com> <35727626+dvdksn@users.noreply.github.com>
|
|
||||||
David M. Karr <davidmichaelkarr@gmail.com>
|
David M. Karr <davidmichaelkarr@gmail.com>
|
||||||
David Sheets <dsheets@docker.com> <sheets@alum.mit.edu>
|
David Sheets <dsheets@docker.com> <sheets@alum.mit.edu>
|
||||||
David Sissitka <me@dsissitka.com>
|
David Sissitka <me@dsissitka.com>
|
||||||
@@ -205,13 +178,9 @@ Gaetan de Villele <gdevillele@gmail.com>
|
|||||||
Gang Qiao <qiaohai8866@gmail.com> <1373319223@qq.com>
|
Gang Qiao <qiaohai8866@gmail.com> <1373319223@qq.com>
|
||||||
George Kontridze <george@bugsnag.com>
|
George Kontridze <george@bugsnag.com>
|
||||||
Gerwim Feiken <g.feiken@tfe.nl> <gerwim@gmail.com>
|
Gerwim Feiken <g.feiken@tfe.nl> <gerwim@gmail.com>
|
||||||
Giau. Tran Minh <hello@giautm.dev>
|
|
||||||
Giau. Tran Minh <hello@giautm.dev> <12751435+giautm@users.noreply.github.com>
|
|
||||||
Giampaolo Mancini <giampaolo@trampolineup.com>
|
Giampaolo Mancini <giampaolo@trampolineup.com>
|
||||||
Gopikannan Venugopalsamy <gopikannan.venugopalsamy@gmail.com>
|
Gopikannan Venugopalsamy <gopikannan.venugopalsamy@gmail.com>
|
||||||
Gou Rao <gou@portworx.com> <gourao@users.noreply.github.com>
|
Gou Rao <gou@portworx.com> <gourao@users.noreply.github.com>
|
||||||
Graeme Wiebe <graeme.wiebe@gmail.com>
|
|
||||||
Graeme Wiebe <graeme.wiebe@gmail.com> <79593869+TheRealGramdalf@users.noreply.github.com>
|
|
||||||
Greg Stephens <greg@udon.org>
|
Greg Stephens <greg@udon.org>
|
||||||
Guillaume J. Charmes <guillaume.charmes@docker.com> <charmes.guillaume@gmail.com>
|
Guillaume J. Charmes <guillaume.charmes@docker.com> <charmes.guillaume@gmail.com>
|
||||||
Guillaume J. Charmes <guillaume.charmes@docker.com> <guillaume.charmes@dotcloud.com>
|
Guillaume J. Charmes <guillaume.charmes@docker.com> <guillaume.charmes@dotcloud.com>
|
||||||
@@ -226,7 +195,6 @@ Günther Jungbluth <gunther@gameslabs.net>
|
|||||||
Hakan Özler <hakan.ozler@kodcu.com>
|
Hakan Özler <hakan.ozler@kodcu.com>
|
||||||
Hao Shu Wei <haosw@cn.ibm.com>
|
Hao Shu Wei <haosw@cn.ibm.com>
|
||||||
Hao Shu Wei <haosw@cn.ibm.com> <haoshuwei1989@163.com>
|
Hao Shu Wei <haosw@cn.ibm.com> <haoshuwei1989@163.com>
|
||||||
Harald Albers <github@albersweb.de>
|
|
||||||
Harald Albers <github@albersweb.de> <albers@users.noreply.github.com>
|
Harald Albers <github@albersweb.de> <albers@users.noreply.github.com>
|
||||||
Harold Cooper <hrldcpr@gmail.com>
|
Harold Cooper <hrldcpr@gmail.com>
|
||||||
Harry Zhang <harryz@hyper.sh> <harryzhang@zju.edu.cn>
|
Harry Zhang <harryz@hyper.sh> <harryzhang@zju.edu.cn>
|
||||||
@@ -271,7 +239,6 @@ Jessica Frazelle <jess@oxide.computer> <jessfraz@google.com>
|
|||||||
Jessica Frazelle <jess@oxide.computer> <jfrazelle@users.noreply.github.com>
|
Jessica Frazelle <jess@oxide.computer> <jfrazelle@users.noreply.github.com>
|
||||||
Jessica Frazelle <jess@oxide.computer> <me@jessfraz.com>
|
Jessica Frazelle <jess@oxide.computer> <me@jessfraz.com>
|
||||||
Jessica Frazelle <jess@oxide.computer> <princess@docker.com>
|
Jessica Frazelle <jess@oxide.computer> <princess@docker.com>
|
||||||
Jim Chen <njucjc@gmail.com>
|
|
||||||
Jim Galasyn <jim.galasyn@docker.com>
|
Jim Galasyn <jim.galasyn@docker.com>
|
||||||
Jiuyue Ma <majiuyue@huawei.com>
|
Jiuyue Ma <majiuyue@huawei.com>
|
||||||
Joey Geiger <jgeiger@gmail.com>
|
Joey Geiger <jgeiger@gmail.com>
|
||||||
@@ -287,8 +254,6 @@ John Howard <github@lowenna.com> <jhowardmsft@users.noreply.github.com>
|
|||||||
John Howard <github@lowenna.com> <John.Howard@microsoft.com>
|
John Howard <github@lowenna.com> <John.Howard@microsoft.com>
|
||||||
John Howard <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>
|
John Stephens <johnstep@docker.com> <johnstep@users.noreply.github.com>
|
||||||
Jonathan A. Sternberg <jonathansternberg@gmail.com>
|
|
||||||
Jonathan A. Sternberg <jonathansternberg@gmail.com> <jonathan.sternberg@docker.com>
|
|
||||||
Jordan Arentsen <blissdev@gmail.com>
|
Jordan Arentsen <blissdev@gmail.com>
|
||||||
Jordan Jennings <jjn2009@gmail.com> <jjn2009@users.noreply.github.com>
|
Jordan Jennings <jjn2009@gmail.com> <jjn2009@users.noreply.github.com>
|
||||||
Jorit Kleine-Möllhoff <joppich@bricknet.de> <joppich@users.noreply.github.com>
|
Jorit Kleine-Möllhoff <joppich@bricknet.de> <joppich@users.noreply.github.com>
|
||||||
@@ -324,8 +289,7 @@ Kelton Bassingthwaite <KeltonBassingthwaite@gmail.com> <github@bassingthwaite.or
|
|||||||
Ken Cochrane <kencochrane@gmail.com> <KenCochrane@gmail.com>
|
Ken Cochrane <kencochrane@gmail.com> <KenCochrane@gmail.com>
|
||||||
Ken Herner <kherner@progress.com> <chosenken@gmail.com>
|
Ken Herner <kherner@progress.com> <chosenken@gmail.com>
|
||||||
Kenfe-Mickaël Laventure <mickael.laventure@gmail.com>
|
Kenfe-Mickaël Laventure <mickael.laventure@gmail.com>
|
||||||
Kevin Alvarez <github@crazymax.dev>
|
Kevin Alvarez <crazy-max@users.noreply.github.com>
|
||||||
Kevin Alvarez <github@crazymax.dev> <crazy-max@users.noreply.github.com>
|
|
||||||
Kevin Feyrer <kevin.feyrer@btinternet.com> <kevinfeyrer@users.noreply.github.com>
|
Kevin Feyrer <kevin.feyrer@btinternet.com> <kevinfeyrer@users.noreply.github.com>
|
||||||
Kevin Kern <kaiwentan@harmonycloud.cn>
|
Kevin Kern <kaiwentan@harmonycloud.cn>
|
||||||
Kevin Meredith <kevin.m.meredith@gmail.com>
|
Kevin Meredith <kevin.m.meredith@gmail.com>
|
||||||
@@ -342,9 +306,6 @@ Kyle Mitofsky <Kylemit@gmail.com>
|
|||||||
Lajos Papp <lajos.papp@sequenceiq.com> <lalyos@yahoo.com>
|
Lajos Papp <lajos.papp@sequenceiq.com> <lalyos@yahoo.com>
|
||||||
Lei Jitang <leijitang@huawei.com>
|
Lei Jitang <leijitang@huawei.com>
|
||||||
Lei Jitang <leijitang@huawei.com> <leijitang@gmail.com>
|
Lei Jitang <leijitang@huawei.com> <leijitang@gmail.com>
|
||||||
Li Fu Bang <lifubang@acmcoder.com>
|
|
||||||
Li Yi <denverdino@gmail.com>
|
|
||||||
Li Yi <denverdino@gmail.com> <weiyuan.yl@alibaba-inc.com>
|
|
||||||
Liang Mingqiang <mqliang.zju@gmail.com>
|
Liang Mingqiang <mqliang.zju@gmail.com>
|
||||||
Liang-Chi Hsieh <viirya@gmail.com>
|
Liang-Chi Hsieh <viirya@gmail.com>
|
||||||
Liao Qingwei <liaoqingwei@huawei.com>
|
Liao Qingwei <liaoqingwei@huawei.com>
|
||||||
@@ -354,11 +315,8 @@ Lokesh Mandvekar <lsm5@fedoraproject.org> <lsm5@redhat.com>
|
|||||||
Lorenzo Fontana <lo@linux.com> <fontanalorenzo@me.com>
|
Lorenzo Fontana <lo@linux.com> <fontanalorenzo@me.com>
|
||||||
Louis Opter <kalessin@kalessin.fr>
|
Louis Opter <kalessin@kalessin.fr>
|
||||||
Louis Opter <kalessin@kalessin.fr> <louis@dotcloud.com>
|
Louis Opter <kalessin@kalessin.fr> <louis@dotcloud.com>
|
||||||
Lovekesh Kumar <lovekesh.kumar@rtcamp.com>
|
|
||||||
Luca Favatella <luca.favatella@erlang-solutions.com> <lucafavatella@users.noreply.github.com>
|
Luca Favatella <luca.favatella@erlang-solutions.com> <lucafavatella@users.noreply.github.com>
|
||||||
Lukas Michael <lukas.23022005@gmail.com>
|
|
||||||
Luke Marsden <me@lukemarsden.net> <luke@digital-crocus.com>
|
Luke Marsden <me@lukemarsden.net> <luke@digital-crocus.com>
|
||||||
Luo Jiyin <luojiyin@hotmail.com>
|
|
||||||
Lyn <energylyn@zju.edu.cn>
|
Lyn <energylyn@zju.edu.cn>
|
||||||
Lynda O'Leary <lyndaoleary29@gmail.com>
|
Lynda O'Leary <lyndaoleary29@gmail.com>
|
||||||
Lynda O'Leary <lyndaoleary29@gmail.com> <lyndaoleary@hotmail.com>
|
Lynda O'Leary <lyndaoleary29@gmail.com> <lyndaoleary@hotmail.com>
|
||||||
@@ -372,7 +330,6 @@ Mansi Nahar <mmn4185@rit.edu> <mansi.nahar@macbookpro-mansinahar.local>
|
|||||||
Mansi Nahar <mmn4185@rit.edu> <mansinahar@users.noreply.github.com>
|
Mansi Nahar <mmn4185@rit.edu> <mansinahar@users.noreply.github.com>
|
||||||
Marc Abramowitz <marc@marc-abramowitz.com> <msabramo@gmail.com>
|
Marc Abramowitz <marc@marc-abramowitz.com> <msabramo@gmail.com>
|
||||||
Marcelo Horacio Fortino <info@fortinux.com> <fortinux@users.noreply.github.com>
|
Marcelo Horacio Fortino <info@fortinux.com> <fortinux@users.noreply.github.com>
|
||||||
Marco Spiess <marco.spiess@hotmail.de>
|
|
||||||
Marcus Linke <marcus.linke@gmx.de>
|
Marcus Linke <marcus.linke@gmx.de>
|
||||||
Marianna Tessel <mtesselh@gmail.com>
|
Marianna Tessel <mtesselh@gmail.com>
|
||||||
Marius Ileana <marius.ileana@gmail.com>
|
Marius Ileana <marius.ileana@gmail.com>
|
||||||
@@ -405,7 +362,6 @@ Michael Huettermann <michael@huettermann.net>
|
|||||||
Michael Käufl <docker@c.michael-kaeufl.de> <michael-k@users.noreply.github.com>
|
Michael Käufl <docker@c.michael-kaeufl.de> <michael-k@users.noreply.github.com>
|
||||||
Michael Spetsiotis <michael_spets@hotmail.com>
|
Michael Spetsiotis <michael_spets@hotmail.com>
|
||||||
Michal Minář <miminar@redhat.com>
|
Michal Minář <miminar@redhat.com>
|
||||||
Mickael Emirkanian <mickael.emirkanian@docker.com>
|
|
||||||
Miguel Angel Alvarez Cabrerizo <doncicuto@gmail.com> <30386061+doncicuto@users.noreply.github.com>
|
Miguel Angel Alvarez Cabrerizo <doncicuto@gmail.com> <30386061+doncicuto@users.noreply.github.com>
|
||||||
Miguel Angel Fernández <elmendalerenda@gmail.com>
|
Miguel Angel Fernández <elmendalerenda@gmail.com>
|
||||||
Mihai Borobocea <MihaiBorob@gmail.com> <MihaiBorobocea@gmail.com>
|
Mihai Borobocea <MihaiBorob@gmail.com> <MihaiBorobocea@gmail.com>
|
||||||
@@ -415,7 +371,6 @@ Mike Dalton <mikedalton@github.com> <19153140+mikedalton@users.noreply.github.co
|
|||||||
Mike Goelzer <mike.goelzer@docker.com> <mgoelzer@docker.com>
|
Mike Goelzer <mike.goelzer@docker.com> <mgoelzer@docker.com>
|
||||||
Milind Chawre <milindchawre@gmail.com>
|
Milind Chawre <milindchawre@gmail.com>
|
||||||
Misty Stanley-Jones <misty@docker.com> <misty@apache.org>
|
Misty Stanley-Jones <misty@docker.com> <misty@apache.org>
|
||||||
Mohammad Hossein <mhm98035@gmail.com>
|
|
||||||
Mohit Soni <mosoni@ebay.com> <mohitsoni1989@gmail.com>
|
Mohit Soni <mosoni@ebay.com> <mohitsoni1989@gmail.com>
|
||||||
Moorthy RS <rsmoorthy@gmail.com> <rsmoorthy@users.noreply.github.com>
|
Moorthy RS <rsmoorthy@gmail.com> <rsmoorthy@users.noreply.github.com>
|
||||||
Morten Hekkvang <morten.hekkvang@sbab.se>
|
Morten Hekkvang <morten.hekkvang@sbab.se>
|
||||||
@@ -439,15 +394,11 @@ O.S. Tezer <ostezer@gmail.com> <ostezer@users.noreply.github.com>
|
|||||||
Oh Jinkyun <tintypemolly@gmail.com> <tintypemolly@Ohui-MacBook-Pro.local>
|
Oh Jinkyun <tintypemolly@gmail.com> <tintypemolly@Ohui-MacBook-Pro.local>
|
||||||
Oliver Pomeroy <oppomeroy@gmail.com>
|
Oliver Pomeroy <oppomeroy@gmail.com>
|
||||||
Ouyang Liduo <oyld0210@163.com>
|
Ouyang Liduo <oyld0210@163.com>
|
||||||
Patrick St. laurent <patrick@saint-laurent.us>
|
|
||||||
Patrick Stapleton <github@gdi2290.com>
|
Patrick Stapleton <github@gdi2290.com>
|
||||||
Paul Liljenberg <liljenberg.paul@gmail.com> <letters@paulnotcom.se>
|
Paul Liljenberg <liljenberg.paul@gmail.com> <letters@paulnotcom.se>
|
||||||
Pavel Tikhomirov <ptikhomirov@virtuozzo.com> <ptikhomirov@parallels.com>
|
Pavel Tikhomirov <ptikhomirov@virtuozzo.com> <ptikhomirov@parallels.com>
|
||||||
Pawel Konczalski <mail@konczalski.de>
|
Pawel Konczalski <mail@konczalski.de>
|
||||||
Paweł Pokrywka <pepawel@users.noreply.github.com>
|
Paweł Pokrywka <pepawel@users.noreply.github.com>
|
||||||
Per Lundberg <perlun@gmail.com>
|
|
||||||
Per Lundberg <perlun@gmail.com> <per.lundberg@ecraft.com>
|
|
||||||
Per Lundberg <perlun@gmail.com> <per.lundberg@hibox.tv>
|
|
||||||
Peter Choi <phkchoi89@gmail.com> <reikani@Peters-MacBook-Pro.local>
|
Peter Choi <phkchoi89@gmail.com> <reikani@Peters-MacBook-Pro.local>
|
||||||
Peter Dave Hello <hsu@peterdavehello.org> <PeterDaveHello@users.noreply.github.com>
|
Peter Dave Hello <hsu@peterdavehello.org> <PeterDaveHello@users.noreply.github.com>
|
||||||
Peter Hsu <shhsu@microsoft.com>
|
Peter Hsu <shhsu@microsoft.com>
|
||||||
@@ -463,8 +414,6 @@ Qiang Huang <h.huangqiang@huawei.com>
|
|||||||
Qiang Huang <h.huangqiang@huawei.com> <qhuang@10.0.2.15>
|
Qiang Huang <h.huangqiang@huawei.com> <qhuang@10.0.2.15>
|
||||||
Ray Tsang <rayt@google.com> <saturnism@users.noreply.github.com>
|
Ray Tsang <rayt@google.com> <saturnism@users.noreply.github.com>
|
||||||
Renaud Gaubert <rgaubert@nvidia.com> <renaud.gaubert@gmail.com>
|
Renaud Gaubert <rgaubert@nvidia.com> <renaud.gaubert@gmail.com>
|
||||||
Rob Murray <rob.murray@docker.com>
|
|
||||||
Rob Murray <rob.murray@docker.com> <148866618+robmry@users.noreply.github.com>
|
|
||||||
Robert Terhaar <rterhaar@atlanticdynamic.com> <robbyt@users.noreply.github.com>
|
Robert Terhaar <rterhaar@atlanticdynamic.com> <robbyt@users.noreply.github.com>
|
||||||
Roberto G. Hashioka <roberto.hashioka@docker.com> <roberto_hashioka@hotmail.com>
|
Roberto G. Hashioka <roberto.hashioka@docker.com> <roberto_hashioka@hotmail.com>
|
||||||
Roberto Muñoz Fernández <robertomf@gmail.com> <roberto.munoz.fernandez.contractor@bbva.com>
|
Roberto Muñoz Fernández <robertomf@gmail.com> <roberto.munoz.fernandez.contractor@bbva.com>
|
||||||
@@ -472,7 +421,6 @@ Roch Feuillade <roch.feuillade@pandobac.com>
|
|||||||
Roch Feuillade <roch.feuillade@pandobac.com> <46478807+rochfeu@users.noreply.github.com>
|
Roch Feuillade <roch.feuillade@pandobac.com> <46478807+rochfeu@users.noreply.github.com>
|
||||||
Roman Dudin <katrmr@gmail.com> <decadent@users.noreply.github.com>
|
Roman Dudin <katrmr@gmail.com> <decadent@users.noreply.github.com>
|
||||||
Ross Boucher <rboucher@gmail.com>
|
Ross Boucher <rboucher@gmail.com>
|
||||||
Rui JingAn <quiterace@gmail.com>
|
|
||||||
Runshen Zhu <runshen.zhu@gmail.com>
|
Runshen Zhu <runshen.zhu@gmail.com>
|
||||||
Ryan Stelly <ryan.stelly@live.com>
|
Ryan Stelly <ryan.stelly@live.com>
|
||||||
Sakeven Jiang <jc5930@sina.cn>
|
Sakeven Jiang <jc5930@sina.cn>
|
||||||
@@ -481,7 +429,6 @@ Sandeep Bansal <sabansal@microsoft.com>
|
|||||||
Sandeep Bansal <sabansal@microsoft.com> <msabansal@microsoft.com>
|
Sandeep Bansal <sabansal@microsoft.com> <msabansal@microsoft.com>
|
||||||
Sandro Jäckel <sandro.jaeckel@gmail.com>
|
Sandro Jäckel <sandro.jaeckel@gmail.com>
|
||||||
Sargun Dhillon <sargun@netflix.com> <sargun@sargun.me>
|
Sargun Dhillon <sargun@netflix.com> <sargun@sargun.me>
|
||||||
Saurabh Kumar <saurabhkumar0184@gmail.com>
|
|
||||||
Sean Lee <seanlee@tw.ibm.com> <scaleoutsean@users.noreply.github.com>
|
Sean Lee <seanlee@tw.ibm.com> <scaleoutsean@users.noreply.github.com>
|
||||||
Sebastiaan van Stijn <github@gone.nl> <sebastiaan@ws-key-sebas3.dpi1.dpi>
|
Sebastiaan van Stijn <github@gone.nl> <sebastiaan@ws-key-sebas3.dpi1.dpi>
|
||||||
Sebastiaan van Stijn <github@gone.nl> <thaJeztah@users.noreply.github.com>
|
Sebastiaan van Stijn <github@gone.nl> <thaJeztah@users.noreply.github.com>
|
||||||
@@ -519,7 +466,6 @@ Stephen Day <stevvooe@gmail.com> <stephen.day@docker.com>
|
|||||||
Stephen Day <stevvooe@gmail.com> <stevvooe@users.noreply.github.com>
|
Stephen Day <stevvooe@gmail.com> <stevvooe@users.noreply.github.com>
|
||||||
Steve Desmond <steve@vtsv.ca> <stevedesmond-ca@users.noreply.github.com>
|
Steve Desmond <steve@vtsv.ca> <stevedesmond-ca@users.noreply.github.com>
|
||||||
Steve Richards <steve.richards@docker.com> stevejr <>
|
Steve Richards <steve.richards@docker.com> stevejr <>
|
||||||
Stuart Williams <pid@pidster.com>
|
|
||||||
Sun Gengze <690388648@qq.com>
|
Sun Gengze <690388648@qq.com>
|
||||||
Sun Jianbo <wonderflow.sun@gmail.com>
|
Sun Jianbo <wonderflow.sun@gmail.com>
|
||||||
Sun Jianbo <wonderflow.sun@gmail.com> <wonderflow@zju.edu.cn>
|
Sun Jianbo <wonderflow.sun@gmail.com> <wonderflow@zju.edu.cn>
|
||||||
@@ -554,8 +500,6 @@ Tim Bart <tim@fewagainstmany.com>
|
|||||||
Tim Bosse <taim@bosboot.org> <maztaim@users.noreply.github.com>
|
Tim Bosse <taim@bosboot.org> <maztaim@users.noreply.github.com>
|
||||||
Tim Ruffles <oi@truffles.me.uk> <timruffles@googlemail.com>
|
Tim Ruffles <oi@truffles.me.uk> <timruffles@googlemail.com>
|
||||||
Tim Terhorst <mynamewastaken+git@gmail.com>
|
Tim Terhorst <mynamewastaken+git@gmail.com>
|
||||||
Tim Welsh <timothy.welsh@docker.com>
|
|
||||||
Tim Welsh <timothy.welsh@docker.com> <84401379+twelsh-aw@users.noreply.github.com>
|
|
||||||
Tim Zju <21651152@zju.edu.cn>
|
Tim Zju <21651152@zju.edu.cn>
|
||||||
Timothy Hobbs <timothyhobbs@seznam.cz>
|
Timothy Hobbs <timothyhobbs@seznam.cz>
|
||||||
Toli Kuznets <toli@docker.com>
|
Toli Kuznets <toli@docker.com>
|
||||||
@@ -575,8 +519,6 @@ Ulysses Souza <ulysses.souza@docker.com>
|
|||||||
Ulysses Souza <ulysses.souza@docker.com> <ulyssessouza@gmail.com>
|
Ulysses Souza <ulysses.souza@docker.com> <ulyssessouza@gmail.com>
|
||||||
Umesh Yadav <umesh4257@gmail.com>
|
Umesh Yadav <umesh4257@gmail.com>
|
||||||
Umesh Yadav <umesh4257@gmail.com> <dungeonmaster18@users.noreply.github.com>
|
Umesh Yadav <umesh4257@gmail.com> <dungeonmaster18@users.noreply.github.com>
|
||||||
Varun Hotani <varunhotani@gmail.com>
|
|
||||||
Vibhu Anan <vibhuanand@outlook.com>
|
|
||||||
Victor Lyuboslavsky <victor@victoreda.com>
|
Victor Lyuboslavsky <victor@victoreda.com>
|
||||||
Victor Vieux <victor.vieux@docker.com> <dev@vvieux.com>
|
Victor Vieux <victor.vieux@docker.com> <dev@vvieux.com>
|
||||||
Victor Vieux <victor.vieux@docker.com> <victor.vieux@dotcloud.com>
|
Victor Vieux <victor.vieux@docker.com> <victor.vieux@dotcloud.com>
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
# This file lists all contributors to the repository.
|
# This file lists all contributors to the repository.
|
||||||
# See scripts/docs/generate-authors.sh to make modifications.
|
# See scripts/docs/generate-authors.sh to make modifications.
|
||||||
|
|
||||||
4RH1T3CT0R7 <iprintercanon@gmail.com>
|
|
||||||
A. Lester Buck III <github-reg@nbolt.com>
|
|
||||||
Aanand Prasad <aanand.prasad@gmail.com>
|
Aanand Prasad <aanand.prasad@gmail.com>
|
||||||
Aaron L. Xu <liker.xu@foxmail.com>
|
Aaron L. Xu <liker.xu@foxmail.com>
|
||||||
Aaron Lehmann <alehmann@netflix.com>
|
Aaron Lehmann <alehmann@netflix.com>
|
||||||
@@ -18,7 +16,6 @@ Adolfo Ochagavía <aochagavia92@gmail.com>
|
|||||||
Adrian Plata <adrian.plata@docker.com>
|
Adrian Plata <adrian.plata@docker.com>
|
||||||
Adrien Duermael <adrien@duermael.com>
|
Adrien Duermael <adrien@duermael.com>
|
||||||
Adrien Folie <folie.adrien@gmail.com>
|
Adrien Folie <folie.adrien@gmail.com>
|
||||||
Adyanth Hosavalike <ahosavalike@ucsd.edu>
|
|
||||||
Ahmet Alp Balkan <ahmetb@microsoft.com>
|
Ahmet Alp Balkan <ahmetb@microsoft.com>
|
||||||
Aidan Feldman <aidan.feldman@gmail.com>
|
Aidan Feldman <aidan.feldman@gmail.com>
|
||||||
Aidan Hobson Sayers <aidanhs@cantab.net>
|
Aidan Hobson Sayers <aidanhs@cantab.net>
|
||||||
@@ -27,10 +24,9 @@ Akhil Mohan <akhil.mohan@mayadata.io>
|
|||||||
Akihiro Suda <akihiro.suda.cz@hco.ntt.co.jp>
|
Akihiro Suda <akihiro.suda.cz@hco.ntt.co.jp>
|
||||||
Akim Demaille <akim.demaille@docker.com>
|
Akim Demaille <akim.demaille@docker.com>
|
||||||
Alan Thompson <cloojure@gmail.com>
|
Alan Thompson <cloojure@gmail.com>
|
||||||
Alano Terblanche <alano.terblanche@docker.com>
|
|
||||||
Albert Callarisa <shark234@gmail.com>
|
Albert Callarisa <shark234@gmail.com>
|
||||||
Alberto Roura <mail@albertoroura.com>
|
Alberto Roura <mail@albertoroura.com>
|
||||||
Albin Kerouanton <albinker@gmail.com>
|
Albin Kerouanton <albin@akerouanton.name>
|
||||||
Aleksa Sarai <asarai@suse.de>
|
Aleksa Sarai <asarai@suse.de>
|
||||||
Aleksander Piotrowski <apiotrowski312@gmail.com>
|
Aleksander Piotrowski <apiotrowski312@gmail.com>
|
||||||
Alessandro Boch <aboch@tetrationanalytics.com>
|
Alessandro Boch <aboch@tetrationanalytics.com>
|
||||||
@@ -38,20 +34,15 @@ Alex Couture-Beil <alex@earthly.dev>
|
|||||||
Alex Mavrogiannis <alex.mavrogiannis@docker.com>
|
Alex Mavrogiannis <alex.mavrogiannis@docker.com>
|
||||||
Alex Mayer <amayer5125@gmail.com>
|
Alex Mayer <amayer5125@gmail.com>
|
||||||
Alexander Boyd <alex@opengroove.org>
|
Alexander Boyd <alex@opengroove.org>
|
||||||
Alexander Chneerov <achneerov@gmail.com>
|
|
||||||
Alexander Larsson <alexl@redhat.com>
|
Alexander Larsson <alexl@redhat.com>
|
||||||
Alexander Morozov <lk4d4math@gmail.com>
|
Alexander Morozov <lk4d4math@gmail.com>
|
||||||
Alexander Ryabov <i@sepa.spb.ru>
|
Alexander Ryabov <i@sepa.spb.ru>
|
||||||
Alexandre González <agonzalezro@gmail.com>
|
Alexandre González <agonzalezro@gmail.com>
|
||||||
Alexandre Levavasseur <alexandre+oss@13x.fr>
|
|
||||||
Alexandre Vallières-Lagacé <alexandre.valliereslagace@docker.com>
|
|
||||||
Alexey Igrychev <alexey.igrychev@flant.com>
|
Alexey Igrychev <alexey.igrychev@flant.com>
|
||||||
Alexis Couvreur <alexiscouvreur.pro@gmail.com>
|
Alexis Couvreur <alexiscouvreur.pro@gmail.com>
|
||||||
Alfred Landrum <alfred.landrum@docker.com>
|
Alfred Landrum <alfred.landrum@docker.com>
|
||||||
Ali Rostami <rostami.ali@gmail.com>
|
|
||||||
Alicia Lauerman <alicia@eta.im>
|
Alicia Lauerman <alicia@eta.im>
|
||||||
Allen Sun <allensun.shl@alibaba-inc.com>
|
Allen Sun <allensun.shl@alibaba-inc.com>
|
||||||
Allie Sadler <allie.sadler@docker.com>
|
|
||||||
Alvin Deng <alvin.q.deng@utexas.edu>
|
Alvin Deng <alvin.q.deng@utexas.edu>
|
||||||
Amen Belayneh <amenbelayneh@gmail.com>
|
Amen Belayneh <amenbelayneh@gmail.com>
|
||||||
Amey Shrivastava <72866602+AmeyShrivastava@users.noreply.github.com>
|
Amey Shrivastava <72866602+AmeyShrivastava@users.noreply.github.com>
|
||||||
@@ -66,13 +57,10 @@ Andreas Köhler <andi5.py@gmx.net>
|
|||||||
Andres G. Aragoneses <knocte@gmail.com>
|
Andres G. Aragoneses <knocte@gmail.com>
|
||||||
Andres Leon Rangel <aleon1220@gmail.com>
|
Andres Leon Rangel <aleon1220@gmail.com>
|
||||||
Andrew France <andrew@avito.co.uk>
|
Andrew France <andrew@avito.co.uk>
|
||||||
Andrew He <he.andrew.mail@gmail.com>
|
|
||||||
Andrew Hopp <andrew.hopp@me.com>
|
|
||||||
Andrew Hsu <andrewhsu@docker.com>
|
Andrew Hsu <andrewhsu@docker.com>
|
||||||
Andrew Macpherson <hopscotch23@gmail.com>
|
Andrew Macpherson <hopscotch23@gmail.com>
|
||||||
Andrew McDonnell <bugs@andrewmcdonnell.net>
|
Andrew McDonnell <bugs@andrewmcdonnell.net>
|
||||||
Andrew Po <absourd.noise@gmail.com>
|
Andrew Po <absourd.noise@gmail.com>
|
||||||
Andrew-Zipperer <atzipperer@gmail.com>
|
|
||||||
Andrey Petrov <andrey.petrov@shazow.net>
|
Andrey Petrov <andrey.petrov@shazow.net>
|
||||||
Andrii Berehuliak <berkusandrew@gmail.com>
|
Andrii Berehuliak <berkusandrew@gmail.com>
|
||||||
André Martins <aanm90@gmail.com>
|
André Martins <aanm90@gmail.com>
|
||||||
@@ -87,16 +75,11 @@ Antonis Kalipetis <akalipetis@gmail.com>
|
|||||||
Anusha Ragunathan <anusha.ragunathan@docker.com>
|
Anusha Ragunathan <anusha.ragunathan@docker.com>
|
||||||
Ao Li <la9249@163.com>
|
Ao Li <la9249@163.com>
|
||||||
Arash Deshmeh <adeshmeh@ca.ibm.com>
|
Arash Deshmeh <adeshmeh@ca.ibm.com>
|
||||||
Archimedes Trajano <developer@trajano.net>
|
|
||||||
Arko Dasgupta <arko@tetrate.io>
|
Arko Dasgupta <arko@tetrate.io>
|
||||||
Arnaud Porterie <icecrime@gmail.com>
|
Arnaud Porterie <icecrime@gmail.com>
|
||||||
Arnaud Rebillout <elboulangero@gmail.com>
|
Arnaud Rebillout <elboulangero@gmail.com>
|
||||||
Arthur Flageul <arthur.flageul@gmail.com>
|
|
||||||
Arthur Peka <arthur.peka@outlook.com>
|
Arthur Peka <arthur.peka@outlook.com>
|
||||||
Ashly Mathew <ashly.mathew@sap.com>
|
|
||||||
Ashwini Oruganti <ashwini.oruganti@gmail.com>
|
Ashwini Oruganti <ashwini.oruganti@gmail.com>
|
||||||
Aslam Ahemad <aslamahemad@gmail.com>
|
|
||||||
Austin Vazquez <austin.vazquez@docker.com>
|
|
||||||
Azat Khuyiyakhmetov <shadow_uz@mail.ru>
|
Azat Khuyiyakhmetov <shadow_uz@mail.ru>
|
||||||
Bardia Keyoumarsi <bkeyouma@ucsc.edu>
|
Bardia Keyoumarsi <bkeyouma@ucsc.edu>
|
||||||
Barnaby Gray <barnaby@pickle.me.uk>
|
Barnaby Gray <barnaby@pickle.me.uk>
|
||||||
@@ -115,9 +98,7 @@ Bill Wang <ozbillwang@gmail.com>
|
|||||||
Bin Liu <liubin0329@gmail.com>
|
Bin Liu <liubin0329@gmail.com>
|
||||||
Bingshen Wang <bingshen.wbs@alibaba-inc.com>
|
Bingshen Wang <bingshen.wbs@alibaba-inc.com>
|
||||||
Bishal Das <bishalhnj127@gmail.com>
|
Bishal Das <bishalhnj127@gmail.com>
|
||||||
Bjorn Neergaard <bjorn.neergaard@docker.com>
|
|
||||||
Boaz Shuster <ripcurld.github@gmail.com>
|
Boaz Shuster <ripcurld.github@gmail.com>
|
||||||
Boban Acimovic <boban.acimovic@gmail.com>
|
|
||||||
Bogdan Anton <contact@bogdananton.ro>
|
Bogdan Anton <contact@bogdananton.ro>
|
||||||
Boris Pruessmann <boris@pruessmann.org>
|
Boris Pruessmann <boris@pruessmann.org>
|
||||||
Brad Baker <brad@brad.fi>
|
Brad Baker <brad@brad.fi>
|
||||||
@@ -128,26 +109,19 @@ Brent Salisbury <brent.salisbury@docker.com>
|
|||||||
Bret Fisher <bret@bretfisher.com>
|
Bret Fisher <bret@bretfisher.com>
|
||||||
Brian (bex) Exelbierd <bexelbie@redhat.com>
|
Brian (bex) Exelbierd <bexelbie@redhat.com>
|
||||||
Brian Goff <cpuguy83@gmail.com>
|
Brian Goff <cpuguy83@gmail.com>
|
||||||
Brian Tracy <brian.tracy33@gmail.com>
|
|
||||||
Brian Wieder <brian@4wieders.com>
|
Brian Wieder <brian@4wieders.com>
|
||||||
Bruno Sousa <bruno.sousa@docker.com>
|
Bruno Sousa <bruno.sousa@docker.com>
|
||||||
Bruno Verachten <gounthar@gmail.com>
|
|
||||||
Bryan Bess <squarejaw@bsbess.com>
|
Bryan Bess <squarejaw@bsbess.com>
|
||||||
Bryan Boreham <bjboreham@gmail.com>
|
Bryan Boreham <bjboreham@gmail.com>
|
||||||
Bryan Murphy <bmurphy1976@gmail.com>
|
Bryan Murphy <bmurphy1976@gmail.com>
|
||||||
bryfry <bryon.fryer@gmail.com>
|
bryfry <bryon.fryer@gmail.com>
|
||||||
Calvin Liu <flycalvin@qq.com>
|
|
||||||
Cameron Spear <cameronspear@gmail.com>
|
Cameron Spear <cameronspear@gmail.com>
|
||||||
Cao Weiwei <cao.weiwei30@zte.com.cn>
|
Cao Weiwei <cao.weiwei30@zte.com.cn>
|
||||||
Carlo Mion <mion00@gmail.com>
|
Carlo Mion <mion00@gmail.com>
|
||||||
Carlos Alexandro Becker <caarlos0@gmail.com>
|
Carlos Alexandro Becker <caarlos0@gmail.com>
|
||||||
Carlos de Paula <me@carlosedp.com>
|
Carlos de Paula <me@carlosedp.com>
|
||||||
carsontham <carsontham@outlook.com>
|
|
||||||
Carston Schilds <Carston.Schilds@visier.com>
|
|
||||||
Casey Korver <casey@korver.dev>
|
|
||||||
Ce Gao <ce.gao@outlook.com>
|
Ce Gao <ce.gao@outlook.com>
|
||||||
Cedric Davies <cedricda@microsoft.com>
|
Cedric Davies <cedricda@microsoft.com>
|
||||||
Cesar Talledo <cesar.talledo@docker.com>
|
|
||||||
Cezar Sa Espinola <cezarsa@gmail.com>
|
Cezar Sa Espinola <cezarsa@gmail.com>
|
||||||
Chad Faragher <wyckster@hotmail.com>
|
Chad Faragher <wyckster@hotmail.com>
|
||||||
Chao Wang <wangchao.fnst@cn.fujitsu.com>
|
Chao Wang <wangchao.fnst@cn.fujitsu.com>
|
||||||
@@ -162,8 +136,6 @@ Chen Chuanliang <chen.chuanliang@zte.com.cn>
|
|||||||
Chen Hanxiao <chenhanxiao@cn.fujitsu.com>
|
Chen Hanxiao <chenhanxiao@cn.fujitsu.com>
|
||||||
Chen Mingjie <chenmingjie0828@163.com>
|
Chen Mingjie <chenmingjie0828@163.com>
|
||||||
Chen Qiu <cheney-90@hotmail.com>
|
Chen Qiu <cheney-90@hotmail.com>
|
||||||
Ching Wei Kang <164879897+WilliamK112@users.noreply.github.com>
|
|
||||||
Chris Chinchilla <chris@chrischinchilla.com>
|
|
||||||
Chris Couzens <ccouzens@gmail.com>
|
Chris Couzens <ccouzens@gmail.com>
|
||||||
Chris Gavin <chris@chrisgavin.me>
|
Chris Gavin <chris@chrisgavin.me>
|
||||||
Chris Gibson <chris@chrisg.io>
|
Chris Gibson <chris@chrisg.io>
|
||||||
@@ -178,13 +150,10 @@ Christophe Vidal <kriss@krizalys.com>
|
|||||||
Christopher Biscardi <biscarch@sketcht.com>
|
Christopher Biscardi <biscarch@sketcht.com>
|
||||||
Christopher Crone <christopher.crone@docker.com>
|
Christopher Crone <christopher.crone@docker.com>
|
||||||
Christopher Jones <tophj@linux.vnet.ibm.com>
|
Christopher Jones <tophj@linux.vnet.ibm.com>
|
||||||
Christopher Petito <47751006+krissetto@users.noreply.github.com>
|
|
||||||
Christopher Petito <chrisjpetito@gmail.com>
|
|
||||||
Christopher Svensson <stoffus@stoffus.com>
|
Christopher Svensson <stoffus@stoffus.com>
|
||||||
Christy Norman <christy@linux.vnet.ibm.com>
|
Christy Norman <christy@linux.vnet.ibm.com>
|
||||||
Chun Chen <ramichen@tencent.com>
|
Chun Chen <ramichen@tencent.com>
|
||||||
Clinton Kitson <clintonskitson@gmail.com>
|
Clinton Kitson <clintonskitson@gmail.com>
|
||||||
Codex <codex@openai.com>
|
|
||||||
Coenraad Loubser <coenraad@wish.org.za>
|
Coenraad Loubser <coenraad@wish.org.za>
|
||||||
Colin Hebert <hebert.colin@gmail.com>
|
Colin Hebert <hebert.colin@gmail.com>
|
||||||
Collin Guarino <collin.guarino@gmail.com>
|
Collin Guarino <collin.guarino@gmail.com>
|
||||||
@@ -194,8 +163,6 @@ Conner Crosby <conner@cavcrosby.tech>
|
|||||||
Corey Farrell <git@cfware.com>
|
Corey Farrell <git@cfware.com>
|
||||||
Corey Quon <corey.quon@docker.com>
|
Corey Quon <corey.quon@docker.com>
|
||||||
Cory Bennet <cbennett@netflix.com>
|
Cory Bennet <cbennett@netflix.com>
|
||||||
Cory Snider <csnider@mirantis.com>
|
|
||||||
Craig Osterhout <craig.osterhout@docker.com>
|
|
||||||
Craig Wilhite <crwilhit@microsoft.com>
|
Craig Wilhite <crwilhit@microsoft.com>
|
||||||
Cristian Staretu <cristian.staretu@gmail.com>
|
Cristian Staretu <cristian.staretu@gmail.com>
|
||||||
Daehyeok Mun <daehyeok@gmail.com>
|
Daehyeok Mun <daehyeok@gmail.com>
|
||||||
@@ -204,8 +171,6 @@ Daisuke Ito <itodaisuke00@gmail.com>
|
|||||||
dalanlan <dalanlan925@gmail.com>
|
dalanlan <dalanlan925@gmail.com>
|
||||||
Damien Nadé <github@livna.org>
|
Damien Nadé <github@livna.org>
|
||||||
Dan Cotora <dan@bluevision.ro>
|
Dan Cotora <dan@bluevision.ro>
|
||||||
Dan Wallis <dan@wallis.nz>
|
|
||||||
Danial Gharib <danial.mail.gh@gmail.com>
|
|
||||||
Daniel Artine <daniel.artine@ufrj.br>
|
Daniel Artine <daniel.artine@ufrj.br>
|
||||||
Daniel Cassidy <mail@danielcassidy.me.uk>
|
Daniel Cassidy <mail@danielcassidy.me.uk>
|
||||||
Daniel Dao <dqminh@cloudflare.com>
|
Daniel Dao <dqminh@cloudflare.com>
|
||||||
@@ -231,31 +196,26 @@ David Alvarez <david.alvarez@flyeralarm.com>
|
|||||||
David Beitey <david@davidjb.com>
|
David Beitey <david@davidjb.com>
|
||||||
David Calavera <david.calavera@gmail.com>
|
David Calavera <david.calavera@gmail.com>
|
||||||
David Cramer <davcrame@cisco.com>
|
David Cramer <davcrame@cisco.com>
|
||||||
David Dooling <david.dooling@docker.com>
|
David Dooling <dooling@gmail.com>
|
||||||
David Gageot <david@gageot.net>
|
David Gageot <david@gageot.net>
|
||||||
David Karlsson <david.karlsson@docker.com>
|
David Karlsson <david.karlsson@docker.com>
|
||||||
David le Blanc <systemmonkey42@users.noreply.github.com>
|
|
||||||
David Lechner <david@lechnology.com>
|
David Lechner <david@lechnology.com>
|
||||||
David Scott <dave@recoil.org>
|
David Scott <dave@recoil.org>
|
||||||
David Sheets <dsheets@docker.com>
|
David Sheets <dsheets@docker.com>
|
||||||
David Williamson <david.williamson@docker.com>
|
David Williamson <david.williamson@docker.com>
|
||||||
David Xia <dxia@spotify.com>
|
David Xia <dxia@spotify.com>
|
||||||
David Young <yangboh@cn.ibm.com>
|
David Young <yangboh@cn.ibm.com>
|
||||||
Davlat Davydov <literally_user@hotmail.com>
|
|
||||||
Deng Guangxing <dengguangxing@huawei.com>
|
Deng Guangxing <dengguangxing@huawei.com>
|
||||||
Denis Defreyne <denis@soundcloud.com>
|
Denis Defreyne <denis@soundcloud.com>
|
||||||
Denis Gladkikh <denis@gladkikh.email>
|
Denis Gladkikh <denis@gladkikh.email>
|
||||||
Denis Ollier <larchunix@users.noreply.github.com>
|
Denis Ollier <larchunix@users.noreply.github.com>
|
||||||
Dennis Docter <dennis@d23.nl>
|
Dennis Docter <dennis@d23.nl>
|
||||||
dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
|
|
||||||
Derek McGowan <derek@mcg.dev>
|
Derek McGowan <derek@mcg.dev>
|
||||||
Derek Misler <derek.misler@docker.com>
|
|
||||||
Des Preston <despreston@gmail.com>
|
Des Preston <despreston@gmail.com>
|
||||||
Deshi Xiao <dxiao@redhat.com>
|
Deshi Xiao <dxiao@redhat.com>
|
||||||
Dharmit Shah <shahdharmit@gmail.com>
|
Dharmit Shah <shahdharmit@gmail.com>
|
||||||
Dhawal Yogesh Bhanushali <dbhanushali@vmware.com>
|
Dhawal Yogesh Bhanushali <dbhanushali@vmware.com>
|
||||||
Dieter Reuter <dieter.reuter@me.com>
|
Dieter Reuter <dieter.reuter@me.com>
|
||||||
Dilep Dev <34891655+DilepDev@users.noreply.github.com>
|
|
||||||
Dima Stopel <dima@twistlock.com>
|
Dima Stopel <dima@twistlock.com>
|
||||||
Dimitry Andric <d.andric@activevideo.com>
|
Dimitry Andric <d.andric@activevideo.com>
|
||||||
Ding Fei <dingfei@stars.org.cn>
|
Ding Fei <dingfei@stars.org.cn>
|
||||||
@@ -269,18 +229,14 @@ Dominik Braun <dominik.braun@nbsp.de>
|
|||||||
Don Kjer <don.kjer@gmail.com>
|
Don Kjer <don.kjer@gmail.com>
|
||||||
Dong Chen <dongluo.chen@docker.com>
|
Dong Chen <dongluo.chen@docker.com>
|
||||||
DongGeon Lee <secmatth1996@gmail.com>
|
DongGeon Lee <secmatth1996@gmail.com>
|
||||||
Dorin Geman <dorin.geman@docker.com>
|
|
||||||
Doug Davis <dug@us.ibm.com>
|
Doug Davis <dug@us.ibm.com>
|
||||||
Drew Erny <derny@mirantis.com>
|
Drew Erny <derny@mirantis.com>
|
||||||
Ed Costello <epc@epcostello.com>
|
Ed Costello <epc@epcostello.com>
|
||||||
Ed Morley <501702+edmorley@users.noreply.github.com>
|
|
||||||
Elango Sivanandam <elango.siva@docker.com>
|
Elango Sivanandam <elango.siva@docker.com>
|
||||||
Eli Uriegas <eli.uriegas@docker.com>
|
Eli Uriegas <eli.uriegas@docker.com>
|
||||||
Eli Uriegas <seemethere101@gmail.com>
|
Eli Uriegas <seemethere101@gmail.com>
|
||||||
Elias Faxö <elias.faxo@tre.se>
|
Elias Faxö <elias.faxo@tre.se>
|
||||||
Elliot Luo <956941328@qq.com>
|
Elliot Luo <956941328@qq.com>
|
||||||
Eng Zer Jun <engzerjun@gmail.com>
|
|
||||||
Eric Bode <eric.bode@foundries.io>
|
|
||||||
Eric Curtin <ericcurtin17@gmail.com>
|
Eric Curtin <ericcurtin17@gmail.com>
|
||||||
Eric Engestrom <eric@engestrom.ch>
|
Eric Engestrom <eric@engestrom.ch>
|
||||||
Eric G. Noriega <enoriega@vizuri.com>
|
Eric G. Noriega <enoriega@vizuri.com>
|
||||||
@@ -298,7 +254,6 @@ Eugene Yakubovich <eugene.yakubovich@coreos.com>
|
|||||||
Evan Allrich <evan@unguku.com>
|
Evan Allrich <evan@unguku.com>
|
||||||
Evan Hazlett <ejhazlett@gmail.com>
|
Evan Hazlett <ejhazlett@gmail.com>
|
||||||
Evan Krall <krall@yelp.com>
|
Evan Krall <krall@yelp.com>
|
||||||
Evan Lezar <elezar@nvidia.com>
|
|
||||||
Evelyn Xu <evelynhsu21@gmail.com>
|
Evelyn Xu <evelynhsu21@gmail.com>
|
||||||
Everett Toews <everett.toews@rackspace.com>
|
Everett Toews <everett.toews@rackspace.com>
|
||||||
Fabio Falci <fabiofalci@gmail.com>
|
Fabio Falci <fabiofalci@gmail.com>
|
||||||
@@ -320,25 +275,19 @@ Frederik Nordahl Jul Sabroe <frederikns@gmail.com>
|
|||||||
Frieder Bluemle <frieder.bluemle@gmail.com>
|
Frieder Bluemle <frieder.bluemle@gmail.com>
|
||||||
Gabriel Gore <gabgore@cisco.com>
|
Gabriel Gore <gabgore@cisco.com>
|
||||||
Gabriel Nicolas Avellaneda <avellaneda.gabriel@gmail.com>
|
Gabriel Nicolas Avellaneda <avellaneda.gabriel@gmail.com>
|
||||||
Gabriela Georgieva <gabriela.georgieva@docker.com>
|
|
||||||
Gaetan de Villele <gdevillele@gmail.com>
|
Gaetan de Villele <gdevillele@gmail.com>
|
||||||
Gang Qiao <qiaohai8866@gmail.com>
|
Gang Qiao <qiaohai8866@gmail.com>
|
||||||
Gary Schaetz <gary@schaetzkc.com>
|
Gary Schaetz <gary@schaetzkc.com>
|
||||||
Genki Takiuchi <genki@s21g.com>
|
Genki Takiuchi <genki@s21g.com>
|
||||||
George MacRorie <gmacr31@gmail.com>
|
George MacRorie <gmacr31@gmail.com>
|
||||||
George Margaritis <gmargaritis@protonmail.com>
|
|
||||||
George Xie <georgexsh@gmail.com>
|
George Xie <georgexsh@gmail.com>
|
||||||
Gianluca Borello <g.borello@gmail.com>
|
Gianluca Borello <g.borello@gmail.com>
|
||||||
Giau. Tran Minh <hello@giautm.dev>
|
|
||||||
Giedrius Jonikas <giedriusj1@gmail.com>
|
|
||||||
Gildas Cuisinier <gildas.cuisinier@gcuisinier.net>
|
Gildas Cuisinier <gildas.cuisinier@gcuisinier.net>
|
||||||
Gio d'Amelio <giodamelio@gmail.com>
|
Gio d'Amelio <giodamelio@gmail.com>
|
||||||
Gleb Stsenov <gleb.stsenov@gmail.com>
|
Gleb Stsenov <gleb.stsenov@gmail.com>
|
||||||
Goksu Toprak <goksu.toprak@docker.com>
|
Goksu Toprak <goksu.toprak@docker.com>
|
||||||
Gou Rao <gou@portworx.com>
|
Gou Rao <gou@portworx.com>
|
||||||
Govind Rai <raigovind93@gmail.com>
|
Govind Rai <raigovind93@gmail.com>
|
||||||
Grace Choi <grace.54109@gmail.com>
|
|
||||||
Graeme Wiebe <graeme.wiebe@gmail.com>
|
|
||||||
Grant Reaber <grant.reaber@gmail.com>
|
Grant Reaber <grant.reaber@gmail.com>
|
||||||
Greg Pflaum <gpflaum@users.noreply.github.com>
|
Greg Pflaum <gpflaum@users.noreply.github.com>
|
||||||
Gsealy <jiaojingwei1001@hotmail.com>
|
Gsealy <jiaojingwei1001@hotmail.com>
|
||||||
@@ -360,15 +309,12 @@ Henning Sprang <henning.sprang@gmail.com>
|
|||||||
Henry N <henrynmail-github@yahoo.de>
|
Henry N <henrynmail-github@yahoo.de>
|
||||||
Hernan Garcia <hernandanielg@gmail.com>
|
Hernan Garcia <hernandanielg@gmail.com>
|
||||||
Hongbin Lu <hongbin034@gmail.com>
|
Hongbin Lu <hongbin034@gmail.com>
|
||||||
Hossein Abbasi <16090309+hsnabszhdn@users.noreply.github.com>
|
|
||||||
Hu Keping <hukeping@huawei.com>
|
Hu Keping <hukeping@huawei.com>
|
||||||
Huayi Zhang <irachex@gmail.com>
|
Huayi Zhang <irachex@gmail.com>
|
||||||
Hugo Chastel <Hugo-C@users.noreply.github.com>
|
|
||||||
Hugo Gabriel Eyherabide <hugogabriel.eyherabide@gmail.com>
|
Hugo Gabriel Eyherabide <hugogabriel.eyherabide@gmail.com>
|
||||||
huqun <huqun@zju.edu.cn>
|
huqun <huqun@zju.edu.cn>
|
||||||
Huu Nguyen <huu@prismskylabs.com>
|
Huu Nguyen <huu@prismskylabs.com>
|
||||||
Hyzhou Zhy <hyzhou.zhy@alibaba-inc.com>
|
Hyzhou Zhy <hyzhou.zhy@alibaba-inc.com>
|
||||||
Iain MacDonald <IJMacD@gmail.com>
|
|
||||||
Iain Samuel McLean Elder <iain@isme.es>
|
Iain Samuel McLean Elder <iain@isme.es>
|
||||||
Ian Campbell <ian.campbell@docker.com>
|
Ian Campbell <ian.campbell@docker.com>
|
||||||
Ian Philpot <ian.philpot@microsoft.com>
|
Ian Philpot <ian.philpot@microsoft.com>
|
||||||
@@ -383,12 +329,9 @@ Ivan Grund <ivan.grund@gmail.com>
|
|||||||
Ivan Markin <sw@nogoegst.net>
|
Ivan Markin <sw@nogoegst.net>
|
||||||
Jacob Atzen <jacob@jacobatzen.dk>
|
Jacob Atzen <jacob@jacobatzen.dk>
|
||||||
Jacob Tomlinson <jacob@tom.linson.uk>
|
Jacob Tomlinson <jacob@tom.linson.uk>
|
||||||
Jacopo Rigoli <rigoli.jacopo@gmail.com>
|
|
||||||
Jaivish Kothari <janonymous.codevulture@gmail.com>
|
Jaivish Kothari <janonymous.codevulture@gmail.com>
|
||||||
Jake Lambert <jake.lambert@volusion.com>
|
Jake Lambert <jake.lambert@volusion.com>
|
||||||
Jake Sanders <jsand@google.com>
|
Jake Sanders <jsand@google.com>
|
||||||
Jake Stokes <contactjake@developerjake.com>
|
|
||||||
Jakub Panek <me@panekj.dev>
|
|
||||||
James Nesbitt <james.nesbitt@wunderkraut.com>
|
James Nesbitt <james.nesbitt@wunderkraut.com>
|
||||||
James Turnbull <james@lovedthanlost.net>
|
James Turnbull <james@lovedthanlost.net>
|
||||||
Jamie Hannaford <jamie@limetree.org>
|
Jamie Hannaford <jamie@limetree.org>
|
||||||
@@ -418,10 +361,8 @@ Jesse Adametz <jesseadametz@gmail.com>
|
|||||||
Jessica Frazelle <jess@oxide.computer>
|
Jessica Frazelle <jess@oxide.computer>
|
||||||
Jezeniel Zapanta <jpzapanta22@gmail.com>
|
Jezeniel Zapanta <jpzapanta22@gmail.com>
|
||||||
Jian Zhang <zhangjian.fnst@cn.fujitsu.com>
|
Jian Zhang <zhangjian.fnst@cn.fujitsu.com>
|
||||||
Jianyong Wu <wujianyong@hygon.cn>
|
|
||||||
Jie Luo <luo612@zju.edu.cn>
|
Jie Luo <luo612@zju.edu.cn>
|
||||||
Jilles Oldenbeuving <ojilles@gmail.com>
|
Jilles Oldenbeuving <ojilles@gmail.com>
|
||||||
Jim Chen <njucjc@gmail.com>
|
|
||||||
Jim Galasyn <jim.galasyn@docker.com>
|
Jim Galasyn <jim.galasyn@docker.com>
|
||||||
Jim Lin <b04705003@ntu.edu.tw>
|
Jim Lin <b04705003@ntu.edu.tw>
|
||||||
Jimmy Leger <jimmy.leger@gmail.com>
|
Jimmy Leger <jimmy.leger@gmail.com>
|
||||||
@@ -452,7 +393,6 @@ John Willis <john.willis@docker.com>
|
|||||||
Jon Johnson <jonjohnson@google.com>
|
Jon Johnson <jonjohnson@google.com>
|
||||||
Jon Zeolla <zeolla@gmail.com>
|
Jon Zeolla <zeolla@gmail.com>
|
||||||
Jonatas Baldin <jonatas.baldin@gmail.com>
|
Jonatas Baldin <jonatas.baldin@gmail.com>
|
||||||
Jonathan A. Sternberg <jonathansternberg@gmail.com>
|
|
||||||
Jonathan Boulle <jonathanboulle@gmail.com>
|
Jonathan Boulle <jonathanboulle@gmail.com>
|
||||||
Jonathan Lee <jonjohn1232009@gmail.com>
|
Jonathan Lee <jonjohn1232009@gmail.com>
|
||||||
Jonathan Lomas <jonathan@floatinglomas.ca>
|
Jonathan Lomas <jonathan@floatinglomas.ca>
|
||||||
@@ -468,20 +408,16 @@ Josh Chorlton <jchorlton@gmail.com>
|
|||||||
Josh Hawn <josh.hawn@docker.com>
|
Josh Hawn <josh.hawn@docker.com>
|
||||||
Josh Horwitz <horwitz@addthis.com>
|
Josh Horwitz <horwitz@addthis.com>
|
||||||
Josh Soref <jsoref@gmail.com>
|
Josh Soref <jsoref@gmail.com>
|
||||||
Julian <gitea+julian@ic.thejulian.uk>
|
|
||||||
Julien Barbier <write0@gmail.com>
|
Julien Barbier <write0@gmail.com>
|
||||||
Julien Kassar <github@kassisol.com>
|
Julien Kassar <github@kassisol.com>
|
||||||
Julien Maitrehenry <julien.maitrehenry@me.com>
|
Julien Maitrehenry <julien.maitrehenry@me.com>
|
||||||
Julio Cesar Garcia <juliogarciamelgarejo@gmail.com>
|
|
||||||
Justas Brazauskas <brazauskasjustas@gmail.com>
|
Justas Brazauskas <brazauskasjustas@gmail.com>
|
||||||
Justin Chadwell <me@jedevc.com>
|
|
||||||
Justin Cormack <justin.cormack@docker.com>
|
Justin Cormack <justin.cormack@docker.com>
|
||||||
Justin Simonelis <justin.p.simonelis@gmail.com>
|
Justin Simonelis <justin.p.simonelis@gmail.com>
|
||||||
Justyn Temme <justyntemme@gmail.com>
|
Justyn Temme <justyntemme@gmail.com>
|
||||||
Jyrki Puttonen <jyrkiput@gmail.com>
|
Jyrki Puttonen <jyrkiput@gmail.com>
|
||||||
Jérémie Drouet <jeremie.drouet@gmail.com>
|
Jérémie Drouet <jeremie.drouet@gmail.com>
|
||||||
Jérôme Petazzoni <jerome.petazzoni@docker.com>
|
Jérôme Petazzoni <jerome.petazzoni@docker.com>
|
||||||
Jörg Sommer <joerg@jo-so.de>
|
|
||||||
Jörg Thalheim <joerg@higgsboson.tk>
|
Jörg Thalheim <joerg@higgsboson.tk>
|
||||||
Kai Blin <kai@samba.org>
|
Kai Blin <kai@samba.org>
|
||||||
Kai Qiang Wu (Kennan) <wkq5325@gmail.com>
|
Kai Qiang Wu (Kennan) <wkq5325@gmail.com>
|
||||||
@@ -498,7 +434,7 @@ Kelton Bassingthwaite <KeltonBassingthwaite@gmail.com>
|
|||||||
Ken Cochrane <kencochrane@gmail.com>
|
Ken Cochrane <kencochrane@gmail.com>
|
||||||
Ken ICHIKAWA <ichikawa.ken@jp.fujitsu.com>
|
Ken ICHIKAWA <ichikawa.ken@jp.fujitsu.com>
|
||||||
Kenfe-Mickaël Laventure <mickael.laventure@gmail.com>
|
Kenfe-Mickaël Laventure <mickael.laventure@gmail.com>
|
||||||
Kevin Alvarez <github@crazymax.dev>
|
Kevin Alvarez <crazy-max@users.noreply.github.com>
|
||||||
Kevin Burke <kev@inburke.com>
|
Kevin Burke <kev@inburke.com>
|
||||||
Kevin Feyrer <kevin.feyrer@btinternet.com>
|
Kevin Feyrer <kevin.feyrer@btinternet.com>
|
||||||
Kevin Kern <kaiwentan@harmonycloud.cn>
|
Kevin Kern <kaiwentan@harmonycloud.cn>
|
||||||
@@ -509,7 +445,6 @@ Kevin Woblick <mail@kovah.de>
|
|||||||
khaled souf <khaled.souf@gmail.com>
|
khaled souf <khaled.souf@gmail.com>
|
||||||
Kim Eik <kim@heldig.org>
|
Kim Eik <kim@heldig.org>
|
||||||
Kir Kolyshkin <kolyshkin@gmail.com>
|
Kir Kolyshkin <kolyshkin@gmail.com>
|
||||||
Kirill A. Korinsky <kirill@korins.ky>
|
|
||||||
Kotaro Yoshimatsu <kotaro.yoshimatsu@gmail.com>
|
Kotaro Yoshimatsu <kotaro.yoshimatsu@gmail.com>
|
||||||
Krasi Georgiev <krasi@vip-consult.solutions>
|
Krasi Georgiev <krasi@vip-consult.solutions>
|
||||||
Kris-Mikael Krister <krismikael@protonmail.com>
|
Kris-Mikael Krister <krismikael@protonmail.com>
|
||||||
@@ -518,23 +453,19 @@ Kunal Kushwaha <kushwaha_kunal_v7@lab.ntt.co.jp>
|
|||||||
Kyle Mitofsky <Kylemit@gmail.com>
|
Kyle Mitofsky <Kylemit@gmail.com>
|
||||||
Lachlan Cooper <lachlancooper@gmail.com>
|
Lachlan Cooper <lachlancooper@gmail.com>
|
||||||
Lai Jiangshan <jiangshanlai@gmail.com>
|
Lai Jiangshan <jiangshanlai@gmail.com>
|
||||||
Lajos Papp <lajos.papp@sequenceiq.com>
|
|
||||||
Lars Kellogg-Stedman <lars@redhat.com>
|
Lars Kellogg-Stedman <lars@redhat.com>
|
||||||
Laura Brehm <laurabrehm@hey.com>
|
|
||||||
Laura Frank <ljfrank@gmail.com>
|
Laura Frank <ljfrank@gmail.com>
|
||||||
Laurent Erignoux <lerignoux@gmail.com>
|
Laurent Erignoux <lerignoux@gmail.com>
|
||||||
Laurent Goderre <laurent.goderre@docker.com>
|
|
||||||
Lee Gaines <eightlimbed@gmail.com>
|
Lee Gaines <eightlimbed@gmail.com>
|
||||||
Lei Jitang <leijitang@huawei.com>
|
Lei Jitang <leijitang@huawei.com>
|
||||||
Lennie <github@consolejunkie.net>
|
Lennie <github@consolejunkie.net>
|
||||||
lentil32 <lentil32@icloud.com>
|
|
||||||
Leo Gallucci <elgalu3@gmail.com>
|
Leo Gallucci <elgalu3@gmail.com>
|
||||||
Leonid Skorospelov <leosko94@gmail.com>
|
Leonid Skorospelov <leosko94@gmail.com>
|
||||||
Lewis Daly <lewisdaly@me.com>
|
Lewis Daly <lewisdaly@me.com>
|
||||||
Li Fu Bang <lifubang@acmcoder.com>
|
|
||||||
Li Yi <denverdino@gmail.com>
|
Li Yi <denverdino@gmail.com>
|
||||||
Li Zeghong <zeghong@hotmail.com>
|
Li Yi <weiyuan.yl@alibaba-inc.com>
|
||||||
Liang-Chi Hsieh <viirya@gmail.com>
|
Liang-Chi Hsieh <viirya@gmail.com>
|
||||||
|
Lifubang <lifubang@acmcoder.com>
|
||||||
Lihua Tang <lhtang@alauda.io>
|
Lihua Tang <lhtang@alauda.io>
|
||||||
Lily Guo <lily.guo@docker.com>
|
Lily Guo <lily.guo@docker.com>
|
||||||
Lin Lu <doraalin@163.com>
|
Lin Lu <doraalin@163.com>
|
||||||
@@ -546,17 +477,12 @@ lixiaobing10051267 <li.xiaobing1@zte.com.cn>
|
|||||||
Lloyd Dewolf <foolswisdom@gmail.com>
|
Lloyd Dewolf <foolswisdom@gmail.com>
|
||||||
Lorenzo Fontana <lo@linux.com>
|
Lorenzo Fontana <lo@linux.com>
|
||||||
Louis Opter <kalessin@kalessin.fr>
|
Louis Opter <kalessin@kalessin.fr>
|
||||||
Lovekesh Kumar <lovekesh.kumar@rtcamp.com>
|
|
||||||
Luca Favatella <luca.favatella@erlang-solutions.com>
|
Luca Favatella <luca.favatella@erlang-solutions.com>
|
||||||
Luca Marturana <lucamarturana@gmail.com>
|
Luca Marturana <lucamarturana@gmail.com>
|
||||||
Lucas Chan <lucas-github@lucaschan.com>
|
Lucas Chan <lucas-github@lucaschan.com>
|
||||||
Ludovic Temgoua Abanda <abandaludovic500@gmail.com>
|
|
||||||
Luis Henrique Mulinari <luis.mulinari@gmail.com>
|
|
||||||
Luka Hartwig <mail@lukahartwig.de>
|
Luka Hartwig <mail@lukahartwig.de>
|
||||||
Lukas Heeren <lukas-heeren@hotmail.com>
|
Lukas Heeren <lukas-heeren@hotmail.com>
|
||||||
Lukas Michael <lukas.23022005@gmail.com>
|
|
||||||
Lukasz Zajaczkowski <Lukasz.Zajaczkowski@ts.fujitsu.com>
|
Lukasz Zajaczkowski <Lukasz.Zajaczkowski@ts.fujitsu.com>
|
||||||
Luo Jiyin <luojiyin@hotmail.com>
|
|
||||||
Lydell Manganti <LydellManganti@users.noreply.github.com>
|
Lydell Manganti <LydellManganti@users.noreply.github.com>
|
||||||
Lénaïc Huard <lhuard@amadeus.com>
|
Lénaïc Huard <lhuard@amadeus.com>
|
||||||
Ma Shimiao <mashimiao.fnst@cn.fujitsu.com>
|
Ma Shimiao <mashimiao.fnst@cn.fujitsu.com>
|
||||||
@@ -565,7 +491,6 @@ Maciej Kalisz <maciej.d.kalisz@gmail.com>
|
|||||||
Madhav Puri <madhav.puri@gmail.com>
|
Madhav Puri <madhav.puri@gmail.com>
|
||||||
Madhu Venugopal <madhu@socketplane.io>
|
Madhu Venugopal <madhu@socketplane.io>
|
||||||
Madhur Batra <madhurbatra097@gmail.com>
|
Madhur Batra <madhurbatra097@gmail.com>
|
||||||
Mahesh Thakur <maheshthakur9152@gmail.com>
|
|
||||||
Malte Janduda <mail@janduda.net>
|
Malte Janduda <mail@janduda.net>
|
||||||
Manjunath A Kumatagi <mkumatag@in.ibm.com>
|
Manjunath A Kumatagi <mkumatag@in.ibm.com>
|
||||||
Mansi Nahar <mmn4185@rit.edu>
|
Mansi Nahar <mmn4185@rit.edu>
|
||||||
@@ -573,12 +498,10 @@ mapk0y <mapk0y@gmail.com>
|
|||||||
Marc Bihlmaier <marc.bihlmaier@reddoxx.com>
|
Marc Bihlmaier <marc.bihlmaier@reddoxx.com>
|
||||||
Marc Cornellà <hello@mcornella.com>
|
Marc Cornellà <hello@mcornella.com>
|
||||||
Marco Mariani <marco.mariani@alterway.fr>
|
Marco Mariani <marco.mariani@alterway.fr>
|
||||||
Marco Spiess <marco.spiess@hotmail.de>
|
|
||||||
Marco Vedovati <mvedovati@suse.com>
|
Marco Vedovati <mvedovati@suse.com>
|
||||||
Marcus Martins <marcus@docker.com>
|
Marcus Martins <marcus@docker.com>
|
||||||
Marianna Tessel <mtesselh@gmail.com>
|
Marianna Tessel <mtesselh@gmail.com>
|
||||||
Marius Ileana <marius.ileana@gmail.com>
|
Marius Ileana <marius.ileana@gmail.com>
|
||||||
Marius Meschter <marius@meschter.me>
|
|
||||||
Marius Sturm <marius@graylog.com>
|
Marius Sturm <marius@graylog.com>
|
||||||
Mark Oates <fl0yd@me.com>
|
Mark Oates <fl0yd@me.com>
|
||||||
Marsh Macy <marsma@microsoft.com>
|
Marsh Macy <marsma@microsoft.com>
|
||||||
@@ -587,25 +510,18 @@ Mary Anthony <mary.anthony@docker.com>
|
|||||||
Mason Fish <mason.fish@docker.com>
|
Mason Fish <mason.fish@docker.com>
|
||||||
Mason Malone <mason.malone@gmail.com>
|
Mason Malone <mason.malone@gmail.com>
|
||||||
Mateusz Major <apkd@users.noreply.github.com>
|
Mateusz Major <apkd@users.noreply.github.com>
|
||||||
Mathias Duedahl <64321057+Lussebullen@users.noreply.github.com>
|
|
||||||
Mathieu Champlon <mathieu.champlon@docker.com>
|
Mathieu Champlon <mathieu.champlon@docker.com>
|
||||||
Mathieu Rollet <matletix@gmail.com>
|
Mathieu Rollet <matletix@gmail.com>
|
||||||
Matt Gucci <matt9ucci@gmail.com>
|
Matt Gucci <matt9ucci@gmail.com>
|
||||||
Matt Robenolt <matt@ydekproductions.com>
|
Matt Robenolt <matt@ydekproductions.com>
|
||||||
Matteo Orefice <matteo.orefice@bites4bits.software>
|
Matteo Orefice <matteo.orefice@bites4bits.software>
|
||||||
Matteo Panzeri <matteo1782@gmail.com>
|
|
||||||
Matthew Heon <mheon@redhat.com>
|
Matthew Heon <mheon@redhat.com>
|
||||||
Matthieu Hauglustaine <matt.hauglustaine@gmail.com>
|
Matthieu Hauglustaine <matt.hauglustaine@gmail.com>
|
||||||
Matthieu MOREL <matthieu.morel35@gmail.com>
|
|
||||||
Mauro Porras P <mauroporrasp@gmail.com>
|
Mauro Porras P <mauroporrasp@gmail.com>
|
||||||
Max Morozov <gtmax.yo@gmail.com>
|
|
||||||
Max Shytikov <mshytikov@gmail.com>
|
Max Shytikov <mshytikov@gmail.com>
|
||||||
Max-Julian Pogner <max-julian@pogner.at>
|
|
||||||
Maxime Petazzoni <max@signalfuse.com>
|
Maxime Petazzoni <max@signalfuse.com>
|
||||||
Maximillian Fan Xavier <maximillianfx@gmail.com>
|
Maximillian Fan Xavier <maximillianfx@gmail.com>
|
||||||
Mei ChunTao <mei.chuntao@zte.com.cn>
|
Mei ChunTao <mei.chuntao@zte.com.cn>
|
||||||
Melroy van den Berg <melroy@melroy.org>
|
|
||||||
Mert Şişmanoğlu <mert190737fb@gmail.com>
|
|
||||||
Metal <2466052+tedhexaflow@users.noreply.github.com>
|
Metal <2466052+tedhexaflow@users.noreply.github.com>
|
||||||
Micah Zoltu <micah@newrelic.com>
|
Micah Zoltu <micah@newrelic.com>
|
||||||
Michael A. Smith <michael@smith-li.com>
|
Michael A. Smith <michael@smith-li.com>
|
||||||
@@ -618,12 +534,9 @@ Michael Prokop <github@michael-prokop.at>
|
|||||||
Michael Scharf <github@scharf.gr>
|
Michael Scharf <github@scharf.gr>
|
||||||
Michael Spetsiotis <michael_spets@hotmail.com>
|
Michael Spetsiotis <michael_spets@hotmail.com>
|
||||||
Michael Steinert <mike.steinert@gmail.com>
|
Michael Steinert <mike.steinert@gmail.com>
|
||||||
Michael Tews <michael@tews.dev>
|
|
||||||
Michael West <mwest@mdsol.com>
|
Michael West <mwest@mdsol.com>
|
||||||
Michael Zampani <michael.zampani@docker.com>
|
|
||||||
Michal Minář <miminar@redhat.com>
|
Michal Minář <miminar@redhat.com>
|
||||||
Michał Czeraszkiewicz <czerasz@gmail.com>
|
Michał Czeraszkiewicz <czerasz@gmail.com>
|
||||||
Mickael Emirkanian <mickael.emirkanian@docker.com>
|
|
||||||
Miguel Angel Alvarez Cabrerizo <doncicuto@gmail.com>
|
Miguel Angel Alvarez Cabrerizo <doncicuto@gmail.com>
|
||||||
Mihai Borobocea <MihaiBorob@gmail.com>
|
Mihai Borobocea <MihaiBorob@gmail.com>
|
||||||
Mihuleacc Sergiu <mihuleac.sergiu@gmail.com>
|
Mihuleacc Sergiu <mihuleac.sergiu@gmail.com>
|
||||||
@@ -636,16 +549,12 @@ Mike Goelzer <mike.goelzer@docker.com>
|
|||||||
Mike MacCana <mike.maccana@gmail.com>
|
Mike MacCana <mike.maccana@gmail.com>
|
||||||
mikelinjie <294893458@qq.com>
|
mikelinjie <294893458@qq.com>
|
||||||
Mikhail Vasin <vasin@cloud-tv.ru>
|
Mikhail Vasin <vasin@cloud-tv.ru>
|
||||||
Milas Bowman <milas.bowman@docker.com>
|
|
||||||
Milind Chawre <milindchawre@gmail.com>
|
Milind Chawre <milindchawre@gmail.com>
|
||||||
Mindaugas Rukas <momomg@gmail.com>
|
Mindaugas Rukas <momomg@gmail.com>
|
||||||
Miroslav Gula <miroslav.gula@naytrolabs.com>
|
Miroslav Gula <miroslav.gula@naytrolabs.com>
|
||||||
Misty Stanley-Jones <misty@docker.com>
|
Misty Stanley-Jones <misty@docker.com>
|
||||||
Mohammad Banikazemi <mb@us.ibm.com>
|
Mohammad Banikazemi <mb@us.ibm.com>
|
||||||
Mohammad Hossein <mhm98035@gmail.com>
|
|
||||||
Mohammed Aaqib Ansari <maaquib@gmail.com>
|
Mohammed Aaqib Ansari <maaquib@gmail.com>
|
||||||
Mohammed Aminu Futa <mohammedfuta2000@gmail.com>
|
|
||||||
Mohammed Thaha <mohammedthahacse@gmail.com>
|
|
||||||
Mohini Anne Dsouza <mohini3917@gmail.com>
|
Mohini Anne Dsouza <mohini3917@gmail.com>
|
||||||
Moorthy RS <rsmoorthy@gmail.com>
|
Moorthy RS <rsmoorthy@gmail.com>
|
||||||
Morgan Bauer <mbauer@us.ibm.com>
|
Morgan Bauer <mbauer@us.ibm.com>
|
||||||
@@ -672,7 +581,6 @@ Nathan McCauley <nathan.mccauley@docker.com>
|
|||||||
Neil Peterson <neilpeterson@outlook.com>
|
Neil Peterson <neilpeterson@outlook.com>
|
||||||
Nick Adcock <nick.adcock@docker.com>
|
Nick Adcock <nick.adcock@docker.com>
|
||||||
Nick Santos <nick.santos@docker.com>
|
Nick Santos <nick.santos@docker.com>
|
||||||
Nick Sieger <nick@nicksieger.com>
|
|
||||||
Nico Stapelbroek <nstapelbroek@gmail.com>
|
Nico Stapelbroek <nstapelbroek@gmail.com>
|
||||||
Nicola Kabar <nicolaka@gmail.com>
|
Nicola Kabar <nicolaka@gmail.com>
|
||||||
Nicolas Borboën <ponsfrilus@gmail.com>
|
Nicolas Borboën <ponsfrilus@gmail.com>
|
||||||
@@ -680,14 +588,11 @@ Nicolas De Loof <nicolas.deloof@gmail.com>
|
|||||||
Nikhil Chawla <chawlanikhil24@gmail.com>
|
Nikhil Chawla <chawlanikhil24@gmail.com>
|
||||||
Nikolas Garofil <nikolas.garofil@uantwerpen.be>
|
Nikolas Garofil <nikolas.garofil@uantwerpen.be>
|
||||||
Nikolay Milovanov <nmil@itransformers.net>
|
Nikolay Milovanov <nmil@itransformers.net>
|
||||||
NinaLua <iturf@sina.cn>
|
|
||||||
Nir Soffer <nsoffer@redhat.com>
|
Nir Soffer <nsoffer@redhat.com>
|
||||||
Nishant Totla <nishanttotla@gmail.com>
|
Nishant Totla <nishanttotla@gmail.com>
|
||||||
NIWA Hideyuki <niwa.niwa@nifty.ne.jp>
|
NIWA Hideyuki <niwa.niwa@nifty.ne.jp>
|
||||||
Noah Silas <noah@hustle.com>
|
|
||||||
Noah Treuhaft <noah.treuhaft@docker.com>
|
Noah Treuhaft <noah.treuhaft@docker.com>
|
||||||
O.S. Tezer <ostezer@gmail.com>
|
O.S. Tezer <ostezer@gmail.com>
|
||||||
Oded Arbel <oded@geek.co.il>
|
|
||||||
Odin Ugedal <odin@ugedal.com>
|
Odin Ugedal <odin@ugedal.com>
|
||||||
ohmystack <jun.jiang02@ele.me>
|
ohmystack <jun.jiang02@ele.me>
|
||||||
OKA Naoya <git@okanaoya.com>
|
OKA Naoya <git@okanaoya.com>
|
||||||
@@ -697,26 +602,21 @@ Olli Janatuinen <olli.janatuinen@gmail.com>
|
|||||||
Oscar Wieman <oscrx@icloud.com>
|
Oscar Wieman <oscrx@icloud.com>
|
||||||
Otto Kekäläinen <otto@seravo.fi>
|
Otto Kekäläinen <otto@seravo.fi>
|
||||||
Ovidio Mallo <ovidio.mallo@gmail.com>
|
Ovidio Mallo <ovidio.mallo@gmail.com>
|
||||||
Park Jaeon <me@finalchild.dev>
|
|
||||||
Pascal Borreli <pascal@borreli.com>
|
Pascal Borreli <pascal@borreli.com>
|
||||||
Patrick Böänziger <patrick.baenziger@bsi-software.com>
|
Patrick Böänziger <patrick.baenziger@bsi-software.com>
|
||||||
Patrick Daigle <114765035+pdaig@users.noreply.github.com>
|
|
||||||
Patrick Hemmer <patrick.hemmer@gmail.com>
|
Patrick Hemmer <patrick.hemmer@gmail.com>
|
||||||
Patrick Lang <plang@microsoft.com>
|
Patrick Lang <plang@microsoft.com>
|
||||||
Patrick St. laurent <patrick@saint-laurent.us>
|
|
||||||
Paul <paul9869@gmail.com>
|
Paul <paul9869@gmail.com>
|
||||||
Paul Kehrer <paul.l.kehrer@gmail.com>
|
Paul Kehrer <paul.l.kehrer@gmail.com>
|
||||||
Paul Lietar <paul@lietar.net>
|
Paul Lietar <paul@lietar.net>
|
||||||
Paul Mulders <justinkb@gmail.com>
|
Paul Mulders <justinkb@gmail.com>
|
||||||
Paul Rogalski <mail@paul-rogalski.de>
|
|
||||||
Paul Seyfert <pseyfert.mathphys@gmail.com>
|
|
||||||
Paul Weaver <pauweave@cisco.com>
|
Paul Weaver <pauweave@cisco.com>
|
||||||
Pavel Pospisil <pospispa@gmail.com>
|
Pavel Pospisil <pospispa@gmail.com>
|
||||||
Paweł Gronowski <pawel.gronowski@docker.com>
|
Paweł Gronowski <pawel.gronowski@docker.com>
|
||||||
Paweł Pokrywka <pepawel@users.noreply.github.com>
|
Paweł Pokrywka <pepawel@users.noreply.github.com>
|
||||||
Paweł Szczekutowicz <pszczekutowicz@gmail.com>
|
Paweł Szczekutowicz <pszczekutowicz@gmail.com>
|
||||||
Peeyush Gupta <gpeeyush@linux.vnet.ibm.com>
|
Peeyush Gupta <gpeeyush@linux.vnet.ibm.com>
|
||||||
Per Lundberg <perlun@gmail.com>
|
Per Lundberg <per.lundberg@ecraft.com>
|
||||||
Peter Dave Hello <hsu@peterdavehello.org>
|
Peter Dave Hello <hsu@peterdavehello.org>
|
||||||
Peter Edge <peter.edge@gmail.com>
|
Peter Edge <peter.edge@gmail.com>
|
||||||
Peter Hsu <shhsu@microsoft.com>
|
Peter Hsu <shhsu@microsoft.com>
|
||||||
@@ -724,13 +624,13 @@ Peter Jaffe <pjaffe@nevo.com>
|
|||||||
Peter Kehl <peter.kehl@gmail.com>
|
Peter Kehl <peter.kehl@gmail.com>
|
||||||
Peter Nagy <xificurC@gmail.com>
|
Peter Nagy <xificurC@gmail.com>
|
||||||
Peter Salvatore <peter@psftw.com>
|
Peter Salvatore <peter@psftw.com>
|
||||||
Peter Valdemar Mørch <peter@morch.com>
|
|
||||||
Peter Waller <p@pwaller.net>
|
Peter Waller <p@pwaller.net>
|
||||||
Phil Estes <estesp@gmail.com>
|
Phil Estes <estesp@gmail.com>
|
||||||
Philip Alexander Etling <paetling@gmail.com>
|
Philip Alexander Etling <paetling@gmail.com>
|
||||||
Philipp Gillé <philipp.gille@gmail.com>
|
Philipp Gillé <philipp.gille@gmail.com>
|
||||||
Philipp Schmied <pschmied@schutzwerk.com>
|
Philipp Schmied <pschmied@schutzwerk.com>
|
||||||
Phong Tran <tran.pho@northeastern.edu>
|
Phong Tran <tran.pho@northeastern.edu>
|
||||||
|
pidster <pid@pidster.com>
|
||||||
Pieter E Smit <diepes@github.com>
|
Pieter E Smit <diepes@github.com>
|
||||||
pixelistik <pixelistik@users.noreply.github.com>
|
pixelistik <pixelistik@users.noreply.github.com>
|
||||||
Pratik Karki <prertik@outlook.com>
|
Pratik Karki <prertik@outlook.com>
|
||||||
@@ -739,7 +639,6 @@ Preston Cowley <preston.cowley@sony.com>
|
|||||||
Pure White <daniel48@126.com>
|
Pure White <daniel48@126.com>
|
||||||
Qiang Huang <h.huangqiang@huawei.com>
|
Qiang Huang <h.huangqiang@huawei.com>
|
||||||
Qinglan Peng <qinglanpeng@zju.edu.cn>
|
Qinglan Peng <qinglanpeng@zju.edu.cn>
|
||||||
QQ喵 <gqqnb2005@gmail.com>
|
|
||||||
qudongfang <qudongfang@gmail.com>
|
qudongfang <qudongfang@gmail.com>
|
||||||
Raghavendra K T <raghavendra.kt@linux.vnet.ibm.com>
|
Raghavendra K T <raghavendra.kt@linux.vnet.ibm.com>
|
||||||
Rahul Kadyan <hi@znck.me>
|
Rahul Kadyan <hi@znck.me>
|
||||||
@@ -749,7 +648,6 @@ Ray Tsang <rayt@google.com>
|
|||||||
Reficul <xuzhenglun@gmail.com>
|
Reficul <xuzhenglun@gmail.com>
|
||||||
Remy Suen <remy.suen@gmail.com>
|
Remy Suen <remy.suen@gmail.com>
|
||||||
Renaud Gaubert <rgaubert@nvidia.com>
|
Renaud Gaubert <rgaubert@nvidia.com>
|
||||||
René Hermenau <rene-hermenau@users.noreply.github.com>
|
|
||||||
Ricardo N Feliciano <FelicianoTech@gmail.com>
|
Ricardo N Feliciano <FelicianoTech@gmail.com>
|
||||||
Rich Moyse <rich@moyse.us>
|
Rich Moyse <rich@moyse.us>
|
||||||
Richard Chen Zheng <58443436+rchenzheng@users.noreply.github.com>
|
Richard Chen Zheng <58443436+rchenzheng@users.noreply.github.com>
|
||||||
@@ -759,7 +657,6 @@ Rick Wieman <git@rickw.nl>
|
|||||||
Ritesh H Shukla <sritesh@vmware.com>
|
Ritesh H Shukla <sritesh@vmware.com>
|
||||||
Riyaz Faizullabhoy <riyaz.faizullabhoy@docker.com>
|
Riyaz Faizullabhoy <riyaz.faizullabhoy@docker.com>
|
||||||
Rob Gulewich <rgulewich@netflix.com>
|
Rob Gulewich <rgulewich@netflix.com>
|
||||||
Rob Murray <rob.murray@docker.com>
|
|
||||||
Robert Wallis <smilingrob@gmail.com>
|
Robert Wallis <smilingrob@gmail.com>
|
||||||
Robin Naundorf <r.naundorf@fh-muenster.de>
|
Robin Naundorf <r.naundorf@fh-muenster.de>
|
||||||
Robin Speekenbrink <robin@kingsquare.nl>
|
Robin Speekenbrink <robin@kingsquare.nl>
|
||||||
@@ -773,7 +670,6 @@ Rory Hunter <roryhunter2@gmail.com>
|
|||||||
Ross Boucher <rboucher@gmail.com>
|
Ross Boucher <rboucher@gmail.com>
|
||||||
Rubens Figueiredo <r.figueiredo.52@gmail.com>
|
Rubens Figueiredo <r.figueiredo.52@gmail.com>
|
||||||
Rui Cao <ruicao@alauda.io>
|
Rui Cao <ruicao@alauda.io>
|
||||||
Rui JingAn <quiterace@gmail.com>
|
|
||||||
Ryan Belgrave <rmb1993@gmail.com>
|
Ryan Belgrave <rmb1993@gmail.com>
|
||||||
Ryan Detzel <ryan.detzel@gmail.com>
|
Ryan Detzel <ryan.detzel@gmail.com>
|
||||||
Ryan Stelly <ryan.stelly@live.com>
|
Ryan Stelly <ryan.stelly@live.com>
|
||||||
@@ -791,16 +687,13 @@ Samuel Cochran <sj26@sj26.com>
|
|||||||
Samuel Karp <skarp@amazon.com>
|
Samuel Karp <skarp@amazon.com>
|
||||||
Sandro Jäckel <sandro.jaeckel@gmail.com>
|
Sandro Jäckel <sandro.jaeckel@gmail.com>
|
||||||
Santhosh Manohar <santhosh@docker.com>
|
Santhosh Manohar <santhosh@docker.com>
|
||||||
Sarah Sanders <sarah.sanders@docker.com>
|
|
||||||
Sargun Dhillon <sargun@netflix.com>
|
Sargun Dhillon <sargun@netflix.com>
|
||||||
Saswat Bhattacharya <sas.saswat@gmail.com>
|
Saswat Bhattacharya <sas.saswat@gmail.com>
|
||||||
Saurabh Kumar <saurabhkumar0184@gmail.com>
|
|
||||||
Scott Brenner <scott@scottbrenner.me>
|
Scott Brenner <scott@scottbrenner.me>
|
||||||
Scott Collier <emailscottcollier@gmail.com>
|
Scott Collier <emailscottcollier@gmail.com>
|
||||||
Sean Christopherson <sean.j.christopherson@intel.com>
|
Sean Christopherson <sean.j.christopherson@intel.com>
|
||||||
Sean Rodman <srodman7689@gmail.com>
|
Sean Rodman <srodman7689@gmail.com>
|
||||||
Sebastiaan van Stijn <github@gone.nl>
|
Sebastiaan van Stijn <github@gone.nl>
|
||||||
Seiya Miyata <odradek38@gmail.com>
|
|
||||||
Sergey Tryuber <Sergeant007@users.noreply.github.com>
|
Sergey Tryuber <Sergeant007@users.noreply.github.com>
|
||||||
Serhat Gülçiçek <serhat25@gmail.com>
|
Serhat Gülçiçek <serhat25@gmail.com>
|
||||||
Sevki Hasirci <s@sevki.org>
|
Sevki Hasirci <s@sevki.org>
|
||||||
@@ -825,7 +718,6 @@ Spencer Brown <spencer@spencerbrown.org>
|
|||||||
Spring Lee <xi.shuai@outlook.com>
|
Spring Lee <xi.shuai@outlook.com>
|
||||||
squeegels <lmscrewy@gmail.com>
|
squeegels <lmscrewy@gmail.com>
|
||||||
Srini Brahmaroutu <srbrahma@us.ibm.com>
|
Srini Brahmaroutu <srbrahma@us.ibm.com>
|
||||||
Stavros Panakakis <stavrospanakakis@gmail.com>
|
|
||||||
Stefan S. <tronicum@user.github.com>
|
Stefan S. <tronicum@user.github.com>
|
||||||
Stefan Scherer <stefan.scherer@docker.com>
|
Stefan Scherer <stefan.scherer@docker.com>
|
||||||
Stefan Weil <sw@weilnetz.de>
|
Stefan Weil <sw@weilnetz.de>
|
||||||
@@ -836,7 +728,6 @@ Steve Durrheimer <s.durrheimer@gmail.com>
|
|||||||
Steve Richards <steve.richards@docker.com>
|
Steve Richards <steve.richards@docker.com>
|
||||||
Steven Burgess <steven.a.burgess@hotmail.com>
|
Steven Burgess <steven.a.burgess@hotmail.com>
|
||||||
Stoica-Marcu Floris-Andrei <floris.sm@gmail.com>
|
Stoica-Marcu Floris-Andrei <floris.sm@gmail.com>
|
||||||
Stuart Williams <pid@pidster.com>
|
|
||||||
Subhajit Ghosh <isubuz.g@gmail.com>
|
Subhajit Ghosh <isubuz.g@gmail.com>
|
||||||
Sun Jianbo <wonderflow.sun@gmail.com>
|
Sun Jianbo <wonderflow.sun@gmail.com>
|
||||||
Sune Keller <absukl@almbrand.dk>
|
Sune Keller <absukl@almbrand.dk>
|
||||||
@@ -871,7 +762,6 @@ Tim Hockin <thockin@google.com>
|
|||||||
Tim Sampson <tim@sampson.fi>
|
Tim Sampson <tim@sampson.fi>
|
||||||
Tim Smith <timbot@google.com>
|
Tim Smith <timbot@google.com>
|
||||||
Tim Waugh <twaugh@redhat.com>
|
Tim Waugh <twaugh@redhat.com>
|
||||||
Tim Welsh <timothy.welsh@docker.com>
|
|
||||||
Tim Wraight <tim.wraight@tangentlabs.co.uk>
|
Tim Wraight <tim.wraight@tangentlabs.co.uk>
|
||||||
timfeirg <kkcocogogo@gmail.com>
|
timfeirg <kkcocogogo@gmail.com>
|
||||||
Timothy Hobbs <timothyhobbs@seznam.cz>
|
Timothy Hobbs <timothyhobbs@seznam.cz>
|
||||||
@@ -898,23 +788,18 @@ uhayate <uhayate.gong@daocloud.io>
|
|||||||
Ulrich Bareth <ulrich.bareth@gmail.com>
|
Ulrich Bareth <ulrich.bareth@gmail.com>
|
||||||
Ulysses Souza <ulysses.souza@docker.com>
|
Ulysses Souza <ulysses.souza@docker.com>
|
||||||
Umesh Yadav <umesh4257@gmail.com>
|
Umesh Yadav <umesh4257@gmail.com>
|
||||||
Vaclav Struhar <struharv@gmail.com>
|
|
||||||
Valentin Lorentz <progval+git@progval.net>
|
Valentin Lorentz <progval+git@progval.net>
|
||||||
Vardan Pogosian <vardan.pogosyan@gmail.com>
|
Vardan Pogosian <vardan.pogosyan@gmail.com>
|
||||||
Varun Hotani <varunhotani@gmail.com>
|
|
||||||
Venkateswara Reddy Bukkasamudram <bukkasamudram@outlook.com>
|
Venkateswara Reddy Bukkasamudram <bukkasamudram@outlook.com>
|
||||||
Veres Lajos <vlajos@gmail.com>
|
Veres Lajos <vlajos@gmail.com>
|
||||||
Vibhu Anan <vibhuanand@outlook.com>
|
|
||||||
Victor Vieux <victor.vieux@docker.com>
|
Victor Vieux <victor.vieux@docker.com>
|
||||||
Victoria Bialas <victoria.bialas@docker.com>
|
Victoria Bialas <victoria.bialas@docker.com>
|
||||||
Viktor Stanchev <me@viktorstanchev.com>
|
Viktor Stanchev <me@viktorstanchev.com>
|
||||||
Ville Skyttä <ville.skytta@iki.fi>
|
|
||||||
Vimal Raghubir <vraghubir0418@gmail.com>
|
Vimal Raghubir <vraghubir0418@gmail.com>
|
||||||
Vincent Batts <vbatts@redhat.com>
|
Vincent Batts <vbatts@redhat.com>
|
||||||
Vincent Bernat <Vincent.Bernat@exoscale.ch>
|
Vincent Bernat <Vincent.Bernat@exoscale.ch>
|
||||||
Vincent Demeester <vincent.demeester@docker.com>
|
Vincent Demeester <vincent.demeester@docker.com>
|
||||||
Vincent Woo <me@vincentwoo.com>
|
Vincent Woo <me@vincentwoo.com>
|
||||||
Vineet Kumar <vineetkumar17112004@gmail.com>
|
|
||||||
Vishnu Kannan <vishnuk@google.com>
|
Vishnu Kannan <vishnuk@google.com>
|
||||||
Vivek Goyal <vgoyal@redhat.com>
|
Vivek Goyal <vgoyal@redhat.com>
|
||||||
Wang Jie <wangjie5@chinaskycloud.com>
|
Wang Jie <wangjie5@chinaskycloud.com>
|
||||||
@@ -927,11 +812,9 @@ Wang Yumu <37442693@qq.com>
|
|||||||
Wataru Ishida <ishida.wataru@lab.ntt.co.jp>
|
Wataru Ishida <ishida.wataru@lab.ntt.co.jp>
|
||||||
Wayne Song <wsong@docker.com>
|
Wayne Song <wsong@docker.com>
|
||||||
Wen Cheng Ma <wenchma@cn.ibm.com>
|
Wen Cheng Ma <wenchma@cn.ibm.com>
|
||||||
Wenlong Zhang <zhangwenlong@loongson.cn>
|
|
||||||
Wenzhi Liang <wenzhi.liang@gmail.com>
|
Wenzhi Liang <wenzhi.liang@gmail.com>
|
||||||
Wes Morgan <cap10morgan@gmail.com>
|
Wes Morgan <cap10morgan@gmail.com>
|
||||||
Wewang Xiaorenfine <wang.xiaoren@zte.com.cn>
|
Wewang Xiaorenfine <wang.xiaoren@zte.com.cn>
|
||||||
Will Wang <willww64@gmail.com>
|
|
||||||
William Henry <whenry@redhat.com>
|
William Henry <whenry@redhat.com>
|
||||||
Xianglin Gao <xlgao@zju.edu.cn>
|
Xianglin Gao <xlgao@zju.edu.cn>
|
||||||
Xiaodong Liu <liuxiaodong@loongson.cn>
|
Xiaodong Liu <liuxiaodong@loongson.cn>
|
||||||
@@ -944,12 +827,10 @@ Yanqiang Miao <miao.yanqiang@zte.com.cn>
|
|||||||
Yassine Tijani <yasstij11@gmail.com>
|
Yassine Tijani <yasstij11@gmail.com>
|
||||||
Yi EungJun <eungjun.yi@navercorp.com>
|
Yi EungJun <eungjun.yi@navercorp.com>
|
||||||
Ying Li <ying.li@docker.com>
|
Ying Li <ying.li@docker.com>
|
||||||
Yoan Wainmann <thebook90yw@gmail.com>
|
|
||||||
Yong Tang <yong.tang.github@outlook.com>
|
Yong Tang <yong.tang.github@outlook.com>
|
||||||
Yosef Fertel <yfertel@gmail.com>
|
Yosef Fertel <yfertel@gmail.com>
|
||||||
Yu Peng <yu.peng36@zte.com.cn>
|
Yu Peng <yu.peng36@zte.com.cn>
|
||||||
Yuan Sun <sunyuan3@huawei.com>
|
Yuan Sun <sunyuan3@huawei.com>
|
||||||
Yucheng Wu <wyc123wyc@gmail.com>
|
|
||||||
Yue Zhang <zy675793960@yeah.net>
|
Yue Zhang <zy675793960@yeah.net>
|
||||||
Yunxiang Huang <hyxqshk@vip.qq.com>
|
Yunxiang Huang <hyxqshk@vip.qq.com>
|
||||||
Zachary Romero <zacromero3@gmail.com>
|
Zachary Romero <zacromero3@gmail.com>
|
||||||
@@ -961,14 +842,11 @@ Zhang Wei <zhangwei555@huawei.com>
|
|||||||
Zhang Wentao <zhangwentao234@huawei.com>
|
Zhang Wentao <zhangwentao234@huawei.com>
|
||||||
ZhangHang <stevezhang2014@gmail.com>
|
ZhangHang <stevezhang2014@gmail.com>
|
||||||
zhenghenghuo <zhenghenghuo@zju.edu.cn>
|
zhenghenghuo <zhenghenghuo@zju.edu.cn>
|
||||||
Zhiwei Liang <zliang@akamai.com>
|
|
||||||
Zhou Hao <zhouhao@cn.fujitsu.com>
|
Zhou Hao <zhouhao@cn.fujitsu.com>
|
||||||
Zhoulin Xie <zhoulin.xie@daocloud.io>
|
Zhoulin Xie <zhoulin.xie@daocloud.io>
|
||||||
Zhu Guihua <zhugh.fnst@cn.fujitsu.com>
|
Zhu Guihua <zhugh.fnst@cn.fujitsu.com>
|
||||||
Zhuo Zhi <h.dwwwwww@gmail.com>
|
|
||||||
Álex González <agonzalezro@gmail.com>
|
Álex González <agonzalezro@gmail.com>
|
||||||
Álvaro Lázaro <alvaro.lazaro.g@gmail.com>
|
Álvaro Lázaro <alvaro.lazaro.g@gmail.com>
|
||||||
Átila Camurça Alves <camurca.home@gmail.com>
|
Átila Camurça Alves <camurca.home@gmail.com>
|
||||||
Александр Менщиков <__Singleton__@hackerdom.ru>
|
Александр Менщиков <__Singleton__@hackerdom.ru>
|
||||||
徐俊杰 <paco.xu@daocloud.io>
|
徐俊杰 <paco.xu@daocloud.io>
|
||||||
林博仁 Buo-ren Lin <Buo.Ren.Lin@gmail.com>
|
|
||||||
|
|||||||
+25
-55
@@ -1,5 +1,9 @@
|
|||||||
# Contributing to Docker
|
# Contributing to Docker
|
||||||
|
|
||||||
|
Want to hack on Docker? Awesome! We have a contributor's guide that explains
|
||||||
|
[setting up a Docker development environment and the contribution
|
||||||
|
process](https://docs.docker.com/opensource/project/who-written-for/).
|
||||||
|
|
||||||
This page contains information about reporting issues as well as some tips and
|
This page contains information about reporting issues as well as some tips and
|
||||||
guidelines useful to experienced open source contributors. Finally, make sure
|
guidelines useful to experienced open source contributors. Finally, make sure
|
||||||
you read our [community guidelines](#docker-community-guidelines) before you
|
you read our [community guidelines](#docker-community-guidelines) before you
|
||||||
@@ -16,9 +20,9 @@ start participating.
|
|||||||
## Reporting security issues
|
## Reporting security issues
|
||||||
|
|
||||||
The Docker maintainers take security seriously. If you discover a security
|
The Docker maintainers take security seriously. If you discover a security
|
||||||
issue, bring it to their attention right away!
|
issue, please bring it to their attention right away!
|
||||||
|
|
||||||
**DO NOT** file a public issue, instead send your report privately to
|
Please **DO NOT** file a public issue, instead send your report privately to
|
||||||
[security@docker.com](mailto:security@docker.com).
|
[security@docker.com](mailto:security@docker.com).
|
||||||
|
|
||||||
Security reports are greatly appreciated and we will publicly thank you for it.
|
Security reports are greatly appreciated and we will publicly thank you for it.
|
||||||
@@ -39,7 +43,7 @@ If you find a match, you can use the "subscribe" button to get notified on
|
|||||||
updates. Do *not* leave random "+1" or "I have this too" comments, as they
|
updates. Do *not* leave random "+1" or "I have this too" comments, as they
|
||||||
only clutter the discussion, and don't help resolving it. However, if you
|
only clutter the discussion, and don't help resolving it. However, if you
|
||||||
have ways to reproduce the issue or have additional information that may help
|
have ways to reproduce the issue or have additional information that may help
|
||||||
resolving the issue, leave a comment.
|
resolving the issue, please leave a comment.
|
||||||
|
|
||||||
When reporting issues, always include:
|
When reporting issues, always include:
|
||||||
|
|
||||||
@@ -66,7 +70,7 @@ anybody starts working on it.
|
|||||||
We are always thrilled to receive pull requests. We do our best to process them
|
We are always thrilled to receive pull requests. We do our best to process them
|
||||||
quickly. If your pull request is not accepted on the first try,
|
quickly. If your pull request is not accepted on the first try,
|
||||||
don't get discouraged! Our contributor's guide explains [the review process we
|
don't get discouraged! Our contributor's guide explains [the review process we
|
||||||
use for simple changes](https://github.com/moby/moby/blob/master/project/REVIEWING.md).
|
use for simple changes](https://docs.docker.com/opensource/workflow/make-a-contribution/).
|
||||||
|
|
||||||
### Talking to other Docker users and contributors
|
### Talking to other Docker users and contributors
|
||||||
|
|
||||||
@@ -84,7 +88,7 @@ use for simple changes](https://github.com/moby/moby/blob/master/project/REVIEWI
|
|||||||
<tr>
|
<tr>
|
||||||
<td>Community Slack</td>
|
<td>Community Slack</td>
|
||||||
<td>
|
<td>
|
||||||
The Docker Community has a dedicated Slack chat to discuss features and issues. You can sign-up <a href="https://dockr.ly/comm-slack" target="_blank">with this link</a>.
|
The Docker Community has a dedicated Slack chat to discuss features and issues. You can sign-up <a href="https://dockr.ly/slack" target="_blank">with this link</a>.
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -124,7 +128,8 @@ submitting a pull request.
|
|||||||
Update the documentation when creating or modifying features. Test your
|
Update the documentation when creating or modifying features. Test your
|
||||||
documentation changes for clarity, concision, and correctness, as well as a
|
documentation changes for clarity, concision, and correctness, as well as a
|
||||||
clean documentation build. See our contributors guide for [our style
|
clean documentation build. See our contributors guide for [our style
|
||||||
guide](https://github.com/docker/docs/blob/main/STYLE.md).
|
guide](https://docs.docker.com/opensource/doc-style) and instructions on [building
|
||||||
|
the documentation](https://docs.docker.com/opensource/project/test-and-docs/#build-and-test-the-documentation).
|
||||||
|
|
||||||
Write clean code. Universally formatted code promotes ease of writing, reading,
|
Write clean code. Universally formatted code promotes ease of writing, reading,
|
||||||
and maintenance. Always run `gofmt -s -w file.go` on each changed file before
|
and maintenance. Always run `gofmt -s -w file.go` on each changed file before
|
||||||
@@ -133,45 +138,9 @@ committing your changes. Most editors have plug-ins that do this automatically.
|
|||||||
Pull request descriptions should be as clear as possible and include a reference
|
Pull request descriptions should be as clear as possible and include a reference
|
||||||
to all the issues that they address.
|
to all the issues that they address.
|
||||||
|
|
||||||
Commit messages must be written in the imperative mood (max. 72 chars), followed
|
Commit messages must start with a capitalized and short summary (max. 50 chars)
|
||||||
by an optional, more detailed explanatory text usually expanding on
|
written in the imperative, followed by an optional, more detailed explanatory
|
||||||
why the work is necessary. The explanatory text should be separated by an
|
text which is separated from the summary by an empty line.
|
||||||
empty line.
|
|
||||||
|
|
||||||
The commit message *could* have a prefix scoping the change, however this is
|
|
||||||
not enforced. Common prefixes are `docs: <message>`, `vendor: <message>`,
|
|
||||||
`chore: <message>` or the package/area related to the change such as `pkg/foo: <message>`
|
|
||||||
or `telemetry: <message>`.
|
|
||||||
|
|
||||||
A standard commit.
|
|
||||||
|
|
||||||
```
|
|
||||||
Fix the exploding flux capacitor
|
|
||||||
|
|
||||||
A call to function A causes the flux capacitor to blow up every time
|
|
||||||
the sun and the moon align.
|
|
||||||
```
|
|
||||||
|
|
||||||
Using a package as prefix.
|
|
||||||
|
|
||||||
```
|
|
||||||
pkg/foo: prevent panic in flux capacitor
|
|
||||||
|
|
||||||
Calling function A causes the flux capacitor to blow up every time
|
|
||||||
the sun and the moon align.
|
|
||||||
```
|
|
||||||
|
|
||||||
Updating a specific vendored dependency.
|
|
||||||
|
|
||||||
```
|
|
||||||
vendor: github.com/moby/moby/client v0.4.0
|
|
||||||
```
|
|
||||||
|
|
||||||
Fixing a broken docs link.
|
|
||||||
|
|
||||||
```
|
|
||||||
docs: fix style/lint issues in deprecated.md
|
|
||||||
```
|
|
||||||
|
|
||||||
Code review comments may be added to your pull request. Discuss, then make the
|
Code review comments may be added to your pull request. Discuss, then make the
|
||||||
suggested modifications and push additional commits to your feature branch. Post
|
suggested modifications and push additional commits to your feature branch. Post
|
||||||
@@ -201,10 +170,10 @@ Include an issue reference like `Closes #XXXX` or `Fixes #XXXX` in the pull requ
|
|||||||
description that close an issue. Including references automatically closes the issue
|
description that close an issue. Including references automatically closes the issue
|
||||||
on a merge.
|
on a merge.
|
||||||
|
|
||||||
Do not add yourself to the `AUTHORS` file, as it is regenerated regularly
|
Please do not add yourself to the `AUTHORS` file, as it is regenerated regularly
|
||||||
from the Git history.
|
from the Git history.
|
||||||
|
|
||||||
See the [Coding Style](#coding-style) for further guidelines.
|
Please see the [Coding Style](#coding-style) for further guidelines.
|
||||||
|
|
||||||
### Merge approval
|
### Merge approval
|
||||||
|
|
||||||
@@ -223,7 +192,7 @@ For more details, see the [MAINTAINERS](MAINTAINERS) page.
|
|||||||
The sign-off is a simple line at the end of the explanation for the patch. Your
|
The sign-off is a simple line at the end of the explanation for the patch. Your
|
||||||
signature certifies that you wrote the patch or otherwise have the right to pass
|
signature certifies that you wrote the patch or otherwise have the right to pass
|
||||||
it on as an open-source patch. The rules are pretty simple: if you can certify
|
it on as an open-source patch. The rules are pretty simple: if you can certify
|
||||||
the below (from [developercertificate.org](https://developercertificate.org):
|
the below (from [developercertificate.org](http://developercertificate.org/)):
|
||||||
|
|
||||||
```
|
```
|
||||||
Developer Certificate of Origin
|
Developer Certificate of Origin
|
||||||
@@ -267,7 +236,7 @@ Then you just add a line to every git commit message:
|
|||||||
|
|
||||||
Signed-off-by: Joe Smith <joe.smith@email.com>
|
Signed-off-by: Joe Smith <joe.smith@email.com>
|
||||||
|
|
||||||
Use a known identity (sorry, no anonymous contributions.)
|
Use your real name (sorry, no pseudonyms or anonymous contributions.)
|
||||||
|
|
||||||
If you set your `user.name` and `user.email` git configs, you can sign your
|
If you set your `user.name` and `user.email` git configs, you can sign your
|
||||||
commit automatically with `git commit -s`.
|
commit automatically with `git commit -s`.
|
||||||
@@ -304,8 +273,8 @@ guidelines for the community as a whole:
|
|||||||
|
|
||||||
* Stay on topic: Make sure that you are posting to the correct channel and
|
* Stay on topic: Make sure that you are posting to the correct channel and
|
||||||
avoid off-topic discussions. Remember when you update an issue or respond
|
avoid off-topic discussions. Remember when you update an issue or respond
|
||||||
to an email you are potentially sending to a large number of people. Consider
|
to an email you are potentially sending to a large number of people. Please
|
||||||
this before you update. Also remember that nobody likes spam.
|
consider this before you update. Also remember that nobody likes spam.
|
||||||
|
|
||||||
* Don't send email to the maintainers: There's no need to send email to the
|
* Don't send email to the maintainers: There's no need to send email to the
|
||||||
maintainers to ask them to investigate an issue or to take a look at a
|
maintainers to ask them to investigate an issue or to take a look at a
|
||||||
@@ -367,8 +336,9 @@ The rules:
|
|||||||
1. All code should be formatted with `gofumpt` (preferred) or `gofmt -s`.
|
1. All code should be formatted with `gofumpt` (preferred) or `gofmt -s`.
|
||||||
2. All code should pass the default levels of
|
2. All code should pass the default levels of
|
||||||
[`golint`](https://github.com/golang/lint).
|
[`golint`](https://github.com/golang/lint).
|
||||||
3. All code should follow the guidelines covered in [Effective Go](https://go.dev/doc/effective_go)
|
3. All code should follow the guidelines covered in [Effective
|
||||||
and [Go Code Review Comments](https://github.com/golang/go/wiki/CodeReviewComments).
|
Go](http://golang.org/doc/effective_go.html) and [Go Code Review
|
||||||
|
Comments](https://github.com/golang/go/wiki/CodeReviewComments).
|
||||||
4. Comment the code. Tell us the why, the history and the context.
|
4. Comment the code. Tell us the why, the history and the context.
|
||||||
5. Document _all_ declarations and methods, even private ones. Declare
|
5. Document _all_ declarations and methods, even private ones. Declare
|
||||||
expectations, caveats and anything else that may be important. If a type
|
expectations, caveats and anything else that may be important. If a type
|
||||||
@@ -390,6 +360,6 @@ The rules:
|
|||||||
guidelines. Since you've read all the rules, you now know that.
|
guidelines. Since you've read all the rules, you now know that.
|
||||||
|
|
||||||
If you are having trouble getting into the mood of idiomatic Go, we recommend
|
If you are having trouble getting into the mood of idiomatic Go, we recommend
|
||||||
reading through [Effective Go](https://go.dev/doc/effective_go). The
|
reading through [Effective Go](https://golang.org/doc/effective_go.html). The
|
||||||
[Go Blog](https://go.dev/blog/) is also a great resource. Drinking the
|
[Go Blog](https://blog.golang.org) is also a great resource. Drinking the
|
||||||
kool-aid is a lot easier than going thirsty.
|
kool-aid is a lot easier than going thirsty.
|
||||||
|
|||||||
+33
-52
@@ -1,43 +1,19 @@
|
|||||||
# syntax=docker/dockerfile:1
|
# syntax=docker/dockerfile:1
|
||||||
|
|
||||||
ARG BASE_VARIANT=alpine
|
ARG BASE_VARIANT=alpine
|
||||||
|
ARG GO_VERSION=1.22.6
|
||||||
# ALPINE_VERSION sets the version of the alpine base image to use, including for the golang image.
|
ARG ALPINE_VERSION=3.20
|
||||||
# It must be a supported tag in the docker.io/library/alpine image repository
|
ARG XX_VERSION=1.6.1
|
||||||
# that's also available as alpine image variant for the Golang version used.
|
ARG GOVERSIONINFO_VERSION=v1.3.0
|
||||||
ARG ALPINE_VERSION=3.23
|
ARG GOTESTSUM_VERSION=v1.10.0
|
||||||
ARG BASE_DEBIAN_DISTRO=bookworm
|
ARG BUILDX_VERSION=0.11.2
|
||||||
|
ARG COMPOSE_VERSION=v2.22.0
|
||||||
ARG GO_VERSION=1.26.8
|
|
||||||
|
|
||||||
# XX_VERSION specifies the version of the xx utility to use.
|
|
||||||
# It must be a valid tag in the docker.io/tonistiigi/xx image repository.
|
|
||||||
ARG XX_VERSION=1.9.0
|
|
||||||
|
|
||||||
# GOVERSIONINFO_VERSION is the version of GoVersionInfo to install.
|
|
||||||
# It must be a valid tag from https://github.com/josephspurrier/goversioninfo
|
|
||||||
ARG GOVERSIONINFO_VERSION=v1.5.0
|
|
||||||
|
|
||||||
# GOTESTSUM_VERSION sets the version of gotestsum to install in the dev container.
|
|
||||||
# It must be a valid tag in the https://github.com/gotestyourself/gotestsum repository.
|
|
||||||
ARG GOTESTSUM_VERSION=v1.13.0
|
|
||||||
|
|
||||||
# BUILDX_VERSION sets the version of buildx to use for the e2e tests.
|
|
||||||
# It must be a tag in the docker.io/docker/buildx-bin image repository
|
|
||||||
# on Docker Hub.
|
|
||||||
ARG BUILDX_VERSION=0.34.1
|
|
||||||
|
|
||||||
# COMPOSE_VERSION is the version of compose to install in the dev container.
|
|
||||||
# It must be a tag in the docker.io/docker/compose-bin image repository
|
|
||||||
# on Docker Hub.
|
|
||||||
ARG COMPOSE_VERSION=v5.1.3
|
|
||||||
|
|
||||||
FROM --platform=$BUILDPLATFORM tonistiigi/xx:${XX_VERSION} AS xx
|
FROM --platform=$BUILDPLATFORM tonistiigi/xx:${XX_VERSION} AS xx
|
||||||
|
|
||||||
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS build-base-alpine
|
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS build-base-alpine
|
||||||
ENV GOTOOLCHAIN=local
|
|
||||||
COPY --link --from=xx / /
|
COPY --link --from=xx / /
|
||||||
RUN apk add --no-cache bash clang lld llvm file git git-daemon
|
RUN apk add --no-cache bash clang lld llvm file git
|
||||||
WORKDIR /go/src/github.com/docker/cli
|
WORKDIR /go/src/github.com/docker/cli
|
||||||
|
|
||||||
FROM build-base-alpine AS build-alpine
|
FROM build-base-alpine AS build-alpine
|
||||||
@@ -45,15 +21,21 @@ ARG TARGETPLATFORM
|
|||||||
# gcc is installed for libgcc only
|
# gcc is installed for libgcc only
|
||||||
RUN xx-apk add --no-cache musl-dev gcc
|
RUN xx-apk add --no-cache musl-dev gcc
|
||||||
|
|
||||||
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-${BASE_DEBIAN_DISTRO} AS build-base-debian
|
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-bullseye AS build-base-bullseye
|
||||||
ENV GOTOOLCHAIN=local
|
|
||||||
COPY --link --from=xx / /
|
COPY --link --from=xx / /
|
||||||
RUN apt-get update && apt-get install --no-install-recommends -y bash clang lld llvm file
|
RUN apt-get update && apt-get install --no-install-recommends -y bash clang lld llvm file
|
||||||
WORKDIR /go/src/github.com/docker/cli
|
WORKDIR /go/src/github.com/docker/cli
|
||||||
|
|
||||||
FROM build-base-debian AS build-debian
|
FROM build-base-bullseye AS build-bullseye
|
||||||
ARG TARGETPLATFORM
|
ARG TARGETPLATFORM
|
||||||
RUN xx-apt-get install --no-install-recommends -y libc6-dev libgcc-12-dev pkgconf
|
RUN xx-apt-get install --no-install-recommends -y libc6-dev libgcc-10-dev
|
||||||
|
# workaround for issue with llvm 11 for darwin/amd64 platform:
|
||||||
|
# # github.com/docker/cli/cmd/docker
|
||||||
|
# /usr/local/go/pkg/tool/linux_amd64/link: /usr/local/go/pkg/tool/linux_amd64/link: running strip failed: exit status 1
|
||||||
|
# llvm-strip: error: unsupported load command (cmd=0x5)
|
||||||
|
# more info: https://github.com/docker/cli/pull/3717
|
||||||
|
# FIXME: remove once llvm 12 available on debian
|
||||||
|
RUN [ "$TARGETPLATFORM" != "darwin/amd64" ] || ln -sfnT /bin/true /usr/bin/llvm-strip
|
||||||
|
|
||||||
FROM build-base-${BASE_VARIANT} AS goversioninfo
|
FROM build-base-${BASE_VARIANT} AS goversioninfo
|
||||||
ARG GOVERSIONINFO_VERSION
|
ARG GOVERSIONINFO_VERSION
|
||||||
@@ -82,9 +64,12 @@ ARG VERSION
|
|||||||
# PACKAGER_NAME sets the company that produced the windows binary
|
# PACKAGER_NAME sets the company that produced the windows binary
|
||||||
ARG PACKAGER_NAME
|
ARG PACKAGER_NAME
|
||||||
COPY --link --from=goversioninfo /out/goversioninfo /usr/bin/goversioninfo
|
COPY --link --from=goversioninfo /out/goversioninfo /usr/bin/goversioninfo
|
||||||
|
# in bullseye arm64 target does not link with lld so configure it to use ld instead
|
||||||
|
RUN [ ! -f /etc/alpine-release ] && xx-info is-cross && [ "$(xx-info arch)" = "arm64" ] && XX_CC_PREFER_LINKER=ld xx-clang --setup-target-triple || true
|
||||||
RUN --mount=type=bind,target=.,ro \
|
RUN --mount=type=bind,target=.,ro \
|
||||||
--mount=type=cache,target=/root/.cache \
|
--mount=type=cache,target=/root/.cache \
|
||||||
--mount=type=tmpfs,target=cmd/docker/winresources \
|
--mount=from=dockercore/golang-cross:xx-sdk-extras,target=/xx-sdk,src=/xx-sdk \
|
||||||
|
--mount=type=tmpfs,target=cli/winresources \
|
||||||
# override the default behavior of go with xx-go
|
# override the default behavior of go with xx-go
|
||||||
xx-go --wrap && \
|
xx-go --wrap && \
|
||||||
# export GOCACHE=$(go env GOCACHE)/$(xx-info)$([ -f /etc/alpine-release ] && echo "alpine") && \
|
# export GOCACHE=$(go env GOCACHE)/$(xx-info)$([ -f /etc/alpine-release ] && echo "alpine") && \
|
||||||
@@ -97,7 +82,7 @@ ENV GO111MODULE=auto
|
|||||||
RUN --mount=type=bind,target=.,rw \
|
RUN --mount=type=bind,target=.,rw \
|
||||||
--mount=type=cache,target=/root/.cache \
|
--mount=type=cache,target=/root/.cache \
|
||||||
--mount=type=cache,target=/go/pkg/mod \
|
--mount=type=cache,target=/go/pkg/mod \
|
||||||
gotestsum -- -coverprofile=/tmp/coverage.txt $(go list ./... | grep -vE '/vendor/|/e2e/|/cmd/docker-trust')
|
gotestsum -- -coverprofile=/tmp/coverage.txt $(go list ./... | grep -vE '/vendor/|/e2e/')
|
||||||
|
|
||||||
FROM scratch AS test-coverage
|
FROM scratch AS test-coverage
|
||||||
COPY --from=test /tmp/coverage.txt /coverage.txt
|
COPY --from=test /tmp/coverage.txt /coverage.txt
|
||||||
@@ -109,19 +94,24 @@ ARG GO_STRIP
|
|||||||
ARG CGO_ENABLED
|
ARG CGO_ENABLED
|
||||||
ARG VERSION
|
ARG VERSION
|
||||||
RUN --mount=ro --mount=type=cache,target=/root/.cache \
|
RUN --mount=ro --mount=type=cache,target=/root/.cache \
|
||||||
|
--mount=from=dockercore/golang-cross:xx-sdk-extras,target=/xx-sdk,src=/xx-sdk \
|
||||||
xx-go --wrap && \
|
xx-go --wrap && \
|
||||||
TARGET=/out ./scripts/build/plugins e2e/cli-plugins/plugins/*
|
TARGET=/out ./scripts/build/plugins e2e/cli-plugins/plugins/*
|
||||||
|
|
||||||
FROM build-base-alpine AS e2e-base-alpine
|
FROM build-base-alpine AS e2e-base-alpine
|
||||||
RUN apk add --no-cache build-base curl openssl openssh-client
|
RUN apk add --no-cache build-base curl openssl openssh-client
|
||||||
|
|
||||||
FROM build-base-debian AS e2e-base-debian
|
FROM build-base-bullseye AS e2e-base-bullseye
|
||||||
RUN apt-get update && apt-get install -y build-essential curl openssl openssh-client
|
RUN apt-get update && apt-get install -y build-essential curl openssl openssh-client
|
||||||
|
|
||||||
FROM docker/buildx-bin:${BUILDX_VERSION} AS buildx
|
FROM docker/buildx-bin:${BUILDX_VERSION} AS buildx
|
||||||
FROM docker/compose-bin:${COMPOSE_VERSION} AS compose
|
FROM docker/compose-bin:${COMPOSE_VERSION} AS compose
|
||||||
|
|
||||||
FROM e2e-base-${BASE_VARIANT} AS e2e
|
FROM e2e-base-${BASE_VARIANT} AS e2e
|
||||||
|
ARG NOTARY_VERSION=v0.6.1
|
||||||
|
ADD --chmod=0755 https://github.com/theupdateframework/notary/releases/download/${NOTARY_VERSION}/notary-Linux-amd64 /usr/local/bin/notary
|
||||||
|
COPY --link e2e/testdata/notary/root-ca.cert /usr/share/ca-certificates/notary.cert
|
||||||
|
RUN echo 'notary.cert' >> /etc/ca-certificates.conf && update-ca-certificates
|
||||||
COPY --link --from=gotestsum /out/gotestsum /usr/bin/gotestsum
|
COPY --link --from=gotestsum /out/gotestsum /usr/bin/gotestsum
|
||||||
COPY --link --from=build /out ./build/
|
COPY --link --from=build /out ./build/
|
||||||
COPY --link --from=build-plugins /out ./build/
|
COPY --link --from=build-plugins /out ./build/
|
||||||
@@ -130,22 +120,13 @@ COPY --link --from=compose /docker-compose /usr/libexec/docker/cli-plugins/docke
|
|||||||
COPY --link . .
|
COPY --link . .
|
||||||
ENV DOCKER_BUILDKIT=1
|
ENV DOCKER_BUILDKIT=1
|
||||||
ENV PATH=/go/src/github.com/docker/cli/build:$PATH
|
ENV PATH=/go/src/github.com/docker/cli/build:$PATH
|
||||||
CMD ["./scripts/test/e2e/entry"]
|
CMD ./scripts/test/e2e/entry
|
||||||
|
|
||||||
FROM build-base-${BASE_VARIANT} AS dev
|
FROM build-base-${BASE_VARIANT} AS dev
|
||||||
COPY --link . .
|
COPY --link . .
|
||||||
|
|
||||||
FROM scratch AS plugins
|
|
||||||
COPY --from=build-plugins /out .
|
|
||||||
|
|
||||||
FROM scratch AS bin-image-linux
|
|
||||||
COPY --from=build /out/docker /docker
|
|
||||||
FROM scratch AS bin-image-darwin
|
|
||||||
COPY --from=build /out/docker /docker
|
|
||||||
FROM scratch AS bin-image-windows
|
|
||||||
COPY --from=build /out/docker /docker.exe
|
|
||||||
|
|
||||||
FROM bin-image-${TARGETOS} AS bin-image
|
|
||||||
|
|
||||||
FROM scratch AS binary
|
FROM scratch AS binary
|
||||||
COPY --from=build /out .
|
COPY --from=build /out .
|
||||||
|
|
||||||
|
FROM scratch AS plugins
|
||||||
|
COPY --from=build-plugins /out .
|
||||||
|
|||||||
+6
-5
@@ -24,6 +24,7 @@
|
|||||||
people = [
|
people = [
|
||||||
"albers",
|
"albers",
|
||||||
"cpuguy83",
|
"cpuguy83",
|
||||||
|
"ndeloof",
|
||||||
"rumpl",
|
"rumpl",
|
||||||
"silvin-lubecki",
|
"silvin-lubecki",
|
||||||
"stevvooe",
|
"stevvooe",
|
||||||
@@ -48,9 +49,9 @@
|
|||||||
|
|
||||||
people = [
|
people = [
|
||||||
"bsousaa",
|
"bsousaa",
|
||||||
"neersighted",
|
|
||||||
"programmerq",
|
"programmerq",
|
||||||
"sam-thibault",
|
"sam-thibault",
|
||||||
|
"thajeztah",
|
||||||
"vvoland"
|
"vvoland"
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -97,10 +98,10 @@
|
|||||||
Email = "dnephin@gmail.com"
|
Email = "dnephin@gmail.com"
|
||||||
GitHub = "dnephin"
|
GitHub = "dnephin"
|
||||||
|
|
||||||
[people.neersighted]
|
[people.ndeloof]
|
||||||
Name = "Bjorn Neergaard"
|
Name = "Nicolas De Loof"
|
||||||
Email = "bneergaard@mirantis.com"
|
Email = "nicolas.deloof@gmail.com"
|
||||||
GitHub = "neersighted"
|
GitHub = "ndeloof"
|
||||||
|
|
||||||
[people.programmerq]
|
[people.programmerq]
|
||||||
Name = "Jeff Anderson"
|
Name = "Jeff Anderson"
|
||||||
|
|||||||
@@ -34,12 +34,12 @@ test: test-unit ## run tests
|
|||||||
|
|
||||||
.PHONY: test-unit
|
.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
|
test-unit: ## run unit tests, to change the output format use: GOTESTSUM_FORMAT=(dots|short|standard-quiet|short-verbose|standard-verbose) make test-unit
|
||||||
gotestsum -- $${TESTDIRS:-$(shell go list ./... | grep -vE '/vendor/|/e2e/|/cmd/docker-trust')} $(TESTFLAGS)
|
gotestsum -- $${TESTDIRS:-$(shell go list ./... | grep -vE '/vendor/|/e2e/')} $(TESTFLAGS)
|
||||||
|
|
||||||
.PHONY: test-coverage
|
.PHONY: test-coverage
|
||||||
test-coverage: ## run test coverage
|
test-coverage: ## run test coverage
|
||||||
mkdir -p $(CURDIR)/build/coverage
|
mkdir -p $(CURDIR)/build/coverage
|
||||||
gotestsum -- $(shell go list ./... | grep -vE '/vendor/|/e2e/|/cmd/docker-trust') -coverprofile=$(CURDIR)/build/coverage/coverage.txt
|
gotestsum -- $(shell go list ./... | grep -vE '/vendor/|/e2e/') -coverprofile=$(CURDIR)/build/coverage/coverage.txt
|
||||||
|
|
||||||
.PHONY: lint
|
.PHONY: lint
|
||||||
lint: ## run all the lint tools
|
lint: ## run all the lint tools
|
||||||
@@ -52,7 +52,7 @@ shellcheck: ## run shellcheck validation
|
|||||||
.PHONY: fmt
|
.PHONY: fmt
|
||||||
fmt: ## run gofumpt (if present) or gofmt
|
fmt: ## run gofumpt (if present) or gofmt
|
||||||
@if command -v gofumpt > /dev/null; then \
|
@if command -v gofumpt > /dev/null; then \
|
||||||
gofumpt -w -d -lang=1.24 . ; \
|
gofumpt -w -d -lang=1.19 . ; \
|
||||||
else \
|
else \
|
||||||
go list -f {{.Dir}} ./... | xargs gofmt -w -s -d ; \
|
go list -f {{.Dir}} ./... | xargs gofmt -w -s -d ; \
|
||||||
fi
|
fi
|
||||||
@@ -67,72 +67,36 @@ dynbinary: ## build dynamically linked binary
|
|||||||
|
|
||||||
.PHONY: plugins
|
.PHONY: plugins
|
||||||
plugins: ## build example CLI plugins
|
plugins: ## build example CLI plugins
|
||||||
scripts/build/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
|
.PHONY: vendor
|
||||||
vendor: ## update vendor with go modules
|
vendor: ## update vendor with go modules
|
||||||
rm -rf vendor
|
rm -rf vendor
|
||||||
scripts/with-go-mod.sh scripts/vendor update
|
./scripts/vendor update
|
||||||
|
|
||||||
.PHONY: validate-vendor
|
.PHONY: validate-vendor
|
||||||
validate-vendor: ## validate vendor
|
validate-vendor: ## validate vendor
|
||||||
scripts/with-go-mod.sh scripts/vendor validate
|
./scripts/vendor validate
|
||||||
|
|
||||||
.PHONY: mod-outdated
|
.PHONY: mod-outdated
|
||||||
mod-outdated: ## check outdated dependencies
|
mod-outdated: ## check outdated dependencies
|
||||||
scripts/with-go-mod.sh scripts/vendor outdated
|
./scripts/vendor outdated
|
||||||
|
|
||||||
.PHONY: authors
|
.PHONY: authors
|
||||||
authors: ## generate AUTHORS file from git history
|
authors: ## generate AUTHORS file from git history
|
||||||
scripts/docs/generate-authors.sh
|
scripts/docs/generate-authors.sh
|
||||||
|
|
||||||
.PHONY: completion
|
|
||||||
completion: shell-completion
|
|
||||||
completion: ## generate and install the shell-completion scripts
|
|
||||||
# Note: this uses system-wide paths, and so may overwrite completion
|
|
||||||
# scripts installed as part of deb/rpm packages.
|
|
||||||
#
|
|
||||||
# Given that this target is intended to debug/test updated versions, we could
|
|
||||||
# consider installing in per-user (~/.config, XDG_DATA_DIR) paths instead, but
|
|
||||||
# this will add more complexity.
|
|
||||||
#
|
|
||||||
# See https://github.com/docker/cli/pull/5770#discussion_r1927772710
|
|
||||||
install -D -p -m 0644 ./build/completion/bash/docker /usr/share/bash-completion/completions/docker
|
|
||||||
install -D -p -m 0644 ./build/completion/fish/docker.fish debian/docker-ce-cli/usr/share/fish/vendor_completions.d/docker.fish
|
|
||||||
install -D -p -m 0644 ./build/completion/zsh/_docker debian/docker-ce-cli/usr/share/zsh/vendor-completions/_docker
|
|
||||||
|
|
||||||
|
|
||||||
build/docker:
|
|
||||||
# This target is used by the "shell-completion" target, which requires either
|
|
||||||
# "binary" or "dynbinary" to have been built. We don't want to trigger those
|
|
||||||
# to prevent replacing a static binary with a dynamic one, or vice-versa.
|
|
||||||
@echo "Run 'make binary' or 'make dynbinary' first" && exit 1
|
|
||||||
|
|
||||||
.PHONY: shell-completion
|
|
||||||
shell-completion: build/docker # requires either "binary" or "dynbinary" to be built.
|
|
||||||
shell-completion: ## generate shell-completion scripts
|
|
||||||
@ ./scripts/build/shell-completion
|
|
||||||
|
|
||||||
.PHONY: manpages
|
.PHONY: manpages
|
||||||
manpages: ## generate man pages from go source and markdown
|
manpages: ## generate man pages from go source and markdown
|
||||||
scripts/with-go-mod.sh scripts/docs/generate-man.sh
|
scripts/docs/generate-man.sh
|
||||||
|
|
||||||
.PHONY: mddocs
|
.PHONY: mddocs
|
||||||
mddocs: ## generate markdown files from go source
|
mddocs: ## generate markdown files from go source
|
||||||
scripts/with-go-mod.sh scripts/docs/generate-md.sh
|
scripts/docs/generate-md.sh
|
||||||
|
|
||||||
.PHONY: yamldocs
|
.PHONY: yamldocs
|
||||||
yamldocs: ## generate documentation YAML files consumed by docs repo
|
yamldocs: ## generate documentation YAML files consumed by docs repo
|
||||||
scripts/with-go-mod.sh scripts/docs/generate-yaml.sh
|
scripts/docs/generate-yaml.sh
|
||||||
|
|
||||||
.PHONY: help
|
.PHONY: help
|
||||||
help: ## print this help
|
help: ## print this help
|
||||||
|
|||||||
@@ -14,6 +14,6 @@ United States and other governments.
|
|||||||
It is your responsibility to ensure that your use and/or transfer does not
|
It is your responsibility to ensure that your use and/or transfer does not
|
||||||
violate applicable laws.
|
violate applicable laws.
|
||||||
|
|
||||||
For more information, see https://www.bis.doc.gov
|
For more information, please see https://www.bis.doc.gov
|
||||||
|
|
||||||
See also https://www.apache.org/dev/crypto.html and/or seek legal counsel.
|
See also https://www.apache.org/dev/crypto.html and/or seek legal counsel.
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
# Docker CLI
|
# Docker CLI
|
||||||
|
|
||||||
[](https://pkg.go.dev/github.com/docker/cli)
|
[](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%3Abuild)
|
||||||
[](https://github.com/docker/cli/actions?query=workflow%3Atest)
|
[](https://github.com/docker/cli/actions?query=workflow%3Atest)
|
||||||
[](https://scorecard.dev/viewer/?uri=github.com/docker/cli)
|
[](https://goreportcard.com/report/github.com/docker/cli)
|
||||||
[](https://codecov.io/gh/docker/cli)
|
[](https://codecov.io/gh/docker/cli)
|
||||||
[](https://community.tracebit.com/supplychainattacks)
|
|
||||||
|
|
||||||
## About
|
## About
|
||||||
|
|
||||||
This repository is the home of the Docker CLI.
|
This repository is the home of the cli used in the Docker CE and
|
||||||
|
Docker EE products.
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
@@ -51,18 +51,6 @@ Run test:
|
|||||||
docker buildx bake test
|
docker buildx bake test
|
||||||
```
|
```
|
||||||
|
|
||||||
Run the unit test:
|
|
||||||
|
|
||||||
```
|
|
||||||
$ make -f docker.Makefile test-unit
|
|
||||||
```
|
|
||||||
|
|
||||||
Run the full test suite:
|
|
||||||
|
|
||||||
```
|
|
||||||
$ make -f docker.Makefile test
|
|
||||||
```
|
|
||||||
|
|
||||||
List all the available targets:
|
List all the available targets:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
@@ -74,13 +62,13 @@ make help
|
|||||||
Start an interactive development environment:
|
Start an interactive development environment:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
make shell
|
make -f docker.Makefile shell
|
||||||
```
|
```
|
||||||
|
|
||||||
## Legal
|
## Legal
|
||||||
|
|
||||||
*Brought to you courtesy of our legal counsel. For more context,
|
*Brought to you courtesy of our legal counsel. For more context,
|
||||||
see the [NOTICE](https://github.com/docker/cli/blob/master/NOTICE) document in this repo.*
|
please see the [NOTICE](https://github.com/docker/cli/blob/master/NOTICE) document in this repo.*
|
||||||
|
|
||||||
Use and transfer of Docker may be subject to certain restrictions by the
|
Use and transfer of Docker may be subject to certain restrictions by the
|
||||||
United States and other governments.
|
United States and other governments.
|
||||||
@@ -88,7 +76,7 @@ United States and other governments.
|
|||||||
It is your responsibility to ensure that your use and/or transfer does not
|
It is your responsibility to ensure that your use and/or transfer does not
|
||||||
violate applicable laws.
|
violate applicable laws.
|
||||||
|
|
||||||
For more information, see https://www.bis.doc.gov
|
For more information, please see https://www.bis.doc.gov
|
||||||
|
|
||||||
## Licensing
|
## Licensing
|
||||||
|
|
||||||
|
|||||||
-44
@@ -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.
|
|
||||||
@@ -5,32 +5,31 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/docker/cli/cli-plugins/metadata"
|
"github.com/docker/cli/cli-plugins/manager"
|
||||||
"github.com/docker/cli/cli-plugins/plugin"
|
"github.com/docker/cli/cli-plugins/plugin"
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
"github.com/moby/moby/client"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
plugin.Run(func(dockerCLI command.Cli) *cobra.Command {
|
plugin.Run(func(dockerCli command.Cli) *cobra.Command {
|
||||||
goodbye := &cobra.Command{
|
goodbye := &cobra.Command{
|
||||||
Use: "goodbye",
|
Use: "goodbye",
|
||||||
Short: "Say Goodbye instead of Hello",
|
Short: "Say Goodbye instead of Hello",
|
||||||
Run: func(cmd *cobra.Command, _ []string) {
|
Run: func(cmd *cobra.Command, _ []string) {
|
||||||
_, _ = fmt.Fprintln(dockerCLI.Out(), "Goodbye World!")
|
fmt.Fprintln(dockerCli.Out(), "Goodbye World!")
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
apiversion := &cobra.Command{
|
apiversion := &cobra.Command{
|
||||||
Use: "apiversion",
|
Use: "apiversion",
|
||||||
Short: "Print the API version of the server",
|
Short: "Print the API version of the server",
|
||||||
RunE: func(_ *cobra.Command, _ []string) error {
|
RunE: func(_ *cobra.Command, _ []string) error {
|
||||||
apiClient := dockerCLI.Client()
|
cli := dockerCli.Client()
|
||||||
ping, err := apiClient.Ping(context.Background(), client.PingOptions{})
|
ping, err := cli.Ping(context.Background())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
_, _ = fmt.Println(ping.APIVersion)
|
fmt.Println(ping.APIVersion)
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -39,15 +38,15 @@ func main() {
|
|||||||
Use: "exitstatus2",
|
Use: "exitstatus2",
|
||||||
Short: "Exit with status 2",
|
Short: "Exit with status 2",
|
||||||
RunE: func(_ *cobra.Command, _ []string) error {
|
RunE: func(_ *cobra.Command, _ []string) error {
|
||||||
_, _ = fmt.Fprintln(dockerCLI.Err(), "Exiting with error status 2")
|
fmt.Fprintln(dockerCli.Err(), "Exiting with error status 2")
|
||||||
os.Exit(2)
|
os.Exit(2)
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
who, optContext string
|
who, context string
|
||||||
preRun, debug bool
|
preRun, debug bool
|
||||||
)
|
)
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "helloworld",
|
Use: "helloworld",
|
||||||
@@ -57,33 +56,33 @@ func main() {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if preRun {
|
if preRun {
|
||||||
_, _ = fmt.Fprintln(dockerCLI.Err(), "Plugin PersistentPreRunE called")
|
fmt.Fprintf(dockerCli.Err(), "Plugin PersistentPreRunE called")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
if debug {
|
if debug {
|
||||||
_, _ = fmt.Fprintln(dockerCLI.Err(), "Plugin debug mode enabled")
|
fmt.Fprintf(dockerCli.Err(), "Plugin debug mode enabled")
|
||||||
}
|
}
|
||||||
|
|
||||||
switch optContext {
|
switch context {
|
||||||
case "Christmas":
|
case "Christmas":
|
||||||
_, _ = fmt.Fprintln(dockerCLI.Out(), "Merry Christmas!")
|
fmt.Fprintf(dockerCli.Out(), "Merry Christmas!\n")
|
||||||
return nil
|
return nil
|
||||||
case "":
|
case "":
|
||||||
// nothing
|
// nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
if who == "" {
|
if who == "" {
|
||||||
who, _ = dockerCLI.ConfigFile().PluginConfig("helloworld", "who")
|
who, _ = dockerCli.ConfigFile().PluginConfig("helloworld", "who")
|
||||||
}
|
}
|
||||||
if who == "" {
|
if who == "" {
|
||||||
who = "World"
|
who = "World"
|
||||||
}
|
}
|
||||||
|
|
||||||
_, _ = fmt.Fprintln(dockerCLI.Out(), "Hello", who)
|
fmt.Fprintf(dockerCli.Out(), "Hello %s!\n", who)
|
||||||
dockerCLI.ConfigFile().SetPluginConfig("helloworld", "lastwho", who)
|
dockerCli.ConfigFile().SetPluginConfig("helloworld", "lastwho", who)
|
||||||
return dockerCLI.ConfigFile().Save()
|
return dockerCli.ConfigFile().Save()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,12 +92,12 @@ func main() {
|
|||||||
// These are intended to deliberately clash with the CLIs own top
|
// These are intended to deliberately clash with the CLIs own top
|
||||||
// level arguments.
|
// level arguments.
|
||||||
flags.BoolVarP(&debug, "debug", "D", false, "Enable debug")
|
flags.BoolVarP(&debug, "debug", "D", false, "Enable debug")
|
||||||
flags.StringVarP(&optContext, "context", "c", "", "Is it Christmas?")
|
flags.StringVarP(&context, "context", "c", "", "Is it Christmas?")
|
||||||
|
|
||||||
cmd.AddCommand(goodbye, apiversion, exitStatus2)
|
cmd.AddCommand(goodbye, apiversion, exitStatus2)
|
||||||
return cmd
|
return cmd
|
||||||
},
|
},
|
||||||
metadata.Metadata{
|
manager.Metadata{
|
||||||
SchemaVersion: "0.1.0",
|
SchemaVersion: "0.1.0",
|
||||||
Vendor: "Docker Inc.",
|
Vendor: "Docker Inc.",
|
||||||
Version: "testing",
|
Version: "testing",
|
||||||
|
|||||||
@@ -1,85 +0,0 @@
|
|||||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
|
||||||
//go:build go1.26
|
|
||||||
|
|
||||||
// Package hooks defines the contract between the Docker CLI and CLI plugin hook
|
|
||||||
// implementations.
|
|
||||||
//
|
|
||||||
// # Audience
|
|
||||||
//
|
|
||||||
// This package is intended to be imported by CLI plugin implementations that
|
|
||||||
// implement a "hooks" subcommand, and by the Docker CLI when invoking those
|
|
||||||
// hooks.
|
|
||||||
//
|
|
||||||
// # Contract and wire format
|
|
||||||
//
|
|
||||||
// Hook inputs (see [Request]) are serialized as JSON and passed to the plugin hook
|
|
||||||
// subcommand (currently as a command-line argument). Hook outputs are emitted by
|
|
||||||
// the plugin as JSON (see [Response]).
|
|
||||||
//
|
|
||||||
// # Stability
|
|
||||||
//
|
|
||||||
// The types that represent the hook contract ([Request], [Response] and related
|
|
||||||
// constants) are considered part of Docker CLI's public Go API.
|
|
||||||
// Fields and values may be extended in a backwards-compatible way (for example,
|
|
||||||
// adding new fields), but existing fields and their meaning should remain stable.
|
|
||||||
// Plugins should ignore unknown fields and unknown hook types to remain
|
|
||||||
// forwards-compatible.
|
|
||||||
package hooks
|
|
||||||
|
|
||||||
// ResponseType is the type of response from the plugin.
|
|
||||||
type ResponseType int
|
|
||||||
|
|
||||||
const (
|
|
||||||
NextSteps ResponseType = 0
|
|
||||||
)
|
|
||||||
|
|
||||||
// Request is the type representing the information
|
|
||||||
// that plugins declaring support for hooks get passed when
|
|
||||||
// being invoked following a CLI command execution.
|
|
||||||
type Request struct {
|
|
||||||
// RootCmd is a string representing the matching hook configuration
|
|
||||||
// which is currently being invoked. If a hook for "docker context"
|
|
||||||
// is configured and the user executes "docker context ls", the plugin
|
|
||||||
// is invoked with "context".
|
|
||||||
RootCmd string `json:"RootCmd,omitzero"`
|
|
||||||
|
|
||||||
// Flags contains flags that were set on the command for which the
|
|
||||||
// hook was invoked. It uses flag names as key, with leading hyphens
|
|
||||||
// removed ("--flag" and "-flag" are included as "flag" and "f").
|
|
||||||
//
|
|
||||||
// Flag values are not included and are set to an empty string,
|
|
||||||
// except for boolean flags known to the CLI itself, for which
|
|
||||||
// the value is either "true", or "false".
|
|
||||||
//
|
|
||||||
// Plugins can use this information to adjust their [Response]
|
|
||||||
// based on whether the command triggering the hook was invoked
|
|
||||||
// with.
|
|
||||||
Flags map[string]string `json:"Flags,omitzero"`
|
|
||||||
|
|
||||||
// CommandError is a string containing the error output (if any)
|
|
||||||
// of the command for which the hook was invoked.
|
|
||||||
CommandError string `json:"CommandError,omitzero"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Response represents a plugin hook response. Plugins
|
|
||||||
// declaring support for CLI hooks need to print a JSON
|
|
||||||
// representation of this type when their hook subcommand
|
|
||||||
// is invoked.
|
|
||||||
type Response struct {
|
|
||||||
Type ResponseType `json:"Type"`
|
|
||||||
Template string `json:"Template,omitzero"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// HookType is the type of response from the plugin.
|
|
||||||
//
|
|
||||||
// Deprecated: use [ResponseType] instead.
|
|
||||||
//
|
|
||||||
//go:fix inline
|
|
||||||
type HookType = ResponseType
|
|
||||||
|
|
||||||
// HookMessage represents a plugin hook response.
|
|
||||||
//
|
|
||||||
// Deprecated: use [Response] instead.
|
|
||||||
//
|
|
||||||
//go:fix inline
|
|
||||||
type HookMessage = Response
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
package hooks
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
hookTemplateCommandName = `{{command}}`
|
|
||||||
hookTemplateFlagValue = `{{flagValue %q}}`
|
|
||||||
hookTemplateArg = `{{argValue %d}}`
|
|
||||||
)
|
|
||||||
|
|
||||||
// TemplateReplaceSubcommandName returns a hook template string
|
|
||||||
// that will be replaced by the CLI subcommand being executed
|
|
||||||
//
|
|
||||||
// Example:
|
|
||||||
//
|
|
||||||
// Response{
|
|
||||||
// Type: NextSteps,
|
|
||||||
// Template: "you ran the subcommand: " + TemplateReplaceSubcommandName(),
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// When being executed after the command:
|
|
||||||
//
|
|
||||||
// docker run --name "my-container" alpine
|
|
||||||
//
|
|
||||||
// It results in the message:
|
|
||||||
//
|
|
||||||
// you ran the subcommand: run
|
|
||||||
func TemplateReplaceSubcommandName() string {
|
|
||||||
return hookTemplateCommandName
|
|
||||||
}
|
|
||||||
|
|
||||||
// TemplateReplaceFlagValue returns a hook template string that will be
|
|
||||||
// replaced with the flags value when printed by the CLI.
|
|
||||||
//
|
|
||||||
// Example:
|
|
||||||
//
|
|
||||||
// Response{
|
|
||||||
// Type: NextSteps,
|
|
||||||
// Template: "you ran a container named: " + TemplateReplaceFlagValue("name"),
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// when executed after the command:
|
|
||||||
//
|
|
||||||
// docker run --name "my-container" alpine
|
|
||||||
//
|
|
||||||
// it results in the message:
|
|
||||||
//
|
|
||||||
// you ran a container named: my-container
|
|
||||||
func TemplateReplaceFlagValue(flag string) string {
|
|
||||||
return fmt.Sprintf(hookTemplateFlagValue, flag)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TemplateReplaceArg takes an index i and returns a hook
|
|
||||||
// template string that the CLI will replace the template with
|
|
||||||
// the ith argument after processing the passed flags.
|
|
||||||
//
|
|
||||||
// Example:
|
|
||||||
//
|
|
||||||
// Response{
|
|
||||||
// Type: NextSteps,
|
|
||||||
// Template: "run this image with `docker run " + TemplateReplaceArg(0) + "`",
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// when being executed after the command:
|
|
||||||
//
|
|
||||||
// docker pull alpine
|
|
||||||
//
|
|
||||||
// It results in the message:
|
|
||||||
//
|
|
||||||
// Run this image with `docker run alpine`
|
|
||||||
func TemplateReplaceArg(i int) string {
|
|
||||||
return fmt.Sprintf(hookTemplateArg, i)
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
package hooks_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/docker/cli/cli-plugins/hooks"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestTemplateHelpers(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
doc string
|
|
||||||
got func() string
|
|
||||||
want string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
doc: "subcommand name",
|
|
||||||
got: hooks.TemplateReplaceSubcommandName,
|
|
||||||
want: `{{command}}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
doc: "flag value",
|
|
||||||
got: func() string {
|
|
||||||
return hooks.TemplateReplaceFlagValue("name")
|
|
||||||
},
|
|
||||||
want: `{{flagValue "name"}}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
doc: "arg",
|
|
||||||
got: func() string {
|
|
||||||
return hooks.TemplateReplaceArg(0)
|
|
||||||
},
|
|
||||||
want: `{{argValue 0}}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
doc: "arg",
|
|
||||||
got: func() string {
|
|
||||||
return hooks.TemplateReplaceArg(3)
|
|
||||||
},
|
|
||||||
want: `{{argValue 3}}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range tests {
|
|
||||||
t.Run(tc.doc, func(t *testing.T) {
|
|
||||||
if got := tc.got(); got != tc.want {
|
|
||||||
t.Fatalf("expected %q, got %q", tc.want, got)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
package hooks
|
|
||||||
|
|
||||||
import "io"
|
|
||||||
|
|
||||||
const (
|
|
||||||
whatsNext = "\n\033[1mWhat's next:\033[0m\n"
|
|
||||||
indent = " "
|
|
||||||
)
|
|
||||||
|
|
||||||
// PrintNextSteps renders list of [NextSteps] messages and writes them
|
|
||||||
// to out. It is a no-op if messages is empty.
|
|
||||||
func PrintNextSteps(out io.Writer, messages []string) {
|
|
||||||
if len(messages) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
_, _ = io.WriteString(out, whatsNext)
|
|
||||||
for _, msg := range messages {
|
|
||||||
_, _ = io.WriteString(out, indent)
|
|
||||||
_, _ = io.WriteString(out, msg)
|
|
||||||
_, _ = io.WriteString(out, "\n")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
package hooks_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/docker/cli/cli-plugins/hooks"
|
|
||||||
"gotest.tools/v3/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestPrintHookMessages(t *testing.T) {
|
|
||||||
const header = "\n\x1b[1mWhat's next:\x1b[0m\n"
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
doc string
|
|
||||||
messages []string
|
|
||||||
expectedOutput string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
doc: "no messages",
|
|
||||||
messages: nil,
|
|
||||||
expectedOutput: "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
doc: "single message",
|
|
||||||
messages: []string{"Bork!"},
|
|
||||||
expectedOutput: header +
|
|
||||||
" Bork!\n",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
doc: "multiple messages",
|
|
||||||
messages: []string{"Foo", "bar"},
|
|
||||||
expectedOutput: header +
|
|
||||||
" Foo\n" +
|
|
||||||
" bar\n",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tc := range tests {
|
|
||||||
t.Run(tc.doc, func(t *testing.T) {
|
|
||||||
var w strings.Builder
|
|
||||||
hooks.PrintNextSteps(&w, tc.messages)
|
|
||||||
assert.Equal(t, w.String(), tc.expectedOutput)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
|
||||||
//go:build go1.26
|
|
||||||
|
|
||||||
package hooks
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"text/template"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
const maxMessages = 10
|
|
||||||
|
|
||||||
func ParseTemplate(hookTemplate string, cmd *cobra.Command) ([]string, error) {
|
|
||||||
out := hookTemplate
|
|
||||||
if strings.Contains(hookTemplate, "{{") {
|
|
||||||
// Message may be a template.
|
|
||||||
msgContext := commandInfo{cmd: cmd}
|
|
||||||
|
|
||||||
tmpl, err := template.New("").Funcs(template.FuncMap{
|
|
||||||
"command": msgContext.command,
|
|
||||||
"flagValue": msgContext.flagValue,
|
|
||||||
"argValue": msgContext.argValue,
|
|
||||||
|
|
||||||
// kept for backward-compatibility with old templates.
|
|
||||||
"flag": func(_ any, flagName string) (string, error) { return msgContext.flagValue(flagName) },
|
|
||||||
"arg": func(_ any, i int) (string, error) { return msgContext.argValue(i) },
|
|
||||||
}).Parse(hookTemplate)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var b bytes.Buffer
|
|
||||||
err = tmpl.Execute(&b, msgContext)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
out = b.String()
|
|
||||||
}
|
|
||||||
if n := strings.Count(out, "\n"); n > maxMessages {
|
|
||||||
return nil, fmt.Errorf("hook template contains too many messages (%d): maximum is %d", n, maxMessages)
|
|
||||||
}
|
|
||||||
return strings.SplitN(out, "\n", maxMessages), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var ErrHookTemplateParse = errors.New("failed to parse hook template")
|
|
||||||
|
|
||||||
// commandInfo provides info about the command for which the hook was invoked.
|
|
||||||
// It is used for templated hook-messages.
|
|
||||||
type commandInfo struct {
|
|
||||||
cmd *cobra.Command
|
|
||||||
}
|
|
||||||
|
|
||||||
// Name returns the name of the (sub)command for which the hook was invoked.
|
|
||||||
//
|
|
||||||
// It's used for backward-compatibility with old templates.
|
|
||||||
func (c commandInfo) Name() string {
|
|
||||||
return c.command()
|
|
||||||
}
|
|
||||||
|
|
||||||
// command returns the name of the (sub)command for which the hook was invoked.
|
|
||||||
func (c commandInfo) command() string {
|
|
||||||
if c.cmd == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return c.cmd.Name()
|
|
||||||
}
|
|
||||||
|
|
||||||
// flagValue returns the value that was set for the given flag when the hook was invoked.
|
|
||||||
func (c commandInfo) flagValue(flagName string) (string, error) {
|
|
||||||
if c.cmd == nil {
|
|
||||||
return "", fmt.Errorf("%w: flagValue: cmd is nil", ErrHookTemplateParse)
|
|
||||||
}
|
|
||||||
f := c.cmd.Flag(flagName)
|
|
||||||
if f == nil {
|
|
||||||
return "", fmt.Errorf("%w: flagValue: no flags found", ErrHookTemplateParse)
|
|
||||||
}
|
|
||||||
return f.Value.String(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// argValue returns the value of the nth argument.
|
|
||||||
func (c commandInfo) argValue(n int) (string, error) {
|
|
||||||
if c.cmd == nil {
|
|
||||||
return "", fmt.Errorf("%w: arg: cmd is nil", ErrHookTemplateParse)
|
|
||||||
}
|
|
||||||
flags := c.cmd.Flags()
|
|
||||||
v := flags.Arg(n)
|
|
||||||
if v == "" && n >= flags.NArg() {
|
|
||||||
return "", fmt.Errorf("%w: arg: %dth argument not set", ErrHookTemplateParse, n)
|
|
||||||
}
|
|
||||||
return v, nil
|
|
||||||
}
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
package hooks_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/docker/cli/cli-plugins/hooks"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"gotest.tools/v3/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TestParseTemplate tests parsing templates as returned by plugins.
|
|
||||||
//
|
|
||||||
// It uses fixed string fixtures to lock in compatibility with existing
|
|
||||||
// plugin templates, so older formats continue to work even if we add new
|
|
||||||
// template forms.
|
|
||||||
//
|
|
||||||
// For helper-backed cases, it also verifies that templates produced by the
|
|
||||||
// current TemplateReplace* helpers parse to the same output. This lets us
|
|
||||||
// evolve the emitted template format without breaking older plugins.
|
|
||||||
func TestParseTemplate(t *testing.T) {
|
|
||||||
type testFlag struct {
|
|
||||||
name string
|
|
||||||
value string
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
doc string
|
|
||||||
template string // compatibility fixture; keep even if helpers emit a newer form
|
|
||||||
templateFunc func() string
|
|
||||||
flags []testFlag
|
|
||||||
args []string
|
|
||||||
expectedOutput []string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
doc: "empty template",
|
|
||||||
template: "",
|
|
||||||
expectedOutput: []string{""},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
doc: "plain message",
|
|
||||||
template: "a plain template message",
|
|
||||||
expectedOutput: []string{"a plain template message"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
doc: "subcommand name",
|
|
||||||
template: "hello {{.Name}}", // NOTE: fixture; do not modify without considering plugin compatibility
|
|
||||||
templateFunc: func() string { return "hello " + hooks.TemplateReplaceSubcommandName() },
|
|
||||||
|
|
||||||
expectedOutput: []string{"hello pull"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
doc: "single flag",
|
|
||||||
template: `{{flag . "tag"}}`, // NOTE: fixture; do not modify without considering plugin compatibility
|
|
||||||
templateFunc: func() string { return hooks.TemplateReplaceFlagValue("tag") },
|
|
||||||
flags: []testFlag{
|
|
||||||
{name: "tag", value: "my-tag"},
|
|
||||||
},
|
|
||||||
expectedOutput: []string{"my-tag"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
doc: "multiple flags",
|
|
||||||
template: `{{flag . "test-one"}} {{flag . "test2"}}`, // NOTE: fixture; do not modify without considering plugin compatibility
|
|
||||||
templateFunc: func() string {
|
|
||||||
return hooks.TemplateReplaceFlagValue("test-one") + " " + hooks.TemplateReplaceFlagValue("test2")
|
|
||||||
},
|
|
||||||
flags: []testFlag{
|
|
||||||
{
|
|
||||||
name: "test-one",
|
|
||||||
value: "value",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "test2",
|
|
||||||
value: "value2",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expectedOutput: []string{"value value2"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
doc: "multiple args",
|
|
||||||
template: `{{arg . 0}} {{arg . 1}}`, // NOTE: fixture; do not modify without considering plugin compatibility
|
|
||||||
templateFunc: func() string { return hooks.TemplateReplaceArg(0) + " " + hooks.TemplateReplaceArg(1) },
|
|
||||||
args: []string{"zero", "one"},
|
|
||||||
expectedOutput: []string{"zero one"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
doc: "arg in sentence",
|
|
||||||
template: "You just pulled {{arg . 0}}", // NOTE: fixture; do not modify without considering plugin compatibility
|
|
||||||
templateFunc: func() string { return "You just pulled " + hooks.TemplateReplaceArg(0) },
|
|
||||||
args: []string{"alpine"},
|
|
||||||
expectedOutput: []string{"You just pulled alpine"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
doc: "multiline output",
|
|
||||||
template: "one line\nanother line!",
|
|
||||||
expectedOutput: []string{"one line", "another line!"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range tests {
|
|
||||||
t.Run(tc.doc, func(t *testing.T) {
|
|
||||||
testCmd := &cobra.Command{
|
|
||||||
Use: "pull",
|
|
||||||
Args: cobra.ExactArgs(len(tc.args)),
|
|
||||||
}
|
|
||||||
for _, f := range tc.flags {
|
|
||||||
_ = testCmd.Flags().String(f.name, "", "")
|
|
||||||
err := testCmd.Flag(f.name).Value.Set(f.value)
|
|
||||||
assert.NilError(t, err)
|
|
||||||
}
|
|
||||||
err := testCmd.Flags().Parse(tc.args)
|
|
||||||
assert.NilError(t, err)
|
|
||||||
|
|
||||||
// Validate using fixtures.
|
|
||||||
out, err := hooks.ParseTemplate(tc.template, testCmd)
|
|
||||||
assert.NilError(t, err)
|
|
||||||
assert.DeepEqual(t, out, tc.expectedOutput)
|
|
||||||
|
|
||||||
if tc.templateFunc != nil {
|
|
||||||
// Validate using the current template function equivalent.
|
|
||||||
out, err = hooks.ParseTemplate(tc.templateFunc(), testCmd)
|
|
||||||
assert.NilError(t, err)
|
|
||||||
assert.DeepEqual(t, out, tc.expectedOutput)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,15 @@
|
|||||||
package manager
|
package manager
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os/exec"
|
exec "golang.org/x/sys/execabs"
|
||||||
|
|
||||||
"github.com/docker/cli/cli-plugins/metadata"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Candidate represents a possible plugin candidate, for mocking purposes
|
||||||
|
type Candidate interface {
|
||||||
|
Path() string
|
||||||
|
Metadata() ([]byte, error)
|
||||||
|
}
|
||||||
|
|
||||||
type candidate struct {
|
type candidate struct {
|
||||||
path string
|
path string
|
||||||
}
|
}
|
||||||
@@ -15,5 +19,5 @@ func (c *candidate) Path() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *candidate) Metadata() ([]byte, error) {
|
func (c *candidate) Metadata() ([]byte, error) {
|
||||||
return exec.Command(c.path, metadata.MetadataSubcommandName).Output() // #nosec G204 -- ignore "Subprocess launched with a potential tainted input or cmd arguments"
|
return exec.Command(c.path, MetadataSubcommandName).Output()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,9 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/docker/cli/cli-plugins/metadata"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"gotest.tools/v3/assert"
|
"gotest.tools/v3/assert"
|
||||||
is "gotest.tools/v3/assert/cmp"
|
"gotest.tools/v3/assert/cmp"
|
||||||
)
|
)
|
||||||
|
|
||||||
type fakeCandidate struct {
|
type fakeCandidate struct {
|
||||||
@@ -31,132 +30,61 @@ func (c *fakeCandidate) Metadata() ([]byte, error) {
|
|||||||
|
|
||||||
func TestValidateCandidate(t *testing.T) {
|
func TestValidateCandidate(t *testing.T) {
|
||||||
const (
|
const (
|
||||||
goodPluginName = metadata.NamePrefix + "goodplugin"
|
goodPluginName = NamePrefix + "goodplugin"
|
||||||
builtinName = metadata.NamePrefix + "builtin"
|
|
||||||
builtinAlias = metadata.NamePrefix + "alias"
|
|
||||||
|
|
||||||
badPrefixPath = "/usr/local/libexec/cli-plugins/wobble"
|
builtinName = NamePrefix + "builtin"
|
||||||
badNamePath = "/usr/local/libexec/cli-plugins/docker-123456"
|
builtinAlias = NamePrefix + "alias"
|
||||||
goodPluginPath = "/usr/local/libexec/cli-plugins/" + goodPluginName
|
|
||||||
|
badPrefixPath = "/usr/local/libexec/cli-plugins/wobble"
|
||||||
|
badNamePath = "/usr/local/libexec/cli-plugins/docker-123456"
|
||||||
|
goodPluginPath = "/usr/local/libexec/cli-plugins/" + goodPluginName
|
||||||
|
metaExperimental = `{"SchemaVersion": "0.1.0", "Vendor": "e2e-testing", "Experimental": true}`
|
||||||
)
|
)
|
||||||
|
|
||||||
fakeroot := &cobra.Command{Use: "docker"}
|
fakeroot := &cobra.Command{Use: "docker"}
|
||||||
fakeroot.AddCommand(&cobra.Command{
|
fakeroot.AddCommand(&cobra.Command{
|
||||||
Use: strings.TrimPrefix(builtinName, metadata.NamePrefix),
|
Use: strings.TrimPrefix(builtinName, NamePrefix),
|
||||||
Aliases: []string{
|
Aliases: []string{
|
||||||
strings.TrimPrefix(builtinAlias, metadata.NamePrefix),
|
strings.TrimPrefix(builtinAlias, NamePrefix),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
name string
|
name string
|
||||||
plugin *fakeCandidate
|
c *fakeCandidate
|
||||||
|
|
||||||
// Either err or invalid may be non-empty, but not both (both can be empty for a good plugin).
|
// Either err or invalid may be non-empty, but not both (both can be empty for a good plugin).
|
||||||
err string
|
err string
|
||||||
invalid string
|
invalid string
|
||||||
expVer string
|
|
||||||
}{
|
}{
|
||||||
// Invalid cases.
|
/* Each failing one of the tests */
|
||||||
{
|
{name: "empty path", c: &fakeCandidate{path: ""}, err: "plugin candidate path cannot be empty"},
|
||||||
name: "empty path",
|
{name: "bad prefix", c: &fakeCandidate{path: badPrefixPath}, err: fmt.Sprintf("does not have %q prefix", NamePrefix)},
|
||||||
plugin: &fakeCandidate{path: ""},
|
{name: "bad path", c: &fakeCandidate{path: badNamePath}, invalid: "did not match"},
|
||||||
err: "plugin candidate path cannot be empty",
|
{name: "builtin command", c: &fakeCandidate{path: builtinName}, invalid: `plugin "builtin" duplicates builtin command`},
|
||||||
},
|
{name: "builtin alias", c: &fakeCandidate{path: builtinAlias}, invalid: `plugin "alias" duplicates an alias of builtin command "builtin"`},
|
||||||
{
|
{name: "fetch failure", c: &fakeCandidate{path: goodPluginPath, exec: false}, invalid: fmt.Sprintf("failed to fetch metadata: faked a failure to exec %q", goodPluginPath)},
|
||||||
name: "bad prefix",
|
{name: "metadata not json", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `xyzzy`}, invalid: "invalid character"},
|
||||||
plugin: &fakeCandidate{path: badPrefixPath},
|
{name: "empty schemaversion", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{}`}, invalid: `plugin SchemaVersion "" is not valid`},
|
||||||
err: fmt.Sprintf("does not have %q prefix", metadata.NamePrefix),
|
{name: "invalid schemaversion", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "xyzzy"}`}, invalid: `plugin SchemaVersion "xyzzy" is not valid`},
|
||||||
},
|
{name: "no vendor", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0"}`}, invalid: "plugin metadata does not define a vendor"},
|
||||||
{
|
{name: "empty vendor", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0", "Vendor": ""}`}, invalid: "plugin metadata does not define a vendor"},
|
||||||
name: "bad path",
|
// This one should work
|
||||||
plugin: &fakeCandidate{path: badNamePath},
|
{name: "valid", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0", "Vendor": "e2e-testing"}`}},
|
||||||
invalid: "did not match",
|
{name: "experimental + allowing experimental", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: metaExperimental}},
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "builtin command",
|
|
||||||
plugin: &fakeCandidate{path: builtinName},
|
|
||||||
invalid: `plugin "builtin" duplicates builtin command`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "builtin alias",
|
|
||||||
plugin: &fakeCandidate{path: builtinAlias},
|
|
||||||
invalid: `plugin "alias" duplicates an alias of builtin command "builtin"`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "fetch failure",
|
|
||||||
plugin: &fakeCandidate{path: goodPluginPath, exec: false},
|
|
||||||
invalid: fmt.Sprintf("failed to fetch metadata: faked a failure to exec %q", goodPluginPath),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "metadata not json",
|
|
||||||
plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `xyzzy`},
|
|
||||||
invalid: "invalid character",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "empty schemaversion",
|
|
||||||
plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{}`},
|
|
||||||
invalid: `plugin SchemaVersion version cannot be empty`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid schemaversion",
|
|
||||||
plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "xyzzy"}`},
|
|
||||||
invalid: `plugin SchemaVersion "xyzzy" has wrong format: must be <major>.<minor>.<patch>`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid schemaversion major",
|
|
||||||
plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "2.0.0"}`},
|
|
||||||
invalid: `plugin SchemaVersion "2.0.0" is not supported: must be lower than 2.0.0`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "no vendor",
|
|
||||||
plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0"}`},
|
|
||||||
invalid: "plugin metadata does not define a vendor",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "empty vendor",
|
|
||||||
plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0", "Vendor": ""}`},
|
|
||||||
invalid: "plugin metadata does not define a vendor",
|
|
||||||
},
|
|
||||||
|
|
||||||
// Valid cases.
|
|
||||||
{
|
|
||||||
name: "valid",
|
|
||||||
plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0", "Vendor": "e2e-testing"}`},
|
|
||||||
expVer: "0.1.0",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// Including the deprecated "experimental" field should not break processing.
|
|
||||||
name: "with legacy experimental",
|
|
||||||
plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0", "Vendor": "e2e-testing", "Experimental": true}`},
|
|
||||||
expVer: "0.1.0",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// note that this may not be supported by older CLIs
|
|
||||||
name: "new minor schema version",
|
|
||||||
plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.2.0", "Vendor": "e2e-testing"}`},
|
|
||||||
expVer: "0.2.0",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// note that this may not be supported by older CLIs
|
|
||||||
name: "new major schema version",
|
|
||||||
plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "1.0.0", "Vendor": "e2e-testing"}`},
|
|
||||||
expVer: "1.0.0",
|
|
||||||
},
|
|
||||||
} {
|
} {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
p, err := newPlugin(tc.plugin, fakeroot.Commands())
|
p, err := newPlugin(tc.c, fakeroot.Commands())
|
||||||
switch {
|
if tc.err != "" {
|
||||||
case tc.err != "":
|
|
||||||
assert.ErrorContains(t, err, tc.err)
|
assert.ErrorContains(t, err, tc.err)
|
||||||
case tc.invalid != "":
|
} else if tc.invalid != "" {
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
assert.Assert(t, is.ErrorType(p.Err, reflect.TypeFor[*pluginError]()))
|
assert.Assert(t, cmp.ErrorType(p.Err, reflect.TypeOf(&pluginError{})))
|
||||||
assert.ErrorContains(t, p.Err, tc.invalid)
|
assert.ErrorContains(t, p.Err, tc.invalid)
|
||||||
default:
|
} else {
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
assert.Equal(t, metadata.NamePrefix+p.Name, goodPluginName)
|
assert.Equal(t, NamePrefix+p.Name, goodPluginName)
|
||||||
assert.Equal(t, p.SchemaVersion, tc.expVer)
|
assert.Equal(t, p.SchemaVersion, "0.1.0")
|
||||||
assert.Equal(t, p.Vendor, "e2e-testing")
|
assert.Equal(t, p.Vendor, "e2e-testing")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5,66 +5,85 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/docker/cli/cli-plugins/metadata"
|
"github.com/docker/cli/cli/command"
|
||||||
"github.com/docker/cli/cli/config"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
var pluginCommandStubsOnce sync.Once
|
var pluginCommandStubsOnce sync.Once
|
||||||
|
|
||||||
// AddPluginCommandStubs adds a stub cobra.Commands for each valid and invalid
|
// AddPluginCommandStubs adds a stub cobra.Commands for each valid and invalid
|
||||||
// plugin. The command stubs will have several annotations added, see
|
// plugin. The command stubs will have several annotations added, see
|
||||||
// `CommandAnnotationPlugin*`.
|
// `CommandAnnotationPlugin*`.
|
||||||
func AddPluginCommandStubs(dockerCLI config.Provider, rootCmd *cobra.Command) (err error) {
|
func AddPluginCommandStubs(dockerCli command.Cli, rootCmd *cobra.Command) (err error) {
|
||||||
pluginCommandStubsOnce.Do(func() {
|
pluginCommandStubsOnce.Do(func() {
|
||||||
var plugins []Plugin
|
var plugins []Plugin
|
||||||
plugins, err = ListPlugins(dockerCLI, rootCmd)
|
plugins, err = ListPlugins(dockerCli, rootCmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for _, p := range plugins {
|
for _, p := range plugins {
|
||||||
|
p := p
|
||||||
vendor := p.Vendor
|
vendor := p.Vendor
|
||||||
if vendor == "" {
|
if vendor == "" {
|
||||||
vendor = "unknown"
|
vendor = "unknown"
|
||||||
}
|
}
|
||||||
annotations := map[string]string{
|
annotations := map[string]string{
|
||||||
metadata.CommandAnnotationPlugin: "true",
|
CommandAnnotationPlugin: "true",
|
||||||
metadata.CommandAnnotationPluginVendor: vendor,
|
CommandAnnotationPluginVendor: vendor,
|
||||||
metadata.CommandAnnotationPluginVersion: p.Version,
|
CommandAnnotationPluginVersion: p.Version,
|
||||||
}
|
}
|
||||||
if p.Err != nil {
|
if p.Err != nil {
|
||||||
annotations[metadata.CommandAnnotationPluginInvalid] = p.Err.Error()
|
annotations[CommandAnnotationPluginInvalid] = p.Err.Error()
|
||||||
}
|
}
|
||||||
rootCmd.AddCommand(&cobra.Command{
|
rootCmd.AddCommand(&cobra.Command{
|
||||||
Use: p.Name,
|
Use: p.Name,
|
||||||
Short: p.ShortDescription,
|
Short: p.ShortDescription,
|
||||||
Hidden: p.Hidden,
|
|
||||||
Run: func(_ *cobra.Command, _ []string) {},
|
Run: func(_ *cobra.Command, _ []string) {},
|
||||||
Annotations: annotations,
|
Annotations: annotations,
|
||||||
DisableFlagParsing: true,
|
DisableFlagParsing: true,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
flags := rootCmd.PersistentFlags()
|
flags := rootCmd.PersistentFlags()
|
||||||
flags.SetOutput(nil)
|
flags.SetOutput(nil)
|
||||||
if err := flags.Parse(args); err != nil {
|
perr := flags.Parse(args)
|
||||||
|
if perr != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if flags.Changed("help") {
|
if flags.Changed("help") {
|
||||||
cmd.HelpFunc()(rootCmd, args)
|
cmd.HelpFunc()(rootCmd, args)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return fmt.Errorf("docker: unknown command: docker %s\n\nRun 'docker --help' for more information", cmd.Name())
|
return fmt.Errorf("docker: '%s' is not a docker command.\nSee 'docker --help'", cmd.Name())
|
||||||
},
|
},
|
||||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
// Delegate completion to plugin
|
// Delegate completion to plugin
|
||||||
cargs := []string{p.Path, cobra.ShellCompRequestCmd, p.Name} //nolint:prealloc // no need to over-complicate things.
|
cargs := []string{p.Path, cobra.ShellCompRequestCmd, p.Name}
|
||||||
cargs = append(cargs, args...)
|
cargs = append(cargs, args...)
|
||||||
cargs = append(cargs, toComplete)
|
cargs = append(cargs, toComplete)
|
||||||
origArgs := os.Args
|
|
||||||
os.Args = cargs
|
os.Args = cargs
|
||||||
defer func() {
|
runCommand, runErr := PluginRunCommand(dockerCli, p.Name, cmd)
|
||||||
os.Args = origArgs
|
|
||||||
}()
|
|
||||||
runCommand, runErr := PluginRunCommand(dockerCLI, p.Name, cmd)
|
|
||||||
if runErr != nil {
|
if runErr != nil {
|
||||||
return nil, cobra.ShellCompDirectiveError
|
return nil, cobra.ShellCompDirectiveError
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,86 +0,0 @@
|
|||||||
package manager
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"slices"
|
|
||||||
"sync"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/docker/cli/internal/test"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"gotest.tools/v3/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestPluginResourceAttributesEnvvar(t *testing.T) {
|
|
||||||
cmd := &cobra.Command{
|
|
||||||
Annotations: map[string]string{
|
|
||||||
cobra.CommandDisplayNameAnnotation: "docker",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure basic usage is fine.
|
|
||||||
env := appendPluginResourceAttributesEnvvar(nil, cmd, Plugin{Name: "compose"})
|
|
||||||
assert.DeepEqual(t, []string{"OTEL_RESOURCE_ATTRIBUTES=docker.cli.cobra.command_path=docker%20compose"}, env)
|
|
||||||
|
|
||||||
// Add a user-based environment variable to OTEL_RESOURCE_ATTRIBUTES.
|
|
||||||
t.Setenv("OTEL_RESOURCE_ATTRIBUTES", "a.b.c=foo")
|
|
||||||
|
|
||||||
env = appendPluginResourceAttributesEnvvar(nil, cmd, Plugin{Name: "compose"})
|
|
||||||
assert.DeepEqual(t, []string{"OTEL_RESOURCE_ATTRIBUTES=a.b.c=foo,docker.cli.cobra.command_path=docker%20compose"}, env)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPluginStubRunEReturnsParseError(t *testing.T) {
|
|
||||||
cmd, err := preparePluginStubCommand(t)
|
|
||||||
assert.NilError(t, err)
|
|
||||||
|
|
||||||
err = cmd.RunE(cmd, []string{"--definitely-not-a-real-flag"})
|
|
||||||
assert.ErrorContains(t, err, "unknown flag: --definitely-not-a-real-flag")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPluginStubCompletionRestoresOSArgs(t *testing.T) {
|
|
||||||
cmd, err := preparePluginStubCommand(t)
|
|
||||||
assert.NilError(t, err)
|
|
||||||
|
|
||||||
savedArgs := os.Args
|
|
||||||
t.Cleanup(func() { os.Args = savedArgs })
|
|
||||||
|
|
||||||
originalArgs := []string{"docker", "image", "ls"}
|
|
||||||
os.Args = slices.Clone(originalArgs)
|
|
||||||
|
|
||||||
_, directive := cmd.ValidArgsFunction(cmd, []string{"--all"}, "alp")
|
|
||||||
assert.Equal(t, directive, cobra.ShellCompDirectiveError)
|
|
||||||
assert.DeepEqual(t, os.Args, originalArgs)
|
|
||||||
}
|
|
||||||
|
|
||||||
func preparePluginStubCommand(t *testing.T) (*cobra.Command, error) {
|
|
||||||
t.Helper()
|
|
||||||
pluginCommandStubsOnce = sync.Once{}
|
|
||||||
|
|
||||||
tmpDir := t.TempDir()
|
|
||||||
const cliPlugin = `#!/bin/sh
|
|
||||||
printf '%s' '{"SchemaVersion":"0.1.0"}'
|
|
||||||
`
|
|
||||||
if err := os.WriteFile(filepath.Join(tmpDir, "docker-testplugin"), []byte(cliPlugin), 0o777); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
cli := test.NewFakeCli(nil)
|
|
||||||
cli.ConfigFile().CLIPluginsExtraDirs = []string{tmpDir}
|
|
||||||
|
|
||||||
root := &cobra.Command{Use: "docker"}
|
|
||||||
root.PersistentFlags().Bool("debug", false, "")
|
|
||||||
|
|
||||||
if err := AddPluginCommandStubs(cli, root); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd, _, err := root.Find([]string{"testplugin"})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if cmd == nil {
|
|
||||||
return nil, os.ErrNotExist
|
|
||||||
}
|
|
||||||
return cmd, nil
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,7 @@
|
|||||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
|
||||||
//go:build go1.26
|
|
||||||
|
|
||||||
package manager
|
package manager
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
// pluginError is set as Plugin.Err by NewPlugin if the plugin
|
// pluginError is set as Plugin.Err by NewPlugin if the plugin
|
||||||
@@ -23,24 +20,29 @@ func (e *pluginError) Error() string {
|
|||||||
return e.cause.Error()
|
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.
|
// Unwrap provides compatibility for Go 1.13 error chains.
|
||||||
func (e *pluginError) Unwrap() error {
|
func (e *pluginError) Unwrap() error {
|
||||||
return e.cause
|
return e.cause
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarshalText marshals the pluginError into a textual form.
|
// MarshalText marshalls the pluginError into a textual form.
|
||||||
func (e *pluginError) MarshalText() (text []byte, err error) {
|
func (e *pluginError) MarshalText() (text []byte, err error) {
|
||||||
return []byte(e.cause.Error()), nil
|
return []byte(e.cause.Error()), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// wrapAsPluginError wraps an error in a pluginError with an
|
// wrapAsPluginError wraps an error in a pluginError with an
|
||||||
// additional message.
|
// additional message, analogous to errors.Wrapf.
|
||||||
func wrapAsPluginError(err error, msg string) error {
|
func wrapAsPluginError(err error, msg string) error {
|
||||||
return &pluginError{cause: fmt.Errorf("%s: %w", msg, err)}
|
return &pluginError{cause: errors.Wrap(err, msg)}
|
||||||
}
|
}
|
||||||
|
|
||||||
// newPluginError creates a new pluginError, analogous to
|
// NewPluginError creates a new pluginError, analogous to
|
||||||
// errors.Errorf.
|
// errors.Errorf.
|
||||||
func newPluginError(msg string, args ...any) error {
|
func NewPluginError(msg string, args ...interface{}) error {
|
||||||
return &pluginError{cause: fmt.Errorf(msg, args...)}
|
return &pluginError{cause: errors.Errorf(msg, args...)}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestPluginError(t *testing.T) {
|
func TestPluginError(t *testing.T) {
|
||||||
err := newPluginError("new error")
|
err := NewPluginError("new error")
|
||||||
assert.Check(t, is.Error(err, "new error"))
|
assert.Check(t, is.Error(err, "new error"))
|
||||||
|
|
||||||
inner := errors.New("testing")
|
inner := errors.New("testing")
|
||||||
@@ -21,7 +21,4 @@ func TestPluginError(t *testing.T) {
|
|||||||
actual, err := json.Marshal(err)
|
actual, err := json.Marshal(err)
|
||||||
assert.Check(t, err)
|
assert.Check(t, err)
|
||||||
assert.Check(t, is.Equal(`"wrapping: testing"`, string(actual)))
|
assert.Check(t, is.Equal(`"wrapping: testing"`, string(actual)))
|
||||||
|
|
||||||
err = wrapAsPluginError(nil, "wrapping")
|
|
||||||
assert.Check(t, is.Error(err, "wrapping: %!w(<nil>)"))
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,237 +0,0 @@
|
|||||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
|
||||||
//go:build go1.26
|
|
||||||
|
|
||||||
package manager
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/docker/cli/cli-plugins/hooks"
|
|
||||||
"github.com/docker/cli/cli/config"
|
|
||||||
"github.com/docker/cli/cli/config/configfile"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/spf13/pflag"
|
|
||||||
)
|
|
||||||
|
|
||||||
// HookPluginData is the type representing the information
|
|
||||||
// that plugins declaring support for hooks get passed when
|
|
||||||
// being invoked following a CLI command execution.
|
|
||||||
//
|
|
||||||
// Deprecated: use [hooks.Request] instead.
|
|
||||||
//
|
|
||||||
//go:fix inline
|
|
||||||
type HookPluginData = hooks.Request
|
|
||||||
|
|
||||||
// RunCLICommandHooks is the entrypoint into the hooks execution flow after
|
|
||||||
// a main CLI command was executed. It calls the hook subcommand for all
|
|
||||||
// present CLI plugins that declare support for hooks in their metadata and
|
|
||||||
// parses/prints their responses.
|
|
||||||
func RunCLICommandHooks(ctx context.Context, dockerCLI config.Provider, rootCmd, subCommand *cobra.Command, cmdErrorMessage string) {
|
|
||||||
commandName := strings.TrimPrefix(subCommand.CommandPath(), rootCmd.Name()+" ")
|
|
||||||
flags := getCommandFlags(subCommand)
|
|
||||||
|
|
||||||
runHooks(ctx, dockerCLI.ConfigFile(), rootCmd, subCommand, commandName, flags, cmdErrorMessage)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RunPluginHooks is the entrypoint for the hooks execution flow
|
|
||||||
// after a plugin command was just executed by the CLI.
|
|
||||||
func RunPluginHooks(ctx context.Context, dockerCLI config.Provider, rootCmd, subCommand *cobra.Command, args []string, cmdErrorMessage string) {
|
|
||||||
commandName := strings.Join(args, " ")
|
|
||||||
flags := getNaiveFlags(args)
|
|
||||||
|
|
||||||
runHooks(ctx, dockerCLI.ConfigFile(), rootCmd, subCommand, commandName, flags, cmdErrorMessage)
|
|
||||||
}
|
|
||||||
|
|
||||||
func runHooks(ctx context.Context, cfg *configfile.ConfigFile, rootCmd, subCommand *cobra.Command, invokedCommand string, flags map[string]string, cmdErrorMessage string) {
|
|
||||||
nextSteps := invokeAndCollectHooks(ctx, cfg, rootCmd, subCommand, invokedCommand, flags, cmdErrorMessage)
|
|
||||||
hooks.PrintNextSteps(subCommand.ErrOrStderr(), nextSteps)
|
|
||||||
}
|
|
||||||
|
|
||||||
func invokeAndCollectHooks(ctx context.Context, cfg *configfile.ConfigFile, rootCmd, subCmd *cobra.Command, subCmdStr string, flags map[string]string, cmdErrorMessage string) []string {
|
|
||||||
if ctx.Err() != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
pluginsCfg := cfg.Plugins
|
|
||||||
if pluginsCfg == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
pluginDirs := getPluginDirs(cfg)
|
|
||||||
nextSteps := make([]string, 0, len(pluginsCfg))
|
|
||||||
|
|
||||||
tryInvokeHook := func(pluginName string, pluginCfg map[string]string) (messages []string, ok bool, err error) {
|
|
||||||
match, matched := pluginMatch(pluginCfg, subCmdStr, cmdErrorMessage)
|
|
||||||
if !matched {
|
|
||||||
return nil, false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
p, err := getPlugin(pluginName, pluginDirs, rootCmd)
|
|
||||||
if err != nil {
|
|
||||||
return nil, false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := p.RunHook(ctx, hooks.Request{
|
|
||||||
RootCmd: match,
|
|
||||||
Flags: flags,
|
|
||||||
CommandError: cmdErrorMessage,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var message hooks.Response
|
|
||||||
if err := json.Unmarshal(resp, &message); err != nil {
|
|
||||||
return nil, false, fmt.Errorf("failed to unmarshal hook response (%q): %w", string(resp), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// currently the only hook type
|
|
||||||
if message.Type != hooks.NextSteps {
|
|
||||||
return nil, false, errors.New("unexpected hook response type: " + strconv.Itoa(int(message.Type)))
|
|
||||||
}
|
|
||||||
|
|
||||||
messages, err = hooks.ParseTemplate(message.Template, subCmd)
|
|
||||||
if err != nil {
|
|
||||||
return nil, false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return messages, true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
for pluginName, pluginCfg := range pluginsCfg {
|
|
||||||
messages, ok, err := tryInvokeHook(pluginName, pluginCfg)
|
|
||||||
if err != nil {
|
|
||||||
// skip misbehaving plugins, but don't halt execution
|
|
||||||
logrus.WithFields(logrus.Fields{
|
|
||||||
"error": err,
|
|
||||||
"plugin": pluginName,
|
|
||||||
}).Debug("Plugin hook invocation failed")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
var appended bool
|
|
||||||
nextSteps, appended = appendNextSteps(nextSteps, messages)
|
|
||||||
if !appended {
|
|
||||||
logrus.WithFields(logrus.Fields{
|
|
||||||
"plugin": pluginName,
|
|
||||||
}).Debug("Plugin responded with an empty hook message; ignoring")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nextSteps
|
|
||||||
}
|
|
||||||
|
|
||||||
// appendNextSteps appends the processed hook output to the nextSteps slice.
|
|
||||||
// If the processed hook output is empty, it is not appended.
|
|
||||||
// Empty lines are not stripped if there's at least one non-empty line.
|
|
||||||
func appendNextSteps(nextSteps []string, processed []string) ([]string, bool) {
|
|
||||||
empty := true
|
|
||||||
for _, l := range processed {
|
|
||||||
if strings.TrimSpace(l) != "" {
|
|
||||||
empty = false
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if empty {
|
|
||||||
return nextSteps, false
|
|
||||||
}
|
|
||||||
|
|
||||||
return append(nextSteps, processed...), true
|
|
||||||
}
|
|
||||||
|
|
||||||
// pluginMatch takes a plugin configuration and a string representing the
|
|
||||||
// command being executed (such as 'image ls' – the root 'docker' is omitted)
|
|
||||||
// and, if the configuration includes a hook for the invoked command, returns
|
|
||||||
// the configured hook string.
|
|
||||||
//
|
|
||||||
// Plugins can declare two types of hooks in their configuration:
|
|
||||||
// - "hooks": fires on every command invocation (success or failure)
|
|
||||||
// - "error-hooks": fires only when a command fails (cmdErrorMessage is non-empty)
|
|
||||||
func pluginMatch(pluginCfg map[string]string, subCmd string, cmdErrorMessage string) (string, bool) {
|
|
||||||
// Check "hooks" first — these always fire regardless of command outcome.
|
|
||||||
if match, ok := matchHookConfig(pluginCfg["hooks"], subCmd); ok {
|
|
||||||
return match, true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check "error-hooks" — these only fire when there was an error.
|
|
||||||
if cmdErrorMessage != "" {
|
|
||||||
if match, ok := matchHookConfig(pluginCfg["error-hooks"], subCmd); ok {
|
|
||||||
return match, true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
|
|
||||||
// matchHookConfig checks if a comma-separated hook configuration string
|
|
||||||
// contains a prefix match for the given subcommand.
|
|
||||||
func matchHookConfig(configuredHooks string, subCmd string) (string, bool) {
|
|
||||||
if configuredHooks == "" {
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
|
|
||||||
for hookCmd := range strings.SplitSeq(configuredHooks, ",") {
|
|
||||||
if hookMatch(hookCmd, subCmd) {
|
|
||||||
return hookCmd, true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
|
|
||||||
func hookMatch(hookCmd, subCmd string) bool {
|
|
||||||
hookCmdTokens := strings.Split(hookCmd, " ")
|
|
||||||
subCmdTokens := strings.Split(subCmd, " ")
|
|
||||||
|
|
||||||
if len(hookCmdTokens) > len(subCmdTokens) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, v := range hookCmdTokens {
|
|
||||||
if v != subCmdTokens[i] {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func getCommandFlags(cmd *cobra.Command) map[string]string {
|
|
||||||
flags := make(map[string]string)
|
|
||||||
cmd.Flags().Visit(func(f *pflag.Flag) {
|
|
||||||
var fValue string
|
|
||||||
if f.Value.Type() == "bool" {
|
|
||||||
fValue = f.Value.String()
|
|
||||||
}
|
|
||||||
flags[f.Name] = fValue
|
|
||||||
})
|
|
||||||
return flags
|
|
||||||
}
|
|
||||||
|
|
||||||
// getNaiveFlags string-matches argv and parses them into a map.
|
|
||||||
// This is used when calling hooks after a plugin command, since
|
|
||||||
// in this case we can't rely on the cobra command tree to parse
|
|
||||||
// flags in this case. In this case, no values are ever passed,
|
|
||||||
// since we don't have enough information to process them.
|
|
||||||
func getNaiveFlags(args []string) map[string]string {
|
|
||||||
flags := make(map[string]string)
|
|
||||||
for _, arg := range args {
|
|
||||||
if strings.HasPrefix(arg, "--") {
|
|
||||||
flags[arg[2:]] = ""
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(arg, "-") {
|
|
||||||
flags[arg[1:]] = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return flags
|
|
||||||
}
|
|
||||||
@@ -1,375 +0,0 @@
|
|||||||
package manager
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/docker/cli/cli/config/configfile"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"gotest.tools/v3/assert"
|
|
||||||
is "gotest.tools/v3/assert/cmp"
|
|
||||||
)
|
|
||||||
|
|
||||||
type fakeConfigProvider struct {
|
|
||||||
cfg *configfile.ConfigFile
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *fakeConfigProvider) ConfigFile() *configfile.ConfigFile {
|
|
||||||
return f.cfg
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetNaiveFlags(t *testing.T) {
|
|
||||||
testCases := []struct {
|
|
||||||
args []string
|
|
||||||
expectedFlags map[string]string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
args: []string{"docker"},
|
|
||||||
expectedFlags: map[string]string{},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
args: []string{"docker", "build", "-q", "--file", "test.Dockerfile", "."},
|
|
||||||
expectedFlags: map[string]string{
|
|
||||||
"q": "",
|
|
||||||
"file": "",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
args: []string{"docker", "--context", "a-context", "pull", "-q", "--progress", "auto", "alpine"},
|
|
||||||
expectedFlags: map[string]string{
|
|
||||||
"context": "",
|
|
||||||
"q": "",
|
|
||||||
"progress": "",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range testCases {
|
|
||||||
assert.DeepEqual(t, getNaiveFlags(tc.args), tc.expectedFlags)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPluginMatch(t *testing.T) {
|
|
||||||
testCases := []struct {
|
|
||||||
doc string
|
|
||||||
commandString string
|
|
||||||
pluginConfig map[string]string
|
|
||||||
cmdErrorMessage string
|
|
||||||
expectedMatch string
|
|
||||||
expectedOk bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
doc: "hooks prefix match",
|
|
||||||
commandString: "image ls",
|
|
||||||
pluginConfig: map[string]string{
|
|
||||||
"hooks": "image",
|
|
||||||
},
|
|
||||||
expectedMatch: "image",
|
|
||||||
expectedOk: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
doc: "hooks no match",
|
|
||||||
commandString: "context ls",
|
|
||||||
pluginConfig: map[string]string{
|
|
||||||
"hooks": "build",
|
|
||||||
},
|
|
||||||
expectedMatch: "",
|
|
||||||
expectedOk: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
doc: "hooks exact match",
|
|
||||||
commandString: "context ls",
|
|
||||||
pluginConfig: map[string]string{
|
|
||||||
"hooks": "context ls",
|
|
||||||
},
|
|
||||||
expectedMatch: "context ls",
|
|
||||||
expectedOk: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
doc: "hooks first match wins",
|
|
||||||
commandString: "image ls",
|
|
||||||
pluginConfig: map[string]string{
|
|
||||||
"hooks": "image ls,image",
|
|
||||||
},
|
|
||||||
expectedMatch: "image ls",
|
|
||||||
expectedOk: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
doc: "hooks empty string",
|
|
||||||
commandString: "image ls",
|
|
||||||
pluginConfig: map[string]string{
|
|
||||||
"hooks": "",
|
|
||||||
},
|
|
||||||
expectedMatch: "",
|
|
||||||
expectedOk: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
doc: "hooks partial token no match",
|
|
||||||
commandString: "image inspect",
|
|
||||||
pluginConfig: map[string]string{
|
|
||||||
"hooks": "image i",
|
|
||||||
},
|
|
||||||
expectedMatch: "",
|
|
||||||
expectedOk: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
doc: "hooks prefix token match",
|
|
||||||
commandString: "image inspect",
|
|
||||||
pluginConfig: map[string]string{
|
|
||||||
"hooks": "image",
|
|
||||||
},
|
|
||||||
expectedMatch: "image",
|
|
||||||
expectedOk: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
doc: "error-hooks match on error",
|
|
||||||
commandString: "build",
|
|
||||||
pluginConfig: map[string]string{
|
|
||||||
"error-hooks": "build",
|
|
||||||
},
|
|
||||||
cmdErrorMessage: "exit status 1",
|
|
||||||
expectedMatch: "build",
|
|
||||||
expectedOk: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
doc: "error-hooks no match on success",
|
|
||||||
commandString: "build",
|
|
||||||
pluginConfig: map[string]string{
|
|
||||||
"error-hooks": "build",
|
|
||||||
},
|
|
||||||
cmdErrorMessage: "",
|
|
||||||
expectedMatch: "",
|
|
||||||
expectedOk: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
doc: "error-hooks prefix match on error",
|
|
||||||
commandString: "compose up",
|
|
||||||
pluginConfig: map[string]string{
|
|
||||||
"error-hooks": "compose",
|
|
||||||
},
|
|
||||||
cmdErrorMessage: "exit status 1",
|
|
||||||
expectedMatch: "compose",
|
|
||||||
expectedOk: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
doc: "error-hooks no match for wrong command",
|
|
||||||
commandString: "pull",
|
|
||||||
pluginConfig: map[string]string{
|
|
||||||
"error-hooks": "build",
|
|
||||||
},
|
|
||||||
cmdErrorMessage: "exit status 1",
|
|
||||||
expectedMatch: "",
|
|
||||||
expectedOk: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
doc: "hooks takes precedence over error-hooks",
|
|
||||||
commandString: "build",
|
|
||||||
pluginConfig: map[string]string{
|
|
||||||
"hooks": "build",
|
|
||||||
"error-hooks": "build",
|
|
||||||
},
|
|
||||||
cmdErrorMessage: "exit status 1",
|
|
||||||
expectedMatch: "build",
|
|
||||||
expectedOk: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
doc: "hooks fires on success even with error-hooks configured",
|
|
||||||
commandString: "build",
|
|
||||||
pluginConfig: map[string]string{
|
|
||||||
"hooks": "build",
|
|
||||||
"error-hooks": "build",
|
|
||||||
},
|
|
||||||
cmdErrorMessage: "",
|
|
||||||
expectedMatch: "build",
|
|
||||||
expectedOk: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
doc: "error-hooks with multiple commands",
|
|
||||||
commandString: "compose up",
|
|
||||||
pluginConfig: map[string]string{
|
|
||||||
"error-hooks": "build,compose up,pull",
|
|
||||||
},
|
|
||||||
cmdErrorMessage: "exit status 1",
|
|
||||||
expectedMatch: "compose up",
|
|
||||||
expectedOk: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range testCases {
|
|
||||||
t.Run(tc.doc, func(t *testing.T) {
|
|
||||||
match, ok := pluginMatch(tc.pluginConfig, tc.commandString, tc.cmdErrorMessage)
|
|
||||||
assert.Equal(t, ok, tc.expectedOk)
|
|
||||||
assert.Equal(t, match, tc.expectedMatch)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMatchHookConfig(t *testing.T) {
|
|
||||||
testCases := []struct {
|
|
||||||
doc string
|
|
||||||
configuredHooks string
|
|
||||||
subCmd string
|
|
||||||
expectedMatch string
|
|
||||||
expectedOk bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
doc: "empty config",
|
|
||||||
configuredHooks: "",
|
|
||||||
subCmd: "build",
|
|
||||||
expectedMatch: "",
|
|
||||||
expectedOk: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
doc: "exact match",
|
|
||||||
configuredHooks: "build",
|
|
||||||
subCmd: "build",
|
|
||||||
expectedMatch: "build",
|
|
||||||
expectedOk: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
doc: "prefix match",
|
|
||||||
configuredHooks: "image",
|
|
||||||
subCmd: "image ls",
|
|
||||||
expectedMatch: "image",
|
|
||||||
expectedOk: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
doc: "comma-separated match",
|
|
||||||
configuredHooks: "pull,build,push",
|
|
||||||
subCmd: "build",
|
|
||||||
expectedMatch: "build",
|
|
||||||
expectedOk: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
doc: "no match",
|
|
||||||
configuredHooks: "pull,push",
|
|
||||||
subCmd: "build",
|
|
||||||
expectedMatch: "",
|
|
||||||
expectedOk: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range testCases {
|
|
||||||
t.Run(tc.doc, func(t *testing.T) {
|
|
||||||
match, ok := matchHookConfig(tc.configuredHooks, tc.subCmd)
|
|
||||||
assert.Equal(t, ok, tc.expectedOk)
|
|
||||||
assert.Equal(t, match, tc.expectedMatch)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAppendNextSteps(t *testing.T) {
|
|
||||||
testCases := []struct {
|
|
||||||
processed []string
|
|
||||||
expectedOut []string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
processed: []string{},
|
|
||||||
expectedOut: []string{},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
processed: []string{"", ""},
|
|
||||||
expectedOut: []string{},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
processed: []string{"Some hint", "", "Some other hint"},
|
|
||||||
expectedOut: []string{"Some hint", "", "Some other hint"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
processed: []string{"Hint 1", "Hint 2"},
|
|
||||||
expectedOut: []string{"Hint 1", "Hint 2"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range testCases {
|
|
||||||
t.Run("", func(t *testing.T) {
|
|
||||||
got, appended := appendNextSteps([]string{}, tc.processed)
|
|
||||||
assert.Check(t, is.DeepEqual(got, tc.expectedOut))
|
|
||||||
assert.Check(t, is.Equal(appended, len(got) > 0))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRunPluginHooksPassesErrorMessage(t *testing.T) {
|
|
||||||
cfg := configfile.New("")
|
|
||||||
cfg.Plugins = map[string]map[string]string{
|
|
||||||
"test-plugin": {"hooks": "build"},
|
|
||||||
}
|
|
||||||
provider := &fakeConfigProvider{cfg: cfg}
|
|
||||||
root := &cobra.Command{Use: "docker"}
|
|
||||||
sub := &cobra.Command{Use: "build"}
|
|
||||||
root.AddCommand(sub)
|
|
||||||
|
|
||||||
// Should not panic with empty error message (success case)
|
|
||||||
RunPluginHooks(context.Background(), provider, root, sub, []string{"build"}, "")
|
|
||||||
|
|
||||||
// Should not panic with non-empty error message (failure case)
|
|
||||||
RunPluginHooks(context.Background(), provider, root, sub, []string{"build"}, "exit status 1")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRunPluginHooksErrorHooks(t *testing.T) {
|
|
||||||
cfg := configfile.New("")
|
|
||||||
cfg.Plugins = map[string]map[string]string{
|
|
||||||
"test-plugin": {"error-hooks": "build"},
|
|
||||||
}
|
|
||||||
provider := &fakeConfigProvider{cfg: cfg}
|
|
||||||
root := &cobra.Command{Use: "docker"}
|
|
||||||
sub := &cobra.Command{Use: "build"}
|
|
||||||
root.AddCommand(sub)
|
|
||||||
|
|
||||||
// Should not panic — error-hooks with error message
|
|
||||||
RunPluginHooks(context.Background(), provider, root, sub, []string{"build"}, "exit status 1")
|
|
||||||
|
|
||||||
// Should not panic — error-hooks with no error (should be skipped)
|
|
||||||
RunPluginHooks(context.Background(), provider, root, sub, []string{"build"}, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestInvokeAndCollectHooksErrorHooksSkippedOnSuccess(t *testing.T) {
|
|
||||||
cfg := configfile.New("")
|
|
||||||
cfg.Plugins = map[string]map[string]string{
|
|
||||||
"nonexistent": {"error-hooks": "build"},
|
|
||||||
}
|
|
||||||
root := &cobra.Command{Use: "docker"}
|
|
||||||
sub := &cobra.Command{Use: "build"}
|
|
||||||
root.AddCommand(sub)
|
|
||||||
|
|
||||||
// On success, error-hooks should not match, so the plugin
|
|
||||||
// binary is never looked up and no results are returned.
|
|
||||||
result := invokeAndCollectHooks(
|
|
||||||
context.Background(), cfg, root, sub,
|
|
||||||
"build", map[string]string{}, "",
|
|
||||||
)
|
|
||||||
assert.Check(t, is.Len(result, 0))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestInvokeAndCollectHooksNoPlugins(t *testing.T) {
|
|
||||||
cfg := configfile.New("")
|
|
||||||
root := &cobra.Command{Use: "docker"}
|
|
||||||
sub := &cobra.Command{Use: "build"}
|
|
||||||
root.AddCommand(sub)
|
|
||||||
|
|
||||||
result := invokeAndCollectHooks(
|
|
||||||
context.Background(), cfg, root, sub,
|
|
||||||
"build", map[string]string{}, "some error",
|
|
||||||
)
|
|
||||||
assert.Check(t, is.Len(result, 0))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestInvokeAndCollectHooksCancelledContext(t *testing.T) {
|
|
||||||
cfg := configfile.New("")
|
|
||||||
cfg.Plugins = map[string]map[string]string{
|
|
||||||
"test-plugin": {"hooks": "build"},
|
|
||||||
}
|
|
||||||
root := &cobra.Command{Use: "docker"}
|
|
||||||
sub := &cobra.Command{Use: "build"}
|
|
||||||
root.AddCommand(sub)
|
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
cancel() // cancel immediately
|
|
||||||
|
|
||||||
result := invokeAndCollectHooks(
|
|
||||||
ctx, cfg, root, sub,
|
|
||||||
"build", map[string]string{}, "exit status 1",
|
|
||||||
)
|
|
||||||
assert.Check(t, is.Nil(result))
|
|
||||||
}
|
|
||||||
@@ -1,112 +1,123 @@
|
|||||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
|
||||||
//go:build go1.26
|
|
||||||
|
|
||||||
package manager
|
package manager
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
slices "slices"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/containerd/errdefs"
|
"github.com/docker/cli/cli/command"
|
||||||
"github.com/docker/cli/cli-plugins/metadata"
|
|
||||||
"github.com/docker/cli/cli/config"
|
"github.com/docker/cli/cli/config"
|
||||||
"github.com/docker/cli/cli/config/configfile"
|
|
||||||
"github.com/docker/cli/cli/debug"
|
|
||||||
"github.com/fvbommel/sortorder"
|
"github.com/fvbommel/sortorder"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"golang.org/x/sync/errgroup"
|
"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.
|
// errPluginNotFound is the error returned when a plugin could not be found.
|
||||||
type errPluginNotFound string
|
type errPluginNotFound string
|
||||||
|
|
||||||
func (errPluginNotFound) NotFound() {}
|
func (e errPluginNotFound) NotFound() {}
|
||||||
|
|
||||||
func (e errPluginNotFound) Error() string {
|
func (e errPluginNotFound) Error() string {
|
||||||
return "Error: No such CLI plugin: " + string(e)
|
return "Error: No such CLI plugin: " + string(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
// getPluginDirs returns the platform-specific locations to search for plugins
|
type notFound interface{ NotFound() }
|
||||||
// in order of preference.
|
|
||||||
//
|
|
||||||
// Plugin-discovery is performed in the following order of preference:
|
|
||||||
//
|
|
||||||
// 1. The "cli-plugins" directory inside the CLIs [config.Path] (usually "~/.docker/cli-plugins").
|
|
||||||
// 2. Additional plugin directories as configured through [ConfigFile.CLIPluginsExtraDirs].
|
|
||||||
// 3. Platform-specific defaultSystemPluginDirs.
|
|
||||||
//
|
|
||||||
// [ConfigFile.CLIPluginsExtraDirs]: https://pkg.go.dev/github.com/docker/cli@v26.1.4+incompatible/cli/config/configfile#ConfigFile.CLIPluginsExtraDirs
|
|
||||||
func getPluginDirs(cfg *configfile.ConfigFile) []string {
|
|
||||||
var pluginDirs []string
|
|
||||||
|
|
||||||
if cfg != nil {
|
// IsNotFound is true if the given error is due to a plugin not being found.
|
||||||
pluginDirs = append(pluginDirs, cfg.CLIPluginsExtraDirs...)
|
func IsNotFound(err error) bool {
|
||||||
|
if e, ok := err.(*pluginError); ok {
|
||||||
|
err = e.Cause()
|
||||||
}
|
}
|
||||||
pluginDir := filepath.Join(config.Dir(), "cli-plugins")
|
_, ok := err.(notFound)
|
||||||
pluginDirs = append(pluginDirs, pluginDir)
|
return ok
|
||||||
pluginDirs = append(pluginDirs, defaultSystemPluginDirs...)
|
|
||||||
return pluginDirs
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func addPluginCandidatesFromDir(res map[string][]string, d string) {
|
func getPluginDirs(dockerCli command.Cli) ([]string, error) {
|
||||||
dentries, err := os.ReadDir(d)
|
var pluginDirs []string
|
||||||
// Silently ignore any directories which we cannot list (e.g. due to
|
|
||||||
// permissions or anything else) or which is not a directory
|
if cfg := dockerCli.ConfigFile(); cfg != nil {
|
||||||
|
pluginDirs = append(pluginDirs, cfg.CLIPluginsExtraDirs...)
|
||||||
|
}
|
||||||
|
pluginDir, err := config.Path("cli-plugins")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pluginDirs = append(pluginDirs, pluginDir)
|
||||||
|
pluginDirs = append(pluginDirs, defaultSystemPluginDirs...)
|
||||||
|
return pluginDirs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func addPluginCandidatesFromDir(res map[string][]string, d string) error {
|
||||||
|
dentries, err := os.ReadDir(d)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
for _, dentry := range dentries {
|
for _, dentry := range dentries {
|
||||||
switch mode := dentry.Type() & os.ModeType; mode { //nolint:exhaustive,nolintlint // no need to include all possible file-modes in this list
|
switch dentry.Type() & os.ModeType {
|
||||||
case os.ModeSymlink:
|
case 0, os.ModeSymlink:
|
||||||
if !debug.IsEnabled() {
|
// Regular file or symlink, keep going
|
||||||
// Skip broken symlinks unless debug is enabled. With debug
|
|
||||||
// enabled, this will print a warning in "docker info".
|
|
||||||
if _, err := os.Stat(filepath.Join(d, dentry.Name())); errors.Is(err, os.ErrNotExist) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case 0:
|
|
||||||
// Regular file, keep going
|
|
||||||
default:
|
default:
|
||||||
// Something else, ignore.
|
// Something else, ignore.
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
name := dentry.Name()
|
name := dentry.Name()
|
||||||
if !strings.HasPrefix(name, metadata.NamePrefix) {
|
if !strings.HasPrefix(name, NamePrefix) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
name = strings.TrimPrefix(name, metadata.NamePrefix)
|
name = strings.TrimPrefix(name, NamePrefix)
|
||||||
var err error
|
var err error
|
||||||
if name, err = trimExeSuffix(name); err != nil {
|
if name, err = trimExeSuffix(name); err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
res[name] = append(res[name], filepath.Join(d, dentry.Name()))
|
res[name] = append(res[name], filepath.Join(d, dentry.Name()))
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// listPluginCandidates returns a map from plugin name to the list of (unvalidated) Candidates. The list is in descending order of priority.
|
// listPluginCandidates returns a map from plugin name to the list of (unvalidated) Candidates. The list is in descending order of priority.
|
||||||
func listPluginCandidates(dirs []string) map[string][]string {
|
func listPluginCandidates(dirs []string) (map[string][]string, error) {
|
||||||
result := make(map[string][]string)
|
result := make(map[string][]string)
|
||||||
for _, d := range dirs {
|
for _, d := range dirs {
|
||||||
addPluginCandidatesFromDir(result, d)
|
// Silently ignore any directories which we cannot
|
||||||
|
// Stat (e.g. due to permissions or anything else) or
|
||||||
|
// which is not a directory.
|
||||||
|
if fi, err := os.Stat(d); err != nil || !fi.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := addPluginCandidatesFromDir(result, d); err != nil {
|
||||||
|
// Silently ignore paths which don't exist.
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return nil, err // Or return partial result?
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return result
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPlugin returns a plugin on the system by its name
|
// GetPlugin returns a plugin on the system by its name
|
||||||
func GetPlugin(name string, dockerCLI config.Provider, rootcmd *cobra.Command) (*Plugin, error) {
|
func GetPlugin(name string, dockerCli command.Cli, rootcmd *cobra.Command) (*Plugin, error) {
|
||||||
pluginDirs := getPluginDirs(dockerCLI.ConfigFile())
|
pluginDirs, err := getPluginDirs(dockerCli)
|
||||||
return getPlugin(name, pluginDirs, rootcmd)
|
if err != nil {
|
||||||
}
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
candidates, err := listPluginCandidates(pluginDirs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
func getPlugin(name string, pluginDirs []string, rootcmd *cobra.Command) (*Plugin, error) {
|
|
||||||
candidates := listPluginCandidates(pluginDirs)
|
|
||||||
if paths, ok := candidates[name]; ok {
|
if paths, ok := candidates[name]; ok {
|
||||||
if len(paths) == 0 {
|
if len(paths) == 0 {
|
||||||
return nil, errPluginNotFound(name)
|
return nil, errPluginNotFound(name)
|
||||||
@@ -116,7 +127,7 @@ func getPlugin(name string, pluginDirs []string, rootcmd *cobra.Command) (*Plugi
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if !errdefs.IsNotFound(p.Err) {
|
if !IsNotFound(p.Err) {
|
||||||
p.ShadowedPaths = paths[1:]
|
p.ShadowedPaths = paths[1:]
|
||||||
}
|
}
|
||||||
return &p, nil
|
return &p, nil
|
||||||
@@ -126,21 +137,20 @@ func getPlugin(name string, pluginDirs []string, rootcmd *cobra.Command) (*Plugi
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ListPlugins produces a list of the plugins available on the system
|
// ListPlugins produces a list of the plugins available on the system
|
||||||
func ListPlugins(dockerCli config.Provider, rootcmd *cobra.Command) ([]Plugin, error) {
|
func ListPlugins(dockerCli command.Cli, rootcmd *cobra.Command) ([]Plugin, error) {
|
||||||
pluginDirs := getPluginDirs(dockerCli.ConfigFile())
|
pluginDirs, err := getPluginDirs(dockerCli)
|
||||||
candidates := listPluginCandidates(pluginDirs)
|
if err != nil {
|
||||||
if len(candidates) == 0 {
|
return nil, err
|
||||||
return nil, nil
|
}
|
||||||
|
|
||||||
|
candidates, err := listPluginCandidates(pluginDirs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var plugins []Plugin
|
var plugins []Plugin
|
||||||
var mu sync.Mutex
|
var mu sync.Mutex
|
||||||
ctx := rootcmd.Context()
|
eg, _ := errgroup.WithContext(context.TODO())
|
||||||
if ctx == nil {
|
|
||||||
// Fallback, mostly for tests that pass a bare cobra.command
|
|
||||||
ctx = context.Background()
|
|
||||||
}
|
|
||||||
eg, _ := errgroup.WithContext(ctx)
|
|
||||||
cmds := rootcmd.Commands()
|
cmds := rootcmd.Commands()
|
||||||
for _, paths := range candidates {
|
for _, paths := range candidates {
|
||||||
func(paths []string) {
|
func(paths []string) {
|
||||||
@@ -153,7 +163,7 @@ func ListPlugins(dockerCli config.Provider, rootcmd *cobra.Command) ([]Plugin, e
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if !errdefs.IsNotFound(p.Err) {
|
if !IsNotFound(p.Err) {
|
||||||
p.ShadowedPaths = paths[1:]
|
p.ShadowedPaths = paths[1:]
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
defer mu.Unlock()
|
defer mu.Unlock()
|
||||||
@@ -167,28 +177,31 @@ func ListPlugins(dockerCli config.Provider, rootcmd *cobra.Command) ([]Plugin, e
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
slices.SortFunc(plugins, func(a, b Plugin) int {
|
sort.Slice(plugins, func(i, j int) bool {
|
||||||
return sortorder.NaturalCompare(a.Name, b.Name)
|
return sortorder.NaturalLess(plugins[i].Name, plugins[j].Name)
|
||||||
})
|
})
|
||||||
|
|
||||||
return plugins, nil
|
return plugins, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// PluginRunCommand returns an [os/exec.Cmd] which when [os/exec.Cmd.Run] will execute the named plugin.
|
// PluginRunCommand returns an "os/exec".Cmd which when .Run() will execute the named plugin.
|
||||||
// The rootcmd argument is referenced to determine the set of builtin commands in order to detect conflicts.
|
// The rootcmd argument is referenced to determine the set of builtin commands in order to detect conficts.
|
||||||
// The error returned satisfies the [errdefs.IsNotFound] predicate if no plugin was found or if the first candidate plugin was invalid somehow.
|
// The error returned satisfies the IsNotFound() predicate if no plugin was found or if the first candidate plugin was invalid somehow.
|
||||||
func PluginRunCommand(dockerCli config.Provider, name string, rootcmd *cobra.Command) (*exec.Cmd, error) {
|
func PluginRunCommand(dockerCli command.Cli, name string, rootcmd *cobra.Command) (*exec.Cmd, error) {
|
||||||
// This uses the full original args, not the args which may
|
// This uses the full original args, not the args which may
|
||||||
// have been provided by cobra to our caller. This is because
|
// have been provided by cobra to our caller. This is because
|
||||||
// they lack e.g. global options which we must propagate here.
|
// they lack e.g. global options which we must propagate here.
|
||||||
args := os.Args[1:]
|
args := os.Args[1:]
|
||||||
if !isValidPluginName(name) {
|
if !pluginNameRe.MatchString(name) {
|
||||||
// We treat this as "not found" so that callers will
|
// We treat this as "not found" so that callers will
|
||||||
// fallback to their "invalid" command path.
|
// fallback to their "invalid" command path.
|
||||||
return nil, errPluginNotFound(name)
|
return nil, errPluginNotFound(name)
|
||||||
}
|
}
|
||||||
exename := addExeSuffix(metadata.NamePrefix + name)
|
exename := addExeSuffix(NamePrefix + name)
|
||||||
pluginDirs := getPluginDirs(dockerCli.ConfigFile())
|
pluginDirs, err := getPluginDirs(dockerCli)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
for _, d := range pluginDirs {
|
for _, d := range pluginDirs {
|
||||||
path := filepath.Join(d, exename)
|
path := filepath.Join(d, exename)
|
||||||
@@ -210,8 +223,7 @@ func PluginRunCommand(dockerCli config.Provider, name string, rootcmd *cobra.Com
|
|||||||
// TODO: why are we not returning plugin.Err?
|
// TODO: why are we not returning plugin.Err?
|
||||||
return nil, errPluginNotFound(name)
|
return nil, errPluginNotFound(name)
|
||||||
}
|
}
|
||||||
cmd := exec.Command(plugin.Path, args...) // #nosec G204 -- ignore "Subprocess launched with a potential tainted input or cmd arguments"
|
cmd := exec.Command(plugin.Path, args...)
|
||||||
|
|
||||||
// Using dockerCli.{In,Out,Err}() here results in a hang until something is input.
|
// Using dockerCli.{In,Out,Err}() here results in a hang until something is input.
|
||||||
// See: - https://github.com/golang/go/issues/10338
|
// See: - https://github.com/golang/go/issues/10338
|
||||||
// - https://github.com/golang/go/commit/d000e8742a173aa0659584aa01b7ba2834ba28ab
|
// - https://github.com/golang/go/commit/d000e8742a173aa0659584aa01b7ba2834ba28ab
|
||||||
@@ -221,8 +233,8 @@ func PluginRunCommand(dockerCli config.Provider, name string, rootcmd *cobra.Com
|
|||||||
cmd.Stdout = os.Stdout
|
cmd.Stdout = os.Stdout
|
||||||
cmd.Stderr = os.Stderr
|
cmd.Stderr = os.Stderr
|
||||||
|
|
||||||
cmd.Env = append(cmd.Environ(), metadata.ReexecEnvvar+"="+os.Args[0])
|
cmd.Env = os.Environ()
|
||||||
cmd.Env = appendPluginResourceAttributesEnvvar(cmd.Env, rootcmd, plugin)
|
cmd.Env = append(cmd.Env, ReexecEnvvar+"="+os.Args[0])
|
||||||
|
|
||||||
return cmd, nil
|
return cmd, nil
|
||||||
}
|
}
|
||||||
@@ -231,5 +243,5 @@ func PluginRunCommand(dockerCli config.Provider, name string, rootcmd *cobra.Com
|
|||||||
|
|
||||||
// IsPluginCommand checks if the given cmd is a plugin-stub.
|
// IsPluginCommand checks if the given cmd is a plugin-stub.
|
||||||
func IsPluginCommand(cmd *cobra.Command) bool {
|
func IsPluginCommand(cmd *cobra.Command) bool {
|
||||||
return cmd.Annotations[metadata.CommandAnnotationPlugin] == "true"
|
return cmd.Annotations[CommandAnnotationPlugin] == "true"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
package manager
|
package manager
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/containerd/errdefs"
|
|
||||||
"github.com/docker/cli/cli/config"
|
"github.com/docker/cli/cli/config"
|
||||||
"github.com/docker/cli/cli/config/configfile"
|
"github.com/docker/cli/cli/config/configfile"
|
||||||
"github.com/docker/cli/internal/test"
|
"github.com/docker/cli/internal/test"
|
||||||
@@ -38,7 +36,7 @@ func TestListPluginCandidates(t *testing.T) {
|
|||||||
"plugins3-target", // Will be referenced as a symlink from below
|
"plugins3-target", // Will be referenced as a symlink from below
|
||||||
fs.WithFile("docker-plugin1", ""),
|
fs.WithFile("docker-plugin1", ""),
|
||||||
fs.WithDir("ignored3"),
|
fs.WithDir("ignored3"),
|
||||||
fs.WithSymlink("docker-brokensymlink", "broken"), // A broken symlink is ignored
|
fs.WithSymlink("docker-brokensymlink", "broken"), // A broken symlink is still a candidate (but would fail tests later)
|
||||||
fs.WithFile("non-plugin-symlinked", ""), // This shouldn't appear, but ...
|
fs.WithFile("non-plugin-symlinked", ""), // This shouldn't appear, but ...
|
||||||
fs.WithSymlink("docker-symlinked", "non-plugin-symlinked"), // ... this link to it should.
|
fs.WithSymlink("docker-symlinked", "non-plugin-symlinked"), // ... this link to it should.
|
||||||
),
|
),
|
||||||
@@ -48,12 +46,13 @@ func TestListPluginCandidates(t *testing.T) {
|
|||||||
)
|
)
|
||||||
defer dir.Remove()
|
defer dir.Remove()
|
||||||
|
|
||||||
dirs := make([]string, 0, 6)
|
var dirs []string
|
||||||
for _, d := range []string{"plugins1", "nonexistent", "plugins2", "plugins3", "plugins4", "plugins5"} {
|
for _, d := range []string{"plugins1", "nonexistent", "plugins2", "plugins3", "plugins4", "plugins5"} {
|
||||||
dirs = append(dirs, dir.Join(d))
|
dirs = append(dirs, dir.Join(d))
|
||||||
}
|
}
|
||||||
|
|
||||||
candidates := listPluginCandidates(dirs)
|
candidates, err := listPluginCandidates(dirs)
|
||||||
|
assert.NilError(t, err)
|
||||||
exp := map[string][]string{
|
exp := map[string][]string{
|
||||||
"plugin1": {
|
"plugin1": {
|
||||||
dir.Join("plugins1", "docker-plugin1"),
|
dir.Join("plugins1", "docker-plugin1"),
|
||||||
@@ -72,6 +71,9 @@ func TestListPluginCandidates(t *testing.T) {
|
|||||||
"hardlink2": {
|
"hardlink2": {
|
||||||
dir.Join("plugins2", "docker-hardlink2"),
|
dir.Join("plugins2", "docker-hardlink2"),
|
||||||
},
|
},
|
||||||
|
"brokensymlink": {
|
||||||
|
dir.Join("plugins3", "docker-brokensymlink"),
|
||||||
|
},
|
||||||
"symlinked": {
|
"symlinked": {
|
||||||
dir.Join("plugins3", "docker-symlinked"),
|
dir.Join("plugins3", "docker-symlinked"),
|
||||||
},
|
},
|
||||||
@@ -80,35 +82,6 @@ func TestListPluginCandidates(t *testing.T) {
|
|||||||
assert.DeepEqual(t, candidates, exp)
|
assert.DeepEqual(t, candidates, exp)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestListPluginCandidatesEmpty(t *testing.T) {
|
|
||||||
tmpDir := t.TempDir()
|
|
||||||
candidates := listPluginCandidates([]string{tmpDir, filepath.Join(tmpDir, "no-such-dir")})
|
|
||||||
assert.Assert(t, len(candidates) == 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Regression test for https://github.com/docker/cli/issues/5643.
|
|
||||||
// Check that inaccessible directories that come before accessible ones are ignored
|
|
||||||
// and do not prevent the latter from being processed.
|
|
||||||
func TestListPluginCandidatesInaccessibleDir(t *testing.T) {
|
|
||||||
dir := fs.NewDir(t, t.Name(),
|
|
||||||
fs.WithDir("no-perm", fs.WithMode(0)),
|
|
||||||
fs.WithDir("plugins",
|
|
||||||
fs.WithFile("docker-buildx", ""),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
defer dir.Remove()
|
|
||||||
|
|
||||||
candidates := listPluginCandidates([]string{
|
|
||||||
dir.Join("no-perm"),
|
|
||||||
dir.Join("plugins"),
|
|
||||||
})
|
|
||||||
assert.DeepEqual(t, candidates, map[string][]string{
|
|
||||||
"buildx": {
|
|
||||||
dir.Join("plugins", "docker-buildx"),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetPlugin(t *testing.T) {
|
func TestGetPlugin(t *testing.T) {
|
||||||
dir := fs.NewDir(t, t.Name(),
|
dir := fs.NewDir(t, t.Name(),
|
||||||
fs.WithFile("docker-bbb", `
|
fs.WithFile("docker-bbb", `
|
||||||
@@ -129,7 +102,7 @@ echo '{"SchemaVersion":"0.1.0"}'`, fs.WithMode(0o777)),
|
|||||||
|
|
||||||
_, err = GetPlugin("ccc", cli, &cobra.Command{})
|
_, err = GetPlugin("ccc", cli, &cobra.Command{})
|
||||||
assert.Error(t, err, "Error: No such CLI plugin: ccc")
|
assert.Error(t, err, "Error: No such CLI plugin: ccc")
|
||||||
assert.Assert(t, errdefs.IsNotFound(err))
|
assert.Assert(t, IsNotFound(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestListPluginsIsSorted(t *testing.T) {
|
func TestListPluginsIsSorted(t *testing.T) {
|
||||||
@@ -164,25 +137,30 @@ func TestErrPluginNotFound(t *testing.T) {
|
|||||||
var err error = errPluginNotFound("test")
|
var err error = errPluginNotFound("test")
|
||||||
err.(errPluginNotFound).NotFound()
|
err.(errPluginNotFound).NotFound()
|
||||||
assert.Error(t, err, "Error: No such CLI plugin: test")
|
assert.Error(t, err, "Error: No such CLI plugin: test")
|
||||||
assert.Assert(t, errdefs.IsNotFound(err))
|
assert.Assert(t, IsNotFound(err))
|
||||||
assert.Assert(t, !errdefs.IsNotFound(nil))
|
assert.Assert(t, !IsNotFound(nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetPluginDirs(t *testing.T) {
|
func TestGetPluginDirs(t *testing.T) {
|
||||||
cli := test.NewFakeCli(nil)
|
cli := test.NewFakeCli(nil)
|
||||||
|
|
||||||
pluginDir := filepath.Join(config.Dir(), "cli-plugins")
|
pluginDir, err := config.Path("cli-plugins")
|
||||||
|
assert.NilError(t, err)
|
||||||
expected := append([]string{pluginDir}, defaultSystemPluginDirs...)
|
expected := append([]string{pluginDir}, defaultSystemPluginDirs...)
|
||||||
|
|
||||||
pluginDirs := getPluginDirs(cli.ConfigFile())
|
var pluginDirs []string
|
||||||
|
pluginDirs, err = getPluginDirs(cli)
|
||||||
assert.Equal(t, strings.Join(expected, ":"), strings.Join(pluginDirs, ":"))
|
assert.Equal(t, strings.Join(expected, ":"), strings.Join(pluginDirs, ":"))
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
extras := make([]string, 0, 3+len(expected))
|
extras := []string{
|
||||||
extras = append(extras, "foo", "bar", "baz")
|
"foo", "bar", "baz",
|
||||||
|
}
|
||||||
expected = append(extras, expected...)
|
expected = append(extras, expected...)
|
||||||
cli.SetConfigFile(&configfile.ConfigFile{
|
cli.SetConfigFile(&configfile.ConfigFile{
|
||||||
CLIPluginsExtraDirs: extras,
|
CLIPluginsExtraDirs: extras,
|
||||||
})
|
})
|
||||||
pluginDirs = getPluginDirs(cli.ConfigFile())
|
pluginDirs, err = getPluginDirs(cli)
|
||||||
assert.DeepEqual(t, expected, pluginDirs)
|
assert.DeepEqual(t, expected, pluginDirs)
|
||||||
|
assert.NilError(t, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,9 @@
|
|||||||
//go:build !windows
|
//go:build !windows
|
||||||
|
// +build !windows
|
||||||
|
|
||||||
package manager
|
package manager
|
||||||
|
|
||||||
// defaultSystemPluginDirs are the platform-specific locations to search
|
|
||||||
// for plugins in order of preference.
|
|
||||||
//
|
|
||||||
// Plugin-discovery is performed in the following order of preference:
|
|
||||||
//
|
|
||||||
// 1. The "cli-plugins" directory inside the CLIs config-directory (usually "~/.docker/cli-plugins").
|
|
||||||
// 2. Additional plugin directories as configured through [ConfigFile.CLIPluginsExtraDirs].
|
|
||||||
// 3. Platform-specific defaultSystemPluginDirs (as defined below).
|
|
||||||
//
|
|
||||||
// [ConfigFile.CLIPluginsExtraDirs]: https://pkg.go.dev/github.com/docker/cli@v26.1.4+incompatible/cli/config/configfile#ConfigFile.CLIPluginsExtraDirs
|
|
||||||
var defaultSystemPluginDirs = []string{
|
var defaultSystemPluginDirs = []string{
|
||||||
"/usr/local/lib/docker/cli-plugins",
|
"/usr/local/lib/docker/cli-plugins", "/usr/local/libexec/docker/cli-plugins",
|
||||||
"/usr/local/libexec/docker/cli-plugins",
|
"/usr/lib/docker/cli-plugins", "/usr/libexec/docker/cli-plugins",
|
||||||
"/usr/lib/docker/cli-plugins",
|
|
||||||
"/usr/libexec/docker/cli-plugins",
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,16 +5,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
)
|
)
|
||||||
|
|
||||||
// defaultSystemPluginDirs are the platform-specific locations to search
|
|
||||||
// for plugins in order of preference.
|
|
||||||
//
|
|
||||||
// Plugin-discovery is performed in the following order of preference:
|
|
||||||
//
|
|
||||||
// 1. The "cli-plugins" directory inside the CLIs config-directory (usually "~/.docker/cli-plugins").
|
|
||||||
// 2. Additional plugin directories as configured through [ConfigFile.CLIPluginsExtraDirs].
|
|
||||||
// 3. Platform-specific defaultSystemPluginDirs (as defined below).
|
|
||||||
//
|
|
||||||
// [ConfigFile.CLIPluginsExtraDirs]: https://pkg.go.dev/github.com/docker/cli@v26.1.4+incompatible/cli/config/configfile#ConfigFile.CLIPluginsExtraDirs
|
|
||||||
var defaultSystemPluginDirs = []string{
|
var defaultSystemPluginDirs = []string{
|
||||||
|
filepath.Join(os.Getenv("ProgramData"), "Docker", "cli-plugins"),
|
||||||
filepath.Join(os.Getenv("ProgramFiles"), "Docker", "cli-plugins"),
|
filepath.Join(os.Getenv("ProgramFiles"), "Docker", "cli-plugins"),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
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"`
|
||||||
|
}
|
||||||
+18
-118
@@ -1,28 +1,20 @@
|
|||||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
|
||||||
//go:build go1.26
|
|
||||||
|
|
||||||
package manager
|
package manager
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"encoding"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/docker/cli/cli-plugins/hooks"
|
"github.com/pkg/errors"
|
||||||
"github.com/docker/cli/cli-plugins/metadata"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var pluginNameRe = regexp.MustCompile("^[a-z][a-z0-9]*$")
|
||||||
|
|
||||||
// Plugin represents a potential plugin with all it's metadata.
|
// Plugin represents a potential plugin with all it's metadata.
|
||||||
type Plugin struct {
|
type Plugin struct {
|
||||||
metadata.Metadata
|
Metadata
|
||||||
|
|
||||||
Name string `json:",omitempty"`
|
Name string `json:",omitempty"`
|
||||||
Path string `json:",omitempty"`
|
Path string `json:",omitempty"`
|
||||||
@@ -34,34 +26,12 @@ type Plugin struct {
|
|||||||
ShadowedPaths []string `json:",omitempty"`
|
ShadowedPaths []string `json:",omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarshalJSON implements [json.Marshaler] to handle marshaling the
|
|
||||||
// [Plugin.Err] field (Go doesn't marshal errors by default).
|
|
||||||
func (p *Plugin) MarshalJSON() ([]byte, error) {
|
|
||||||
type Alias Plugin // avoid recursion
|
|
||||||
|
|
||||||
cp := *p // shallow copy to avoid mutating original
|
|
||||||
|
|
||||||
if cp.Err != nil {
|
|
||||||
if _, ok := cp.Err.(encoding.TextMarshaler); !ok {
|
|
||||||
cp.Err = &pluginError{cp.Err}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return json.Marshal((*Alias)(&cp))
|
|
||||||
}
|
|
||||||
|
|
||||||
// pluginCandidate represents a possible plugin candidate, for mocking purposes.
|
|
||||||
type pluginCandidate interface {
|
|
||||||
Path() string
|
|
||||||
Metadata() ([]byte, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// newPlugin determines if the given candidate is valid and returns a
|
// newPlugin determines if the given candidate is valid and returns a
|
||||||
// Plugin. If the candidate fails one of the tests then `Plugin.Err`
|
// Plugin. If the candidate fails one of the tests then `Plugin.Err`
|
||||||
// is set, and is always a `pluginError`, but the `Plugin` is still
|
// is set, and is always a `pluginError`, but the `Plugin` is still
|
||||||
// returned with no error. An error is only returned due to a
|
// returned with no error. An error is only returned due to a
|
||||||
// non-recoverable error.
|
// non-recoverable error.
|
||||||
func newPlugin(c pluginCandidate, cmds []*cobra.Command) (Plugin, error) {
|
func newPlugin(c Candidate, cmds []*cobra.Command) (Plugin, error) {
|
||||||
path := c.Path()
|
path := c.Path()
|
||||||
if path == "" {
|
if path == "" {
|
||||||
return Plugin{}, errors.New("plugin candidate path cannot be empty")
|
return Plugin{}, errors.New("plugin candidate path cannot be empty")
|
||||||
@@ -71,24 +41,24 @@ func newPlugin(c pluginCandidate, cmds []*cobra.Command) (Plugin, error) {
|
|||||||
// which would fail here, so there are all real errors.
|
// which would fail here, so there are all real errors.
|
||||||
fullname := filepath.Base(path)
|
fullname := filepath.Base(path)
|
||||||
if fullname == "." {
|
if fullname == "." {
|
||||||
return Plugin{}, fmt.Errorf("unable to determine basename of plugin candidate %q", path)
|
return Plugin{}, errors.Errorf("unable to determine basename of plugin candidate %q", path)
|
||||||
}
|
}
|
||||||
var err error
|
var err error
|
||||||
if fullname, err = trimExeSuffix(fullname); err != nil {
|
if fullname, err = trimExeSuffix(fullname); err != nil {
|
||||||
return Plugin{}, fmt.Errorf("plugin candidate %q: %w", path, err)
|
return Plugin{}, errors.Wrapf(err, "plugin candidate %q", path)
|
||||||
}
|
}
|
||||||
if !strings.HasPrefix(fullname, metadata.NamePrefix) {
|
if !strings.HasPrefix(fullname, NamePrefix) {
|
||||||
return Plugin{}, fmt.Errorf("plugin candidate %q: does not have %q prefix", path, metadata.NamePrefix)
|
return Plugin{}, errors.Errorf("plugin candidate %q: does not have %q prefix", path, NamePrefix)
|
||||||
}
|
}
|
||||||
|
|
||||||
p := Plugin{
|
p := Plugin{
|
||||||
Name: strings.TrimPrefix(fullname, metadata.NamePrefix),
|
Name: strings.TrimPrefix(fullname, NamePrefix),
|
||||||
Path: path,
|
Path: path,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now apply the candidate tests, so these update p.Err.
|
// Now apply the candidate tests, so these update p.Err.
|
||||||
if !isValidPluginName(p.Name) {
|
if !pluginNameRe.MatchString(p.Name) {
|
||||||
p.Err = newPluginError("plugin candidate %q did not match %q", p.Name, pluginNameFormat)
|
p.Err = NewPluginError("plugin candidate %q did not match %q", p.Name, pluginNameRe.String())
|
||||||
return p, nil
|
return p, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,11 +70,11 @@ func newPlugin(c pluginCandidate, cmds []*cobra.Command) (Plugin, error) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if cmd.Name() == p.Name {
|
if cmd.Name() == p.Name {
|
||||||
p.Err = newPluginError("plugin %q duplicates builtin command", p.Name)
|
p.Err = NewPluginError("plugin %q duplicates builtin command", p.Name)
|
||||||
return p, nil
|
return p, nil
|
||||||
}
|
}
|
||||||
if cmd.HasAlias(p.Name) {
|
if cmd.HasAlias(p.Name) {
|
||||||
p.Err = newPluginError("plugin %q duplicates an alias of builtin command %q", p.Name, cmd.Name())
|
p.Err = NewPluginError("plugin %q duplicates an alias of builtin command %q", p.Name, cmd.Name())
|
||||||
return p, nil
|
return p, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -120,83 +90,13 @@ func newPlugin(c pluginCandidate, cmds []*cobra.Command) (Plugin, error) {
|
|||||||
p.Err = wrapAsPluginError(err, "invalid metadata")
|
p.Err = wrapAsPluginError(err, "invalid metadata")
|
||||||
return p, nil
|
return p, nil
|
||||||
}
|
}
|
||||||
if err := validateSchemaVersion(p.Metadata.SchemaVersion); err != nil {
|
if p.Metadata.SchemaVersion != "0.1.0" {
|
||||||
p.Err = &pluginError{cause: err}
|
p.Err = NewPluginError("plugin SchemaVersion %q is not valid, must be 0.1.0", p.Metadata.SchemaVersion)
|
||||||
return p, nil
|
return p, nil
|
||||||
}
|
}
|
||||||
if p.Metadata.Vendor == "" {
|
if p.Metadata.Vendor == "" {
|
||||||
p.Err = newPluginError("plugin metadata does not define a vendor")
|
p.Err = NewPluginError("plugin metadata does not define a vendor")
|
||||||
return p, nil
|
return p, nil
|
||||||
}
|
}
|
||||||
return p, nil
|
return p, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// validateSchemaVersion validates if the plugin's schemaVersion is supported.
|
|
||||||
//
|
|
||||||
// The current schema-version is "0.1.0", but we don't want to break compatibility
|
|
||||||
// until v2.0.0 of the schema version. Check for the major version to be < 2.0.0.
|
|
||||||
//
|
|
||||||
// Note that CLI versions before 28.4.1 may not support these versions as they were
|
|
||||||
// hard-coded to only accept "0.1.0".
|
|
||||||
func validateSchemaVersion(version string) error {
|
|
||||||
if version == "0.1.0" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if version == "" {
|
|
||||||
return errors.New("plugin SchemaVersion version cannot be empty")
|
|
||||||
}
|
|
||||||
major, _, ok := strings.Cut(version, ".")
|
|
||||||
majorVersion, err := strconv.Atoi(major)
|
|
||||||
if !ok || err != nil {
|
|
||||||
return fmt.Errorf("plugin SchemaVersion %q has wrong format: must be <major>.<minor>.<patch>", version)
|
|
||||||
}
|
|
||||||
if majorVersion > 1 {
|
|
||||||
return fmt.Errorf("plugin SchemaVersion %q is not supported: must be lower than 2.0.0", version)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RunHook executes the plugin's hooks command
|
|
||||||
// and returns its unprocessed output.
|
|
||||||
func (p *Plugin) RunHook(ctx context.Context, hookData hooks.Request) ([]byte, error) {
|
|
||||||
hDataBytes, err := json.Marshal(hookData)
|
|
||||||
if err != nil {
|
|
||||||
return nil, wrapAsPluginError(err, "failed to marshall hook data")
|
|
||||||
}
|
|
||||||
|
|
||||||
pCmd := exec.CommandContext(ctx, p.Path, p.Name, metadata.HookSubcommandName, string(hDataBytes)) // #nosec G204 -- ignore "Subprocess launched with a potential tainted input or cmd arguments"
|
|
||||||
pCmd.Env = os.Environ()
|
|
||||||
pCmd.Env = append(pCmd.Env, metadata.ReexecEnvvar+"="+os.Args[0])
|
|
||||||
|
|
||||||
out, err := pCmd.Output()
|
|
||||||
if err != nil {
|
|
||||||
if _, ok := errors.AsType[*exec.ExitError](err); ok {
|
|
||||||
return nil, wrapAsPluginError(err, "plugin hook subcommand exited unsuccessfully")
|
|
||||||
}
|
|
||||||
return nil, wrapAsPluginError(err, "failed to execute plugin hook subcommand: "+pCmd.String())
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// pluginNameFormat is used as part of errors for invalid plugin-names.
|
|
||||||
// We should consider making this less technical ("must start with "a-z",
|
|
||||||
// and only consist of lowercase alphanumeric characters").
|
|
||||||
const pluginNameFormat = `^[a-z][a-z0-9]*$`
|
|
||||||
|
|
||||||
func isValidPluginName(s string) bool {
|
|
||||||
if len(s) == 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
// first character must be a-z
|
|
||||||
if c := s[0]; c < 'a' || c > 'z' {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
// followed by a-z or 0-9
|
|
||||||
for i := 1; i < len(s); i++ {
|
|
||||||
c := s[i]
|
|
||||||
if (c < 'a' || c > 'z') && (c < '0' || c > '9') {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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,4 +1,5 @@
|
|||||||
//go:build !windows
|
//go:build !windows
|
||||||
|
// +build !windows
|
||||||
|
|
||||||
package manager
|
package manager
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,22 @@
|
|||||||
package manager
|
package manager
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
// This is made slightly more complex due to needing to be case-insensitive.
|
// This is made slightly more complex due to needing to be case insensitive.
|
||||||
func trimExeSuffix(s string) (string, error) {
|
func trimExeSuffix(s string) (string, error) {
|
||||||
ext := filepath.Ext(s)
|
ext := filepath.Ext(s)
|
||||||
if ext == "" || !strings.EqualFold(ext, ".exe") {
|
if ext == "" {
|
||||||
return "", fmt.Errorf("path %q lacks required file extension (.exe)", s)
|
return "", errors.Errorf("path %q lacks required file extension", s)
|
||||||
|
}
|
||||||
|
|
||||||
|
exe := ".exe"
|
||||||
|
if !strings.EqualFold(ext, exe) {
|
||||||
|
return "", errors.Errorf("path %q lacks required %q suffix", s, exe)
|
||||||
}
|
}
|
||||||
return strings.TrimSuffix(s, ext), nil
|
return strings.TrimSuffix(s, ext), nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,76 +1,44 @@
|
|||||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
|
||||||
//go:build go1.26
|
|
||||||
|
|
||||||
package plugin
|
package plugin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/docker/cli/cli"
|
"github.com/docker/cli/cli"
|
||||||
"github.com/docker/cli/cli-plugins/metadata"
|
"github.com/docker/cli/cli-plugins/manager"
|
||||||
"github.com/docker/cli/cli-plugins/socket"
|
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
"github.com/docker/cli/cli/connhelper"
|
"github.com/docker/cli/cli/connhelper"
|
||||||
"github.com/docker/cli/cli/debug"
|
"github.com/docker/docker/client"
|
||||||
"github.com/moby/moby/client"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"go.opentelemetry.io/otel"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// PersistentPreRunE must be called by any plugin command (or
|
// PersistentPreRunE must be called by any plugin command (or
|
||||||
// subcommand) which uses the cobra `PersistentPreRun*` hook. Plugins
|
// subcommand) which uses the cobra `PersistentPreRun*` hook. Plugins
|
||||||
// which do not make use of `PersistentPreRun*` do not need to call
|
// which do not make use of `PersistentPreRun*` do not need to call
|
||||||
// this (although it remains safe to do so). Plugins are recommended
|
// this (although it remains safe to do so). Plugins are recommended
|
||||||
// to use `PersistentPreRunE` to enable the error to be
|
// to use `PersistenPreRunE` to enable the error to be
|
||||||
// returned. Should not be called outside of a command's
|
// returned. Should not be called outside of a command's
|
||||||
// PersistentPreRunE hook and must not be run unless Run has been
|
// PersistentPreRunE hook and must not be run unless Run has been
|
||||||
// called.
|
// called.
|
||||||
var PersistentPreRunE func(*cobra.Command, []string) error
|
var PersistentPreRunE func(*cobra.Command, []string) error
|
||||||
|
|
||||||
// RunPlugin executes the specified plugin command
|
// 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)
|
tcmd := newPluginCommand(dockerCli, plugin, meta)
|
||||||
|
|
||||||
var persistentPreRunOnce sync.Once
|
var persistentPreRunOnce sync.Once
|
||||||
PersistentPreRunE = func(cmd *cobra.Command, _ []string) error {
|
PersistentPreRunE = func(_ *cobra.Command, _ []string) error {
|
||||||
var retErr error
|
var err error
|
||||||
persistentPreRunOnce.Do(func() {
|
persistentPreRunOnce.Do(func() {
|
||||||
ctx, cancel := context.WithCancel(cmd.Context())
|
var opts []command.InitializeOpt
|
||||||
cmd.SetContext(ctx)
|
|
||||||
// Set up the context to cancel based on signalling via CLI socket.
|
|
||||||
socket.ConnectAndWait(cancel)
|
|
||||||
|
|
||||||
var opts []command.CLIOption
|
|
||||||
if os.Getenv("DOCKER_CLI_PLUGIN_USE_DIAL_STDIO") != "" {
|
if os.Getenv("DOCKER_CLI_PLUGIN_USE_DIAL_STDIO") != "" {
|
||||||
opts = append(opts, withPluginClientConn(plugin.Name()))
|
opts = append(opts, withPluginClientConn(plugin.Name()))
|
||||||
}
|
}
|
||||||
opts = append(opts, command.WithEnableGlobalMeterProvider(), command.WithEnableGlobalTracerProvider())
|
err = tcmd.Initialize(opts...)
|
||||||
retErr = tcmd.Initialize(opts...)
|
|
||||||
ogRunE := cmd.RunE
|
|
||||||
if ogRunE == nil {
|
|
||||||
ogRun := cmd.Run
|
|
||||||
// necessary because error will always be nil here
|
|
||||||
// see: https://github.com/golangci/golangci-lint/issues/1379
|
|
||||||
//nolint:unparam
|
|
||||||
ogRunE = func(cmd *cobra.Command, args []string) error {
|
|
||||||
ogRun(cmd, args)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
cmd.Run = nil
|
|
||||||
}
|
|
||||||
cmd.RunE = func(cmd *cobra.Command, args []string) error {
|
|
||||||
stopInstrumentation := dockerCli.StartInstrumentation(cmd)
|
|
||||||
err := ogRunE(cmd, args)
|
|
||||||
stopInstrumentation(err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
return retErr
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd, args, err := tcmd.HandleGlobalFlags()
|
cmd, args, err := tcmd.HandleGlobalFlags()
|
||||||
@@ -83,41 +51,37 @@ func RunPlugin(dockerCli *command.DockerCli, plugin *cobra.Command, meta metadat
|
|||||||
return cmd.Execute()
|
return cmd.Execute()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run is the top-level entry point to the CLI plugin framework. It should
|
// Run is the top-level entry point to the CLI plugin framework. It should be called from your plugin's `main()` function.
|
||||||
// be called from the plugin's "main()" function. It initializes a new
|
func Run(makeCmd func(command.Cli) *cobra.Command, meta manager.Metadata) {
|
||||||
// [command.DockerCli] instance with the given options before calling
|
dockerCli, err := command.NewDockerCli()
|
||||||
// makeCmd to construct the plugin command, then invokes the plugin command
|
|
||||||
// using [RunPlugin].
|
|
||||||
func Run(makeCmd func(command.Cli) *cobra.Command, meta metadata.Metadata, ops ...command.CLIOption) {
|
|
||||||
otel.SetErrorHandler(debug.OTELErrorHandler)
|
|
||||||
|
|
||||||
dockerCLI, err := command.NewDockerCli(ops...)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_, _ = fmt.Fprintln(os.Stderr, err)
|
fmt.Fprintln(os.Stderr, err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
plugin := makeCmd(dockerCLI)
|
plugin := makeCmd(dockerCli)
|
||||||
|
|
||||||
if err := RunPlugin(dockerCLI, plugin, meta); err != nil {
|
if err := RunPlugin(dockerCli, plugin, meta); err != nil {
|
||||||
if stErr, ok := errors.AsType[cli.StatusError](err); ok {
|
if sterr, ok := err.(cli.StatusError); ok {
|
||||||
|
if sterr.Status != "" {
|
||||||
|
fmt.Fprintln(dockerCli.Err(), sterr.Status)
|
||||||
|
}
|
||||||
// StatusError should only be used for errors, and all errors should
|
// StatusError should only be used for errors, and all errors should
|
||||||
// have a non-zero exit status, so never exit with 0
|
// have a non-zero exit status, so never exit with 0
|
||||||
if stErr.StatusCode == 0 { // FIXME(thaJeztah): this should never be used with a zero status-code. Check if we do this anywhere.
|
if sterr.StatusCode == 0 {
|
||||||
stErr.StatusCode = 1
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
_, _ = fmt.Fprintln(dockerCLI.Err(), stErr)
|
os.Exit(sterr.StatusCode)
|
||||||
os.Exit(stErr.StatusCode)
|
|
||||||
}
|
}
|
||||||
_, _ = fmt.Fprintln(dockerCLI.Err(), err)
|
fmt.Fprintln(dockerCli.Err(), err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func withPluginClientConn(name string) command.CLIOption {
|
func withPluginClientConn(name string) command.InitializeOpt {
|
||||||
return func(cli *command.DockerCli) error {
|
return command.WithInitializeClient(func(dockerCli *command.DockerCli) (client.APIClient, error) {
|
||||||
cmd := "docker"
|
cmd := "docker"
|
||||||
if x := os.Getenv(metadata.ReexecEnvvar); x != "" {
|
if x := os.Getenv(manager.ReexecEnvvar); x != "" {
|
||||||
cmd = x
|
cmd = x
|
||||||
}
|
}
|
||||||
var flags []string
|
var flags []string
|
||||||
@@ -139,19 +103,16 @@ func withPluginClientConn(name string) command.CLIOption {
|
|||||||
|
|
||||||
helper, err := connhelper.GetCommandConnectionHelper(cmd, flags...)
|
helper, err := connhelper.GetCommandConnectionHelper(cmd, flags...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
apiClient, err := client.New(client.WithDialContext(helper.Dialer))
|
|
||||||
if err != nil {
|
return client.NewClientWithOpts(client.WithDialContext(helper.Dialer))
|
||||||
return err
|
})
|
||||||
}
|
|
||||||
return command.WithAPIClient(apiClient)(cli)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func newPluginCommand(dockerCli *command.DockerCli, plugin *cobra.Command, meta metadata.Metadata) *cli.TopLevelCommand {
|
func newPluginCommand(dockerCli *command.DockerCli, plugin *cobra.Command, meta manager.Metadata) *cli.TopLevelCommand {
|
||||||
name := plugin.Name()
|
name := plugin.Name()
|
||||||
fullname := metadata.NamePrefix + name
|
fullname := manager.NamePrefix + name
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: fmt.Sprintf("docker [OPTIONS] %s [ARG...]", name),
|
Use: fmt.Sprintf("docker [OPTIONS] %s [ARG...]", name),
|
||||||
@@ -167,15 +128,10 @@ func newPluginCommand(dockerCli *command.DockerCli, plugin *cobra.Command, meta
|
|||||||
CompletionOptions: cobra.CompletionOptions{
|
CompletionOptions: cobra.CompletionOptions{
|
||||||
DisableDefaultCmd: false,
|
DisableDefaultCmd: false,
|
||||||
HiddenDefaultCmd: true,
|
HiddenDefaultCmd: true,
|
||||||
DisableDescriptions: os.Getenv("DOCKER_CLI_DISABLE_COMPLETION_DESCRIPTION") != "",
|
DisableDescriptions: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
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.SetIn(dockerCli.In())
|
||||||
cmd.SetOut(dockerCli.Out())
|
cmd.SetOut(dockerCli.Out())
|
||||||
@@ -186,30 +142,17 @@ func newPluginCommand(dockerCli *command.DockerCli, plugin *cobra.Command, meta
|
|||||||
newMetadataSubcommand(plugin, meta),
|
newMetadataSubcommand(plugin, meta),
|
||||||
)
|
)
|
||||||
|
|
||||||
visitAll(cmd,
|
cli.DisableFlagsInUseLine(cmd)
|
||||||
// prevent adding "[flags]" to the end of the usage line.
|
|
||||||
func(c *cobra.Command) { c.DisableFlagsInUseLine = true },
|
|
||||||
)
|
|
||||||
|
|
||||||
return cli.NewTopLevelCommand(cmd, dockerCli, opts, cmd.Flags())
|
return cli.NewTopLevelCommand(cmd, dockerCli, opts, flags)
|
||||||
}
|
}
|
||||||
|
|
||||||
// visitAll traverses all commands from the root.
|
func newMetadataSubcommand(plugin *cobra.Command, meta manager.Metadata) *cobra.Command {
|
||||||
func visitAll(root *cobra.Command, fns ...func(*cobra.Command)) {
|
|
||||||
for _, cmd := range root.Commands() {
|
|
||||||
visitAll(cmd, fns...)
|
|
||||||
}
|
|
||||||
for _, fn := range fns {
|
|
||||||
fn(root)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newMetadataSubcommand(plugin *cobra.Command, meta metadata.Metadata) *cobra.Command {
|
|
||||||
if meta.ShortDescription == "" {
|
if meta.ShortDescription == "" {
|
||||||
meta.ShortDescription = plugin.Short
|
meta.ShortDescription = plugin.Short
|
||||||
}
|
}
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: metadata.MetadataSubcommandName,
|
Use: manager.MetadataSubcommandName,
|
||||||
Hidden: true,
|
Hidden: true,
|
||||||
// Suppress the global/parent PersistentPreRunE, which
|
// Suppress the global/parent PersistentPreRunE, which
|
||||||
// needlessly initializes the client and tries to
|
// needlessly initializes the client and tries to
|
||||||
@@ -227,8 +170,8 @@ func newMetadataSubcommand(plugin *cobra.Command, meta metadata.Metadata) *cobra
|
|||||||
|
|
||||||
// RunningStandalone tells a CLI plugin it is run standalone by direct execution
|
// RunningStandalone tells a CLI plugin it is run standalone by direct execution
|
||||||
func RunningStandalone() bool {
|
func RunningStandalone() bool {
|
||||||
if os.Getenv(metadata.ReexecEnvvar) != "" {
|
if os.Getenv(manager.ReexecEnvvar) != "" {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return len(os.Args) < 2 || os.Args[1] != metadata.MetadataSubcommandName
|
return len(os.Args) < 2 || os.Args[1] != manager.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.Listener.Close] call.
|
|
||||||
func (pl *PluginServer) Close() error {
|
|
||||||
if pl == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
logrus.Trace("Closing plugin server")
|
|
||||||
// Close connections first to ensure the connections get io.EOF instead
|
|
||||||
// of a connection reset.
|
|
||||||
pl.closeAllConns()
|
|
||||||
|
|
||||||
// Try to ensure that any active connections have a chance to receive
|
|
||||||
// io.EOF.
|
|
||||||
runtime.Gosched()
|
|
||||||
|
|
||||||
return pl.l.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pl *PluginServer) closeAllConns() {
|
|
||||||
pl.mu.Lock()
|
|
||||||
defer pl.mu.Unlock()
|
|
||||||
|
|
||||||
if pl.closed {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prevent new connections from being accepted.
|
|
||||||
pl.closed = true
|
|
||||||
|
|
||||||
for _, conn := range pl.conns {
|
|
||||||
conn.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
pl.conns = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func randomID() string {
|
|
||||||
b := make([]byte, 16)
|
|
||||||
if _, err := rand.Read(b); err != nil {
|
|
||||||
panic(err) // This shouldn't happen
|
|
||||||
}
|
|
||||||
return hex.EncodeToString(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConnectAndWait connects to the socket passed via well-known env var,
|
|
||||||
// if present, and attempts to read from it until it receives an EOF, at which
|
|
||||||
// point cb is called.
|
|
||||||
func ConnectAndWait(cb func()) {
|
|
||||||
socketAddr, ok := os.LookupEnv(EnvKey)
|
|
||||||
if !ok {
|
|
||||||
// if a plugin compiled against a more recent version of docker/cli
|
|
||||||
// is executed by an older CLI binary, ignore missing environment
|
|
||||||
// variable and behave as usual
|
|
||||||
return
|
|
||||||
}
|
|
||||||
addr, err := net.ResolveUnixAddr("unix", socketAddr)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
conn, err := net.DialUnix("unix", nil, addr)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
b := make([]byte, 1)
|
|
||||||
for {
|
|
||||||
_, err := conn.Read(b)
|
|
||||||
if errors.Is(err, io.EOF) {
|
|
||||||
cb()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
@@ -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("expected EOF error, got: %v", err)
|
|
||||||
}
|
|
||||||
case <-time.After(10 * time.Millisecond):
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("allows reconnects", func(t *testing.T) {
|
|
||||||
var calls atomic.Int32
|
|
||||||
h := func(_ net.Conn) {
|
|
||||||
calls.Add(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
srv, err := NewPluginServer(h)
|
|
||||||
assert.NilError(t, err)
|
|
||||||
defer srv.Close()
|
|
||||||
|
|
||||||
assert.Check(t, srv.Addr() != nil, "returned nil addr but no error")
|
|
||||||
|
|
||||||
addr, err := net.ResolveUnixAddr("unix", srv.Addr().String())
|
|
||||||
assert.NilError(t, err, "failed to resolve server address")
|
|
||||||
|
|
||||||
waitForCalls := func(n int) {
|
|
||||||
poll.WaitOn(t, func(t poll.LogT) poll.Result {
|
|
||||||
if calls.Load() == int32(n) {
|
|
||||||
return poll.Success()
|
|
||||||
}
|
|
||||||
return poll.Continue("waiting for handler to be called")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
otherConn, err := net.DialUnix("unix", nil, addr)
|
|
||||||
assert.NilError(t, err, "failed to dial returned server")
|
|
||||||
otherConn.Close()
|
|
||||||
waitForCalls(1)
|
|
||||||
|
|
||||||
conn, err := net.DialUnix("unix", nil, addr)
|
|
||||||
assert.NilError(t, err, "failed to redial server")
|
|
||||||
defer conn.Close()
|
|
||||||
waitForCalls(2)
|
|
||||||
|
|
||||||
// and again but don't close the existing connection
|
|
||||||
conn2, err := net.DialUnix("unix", nil, addr)
|
|
||||||
assert.NilError(t, err, "failed to redial server")
|
|
||||||
defer conn2.Close()
|
|
||||||
waitForCalls(3)
|
|
||||||
|
|
||||||
srv.Close()
|
|
||||||
|
|
||||||
// now make sure we get EOF on the existing connections
|
|
||||||
buf := make([]byte, 1)
|
|
||||||
_, err = conn.Read(buf)
|
|
||||||
assert.ErrorIs(t, err, io.EOF, "expected EOF error, got: %v", err)
|
|
||||||
|
|
||||||
_, err = conn2.Read(buf)
|
|
||||||
assert.ErrorIs(t, err, io.EOF, "expected EOF error, got: %v", err)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("does not leak sockets to local directory", func(t *testing.T) {
|
|
||||||
srv, err := NewPluginServer(nil)
|
|
||||||
assert.NilError(t, err)
|
|
||||||
assert.Check(t, srv != nil, "returned nil server but no error")
|
|
||||||
checkDirNoNewPluginServer(t)
|
|
||||||
|
|
||||||
addr, err := net.ResolveUnixAddr("unix", srv.Addr().String())
|
|
||||||
assert.NilError(t, err, "failed to resolve server address")
|
|
||||||
|
|
||||||
_, err = net.DialUnix("unix", nil, addr)
|
|
||||||
assert.NilError(t, err, "failed to dial returned server")
|
|
||||||
checkDirNoNewPluginServer(t)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("does not panic on Close if server is nil", func(t *testing.T) {
|
|
||||||
var srv *PluginServer
|
|
||||||
defer func() {
|
|
||||||
if r := recover(); r != nil {
|
|
||||||
t.Errorf("panicked on Close")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
err := srv.Close()
|
|
||||||
assert.NilError(t, err)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkDirNoNewPluginServer(t *testing.T) {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
files, err := os.ReadDir(".")
|
|
||||||
assert.NilError(t, err, "failed to list files in dir to check for leaked sockets")
|
|
||||||
|
|
||||||
for _, f := range files {
|
|
||||||
info, err := f.Info()
|
|
||||||
assert.NilError(t, err, "failed to check file info")
|
|
||||||
// check for a socket with `docker_cli_` in the name (from `SetupConn()`)
|
|
||||||
if strings.Contains(f.Name(), "docker_cli_") && info.Mode().Type() == fs.ModeSocket {
|
|
||||||
t.Fatal("found socket in a local directory")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConnectAndWait(t *testing.T) {
|
|
||||||
t.Run("calls cancel func on EOF", func(t *testing.T) {
|
|
||||||
srv, err := NewPluginServer(nil)
|
|
||||||
assert.NilError(t, err, "failed to setup server")
|
|
||||||
defer srv.Close()
|
|
||||||
|
|
||||||
done := make(chan struct{})
|
|
||||||
t.Setenv(EnvKey, srv.Addr().String())
|
|
||||||
cancelFunc := func() {
|
|
||||||
done <- struct{}{}
|
|
||||||
}
|
|
||||||
ConnectAndWait(cancelFunc)
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-done:
|
|
||||||
t.Fatal("unexpectedly done")
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
srv.Close()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-done:
|
|
||||||
case <-time.After(10 * time.Millisecond):
|
|
||||||
t.Fatal("cancel function not closed after 10ms")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// TODO: this test cannot be executed with `t.Parallel()`, due to
|
|
||||||
// relying on goroutine numbers to ensure correct behaviour
|
|
||||||
t.Run("connect goroutine exits after EOF", func(t *testing.T) {
|
|
||||||
runtime.LockOSThread()
|
|
||||||
defer runtime.UnlockOSThread()
|
|
||||||
srv, err := NewPluginServer(nil)
|
|
||||||
assert.NilError(t, err, "failed to setup server")
|
|
||||||
|
|
||||||
defer srv.Close()
|
|
||||||
|
|
||||||
t.Setenv(EnvKey, srv.Addr().String())
|
|
||||||
|
|
||||||
runtime.Gosched()
|
|
||||||
numGoroutines := runtime.NumGoroutine()
|
|
||||||
|
|
||||||
ConnectAndWait(func() {})
|
|
||||||
|
|
||||||
runtime.Gosched()
|
|
||||||
poll.WaitOn(t, func(t poll.LogT) poll.Result {
|
|
||||||
// +1 goroutine for the poll.WaitOn
|
|
||||||
// +1 goroutine for the connect goroutine
|
|
||||||
if runtime.NumGoroutine() < numGoroutines+1+1 {
|
|
||||||
return poll.Continue("waiting for connect goroutine to spawn")
|
|
||||||
}
|
|
||||||
return poll.Success()
|
|
||||||
}, poll.WithDelay(1*time.Millisecond), poll.WithTimeout(500*time.Millisecond))
|
|
||||||
|
|
||||||
srv.Close()
|
|
||||||
|
|
||||||
runtime.Gosched()
|
|
||||||
poll.WaitOn(t, func(t poll.LogT) poll.Result {
|
|
||||||
// +1 goroutine for the poll.WaitOn
|
|
||||||
if runtime.NumGoroutine() > numGoroutines+1 {
|
|
||||||
return poll.Continue("waiting for connect goroutine to exit")
|
|
||||||
}
|
|
||||||
return poll.Success()
|
|
||||||
}, poll.WithDelay(1*time.Millisecond), poll.WithTimeout(500*time.Millisecond))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
+80
-35
@@ -1,29 +1,34 @@
|
|||||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
|
||||||
//go:build go1.26
|
|
||||||
|
|
||||||
package cli
|
package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"slices"
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/docker/cli/cli-plugins/metadata"
|
pluginmanager "github.com/docker/cli/cli-plugins/manager"
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
|
"github.com/docker/cli/cli/config"
|
||||||
cliflags "github.com/docker/cli/cli/flags"
|
cliflags "github.com/docker/cli/cli/flags"
|
||||||
|
"github.com/docker/docker/pkg/homedir"
|
||||||
|
"github.com/docker/docker/registry"
|
||||||
"github.com/fvbommel/sortorder"
|
"github.com/fvbommel/sortorder"
|
||||||
"github.com/moby/term"
|
"github.com/moby/term"
|
||||||
"github.com/morikuni/aec"
|
"github.com/morikuni/aec"
|
||||||
|
"github.com/pkg/errors"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
)
|
)
|
||||||
|
|
||||||
// setupCommonRootCommand contains the setup common to
|
// setupCommonRootCommand contains the setup common to
|
||||||
// SetupRootCommand and SetupPluginRootCommand.
|
// SetupRootCommand and SetupPluginRootCommand.
|
||||||
func setupCommonRootCommand(rootCmd *cobra.Command) (*cliflags.ClientOptions, *cobra.Command) {
|
func setupCommonRootCommand(rootCmd *cobra.Command) (*cliflags.ClientOptions, *pflag.FlagSet, *cobra.Command) {
|
||||||
opts := cliflags.NewClientOptions()
|
opts := cliflags.NewClientOptions()
|
||||||
opts.InstallFlags(rootCmd.Flags())
|
flags := rootCmd.Flags()
|
||||||
|
|
||||||
|
flags.StringVar(&opts.ConfigDir, "config", config.Dir(), "Location of client config files")
|
||||||
|
opts.InstallFlags(flags)
|
||||||
|
|
||||||
cobra.AddTemplateFunc("add", func(a, b int) int { return a + b })
|
cobra.AddTemplateFunc("add", func(a, b int) int { return a + b })
|
||||||
cobra.AddTemplateFunc("hasAliases", hasAliases)
|
cobra.AddTemplateFunc("hasAliases", hasAliases)
|
||||||
@@ -53,7 +58,7 @@ func setupCommonRootCommand(rootCmd *cobra.Command) (*cliflags.ClientOptions, *c
|
|||||||
rootCmd.SetHelpCommand(helpCommand)
|
rootCmd.SetHelpCommand(helpCommand)
|
||||||
|
|
||||||
rootCmd.PersistentFlags().BoolP("help", "h", false, "Print usage")
|
rootCmd.PersistentFlags().BoolP("help", "h", false, "Print usage")
|
||||||
rootCmd.PersistentFlags().MarkShorthandDeprecated("help", "use --help")
|
rootCmd.PersistentFlags().MarkShorthandDeprecated("help", "please use --help")
|
||||||
rootCmd.PersistentFlags().Lookup("help").Hidden = true
|
rootCmd.PersistentFlags().Lookup("help").Hidden = true
|
||||||
|
|
||||||
rootCmd.Annotations = map[string]string{
|
rootCmd.Annotations = map[string]string{
|
||||||
@@ -61,20 +66,27 @@ func setupCommonRootCommand(rootCmd *cobra.Command) (*cliflags.ClientOptions, *c
|
|||||||
"docs.code-delimiter": `"`, // https://github.com/docker/cli-docs-tool/blob/77abede22166eaea4af7335096bdcedd043f5b19/annotation/annotation.go#L20-L22
|
"docs.code-delimiter": `"`, // https://github.com/docker/cli-docs-tool/blob/77abede22166eaea4af7335096bdcedd043f5b19/annotation/annotation.go#L20-L22
|
||||||
}
|
}
|
||||||
|
|
||||||
return opts, helpCommand
|
// Configure registry.CertsDir() when running in rootless-mode
|
||||||
|
if os.Getenv("ROOTLESSKIT_STATE_DIR") != "" {
|
||||||
|
if configHome, err := homedir.GetConfigHome(); err == nil {
|
||||||
|
registry.SetCertsDir(filepath.Join(configHome, "docker/certs.d"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return opts, flags, helpCommand
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetupRootCommand sets default usage, help, and error handling for the
|
// SetupRootCommand sets default usage, help, and error handling for the
|
||||||
// root command.
|
// root command.
|
||||||
func SetupRootCommand(rootCmd *cobra.Command) (opts *cliflags.ClientOptions, helpCmd *cobra.Command) {
|
func SetupRootCommand(rootCmd *cobra.Command) (*cliflags.ClientOptions, *pflag.FlagSet, *cobra.Command) {
|
||||||
rootCmd.SetVersionTemplate("Docker version {{.Version}}\n")
|
rootCmd.SetVersionTemplate("Docker version {{.Version}}\n")
|
||||||
return setupCommonRootCommand(rootCmd)
|
return setupCommonRootCommand(rootCmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetupPluginRootCommand sets default usage, help and error handling for a plugin root command.
|
// SetupPluginRootCommand sets default usage, help and error handling for a plugin root command.
|
||||||
func SetupPluginRootCommand(rootCmd *cobra.Command) (*cliflags.ClientOptions, *pflag.FlagSet) {
|
func SetupPluginRootCommand(rootCmd *cobra.Command) (*cliflags.ClientOptions, *pflag.FlagSet) {
|
||||||
opts, _ := setupCommonRootCommand(rootCmd)
|
opts, flags, _ := setupCommonRootCommand(rootCmd)
|
||||||
return opts, rootCmd.Flags()
|
return opts, flags
|
||||||
}
|
}
|
||||||
|
|
||||||
// FlagErrorFunc prints an error message which matches the format of the
|
// FlagErrorFunc prints an error message which matches the format of the
|
||||||
@@ -84,8 +96,12 @@ func FlagErrorFunc(cmd *cobra.Command, err error) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
usage := ""
|
||||||
|
if cmd.HasSubCommands() {
|
||||||
|
usage = "\n\n" + cmd.UsageString()
|
||||||
|
}
|
||||||
return StatusError{
|
return StatusError{
|
||||||
Status: fmt.Sprintf("%s\n\nUsage: %s\n\nRun '%s --help' for more information", err, cmd.UseLine(), cmd.CommandPath()),
|
Status: fmt.Sprintf("%s\nSee '%s --help'.%s", err, cmd.CommandPath(), usage),
|
||||||
StatusCode: 125,
|
StatusCode: 125,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -164,11 +180,40 @@ func (tcmd *TopLevelCommand) HandleGlobalFlags() (*cobra.Command, []string, erro
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Initialize finalises global option parsing and initializes the docker client.
|
// Initialize finalises global option parsing and initializes the docker client.
|
||||||
func (tcmd *TopLevelCommand) Initialize(ops ...command.CLIOption) error {
|
func (tcmd *TopLevelCommand) Initialize(ops ...command.InitializeOpt) error {
|
||||||
tcmd.opts.SetDefaultOptions(tcmd.flags)
|
tcmd.opts.SetDefaultOptions(tcmd.flags)
|
||||||
return tcmd.dockerCli.Initialize(tcmd.opts, ops...)
|
return tcmd.dockerCli.Initialize(tcmd.opts, ops...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// VisitAll will traverse all commands from the root.
|
||||||
|
// This is different from the VisitAll of cobra.Command where only parents
|
||||||
|
// are checked.
|
||||||
|
func VisitAll(root *cobra.Command, fn func(*cobra.Command)) {
|
||||||
|
for _, cmd := range root.Commands() {
|
||||||
|
VisitAll(cmd, fn)
|
||||||
|
}
|
||||||
|
fn(root)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DisableFlagsInUseLine sets the DisableFlagsInUseLine flag on all
|
||||||
|
// commands within the tree rooted at cmd.
|
||||||
|
func DisableFlagsInUseLine(cmd *cobra.Command) {
|
||||||
|
VisitAll(cmd, func(ccmd *cobra.Command) {
|
||||||
|
// do not add a `[flags]` to the end of the usage line.
|
||||||
|
ccmd.DisableFlagsInUseLine = true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasCompletionArg returns true if a cobra completion arg request is found.
|
||||||
|
func HasCompletionArg(args []string) bool {
|
||||||
|
for _, arg := range args {
|
||||||
|
if arg == cobra.ShellCompRequestCmd || arg == cobra.ShellCompNoDescRequestCmd {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
var helpCommand = &cobra.Command{
|
var helpCommand = &cobra.Command{
|
||||||
Use: "help [command]",
|
Use: "help [command]",
|
||||||
Short: "Help about the command",
|
Short: "Help about the command",
|
||||||
@@ -177,7 +222,7 @@ var helpCommand = &cobra.Command{
|
|||||||
RunE: func(c *cobra.Command, args []string) error {
|
RunE: func(c *cobra.Command, args []string) error {
|
||||||
cmd, args, e := c.Root().Find(args)
|
cmd, args, e := c.Root().Find(args)
|
||||||
if cmd == nil || e != nil || len(args) > 0 {
|
if cmd == nil || e != nil || len(args) > 0 {
|
||||||
return fmt.Errorf("unknown help topic: %v", strings.Join(args, " "))
|
return errors.Errorf("unknown help topic: %v", strings.Join(args, " "))
|
||||||
}
|
}
|
||||||
helpFunc := cmd.HelpFunc()
|
helpFunc := cmd.HelpFunc()
|
||||||
helpFunc(cmd, args)
|
helpFunc(cmd, args)
|
||||||
@@ -215,7 +260,7 @@ func hasAdditionalHelp(cmd *cobra.Command) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func isPlugin(cmd *cobra.Command) bool {
|
func isPlugin(cmd *cobra.Command) bool {
|
||||||
return cmd.Annotations[metadata.CommandAnnotationPlugin] == "true"
|
return pluginmanager.IsPluginCommand(cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
func hasAliases(cmd *cobra.Command) bool {
|
func hasAliases(cmd *cobra.Command) bool {
|
||||||
@@ -253,12 +298,11 @@ func commandAliases(cmd *cobra.Command) string {
|
|||||||
if cmd.HasParent() {
|
if cmd.HasParent() {
|
||||||
parentPath = cmd.Parent().CommandPath() + " "
|
parentPath = cmd.Parent().CommandPath() + " "
|
||||||
}
|
}
|
||||||
var aliases strings.Builder
|
aliases := cmd.CommandPath()
|
||||||
aliases.WriteString(cmd.CommandPath())
|
|
||||||
for _, alias := range cmd.Aliases {
|
for _, alias := range cmd.Aliases {
|
||||||
aliases.WriteString(", " + parentPath + alias)
|
aliases += ", " + parentPath + alias
|
||||||
}
|
}
|
||||||
return aliases.String()
|
return aliases
|
||||||
}
|
}
|
||||||
|
|
||||||
func topCommands(cmd *cobra.Command) []*cobra.Command {
|
func topCommands(cmd *cobra.Command) []*cobra.Command {
|
||||||
@@ -276,8 +320,8 @@ func topCommands(cmd *cobra.Command) []*cobra.Command {
|
|||||||
cmds = append(cmds, sub)
|
cmds = append(cmds, sub)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
slices.SortStableFunc(cmds, func(a, b *cobra.Command) int {
|
sort.SliceStable(cmds, func(i, j int) bool {
|
||||||
return sortorder.NaturalCompare(a.Annotations["category-top"], b.Annotations["category-top"])
|
return sortorder.NaturalLess(cmds[i].Annotations["category-top"], cmds[j].Annotations["category-top"])
|
||||||
})
|
})
|
||||||
return cmds
|
return cmds
|
||||||
}
|
}
|
||||||
@@ -301,10 +345,8 @@ func operationSubCommands(cmd *cobra.Command) []*cobra.Command {
|
|||||||
return cmds
|
return cmds
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultTermWidth = 80
|
|
||||||
|
|
||||||
func wrappedFlagUsages(cmd *cobra.Command) string {
|
func wrappedFlagUsages(cmd *cobra.Command) string {
|
||||||
width := defaultTermWidth
|
width := 80
|
||||||
if ws, err := term.GetWinsize(0); err == nil {
|
if ws, err := term.GetWinsize(0); err == nil {
|
||||||
width = int(ws.Width)
|
width = int(ws.Width)
|
||||||
}
|
}
|
||||||
@@ -320,9 +362,9 @@ func decoratedName(cmd *cobra.Command) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func vendorAndVersion(cmd *cobra.Command) string {
|
func vendorAndVersion(cmd *cobra.Command) string {
|
||||||
if vendor, ok := cmd.Annotations[metadata.CommandAnnotationPluginVendor]; ok && isPlugin(cmd) {
|
if vendor, ok := cmd.Annotations[pluginmanager.CommandAnnotationPluginVendor]; ok && isPlugin(cmd) {
|
||||||
version := ""
|
version := ""
|
||||||
if v, ok := cmd.Annotations[metadata.CommandAnnotationPluginVersion]; ok && v != "" {
|
if v, ok := cmd.Annotations[pluginmanager.CommandAnnotationPluginVersion]; ok && v != "" {
|
||||||
version = ", " + v
|
version = ", " + v
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("(%s%s)", vendor, version)
|
return fmt.Sprintf("(%s%s)", vendor, version)
|
||||||
@@ -354,10 +396,13 @@ func orchestratorSubCommands(cmd *cobra.Command) []*cobra.Command {
|
|||||||
func allManagementSubCommands(cmd *cobra.Command) []*cobra.Command {
|
func allManagementSubCommands(cmd *cobra.Command) []*cobra.Command {
|
||||||
cmds := []*cobra.Command{}
|
cmds := []*cobra.Command{}
|
||||||
for _, sub := range cmd.Commands() {
|
for _, sub := range cmd.Commands() {
|
||||||
if invalidPluginReason(sub) != "" {
|
if isPlugin(sub) {
|
||||||
|
if invalidPluginReason(sub) == "" {
|
||||||
|
cmds = append(cmds, sub)
|
||||||
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if sub.IsAvailableCommand() && (isPlugin(sub) || sub.HasSubCommands()) {
|
if sub.IsAvailableCommand() && sub.HasSubCommands() {
|
||||||
cmds = append(cmds, sub)
|
cmds = append(cmds, sub)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -378,10 +423,10 @@ func invalidPlugins(cmd *cobra.Command) []*cobra.Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func invalidPluginReason(cmd *cobra.Command) string {
|
func invalidPluginReason(cmd *cobra.Command) string {
|
||||||
return cmd.Annotations[metadata.CommandAnnotationPluginInvalid]
|
return cmd.Annotations[pluginmanager.CommandAnnotationPluginInvalid]
|
||||||
}
|
}
|
||||||
|
|
||||||
const usageTemplate = `Usage:
|
var usageTemplate = `Usage:
|
||||||
|
|
||||||
{{- if not .HasSubCommands}} {{.UseLine}}{{end}}
|
{{- if not .HasSubCommands}} {{.UseLine}}{{end}}
|
||||||
{{- if .HasSubCommands}} {{ .CommandPath}}{{- if .HasAvailableFlags}} [OPTIONS]{{end}} COMMAND{{end}}
|
{{- if .HasSubCommands}} {{ .CommandPath}}{{- if .HasAvailableFlags}} [OPTIONS]{{end}} COMMAND{{end}}
|
||||||
@@ -429,7 +474,7 @@ Common Commands:
|
|||||||
Management Commands:
|
Management Commands:
|
||||||
|
|
||||||
{{- range managementSubCommands . }}
|
{{- range managementSubCommands . }}
|
||||||
{{rpad (decoratedName .) (add .NamePadding 1)}}{{.Short}}
|
{{rpad (decoratedName .) (add .NamePadding 1)}}{{.Short}}{{ if isPlugin .}} {{vendorAndVersion .}}{{ end}}
|
||||||
{{- end}}
|
{{- end}}
|
||||||
|
|
||||||
{{- end}}
|
{{- end}}
|
||||||
@@ -438,7 +483,7 @@ Management Commands:
|
|||||||
Swarm Commands:
|
Swarm Commands:
|
||||||
|
|
||||||
{{- range orchestratorSubCommands . }}
|
{{- range orchestratorSubCommands . }}
|
||||||
{{rpad (decoratedName .) (add .NamePadding 1)}}{{.Short}}
|
{{rpad (decoratedName .) (add .NamePadding 1)}}{{.Short}}{{ if isPlugin .}} {{vendorAndVersion .}}{{ end}}
|
||||||
{{- end}}
|
{{- end}}
|
||||||
|
|
||||||
{{- end}}
|
{{- end}}
|
||||||
@@ -480,5 +525,5 @@ Run '{{.CommandPath}} COMMAND --help' for more information on a command.
|
|||||||
{{- end}}
|
{{- end}}
|
||||||
`
|
`
|
||||||
|
|
||||||
const helpTemplate = `
|
var helpTemplate = `
|
||||||
{{- if or .Runnable .HasSubCommands}}{{.UsageString}}{{end}}`
|
{{if or .Runnable .HasSubCommands}}{{.UsageString}}{{end}}`
|
||||||
|
|||||||
+29
-34
@@ -3,13 +3,35 @@ package cli
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/docker/cli/cli-plugins/metadata"
|
pluginmanager "github.com/docker/cli/cli-plugins/manager"
|
||||||
"github.com/google/go-cmp/cmp/cmpopts"
|
"github.com/google/go-cmp/cmp/cmpopts"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"gotest.tools/v3/assert"
|
"gotest.tools/v3/assert"
|
||||||
is "gotest.tools/v3/assert/cmp"
|
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) {
|
func TestVendorAndVersion(t *testing.T) {
|
||||||
// Non plugin.
|
// Non plugin.
|
||||||
assert.Equal(t, vendorAndVersion(&cobra.Command{Use: "test"}), "")
|
assert.Equal(t, vendorAndVersion(&cobra.Command{Use: "test"}), "")
|
||||||
@@ -27,9 +49,9 @@ func TestVendorAndVersion(t *testing.T) {
|
|||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "test",
|
Use: "test",
|
||||||
Annotations: map[string]string{
|
Annotations: map[string]string{
|
||||||
metadata.CommandAnnotationPlugin: "true",
|
pluginmanager.CommandAnnotationPlugin: "true",
|
||||||
metadata.CommandAnnotationPluginVendor: tc.vendor,
|
pluginmanager.CommandAnnotationPluginVendor: tc.vendor,
|
||||||
metadata.CommandAnnotationPluginVersion: tc.version,
|
pluginmanager.CommandAnnotationPluginVersion: tc.version,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
assert.Equal(t, vendorAndVersion(cmd), tc.expected)
|
assert.Equal(t, vendorAndVersion(cmd), tc.expected)
|
||||||
@@ -47,8 +69,8 @@ func TestInvalidPlugin(t *testing.T) {
|
|||||||
assert.Assert(t, is.Len(invalidPlugins(root), 0))
|
assert.Assert(t, is.Len(invalidPlugins(root), 0))
|
||||||
|
|
||||||
sub1.Annotations = map[string]string{
|
sub1.Annotations = map[string]string{
|
||||||
metadata.CommandAnnotationPlugin: "true",
|
pluginmanager.CommandAnnotationPlugin: "true",
|
||||||
metadata.CommandAnnotationPluginInvalid: "foo",
|
pluginmanager.CommandAnnotationPluginInvalid: "foo",
|
||||||
}
|
}
|
||||||
root.AddCommand(sub1, sub2)
|
root.AddCommand(sub1, sub2)
|
||||||
sub1.AddCommand(sub1sub1, sub1sub2)
|
sub1.AddCommand(sub1sub1, sub1sub2)
|
||||||
@@ -56,33 +78,6 @@ func TestInvalidPlugin(t *testing.T) {
|
|||||||
assert.DeepEqual(t, invalidPlugins(root), []*cobra.Command{sub1}, cmpopts.IgnoreUnexported(cobra.Command{}))
|
assert.DeepEqual(t, invalidPlugins(root), []*cobra.Command{sub1}, cmpopts.IgnoreUnexported(cobra.Command{}))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHiddenPlugin(t *testing.T) {
|
|
||||||
root := &cobra.Command{Use: "root"}
|
|
||||||
sub1 := &cobra.Command{
|
|
||||||
Use: "sub1",
|
|
||||||
Hidden: true,
|
|
||||||
Annotations: map[string]string{
|
|
||||||
metadata.CommandAnnotationPlugin: "true",
|
|
||||||
},
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {},
|
|
||||||
}
|
|
||||||
|
|
||||||
sub1sub1 := &cobra.Command{Use: "sub1sub1"}
|
|
||||||
sub1sub2 := &cobra.Command{Use: "sub1sub2"}
|
|
||||||
sub2 := &cobra.Command{
|
|
||||||
Use: "sub2",
|
|
||||||
Annotations: map[string]string{
|
|
||||||
metadata.CommandAnnotationPlugin: "true",
|
|
||||||
},
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {},
|
|
||||||
}
|
|
||||||
|
|
||||||
root.AddCommand(sub1, sub2)
|
|
||||||
sub1.AddCommand(sub1sub1, sub1sub2)
|
|
||||||
|
|
||||||
assert.DeepEqual(t, allManagementSubCommands(root), []*cobra.Command{sub2}, cmpopts.IgnoreFields(cobra.Command{}, "Run"), cmpopts.IgnoreUnexported(cobra.Command{}))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCommandAliases(t *testing.T) {
|
func TestCommandAliases(t *testing.T) {
|
||||||
root := &cobra.Command{Use: "root"}
|
root := &cobra.Command{Use: "root"}
|
||||||
sub := &cobra.Command{Use: "subcommand", Aliases: []string{"alias1", "alias2"}}
|
sub := &cobra.Command{Use: "subcommand", Aliases: []string{"alias1", "alias2"}}
|
||||||
@@ -105,6 +100,6 @@ func TestDecoratedName(t *testing.T) {
|
|||||||
topLevelCommand := &cobra.Command{Use: "pluginTopLevelCommand"}
|
topLevelCommand := &cobra.Command{Use: "pluginTopLevelCommand"}
|
||||||
root.AddCommand(topLevelCommand)
|
root.AddCommand(topLevelCommand)
|
||||||
assert.Equal(t, decoratedName(topLevelCommand), "pluginTopLevelCommand ")
|
assert.Equal(t, decoratedName(topLevelCommand), "pluginTopLevelCommand ")
|
||||||
topLevelCommand.Annotations = map[string]string{metadata.CommandAnnotationPlugin: "true"}
|
topLevelCommand.Annotations = map[string]string{pluginmanager.CommandAnnotationPlugin: "true"}
|
||||||
assert.Equal(t, decoratedName(topLevelCommand), "pluginTopLevelCommand*")
|
assert.Equal(t, decoratedName(topLevelCommand), "pluginTopLevelCommand*")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
"github.com/docker/cli/cli/command/image"
|
"github.com/docker/cli/cli/command/image"
|
||||||
"github.com/docker/cli/internal/commands"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
// NewBuilderCommand returns a cobra command for `builder` subcommands
|
||||||
commands.Register(newBuilderCommand)
|
func NewBuilderCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
commands.Register(func(c command.Cli) *cobra.Command {
|
|
||||||
return newBakeStubCommand(c)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// newBuilderCommand returns a cobra command for `builder` subcommands
|
|
||||||
func newBuilderCommand(dockerCLI command.Cli) *cobra.Command {
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "builder",
|
Use: "builder",
|
||||||
Short: "Manage builds",
|
Short: "Manage builds",
|
||||||
Args: cli.NoArgs,
|
Args: cli.NoArgs,
|
||||||
RunE: command.ShowHelp(dockerCLI.Err()),
|
RunE: command.ShowHelp(dockerCli.Err()),
|
||||||
Annotations: map[string]string{"version": "1.31"},
|
Annotations: map[string]string{"version": "1.31"},
|
||||||
|
|
||||||
DisableFlagsInUseLine: true,
|
|
||||||
}
|
}
|
||||||
cmd.AddCommand(
|
cmd.AddCommand(
|
||||||
newPruneCommand(dockerCLI),
|
NewPruneCommand(dockerCli),
|
||||||
// we should have a mechanism for registering sub-commands in the cli/internal/commands.Register function.
|
image.NewBuildCommand(dockerCli),
|
||||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
|
||||||
image.NewBuildCommand(dockerCLI),
|
|
||||||
)
|
)
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
// newBakeStubCommand returns a cobra command "stub" for the "bake" subcommand.
|
|
||||||
// This command is a placeholder / stub that is dynamically replaced by an
|
|
||||||
// alias for "docker buildx bake" if BuildKit is enabled (and the buildx plugin
|
|
||||||
// installed).
|
|
||||||
func newBakeStubCommand(dockerCLI command.Streams) *cobra.Command {
|
|
||||||
return &cobra.Command{
|
|
||||||
Use: "bake [OPTIONS] [TARGET...]",
|
|
||||||
Short: "Build from a file",
|
|
||||||
RunE: command.ShowHelp(dockerCLI.Err()),
|
|
||||||
Annotations: map[string]string{
|
|
||||||
// We want to show this command in the "top" category in --help
|
|
||||||
// output, and not to be grouped under "management commands".
|
|
||||||
"category-top": "5",
|
|
||||||
"aliases": "docker buildx bake",
|
|
||||||
"version": "1.31",
|
|
||||||
},
|
|
||||||
DisableFlagsInUseLine: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,36 +2,27 @@ package builder
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/docker/cli/cli"
|
"github.com/docker/cli/cli"
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
"github.com/docker/cli/cli/command/system/pruner"
|
"github.com/docker/cli/cli/command/completion"
|
||||||
"github.com/docker/cli/internal/prompt"
|
|
||||||
"github.com/docker/cli/opts"
|
"github.com/docker/cli/opts"
|
||||||
"github.com/docker/go-units"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/moby/moby/client"
|
units "github.com/docker/go-units"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
|
||||||
// Register the prune command to run as part of "docker system prune"
|
|
||||||
if err := pruner.Register(pruner.TypeBuildCache, pruneFn); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type pruneOptions struct {
|
type pruneOptions struct {
|
||||||
force bool
|
force bool
|
||||||
all bool
|
all bool
|
||||||
filter opts.FilterOpt
|
filter opts.FilterOpt
|
||||||
reservedSpace opts.MemBytes
|
keepStorage opts.MemBytes
|
||||||
}
|
}
|
||||||
|
|
||||||
// newPruneCommand returns a new cobra prune command for images
|
// NewPruneCommand returns a new cobra prune command for images
|
||||||
func newPruneCommand(dockerCLI command.Cli) *cobra.Command {
|
func NewPruneCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
options := pruneOptions{filter: opts.NewFilterOpt()}
|
options := pruneOptions{filter: opts.NewFilterOpt()}
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
@@ -39,26 +30,25 @@ func newPruneCommand(dockerCLI command.Cli) *cobra.Command {
|
|||||||
Short: "Remove build cache",
|
Short: "Remove build cache",
|
||||||
Args: cli.NoArgs,
|
Args: cli.NoArgs,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
spaceReclaimed, output, err := runPrune(cmd.Context(), dockerCLI, options)
|
spaceReclaimed, output, err := runPrune(dockerCli, options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if output != "" {
|
if output != "" {
|
||||||
_, _ = fmt.Fprintln(dockerCLI.Out(), output)
|
fmt.Fprintln(dockerCli.Out(), output)
|
||||||
}
|
}
|
||||||
_, _ = fmt.Fprintln(dockerCLI.Out(), "Total reclaimed space:", units.HumanSize(float64(spaceReclaimed)))
|
fmt.Fprintln(dockerCli.Out(), "Total reclaimed space:", units.HumanSize(float64(spaceReclaimed)))
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
Annotations: map[string]string{"version": "1.39"},
|
Annotations: map[string]string{"version": "1.39"},
|
||||||
ValidArgsFunction: cobra.NoFileCompletions,
|
ValidArgsFunction: completion.NoComplete,
|
||||||
DisableFlagsInUseLine: true,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
flags := cmd.Flags()
|
flags := cmd.Flags()
|
||||||
flags.BoolVarP(&options.force, "force", "f", false, "Do not prompt for confirmation")
|
flags.BoolVarP(&options.force, "force", "f", false, "Do not prompt for confirmation")
|
||||||
flags.BoolVarP(&options.all, "all", "a", false, "Remove all unused build cache, not just dangling ones")
|
flags.BoolVarP(&options.all, "all", "a", false, "Remove all unused build cache, not just dangling ones")
|
||||||
flags.Var(&options.filter, "filter", `Provide filter values (e.g. "until=24h")`)
|
flags.Var(&options.filter, "filter", `Provide filter values (e.g. "until=24h")`)
|
||||||
flags.Var(&options.reservedSpace, "keep-storage", "Amount of disk space to keep for cache")
|
flags.Var(&options.keepStorage, "keep-storage", "Amount of disk space to keep for cache")
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
@@ -68,32 +58,27 @@ const (
|
|||||||
allCacheWarning = `WARNING! This will remove all build cache. Are you sure you want to continue?`
|
allCacheWarning = `WARNING! This will remove all build cache. Are you sure you want to continue?`
|
||||||
)
|
)
|
||||||
|
|
||||||
func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions) (spaceReclaimed uint64, output string, err error) {
|
func runPrune(dockerCli command.Cli, options pruneOptions) (spaceReclaimed uint64, output string, err error) {
|
||||||
pruneFilters := command.PruneFilters(dockerCli, options.filter.Value())
|
pruneFilters := options.filter.Value()
|
||||||
|
pruneFilters = command.PruneFilters(dockerCli, pruneFilters)
|
||||||
|
|
||||||
warning := normalWarning
|
warning := normalWarning
|
||||||
if options.all {
|
if options.all {
|
||||||
warning = allCacheWarning
|
warning = allCacheWarning
|
||||||
}
|
}
|
||||||
if !options.force {
|
if !options.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), warning) {
|
||||||
r, err := prompt.Confirm(ctx, dockerCli.In(), dockerCli.Out(), warning)
|
return 0, "", nil
|
||||||
if err != nil {
|
|
||||||
return 0, "", err
|
|
||||||
}
|
|
||||||
if !r {
|
|
||||||
return 0, "", cancelledErr{errors.New("builder prune has been cancelled")}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := dockerCli.Client().BuildCachePrune(ctx, client.BuildCachePruneOptions{
|
report, err := dockerCli.Client().BuildCachePrune(context.Background(), types.BuildCachePruneOptions{
|
||||||
All: options.all,
|
All: options.all,
|
||||||
ReservedSpace: options.reservedSpace.Value(),
|
KeepStorage: options.keepStorage.Value(),
|
||||||
Filters: pruneFilters,
|
Filters: pruneFilters,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, "", err
|
return 0, "", err
|
||||||
}
|
}
|
||||||
report := resp.Report
|
|
||||||
if len(report.CachesDeleted) > 0 {
|
if len(report.CachesDeleted) > 0 {
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
sb.WriteString("Deleted build cache objects:\n")
|
sb.WriteString("Deleted build cache objects:\n")
|
||||||
@@ -107,26 +92,7 @@ func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions)
|
|||||||
return report.SpaceReclaimed, output, nil
|
return report.SpaceReclaimed, output, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type cancelledErr struct{ error }
|
// CachePrune executes a prune command for build cache
|
||||||
|
func CachePrune(dockerCli command.Cli, all bool, filter opts.FilterOpt) (uint64, string, error) {
|
||||||
func (cancelledErr) Cancelled() {}
|
return runPrune(dockerCli, pruneOptions{force: true, all: all, filter: filter})
|
||||||
|
|
||||||
// pruneFn prunes the build cache for use in "docker system prune" and
|
|
||||||
// returns the amount of space reclaimed and a detailed output string.
|
|
||||||
func pruneFn(ctx context.Context, dockerCLI command.Cli, options pruner.PruneOptions) (uint64, string, error) {
|
|
||||||
if !options.Confirmed {
|
|
||||||
// Dry-run: perform validation and produce confirmation before pruning.
|
|
||||||
var confirmMsg string
|
|
||||||
if options.All {
|
|
||||||
confirmMsg = "all build cache"
|
|
||||||
} else {
|
|
||||||
confirmMsg = "unused build cache"
|
|
||||||
}
|
|
||||||
return 0, confirmMsg, cancelledErr{errors.New("builder prune has been cancelled")}
|
|
||||||
}
|
|
||||||
return runPrune(ctx, dockerCLI, pruneOptions{
|
|
||||||
force: true,
|
|
||||||
all: options.All,
|
|
||||||
filter: options.Filter,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/moby/moby/client"
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
type fakeClient struct {
|
type fakeClient struct {
|
||||||
client.Client
|
client.Client
|
||||||
checkpointCreateFunc func(container string, options client.CheckpointCreateOptions) (client.CheckpointCreateResult, error)
|
checkpointCreateFunc func(container string, options types.CheckpointCreateOptions) error
|
||||||
checkpointDeleteFunc func(container string, options client.CheckpointRemoveOptions) (client.CheckpointRemoveResult, error)
|
checkpointDeleteFunc func(container string, options types.CheckpointDeleteOptions) error
|
||||||
checkpointListFunc func(container string, options client.CheckpointListOptions) (client.CheckpointListResult, error)
|
checkpointListFunc func(container string, options types.CheckpointListOptions) ([]types.Checkpoint, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cli *fakeClient) CheckpointCreate(_ context.Context, container string, options client.CheckpointCreateOptions) (client.CheckpointCreateResult, error) {
|
func (cli *fakeClient) CheckpointCreate(_ context.Context, container string, options types.CheckpointCreateOptions) error {
|
||||||
if cli.checkpointCreateFunc != nil {
|
if cli.checkpointCreateFunc != nil {
|
||||||
return cli.checkpointCreateFunc(container, options)
|
return cli.checkpointCreateFunc(container, options)
|
||||||
}
|
}
|
||||||
return client.CheckpointCreateResult{}, nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cli *fakeClient) CheckpointRemove(_ context.Context, container string, options client.CheckpointRemoveOptions) (client.CheckpointRemoveResult, error) {
|
func (cli *fakeClient) CheckpointDelete(_ context.Context, container string, options types.CheckpointDeleteOptions) error {
|
||||||
if cli.checkpointDeleteFunc != nil {
|
if cli.checkpointDeleteFunc != nil {
|
||||||
return cli.checkpointDeleteFunc(container, options)
|
return cli.checkpointDeleteFunc(container, options)
|
||||||
}
|
}
|
||||||
return client.CheckpointRemoveResult{}, nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cli *fakeClient) CheckpointList(_ context.Context, container string, options client.CheckpointListOptions) (client.CheckpointListResult, error) {
|
func (cli *fakeClient) CheckpointList(_ context.Context, container string, options types.CheckpointListOptions) ([]types.Checkpoint, error) {
|
||||||
if cli.checkpointListFunc != nil {
|
if cli.checkpointListFunc != nil {
|
||||||
return cli.checkpointListFunc(container, options)
|
return cli.checkpointListFunc(container, options)
|
||||||
}
|
}
|
||||||
return client.CheckpointListResult{}, nil
|
return []types.Checkpoint{}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,32 +3,26 @@ package checkpoint
|
|||||||
import (
|
import (
|
||||||
"github.com/docker/cli/cli"
|
"github.com/docker/cli/cli"
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
"github.com/docker/cli/internal/commands"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
// NewCheckpointCommand returns the `checkpoint` subcommand (only in experimental)
|
||||||
commands.Register(newCheckpointCommand)
|
func NewCheckpointCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
}
|
|
||||||
|
|
||||||
// newCheckpointCommand returns the `checkpoint` subcommand (only in experimental)
|
|
||||||
func newCheckpointCommand(dockerCLI command.Cli) *cobra.Command {
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "checkpoint",
|
Use: "checkpoint",
|
||||||
Short: "Manage checkpoints",
|
Short: "Manage checkpoints",
|
||||||
Args: cli.NoArgs,
|
Args: cli.NoArgs,
|
||||||
RunE: command.ShowHelp(dockerCLI.Err()),
|
RunE: command.ShowHelp(dockerCli.Err()),
|
||||||
Annotations: map[string]string{
|
Annotations: map[string]string{
|
||||||
"experimental": "",
|
"experimental": "",
|
||||||
"ostype": "linux",
|
"ostype": "linux",
|
||||||
"version": "1.25",
|
"version": "1.25",
|
||||||
},
|
},
|
||||||
DisableFlagsInUseLine: true,
|
|
||||||
}
|
}
|
||||||
cmd.AddCommand(
|
cmd.AddCommand(
|
||||||
newCreateCommand(dockerCLI),
|
newCreateCommand(dockerCli),
|
||||||
newListCommand(dockerCLI),
|
newListCommand(dockerCli),
|
||||||
newRemoveCommand(dockerCLI),
|
newRemoveCommand(dockerCli),
|
||||||
)
|
)
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import (
|
|||||||
|
|
||||||
"github.com/docker/cli/cli"
|
"github.com/docker/cli/cli"
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
"github.com/moby/moby/client"
|
"github.com/docker/cli/cli/command/completion"
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -17,7 +18,7 @@ type createOptions struct {
|
|||||||
leaveRunning bool
|
leaveRunning bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func newCreateCommand(dockerCLI command.Cli) *cobra.Command {
|
func newCreateCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
var opts createOptions
|
var opts createOptions
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
@@ -27,29 +28,32 @@ func newCreateCommand(dockerCLI command.Cli) *cobra.Command {
|
|||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
opts.container = args[0]
|
opts.container = args[0]
|
||||||
opts.checkpoint = args[1]
|
opts.checkpoint = args[1]
|
||||||
return runCreate(cmd.Context(), dockerCLI, opts)
|
return runCreate(dockerCli, opts)
|
||||||
},
|
},
|
||||||
ValidArgsFunction: cobra.NoFileCompletions,
|
ValidArgsFunction: completion.NoComplete,
|
||||||
DisableFlagsInUseLine: true,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
flags := cmd.Flags()
|
flags := cmd.Flags()
|
||||||
flags.BoolVar(&opts.leaveRunning, "leave-running", false, "Leave the container running after checkpoint")
|
flags.BoolVar(&opts.leaveRunning, "leave-running", false, "Leave the container running after checkpoint")
|
||||||
flags.StringVar(&opts.checkpointDir, "checkpoint-dir", "", "Use a custom checkpoint storage directory")
|
flags.StringVarP(&opts.checkpointDir, "checkpoint-dir", "", "", "Use a custom checkpoint storage directory")
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func runCreate(ctx context.Context, dockerCLI command.Cli, opts createOptions) error {
|
func runCreate(dockerCli command.Cli, opts createOptions) error {
|
||||||
_, err := dockerCLI.Client().CheckpointCreate(ctx, opts.container, client.CheckpointCreateOptions{
|
client := dockerCli.Client()
|
||||||
|
|
||||||
|
checkpointOpts := types.CheckpointCreateOptions{
|
||||||
CheckpointID: opts.checkpoint,
|
CheckpointID: opts.checkpoint,
|
||||||
CheckpointDir: opts.checkpointDir,
|
CheckpointDir: opts.checkpointDir,
|
||||||
Exit: !opts.leaveRunning,
|
Exit: !opts.leaveRunning,
|
||||||
})
|
}
|
||||||
|
|
||||||
|
err := client.CheckpointCreate(context.Background(), opts.container, checkpointOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, _ = fmt.Fprintln(dockerCLI.Out(), opts.checkpoint)
|
fmt.Fprintf(dockerCli.Out(), "%s\n", opts.checkpoint)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
package checkpoint
|
package checkpoint
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"io"
|
"io"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/docker/cli/internal/test"
|
"github.com/docker/cli/internal/test"
|
||||||
"github.com/moby/moby/client"
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/pkg/errors"
|
||||||
"gotest.tools/v3/assert"
|
"gotest.tools/v3/assert"
|
||||||
is "gotest.tools/v3/assert/cmp"
|
is "gotest.tools/v3/assert/cmp"
|
||||||
)
|
)
|
||||||
@@ -16,21 +15,21 @@ import (
|
|||||||
func TestCheckpointCreateErrors(t *testing.T) {
|
func TestCheckpointCreateErrors(t *testing.T) {
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
args []string
|
args []string
|
||||||
checkpointCreateFunc func(container string, options client.CheckpointCreateOptions) (client.CheckpointCreateResult, error)
|
checkpointCreateFunc func(container string, options types.CheckpointCreateOptions) error
|
||||||
expectedError string
|
expectedError string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
args: []string{"too-few-arguments"},
|
args: []string{"too-few-arguments"},
|
||||||
expectedError: "requires 2 arguments",
|
expectedError: "requires exactly 2 arguments",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
args: []string{"too", "many", "arguments"},
|
args: []string{"too", "many", "arguments"},
|
||||||
expectedError: "requires 2 arguments",
|
expectedError: "requires exactly 2 arguments",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
args: []string{"foo", "bar"},
|
args: []string{"foo", "bar"},
|
||||||
checkpointCreateFunc: func(container string, options client.CheckpointCreateOptions) (client.CheckpointCreateResult, error) {
|
checkpointCreateFunc: func(container string, options types.CheckpointCreateOptions) error {
|
||||||
return client.CheckpointCreateResult{}, errors.New("error creating checkpoint for container foo")
|
return errors.Errorf("error creating checkpoint for container foo")
|
||||||
},
|
},
|
||||||
expectedError: "error creating checkpoint for container foo",
|
expectedError: "error creating checkpoint for container foo",
|
||||||
},
|
},
|
||||||
@@ -43,45 +42,31 @@ func TestCheckpointCreateErrors(t *testing.T) {
|
|||||||
cmd := newCreateCommand(cli)
|
cmd := newCreateCommand(cli)
|
||||||
cmd.SetArgs(tc.args)
|
cmd.SetArgs(tc.args)
|
||||||
cmd.SetOut(io.Discard)
|
cmd.SetOut(io.Discard)
|
||||||
cmd.SetErr(io.Discard)
|
|
||||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCheckpointCreateWithOptions(t *testing.T) {
|
func TestCheckpointCreateWithOptions(t *testing.T) {
|
||||||
const (
|
var containerID, checkpointID, checkpointDir string
|
||||||
containerName = "container-foo"
|
var exit bool
|
||||||
checkpointName = "checkpoint-bar"
|
cli := test.NewFakeCli(&fakeClient{
|
||||||
checkpointDir = "/dir/foo"
|
checkpointCreateFunc: func(container string, options types.CheckpointCreateOptions) error {
|
||||||
)
|
containerID = container
|
||||||
|
checkpointID = options.CheckpointID
|
||||||
for _, tc := range []bool{true, false} {
|
checkpointDir = options.CheckpointDir
|
||||||
leaveRunning := strconv.FormatBool(tc)
|
exit = options.Exit
|
||||||
t.Run("leave-running="+leaveRunning, func(t *testing.T) {
|
return nil
|
||||||
var actualContainerName string
|
},
|
||||||
var actualOptions client.CheckpointCreateOptions
|
})
|
||||||
cli := test.NewFakeCli(&fakeClient{
|
cmd := newCreateCommand(cli)
|
||||||
checkpointCreateFunc: func(container string, options client.CheckpointCreateOptions) (client.CheckpointCreateResult, error) {
|
checkpoint := "checkpoint-bar"
|
||||||
actualContainerName = container
|
cmd.SetArgs([]string{"container-foo", checkpoint})
|
||||||
actualOptions = options
|
cmd.Flags().Set("leave-running", "true")
|
||||||
return client.CheckpointCreateResult{}, nil
|
cmd.Flags().Set("checkpoint-dir", "/dir/foo")
|
||||||
},
|
assert.NilError(t, cmd.Execute())
|
||||||
})
|
assert.Check(t, is.Equal("container-foo", containerID))
|
||||||
cmd := newCreateCommand(cli)
|
assert.Check(t, is.Equal(checkpoint, checkpointID))
|
||||||
cmd.SetOut(io.Discard)
|
assert.Check(t, is.Equal("/dir/foo", checkpointDir))
|
||||||
cmd.SetErr(io.Discard)
|
assert.Check(t, is.Equal(false, exit))
|
||||||
cmd.SetArgs([]string{containerName, checkpointName})
|
assert.Check(t, is.Equal(checkpoint, strings.TrimSpace(cli.OutBuffer().String())))
|
||||||
assert.Check(t, cmd.Flags().Set("leave-running", leaveRunning))
|
|
||||||
assert.Check(t, cmd.Flags().Set("checkpoint-dir", checkpointDir))
|
|
||||||
assert.NilError(t, cmd.Execute())
|
|
||||||
assert.Check(t, is.Equal(actualContainerName, containerName))
|
|
||||||
expected := client.CheckpointCreateOptions{
|
|
||||||
CheckpointID: checkpointName,
|
|
||||||
CheckpointDir: checkpointDir,
|
|
||||||
Exit: !tc,
|
|
||||||
}
|
|
||||||
assert.Check(t, is.Equal(actualOptions, expected))
|
|
||||||
assert.Check(t, is.Equal(strings.TrimSpace(cli.OutBuffer().String()), checkpointName))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,44 +2,48 @@ package checkpoint
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/docker/cli/cli/command/formatter"
|
"github.com/docker/cli/cli/command/formatter"
|
||||||
"github.com/moby/moby/api/types/checkpoint"
|
"github.com/docker/docker/api/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
defaultCheckpointFormat = "table {{.Name}}"
|
defaultCheckpointFormat = "table {{.Name}}"
|
||||||
checkpointNameHeader = "CHECKPOINT NAME"
|
|
||||||
|
checkpointNameHeader = "CHECKPOINT NAME"
|
||||||
)
|
)
|
||||||
|
|
||||||
// newFormat returns a format for use with a checkpointContext.
|
// NewFormat returns a format for use with a checkpoint Context
|
||||||
func newFormat(source string) formatter.Format {
|
func NewFormat(source string) formatter.Format {
|
||||||
if source == formatter.TableFormatKey {
|
switch source {
|
||||||
|
case formatter.TableFormatKey:
|
||||||
return defaultCheckpointFormat
|
return defaultCheckpointFormat
|
||||||
}
|
}
|
||||||
return formatter.Format(source)
|
return formatter.Format(source)
|
||||||
}
|
}
|
||||||
|
|
||||||
// formatWrite writes formatted checkpoints using the Context
|
// FormatWrite writes formatted checkpoints using the Context
|
||||||
func formatWrite(fmtCtx formatter.Context, checkpoints []checkpoint.Summary) error {
|
func FormatWrite(ctx formatter.Context, checkpoints []types.Checkpoint) error {
|
||||||
cpContext := &checkpointContext{
|
render := func(format func(subContext formatter.SubContext) error) error {
|
||||||
HeaderContext: formatter.HeaderContext{
|
for _, checkpoint := range checkpoints {
|
||||||
Header: formatter.SubHeaderContext{
|
if err := format(&checkpointContext{c: checkpoint}); err != nil {
|
||||||
"Name": checkpointNameHeader,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return fmtCtx.Write(cpContext, func(format func(subContext formatter.SubContext) error) error {
|
|
||||||
for _, cp := range checkpoints {
|
|
||||||
if err := format(&checkpointContext{c: cp}); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
}
|
||||||
|
return ctx.Write(newCheckpointContext(), render)
|
||||||
}
|
}
|
||||||
|
|
||||||
type checkpointContext struct {
|
type checkpointContext struct {
|
||||||
formatter.HeaderContext
|
formatter.HeaderContext
|
||||||
c checkpoint.Summary
|
c types.Checkpoint
|
||||||
|
}
|
||||||
|
|
||||||
|
func newCheckpointContext() *checkpointContext {
|
||||||
|
cpCtx := checkpointContext{}
|
||||||
|
cpCtx.Header = formatter.SubHeaderContext{
|
||||||
|
"Name": checkpointNameHeader,
|
||||||
|
}
|
||||||
|
return &cpCtx
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *checkpointContext) MarshalJSON() ([]byte, error) {
|
func (c *checkpointContext) MarshalJSON() ([]byte, error) {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/docker/cli/cli/command/formatter"
|
"github.com/docker/cli/cli/command/formatter"
|
||||||
"github.com/moby/moby/api/types/checkpoint"
|
"github.com/docker/docker/api/types"
|
||||||
"gotest.tools/v3/assert"
|
"gotest.tools/v3/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ func TestCheckpointContextFormatWrite(t *testing.T) {
|
|||||||
expected string
|
expected string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
formatter.Context{Format: newFormat(defaultCheckpointFormat)},
|
formatter.Context{Format: NewFormat(defaultCheckpointFormat)},
|
||||||
`CHECKPOINT NAME
|
`CHECKPOINT NAME
|
||||||
checkpoint-1
|
checkpoint-1
|
||||||
checkpoint-2
|
checkpoint-2
|
||||||
@@ -23,14 +23,14 @@ checkpoint-3
|
|||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
formatter.Context{Format: newFormat("{{.Name}}")},
|
formatter.Context{Format: NewFormat("{{.Name}}")},
|
||||||
`checkpoint-1
|
`checkpoint-1
|
||||||
checkpoint-2
|
checkpoint-2
|
||||||
checkpoint-3
|
checkpoint-3
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
formatter.Context{Format: newFormat("{{.Name}}:")},
|
formatter.Context{Format: NewFormat("{{.Name}}:")},
|
||||||
`checkpoint-1:
|
`checkpoint-1:
|
||||||
checkpoint-2:
|
checkpoint-2:
|
||||||
checkpoint-3:
|
checkpoint-3:
|
||||||
@@ -38,14 +38,15 @@ checkpoint-3:
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
checkpoints := []types.Checkpoint{
|
||||||
|
{Name: "checkpoint-1"},
|
||||||
|
{Name: "checkpoint-2"},
|
||||||
|
{Name: "checkpoint-3"},
|
||||||
|
}
|
||||||
for _, testcase := range cases {
|
for _, testcase := range cases {
|
||||||
out := bytes.NewBufferString("")
|
out := bytes.NewBufferString("")
|
||||||
testcase.context.Output = out
|
testcase.context.Output = out
|
||||||
err := formatWrite(testcase.context, []checkpoint.Summary{
|
err := FormatWrite(testcase.context, checkpoints)
|
||||||
{Name: "checkpoint-1"},
|
|
||||||
{Name: "checkpoint-2"},
|
|
||||||
{Name: "checkpoint-3"},
|
|
||||||
})
|
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
assert.Equal(t, out.String(), testcase.expected)
|
assert.Equal(t, out.String(), testcase.expected)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
"github.com/docker/cli/cli/command/completion"
|
"github.com/docker/cli/cli/command/completion"
|
||||||
"github.com/docker/cli/cli/command/formatter"
|
"github.com/docker/cli/cli/command/formatter"
|
||||||
"github.com/moby/moby/client"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ type listOptions struct {
|
|||||||
checkpointDir string
|
checkpointDir string
|
||||||
}
|
}
|
||||||
|
|
||||||
func newListCommand(dockerCLI command.Cli) *cobra.Command {
|
func newListCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
var opts listOptions
|
var opts listOptions
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
@@ -24,29 +24,32 @@ func newListCommand(dockerCLI command.Cli) *cobra.Command {
|
|||||||
Short: "List checkpoints for a container",
|
Short: "List checkpoints for a container",
|
||||||
Args: cli.ExactArgs(1),
|
Args: cli.ExactArgs(1),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return runList(cmd.Context(), dockerCLI, args[0], opts)
|
return runList(dockerCli, args[0], opts)
|
||||||
},
|
},
|
||||||
ValidArgsFunction: completion.ContainerNames(dockerCLI, false),
|
ValidArgsFunction: completion.ContainerNames(dockerCli, false),
|
||||||
DisableFlagsInUseLine: true,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
flags := cmd.Flags()
|
flags := cmd.Flags()
|
||||||
flags.StringVar(&opts.checkpointDir, "checkpoint-dir", "", "Use a custom checkpoint storage directory")
|
flags.StringVarP(&opts.checkpointDir, "checkpoint-dir", "", "", "Use a custom checkpoint storage directory")
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func runList(ctx context.Context, dockerCLI command.Cli, container string, opts listOptions) error {
|
func runList(dockerCli command.Cli, container string, opts listOptions) error {
|
||||||
checkpoints, err := dockerCLI.Client().CheckpointList(ctx, container, client.CheckpointListOptions{
|
client := dockerCli.Client()
|
||||||
|
|
||||||
|
listOpts := types.CheckpointListOptions{
|
||||||
CheckpointDir: opts.checkpointDir,
|
CheckpointDir: opts.checkpointDir,
|
||||||
})
|
}
|
||||||
|
|
||||||
|
checkpoints, err := client.CheckpointList(context.Background(), container, listOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
cpCtx := formatter.Context{
|
cpCtx := formatter.Context{
|
||||||
Output: dockerCLI.Out(),
|
Output: dockerCli.Out(),
|
||||||
Format: newFormat(formatter.TableFormatKey),
|
Format: NewFormat(formatter.TableFormatKey),
|
||||||
}
|
}
|
||||||
return formatWrite(cpCtx, checkpoints.Items)
|
return FormatWrite(cpCtx, checkpoints)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
package checkpoint
|
package checkpoint
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"io"
|
"io"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/docker/cli/internal/test"
|
"github.com/docker/cli/internal/test"
|
||||||
"github.com/moby/moby/api/types/checkpoint"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/moby/moby/client"
|
"github.com/pkg/errors"
|
||||||
"gotest.tools/v3/assert"
|
"gotest.tools/v3/assert"
|
||||||
is "gotest.tools/v3/assert/cmp"
|
is "gotest.tools/v3/assert/cmp"
|
||||||
"gotest.tools/v3/golden"
|
"gotest.tools/v3/golden"
|
||||||
@@ -16,21 +15,21 @@ import (
|
|||||||
func TestCheckpointListErrors(t *testing.T) {
|
func TestCheckpointListErrors(t *testing.T) {
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
args []string
|
args []string
|
||||||
checkpointListFunc func(container string, options client.CheckpointListOptions) (client.CheckpointListResult, error)
|
checkpointListFunc func(container string, options types.CheckpointListOptions) ([]types.Checkpoint, error)
|
||||||
expectedError string
|
expectedError string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
args: []string{},
|
args: []string{},
|
||||||
expectedError: "requires 1 argument",
|
expectedError: "requires exactly 1 argument",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
args: []string{"too", "many", "arguments"},
|
args: []string{"too", "many", "arguments"},
|
||||||
expectedError: "requires 1 argument",
|
expectedError: "requires exactly 1 argument",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
args: []string{"foo"},
|
args: []string{"foo"},
|
||||||
checkpointListFunc: func(container string, options client.CheckpointListOptions) (client.CheckpointListResult, error) {
|
checkpointListFunc: func(container string, options types.CheckpointListOptions) ([]types.Checkpoint, error) {
|
||||||
return client.CheckpointListResult{}, errors.New("error getting checkpoints for container foo")
|
return []types.Checkpoint{}, errors.Errorf("error getting checkpoints for container foo")
|
||||||
},
|
},
|
||||||
expectedError: "error getting checkpoints for container foo",
|
expectedError: "error getting checkpoints for container foo",
|
||||||
},
|
},
|
||||||
@@ -43,7 +42,6 @@ func TestCheckpointListErrors(t *testing.T) {
|
|||||||
cmd := newListCommand(cli)
|
cmd := newListCommand(cli)
|
||||||
cmd.SetArgs(tc.args)
|
cmd.SetArgs(tc.args)
|
||||||
cmd.SetOut(io.Discard)
|
cmd.SetOut(io.Discard)
|
||||||
cmd.SetErr(io.Discard)
|
|
||||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -51,19 +49,17 @@ func TestCheckpointListErrors(t *testing.T) {
|
|||||||
func TestCheckpointListWithOptions(t *testing.T) {
|
func TestCheckpointListWithOptions(t *testing.T) {
|
||||||
var containerID, checkpointDir string
|
var containerID, checkpointDir string
|
||||||
cli := test.NewFakeCli(&fakeClient{
|
cli := test.NewFakeCli(&fakeClient{
|
||||||
checkpointListFunc: func(container string, options client.CheckpointListOptions) (client.CheckpointListResult, error) {
|
checkpointListFunc: func(container string, options types.CheckpointListOptions) ([]types.Checkpoint, error) {
|
||||||
containerID = container
|
containerID = container
|
||||||
checkpointDir = options.CheckpointDir
|
checkpointDir = options.CheckpointDir
|
||||||
return client.CheckpointListResult{
|
return []types.Checkpoint{
|
||||||
Items: []checkpoint.Summary{
|
{Name: "checkpoint-foo"},
|
||||||
{Name: "checkpoint-foo"},
|
|
||||||
},
|
|
||||||
}, nil
|
}, nil
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
cmd := newListCommand(cli)
|
cmd := newListCommand(cli)
|
||||||
cmd.SetArgs([]string{"container-foo"})
|
cmd.SetArgs([]string{"container-foo"})
|
||||||
assert.Check(t, cmd.Flags().Set("checkpoint-dir", "/dir/foo"))
|
cmd.Flags().Set("checkpoint-dir", "/dir/foo")
|
||||||
assert.NilError(t, cmd.Execute())
|
assert.NilError(t, cmd.Execute())
|
||||||
assert.Check(t, is.Equal("container-foo", containerID))
|
assert.Check(t, is.Equal("container-foo", containerID))
|
||||||
assert.Check(t, is.Equal("/dir/foo", checkpointDir))
|
assert.Check(t, is.Equal("/dir/foo", checkpointDir))
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
package checkpoint
|
package checkpoint
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
"github.com/docker/cli/cli"
|
"github.com/docker/cli/cli"
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
"github.com/moby/moby/client"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -11,7 +13,7 @@ type removeOptions struct {
|
|||||||
checkpointDir string
|
checkpointDir string
|
||||||
}
|
}
|
||||||
|
|
||||||
func newRemoveCommand(dockerCLI command.Cli) *cobra.Command {
|
func newRemoveCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
var opts removeOptions
|
var opts removeOptions
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
@@ -20,18 +22,23 @@ func newRemoveCommand(dockerCLI command.Cli) *cobra.Command {
|
|||||||
Short: "Remove a checkpoint",
|
Short: "Remove a checkpoint",
|
||||||
Args: cli.ExactArgs(2),
|
Args: cli.ExactArgs(2),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
containerID, checkpointID := args[0], args[1]
|
return runRemove(dockerCli, args[0], args[1], opts)
|
||||||
_, err := dockerCLI.Client().CheckpointRemove(cmd.Context(), containerID, client.CheckpointRemoveOptions{
|
|
||||||
CheckpointID: checkpointID,
|
|
||||||
CheckpointDir: opts.checkpointDir,
|
|
||||||
})
|
|
||||||
return err
|
|
||||||
},
|
},
|
||||||
DisableFlagsInUseLine: true,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
flags := cmd.Flags()
|
flags := cmd.Flags()
|
||||||
flags.StringVar(&opts.checkpointDir, "checkpoint-dir", "", "Use a custom checkpoint storage directory")
|
flags.StringVarP(&opts.checkpointDir, "checkpoint-dir", "", "", "Use a custom checkpoint storage directory")
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func runRemove(dockerCli command.Cli, container string, checkpoint string, opts removeOptions) error {
|
||||||
|
client := dockerCli.Client()
|
||||||
|
|
||||||
|
removeOpts := types.CheckpointDeleteOptions{
|
||||||
|
CheckpointID: checkpoint,
|
||||||
|
CheckpointDir: opts.checkpointDir,
|
||||||
|
}
|
||||||
|
|
||||||
|
return client.CheckpointDelete(context.Background(), container, removeOpts)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
package checkpoint
|
package checkpoint
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"io"
|
"io"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/docker/cli/internal/test"
|
"github.com/docker/cli/internal/test"
|
||||||
"github.com/moby/moby/client"
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/pkg/errors"
|
||||||
"gotest.tools/v3/assert"
|
"gotest.tools/v3/assert"
|
||||||
is "gotest.tools/v3/assert/cmp"
|
is "gotest.tools/v3/assert/cmp"
|
||||||
)
|
)
|
||||||
@@ -14,21 +14,21 @@ import (
|
|||||||
func TestCheckpointRemoveErrors(t *testing.T) {
|
func TestCheckpointRemoveErrors(t *testing.T) {
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
args []string
|
args []string
|
||||||
checkpointDeleteFunc func(container string, options client.CheckpointRemoveOptions) (client.CheckpointRemoveResult, error)
|
checkpointDeleteFunc func(container string, options types.CheckpointDeleteOptions) error
|
||||||
expectedError string
|
expectedError string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
args: []string{"too-few-arguments"},
|
args: []string{"too-few-arguments"},
|
||||||
expectedError: "requires 2 arguments",
|
expectedError: "requires exactly 2 arguments",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
args: []string{"too", "many", "arguments"},
|
args: []string{"too", "many", "arguments"},
|
||||||
expectedError: "requires 2 arguments",
|
expectedError: "requires exactly 2 arguments",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
args: []string{"foo", "bar"},
|
args: []string{"foo", "bar"},
|
||||||
checkpointDeleteFunc: func(container string, options client.CheckpointRemoveOptions) (client.CheckpointRemoveResult, error) {
|
checkpointDeleteFunc: func(container string, options types.CheckpointDeleteOptions) error {
|
||||||
return client.CheckpointRemoveResult{}, errors.New("error deleting checkpoint")
|
return errors.Errorf("error deleting checkpoint")
|
||||||
},
|
},
|
||||||
expectedError: "error deleting checkpoint",
|
expectedError: "error deleting checkpoint",
|
||||||
},
|
},
|
||||||
@@ -41,7 +41,6 @@ func TestCheckpointRemoveErrors(t *testing.T) {
|
|||||||
cmd := newRemoveCommand(cli)
|
cmd := newRemoveCommand(cli)
|
||||||
cmd.SetArgs(tc.args)
|
cmd.SetArgs(tc.args)
|
||||||
cmd.SetOut(io.Discard)
|
cmd.SetOut(io.Discard)
|
||||||
cmd.SetErr(io.Discard)
|
|
||||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -49,16 +48,16 @@ func TestCheckpointRemoveErrors(t *testing.T) {
|
|||||||
func TestCheckpointRemoveWithOptions(t *testing.T) {
|
func TestCheckpointRemoveWithOptions(t *testing.T) {
|
||||||
var containerID, checkpointID, checkpointDir string
|
var containerID, checkpointID, checkpointDir string
|
||||||
cli := test.NewFakeCli(&fakeClient{
|
cli := test.NewFakeCli(&fakeClient{
|
||||||
checkpointDeleteFunc: func(container string, options client.CheckpointRemoveOptions) (client.CheckpointRemoveResult, error) {
|
checkpointDeleteFunc: func(container string, options types.CheckpointDeleteOptions) error {
|
||||||
containerID = container
|
containerID = container
|
||||||
checkpointID = options.CheckpointID
|
checkpointID = options.CheckpointID
|
||||||
checkpointDir = options.CheckpointDir
|
checkpointDir = options.CheckpointDir
|
||||||
return client.CheckpointRemoveResult{}, nil
|
return nil
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
cmd := newRemoveCommand(cli)
|
cmd := newRemoveCommand(cli)
|
||||||
cmd.SetArgs([]string{"container-foo", "checkpoint-bar"})
|
cmd.SetArgs([]string{"container-foo", "checkpoint-bar"})
|
||||||
assert.Check(t, cmd.Flags().Set("checkpoint-dir", "/dir/foo"))
|
cmd.Flags().Set("checkpoint-dir", "/dir/foo")
|
||||||
assert.NilError(t, cmd.Execute())
|
assert.NilError(t, cmd.Execute())
|
||||||
assert.Check(t, is.Equal("container-foo", containerID))
|
assert.Check(t, is.Equal("container-foo", containerID))
|
||||||
assert.Check(t, is.Equal("checkpoint-bar", checkpointID))
|
assert.Check(t, is.Equal("checkpoint-bar", checkpointID))
|
||||||
|
|||||||
+143
-223
@@ -1,16 +1,14 @@
|
|||||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
|
||||||
//go:build go1.26
|
|
||||||
|
|
||||||
package command
|
package command
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -21,12 +19,21 @@ import (
|
|||||||
"github.com/docker/cli/cli/context/store"
|
"github.com/docker/cli/cli/context/store"
|
||||||
"github.com/docker/cli/cli/debug"
|
"github.com/docker/cli/cli/debug"
|
||||||
cliflags "github.com/docker/cli/cli/flags"
|
cliflags "github.com/docker/cli/cli/flags"
|
||||||
|
manifeststore "github.com/docker/cli/cli/manifest/store"
|
||||||
|
registryclient "github.com/docker/cli/cli/registry/client"
|
||||||
"github.com/docker/cli/cli/streams"
|
"github.com/docker/cli/cli/streams"
|
||||||
|
"github.com/docker/cli/cli/trust"
|
||||||
"github.com/docker/cli/cli/version"
|
"github.com/docker/cli/cli/version"
|
||||||
dopts "github.com/docker/cli/opts"
|
dopts "github.com/docker/cli/opts"
|
||||||
"github.com/moby/moby/api/types/build"
|
"github.com/docker/docker/api"
|
||||||
"github.com/moby/moby/client"
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/api/types/registry"
|
||||||
|
"github.com/docker/docker/api/types/swarm"
|
||||||
|
"github.com/docker/docker/client"
|
||||||
|
"github.com/docker/go-connections/tlsconfig"
|
||||||
|
"github.com/pkg/errors"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
notaryclient "github.com/theupdateframework/notary/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
const defaultInitTimeout = 2 * time.Second
|
const defaultInitTimeout = 2 * time.Second
|
||||||
@@ -35,52 +42,54 @@ const defaultInitTimeout = 2 * time.Second
|
|||||||
type Streams interface {
|
type Streams interface {
|
||||||
In() *streams.In
|
In() *streams.In
|
||||||
Out() *streams.Out
|
Out() *streams.Out
|
||||||
Err() *streams.Out
|
Err() io.Writer
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cli represents the docker command line client.
|
// Cli represents the docker command line client.
|
||||||
type Cli interface {
|
type Cli interface {
|
||||||
Client() client.APIClient
|
Client() client.APIClient
|
||||||
Streams
|
Out() *streams.Out
|
||||||
|
Err() io.Writer
|
||||||
|
In() *streams.In
|
||||||
SetIn(in *streams.In)
|
SetIn(in *streams.In)
|
||||||
config.Provider
|
Apply(ops ...DockerCliOption) error
|
||||||
|
ConfigFile() *configfile.ConfigFile
|
||||||
ServerInfo() ServerInfo
|
ServerInfo() ServerInfo
|
||||||
|
NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (notaryclient.Repository, error)
|
||||||
|
DefaultVersion() string
|
||||||
CurrentVersion() string
|
CurrentVersion() string
|
||||||
|
ManifestStore() manifeststore.Store
|
||||||
|
RegistryClient(bool) registryclient.RegistryClient
|
||||||
|
ContentTrustEnabled() bool
|
||||||
BuildKitEnabled() (bool, error)
|
BuildKitEnabled() (bool, error)
|
||||||
ContextStore() store.Store
|
ContextStore() store.Store
|
||||||
CurrentContext() string
|
CurrentContext() string
|
||||||
DockerEndpoint() docker.Endpoint
|
DockerEndpoint() docker.Endpoint
|
||||||
TelemetryClient
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DockerCli is an instance the docker command line client.
|
// DockerCli is an instance the docker command line client.
|
||||||
// Instances of the client should be created using the [NewDockerCli]
|
// Instances of the client can be returned from NewDockerCli.
|
||||||
// constructor to make sure they are properly initialized with defaults
|
|
||||||
// set.
|
|
||||||
type DockerCli struct {
|
type DockerCli struct {
|
||||||
configFile *configfile.ConfigFile
|
configFile *configfile.ConfigFile
|
||||||
options *cliflags.ClientOptions
|
options *cliflags.ClientOptions
|
||||||
clientOpts []client.Opt
|
|
||||||
in *streams.In
|
in *streams.In
|
||||||
out *streams.Out
|
out *streams.Out
|
||||||
err *streams.Out
|
err io.Writer
|
||||||
client client.APIClient
|
client client.APIClient
|
||||||
serverInfo ServerInfo
|
serverInfo ServerInfo
|
||||||
|
contentTrust bool
|
||||||
contextStore store.Store
|
contextStore store.Store
|
||||||
currentContext string
|
currentContext string
|
||||||
init sync.Once
|
init sync.Once
|
||||||
initErr error
|
initErr error
|
||||||
dockerEndpoint docker.Endpoint
|
dockerEndpoint docker.Endpoint
|
||||||
contextStoreConfig *store.Config
|
contextStoreConfig store.Config
|
||||||
initTimeout time.Duration
|
initTimeout time.Duration
|
||||||
res telemetryResource
|
}
|
||||||
|
|
||||||
// baseCtx is the base context used for internal operations. In the future
|
// DefaultVersion returns api.defaultVersion.
|
||||||
// this may be replaced by explicitly passing a context to functions that
|
func (cli *DockerCli) DefaultVersion() string {
|
||||||
// need it.
|
return api.DefaultVersion
|
||||||
baseCtx context.Context
|
|
||||||
|
|
||||||
enableGlobalMeter, enableGlobalTracer bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CurrentVersion returns the API version currently negotiated, or the default
|
// CurrentVersion returns the API version currently negotiated, or the default
|
||||||
@@ -88,7 +97,7 @@ type DockerCli struct {
|
|||||||
func (cli *DockerCli) CurrentVersion() string {
|
func (cli *DockerCli) CurrentVersion() string {
|
||||||
_ = cli.initialize()
|
_ = cli.initialize()
|
||||||
if cli.client == nil {
|
if cli.client == nil {
|
||||||
return client.MaxAPIVersion
|
return api.DefaultVersion
|
||||||
}
|
}
|
||||||
return cli.client.ClientVersion()
|
return cli.client.ClientVersion()
|
||||||
}
|
}
|
||||||
@@ -96,7 +105,7 @@ func (cli *DockerCli) CurrentVersion() string {
|
|||||||
// Client returns the APIClient
|
// Client returns the APIClient
|
||||||
func (cli *DockerCli) Client() client.APIClient {
|
func (cli *DockerCli) Client() client.APIClient {
|
||||||
if err := cli.initialize(); err != nil {
|
if err := cli.initialize(); err != nil {
|
||||||
_, _ = fmt.Fprintln(cli.Err(), "Failed to initialize:", err)
|
_, _ = fmt.Fprintf(cli.Err(), "Failed to initialize: %s\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
return cli.client
|
return cli.client
|
||||||
@@ -108,7 +117,7 @@ func (cli *DockerCli) Out() *streams.Out {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Err returns the writer used for stderr
|
// Err returns the writer used for stderr
|
||||||
func (cli *DockerCli) Err() *streams.Out {
|
func (cli *DockerCli) Err() io.Writer {
|
||||||
return cli.err
|
return cli.err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,13 +156,19 @@ func (cli *DockerCli) ServerInfo() ServerInfo {
|
|||||||
return cli.serverInfo
|
return cli.serverInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ContentTrustEnabled returns whether content trust has been enabled by an
|
||||||
|
// environment variable.
|
||||||
|
func (cli *DockerCli) ContentTrustEnabled() bool {
|
||||||
|
return cli.contentTrust
|
||||||
|
}
|
||||||
|
|
||||||
// BuildKitEnabled returns buildkit is enabled or not.
|
// BuildKitEnabled returns buildkit is enabled or not.
|
||||||
func (cli *DockerCli) BuildKitEnabled() (bool, error) {
|
func (cli *DockerCli) BuildKitEnabled() (bool, error) {
|
||||||
// use DOCKER_BUILDKIT env var value if set and not empty
|
// use DOCKER_BUILDKIT env var value if set and not empty
|
||||||
if v := os.Getenv("DOCKER_BUILDKIT"); v != "" {
|
if v := os.Getenv("DOCKER_BUILDKIT"); v != "" {
|
||||||
enabled, err := strconv.ParseBool(v)
|
enabled, err := strconv.ParseBool(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, fmt.Errorf("DOCKER_BUILDKIT environment variable expects boolean value: %w", err)
|
return false, errors.Wrap(err, "DOCKER_BUILDKIT environment variable expects boolean value")
|
||||||
}
|
}
|
||||||
return enabled, nil
|
return enabled, nil
|
||||||
}
|
}
|
||||||
@@ -162,53 +177,41 @@ func (cli *DockerCli) BuildKitEnabled() (bool, error) {
|
|||||||
if _, ok := aliasMap["builder"]; ok {
|
if _, ok := aliasMap["builder"]; ok {
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
// otherwise, assume BuildKit is enabled but
|
||||||
si := cli.ServerInfo()
|
// not if wcow reported from server side
|
||||||
if si.BuildkitVersion == build.BuilderBuildKit {
|
return cli.ServerInfo().OSType != "windows", nil
|
||||||
// The daemon advertised BuildKit as the preferred builder; this may
|
|
||||||
// be either a Linux daemon or a Windows daemon with experimental
|
|
||||||
// BuildKit support enabled.
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// otherwise, assume BuildKit is enabled for Linux, but disabled for
|
|
||||||
// Windows / WCOW, which does not yet support BuildKit by default.
|
|
||||||
return si.OSType != "windows", nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// HooksEnabled returns whether plugin hooks are enabled.
|
// ManifestStore returns a store for local manifests
|
||||||
func (cli *DockerCli) HooksEnabled() bool {
|
func (cli *DockerCli) ManifestStore() manifeststore.Store {
|
||||||
// use DOCKER_CLI_HOOKS env var value if set and not empty
|
// TODO: support override default location from config file
|
||||||
if v := os.Getenv("DOCKER_CLI_HOOKS"); v != "" {
|
return manifeststore.NewStore(filepath.Join(config.Dir(), "manifests"))
|
||||||
enabled, err := strconv.ParseBool(v)
|
}
|
||||||
if err != nil {
|
|
||||||
return false
|
// RegistryClient returns a client for communicating with a Docker distribution
|
||||||
}
|
// registry
|
||||||
return enabled
|
func (cli *DockerCli) RegistryClient(allowInsecure bool) registryclient.RegistryClient {
|
||||||
|
resolver := func(ctx context.Context, index *registry.IndexInfo) types.AuthConfig {
|
||||||
|
return ResolveAuthConfig(ctx, cli, index)
|
||||||
}
|
}
|
||||||
// legacy support DOCKER_CLI_HINTS env var
|
return registryclient.NewRegistryClient(resolver, UserAgent(), allowInsecure)
|
||||||
if v := os.Getenv("DOCKER_CLI_HINTS"); v != "" {
|
}
|
||||||
enabled, err := strconv.ParseBool(v)
|
|
||||||
if err != nil {
|
// InitializeOpt is the type of the functional options passed to DockerCli.Initialize
|
||||||
return false
|
type InitializeOpt func(dockerCli *DockerCli) error
|
||||||
}
|
|
||||||
return enabled
|
// WithInitializeClient is passed to DockerCli.Initialize by callers who wish to set a particular API Client for use by the CLI.
|
||||||
|
func WithInitializeClient(makeClient func(dockerCli *DockerCli) (client.APIClient, error)) InitializeOpt {
|
||||||
|
return func(dockerCli *DockerCli) error {
|
||||||
|
var err error
|
||||||
|
dockerCli.client, err = makeClient(dockerCli)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
featuresMap := cli.ConfigFile().Features
|
|
||||||
if v, ok := featuresMap["hooks"]; ok {
|
|
||||||
enabled, err := strconv.ParseBool(v)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return enabled
|
|
||||||
}
|
|
||||||
// default to false
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize the dockerCli runs initialization that must happen after command
|
// Initialize the dockerCli runs initialization that must happen after command
|
||||||
// line flags are parsed.
|
// line flags are parsed.
|
||||||
func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...CLIOption) error {
|
func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...InitializeOpt) error {
|
||||||
for _, o := range ops {
|
for _, o := range ops {
|
||||||
if err := o(cli); err != nil {
|
if err := o(cli); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -224,103 +227,53 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...CLIOption)
|
|||||||
debug.Enable()
|
debug.Enable()
|
||||||
}
|
}
|
||||||
if opts.Context != "" && len(opts.Hosts) > 0 {
|
if opts.Context != "" && len(opts.Hosts) > 0 {
|
||||||
return errors.New("conflicting options: cannot specify both --host and --context")
|
return errors.New("conflicting options: either specify --host or --context, not both")
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cli.options = opts
|
cli.options = opts
|
||||||
cli.configFile = config.LoadDefaultConfigFile(cli.err)
|
cli.configFile = config.LoadDefaultConfigFile(cli.err)
|
||||||
cli.currentContext = resolveContextName(cli.options, cli.configFile)
|
cli.currentContext = resolveContextName(cli.options, cli.configFile)
|
||||||
cli.contextStore = &ContextStoreWithDefault{
|
cli.contextStore = &ContextStoreWithDefault{
|
||||||
Store: store.New(config.ContextStoreDir(), *cli.contextStoreConfig),
|
Store: store.New(config.ContextStoreDir(), cli.contextStoreConfig),
|
||||||
Resolver: func() (*DefaultContext, error) {
|
Resolver: func() (*DefaultContext, error) {
|
||||||
return resolveDefaultContext(cli.options, *cli.contextStoreConfig)
|
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewAPIClientFromFlags creates a new APIClient from command line flags
|
// NewAPIClientFromFlags creates a new APIClient from command line flags
|
||||||
func NewAPIClientFromFlags(opts *cliflags.ClientOptions, configFile *configfile.ConfigFile) (client.APIClient, error) {
|
func NewAPIClientFromFlags(opts *cliflags.ClientOptions, configFile *configfile.ConfigFile) (client.APIClient, error) {
|
||||||
if opts.Context != "" && len(opts.Hosts) > 0 {
|
if opts.Context != "" && len(opts.Hosts) > 0 {
|
||||||
return nil, errors.New("conflicting options: cannot specify both --host and --context")
|
return nil, errors.New("conflicting options: either specify --host or --context, not both")
|
||||||
}
|
}
|
||||||
|
|
||||||
storeConfig := DefaultContextStoreConfig()
|
storeConfig := DefaultContextStoreConfig()
|
||||||
contextStore := &ContextStoreWithDefault{
|
contextStore := &ContextStoreWithDefault{
|
||||||
Store: store.New(config.ContextStoreDir(), storeConfig),
|
Store: store.New(config.ContextStoreDir(), storeConfig),
|
||||||
Resolver: func() (*DefaultContext, error) {
|
Resolver: func() (*DefaultContext, error) {
|
||||||
return resolveDefaultContext(opts, storeConfig)
|
return ResolveDefaultContext(opts, storeConfig)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
endpoint, err := resolveDockerEndpoint(contextStore, resolveContextName(opts, configFile))
|
endpoint, err := resolveDockerEndpoint(contextStore, resolveContextName(opts, configFile))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("unable to resolve docker endpoint: %w", err)
|
return nil, errors.Wrap(err, "unable to resolve docker endpoint")
|
||||||
}
|
}
|
||||||
return newAPIClientFromEndpoint(endpoint, configFile, client.WithUserAgent(UserAgent()))
|
return newAPIClientFromEndpoint(endpoint, configFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newAPIClientFromEndpoint(ep docker.Endpoint, configFile *configfile.ConfigFile, extraOpts ...client.Opt) (client.APIClient, error) {
|
func newAPIClientFromEndpoint(ep docker.Endpoint, configFile *configfile.ConfigFile) (client.APIClient, error) {
|
||||||
opts, err := ep.ClientOpts()
|
clientOpts, err := ep.ClientOpts()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if len(configFile.HTTPHeaders) > 0 {
|
customHeaders := make(map[string]string, len(configFile.HTTPHeaders))
|
||||||
opts = append(opts, client.WithHTTPHeaders(configFile.HTTPHeaders))
|
for k, v := range configFile.HTTPHeaders {
|
||||||
|
customHeaders[k] = v
|
||||||
}
|
}
|
||||||
withCustomHeaders, err := withCustomHeadersFromEnv()
|
customHeaders["User-Agent"] = UserAgent()
|
||||||
if err != nil {
|
clientOpts = append(clientOpts, client.WithHTTPHeaders(customHeaders))
|
||||||
return nil, err
|
return client.NewClientWithOpts(clientOpts...)
|
||||||
}
|
|
||||||
if withCustomHeaders != nil {
|
|
||||||
opts = append(opts, withCustomHeaders)
|
|
||||||
}
|
|
||||||
opts = append(opts, extraOpts...)
|
|
||||||
apiClient, err := client.New(opts...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return apiClient, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func resolveDockerEndpoint(s store.Reader, contextName string) (docker.Endpoint, error) {
|
func resolveDockerEndpoint(s store.Reader, contextName string) (docker.Endpoint, error) {
|
||||||
@@ -340,10 +293,7 @@ func resolveDockerEndpoint(s store.Reader, contextName string) (docker.Endpoint,
|
|||||||
|
|
||||||
// Resolve the Docker endpoint for the default context (based on config, env vars and CLI flags)
|
// Resolve the Docker endpoint for the default context (based on config, env vars and CLI flags)
|
||||||
func resolveDefaultDockerEndpoint(opts *cliflags.ClientOptions) (docker.Endpoint, error) {
|
func resolveDefaultDockerEndpoint(opts *cliflags.ClientOptions) (docker.Endpoint, error) {
|
||||||
// defaultToTLS determines whether we should use a TLS host as default
|
host, err := getServerHost(opts.Hosts, opts.TLSOptions)
|
||||||
// if nothing was configured by the user.
|
|
||||||
defaultToTLS := opts.TLSOptions != nil
|
|
||||||
host, err := getServerHost(opts.Hosts, defaultToTLS)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return docker.Endpoint{}, err
|
return docker.Endpoint{}, err
|
||||||
}
|
}
|
||||||
@@ -378,24 +328,38 @@ func (cli *DockerCli) getInitTimeout() time.Duration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (cli *DockerCli) initializeFromClient() {
|
func (cli *DockerCli) initializeFromClient() {
|
||||||
ctx, cancel := context.WithTimeout(cli.baseCtx, cli.getInitTimeout())
|
ctx := context.Background()
|
||||||
defer cancel()
|
if !strings.HasPrefix(cli.dockerEndpoint.Host, "ssh://") {
|
||||||
|
// @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, cli.getInitTimeout())
|
||||||
|
defer cancel()
|
||||||
|
}
|
||||||
|
|
||||||
ping, err := cli.client.Ping(ctx, client.PingOptions{
|
ping, err := cli.client.Ping(ctx)
|
||||||
NegotiateAPIVersion: true,
|
|
||||||
ForceNegotiate: true,
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Default to true if we fail to connect to daemon
|
// Default to true if we fail to connect to daemon
|
||||||
cli.serverInfo = ServerInfo{HasExperimental: true}
|
cli.serverInfo = ServerInfo{HasExperimental: true}
|
||||||
|
|
||||||
|
if ping.APIVersion != "" {
|
||||||
|
cli.client.NegotiateAPIVersionPing(ping)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
cli.serverInfo = ServerInfo{
|
cli.serverInfo = ServerInfo{
|
||||||
HasExperimental: ping.Experimental,
|
HasExperimental: ping.Experimental,
|
||||||
OSType: ping.OSType,
|
OSType: ping.OSType,
|
||||||
BuildkitVersion: ping.BuilderVersion,
|
BuildkitVersion: ping.BuilderVersion,
|
||||||
SwarmStatus: ping.SwarmStatus,
|
SwarmStatus: ping.SwarmStatus,
|
||||||
}
|
}
|
||||||
|
cli.client.NegotiateAPIVersionPing(ping)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
// ContextStore returns the ContextStore
|
||||||
@@ -408,7 +372,7 @@ func (cli *DockerCli) ContextStore() store.Store {
|
|||||||
// order of preference:
|
// order of preference:
|
||||||
//
|
//
|
||||||
// 1. The "--context" command-line option.
|
// 1. The "--context" command-line option.
|
||||||
// 2. The "DOCKER_CONTEXT" environment variable ([EnvOverrideContext]).
|
// 2. The "DOCKER_CONTEXT" environment variable.
|
||||||
// 3. The current context as configured through the in "currentContext"
|
// 3. The current context as configured through the in "currentContext"
|
||||||
// field in the CLI configuration file ("~/.docker/config.json").
|
// field in the CLI configuration file ("~/.docker/config.json").
|
||||||
// 4. If no context is configured, use the "default" context.
|
// 4. If no context is configured, use the "default" context.
|
||||||
@@ -419,7 +383,7 @@ func (cli *DockerCli) ContextStore() store.Store {
|
|||||||
// the "default" context is used if:
|
// the "default" context is used if:
|
||||||
//
|
//
|
||||||
// - The "--host" option is set
|
// - The "--host" option is set
|
||||||
// - The "DOCKER_HOST" ([client.EnvOverrideHost]) environment variable is set
|
// - The "DOCKER_HOST" ([DefaultContextName]) environment variable is set
|
||||||
// to a non-empty value.
|
// to a non-empty value.
|
||||||
//
|
//
|
||||||
// In these cases, the default context is used, which uses the host as
|
// In these cases, the default context is used, which uses the host as
|
||||||
@@ -440,7 +404,7 @@ func (cli *DockerCli) CurrentContext() string {
|
|||||||
// occur when trying to use it.
|
// occur when trying to use it.
|
||||||
//
|
//
|
||||||
// Refer to [DockerCli.CurrentContext] above for further details.
|
// Refer to [DockerCli.CurrentContext] above for further details.
|
||||||
func resolveContextName(opts *cliflags.ClientOptions, cfg *configfile.ConfigFile) string {
|
func resolveContextName(opts *cliflags.ClientOptions, config *configfile.ConfigFile) string {
|
||||||
if opts != nil && opts.Context != "" {
|
if opts != nil && opts.Context != "" {
|
||||||
return opts.Context
|
return opts.Context
|
||||||
}
|
}
|
||||||
@@ -450,12 +414,12 @@ func resolveContextName(opts *cliflags.ClientOptions, cfg *configfile.ConfigFile
|
|||||||
if os.Getenv(client.EnvOverrideHost) != "" {
|
if os.Getenv(client.EnvOverrideHost) != "" {
|
||||||
return DefaultContextName
|
return DefaultContextName
|
||||||
}
|
}
|
||||||
if ctxName := os.Getenv(EnvOverrideContext); ctxName != "" {
|
if ctxName := os.Getenv("DOCKER_CONTEXT"); ctxName != "" {
|
||||||
return ctxName
|
return ctxName
|
||||||
}
|
}
|
||||||
if cfg != nil && cfg.CurrentContext != "" {
|
if config != nil && config.CurrentContext != "" {
|
||||||
// We don't validate if this context exists: errors may occur when trying to use it.
|
// We don't validate if this context exists: errors may occur when trying to use it.
|
||||||
return cfg.CurrentContext
|
return config.CurrentContext
|
||||||
}
|
}
|
||||||
return DefaultContextName
|
return DefaultContextName
|
||||||
}
|
}
|
||||||
@@ -465,7 +429,7 @@ func (cli *DockerCli) DockerEndpoint() docker.Endpoint {
|
|||||||
if err := cli.initialize(); err != nil {
|
if err := cli.initialize(); err != nil {
|
||||||
// Note that we're not terminating here, as this function may be used
|
// Note that we're not terminating here, as this function may be used
|
||||||
// in cases where we're able to continue.
|
// in cases where we're able to continue.
|
||||||
_, _ = fmt.Fprintln(cli.Err(), cli.initErr)
|
_, _ = fmt.Fprintf(cli.Err(), "%v\n", cli.initErr)
|
||||||
}
|
}
|
||||||
return cli.dockerEndpoint
|
return cli.dockerEndpoint
|
||||||
}
|
}
|
||||||
@@ -478,83 +442,39 @@ func (cli *DockerCli) getDockerEndPoint() (ep docker.Endpoint, err error) {
|
|||||||
return resolveDockerEndpoint(cli.contextStore, cn)
|
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 {
|
func (cli *DockerCli) initialize() error {
|
||||||
cli.init.Do(func() {
|
cli.init.Do(func() {
|
||||||
cli.dockerEndpoint, cli.initErr = cli.getDockerEndPoint()
|
cli.dockerEndpoint, cli.initErr = cli.getDockerEndPoint()
|
||||||
if cli.initErr != nil {
|
if cli.initErr != nil {
|
||||||
cli.initErr = fmt.Errorf("unable to resolve docker endpoint: %w", cli.initErr)
|
cli.initErr = errors.Wrap(cli.initErr, "unable to resolve docker endpoint")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if cli.client == nil {
|
if cli.client == nil {
|
||||||
if cli.client, cli.initErr = newAPIClientFromEndpoint(cli.dockerEndpoint, cli.configFile, cli.clientOpts...); cli.initErr != nil {
|
if cli.client, cli.initErr = newAPIClientFromEndpoint(cli.dockerEndpoint, cli.configFile); cli.initErr != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if cli.baseCtx == nil {
|
|
||||||
cli.baseCtx = context.Background()
|
|
||||||
}
|
|
||||||
cli.initializeFromClient()
|
cli.initializeFromClient()
|
||||||
})
|
})
|
||||||
return cli.initErr
|
return cli.initErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 nil
|
||||||
|
}
|
||||||
|
|
||||||
// ServerInfo stores details about the supported features and platform of the
|
// ServerInfo stores details about the supported features and platform of the
|
||||||
// server
|
// server
|
||||||
type ServerInfo struct {
|
type ServerInfo struct {
|
||||||
HasExperimental bool
|
HasExperimental bool
|
||||||
OSType string
|
OSType string
|
||||||
BuildkitVersion build.BuilderVersion
|
BuildkitVersion types.BuilderVersion
|
||||||
|
|
||||||
// SwarmStatus provides information about the current swarm status of the
|
// SwarmStatus provides information about the current swarm status of the
|
||||||
// engine, obtained from the "Swarm" header in the API response.
|
// engine, obtained from the "Swarm" header in the API response.
|
||||||
@@ -563,48 +483,48 @@ type ServerInfo struct {
|
|||||||
// in the ping response, or if an error occurred, in which case the client
|
// 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
|
// should use other ways to get the current swarm status, such as the /swarm
|
||||||
// endpoint.
|
// endpoint.
|
||||||
SwarmStatus *client.SwarmStatus
|
SwarmStatus *swarm.Status
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDockerCli returns a DockerCli instance with all operators applied on it.
|
// NewDockerCli returns a DockerCli instance with all operators applied on it.
|
||||||
// It applies by default the standard streams, and the content trust from
|
// It applies by default the standard streams, and the content trust from
|
||||||
// environment.
|
// environment.
|
||||||
func NewDockerCli(ops ...CLIOption) (*DockerCli, error) {
|
func NewDockerCli(ops ...DockerCliOption) (*DockerCli, error) {
|
||||||
defaultOps := make([]CLIOption, 0, 3+len(ops))
|
defaultOps := []DockerCliOption{
|
||||||
defaultOps = append(defaultOps,
|
WithContentTrustFromEnv(),
|
||||||
WithDefaultContextStoreConfig(),
|
WithDefaultContextStoreConfig(),
|
||||||
WithStandardStreams(),
|
WithStandardStreams(),
|
||||||
WithUserAgent(UserAgent()),
|
}
|
||||||
)
|
|
||||||
ops = append(defaultOps, ops...)
|
ops = append(defaultOps, ops...)
|
||||||
|
|
||||||
cli := &DockerCli{baseCtx: context.Background()}
|
cli := &DockerCli{}
|
||||||
for _, op := range ops {
|
if err := cli.Apply(ops...); err != nil {
|
||||||
if err := op(cli); err != nil {
|
return nil, err
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return cli, nil
|
return cli, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getServerHost(hosts []string, defaultToTLS bool) (string, error) {
|
func getServerHost(hosts []string, tlsOptions *tlsconfig.Options) (string, error) {
|
||||||
|
var host string
|
||||||
switch len(hosts) {
|
switch len(hosts) {
|
||||||
case 0:
|
case 0:
|
||||||
return dopts.ParseHost(defaultToTLS, os.Getenv(client.EnvOverrideHost))
|
host = os.Getenv(client.EnvOverrideHost)
|
||||||
case 1:
|
case 1:
|
||||||
return dopts.ParseHost(defaultToTLS, hosts[0])
|
host = hosts[0]
|
||||||
default:
|
default:
|
||||||
return "", errors.New("specify only one -H")
|
return "", errors.New("Please specify only one -H")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return dopts.ParseHost(tlsOptions != nil, host)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UserAgent returns the default user agent string used for making API requests.
|
// UserAgent returns the user agent string used for making API requests
|
||||||
func UserAgent() string {
|
func UserAgent() string {
|
||||||
return "Docker-Client/" + version.Version + " (" + runtime.GOOS + ")"
|
return "Docker-Client/" + version.Version + " (" + runtime.GOOS + ")"
|
||||||
}
|
}
|
||||||
|
|
||||||
var defaultStoreEndpoints = []store.NamedTypeGetter{
|
var defaultStoreEndpoints = []store.NamedTypeGetter{
|
||||||
store.EndpointTypeGetter(docker.DockerEndpoint, func() any { return &docker.EndpointMeta{} }),
|
store.EndpointTypeGetter(docker.DockerEndpoint, func() interface{} { return &docker.EndpointMeta{} }),
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegisterDefaultStoreEndpoints registers a new named endpoint
|
// RegisterDefaultStoreEndpoints registers a new named endpoint
|
||||||
@@ -618,7 +538,7 @@ func RegisterDefaultStoreEndpoints(ep ...store.NamedTypeGetter) {
|
|||||||
// DefaultContextStoreConfig returns a new store.Config with the default set of endpoints configured.
|
// DefaultContextStoreConfig returns a new store.Config with the default set of endpoints configured.
|
||||||
func DefaultContextStoreConfig() store.Config {
|
func DefaultContextStoreConfig() store.Config {
|
||||||
return store.NewConfig(
|
return store.NewConfig(
|
||||||
func() any { return &DockerContext{} },
|
func() interface{} { return &DockerContext{} },
|
||||||
defaultStoreEndpoints...,
|
defaultStoreEndpoints...,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+38
-178
@@ -1,58 +1,41 @@
|
|||||||
package command
|
package command
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"encoding/csv"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strconv"
|
||||||
|
|
||||||
"github.com/docker/cli/cli/streams"
|
"github.com/docker/cli/cli/streams"
|
||||||
"github.com/moby/moby/client"
|
"github.com/docker/docker/client"
|
||||||
"github.com/moby/term"
|
"github.com/moby/term"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CLIOption is a functional argument to apply options to a [DockerCli]. These
|
// DockerCliOption applies a modification on a DockerCli.
|
||||||
// options can be passed to [NewDockerCli] to initialize a new CLI, or
|
type DockerCliOption func(cli *DockerCli) error
|
||||||
// applied with [DockerCli.Initialize] or [DockerCli.Apply].
|
|
||||||
type CLIOption func(cli *DockerCli) error
|
|
||||||
|
|
||||||
// WithStandardStreams sets a cli in, out and err streams with the standard streams.
|
// WithStandardStreams sets a cli in, out and err streams with the standard streams.
|
||||||
func WithStandardStreams() CLIOption {
|
func WithStandardStreams() DockerCliOption {
|
||||||
return func(cli *DockerCli) error {
|
return func(cli *DockerCli) error {
|
||||||
// Set terminal emulation based on platform as required.
|
// Set terminal emulation based on platform as required.
|
||||||
stdin, stdout, stderr := term.StdStreams()
|
stdin, stdout, stderr := term.StdStreams()
|
||||||
cli.in = streams.NewIn(stdin)
|
cli.in = streams.NewIn(stdin)
|
||||||
cli.out = streams.NewOut(stdout)
|
cli.out = streams.NewOut(stdout)
|
||||||
cli.err = streams.NewOut(stderr)
|
cli.err = stderr
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithBaseContext sets the base context of a cli. It is used to propagate
|
|
||||||
// the context from the command line to the client.
|
|
||||||
func WithBaseContext(ctx context.Context) CLIOption {
|
|
||||||
return func(cli *DockerCli) error {
|
|
||||||
cli.baseCtx = ctx
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithCombinedStreams uses the same stream for the output and error streams.
|
// WithCombinedStreams uses the same stream for the output and error streams.
|
||||||
func WithCombinedStreams(combined io.Writer) CLIOption {
|
func WithCombinedStreams(combined io.Writer) DockerCliOption {
|
||||||
return func(cli *DockerCli) error {
|
return func(cli *DockerCli) error {
|
||||||
s := streams.NewOut(combined)
|
cli.out = streams.NewOut(combined)
|
||||||
cli.out = s
|
cli.err = combined
|
||||||
cli.err = s
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithInputStream sets a cli input stream.
|
// WithInputStream sets a cli input stream.
|
||||||
func WithInputStream(in io.ReadCloser) CLIOption {
|
func WithInputStream(in io.ReadCloser) DockerCliOption {
|
||||||
return func(cli *DockerCli) error {
|
return func(cli *DockerCli) error {
|
||||||
cli.in = streams.NewIn(in)
|
cli.in = streams.NewIn(in)
|
||||||
return nil
|
return nil
|
||||||
@@ -60,7 +43,7 @@ func WithInputStream(in io.ReadCloser) CLIOption {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// WithOutputStream sets a cli output stream.
|
// WithOutputStream sets a cli output stream.
|
||||||
func WithOutputStream(out io.Writer) CLIOption {
|
func WithOutputStream(out io.Writer) DockerCliOption {
|
||||||
return func(cli *DockerCli) error {
|
return func(cli *DockerCli) error {
|
||||||
cli.out = streams.NewOut(out)
|
cli.out = streams.NewOut(out)
|
||||||
return nil
|
return nil
|
||||||
@@ -68,170 +51,47 @@ func WithOutputStream(out io.Writer) CLIOption {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// WithErrorStream sets a cli error stream.
|
// WithErrorStream sets a cli error stream.
|
||||||
func WithErrorStream(err io.Writer) CLIOption {
|
func WithErrorStream(err io.Writer) DockerCliOption {
|
||||||
return func(cli *DockerCli) error {
|
return func(cli *DockerCli) error {
|
||||||
cli.err = streams.NewOut(err)
|
cli.err = err
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithContentTrustFromEnv enables content trust on a cli from environment variable DOCKER_CONTENT_TRUST value.
|
||||||
|
func WithContentTrustFromEnv() DockerCliOption {
|
||||||
|
return func(cli *DockerCli) error {
|
||||||
|
cli.contentTrust = false
|
||||||
|
if e := os.Getenv("DOCKER_CONTENT_TRUST"); e != "" {
|
||||||
|
if t, err := strconv.ParseBool(e); t || err != nil {
|
||||||
|
// treat any other value as true
|
||||||
|
cli.contentTrust = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithContentTrust enables content trust on a cli.
|
||||||
|
func WithContentTrust(enabled bool) DockerCliOption {
|
||||||
|
return func(cli *DockerCli) error {
|
||||||
|
cli.contentTrust = enabled
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithDefaultContextStoreConfig configures the cli to use the default context store configuration.
|
// WithDefaultContextStoreConfig configures the cli to use the default context store configuration.
|
||||||
func WithDefaultContextStoreConfig() CLIOption {
|
func WithDefaultContextStoreConfig() DockerCliOption {
|
||||||
return func(cli *DockerCli) error {
|
return func(cli *DockerCli) error {
|
||||||
cfg := DefaultContextStoreConfig()
|
cli.contextStoreConfig = DefaultContextStoreConfig()
|
||||||
cli.contextStoreConfig = &cfg
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithAPIClient configures the cli to use the given API client.
|
// WithAPIClient configures the cli to use the given API client.
|
||||||
func WithAPIClient(c client.APIClient) CLIOption {
|
func WithAPIClient(c client.APIClient) DockerCliOption {
|
||||||
return func(cli *DockerCli) error {
|
return func(cli *DockerCli) error {
|
||||||
cli.client = c
|
cli.client = c
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithInitializeClient is passed to [DockerCli.Initialize] to initialize
|
|
||||||
// an API Client for use by the CLI.
|
|
||||||
func WithInitializeClient(makeClient func(*DockerCli) (client.APIClient, error)) CLIOption {
|
|
||||||
return func(cli *DockerCli) error {
|
|
||||||
c, err := makeClient(cli)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return WithAPIClient(c)(cli)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithAPIClientOptions configures additional [client.Opt] to use when
|
|
||||||
// initializing the API client. These options have no effect if a custom
|
|
||||||
// client is set (through [WithAPIClient] or [WithInitializeClient]).
|
|
||||||
func WithAPIClientOptions(c ...client.Opt) CLIOption {
|
|
||||||
return func(cli *DockerCli) error {
|
|
||||||
cli.clientOpts = append(cli.clientOpts, c...)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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.clientOpts = append(cli.clientOpts, client.WithUserAgent(userAgent))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
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) {
|
||||||
|
const envvar = "DOCKER_CONTENT_TRUST"
|
||||||
|
t.Setenv(envvar, "true")
|
||||||
|
assert.Check(t, contentTrustEnabled(t))
|
||||||
|
t.Setenv(envvar, "false")
|
||||||
|
assert.Check(t, !contentTrustEnabled(t))
|
||||||
|
t.Setenv(envvar, "invalid")
|
||||||
|
assert.Check(t, contentTrustEnabled(t))
|
||||||
|
os.Unsetenv(envvar)
|
||||||
|
assert.Check(t, !contentTrustEnabled(t))
|
||||||
|
}
|
||||||
+112
-248
@@ -3,7 +3,6 @@ package command
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
@@ -18,10 +17,13 @@ import (
|
|||||||
|
|
||||||
"github.com/docker/cli/cli/config"
|
"github.com/docker/cli/cli/config"
|
||||||
"github.com/docker/cli/cli/config/configfile"
|
"github.com/docker/cli/cli/config/configfile"
|
||||||
"github.com/docker/cli/cli/context/store"
|
|
||||||
"github.com/docker/cli/cli/flags"
|
"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"
|
"gotest.tools/v3/assert"
|
||||||
|
"gotest.tools/v3/fs"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNewAPIClientFromFlags(t *testing.T) {
|
func TestNewAPIClientFromFlags(t *testing.T) {
|
||||||
@@ -33,7 +35,7 @@ func TestNewAPIClientFromFlags(t *testing.T) {
|
|||||||
apiClient, err := NewAPIClientFromFlags(opts, &configfile.ConfigFile{})
|
apiClient, err := NewAPIClientFromFlags(opts, &configfile.ConfigFile{})
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
assert.Equal(t, apiClient.DaemonHost(), host)
|
assert.Equal(t, apiClient.DaemonHost(), host)
|
||||||
assert.Equal(t, apiClient.ClientVersion(), client.MaxAPIVersion)
|
assert.Equal(t, apiClient.ClientVersion(), api.DefaultVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNewAPIClientFromFlagsForDefaultSchema(t *testing.T) {
|
func TestNewAPIClientFromFlagsForDefaultSchema(t *testing.T) {
|
||||||
@@ -46,7 +48,7 @@ func TestNewAPIClientFromFlagsForDefaultSchema(t *testing.T) {
|
|||||||
apiClient, err := NewAPIClientFromFlags(opts, &configfile.ConfigFile{})
|
apiClient, err := NewAPIClientFromFlags(opts, &configfile.ConfigFile{})
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
assert.Equal(t, apiClient.DaemonHost(), slug+host)
|
assert.Equal(t, apiClient.DaemonHost(), slug+host)
|
||||||
assert.Equal(t, apiClient.ClientVersion(), client.MaxAPIVersion)
|
assert.Equal(t, apiClient.ClientVersion(), api.DefaultVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNewAPIClientFromFlagsWithCustomHeaders(t *testing.T) {
|
func TestNewAPIClientFromFlagsWithCustomHeaders(t *testing.T) {
|
||||||
@@ -70,7 +72,7 @@ func TestNewAPIClientFromFlagsWithCustomHeaders(t *testing.T) {
|
|||||||
apiClient, err := NewAPIClientFromFlags(opts, configFile)
|
apiClient, err := NewAPIClientFromFlags(opts, configFile)
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
assert.Equal(t, apiClient.DaemonHost(), host)
|
assert.Equal(t, apiClient.DaemonHost(), host)
|
||||||
assert.Equal(t, apiClient.ClientVersion(), client.MaxAPIVersion)
|
assert.Equal(t, apiClient.ClientVersion(), api.DefaultVersion)
|
||||||
|
|
||||||
// verify User-Agent is not appended to the configfile. see https://github.com/docker/cli/pull/2756
|
// 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.DeepEqual(t, configFile.HTTPHeaders, map[string]string{"My-Header": "Custom-Value"})
|
||||||
@@ -79,49 +81,13 @@ func TestNewAPIClientFromFlagsWithCustomHeaders(t *testing.T) {
|
|||||||
"My-Header": "Custom-Value",
|
"My-Header": "Custom-Value",
|
||||||
"User-Agent": UserAgent(),
|
"User-Agent": UserAgent(),
|
||||||
}
|
}
|
||||||
_, err = apiClient.Ping(context.TODO(), client.PingOptions{})
|
_, err = apiClient.Ping(context.Background())
|
||||||
assert.NilError(t, err)
|
|
||||||
assert.DeepEqual(t, received, expectedHeaders)
|
|
||||||
}
|
|
||||||
|
|
||||||
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}}
|
|
||||||
configFile := &configfile.ConfigFile{
|
|
||||||
HTTPHeaders: map[string]string{
|
|
||||||
"My-Header": "Custom-Value from config-file",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// envOverrideHTTPHeaders should override the HTTPHeaders from the config-file,
|
|
||||||
// so "My-Header" should not be present.
|
|
||||||
t.Setenv(envOverrideHTTPHeaders, `one=one-value,"two=two,value",three=,four=four-value,four=four-value-override`)
|
|
||||||
apiClient, err := NewAPIClientFromFlags(opts, configFile)
|
|
||||||
assert.NilError(t, err)
|
|
||||||
assert.Equal(t, apiClient.DaemonHost(), host)
|
|
||||||
assert.Equal(t, apiClient.ClientVersion(), client.MaxAPIVersion)
|
|
||||||
|
|
||||||
expectedHeaders := http.Header{
|
|
||||||
"One": []string{"one-value"},
|
|
||||||
"Two": []string{"two,value"},
|
|
||||||
"Three": []string{""},
|
|
||||||
"Four": []string{"four-value-override"},
|
|
||||||
"User-Agent": []string{UserAgent()},
|
|
||||||
}
|
|
||||||
_, err = apiClient.Ping(context.TODO(), client.PingOptions{})
|
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
assert.DeepEqual(t, received, expectedHeaders)
|
assert.DeepEqual(t, received, expectedHeaders)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNewAPIClientFromFlagsWithAPIVersionFromEnv(t *testing.T) {
|
func TestNewAPIClientFromFlagsWithAPIVersionFromEnv(t *testing.T) {
|
||||||
const customVersion = "v3.3"
|
customVersion := "v3.3.3"
|
||||||
const expectedVersion = "3.3"
|
|
||||||
t.Setenv("DOCKER_API_VERSION", customVersion)
|
t.Setenv("DOCKER_API_VERSION", customVersion)
|
||||||
t.Setenv("DOCKER_HOST", ":2375")
|
t.Setenv("DOCKER_HOST", ":2375")
|
||||||
|
|
||||||
@@ -129,87 +95,75 @@ func TestNewAPIClientFromFlagsWithAPIVersionFromEnv(t *testing.T) {
|
|||||||
configFile := &configfile.ConfigFile{}
|
configFile := &configfile.ConfigFile{}
|
||||||
apiclient, err := NewAPIClientFromFlags(opts, configFile)
|
apiclient, err := NewAPIClientFromFlags(opts, configFile)
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
assert.Equal(t, apiclient.ClientVersion(), expectedVersion)
|
assert.Equal(t, apiclient.ClientVersion(), customVersion)
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewAPIClientFromFlagsWithInvalidAPIVersionFromEnv(t *testing.T) {
|
|
||||||
t.Setenv("DOCKER_API_VERSION", "1")
|
|
||||||
t.Setenv("DOCKER_HOST", ":2375")
|
|
||||||
|
|
||||||
apiClient, err := NewAPIClientFromFlags(&flags.ClientOptions{}, &configfile.ConfigFile{})
|
|
||||||
assert.ErrorContains(t, err, "invalid API version")
|
|
||||||
assert.Check(t, apiClient == nil)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type fakeClient struct {
|
type fakeClient struct {
|
||||||
client.Client
|
client.Client
|
||||||
pingFunc func() (client.PingResult, error)
|
pingFunc func() (types.Ping, error)
|
||||||
version string
|
version string
|
||||||
negotiated bool
|
negotiated bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *fakeClient) Ping(_ context.Context, options client.PingOptions) (client.PingResult, error) {
|
func (c *fakeClient) Ping(_ context.Context) (types.Ping, error) {
|
||||||
res, err := c.pingFunc()
|
return c.pingFunc()
|
||||||
if options.NegotiateAPIVersion {
|
|
||||||
if res.APIVersion != "" {
|
|
||||||
if c.negotiated || options.ForceNegotiate {
|
|
||||||
c.negotiated = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return res, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *fakeClient) ClientVersion() string {
|
func (c *fakeClient) ClientVersion() string {
|
||||||
return c.version
|
return c.version
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *fakeClient) NegotiateAPIVersionPing(types.Ping) {
|
||||||
|
c.negotiated = true
|
||||||
|
}
|
||||||
|
|
||||||
func TestInitializeFromClient(t *testing.T) {
|
func TestInitializeFromClient(t *testing.T) {
|
||||||
const defaultVersion = "v1.55"
|
const defaultVersion = "v1.55"
|
||||||
|
|
||||||
testcases := []struct {
|
testcases := []struct {
|
||||||
doc string
|
doc string
|
||||||
pingFunc func() (client.PingResult, error)
|
pingFunc func() (types.Ping, error)
|
||||||
expectedServer ServerInfo
|
expectedServer ServerInfo
|
||||||
negotiated bool
|
negotiated bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
doc: "successful ping",
|
doc: "successful ping",
|
||||||
pingFunc: func() (client.PingResult, error) {
|
pingFunc: func() (types.Ping, error) {
|
||||||
return client.PingResult{Experimental: true, OSType: "linux", APIVersion: "v1.44"}, nil
|
return types.Ping{Experimental: true, OSType: "linux", APIVersion: "v1.30"}, nil
|
||||||
},
|
},
|
||||||
expectedServer: ServerInfo{HasExperimental: true, OSType: "linux"},
|
expectedServer: ServerInfo{HasExperimental: true, OSType: "linux"},
|
||||||
negotiated: true,
|
negotiated: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
doc: "failed ping, no API version",
|
doc: "failed ping, no API version",
|
||||||
pingFunc: func() (client.PingResult, error) {
|
pingFunc: func() (types.Ping, error) {
|
||||||
return client.PingResult{}, errors.New("failed")
|
return types.Ping{}, errors.New("failed")
|
||||||
},
|
},
|
||||||
expectedServer: ServerInfo{HasExperimental: true},
|
expectedServer: ServerInfo{HasExperimental: true},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
doc: "failed ping, with API version",
|
doc: "failed ping, with API version",
|
||||||
pingFunc: func() (client.PingResult, error) {
|
pingFunc: func() (types.Ping, error) {
|
||||||
return client.PingResult{APIVersion: "v1.44"}, errors.New("failed")
|
return types.Ping{APIVersion: "v1.33"}, errors.New("failed")
|
||||||
},
|
},
|
||||||
expectedServer: ServerInfo{HasExperimental: true},
|
expectedServer: ServerInfo{HasExperimental: true},
|
||||||
negotiated: true,
|
negotiated: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range testcases {
|
for _, testcase := range testcases {
|
||||||
t.Run(tc.doc, func(t *testing.T) {
|
testcase := testcase
|
||||||
apiClient := &fakeClient{
|
t.Run(testcase.doc, func(t *testing.T) {
|
||||||
pingFunc: tc.pingFunc,
|
apiclient := &fakeClient{
|
||||||
|
pingFunc: testcase.pingFunc,
|
||||||
version: defaultVersion,
|
version: defaultVersion,
|
||||||
}
|
}
|
||||||
|
|
||||||
cli := &DockerCli{client: apiClient}
|
cli := &DockerCli{client: apiclient}
|
||||||
err := cli.Initialize(flags.NewClientOptions())
|
err := cli.Initialize(flags.NewClientOptions())
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
assert.DeepEqual(t, cli.ServerInfo(), tc.expectedServer)
|
assert.DeepEqual(t, cli.ServerInfo(), testcase.expectedServer)
|
||||||
assert.Equal(t, apiClient.negotiated, tc.negotiated)
|
assert.Equal(t, apiclient.negotiated, testcase.negotiated)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -217,100 +171,129 @@ func TestInitializeFromClient(t *testing.T) {
|
|||||||
// Makes sure we don't hang forever on the initial connection.
|
// Makes sure we don't hang forever on the initial connection.
|
||||||
// https://github.com/docker/cli/issues/3652
|
// https://github.com/docker/cli/issues/3652
|
||||||
func TestInitializeFromClientHangs(t *testing.T) {
|
func TestInitializeFromClientHangs(t *testing.T) {
|
||||||
const (
|
dir := t.TempDir()
|
||||||
// Sized against measured scheduler stalls:
|
socket := filepath.Join(dir, "my.sock")
|
||||||
// under CPU pressure this test sees 40-90ms stalls;
|
|
||||||
// this should give about 5x headroom.
|
|
||||||
// See https://github.com/docker/cli/issues/6003.
|
|
||||||
clientInitTimeout = 500 * time.Millisecond
|
|
||||||
|
|
||||||
// This is only a backstop against a genuine hang.
|
|
||||||
// It should never be reached on a healthy run.
|
|
||||||
// So, it should be fine to have a lenient timeout here.
|
|
||||||
waitTimeout = 10 * time.Second
|
|
||||||
)
|
|
||||||
|
|
||||||
tmpDir := t.TempDir()
|
|
||||||
socket := filepath.Join(tmpDir, "my.sock")
|
|
||||||
l, err := net.Listen("unix", socket)
|
l, err := net.Listen("unix", socket)
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
|
|
||||||
// Buffered, so the handler can record
|
receiveReqCh := make(chan bool)
|
||||||
// that it was reached without a reader
|
timeoutCtx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||||
// having to be ready at that instant.
|
defer cancel()
|
||||||
receivedReqCh := make(chan struct{}, 1)
|
|
||||||
releaseHandlerCh := make(chan struct{})
|
|
||||||
|
|
||||||
// Simulate a server that hangs on connections.
|
// Simulate a server that hangs on connections.
|
||||||
ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
select {
|
select {
|
||||||
case receivedReqCh <- struct{}{}:
|
case <-timeoutCtx.Done():
|
||||||
default:
|
case receiveReqCh <- true: // Blocks until someone receives on the channel.
|
||||||
}
|
}
|
||||||
<-releaseHandlerCh
|
_, _ = w.Write([]byte("OK"))
|
||||||
}))
|
}))
|
||||||
ts.Listener = l
|
ts.Listener = l
|
||||||
ts.Start()
|
ts.Start()
|
||||||
t.Cleanup(func() {
|
defer ts.Close()
|
||||||
close(releaseHandlerCh)
|
|
||||||
ts.Close()
|
|
||||||
})
|
|
||||||
|
|
||||||
opts := &flags.ClientOptions{Hosts: []string{"unix://" + socket}}
|
opts := &flags.ClientOptions{Hosts: []string{"unix://" + socket}}
|
||||||
configFile := &configfile.ConfigFile{}
|
configFile := &configfile.ConfigFile{}
|
||||||
apiClient, err := NewAPIClientFromFlags(opts, configFile)
|
apiClient, err := NewAPIClientFromFlags(opts, configFile)
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
|
|
||||||
initErrCh := make(chan error, 1)
|
initializedCh := make(chan bool)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
cli := &DockerCli{client: apiClient, initTimeout: clientInitTimeout}
|
cli := &DockerCli{client: apiClient, initTimeout: time.Millisecond}
|
||||||
err := cli.Initialize(flags.NewClientOptions())
|
err := cli.Initialize(flags.NewClientOptions())
|
||||||
|
assert.Check(t, err)
|
||||||
cli.CurrentVersion()
|
cli.CurrentVersion()
|
||||||
initErrCh <- err
|
close(initializedCh)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case err := <-initErrCh:
|
case <-timeoutCtx.Done():
|
||||||
assert.Check(t, err)
|
|
||||||
case <-time.After(waitTimeout):
|
|
||||||
t.Fatal("timeout waiting for initialization to complete")
|
t.Fatal("timeout waiting for initialization to complete")
|
||||||
|
case <-initializedCh:
|
||||||
}
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case <-receivedReqCh:
|
case <-timeoutCtx.Done():
|
||||||
case <-time.After(waitTimeout):
|
|
||||||
t.Fatal("server never received an init request")
|
t.Fatal("server never received an init request")
|
||||||
|
case <-receiveReqCh:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The CLI no longer disables/hides experimental CLI features, however, we need
|
||||||
|
// to verify that existing configuration files do not break
|
||||||
|
func TestExperimentalCLI(t *testing.T) {
|
||||||
|
defaultVersion := "v1.55"
|
||||||
|
|
||||||
|
testcases := []struct {
|
||||||
|
doc string
|
||||||
|
configfile string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
doc: "default",
|
||||||
|
configfile: `{}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
doc: "experimental",
|
||||||
|
configfile: `{
|
||||||
|
"experimental": "enabled"
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
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}
|
||||||
|
config.SetDir(dir.Path())
|
||||||
|
err := cli.Initialize(flags.NewClientOptions())
|
||||||
|
assert.NilError(t, err)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNewDockerCliAndOperators(t *testing.T) {
|
func TestNewDockerCliAndOperators(t *testing.T) {
|
||||||
outbuf := bytes.NewBuffer(nil)
|
// Test default operations and also overriding default ones
|
||||||
errbuf := bytes.NewBuffer(nil)
|
|
||||||
|
|
||||||
cli, err := NewDockerCli(
|
cli, err := NewDockerCli(
|
||||||
WithInputStream(io.NopCloser(strings.NewReader("some input"))),
|
WithContentTrust(true),
|
||||||
WithOutputStream(outbuf),
|
|
||||||
WithErrorStream(errbuf),
|
|
||||||
)
|
)
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
// Check streams are initialized
|
// Check streams are initialized
|
||||||
assert.Check(t, cli.In() != nil)
|
assert.Check(t, cli.In() != nil)
|
||||||
assert.Check(t, cli.Out() != nil)
|
assert.Check(t, cli.Out() != nil)
|
||||||
assert.Check(t, cli.Err() != nil)
|
assert.Check(t, cli.Err() != nil)
|
||||||
|
assert.Equal(t, cli.ContentTrustEnabled(), true)
|
||||||
|
|
||||||
|
// Apply can modify a dockerCli after construction
|
||||||
|
inbuf := bytes.NewBuffer([]byte("input"))
|
||||||
|
outbuf := bytes.NewBuffer(nil)
|
||||||
|
errbuf := bytes.NewBuffer(nil)
|
||||||
|
err = cli.Apply(
|
||||||
|
WithInputStream(io.NopCloser(inbuf)),
|
||||||
|
WithOutputStream(outbuf),
|
||||||
|
WithErrorStream(errbuf),
|
||||||
|
)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
// Check input stream
|
||||||
inputStream, err := io.ReadAll(cli.In())
|
inputStream, err := io.ReadAll(cli.In())
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
assert.Equal(t, string(inputStream), "some input")
|
assert.Equal(t, string(inputStream), "input")
|
||||||
|
|
||||||
// Check output stream
|
// Check output stream
|
||||||
_, err = fmt.Fprint(cli.Out(), "output")
|
fmt.Fprintf(cli.Out(), "output")
|
||||||
assert.NilError(t, err)
|
|
||||||
outputStream, err := io.ReadAll(outbuf)
|
outputStream, err := io.ReadAll(outbuf)
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
assert.Equal(t, string(outputStream), "output")
|
assert.Equal(t, string(outputStream), "output")
|
||||||
// Check error stream
|
// Check error stream
|
||||||
_, err = fmt.Fprint(cli.Err(), "error")
|
fmt.Fprintf(cli.Err(), "error")
|
||||||
assert.NilError(t, err)
|
|
||||||
errStream, err := io.ReadAll(errbuf)
|
errStream, err := io.ReadAll(errbuf)
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
assert.Equal(t, string(errStream), "error")
|
assert.Equal(t, string(errStream), "error")
|
||||||
@@ -319,127 +302,8 @@ func TestNewDockerCliAndOperators(t *testing.T) {
|
|||||||
func TestInitializeShouldAlwaysCreateTheContextStore(t *testing.T) {
|
func TestInitializeShouldAlwaysCreateTheContextStore(t *testing.T) {
|
||||||
cli, err := NewDockerCli()
|
cli, err := NewDockerCli()
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
apiClient, err := client.New()
|
assert.NilError(t, cli.Initialize(flags.NewClientOptions(), WithInitializeClient(func(cli *DockerCli) (client.APIClient, error) {
|
||||||
assert.NilError(t, err)
|
return client.NewClientWithOpts()
|
||||||
assert.NilError(t, cli.Initialize(flags.NewClientOptions(), WithAPIClient(apiClient)))
|
})))
|
||||||
assert.Check(t, cli.ContextStore() != nil)
|
assert.Check(t, cli.ContextStore() != nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHooksEnabled(t *testing.T) {
|
|
||||||
t.Run("disabled by default", func(t *testing.T) {
|
|
||||||
// Make sure we don't depend on any existing ~/.docker/config.json
|
|
||||||
config.SetDir(t.TempDir())
|
|
||||||
cli, err := NewDockerCli()
|
|
||||||
assert.NilError(t, err)
|
|
||||||
|
|
||||||
assert.Check(t, !cli.HooksEnabled())
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("enabled in configFile", func(t *testing.T) {
|
|
||||||
configFile := `{
|
|
||||||
"features": {
|
|
||||||
"hooks": "true"
|
|
||||||
}}`
|
|
||||||
config.SetDir(t.TempDir())
|
|
||||||
err := os.WriteFile(filepath.Join(config.Dir(), "config.json"), []byte(configFile), 0o600)
|
|
||||||
assert.NilError(t, err)
|
|
||||||
cli, err := NewDockerCli()
|
|
||||||
assert.NilError(t, err)
|
|
||||||
assert.Check(t, cli.HooksEnabled())
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("env var overrides configFile", func(t *testing.T) {
|
|
||||||
configFile := `{
|
|
||||||
"features": {
|
|
||||||
"hooks": "true"
|
|
||||||
}}`
|
|
||||||
t.Setenv("DOCKER_CLI_HOOKS", "false")
|
|
||||||
config.SetDir(t.TempDir())
|
|
||||||
err := os.WriteFile(filepath.Join(config.Dir(), "config.json"), []byte(configFile), 0o600)
|
|
||||||
assert.NilError(t, err)
|
|
||||||
cli, err := NewDockerCli()
|
|
||||||
assert.NilError(t, err)
|
|
||||||
assert.Check(t, !cli.HooksEnabled())
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("legacy env var overrides configFile", func(t *testing.T) {
|
|
||||||
configFile := `{
|
|
||||||
"features": {
|
|
||||||
"hooks": "true"
|
|
||||||
}}`
|
|
||||||
t.Setenv("DOCKER_CLI_HINTS", "false")
|
|
||||||
config.SetDir(t.TempDir())
|
|
||||||
err := os.WriteFile(filepath.Join(config.Dir(), "config.json"), []byte(configFile), 0o600)
|
|
||||||
assert.NilError(t, err)
|
|
||||||
cli, err := NewDockerCli()
|
|
||||||
assert.NilError(t, err)
|
|
||||||
assert.Check(t, !cli.HooksEnabled())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSetGoDebug(t *testing.T) {
|
|
||||||
t.Run("GODEBUG already set", func(t *testing.T) {
|
|
||||||
t.Setenv("GODEBUG", "val1,val2")
|
|
||||||
meta := store.Metadata{}
|
|
||||||
setGoDebug(meta)
|
|
||||||
assert.Equal(t, "val1,val2", os.Getenv("GODEBUG"))
|
|
||||||
})
|
|
||||||
t.Run("GODEBUG in context metadata can set env", func(t *testing.T) {
|
|
||||||
t.Setenv("GODEBUG", "")
|
|
||||||
meta := store.Metadata{
|
|
||||||
Metadata: DockerContext{
|
|
||||||
AdditionalFields: map[string]any{
|
|
||||||
"GODEBUG": "val1,val2=1",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
setGoDebug(meta)
|
|
||||||
assert.Equal(t, "val1,val2=1", os.Getenv("GODEBUG"))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewDockerCliWithCustomUserAgent(t *testing.T) {
|
|
||||||
var received string
|
|
||||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
received = r.UserAgent()
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
}))
|
|
||||||
defer ts.Close()
|
|
||||||
host := strings.Replace(ts.URL, "http://", "tcp://", 1)
|
|
||||||
opts := &flags.ClientOptions{Hosts: []string{host}}
|
|
||||||
|
|
||||||
cli, err := NewDockerCli(
|
|
||||||
WithUserAgent("fake-agent/0.0.1"),
|
|
||||||
)
|
|
||||||
assert.NilError(t, err)
|
|
||||||
cli.currentContext = DefaultContextName
|
|
||||||
cli.options = opts
|
|
||||||
cli.configFile = &configfile.ConfigFile{}
|
|
||||||
|
|
||||||
_, err = cli.Client().Ping(context.TODO(), client.PingOptions{})
|
|
||||||
assert.NilError(t, err)
|
|
||||||
assert.DeepEqual(t, received, "fake-agent/0.0.1")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewDockerCliWithAPIClientOptions(t *testing.T) {
|
|
||||||
var received string
|
|
||||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
received = r.UserAgent()
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
}))
|
|
||||||
defer ts.Close()
|
|
||||||
host := strings.Replace(ts.URL, "http://", "tcp://", 1)
|
|
||||||
opts := &flags.ClientOptions{Hosts: []string{host}}
|
|
||||||
|
|
||||||
cli, err := NewDockerCli(
|
|
||||||
WithAPIClientOptions(client.WithUserAgent("fake-agent/0.0.1")),
|
|
||||||
)
|
|
||||||
assert.NilError(t, err)
|
|
||||||
cli.currentContext = DefaultContextName
|
|
||||||
cli.options = opts
|
|
||||||
cli.configFile = &configfile.ConfigFile{}
|
|
||||||
|
|
||||||
_, err = cli.Client().Ping(t.Context(), client.PingOptions{})
|
|
||||||
assert.NilError(t, err)
|
|
||||||
assert.DeepEqual(t, received, "fake-agent/0.0.1")
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,30 +1,109 @@
|
|||||||
package commands
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
_ "github.com/docker/cli/cli/command/builder"
|
"github.com/docker/cli/cli/command/builder"
|
||||||
_ "github.com/docker/cli/cli/command/checkpoint"
|
"github.com/docker/cli/cli/command/checkpoint"
|
||||||
_ "github.com/docker/cli/cli/command/config"
|
"github.com/docker/cli/cli/command/config"
|
||||||
_ "github.com/docker/cli/cli/command/container"
|
"github.com/docker/cli/cli/command/container"
|
||||||
_ "github.com/docker/cli/cli/command/context"
|
"github.com/docker/cli/cli/command/context"
|
||||||
_ "github.com/docker/cli/cli/command/image"
|
"github.com/docker/cli/cli/command/image"
|
||||||
_ "github.com/docker/cli/cli/command/manifest"
|
"github.com/docker/cli/cli/command/manifest"
|
||||||
_ "github.com/docker/cli/cli/command/network"
|
"github.com/docker/cli/cli/command/network"
|
||||||
_ "github.com/docker/cli/cli/command/node"
|
"github.com/docker/cli/cli/command/node"
|
||||||
_ "github.com/docker/cli/cli/command/plugin"
|
"github.com/docker/cli/cli/command/plugin"
|
||||||
_ "github.com/docker/cli/cli/command/registry"
|
"github.com/docker/cli/cli/command/registry"
|
||||||
_ "github.com/docker/cli/cli/command/secret"
|
"github.com/docker/cli/cli/command/secret"
|
||||||
_ "github.com/docker/cli/cli/command/service"
|
"github.com/docker/cli/cli/command/service"
|
||||||
_ "github.com/docker/cli/cli/command/stack"
|
"github.com/docker/cli/cli/command/stack"
|
||||||
_ "github.com/docker/cli/cli/command/swarm"
|
"github.com/docker/cli/cli/command/swarm"
|
||||||
_ "github.com/docker/cli/cli/command/system"
|
"github.com/docker/cli/cli/command/system"
|
||||||
_ "github.com/docker/cli/cli/command/volume"
|
"github.com/docker/cli/cli/command/trust"
|
||||||
"github.com/docker/cli/internal/commands"
|
"github.com/docker/cli/cli/command/volume"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
func AddCommands(cmd *cobra.Command, dockerCLI command.Cli) {
|
// AddCommands adds all the commands from cli/command to the root command
|
||||||
for _, c := range commands.Commands() {
|
func AddCommands(cmd *cobra.Command, dockerCli command.Cli) {
|
||||||
cmd.AddCommand(c(dockerCLI))
|
cmd.AddCommand(
|
||||||
|
// commonly used shorthands
|
||||||
|
container.NewRunCommand(dockerCli),
|
||||||
|
container.NewExecCommand(dockerCli),
|
||||||
|
container.NewPsCommand(dockerCli),
|
||||||
|
image.NewBuildCommand(dockerCli),
|
||||||
|
image.NewPullCommand(dockerCli),
|
||||||
|
image.NewPushCommand(dockerCli),
|
||||||
|
image.NewImagesCommand(dockerCli),
|
||||||
|
registry.NewLoginCommand(dockerCli),
|
||||||
|
registry.NewLogoutCommand(dockerCli),
|
||||||
|
registry.NewSearchCommand(dockerCli),
|
||||||
|
system.NewVersionCommand(dockerCli),
|
||||||
|
system.NewInfoCommand(dockerCli),
|
||||||
|
|
||||||
|
// management commands
|
||||||
|
builder.NewBuilderCommand(dockerCli),
|
||||||
|
checkpoint.NewCheckpointCommand(dockerCli),
|
||||||
|
container.NewContainerCommand(dockerCli),
|
||||||
|
context.NewContextCommand(dockerCli),
|
||||||
|
image.NewImageCommand(dockerCli),
|
||||||
|
manifest.NewManifestCommand(dockerCli),
|
||||||
|
network.NewNetworkCommand(dockerCli),
|
||||||
|
plugin.NewPluginCommand(dockerCli),
|
||||||
|
system.NewSystemCommand(dockerCli),
|
||||||
|
trust.NewTrustCommand(dockerCli),
|
||||||
|
volume.NewVolumeCommand(dockerCli),
|
||||||
|
|
||||||
|
// orchestration (swarm) commands
|
||||||
|
config.NewConfigCommand(dockerCli),
|
||||||
|
node.NewNodeCommand(dockerCli),
|
||||||
|
secret.NewSecretCommand(dockerCli),
|
||||||
|
service.NewServiceCommand(dockerCli),
|
||||||
|
stack.NewStackCommand(dockerCli),
|
||||||
|
swarm.NewSwarmCommand(dockerCli),
|
||||||
|
|
||||||
|
// legacy commands may be hidden
|
||||||
|
hide(container.NewAttachCommand(dockerCli)),
|
||||||
|
hide(container.NewCommitCommand(dockerCli)),
|
||||||
|
hide(container.NewCopyCommand(dockerCli)),
|
||||||
|
hide(container.NewCreateCommand(dockerCli)),
|
||||||
|
hide(container.NewDiffCommand(dockerCli)),
|
||||||
|
hide(container.NewExportCommand(dockerCli)),
|
||||||
|
hide(container.NewKillCommand(dockerCli)),
|
||||||
|
hide(container.NewLogsCommand(dockerCli)),
|
||||||
|
hide(container.NewPauseCommand(dockerCli)),
|
||||||
|
hide(container.NewPortCommand(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.NewImportCommand(dockerCli)),
|
||||||
|
hide(image.NewLoadCommand(dockerCli)),
|
||||||
|
hide(image.NewRemoveCommand(dockerCli)),
|
||||||
|
hide(image.NewSaveCommand(dockerCli)),
|
||||||
|
hide(image.NewTagCommand(dockerCli)),
|
||||||
|
hide(system.NewEventsCommand(dockerCli)),
|
||||||
|
hide(system.NewInspectCommand(dockerCli)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func hide(cmd *cobra.Command) *cobra.Command {
|
||||||
|
// If the environment variable with name "DOCKER_HIDE_LEGACY_COMMANDS" is not empty,
|
||||||
|
// these legacy commands (such as `docker ps`, `docker exec`, etc)
|
||||||
|
// will not be shown in output console.
|
||||||
|
if os.Getenv("DOCKER_HIDE_LEGACY_COMMANDS") == "" {
|
||||||
|
return cmd
|
||||||
}
|
}
|
||||||
|
cmdCopy := *cmd
|
||||||
|
cmdCopy.Hidden = true
|
||||||
|
cmdCopy.Aliases = []string{}
|
||||||
|
return &cmdCopy
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,81 +2,38 @@ package completion
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/distribution/reference"
|
"github.com/docker/cli/cli/command"
|
||||||
"github.com/moby/moby/api/types/container"
|
"github.com/docker/cli/cli/command/formatter"
|
||||||
"github.com/moby/moby/client"
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/api/types/filters"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
// APIClientProvider provides a method to get a [client.APIClient], initializing
|
// ValidArgsFn a function to be used by cobra command as `ValidArgsFunction` to offer command line completion
|
||||||
// it if needed.
|
type ValidArgsFn func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective)
|
||||||
//
|
|
||||||
// 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
|
// ImageNames offers completion for images present within the local store
|
||||||
func ImageNames(dockerCLI APIClientProvider, limit int) cobra.CompletionFunc {
|
func ImageNames(dockerCli command.Cli) ValidArgsFn {
|
||||||
return Unique(func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
if limit > 0 && len(args) >= limit {
|
list, err := dockerCli.Client().ImageList(cmd.Context(), types.ImageListOptions{})
|
||||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
|
||||||
}
|
|
||||||
res, err := dockerCLI.Client().ImageList(cmd.Context(), client.ImageListOptions{})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, cobra.ShellCompDirectiveError
|
return nil, cobra.ShellCompDirectiveError
|
||||||
}
|
}
|
||||||
var names []string
|
var names []string
|
||||||
for _, img := range res.Items {
|
for _, image := range list {
|
||||||
names = append(names, img.RepoTags...)
|
names = append(names, image.RepoTags...)
|
||||||
}
|
}
|
||||||
return names, cobra.ShellCompDirectiveNoFileComp
|
return names, cobra.ShellCompDirectiveNoFileComp
|
||||||
})
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// ImageNamesWithBase offers completion for images present within the local store,
|
|
||||||
// including both full image names with tags and base image names (repository names only)
|
|
||||||
// when multiple tags exist for the same base name
|
|
||||||
func ImageNamesWithBase(dockerCLI APIClientProvider, limit int) cobra.CompletionFunc {
|
|
||||||
return Unique(func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
|
||||||
if limit > 0 && len(args) >= limit {
|
|
||||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
|
||||||
}
|
|
||||||
res, err := dockerCLI.Client().ImageList(cmd.Context(), client.ImageListOptions{})
|
|
||||||
if err != nil {
|
|
||||||
return nil, cobra.ShellCompDirectiveError
|
|
||||||
}
|
|
||||||
var names []string
|
|
||||||
baseNameCounts := make(map[string]int)
|
|
||||||
for _, img := range res.Items {
|
|
||||||
names = append(names, img.RepoTags...)
|
|
||||||
for _, tag := range img.RepoTags {
|
|
||||||
ref, err := reference.ParseNormalizedNamed(tag)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
baseNameCounts[reference.FamiliarName(ref)]++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for baseName, count := range baseNameCounts {
|
|
||||||
if count > 1 {
|
|
||||||
names = append(names, baseName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return names, cobra.ShellCompDirectiveNoSpace | cobra.ShellCompDirectiveNoFileComp
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ContainerNames offers completion for container names and IDs
|
// ContainerNames offers completion for container names and IDs
|
||||||
// By default, only names are returned.
|
// By default, only names are returned.
|
||||||
// Set DOCKER_COMPLETION_SHOW_CONTAINER_IDS=yes to also complete IDs.
|
// Set DOCKER_COMPLETION_SHOW_CONTAINER_IDS=yes to also complete IDs.
|
||||||
func ContainerNames(dockerCLI APIClientProvider, all bool, filters ...func(container.Summary) bool) cobra.CompletionFunc {
|
func ContainerNames(dockerCli command.Cli, all bool, filters ...func(types.Container) bool) ValidArgsFn {
|
||||||
return Unique(func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
res, err := dockerCLI.Client().ContainerList(cmd.Context(), client.ContainerListOptions{
|
list, err := dockerCli.Client().ContainerList(cmd.Context(), types.ContainerListOptions{
|
||||||
All: all,
|
All: all,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -86,10 +43,10 @@ func ContainerNames(dockerCLI APIClientProvider, all bool, filters ...func(conta
|
|||||||
showContainerIDs := os.Getenv("DOCKER_COMPLETION_SHOW_CONTAINER_IDS") == "yes"
|
showContainerIDs := os.Getenv("DOCKER_COMPLETION_SHOW_CONTAINER_IDS") == "yes"
|
||||||
|
|
||||||
var names []string
|
var names []string
|
||||||
for _, ctr := range res.Items {
|
for _, container := range list {
|
||||||
skip := false
|
skip := false
|
||||||
for _, fn := range filters {
|
for _, fn := range filters {
|
||||||
if fn != nil && !fn(ctr) {
|
if !fn(container) {
|
||||||
skip = true
|
skip = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -98,185 +55,45 @@ func ContainerNames(dockerCLI APIClientProvider, all bool, filters ...func(conta
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if showContainerIDs {
|
if showContainerIDs {
|
||||||
names = append(names, ctr.ID)
|
names = append(names, container.ID)
|
||||||
}
|
|
||||||
for _, n := range ctr.Names {
|
|
||||||
// Skip legacy link names: "/linked-container/link-name"
|
|
||||||
if len(n) <= 1 || strings.IndexByte(n[1:], '/') != -1 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
names = append(names, strings.TrimPrefix(n, "/"))
|
|
||||||
}
|
}
|
||||||
|
names = append(names, formatter.StripNamePrefix(container.Names)...)
|
||||||
}
|
}
|
||||||
return names, cobra.ShellCompDirectiveNoFileComp
|
return names, cobra.ShellCompDirectiveNoFileComp
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// VolumeNames offers completion for volumes
|
// VolumeNames offers completion for volumes
|
||||||
func VolumeNames(dockerCLI APIClientProvider) cobra.CompletionFunc {
|
func VolumeNames(dockerCli command.Cli) ValidArgsFn {
|
||||||
return Unique(func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
res, err := dockerCLI.Client().VolumeList(cmd.Context(), client.VolumeListOptions{})
|
list, err := dockerCli.Client().VolumeList(cmd.Context(), filters.Args{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, cobra.ShellCompDirectiveError
|
return nil, cobra.ShellCompDirectiveError
|
||||||
}
|
}
|
||||||
var names []string
|
var names []string
|
||||||
for _, vol := range res.Items {
|
for _, volume := range list.Volumes {
|
||||||
names = append(names, vol.Name)
|
names = append(names, volume.Name)
|
||||||
}
|
}
|
||||||
return names, cobra.ShellCompDirectiveNoFileComp
|
return names, cobra.ShellCompDirectiveNoFileComp
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NetworkNames offers completion for networks
|
// NetworkNames offers completion for networks
|
||||||
func NetworkNames(dockerCLI APIClientProvider) cobra.CompletionFunc {
|
func NetworkNames(dockerCli command.Cli) ValidArgsFn {
|
||||||
return Unique(func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
res, err := dockerCLI.Client().NetworkList(cmd.Context(), client.NetworkListOptions{})
|
list, err := dockerCli.Client().NetworkList(cmd.Context(), types.NetworkListOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, cobra.ShellCompDirectiveError
|
return nil, cobra.ShellCompDirectiveError
|
||||||
}
|
}
|
||||||
var names []string
|
var names []string
|
||||||
for _, nw := range res.Items {
|
for _, network := range list {
|
||||||
names = append(names, nw.Name)
|
names = append(names, network.Name)
|
||||||
}
|
}
|
||||||
return names, cobra.ShellCompDirectiveNoFileComp
|
return names, cobra.ShellCompDirectiveNoFileComp
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// EnvVarNames offers completion for environment-variable names. This
|
|
||||||
// completion can be used for "--env" and "--build-arg" flags, which
|
|
||||||
// allow obtaining the value of the given environment-variable if present
|
|
||||||
// in the local environment, so we only should complete the names of the
|
|
||||||
// environment variables, and not their value. This also prevents the
|
|
||||||
// completion script from printing values of environment variables
|
|
||||||
// containing sensitive values.
|
|
||||||
//
|
|
||||||
// For example;
|
|
||||||
//
|
|
||||||
// export MY_VAR=hello
|
|
||||||
// docker run --rm --env MY_VAR alpine printenv MY_VAR
|
|
||||||
// hello
|
|
||||||
func EnvVarNames() cobra.CompletionFunc {
|
|
||||||
return Unique(func(_ *cobra.Command, _ []string, _ string) (names []string, _ cobra.ShellCompDirective) {
|
|
||||||
envs := os.Environ()
|
|
||||||
names = make([]string, 0, len(envs))
|
|
||||||
for _, env := range envs {
|
|
||||||
name, _, _ := strings.Cut(env, "=")
|
|
||||||
names = append(names, name)
|
|
||||||
}
|
|
||||||
return names, cobra.ShellCompDirectiveNoFileComp
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// FromList offers completion for the given list of options.
|
|
||||||
func FromList(options ...string) cobra.CompletionFunc {
|
|
||||||
return Unique(cobra.FixedCompletions(options, cobra.ShellCompDirectiveNoFileComp))
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithPrefix prefixes every element in the slice with the given prefix.
|
|
||||||
// It is a helper for building "--filter" completions, where each candidate
|
|
||||||
// value is offered as "key=value".
|
|
||||||
func WithPrefix(prefix string, values []string) []string {
|
|
||||||
result := make([]string, len(values))
|
|
||||||
for i, v := range values {
|
|
||||||
result[i] = prefix + v
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithSuffix appends the given suffix to every element in the slice. It is a
|
|
||||||
// helper for building "--filter" completions, where filter keys are offered
|
|
||||||
// with a trailing "=" (combined with [cobra.ShellCompDirectiveNoSpace]).
|
|
||||||
func WithSuffix(suffix string, values []string) []string {
|
|
||||||
result := make([]string, len(values))
|
|
||||||
for i, v := range values {
|
|
||||||
result[i] = v + suffix
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// FileNames is a convenience function to use [cobra.ShellCompDirectiveDefault],
|
|
||||||
// which indicates to let the shell perform its default behavior after
|
|
||||||
// completions have been provided.
|
|
||||||
func FileNames() cobra.CompletionFunc {
|
|
||||||
return func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
|
|
||||||
return nil, cobra.ShellCompDirectiveDefault
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var commonPlatforms = []string{
|
// NoComplete is used for commands where there's no relevant completion
|
||||||
"linux/386",
|
func NoComplete(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
|
||||||
"linux/amd64",
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
"linux/arm",
|
|
||||||
"linux/arm/v5",
|
|
||||||
"linux/arm/v6",
|
|
||||||
"linux/arm/v7",
|
|
||||||
"linux/arm64",
|
|
||||||
"linux/arm64/v8",
|
|
||||||
|
|
||||||
// IBM power and z platforms
|
|
||||||
"linux/ppc64le",
|
|
||||||
"linux/s390x",
|
|
||||||
|
|
||||||
// Not yet supported
|
|
||||||
"linux/riscv64",
|
|
||||||
|
|
||||||
"windows/amd64",
|
|
||||||
|
|
||||||
"wasip1/wasm",
|
|
||||||
}
|
|
||||||
|
|
||||||
// Platforms offers completion for platform-strings. It provides a non-exhaustive
|
|
||||||
// list of platforms to be used for completion. Platform-strings are based on
|
|
||||||
// [runtime.GOOS] and [runtime.GOARCH], but with (optional) variants added. A
|
|
||||||
// list of recognised os/arch combinations from the Go runtime can be obtained
|
|
||||||
// through "go tool dist list".
|
|
||||||
//
|
|
||||||
// Some noteworthy exclusions from this list:
|
|
||||||
//
|
|
||||||
// - arm64 images ("windows/arm64", "windows/arm64/v8") do not yet exist for windows.
|
|
||||||
// - we don't (yet) include `os-variant` for completion (as can be used for Windows images)
|
|
||||||
// - we don't (yet) include platforms for which we don't build binaries, such as
|
|
||||||
// BSD platforms (freebsd, netbsd, openbsd), android, macOS (darwin).
|
|
||||||
// - we currently exclude architectures that may have unofficial builds,
|
|
||||||
// but don't have wide adoption (and no support), such as loong64, mipsXXX,
|
|
||||||
// ppc64 (non-le) to prevent confusion.
|
|
||||||
func Platforms() cobra.CompletionFunc {
|
|
||||||
return func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
|
|
||||||
return commonPlatforms, cobra.ShellCompDirectiveNoFileComp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unique wraps a completion func and removes completion results that are
|
|
||||||
// already consumed (i.e., appear in "args").
|
|
||||||
//
|
|
||||||
// For example:
|
|
||||||
//
|
|
||||||
// # initial completion: args is empty, so all results are shown
|
|
||||||
// command <tab>
|
|
||||||
// one two three
|
|
||||||
//
|
|
||||||
// # "one" is already used so omitted
|
|
||||||
// command one <tab>
|
|
||||||
// two three
|
|
||||||
func Unique(fn cobra.CompletionFunc) cobra.CompletionFunc {
|
|
||||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
|
||||||
all, dir := fn(cmd, args, toComplete)
|
|
||||||
if len(all) == 0 || len(args) == 0 {
|
|
||||||
return all, dir
|
|
||||||
}
|
|
||||||
|
|
||||||
alreadyCompleted := make(map[string]struct{}, len(args))
|
|
||||||
for _, a := range args {
|
|
||||||
alreadyCompleted[a] = struct{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
out := make([]string, 0, len(all))
|
|
||||||
for _, c := range all {
|
|
||||||
if _, ok := alreadyCompleted[c]; !ok {
|
|
||||||
out = append(out, c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return out, dir
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,386 +0,0 @@
|
|||||||
package completion
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp/cmpopts"
|
|
||||||
"github.com/moby/moby/api/types/container"
|
|
||||||
"github.com/moby/moby/api/types/image"
|
|
||||||
"github.com/moby/moby/api/types/network"
|
|
||||||
"github.com/moby/moby/api/types/volume"
|
|
||||||
"github.com/moby/moby/client"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"gotest.tools/v3/assert"
|
|
||||||
is "gotest.tools/v3/assert/cmp"
|
|
||||||
"gotest.tools/v3/env"
|
|
||||||
)
|
|
||||||
|
|
||||||
type fakeCLI struct {
|
|
||||||
*fakeClient
|
|
||||||
}
|
|
||||||
|
|
||||||
// Client implements [APIClientProvider].
|
|
||||||
func (c fakeCLI) Client() client.APIClient {
|
|
||||||
return c.fakeClient
|
|
||||||
}
|
|
||||||
|
|
||||||
type fakeClient struct {
|
|
||||||
client.Client
|
|
||||||
containerListFunc func(context.Context, client.ContainerListOptions) (client.ContainerListResult, error)
|
|
||||||
imageListFunc func(context.Context, client.ImageListOptions) (client.ImageListResult, error)
|
|
||||||
networkListFunc func(context.Context, client.NetworkListOptions) (client.NetworkListResult, error)
|
|
||||||
volumeListFunc func(context.Context, client.VolumeListOptions) (client.VolumeListResult, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *fakeClient) ContainerList(ctx context.Context, options client.ContainerListOptions) (client.ContainerListResult, error) {
|
|
||||||
if c.containerListFunc != nil {
|
|
||||||
return c.containerListFunc(ctx, options)
|
|
||||||
}
|
|
||||||
return client.ContainerListResult{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *fakeClient) ImageList(ctx context.Context, options client.ImageListOptions) (client.ImageListResult, error) {
|
|
||||||
if c.imageListFunc != nil {
|
|
||||||
return c.imageListFunc(ctx, options)
|
|
||||||
}
|
|
||||||
return client.ImageListResult{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *fakeClient) NetworkList(ctx context.Context, options client.NetworkListOptions) (client.NetworkListResult, error) {
|
|
||||||
if c.networkListFunc != nil {
|
|
||||||
return c.networkListFunc(ctx, options)
|
|
||||||
}
|
|
||||||
return client.NetworkListResult{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *fakeClient) VolumeList(ctx context.Context, options client.VolumeListOptions) (client.VolumeListResult, error) {
|
|
||||||
if c.volumeListFunc != nil {
|
|
||||||
return c.volumeListFunc(ctx, options)
|
|
||||||
}
|
|
||||||
return client.VolumeListResult{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCompleteContainerNames(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
doc string
|
|
||||||
showAll, showIDs bool
|
|
||||||
filters []func(container.Summary) bool
|
|
||||||
containers []container.Summary
|
|
||||||
expOut []string
|
|
||||||
expOpts client.ContainerListOptions
|
|
||||||
expDirective cobra.ShellCompDirective
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
doc: "no results",
|
|
||||||
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
doc: "all containers",
|
|
||||||
showAll: true,
|
|
||||||
containers: []container.Summary{
|
|
||||||
{ID: "id-c", State: container.StateRunning, Names: []string{"/container-c", "/container-c/link-b"}},
|
|
||||||
{ID: "id-b", State: container.StateCreated, Names: []string{"/container-b"}},
|
|
||||||
{ID: "id-a", State: container.StateExited, Names: []string{"/container-a"}},
|
|
||||||
},
|
|
||||||
expOut: []string{"container-c", "container-b", "container-a"},
|
|
||||||
expOpts: client.ContainerListOptions{All: true},
|
|
||||||
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
doc: "all containers with ids",
|
|
||||||
showAll: true,
|
|
||||||
showIDs: true,
|
|
||||||
containers: []container.Summary{
|
|
||||||
{ID: "id-c", State: container.StateRunning, Names: []string{"/container-c", "/container-c/link-b"}},
|
|
||||||
{ID: "id-b", State: container.StateCreated, Names: []string{"/container-b"}},
|
|
||||||
{ID: "id-a", State: container.StateExited, Names: []string{"/container-a"}},
|
|
||||||
},
|
|
||||||
expOut: []string{"id-c", "container-c", "id-b", "container-b", "id-a", "container-a"},
|
|
||||||
expOpts: client.ContainerListOptions{All: true},
|
|
||||||
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
doc: "only running containers",
|
|
||||||
showAll: false,
|
|
||||||
containers: []container.Summary{
|
|
||||||
{ID: "id-c", State: container.StateRunning, Names: []string{"/container-c", "/container-c/link-b"}},
|
|
||||||
},
|
|
||||||
expOut: []string{"container-c"},
|
|
||||||
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
doc: "with filter",
|
|
||||||
showAll: true,
|
|
||||||
filters: []func(container.Summary) bool{
|
|
||||||
func(ctr container.Summary) bool { return ctr.State == container.StateCreated },
|
|
||||||
},
|
|
||||||
containers: []container.Summary{
|
|
||||||
{ID: "id-c", State: container.StateRunning, Names: []string{"/container-c"}},
|
|
||||||
{ID: "id-b", State: container.StateCreated, Names: []string{"/container-b"}},
|
|
||||||
{ID: "id-a", State: container.StateExited, Names: []string{"/container-a"}},
|
|
||||||
},
|
|
||||||
expOut: []string{"container-b"},
|
|
||||||
expOpts: client.ContainerListOptions{All: true},
|
|
||||||
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
doc: "multiple filters",
|
|
||||||
showAll: true,
|
|
||||||
filters: []func(container.Summary) bool{
|
|
||||||
func(ctr container.Summary) bool { return ctr.ID == "id-a" },
|
|
||||||
func(ctr container.Summary) bool { return ctr.State == container.StateCreated },
|
|
||||||
},
|
|
||||||
containers: []container.Summary{
|
|
||||||
{ID: "id-c", State: container.StateRunning, Names: []string{"/container-c"}},
|
|
||||||
{ID: "id-b", State: container.StateCreated, Names: []string{"/container-b"}},
|
|
||||||
{ID: "id-a", State: container.StateCreated, Names: []string{"/container-a"}},
|
|
||||||
},
|
|
||||||
expOut: []string{"container-a"},
|
|
||||||
expOpts: client.ContainerListOptions{All: true},
|
|
||||||
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
doc: "with error",
|
|
||||||
expDirective: cobra.ShellCompDirectiveError,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range tests {
|
|
||||||
t.Run(tc.doc, func(t *testing.T) {
|
|
||||||
if tc.showIDs {
|
|
||||||
t.Setenv("DOCKER_COMPLETION_SHOW_CONTAINER_IDS", "yes")
|
|
||||||
}
|
|
||||||
comp := ContainerNames(fakeCLI{&fakeClient{
|
|
||||||
containerListFunc: func(_ context.Context, opts client.ContainerListOptions) (client.ContainerListResult, error) {
|
|
||||||
assert.Check(t, is.DeepEqual(opts, tc.expOpts))
|
|
||||||
if tc.expDirective == cobra.ShellCompDirectiveError {
|
|
||||||
return client.ContainerListResult{}, errors.New("some error occurred")
|
|
||||||
}
|
|
||||||
return client.ContainerListResult{Items: tc.containers}, nil
|
|
||||||
},
|
|
||||||
}}, tc.showAll, tc.filters...)
|
|
||||||
|
|
||||||
containers, directives := comp(&cobra.Command{}, nil, "")
|
|
||||||
assert.Check(t, is.Equal(directives&tc.expDirective, tc.expDirective))
|
|
||||||
assert.Check(t, is.DeepEqual(containers, tc.expOut))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCompleteEnvVarNames(t *testing.T) {
|
|
||||||
env.PatchAll(t, map[string]string{
|
|
||||||
"ENV_A": "hello-a",
|
|
||||||
"ENV_B": "hello-b",
|
|
||||||
})
|
|
||||||
values, directives := EnvVarNames()(nil, nil, "")
|
|
||||||
assert.Check(t, is.Equal(directives&cobra.ShellCompDirectiveNoFileComp, cobra.ShellCompDirectiveNoFileComp), "Should not perform file completion")
|
|
||||||
|
|
||||||
expected := []string{"ENV_A", "ENV_B"}
|
|
||||||
assert.Check(t, is.DeepEqual(values, expected, cmpopts.SortSlices(func(a, b string) bool {
|
|
||||||
return a < b
|
|
||||||
})))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCompleteFileNames(t *testing.T) {
|
|
||||||
values, directives := FileNames()(nil, nil, "")
|
|
||||||
assert.Check(t, is.Equal(directives, cobra.ShellCompDirectiveDefault))
|
|
||||||
assert.Check(t, is.Len(values, 0))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCompleteFromList(t *testing.T) {
|
|
||||||
expected := []string{"one", "two", "three"}
|
|
||||||
|
|
||||||
values, directives := FromList(expected...)(nil, nil, "")
|
|
||||||
assert.Check(t, is.Equal(directives&cobra.ShellCompDirectiveNoFileComp, cobra.ShellCompDirectiveNoFileComp), "Should not perform file completion")
|
|
||||||
assert.Check(t, is.DeepEqual(values, expected))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWithPrefix(t *testing.T) {
|
|
||||||
assert.Check(t, is.DeepEqual(WithPrefix("node=", []string{"n1", "n2"}), []string{"node=n1", "node=n2"}))
|
|
||||||
assert.Check(t, is.DeepEqual(WithPrefix("node=", []string{}), []string{}))
|
|
||||||
assert.Check(t, is.DeepEqual(WithPrefix("", []string{"n1"}), []string{"n1"}))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWithSuffix(t *testing.T) {
|
|
||||||
assert.Check(t, is.DeepEqual(WithSuffix("=", []string{"id", "name"}), []string{"id=", "name="}))
|
|
||||||
assert.Check(t, is.DeepEqual(WithSuffix("=", []string{}), []string{}))
|
|
||||||
assert.Check(t, is.DeepEqual(WithSuffix("", []string{"id"}), []string{"id"}))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCompleteImageNames(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
doc string
|
|
||||||
images []image.Summary
|
|
||||||
expOut []string
|
|
||||||
expDirective cobra.ShellCompDirective
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
doc: "no results",
|
|
||||||
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
doc: "with results",
|
|
||||||
images: []image.Summary{
|
|
||||||
{RepoTags: []string{"image-c:latest", "image-c:other"}},
|
|
||||||
{RepoTags: []string{"image-b:latest", "image-b:other"}},
|
|
||||||
{RepoTags: []string{"image-a:latest", "image-a:other"}},
|
|
||||||
},
|
|
||||||
expOut: []string{"image-c:latest", "image-c:other", "image-b:latest", "image-b:other", "image-a:latest", "image-a:other"},
|
|
||||||
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
doc: "with error",
|
|
||||||
expDirective: cobra.ShellCompDirectiveError,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range tests {
|
|
||||||
t.Run(tc.doc, func(t *testing.T) {
|
|
||||||
comp := ImageNames(fakeCLI{&fakeClient{
|
|
||||||
imageListFunc: func(context.Context, client.ImageListOptions) (client.ImageListResult, error) {
|
|
||||||
if tc.expDirective == cobra.ShellCompDirectiveError {
|
|
||||||
return client.ImageListResult{}, errors.New("some error occurred")
|
|
||||||
}
|
|
||||||
return client.ImageListResult{Items: tc.images}, nil
|
|
||||||
},
|
|
||||||
}}, -1)
|
|
||||||
|
|
||||||
volumes, directives := comp(&cobra.Command{}, nil, "")
|
|
||||||
assert.Check(t, is.Equal(directives&tc.expDirective, tc.expDirective))
|
|
||||||
assert.Check(t, is.DeepEqual(volumes, tc.expOut))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCompleteNetworkNames(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
doc string
|
|
||||||
networks []network.Summary
|
|
||||||
expOut []string
|
|
||||||
expDirective cobra.ShellCompDirective
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
doc: "no results",
|
|
||||||
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
doc: "with results",
|
|
||||||
networks: []network.Summary{
|
|
||||||
{
|
|
||||||
Network: network.Network{
|
|
||||||
ID: "nw-c",
|
|
||||||
Name: "network-c",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Network: network.Network{
|
|
||||||
ID: "nw-b",
|
|
||||||
Name: "network-b",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Network: network.Network{
|
|
||||||
ID: "nw-a",
|
|
||||||
Name: "network-a",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expOut: []string{"network-c", "network-b", "network-a"},
|
|
||||||
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
doc: "with error",
|
|
||||||
expDirective: cobra.ShellCompDirectiveError,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range tests {
|
|
||||||
t.Run(tc.doc, func(t *testing.T) {
|
|
||||||
comp := NetworkNames(fakeCLI{&fakeClient{
|
|
||||||
networkListFunc: func(context.Context, client.NetworkListOptions) (client.NetworkListResult, error) {
|
|
||||||
if tc.expDirective == cobra.ShellCompDirectiveError {
|
|
||||||
return client.NetworkListResult{}, errors.New("some error occurred")
|
|
||||||
}
|
|
||||||
return client.NetworkListResult{Items: tc.networks}, nil
|
|
||||||
},
|
|
||||||
}})
|
|
||||||
|
|
||||||
volumes, directives := comp(&cobra.Command{}, nil, "")
|
|
||||||
assert.Check(t, is.Equal(directives&tc.expDirective, tc.expDirective))
|
|
||||||
assert.Check(t, is.DeepEqual(volumes, tc.expOut))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCompletePlatforms(t *testing.T) {
|
|
||||||
values, directives := Platforms()(nil, nil, "")
|
|
||||||
assert.Check(t, is.Equal(directives&cobra.ShellCompDirectiveNoFileComp, cobra.ShellCompDirectiveNoFileComp), "Should not perform file completion")
|
|
||||||
assert.Check(t, is.DeepEqual(values, commonPlatforms))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCompleteVolumeNames(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
doc string
|
|
||||||
volumes []volume.Volume
|
|
||||||
expOut []string
|
|
||||||
expDirective cobra.ShellCompDirective
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
doc: "no results",
|
|
||||||
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
doc: "with results",
|
|
||||||
volumes: []volume.Volume{
|
|
||||||
{Name: "volume-c"},
|
|
||||||
{Name: "volume-b"},
|
|
||||||
{Name: "volume-a"},
|
|
||||||
},
|
|
||||||
expOut: []string{"volume-c", "volume-b", "volume-a"},
|
|
||||||
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
doc: "with error",
|
|
||||||
expDirective: cobra.ShellCompDirectiveError,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range tests {
|
|
||||||
t.Run(tc.doc, func(t *testing.T) {
|
|
||||||
comp := VolumeNames(fakeCLI{&fakeClient{
|
|
||||||
volumeListFunc: func(context.Context, client.VolumeListOptions) (client.VolumeListResult, error) {
|
|
||||||
if tc.expDirective == cobra.ShellCompDirectiveError {
|
|
||||||
return client.VolumeListResult{}, errors.New("some error occurred")
|
|
||||||
}
|
|
||||||
return client.VolumeListResult{Items: tc.volumes}, nil
|
|
||||||
},
|
|
||||||
}})
|
|
||||||
|
|
||||||
volumes, directives := comp(&cobra.Command{}, nil, "")
|
|
||||||
assert.Check(t, is.Equal(directives&tc.expDirective, tc.expDirective))
|
|
||||||
assert.Check(t, is.DeepEqual(volumes, tc.expOut))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUnique(t *testing.T) {
|
|
||||||
base := []string{"alpha", "beta", "gamma"}
|
|
||||||
|
|
||||||
comp := Unique(func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
|
|
||||||
return base, cobra.ShellCompDirectiveNoFileComp
|
|
||||||
})
|
|
||||||
|
|
||||||
values, directives := comp(&cobra.Command{}, []string{"beta"}, "")
|
|
||||||
assert.Check(t, is.Equal(directives&cobra.ShellCompDirectiveNoFileComp, cobra.ShellCompDirectiveNoFileComp))
|
|
||||||
assert.Check(t, is.DeepEqual(values, []string{"alpha", "gamma"}))
|
|
||||||
|
|
||||||
assert.Check(t, is.DeepEqual(base, []string{"alpha", "beta", "gamma"}))
|
|
||||||
|
|
||||||
values, directives = comp(&cobra.Command{}, []string{"gamma"}, "")
|
|
||||||
assert.Check(t, is.Equal(directives&cobra.ShellCompDirectiveNoFileComp, cobra.ShellCompDirectiveNoFileComp))
|
|
||||||
assert.Check(t, is.DeepEqual(values, []string{"alpha", "beta"}))
|
|
||||||
|
|
||||||
assert.Check(t, is.DeepEqual(base, []string{"alpha", "beta", "gamma"}))
|
|
||||||
}
|
|
||||||
@@ -3,41 +3,43 @@ package config
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/moby/moby/client"
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/api/types/swarm"
|
||||||
|
"github.com/docker/docker/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
type fakeClient struct {
|
type fakeClient struct {
|
||||||
client.Client
|
client.Client
|
||||||
configCreateFunc func(context.Context, client.ConfigCreateOptions) (client.ConfigCreateResult, error)
|
configCreateFunc func(context.Context, swarm.ConfigSpec) (types.ConfigCreateResponse, error)
|
||||||
configInspectFunc func(context.Context, string, client.ConfigInspectOptions) (client.ConfigInspectResult, error)
|
configInspectFunc func(context.Context, string) (swarm.Config, []byte, error)
|
||||||
configListFunc func(context.Context, client.ConfigListOptions) (client.ConfigListResult, error)
|
configListFunc func(context.Context, types.ConfigListOptions) ([]swarm.Config, error)
|
||||||
configRemoveFunc func(context.Context, string, client.ConfigRemoveOptions) (client.ConfigRemoveResult, error)
|
configRemoveFunc func(string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *fakeClient) ConfigCreate(ctx context.Context, options client.ConfigCreateOptions) (client.ConfigCreateResult, error) {
|
func (c *fakeClient) ConfigCreate(ctx context.Context, spec swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
|
||||||
if c.configCreateFunc != nil {
|
if c.configCreateFunc != nil {
|
||||||
return c.configCreateFunc(ctx, options)
|
return c.configCreateFunc(ctx, spec)
|
||||||
}
|
}
|
||||||
return client.ConfigCreateResult{}, nil
|
return types.ConfigCreateResponse{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *fakeClient) ConfigInspect(ctx context.Context, id string, options client.ConfigInspectOptions) (client.ConfigInspectResult, error) {
|
func (c *fakeClient) ConfigInspectWithRaw(ctx context.Context, id string) (swarm.Config, []byte, error) {
|
||||||
if c.configInspectFunc != nil {
|
if c.configInspectFunc != nil {
|
||||||
return c.configInspectFunc(ctx, id, options)
|
return c.configInspectFunc(ctx, id)
|
||||||
}
|
}
|
||||||
return client.ConfigInspectResult{}, nil
|
return swarm.Config{}, nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *fakeClient) ConfigList(ctx context.Context, options client.ConfigListOptions) (client.ConfigListResult, error) {
|
func (c *fakeClient) ConfigList(ctx context.Context, options types.ConfigListOptions) ([]swarm.Config, error) {
|
||||||
if c.configListFunc != nil {
|
if c.configListFunc != nil {
|
||||||
return c.configListFunc(ctx, options)
|
return c.configListFunc(ctx, options)
|
||||||
}
|
}
|
||||||
return client.ConfigListResult{}, nil
|
return []swarm.Config{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *fakeClient) ConfigRemove(ctx context.Context, name string, options client.ConfigRemoveOptions) (client.ConfigRemoveResult, error) {
|
func (c *fakeClient) ConfigRemove(_ context.Context, name string) error {
|
||||||
if c.configRemoveFunc != nil {
|
if c.configRemoveFunc != nil {
|
||||||
return c.configRemoveFunc(ctx, name, options)
|
return c.configRemoveFunc(name)
|
||||||
}
|
}
|
||||||
return client.ConfigRemoveResult{}, nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-17
@@ -4,46 +4,40 @@ import (
|
|||||||
"github.com/docker/cli/cli"
|
"github.com/docker/cli/cli"
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
"github.com/docker/cli/cli/command/completion"
|
"github.com/docker/cli/cli/command/completion"
|
||||||
"github.com/docker/cli/internal/commands"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/moby/moby/client"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
// NewConfigCommand returns a cobra command for `config` subcommands
|
||||||
commands.Register(newConfigCommand)
|
func NewConfigCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
}
|
|
||||||
|
|
||||||
// newConfigCommand returns a cobra command for `config` subcommands
|
|
||||||
func newConfigCommand(dockerCLI command.Cli) *cobra.Command {
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "config",
|
Use: "config",
|
||||||
Short: "Manage Swarm configs",
|
Short: "Manage Swarm configs",
|
||||||
Args: cli.NoArgs,
|
Args: cli.NoArgs,
|
||||||
RunE: command.ShowHelp(dockerCLI.Err()),
|
RunE: command.ShowHelp(dockerCli.Err()),
|
||||||
Annotations: map[string]string{
|
Annotations: map[string]string{
|
||||||
"version": "1.30",
|
"version": "1.30",
|
||||||
"swarm": "manager",
|
"swarm": "manager",
|
||||||
},
|
},
|
||||||
DisableFlagsInUseLine: true,
|
|
||||||
}
|
}
|
||||||
cmd.AddCommand(
|
cmd.AddCommand(
|
||||||
newConfigListCommand(dockerCLI),
|
newConfigListCommand(dockerCli),
|
||||||
newConfigCreateCommand(dockerCLI),
|
newConfigCreateCommand(dockerCli),
|
||||||
newConfigInspectCommand(dockerCLI),
|
newConfigInspectCommand(dockerCli),
|
||||||
newConfigRemoveCommand(dockerCLI),
|
newConfigRemoveCommand(dockerCli),
|
||||||
)
|
)
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
// completeNames offers completion for swarm configs
|
// completeNames offers completion for swarm configs
|
||||||
func completeNames(dockerCLI completion.APIClientProvider) cobra.CompletionFunc {
|
func completeNames(dockerCli command.Cli) completion.ValidArgsFn {
|
||||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
res, err := dockerCLI.Client().ConfigList(cmd.Context(), client.ConfigListOptions{})
|
list, err := dockerCli.Client().ConfigList(cmd.Context(), types.ConfigListOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, cobra.ShellCompDirectiveError
|
return nil, cobra.ShellCompDirectiveError
|
||||||
}
|
}
|
||||||
var names []string
|
var names []string
|
||||||
for _, config := range res.Items {
|
for _, config := range list {
|
||||||
names = append(names, config.ID)
|
names = append(names, config.ID)
|
||||||
}
|
}
|
||||||
return names, cobra.ShellCompDirectiveNoFileComp
|
return names, cobra.ShellCompDirectiveNoFileComp
|
||||||
|
|||||||
@@ -2,30 +2,30 @@ package config
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
"github.com/docker/cli/cli"
|
"github.com/docker/cli/cli"
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
|
"github.com/docker/cli/cli/command/completion"
|
||||||
"github.com/docker/cli/opts"
|
"github.com/docker/cli/opts"
|
||||||
"github.com/moby/moby/api/types/swarm"
|
"github.com/docker/docker/api/types/swarm"
|
||||||
"github.com/moby/moby/client"
|
|
||||||
"github.com/moby/sys/sequential"
|
"github.com/moby/sys/sequential"
|
||||||
|
"github.com/pkg/errors"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
// createOptions specifies some options that are used when creating a config.
|
// CreateOptions specifies some options that are used when creating a config.
|
||||||
type createOptions struct {
|
type CreateOptions struct {
|
||||||
name string
|
Name string
|
||||||
templateDriver string
|
TemplateDriver string
|
||||||
file string
|
File string
|
||||||
labels opts.ListOpts
|
Labels opts.ListOpts
|
||||||
}
|
}
|
||||||
|
|
||||||
func newConfigCreateCommand(dockerCLI command.Cli) *cobra.Command {
|
func newConfigCreateCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
createOpts := createOptions{
|
createOpts := CreateOptions{
|
||||||
labels: opts.NewListOpts(opts.ValidateLabel),
|
Labels: opts.NewListOpts(opts.ValidateLabel),
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
@@ -33,113 +33,57 @@ func newConfigCreateCommand(dockerCLI command.Cli) *cobra.Command {
|
|||||||
Short: "Create a config from a file or STDIN",
|
Short: "Create a config from a file or STDIN",
|
||||||
Args: cli.ExactArgs(2),
|
Args: cli.ExactArgs(2),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
createOpts.name = args[0]
|
createOpts.Name = args[0]
|
||||||
createOpts.file = args[1]
|
createOpts.File = args[1]
|
||||||
return runCreate(cmd.Context(), dockerCLI, createOpts)
|
return RunConfigCreate(dockerCli, createOpts)
|
||||||
},
|
},
|
||||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
ValidArgsFunction: completion.NoComplete,
|
||||||
switch len(args) {
|
|
||||||
case 0:
|
|
||||||
// No completion for the first argument, which is the name for
|
|
||||||
// the new config, but if a non-empty name is given, we return
|
|
||||||
// it as completion to allow "tab"-ing to the next completion.
|
|
||||||
return []string{toComplete}, cobra.ShellCompDirectiveNoFileComp
|
|
||||||
case 1:
|
|
||||||
// Second argument is either "-" or a file to load.
|
|
||||||
//
|
|
||||||
// TODO(thaJeztah): provide completion for "-".
|
|
||||||
return nil, cobra.ShellCompDirectiveNoSpace | cobra.ShellCompDirectiveDefault
|
|
||||||
default:
|
|
||||||
// Command only accepts two arguments.
|
|
||||||
return nil, cobra.ShellCompDirectiveNoSpace | cobra.ShellCompDirectiveNoFileComp
|
|
||||||
}
|
|
||||||
},
|
|
||||||
DisableFlagsInUseLine: true,
|
|
||||||
}
|
}
|
||||||
flags := cmd.Flags()
|
flags := cmd.Flags()
|
||||||
flags.VarP(&createOpts.labels, "label", "l", "Config labels")
|
flags.VarP(&createOpts.Labels, "label", "l", "Config labels")
|
||||||
flags.StringVar(&createOpts.templateDriver, "template-driver", "", "Template driver")
|
flags.StringVar(&createOpts.TemplateDriver, "template-driver", "", "Template driver")
|
||||||
_ = flags.SetAnnotation("template-driver", "version", []string{"1.37"})
|
flags.SetAnnotation("template-driver", "version", []string{"1.37"})
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
// runCreate creates a config with the given options.
|
// RunConfigCreate creates a config with the given options.
|
||||||
func runCreate(ctx context.Context, dockerCLI command.Cli, options createOptions) error {
|
func RunConfigCreate(dockerCli command.Cli, options CreateOptions) error {
|
||||||
apiClient := dockerCLI.Client()
|
client := dockerCli.Client()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
configData, err := readConfigData(dockerCLI.In(), options.file)
|
var in io.Reader = dockerCli.In()
|
||||||
|
if options.File != "-" {
|
||||||
|
file, err := sequential.Open(options.File)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
in = file
|
||||||
|
defer file.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
configData, err := io.ReadAll(in)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error reading content from %q: %v", options.file, err)
|
return errors.Errorf("Error reading content from %q: %v", options.File, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
spec := swarm.ConfigSpec{
|
spec := swarm.ConfigSpec{
|
||||||
Annotations: swarm.Annotations{
|
Annotations: swarm.Annotations{
|
||||||
Name: options.name,
|
Name: options.Name,
|
||||||
Labels: opts.ConvertKVStringsToMap(options.labels.GetSlice()),
|
Labels: opts.ConvertKVStringsToMap(options.Labels.GetAll()),
|
||||||
},
|
},
|
||||||
Data: configData,
|
Data: configData,
|
||||||
}
|
}
|
||||||
if options.templateDriver != "" {
|
if options.TemplateDriver != "" {
|
||||||
spec.Templating = &swarm.Driver{
|
spec.Templating = &swarm.Driver{
|
||||||
Name: options.templateDriver,
|
Name: options.TemplateDriver,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
r, err := apiClient.ConfigCreate(ctx, client.ConfigCreateOptions{
|
r, err := client.ConfigCreate(ctx, spec)
|
||||||
Spec: spec,
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, _ = fmt.Fprintln(dockerCLI.Out(), r.ID)
|
fmt.Fprintln(dockerCli.Out(), r.ID)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// maxConfigSize is the maximum byte length of the [swarm.ConfigSpec.Data] field,
|
|
||||||
// as defined by [MaxConfigSize] in SwarmKit.
|
|
||||||
//
|
|
||||||
// [MaxConfigSize]: https://pkg.go.dev/github.com/moby/swarmkit/v2@v2.0.0-20250103191802-8c1959736554/manager/controlapi#MaxConfigSize
|
|
||||||
const maxConfigSize = 1000 * 1024 // 1000KB
|
|
||||||
|
|
||||||
// readConfigData reads the config from either stdin or the given fileName.
|
|
||||||
//
|
|
||||||
// It reads up to twice the maximum size of the config ([maxConfigSize]),
|
|
||||||
// just in case swarm's limit changes; this is only a safeguard to prevent
|
|
||||||
// reading arbitrary files into memory.
|
|
||||||
func readConfigData(in io.Reader, fileName string) ([]byte, error) {
|
|
||||||
switch fileName {
|
|
||||||
case "-":
|
|
||||||
data, err := io.ReadAll(io.LimitReader(in, 2*maxConfigSize))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error reading from STDIN: %w", err)
|
|
||||||
}
|
|
||||||
if len(data) == 0 {
|
|
||||||
return nil, errors.New("error reading from STDIN: data is empty")
|
|
||||||
}
|
|
||||||
return data, nil
|
|
||||||
case "":
|
|
||||||
return nil, errors.New("config file is required")
|
|
||||||
default:
|
|
||||||
// Open file with [FILE_FLAG_SEQUENTIAL_SCAN] on Windows, which
|
|
||||||
// prevents Windows from aggressively caching it. We expect this
|
|
||||||
// file to be only read once. Given that this is expected to be
|
|
||||||
// a small file, this may not be a significant optimization, so
|
|
||||||
// we could choose to omit this, and use a regular [os.Open].
|
|
||||||
//
|
|
||||||
// [FILE_FLAG_SEQUENTIAL_SCAN]: https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea#FILE_FLAG_SEQUENTIAL_SCAN
|
|
||||||
f, err := sequential.Open(fileName)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error reading from %s: %w", fileName, err)
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
data, err := io.ReadAll(io.LimitReader(f, 2*maxConfigSize))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error reading from %s: %w", fileName, err)
|
|
||||||
}
|
|
||||||
if len(data) == 0 {
|
|
||||||
return nil, fmt.Errorf("error reading from %s: data is empty", fileName)
|
|
||||||
}
|
|
||||||
return data, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ package config
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -12,8 +10,9 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/docker/cli/internal/test"
|
"github.com/docker/cli/internal/test"
|
||||||
"github.com/moby/moby/api/types/swarm"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/moby/moby/client"
|
"github.com/docker/docker/api/types/swarm"
|
||||||
|
"github.com/pkg/errors"
|
||||||
"gotest.tools/v3/assert"
|
"gotest.tools/v3/assert"
|
||||||
is "gotest.tools/v3/assert/cmp"
|
is "gotest.tools/v3/assert/cmp"
|
||||||
"gotest.tools/v3/golden"
|
"gotest.tools/v3/golden"
|
||||||
@@ -24,53 +23,50 @@ const configDataFile = "config-create-with-name.golden"
|
|||||||
func TestConfigCreateErrors(t *testing.T) {
|
func TestConfigCreateErrors(t *testing.T) {
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
args []string
|
args []string
|
||||||
configCreateFunc func(context.Context, client.ConfigCreateOptions) (client.ConfigCreateResult, error)
|
configCreateFunc func(context.Context, swarm.ConfigSpec) (types.ConfigCreateResponse, error)
|
||||||
expectedError string
|
expectedError string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
args: []string{"too_few"},
|
args: []string{"too_few"},
|
||||||
expectedError: "requires 2 arguments",
|
expectedError: "requires exactly 2 arguments",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
args: []string{"too", "many", "arguments"},
|
args: []string{"too", "many", "arguments"},
|
||||||
expectedError: "requires 2 arguments",
|
expectedError: "requires exactly 2 arguments",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
args: []string{"name", filepath.Join("testdata", configDataFile)},
|
args: []string{"name", filepath.Join("testdata", configDataFile)},
|
||||||
configCreateFunc: func(_ context.Context, options client.ConfigCreateOptions) (client.ConfigCreateResult, error) {
|
configCreateFunc: func(_ context.Context, configSpec swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
|
||||||
return client.ConfigCreateResult{}, errors.New("error creating config")
|
return types.ConfigCreateResponse{}, errors.Errorf("error creating config")
|
||||||
},
|
},
|
||||||
expectedError: "error creating config",
|
expectedError: "error creating config",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.expectedError, func(t *testing.T) {
|
cmd := newConfigCreateCommand(
|
||||||
cmd := newConfigCreateCommand(
|
test.NewFakeCli(&fakeClient{
|
||||||
test.NewFakeCli(&fakeClient{
|
configCreateFunc: tc.configCreateFunc,
|
||||||
configCreateFunc: tc.configCreateFunc,
|
}),
|
||||||
}),
|
)
|
||||||
)
|
cmd.SetArgs(tc.args)
|
||||||
cmd.SetArgs(tc.args)
|
cmd.SetOut(io.Discard)
|
||||||
cmd.SetOut(io.Discard)
|
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||||
cmd.SetErr(io.Discard)
|
|
||||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConfigCreateWithName(t *testing.T) {
|
func TestConfigCreateWithName(t *testing.T) {
|
||||||
const name = "config-with-name"
|
name := "foo"
|
||||||
var actual []byte
|
var actual []byte
|
||||||
cli := test.NewFakeCli(&fakeClient{
|
cli := test.NewFakeCli(&fakeClient{
|
||||||
configCreateFunc: func(_ context.Context, options client.ConfigCreateOptions) (client.ConfigCreateResult, error) {
|
configCreateFunc: func(_ context.Context, spec swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
|
||||||
if options.Spec.Name != name {
|
if spec.Name != name {
|
||||||
return client.ConfigCreateResult{}, fmt.Errorf("expected name %q, got %q", name, options.Spec.Name)
|
return types.ConfigCreateResponse{}, errors.Errorf("expected name %q, got %q", name, spec.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
actual = options.Spec.Data
|
actual = spec.Data
|
||||||
|
|
||||||
return client.ConfigCreateResult{
|
return types.ConfigCreateResponse{
|
||||||
ID: "ID-" + options.Spec.Name,
|
ID: "ID-" + spec.Name,
|
||||||
}, nil
|
}, nil
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -87,7 +83,7 @@ func TestConfigCreateWithLabels(t *testing.T) {
|
|||||||
"lbl1": "Label-foo",
|
"lbl1": "Label-foo",
|
||||||
"lbl2": "Label-bar",
|
"lbl2": "Label-bar",
|
||||||
}
|
}
|
||||||
const name = "config-with-labels"
|
name := "foo"
|
||||||
|
|
||||||
data, err := os.ReadFile(filepath.Join("testdata", configDataFile))
|
data, err := os.ReadFile(filepath.Join("testdata", configDataFile))
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
@@ -101,13 +97,13 @@ func TestConfigCreateWithLabels(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cli := test.NewFakeCli(&fakeClient{
|
cli := test.NewFakeCli(&fakeClient{
|
||||||
configCreateFunc: func(_ context.Context, options client.ConfigCreateOptions) (client.ConfigCreateResult, error) {
|
configCreateFunc: func(_ context.Context, spec swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
|
||||||
if !reflect.DeepEqual(options.Spec, expected) {
|
if !reflect.DeepEqual(spec, expected) {
|
||||||
return client.ConfigCreateResult{}, fmt.Errorf("expected %+v, got %+v", expected, options.Spec)
|
return types.ConfigCreateResponse{}, errors.Errorf("expected %+v, got %+v", expected, spec)
|
||||||
}
|
}
|
||||||
|
|
||||||
return client.ConfigCreateResult{
|
return types.ConfigCreateResponse{
|
||||||
ID: "ID-" + options.Spec.Name,
|
ID: "ID-" + spec.Name,
|
||||||
}, nil
|
}, nil
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -124,20 +120,20 @@ func TestConfigCreateWithTemplatingDriver(t *testing.T) {
|
|||||||
expectedDriver := &swarm.Driver{
|
expectedDriver := &swarm.Driver{
|
||||||
Name: "template-driver",
|
Name: "template-driver",
|
||||||
}
|
}
|
||||||
const name = "config-with-template-driver"
|
name := "foo"
|
||||||
|
|
||||||
cli := test.NewFakeCli(&fakeClient{
|
cli := test.NewFakeCli(&fakeClient{
|
||||||
configCreateFunc: func(_ context.Context, options client.ConfigCreateOptions) (client.ConfigCreateResult, error) {
|
configCreateFunc: func(_ context.Context, spec swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
|
||||||
if options.Spec.Name != name {
|
if spec.Name != name {
|
||||||
return client.ConfigCreateResult{}, fmt.Errorf("expected name %q, got %q", name, options.Spec.Name)
|
return types.ConfigCreateResponse{}, errors.Errorf("expected name %q, got %q", name, spec.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
if options.Spec.Templating.Name != expectedDriver.Name {
|
if spec.Templating.Name != expectedDriver.Name {
|
||||||
return client.ConfigCreateResult{}, fmt.Errorf("expected driver %v, got %v", expectedDriver, options.Spec.Labels)
|
return types.ConfigCreateResponse{}, errors.Errorf("expected driver %v, got %v", expectedDriver, spec.Labels)
|
||||||
}
|
}
|
||||||
|
|
||||||
return client.ConfigCreateResult{
|
return types.ConfigCreateResponse{
|
||||||
ID: "ID-" + options.Spec.Name,
|
ID: "ID-" + spec.Name,
|
||||||
}, nil
|
}, nil
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,19 +1,15 @@
|
|||||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
|
||||||
//go:build go1.26
|
|
||||||
|
|
||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"slices"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/docker/cli/cli/command"
|
||||||
"github.com/docker/cli/cli/command/formatter"
|
"github.com/docker/cli/cli/command/formatter"
|
||||||
"github.com/docker/cli/cli/command/inspect"
|
"github.com/docker/cli/cli/command/inspect"
|
||||||
"github.com/docker/go-units"
|
"github.com/docker/docker/api/types/swarm"
|
||||||
"github.com/moby/moby/api/types/swarm"
|
units "github.com/docker/go-units"
|
||||||
"github.com/moby/moby/client"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -34,8 +30,8 @@ Data:
|
|||||||
{{.Data}}`
|
{{.Data}}`
|
||||||
)
|
)
|
||||||
|
|
||||||
// newFormat returns a Format for rendering using a configContext.
|
// NewFormat returns a Format for rendering using a config Context
|
||||||
func newFormat(source string, quiet bool) formatter.Format {
|
func NewFormat(source string, quiet bool) formatter.Format {
|
||||||
switch source {
|
switch source {
|
||||||
case formatter.PrettyFormatKey:
|
case formatter.PrettyFormatKey:
|
||||||
return configInspectPrettyTemplate
|
return configInspectPrettyTemplate
|
||||||
@@ -48,28 +44,31 @@ func newFormat(source string, quiet bool) formatter.Format {
|
|||||||
return formatter.Format(source)
|
return formatter.Format(source)
|
||||||
}
|
}
|
||||||
|
|
||||||
// formatWrite writes the context
|
// FormatWrite writes the context
|
||||||
func formatWrite(fmtCtx formatter.Context, configs client.ConfigListResult) error {
|
func FormatWrite(ctx formatter.Context, configs []swarm.Config) error {
|
||||||
cCtx := &configContext{
|
render := func(format func(subContext formatter.SubContext) error) error {
|
||||||
HeaderContext: formatter.HeaderContext{
|
for _, config := range configs {
|
||||||
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 {
|
|
||||||
configCtx := &configContext{c: config}
|
configCtx := &configContext{c: config}
|
||||||
if err := format(configCtx); err != nil {
|
if err := format(configCtx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
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 {
|
type configContext struct {
|
||||||
@@ -102,11 +101,10 @@ func (c *configContext) Labels() string {
|
|||||||
if mapLabels == nil {
|
if mapLabels == nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
joinLabels := make([]string, 0, len(mapLabels))
|
var joinLabels []string
|
||||||
for k, v := range mapLabels {
|
for k, v := range mapLabels {
|
||||||
joinLabels = append(joinLabels, k+"="+v)
|
joinLabels = append(joinLabels, fmt.Sprintf("%s=%s", k, v))
|
||||||
}
|
}
|
||||||
slices.Sort(joinLabels)
|
|
||||||
return strings.Join(joinLabels, ",")
|
return strings.Join(joinLabels, ",")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,12 +115,12 @@ func (c *configContext) Label(name string) string {
|
|||||||
return c.c.Spec.Annotations.Labels[name]
|
return c.c.Spec.Annotations.Labels[name]
|
||||||
}
|
}
|
||||||
|
|
||||||
// inspectFormatWrite renders the context for a list of configs
|
// InspectFormatWrite renders the context for a list of configs
|
||||||
func inspectFormatWrite(fmtCtx formatter.Context, refs []string, getRef inspect.GetRefFunc) error {
|
func InspectFormatWrite(ctx formatter.Context, refs []string, getRef inspect.GetRefFunc) error {
|
||||||
if fmtCtx.Format != configInspectPrettyTemplate {
|
if ctx.Format != configInspectPrettyTemplate {
|
||||||
return inspect.Inspect(fmtCtx.Output, refs, string(fmtCtx.Format), getRef)
|
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 {
|
for _, ref := range refs {
|
||||||
configI, _, err := getRef(ref)
|
configI, _, err := getRef(ref)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -137,7 +135,8 @@ func inspectFormatWrite(fmtCtx formatter.Context, refs []string, getRef inspect.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
}
|
||||||
|
return ctx.Write(&configInspectContext{}, render)
|
||||||
}
|
}
|
||||||
|
|
||||||
type configInspectContext struct {
|
type configInspectContext struct {
|
||||||
@@ -158,11 +157,11 @@ func (ctx *configInspectContext) Labels() map[string]string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ctx *configInspectContext) CreatedAt() string {
|
func (ctx *configInspectContext) CreatedAt() string {
|
||||||
return formatter.PrettyPrint(ctx.Config.CreatedAt)
|
return command.PrettyPrint(ctx.Config.CreatedAt)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctx *configInspectContext) UpdatedAt() string {
|
func (ctx *configInspectContext) UpdatedAt() string {
|
||||||
return formatter.PrettyPrint(ctx.Config.UpdatedAt)
|
return command.PrettyPrint(ctx.Config.UpdatedAt)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctx *configInspectContext) Data() string {
|
func (ctx *configInspectContext) Data() string {
|
||||||
|
|||||||
@@ -6,8 +6,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/docker/cli/cli/command/formatter"
|
"github.com/docker/cli/cli/command/formatter"
|
||||||
"github.com/moby/moby/api/types/swarm"
|
"github.com/docker/docker/api/types/swarm"
|
||||||
"github.com/moby/moby/client"
|
|
||||||
"gotest.tools/v3/assert"
|
"gotest.tools/v3/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -28,46 +27,45 @@ func TestConfigContextFormatWrite(t *testing.T) {
|
|||||||
},
|
},
|
||||||
// Table format
|
// Table format
|
||||||
{
|
{
|
||||||
formatter.Context{Format: newFormat("table", false)},
|
formatter.Context{Format: NewFormat("table", false)},
|
||||||
`ID NAME CREATED UPDATED
|
`ID NAME CREATED UPDATED
|
||||||
1 passwords Less than a second ago Less than a second ago
|
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
|
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
|
`NAME
|
||||||
passwords
|
passwords
|
||||||
id_rsa
|
id_rsa
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
formatter.Context{Format: newFormat("{{.ID}}-{{.Name}}", false)},
|
formatter.Context{Format: NewFormat("{{.ID}}-{{.Name}}", false)},
|
||||||
`1-passwords
|
`1-passwords
|
||||||
2-id_rsa
|
2-id_rsa
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
res := client.ConfigListResult{
|
configs := []swarm.Config{
|
||||||
Items: []swarm.Config{
|
{
|
||||||
{
|
ID: "1",
|
||||||
ID: "1",
|
Meta: swarm.Meta{CreatedAt: time.Now(), UpdatedAt: time.Now()},
|
||||||
Meta: swarm.Meta{CreatedAt: time.Now(), UpdatedAt: time.Now()},
|
Spec: swarm.ConfigSpec{Annotations: swarm.Annotations{Name: "passwords"}},
|
||||||
Spec: swarm.ConfigSpec{Annotations: swarm.Annotations{Name: "passwords"}},
|
},
|
||||||
},
|
{
|
||||||
{
|
ID: "2",
|
||||||
ID: "2",
|
Meta: swarm.Meta{CreatedAt: time.Now(), UpdatedAt: time.Now()},
|
||||||
Meta: swarm.Meta{CreatedAt: time.Now(), UpdatedAt: time.Now()},
|
Spec: swarm.ConfigSpec{Annotations: swarm.Annotations{Name: "id_rsa"}},
|
||||||
Spec: swarm.ConfigSpec{Annotations: swarm.Annotations{Name: "id_rsa"}},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
|
tc := tc
|
||||||
t.Run(string(tc.context.Format), func(t *testing.T) {
|
t.Run(string(tc.context.Format), func(t *testing.T) {
|
||||||
var out bytes.Buffer
|
var out bytes.Buffer
|
||||||
tc.context.Output = &out
|
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)
|
assert.ErrorContains(t, err, tc.expected)
|
||||||
} else {
|
} else {
|
||||||
assert.Equal(t, out.String(), tc.expected)
|
assert.Equal(t, out.String(), tc.expected)
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
|
||||||
//go:build go1.26
|
|
||||||
|
|
||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -12,61 +9,62 @@ import (
|
|||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
"github.com/docker/cli/cli/command/formatter"
|
"github.com/docker/cli/cli/command/formatter"
|
||||||
flagsHelper "github.com/docker/cli/cli/flags"
|
flagsHelper "github.com/docker/cli/cli/flags"
|
||||||
"github.com/moby/moby/client"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
// inspectOptions contains options for the docker config inspect command.
|
// InspectOptions contains options for the docker config inspect command.
|
||||||
type inspectOptions struct {
|
type InspectOptions struct {
|
||||||
names []string
|
Names []string
|
||||||
format string
|
Format string
|
||||||
pretty bool
|
Pretty bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func newConfigInspectCommand(dockerCLI command.Cli) *cobra.Command {
|
func newConfigInspectCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
opts := inspectOptions{}
|
opts := InspectOptions{}
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "inspect [OPTIONS] CONFIG [CONFIG...]",
|
Use: "inspect [OPTIONS] CONFIG [CONFIG...]",
|
||||||
Short: "Display detailed information on one or more configs",
|
Short: "Display detailed information on one or more configs",
|
||||||
Args: cli.RequiresMinArgs(1),
|
Args: cli.RequiresMinArgs(1),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
opts.names = args
|
opts.Names = args
|
||||||
return runInspect(cmd.Context(), dockerCLI, opts)
|
return RunConfigInspect(dockerCli, opts)
|
||||||
|
},
|
||||||
|
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
return completeNames(dockerCli)(cmd, args, toComplete)
|
||||||
},
|
},
|
||||||
ValidArgsFunction: completeNames(dockerCLI),
|
|
||||||
DisableFlagsInUseLine: true,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd.Flags().StringVarP(&opts.format, "format", "f", "", flagsHelper.InspectFormatHelp)
|
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().BoolVar(&opts.Pretty, "pretty", false, "Print the information in a human friendly format")
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
// runInspect inspects the given Swarm config.
|
// RunConfigInspect inspects the given Swarm config.
|
||||||
func runInspect(ctx context.Context, dockerCLI command.Cli, opts inspectOptions) error {
|
func RunConfigInspect(dockerCli command.Cli, opts InspectOptions) error {
|
||||||
apiClient := dockerCLI.Client()
|
client := dockerCli.Client()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
if opts.pretty {
|
if opts.Pretty {
|
||||||
opts.format = "pretty"
|
opts.Format = "pretty"
|
||||||
}
|
}
|
||||||
|
|
||||||
getRef := func(id string) (any, []byte, error) {
|
getRef := func(id string) (interface{}, []byte, error) {
|
||||||
res, err := apiClient.ConfigInspect(ctx, id, client.ConfigInspectOptions{})
|
return client.ConfigInspectWithRaw(ctx, id)
|
||||||
return res.Config, res.Raw, err
|
|
||||||
}
|
}
|
||||||
|
f := opts.Format
|
||||||
|
|
||||||
// check if the user is trying to apply a template to the pretty format, which
|
// check if the user is trying to apply a template to the pretty format, which
|
||||||
// is not supported
|
// is not supported
|
||||||
if strings.HasPrefix(opts.format, "pretty") && opts.format != "pretty" {
|
if strings.HasPrefix(f, "pretty") && f != "pretty" {
|
||||||
return errors.New("cannot supply extra formatting options to the pretty template")
|
return errors.New("cannot supply extra formatting options to the pretty template")
|
||||||
}
|
}
|
||||||
|
|
||||||
configCtx := formatter.Context{
|
configCtx := formatter.Context{
|
||||||
Output: dockerCLI.Out(),
|
Output: dockerCli.Out(),
|
||||||
Format: newFormat(opts.format, false),
|
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 cli.StatusError{StatusCode: 1, Status: err.Error()}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package config
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -10,7 +9,8 @@ import (
|
|||||||
|
|
||||||
"github.com/docker/cli/internal/test"
|
"github.com/docker/cli/internal/test"
|
||||||
"github.com/docker/cli/internal/test/builders"
|
"github.com/docker/cli/internal/test/builders"
|
||||||
"github.com/moby/moby/client"
|
"github.com/docker/docker/api/types/swarm"
|
||||||
|
"github.com/pkg/errors"
|
||||||
"gotest.tools/v3/assert"
|
"gotest.tools/v3/assert"
|
||||||
"gotest.tools/v3/golden"
|
"gotest.tools/v3/golden"
|
||||||
)
|
)
|
||||||
@@ -19,7 +19,7 @@ func TestConfigInspectErrors(t *testing.T) {
|
|||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
args []string
|
args []string
|
||||||
flags map[string]string
|
flags map[string]string
|
||||||
configInspectFunc func(_ context.Context, configID string, _ client.ConfigInspectOptions) (client.ConfigInspectResult, error)
|
configInspectFunc func(_ context.Context, configID string) (swarm.Config, []byte, error)
|
||||||
expectedError string
|
expectedError string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
@@ -27,8 +27,8 @@ func TestConfigInspectErrors(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
args: []string{"foo"},
|
args: []string{"foo"},
|
||||||
configInspectFunc: func(context.Context, string, client.ConfigInspectOptions) (client.ConfigInspectResult, error) {
|
configInspectFunc: func(_ context.Context, configID string) (swarm.Config, []byte, error) {
|
||||||
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",
|
expectedError: "error while inspecting the config",
|
||||||
},
|
},
|
||||||
@@ -41,13 +41,11 @@ func TestConfigInspectErrors(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
args: []string{"foo", "bar"},
|
args: []string{"foo", "bar"},
|
||||||
configInspectFunc: func(_ context.Context, configID string, _ client.ConfigInspectOptions) (client.ConfigInspectResult, error) {
|
configInspectFunc: func(_ context.Context, configID string) (swarm.Config, []byte, error) {
|
||||||
if configID == "foo" {
|
if configID == "foo" {
|
||||||
return client.ConfigInspectResult{
|
return *builders.Config(builders.ConfigName("foo")), nil, nil
|
||||||
Config: *builders.Config(builders.ConfigName("foo")),
|
|
||||||
}, 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",
|
expectedError: "error while inspecting the config",
|
||||||
},
|
},
|
||||||
@@ -63,7 +61,6 @@ func TestConfigInspectErrors(t *testing.T) {
|
|||||||
assert.Check(t, cmd.Flags().Set(key, value))
|
assert.Check(t, cmd.Flags().Set(key, value))
|
||||||
}
|
}
|
||||||
cmd.SetOut(io.Discard)
|
cmd.SetOut(io.Discard)
|
||||||
cmd.SetErr(io.Discard)
|
|
||||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -72,34 +69,25 @@ func TestConfigInspectWithoutFormat(t *testing.T) {
|
|||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
args []string
|
args []string
|
||||||
configInspectFunc func(_ context.Context, configID string, _ client.ConfigInspectOptions) (client.ConfigInspectResult, error)
|
configInspectFunc func(_ context.Context, configID string) (swarm.Config, []byte, error)
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "single-config",
|
name: "single-config",
|
||||||
args: []string{"foo"},
|
args: []string{"foo"},
|
||||||
configInspectFunc: func(_ context.Context, name string, _ client.ConfigInspectOptions) (client.ConfigInspectResult, error) {
|
configInspectFunc: func(_ context.Context, name string) (swarm.Config, []byte, error) {
|
||||||
if name != "foo" {
|
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{
|
return *builders.Config(builders.ConfigID("ID-foo"), builders.ConfigName("foo")), nil, nil
|
||||||
Config: *builders.Config(
|
|
||||||
builders.ConfigID("ID-foo"),
|
|
||||||
builders.ConfigName("foo"),
|
|
||||||
),
|
|
||||||
}, nil
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "multiple-configs-with-labels",
|
name: "multiple-configs-with-labels",
|
||||||
args: []string{"foo", "bar"},
|
args: []string{"foo", "bar"},
|
||||||
configInspectFunc: func(_ context.Context, name string, _ client.ConfigInspectOptions) (client.ConfigInspectResult, error) {
|
configInspectFunc: func(_ context.Context, name string) (swarm.Config, []byte, error) {
|
||||||
return client.ConfigInspectResult{
|
return *builders.Config(builders.ConfigID("ID-"+name), builders.ConfigName(name), builders.ConfigLabels(map[string]string{
|
||||||
Config: *builders.Config(
|
"label1": "label-foo",
|
||||||
builders.ConfigID("ID-"+name),
|
})), nil, nil
|
||||||
builders.ConfigName(name),
|
|
||||||
builders.ConfigLabels(map[string]string{"label1": "label-foo"}),
|
|
||||||
),
|
|
||||||
}, nil
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -113,19 +101,16 @@ func TestConfigInspectWithoutFormat(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestConfigInspectWithFormat(t *testing.T) {
|
func TestConfigInspectWithFormat(t *testing.T) {
|
||||||
configInspectFunc := func(_ context.Context, name string, _ client.ConfigInspectOptions) (client.ConfigInspectResult, error) {
|
configInspectFunc := func(_ context.Context, name string) (swarm.Config, []byte, error) {
|
||||||
return client.ConfigInspectResult{
|
return *builders.Config(builders.ConfigName("foo"), builders.ConfigLabels(map[string]string{
|
||||||
Config: *builders.Config(
|
"label1": "label-foo",
|
||||||
builders.ConfigName("foo"),
|
})), nil, nil
|
||||||
builders.ConfigLabels(map[string]string{"label1": "label-foo"}),
|
|
||||||
),
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
format string
|
format string
|
||||||
args []string
|
args []string
|
||||||
configInspectFunc func(_ context.Context, name string, _ client.ConfigInspectOptions) (client.ConfigInspectResult, error)
|
configInspectFunc func(_ context.Context, name string) (swarm.Config, []byte, error)
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "simple-template",
|
name: "simple-template",
|
||||||
@@ -155,23 +140,21 @@ func TestConfigInspectWithFormat(t *testing.T) {
|
|||||||
func TestConfigInspectPretty(t *testing.T) {
|
func TestConfigInspectPretty(t *testing.T) {
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
configInspectFunc func(context.Context, string, client.ConfigInspectOptions) (client.ConfigInspectResult, error)
|
configInspectFunc func(context.Context, string) (swarm.Config, []byte, error)
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "simple",
|
name: "simple",
|
||||||
configInspectFunc: func(_ context.Context, id string, _ client.ConfigInspectOptions) (client.ConfigInspectResult, error) {
|
configInspectFunc: func(_ context.Context, id string) (swarm.Config, []byte, error) {
|
||||||
return client.ConfigInspectResult{
|
return *builders.Config(
|
||||||
Config: *builders.Config(
|
builders.ConfigLabels(map[string]string{
|
||||||
builders.ConfigLabels(map[string]string{
|
"lbl1": "value1",
|
||||||
"lbl1": "value1",
|
}),
|
||||||
}),
|
builders.ConfigID("configID"),
|
||||||
builders.ConfigID("configID"),
|
builders.ConfigName("configName"),
|
||||||
builders.ConfigName("configName"),
|
builders.ConfigCreatedAt(time.Time{}),
|
||||||
builders.ConfigCreatedAt(time.Time{}),
|
builders.ConfigUpdatedAt(time.Time{}),
|
||||||
builders.ConfigUpdatedAt(time.Time{}),
|
builders.ConfigData([]byte("payload here")),
|
||||||
builders.ConfigData([]byte("payload here")),
|
), []byte{}, nil
|
||||||
),
|
|
||||||
}, nil
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
+28
-31
@@ -1,32 +1,29 @@
|
|||||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
|
||||||
//go:build go1.26
|
|
||||||
|
|
||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"slices"
|
"sort"
|
||||||
|
|
||||||
"github.com/docker/cli/cli"
|
"github.com/docker/cli/cli"
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
|
"github.com/docker/cli/cli/command/completion"
|
||||||
"github.com/docker/cli/cli/command/formatter"
|
"github.com/docker/cli/cli/command/formatter"
|
||||||
flagsHelper "github.com/docker/cli/cli/flags"
|
flagsHelper "github.com/docker/cli/cli/flags"
|
||||||
"github.com/docker/cli/opts"
|
"github.com/docker/cli/opts"
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/fvbommel/sortorder"
|
"github.com/fvbommel/sortorder"
|
||||||
"github.com/moby/moby/api/types/swarm"
|
|
||||||
"github.com/moby/moby/client"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
// listOptions contains options for the docker config ls command.
|
// ListOptions contains options for the docker config ls command.
|
||||||
type listOptions struct {
|
type ListOptions struct {
|
||||||
quiet bool
|
Quiet bool
|
||||||
format string
|
Format string
|
||||||
filter opts.FilterOpt
|
Filter opts.FilterOpt
|
||||||
}
|
}
|
||||||
|
|
||||||
func newConfigListCommand(dockerCLI command.Cli) *cobra.Command {
|
func newConfigListCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
listOpts := listOptions{filter: opts.NewFilterOpt()}
|
listOpts := ListOptions{Filter: opts.NewFilterOpt()}
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "ls [OPTIONS]",
|
Use: "ls [OPTIONS]",
|
||||||
@@ -34,45 +31,45 @@ func newConfigListCommand(dockerCLI command.Cli) *cobra.Command {
|
|||||||
Short: "List configs",
|
Short: "List configs",
|
||||||
Args: cli.NoArgs,
|
Args: cli.NoArgs,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return runList(cmd.Context(), dockerCLI, listOpts)
|
return RunConfigList(dockerCli, listOpts)
|
||||||
},
|
},
|
||||||
ValidArgsFunction: cobra.NoFileCompletions,
|
ValidArgsFunction: completion.NoComplete,
|
||||||
DisableFlagsInUseLine: true,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
flags := cmd.Flags()
|
flags := cmd.Flags()
|
||||||
flags.BoolVarP(&listOpts.quiet, "quiet", "q", false, "Only display IDs")
|
flags.BoolVarP(&listOpts.Quiet, "quiet", "q", false, "Only display IDs")
|
||||||
flags.StringVar(&listOpts.format, "format", "", flagsHelper.FormatHelp)
|
flags.StringVarP(&listOpts.Format, "format", "", "", flagsHelper.FormatHelp)
|
||||||
flags.VarP(&listOpts.filter, "filter", "f", "Filter output based on conditions provided")
|
flags.VarP(&listOpts.Filter, "filter", "f", "Filter output based on conditions provided")
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
// runList lists Swarm configs.
|
// RunConfigList lists Swarm configs.
|
||||||
func runList(ctx context.Context, dockerCLI command.Cli, options listOptions) error {
|
func RunConfigList(dockerCli command.Cli, options ListOptions) error {
|
||||||
apiClient := dockerCLI.Client()
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
format := options.format
|
format := options.Format
|
||||||
if len(format) == 0 {
|
if len(format) == 0 {
|
||||||
if len(dockerCLI.ConfigFile().ConfigFormat) > 0 && !options.quiet {
|
if len(dockerCli.ConfigFile().ConfigFormat) > 0 && !options.Quiet {
|
||||||
format = dockerCLI.ConfigFile().ConfigFormat
|
format = dockerCli.ConfigFile().ConfigFormat
|
||||||
} else {
|
} else {
|
||||||
format = formatter.TableFormatKey
|
format = formatter.TableFormatKey
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
slices.SortFunc(res.Items, func(a, b swarm.Config) int {
|
sort.Slice(configs, func(i, j int) bool {
|
||||||
return sortorder.NaturalCompare(a.Spec.Name, b.Spec.Name)
|
return sortorder.NaturalLess(configs[i].Spec.Name, configs[j].Spec.Name)
|
||||||
})
|
})
|
||||||
|
|
||||||
configCtx := formatter.Context{
|
configCtx := formatter.Context{
|
||||||
Output: dockerCLI.Out(),
|
Output: dockerCli.Out(),
|
||||||
Format: newFormat(format, options.quiet),
|
Format: NewFormat(format, options.Quiet),
|
||||||
}
|
}
|
||||||
return formatWrite(configCtx, res)
|
return FormatWrite(configCtx, configs)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package config
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"io"
|
"io"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -10,16 +9,18 @@ import (
|
|||||||
"github.com/docker/cli/cli/config/configfile"
|
"github.com/docker/cli/cli/config/configfile"
|
||||||
"github.com/docker/cli/internal/test"
|
"github.com/docker/cli/internal/test"
|
||||||
"github.com/docker/cli/internal/test/builders"
|
"github.com/docker/cli/internal/test/builders"
|
||||||
"github.com/moby/moby/api/types/swarm"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/moby/moby/client"
|
"github.com/docker/docker/api/types/swarm"
|
||||||
|
"github.com/pkg/errors"
|
||||||
"gotest.tools/v3/assert"
|
"gotest.tools/v3/assert"
|
||||||
|
is "gotest.tools/v3/assert/cmp"
|
||||||
"gotest.tools/v3/golden"
|
"gotest.tools/v3/golden"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestConfigListErrors(t *testing.T) {
|
func TestConfigListErrors(t *testing.T) {
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
args []string
|
args []string
|
||||||
configListFunc func(context.Context, client.ConfigListOptions) (client.ConfigListResult, error)
|
configListFunc func(context.Context, types.ConfigListOptions) ([]swarm.Config, error)
|
||||||
expectedError string
|
expectedError string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
@@ -27,8 +28,8 @@ func TestConfigListErrors(t *testing.T) {
|
|||||||
expectedError: "accepts no argument",
|
expectedError: "accepts no argument",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
configListFunc: func(_ context.Context, options client.ConfigListOptions) (client.ConfigListResult, error) {
|
configListFunc: func(_ context.Context, options types.ConfigListOptions) ([]swarm.Config, error) {
|
||||||
return client.ConfigListResult{}, errors.New("error listing configs")
|
return []swarm.Config{}, errors.Errorf("error listing configs")
|
||||||
},
|
},
|
||||||
expectedError: "error listing configs",
|
expectedError: "error listing configs",
|
||||||
},
|
},
|
||||||
@@ -41,35 +42,32 @@ func TestConfigListErrors(t *testing.T) {
|
|||||||
)
|
)
|
||||||
cmd.SetArgs(tc.args)
|
cmd.SetArgs(tc.args)
|
||||||
cmd.SetOut(io.Discard)
|
cmd.SetOut(io.Discard)
|
||||||
cmd.SetErr(io.Discard)
|
|
||||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConfigList(t *testing.T) {
|
func TestConfigList(t *testing.T) {
|
||||||
cli := test.NewFakeCli(&fakeClient{
|
cli := test.NewFakeCli(&fakeClient{
|
||||||
configListFunc: func(_ context.Context, options client.ConfigListOptions) (client.ConfigListResult, error) {
|
configListFunc: func(_ context.Context, options types.ConfigListOptions) ([]swarm.Config, error) {
|
||||||
return client.ConfigListResult{
|
return []swarm.Config{
|
||||||
Items: []swarm.Config{
|
*builders.Config(builders.ConfigID("ID-1-foo"),
|
||||||
*builders.Config(builders.ConfigID("ID-1-foo"),
|
builders.ConfigName("1-foo"),
|
||||||
builders.ConfigName("1-foo"),
|
builders.ConfigVersion(swarm.Version{Index: 10}),
|
||||||
builders.ConfigVersion(swarm.Version{Index: 10}),
|
builders.ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
|
||||||
builders.ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
|
builders.ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
|
||||||
builders.ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
|
),
|
||||||
),
|
*builders.Config(builders.ConfigID("ID-10-foo"),
|
||||||
*builders.Config(builders.ConfigID("ID-10-foo"),
|
builders.ConfigName("10-foo"),
|
||||||
builders.ConfigName("10-foo"),
|
builders.ConfigVersion(swarm.Version{Index: 11}),
|
||||||
builders.ConfigVersion(swarm.Version{Index: 11}),
|
builders.ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
|
||||||
builders.ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
|
builders.ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
|
||||||
builders.ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
|
),
|
||||||
),
|
*builders.Config(builders.ConfigID("ID-2-foo"),
|
||||||
*builders.Config(builders.ConfigID("ID-2-foo"),
|
builders.ConfigName("2-foo"),
|
||||||
builders.ConfigName("2-foo"),
|
builders.ConfigVersion(swarm.Version{Index: 11}),
|
||||||
builders.ConfigVersion(swarm.Version{Index: 11}),
|
builders.ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
|
||||||
builders.ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
|
builders.ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
|
||||||
builders.ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
|
),
|
||||||
),
|
|
||||||
},
|
|
||||||
}, nil
|
}, nil
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -80,14 +78,12 @@ func TestConfigList(t *testing.T) {
|
|||||||
|
|
||||||
func TestConfigListWithQuietOption(t *testing.T) {
|
func TestConfigListWithQuietOption(t *testing.T) {
|
||||||
cli := test.NewFakeCli(&fakeClient{
|
cli := test.NewFakeCli(&fakeClient{
|
||||||
configListFunc: func(_ context.Context, options client.ConfigListOptions) (client.ConfigListResult, error) {
|
configListFunc: func(_ context.Context, options types.ConfigListOptions) ([]swarm.Config, error) {
|
||||||
return client.ConfigListResult{
|
return []swarm.Config{
|
||||||
Items: []swarm.Config{
|
*builders.Config(builders.ConfigID("ID-foo"), builders.ConfigName("foo")),
|
||||||
*builders.Config(builders.ConfigID("ID-foo"), builders.ConfigName("foo")),
|
*builders.Config(builders.ConfigID("ID-bar"), builders.ConfigName("bar"), builders.ConfigLabels(map[string]string{
|
||||||
*builders.Config(builders.ConfigID("ID-bar"), builders.ConfigName("bar"), builders.ConfigLabels(map[string]string{
|
"label": "label-bar",
|
||||||
"label": "label-bar",
|
})),
|
||||||
})),
|
|
||||||
},
|
|
||||||
}, nil
|
}, nil
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -99,14 +95,12 @@ func TestConfigListWithQuietOption(t *testing.T) {
|
|||||||
|
|
||||||
func TestConfigListWithConfigFormat(t *testing.T) {
|
func TestConfigListWithConfigFormat(t *testing.T) {
|
||||||
cli := test.NewFakeCli(&fakeClient{
|
cli := test.NewFakeCli(&fakeClient{
|
||||||
configListFunc: func(_ context.Context, options client.ConfigListOptions) (client.ConfigListResult, error) {
|
configListFunc: func(_ context.Context, options types.ConfigListOptions) ([]swarm.Config, error) {
|
||||||
return client.ConfigListResult{
|
return []swarm.Config{
|
||||||
Items: []swarm.Config{
|
*builders.Config(builders.ConfigID("ID-foo"), builders.ConfigName("foo")),
|
||||||
*builders.Config(builders.ConfigID("ID-foo"), builders.ConfigName("foo")),
|
*builders.Config(builders.ConfigID("ID-bar"), builders.ConfigName("bar"), builders.ConfigLabels(map[string]string{
|
||||||
*builders.Config(builders.ConfigID("ID-bar"), builders.ConfigName("bar"), builders.ConfigLabels(map[string]string{
|
"label": "label-bar",
|
||||||
"label": "label-bar",
|
})),
|
||||||
})),
|
|
||||||
},
|
|
||||||
}, nil
|
}, nil
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -120,14 +114,12 @@ func TestConfigListWithConfigFormat(t *testing.T) {
|
|||||||
|
|
||||||
func TestConfigListWithFormat(t *testing.T) {
|
func TestConfigListWithFormat(t *testing.T) {
|
||||||
cli := test.NewFakeCli(&fakeClient{
|
cli := test.NewFakeCli(&fakeClient{
|
||||||
configListFunc: func(_ context.Context, options client.ConfigListOptions) (client.ConfigListResult, error) {
|
configListFunc: func(_ context.Context, options types.ConfigListOptions) ([]swarm.Config, error) {
|
||||||
return client.ConfigListResult{
|
return []swarm.Config{
|
||||||
Items: []swarm.Config{
|
*builders.Config(builders.ConfigID("ID-foo"), builders.ConfigName("foo")),
|
||||||
*builders.Config(builders.ConfigID("ID-foo"), builders.ConfigName("foo")),
|
*builders.Config(builders.ConfigID("ID-bar"), builders.ConfigName("bar"), builders.ConfigLabels(map[string]string{
|
||||||
*builders.Config(builders.ConfigID("ID-bar"), builders.ConfigName("bar"), builders.ConfigLabels(map[string]string{
|
"label": "label-bar",
|
||||||
"label": "label-bar",
|
})),
|
||||||
})),
|
|
||||||
},
|
|
||||||
}, nil
|
}, nil
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -139,24 +131,22 @@ func TestConfigListWithFormat(t *testing.T) {
|
|||||||
|
|
||||||
func TestConfigListWithFilter(t *testing.T) {
|
func TestConfigListWithFilter(t *testing.T) {
|
||||||
cli := test.NewFakeCli(&fakeClient{
|
cli := test.NewFakeCli(&fakeClient{
|
||||||
configListFunc: func(_ context.Context, options client.ConfigListOptions) (client.ConfigListResult, error) {
|
configListFunc: func(_ context.Context, options types.ConfigListOptions) ([]swarm.Config, error) {
|
||||||
assert.Check(t, options.Filters["name"]["foo"])
|
assert.Check(t, is.Equal("foo", options.Filters.Get("name")[0]))
|
||||||
assert.Check(t, options.Filters["label"]["lbl1=Label-bar"])
|
assert.Check(t, is.Equal("lbl1=Label-bar", options.Filters.Get("label")[0]))
|
||||||
return client.ConfigListResult{
|
return []swarm.Config{
|
||||||
Items: []swarm.Config{
|
*builders.Config(builders.ConfigID("ID-foo"),
|
||||||
*builders.Config(builders.ConfigID("ID-foo"),
|
builders.ConfigName("foo"),
|
||||||
builders.ConfigName("foo"),
|
builders.ConfigVersion(swarm.Version{Index: 10}),
|
||||||
builders.ConfigVersion(swarm.Version{Index: 10}),
|
builders.ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
|
||||||
builders.ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
|
builders.ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
|
||||||
builders.ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
|
),
|
||||||
),
|
*builders.Config(builders.ConfigID("ID-bar"),
|
||||||
*builders.Config(builders.ConfigID("ID-bar"),
|
builders.ConfigName("bar"),
|
||||||
builders.ConfigName("bar"),
|
builders.ConfigVersion(swarm.Version{Index: 11}),
|
||||||
builders.ConfigVersion(swarm.Version{Index: 11}),
|
builders.ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
|
||||||
builders.ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
|
builders.ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
|
||||||
builders.ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
|
),
|
||||||
),
|
|
||||||
},
|
|
||||||
}, nil
|
}, nil
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,41 +2,57 @@ package config
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/docker/cli/cli"
|
"github.com/docker/cli/cli"
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
"github.com/moby/moby/client"
|
"github.com/pkg/errors"
|
||||||
"github.com/spf13/cobra"
|
"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{
|
return &cobra.Command{
|
||||||
Use: "rm CONFIG [CONFIG...]",
|
Use: "rm CONFIG [CONFIG...]",
|
||||||
Aliases: []string{"remove"},
|
Aliases: []string{"remove"},
|
||||||
Short: "Remove one or more configs",
|
Short: "Remove one or more configs",
|
||||||
Args: cli.RequiresMinArgs(1),
|
Args: cli.RequiresMinArgs(1),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return runRemove(cmd.Context(), dockerCLI, args)
|
opts := RemoveOptions{
|
||||||
|
Names: args,
|
||||||
|
}
|
||||||
|
return RunConfigRemove(dockerCli, opts)
|
||||||
|
},
|
||||||
|
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
return completeNames(dockerCli)(cmd, args, toComplete)
|
||||||
},
|
},
|
||||||
ValidArgsFunction: completeNames(dockerCLI),
|
|
||||||
DisableFlagsInUseLine: true,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// runRemove removes the given Swarm configs.
|
// RunConfigRemove removes the given Swarm configs.
|
||||||
func runRemove(ctx context.Context, dockerCLI command.Cli, names []string) error {
|
func RunConfigRemove(dockerCli command.Cli, opts RemoveOptions) error {
|
||||||
apiClient := dockerCLI.Client()
|
client := dockerCli.Client()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
var errs []error
|
var errs []string
|
||||||
for _, name := range names {
|
|
||||||
if _, err := apiClient.ConfigRemove(ctx, name, client.ConfigRemoveOptions{}); err != nil {
|
for _, name := range opts.Names {
|
||||||
errs = append(errs, err)
|
if err := client.ConfigRemove(ctx, name); err != nil {
|
||||||
|
errs = append(errs, err.Error())
|
||||||
continue
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user