mirror of
https://github.com/docker/cli.git
synced 2026-11-12 15:53:52 -04:00
Compare commits
438
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
915f5cf25d | ||
|
|
6ee2756538 | ||
|
|
9f9e405801 | ||
|
|
d7cd22f0a2 | ||
|
|
0e2d4fe890 | ||
|
|
fbc0a73b35 | ||
|
|
777695f6d5 | ||
|
|
092d23532c | ||
|
|
fa1ba052d6 | ||
|
|
1ccfae7946 | ||
|
|
397c2f87d5 | ||
|
|
a187fa5d2d | ||
|
|
e32d69e31c | ||
|
|
2767e9eb46 | ||
|
|
a999a40714 | ||
|
|
b277537cbe | ||
|
|
919350df54 | ||
|
|
80f7848805 | ||
|
|
0d27375b07 | ||
|
|
0ced103038 | ||
|
|
f068c14063 | ||
|
|
5d973da95b | ||
|
|
498a416226 | ||
|
|
261d2241eb | ||
|
|
e0e9feaa16 | ||
|
|
074820d7bb | ||
|
|
8b340f4e5c | ||
|
|
21cdefcec6 | ||
|
|
0e053a5cee | ||
|
|
7595cea3e5 | ||
|
|
f009ed2b34 | ||
|
|
2dcdf75ad0 | ||
|
|
fd818019f5 | ||
|
|
f264af6124 | ||
|
|
3e51dace7c | ||
|
|
2d3e97f32d | ||
|
|
d8f2d1b155 | ||
|
|
e3d2562114 | ||
|
|
bcec6cb71a | ||
|
|
ef6bc69c04 | ||
|
|
68a89eaa17 | ||
|
|
d4dc4efef0 | ||
|
|
b9d17eaebb | ||
|
|
a08a120bae | ||
|
|
4870b3d067 | ||
|
|
d3b59fbd93 | ||
|
|
ac40240c02 | ||
|
|
3fa9480d26 | ||
|
|
fce7c04971 | ||
|
|
70815c1aa9 | ||
|
|
12d98b057f | ||
|
|
f9783ec5c6 | ||
|
|
27cf4d7d85 | ||
|
|
6377f54f27 | ||
|
|
0a1f8e414b | ||
|
|
dc8511cd60 | ||
|
|
bde2b89313 | ||
|
|
3284a80b05 | ||
|
|
b7064a2758 | ||
|
|
67b6fe0b55 | ||
|
|
5a0508ccc8 | ||
|
|
9348385006 | ||
|
|
9ea09fd364 | ||
|
|
0c9e0b43ce | ||
|
|
f4fec76472 | ||
|
|
6fd48251ce | ||
|
|
530cf098de | ||
|
|
26da5bf025 | ||
|
|
790d09e8dc | ||
|
|
5476e9e1dc | ||
|
|
ce03e25b14 | ||
|
|
47a4f70d8a | ||
|
|
fa845f4a3c | ||
|
|
6b9fb59d31 | ||
|
|
af56ef5b07 | ||
|
|
39d73afbdd | ||
|
|
c6537362af | ||
|
|
877668c92a | ||
|
|
8d1bacae3e | ||
|
|
cfe6090d5d | ||
|
|
3df40364f6 | ||
|
|
c5d93f53a7 | ||
|
|
a4dc3d78e5 | ||
|
|
75876b4e20 | ||
|
|
69bc2b1f0c | ||
|
|
955e003345 | ||
|
|
ce77f4a545 | ||
|
|
e20bcf3565 | ||
|
|
78544cd587 | ||
|
|
d11291966d | ||
|
|
25ff89519f | ||
|
|
83303411ee | ||
|
|
e25548d1c6 | ||
|
|
99e60c4e39 | ||
|
|
6d087eb91b | ||
|
|
83f79c3e86 | ||
|
|
6f439ea4a4 | ||
|
|
7e35be13ea | ||
|
|
f9ccc50304 | ||
|
|
38e37778b0 | ||
|
|
49b6551e97 | ||
|
|
fd53fe7f47 | ||
|
|
929e861812 | ||
|
|
8caf347188 | ||
|
|
9e7d01bfb3 | ||
|
|
8be6bec27d | ||
|
|
d515955b62 | ||
|
|
d5b327258f | ||
|
|
9128f7b2d5 | ||
|
|
0529d64f7f | ||
|
|
2958a27e0f | ||
|
|
8fe93724a3 | ||
|
|
df7113d7eb | ||
|
|
35122a0692 | ||
|
|
1890ea0762 | ||
|
|
7a906196ed | ||
|
|
2734299993 | ||
|
|
9d5a2a6b66 | ||
|
|
0dd07f0bfd | ||
|
|
168a4bdc05 | ||
|
|
ddeb7eb4ed | ||
|
|
da3a1c3027 | ||
|
|
1377310110 | ||
|
|
78dbcca264 | ||
|
|
97c25b7574 | ||
|
|
05455f8505 | ||
|
|
d23ab524c3 | ||
|
|
46844b4f1d | ||
|
|
3f28d05292 | ||
|
|
2521abdb1b | ||
|
|
eb986ae71b | ||
|
|
f39012209b | ||
|
|
e15a979e3d | ||
|
|
35c87e326c | ||
|
|
f4f328b84b | ||
|
|
470ab05503 | ||
|
|
968506fd0f | ||
|
|
6736be779a | ||
|
|
720da3c65a | ||
|
|
05c860e866 | ||
|
|
365e7a5a4e | ||
|
|
79113a3db5 | ||
|
|
df7e6e5c5f | ||
|
|
254b966b0d | ||
|
|
220033dd80 | ||
|
|
dd013610c6 | ||
|
|
dc36908ec5 | ||
|
|
a8a8f68268 | ||
|
|
b3a55ecbba | ||
|
|
d3bf82db86 | ||
|
|
0008d79159 | ||
|
|
16201b3eb2 | ||
|
|
4241e00d1d | ||
|
|
b018e55ca4 | ||
|
|
e2831282ee | ||
|
|
b68bf3afe4 | ||
|
|
77d002ae25 | ||
|
|
95e329d3e3 | ||
|
|
02da13fb18 | ||
|
|
62230c7ec2 | ||
|
|
954dba4482 | ||
|
|
7d1fa132fb | ||
|
|
b6e7eba447 | ||
|
|
099d4baeed | ||
|
|
dd4b370d3c | ||
|
|
64e98fa375 | ||
|
|
e6c6658563 | ||
|
|
52ac568385 | ||
|
|
9e2d548799 | ||
|
|
2668b11ce4 | ||
|
|
5cbb4ca191 | ||
|
|
78be4c464f | ||
|
|
c93b0f1a0e | ||
|
|
949859a39e | ||
|
|
ae9b655aa8 | ||
|
|
e93d817980 | ||
|
|
ed7d30ef89 | ||
|
|
8437ca4e64 | ||
|
|
3a1e08d6de | ||
|
|
cb3048fbeb | ||
|
|
5721cdc76d | ||
|
|
5c1cb99051 | ||
|
|
9ab3d3a983 | ||
|
|
40539963c2 | ||
|
|
25224ab615 | ||
|
|
4694fc8e45 | ||
|
|
352ee0acd4 | ||
|
|
d0cb93ab08 | ||
|
|
132ecbce8b | ||
|
|
011cb195f0 | ||
|
|
6f5bfcb276 | ||
|
|
fa0bb9f4b4 | ||
|
|
4c5eb4c301 | ||
|
|
b00668d4bc | ||
|
|
e978602169 | ||
|
|
19279c9e53 | ||
|
|
d2aec10148 | ||
|
|
6445ae2f02 | ||
|
|
5771be8156 | ||
|
|
fbd51c01c6 | ||
|
|
23cea90233 | ||
|
|
ce1223035a | ||
|
|
263ba959c4 | ||
|
|
be9b9f3d70 | ||
|
|
a4149b0c3d | ||
|
|
4aac4154b0 | ||
|
|
854695884a | ||
|
|
f05200311d | ||
|
|
460f1becc5 | ||
|
|
e85edf8556 | ||
|
|
ca62759b0f | ||
|
|
e4dc9d2ac7 | ||
|
|
ddf8f23403 | ||
|
|
c6dde25470 | ||
|
|
fcf6dd05df | ||
|
|
4b5a196fee | ||
|
|
e2eb069aed | ||
|
|
58a14cc379 | ||
|
|
81ca58bd20 | ||
|
|
6b5e4be0cb | ||
|
|
b5c6541769 | ||
|
|
6fdd11b012 | ||
|
|
6bf64a88af | ||
|
|
d896559894 | ||
|
|
f350724e21 | ||
|
|
84d4a5ef11 | ||
|
|
7c72283b37 | ||
|
|
245f79ff57 | ||
|
|
db7a014d37 | ||
|
|
bc87ef962a | ||
|
|
7811c442d6 | ||
|
|
48a2cdff97 | ||
|
|
b48720e65e | ||
|
|
c85c3df6b5 | ||
|
|
74bebe2719 | ||
|
|
9748bef1c3 | ||
|
|
2fc18b9874 | ||
|
|
1a199b63da | ||
|
|
968341cc7d | ||
|
|
fbb0cfd86a | ||
|
|
49e33a03df | ||
|
|
295b75e5ff | ||
|
|
090d1ff555 | ||
|
|
5dde3d8570 | ||
|
|
d64740d347 | ||
|
|
f68936ef2e | ||
|
|
4607c883c5 | ||
|
|
7ff3daa446 | ||
|
|
1fe06dd0e7 | ||
|
|
b381ab1d8d | ||
|
|
05fbfc6995 | ||
|
|
fba240c5b4 | ||
|
|
965699ba0f | ||
|
|
c65ac2d90d | ||
|
|
05fb576772 | ||
|
|
4be6b1f3d7 | ||
|
|
65decb5731 | ||
|
|
90559a6143 | ||
|
|
dbde5b3681 | ||
|
|
b1e50eea92 | ||
|
|
9e34c9bb39 | ||
|
|
324cdbca40 | ||
|
|
b5290d4e0b | ||
|
|
3db9538748 | ||
|
|
1ab89e71fa | ||
|
|
667d9fd4df | ||
|
|
41e61c45d9 | ||
|
|
869df10064 | ||
|
|
6feee4ab35 | ||
|
|
d0c1a80617 | ||
|
|
383c428451 | ||
|
|
5f8416e541 | ||
|
|
5bf5cb9ff6 | ||
|
|
3a15d5a640 | ||
|
|
1dfd11acc0 | ||
|
|
de2b49b074 | ||
|
|
c5d846735c | ||
|
|
6274754e66 | ||
|
|
7a50cd0f01 | ||
|
|
074dfc0f88 | ||
|
|
92423287cc | ||
|
|
1a0b6a7a44 | ||
|
|
8fcfc0b803 | ||
|
|
28d2fed463 | ||
|
|
83072c0232 | ||
|
|
40109aa45f | ||
|
|
32aadc9902 | ||
|
|
3ab4256958 | ||
|
|
88a49df297 | ||
|
|
5d17c29eb2 | ||
|
|
64b9e4cd16 | ||
|
|
4b71d0d1af | ||
|
|
002cfcde85 | ||
|
|
d8af7812b5 | ||
|
|
f042ddb5c9 | ||
|
|
8e94ed15e6 | ||
|
|
7a82aeeeba | ||
|
|
24837f9260 | ||
|
|
5805df0205 | ||
|
|
fb20f009f7 | ||
|
|
6ceb0aba82 | ||
|
|
2d7b8998c4 | ||
|
|
cabd410a1a | ||
|
|
a58af379e1 | ||
|
|
1b3fa65759 | ||
|
|
cf01923519 | ||
|
|
a0d7f0dbd3 | ||
|
|
0c4e7478e2 | ||
|
|
60ce3fbc96 | ||
|
|
7902b52714 | ||
|
|
7196200fc2 | ||
|
|
f42fa0b8e1 | ||
|
|
b719b10257 | ||
|
|
ab55d75cf5 | ||
|
|
324cc5d30f | ||
|
|
44a9ffa0ad | ||
|
|
ba43ae0bd2 | ||
|
|
99b647cfca | ||
|
|
f90dc28f1e | ||
|
|
26536d1145 | ||
|
|
c5e733becc | ||
|
|
7227402d94 | ||
|
|
83f6ca4a73 | ||
|
|
ad7912a846 | ||
|
|
afb5e143b1 | ||
|
|
b8a38fd22d | ||
|
|
0c29d6bac1 | ||
|
|
3eaf30278f | ||
|
|
d01f264bcc | ||
|
|
65dec14ac0 | ||
|
|
1f80c54b51 | ||
|
|
33573e20bc | ||
|
|
73452e316f | ||
|
|
bcd90be73a | ||
|
|
f62c68eedd | ||
|
|
946d1097b8 | ||
|
|
096e42b366 | ||
|
|
984ef9072c | ||
|
|
30c7951192 | ||
|
|
54135b0724 | ||
|
|
40707e17b8 | ||
|
|
edd71d77c7 | ||
|
|
9593373f9e | ||
|
|
5761e662f1 | ||
|
|
c7f3031f74 | ||
|
|
53cb00a818 | ||
|
|
a35c363ffc | ||
|
|
1cf3637198 | ||
|
|
fd3157bf35 | ||
|
|
dfb8f2155a | ||
|
|
2e506cbb10 | ||
|
|
7f02bc9704 | ||
|
|
8a6f7d849d | ||
|
|
1b2782ef64 | ||
|
|
a74040315e | ||
|
|
b889b2562c | ||
|
|
b24c7417e4 | ||
|
|
63125853e3 | ||
|
|
c599566439 | ||
|
|
fb19def3ce | ||
|
|
bccd4786f7 | ||
|
|
8992378c87 | ||
|
|
f90273c340 | ||
|
|
ca9636a1c3 | ||
|
|
ad47d2a2c1 | ||
|
|
a2a0fb73ea | ||
|
|
16d6c90a94 | ||
|
|
f7be714467 | ||
|
|
33bf62c4cb | ||
|
|
7e972d2f5c | ||
|
|
a0791d0e8a | ||
|
|
259d5e0f57 | ||
|
|
c365c3cc8a | ||
|
|
54c8eb7941 | ||
|
|
5ab44a4690 | ||
|
|
33e5c87957 | ||
|
|
674f8d2979 | ||
|
|
2794ebd599 | ||
|
|
76863b46b7 | ||
|
|
e7bcae9053 | ||
|
|
3577a17ce1 | ||
|
|
a8e2130643 | ||
|
|
4f1a67e06b | ||
|
|
dfe3c0c074 | ||
|
|
66ec7142ae | ||
|
|
ab8a2c0716 | ||
|
|
d11e73d6e6 | ||
|
|
36802176a7 | ||
|
|
3926ed6b24 | ||
|
|
787caf2fe4 | ||
|
|
4473f48847 | ||
|
|
7a4062a4a9 | ||
|
|
d914a3f97e | ||
|
|
68fe829c96 | ||
|
|
d60252954b | ||
|
|
6cd1f6f26d | ||
|
|
338e7a604c | ||
|
|
5dea9d881c | ||
|
|
4d67ef09c8 | ||
|
|
69a2c9fb6d | ||
|
|
333103d93f | ||
|
|
d36bdb0d84 | ||
|
|
5777558c77 | ||
|
|
52848fb798 | ||
|
|
bd43ca786b | ||
|
|
330a8e4e23 | ||
|
|
e0b44d6d3f | ||
|
|
d41cb083c3 | ||
|
|
0a8bb6e5b4 | ||
|
|
c777dd16df | ||
|
|
eea26c50dd | ||
|
|
1cf2c4efb3 | ||
|
|
c2b9c1474a | ||
|
|
598442d37d | ||
|
|
c964b80e53 | ||
|
|
0ca7be015e | ||
|
|
59fb099da2 | ||
|
|
27bf78d335 | ||
|
|
71b756be32 | ||
|
|
63a27bc9b6 | ||
|
|
5ed8f858cf | ||
|
|
6b99e0370c | ||
|
|
7d4bcd863a | ||
|
|
3134d55821 | ||
|
|
912c1ddf8a | ||
|
|
c97e8091a6 | ||
|
|
82bd8158f7 | ||
|
|
8945848025 | ||
|
|
b54897bcb8 | ||
|
|
cd560916f3 | ||
|
|
9a101a955b | ||
|
|
50fae20748 | ||
|
|
37533c2f55 | ||
|
|
217971d481 | ||
|
|
fce24d5f8d | ||
|
|
0e4f16f3bf | ||
|
|
6e35a78fd9 | ||
|
|
bf1a701820 |
+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
|
|
||||||
|
|||||||
@@ -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: "Report any security issues or vulnerabilities responsibly to the Docker security team. Do not use the public issue tracker."
|
||||||
url: "https://github.com/moby/moby/security/policy"
|
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/"
|
||||||
|
|||||||
@@ -1,37 +1,34 @@
|
|||||||
<!--
|
<!--
|
||||||
Please make sure you've read and understood our contributing guidelines;
|
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
|
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.
|
It must be placed inside the below triple backticks section:
|
||||||
|
|
||||||
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
|
```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/*"
|
|
||||||
|
|||||||
+61
-74
@@ -29,23 +29,13 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
prepare:
|
prepare:
|
||||||
runs-on: ubuntu-26.04
|
runs-on: ubuntu-24.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@v4
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
-
|
-
|
||||||
name: Create matrix
|
name: Create matrix
|
||||||
id: platforms
|
id: platforms
|
||||||
@@ -57,7 +47,7 @@ jobs:
|
|||||||
echo ${{ steps.platforms.outputs.matrix }}
|
echo ${{ steps.platforms.outputs.matrix }}
|
||||||
|
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-26.04
|
runs-on: ubuntu-24.04
|
||||||
needs:
|
needs:
|
||||||
- prepare
|
- prepare
|
||||||
strategy:
|
strategy:
|
||||||
@@ -71,20 +61,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@v4
|
||||||
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@v3
|
||||||
-
|
-
|
||||||
name: Build
|
name: Build
|
||||||
uses: docker/bake-action@018cb6412ab401ebaa809aa5f85966b74628600f # v7.4.0
|
uses: docker/bake-action@v5
|
||||||
with:
|
with:
|
||||||
targets: ${{ matrix.target }}
|
targets: ${{ matrix.target }}
|
||||||
set: |
|
set: |
|
||||||
@@ -96,69 +83,74 @@ 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:
|
bin-image:
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
if: ${{ github.event_name != 'pull_request' && github.repository == 'docker/cli' }}
|
if: ${{ github.event_name != 'pull_request' && github.repository == 'docker/cli' }}
|
||||||
uses: docker/github-builder/.github/workflows/bake.yml@58cb9f5b71b1836d6f690c1e95effdeb9b98cb8a # v1.17.0
|
steps:
|
||||||
permissions:
|
-
|
||||||
contents: read # same as global permission
|
name: Checkout
|
||||||
id-token: write # for signing attestation(s) and authenticating to Docker Hub with GitHub OIDC Token
|
uses: actions/checkout@v4
|
||||||
with:
|
-
|
||||||
setup-qemu: true
|
name: Set up QEMU
|
||||||
target: bin-image-cross
|
uses: docker/setup-qemu-action@v3
|
||||||
cache: true
|
-
|
||||||
cache-scope: bin-image
|
name: Set up Docker Buildx
|
||||||
output: image
|
uses: docker/setup-buildx-action@v3
|
||||||
push: true
|
-
|
||||||
vars: |
|
name: Docker meta
|
||||||
VERSION=${{ github.ref }}
|
id: meta
|
||||||
meta-images: |
|
uses: docker/metadata-action@v5
|
||||||
dockereng/cli-bin
|
with:
|
||||||
meta-tags: |
|
images: dockereng/cli-bin
|
||||||
type=semver,pattern={{version}}
|
tags: |
|
||||||
type=ref,event=branch
|
type=semver,pattern={{version}}
|
||||||
type=ref,event=pr
|
type=ref,event=branch
|
||||||
type=semver,pattern={{major}}
|
type=ref,event=pr
|
||||||
type=semver,pattern={{major}}.{{minor}}
|
type=sha
|
||||||
registry-identities: |
|
-
|
||||||
- type: dockerhub
|
name: Login to DockerHub
|
||||||
username: dockereng
|
if: github.event_name != 'pull_request'
|
||||||
connection_id: ${{ vars.DOCKERHUB_OIDC_CONNECTIONID }}
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_CLIBIN_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_CLIBIN_TOKEN }}
|
||||||
|
-
|
||||||
|
name: Build and push image
|
||||||
|
uses: docker/bake-action@v5
|
||||||
|
with:
|
||||||
|
files: |
|
||||||
|
./docker-bake.hcl
|
||||||
|
${{ steps.meta.outputs.bake-file }}
|
||||||
|
targets: bin-image-cross
|
||||||
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
|
set: |
|
||||||
|
*.cache-from=type=gha,scope=bin-image
|
||||||
|
*.cache-to=type=gha,scope=bin-image,mode=max
|
||||||
|
|
||||||
prepare-plugins:
|
prepare-plugins:
|
||||||
runs-on: ubuntu-26.04
|
runs-on: ubuntu-24.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@v4
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
-
|
-
|
||||||
name: Create matrix
|
name: Create matrix
|
||||||
id: platforms
|
id: platforms
|
||||||
@@ -170,7 +162,7 @@ jobs:
|
|||||||
echo ${{ steps.platforms.outputs.matrix }}
|
echo ${{ steps.platforms.outputs.matrix }}
|
||||||
|
|
||||||
plugins:
|
plugins:
|
||||||
runs-on: ubuntu-26.04
|
runs-on: ubuntu-24.04
|
||||||
needs:
|
needs:
|
||||||
- prepare-plugins
|
- prepare-plugins
|
||||||
strategy:
|
strategy:
|
||||||
@@ -178,20 +170,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@v4
|
||||||
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@v3
|
||||||
-
|
-
|
||||||
name: Build
|
name: Build
|
||||||
uses: docker/bake-action@018cb6412ab401ebaa809aa5f85966b74628600f # v7.4.0
|
uses: docker/bake-action@v5
|
||||||
with:
|
with:
|
||||||
targets: plugins-cross
|
targets: plugins-cross
|
||||||
set: |
|
set: |
|
||||||
|
|||||||
@@ -11,15 +11,15 @@ permissions:
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- 'master'
|
- 'master'
|
||||||
- '[0-9]+.[0-9]+'
|
- '[0-9]+.[0-9]+'
|
||||||
- '[0-9]+.x'
|
- '[0-9]+.x'
|
||||||
tags:
|
tags:
|
||||||
- 'v*'
|
- 'v*'
|
||||||
pull_request:
|
pull_request:
|
||||||
# The branches below must be a subset of the branches above
|
# The branches below must be a subset of the branches above
|
||||||
branches: ["master"]
|
branches: [ "master" ]
|
||||||
schedule:
|
schedule:
|
||||||
# ┌───────────── minute (0 - 59)
|
# ┌───────────── minute (0 - 59)
|
||||||
# │ ┌───────────── hour (0 - 23)
|
# │ ┌───────────── hour (0 - 23)
|
||||||
@@ -34,22 +34,26 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
codeql:
|
codeql:
|
||||||
runs-on: ubuntu-26.04
|
runs-on: 'ubuntu-latest'
|
||||||
timeout-minutes: 10
|
timeout-minutes: 360
|
||||||
env:
|
env:
|
||||||
DISABLE_WARN_OUTSIDE_CONTAINER: '1'
|
DISABLE_WARN_OUTSIDE_CONTAINER: '1'
|
||||||
permissions:
|
permissions:
|
||||||
actions: read
|
actions: read
|
||||||
contents: read
|
contents: read
|
||||||
security-events: write
|
security-events: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
-
|
-
|
||||||
name: Checkout
|
name: Checkout
|
||||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
persist-credentials: false
|
-
|
||||||
|
name: Checkout HEAD on PR
|
||||||
|
if: ${{ github.event_name == 'pull_request' }}
|
||||||
|
run: |
|
||||||
|
git checkout HEAD^2
|
||||||
# CodeQL 2.16.4's auto-build added support for multi-module repositories,
|
# 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,
|
# 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
|
# including vendor directories. If no module is found, it's creating one
|
||||||
@@ -62,20 +66,19 @@ jobs:
|
|||||||
ln -s vendor.sum go.sum
|
ln -s vendor.sum go.sum
|
||||||
-
|
-
|
||||||
name: Update Go
|
name: Update Go
|
||||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: "1.26.8"
|
go-version: "1.22.12"
|
||||||
cache: false
|
|
||||||
-
|
-
|
||||||
name: Initialize CodeQL
|
name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
|
uses: github/codeql-action/init@v3
|
||||||
with:
|
with:
|
||||||
languages: go
|
languages: go
|
||||||
-
|
-
|
||||||
name: Autobuild
|
name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
|
uses: github/codeql-action/autobuild@v3
|
||||||
-
|
-
|
||||||
name: Perform CodeQL Analysis
|
name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
|
uses: github/codeql-action/analyze@v3
|
||||||
with:
|
with:
|
||||||
category: "/language:go"
|
category: "/language:go"
|
||||||
|
|||||||
+14
-22
@@ -25,41 +25,33 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
tests:
|
e2e:
|
||||||
runs-on: ubuntu-26.04
|
runs-on: ubuntu-24.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
|
- debian
|
||||||
engine-version:
|
engine-version:
|
||||||
- rc # latest rc
|
- 27.0 # latest
|
||||||
- 29 # latest
|
- 26.1 # latest - 1
|
||||||
- 28 # latest - 1
|
- 23.0 # mirantis lts
|
||||||
- 25 # mirantis lts
|
# TODO(krissetto) 19.03 needs a look, doesn't work ubuntu 22.04 (cgroup errors).
|
||||||
|
# we could have a separate job that tests it against ubuntu 20.04
|
||||||
steps:
|
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@v4
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
-
|
-
|
||||||
name: Update daemon.json
|
name: Update daemon.json
|
||||||
run: |
|
run: |
|
||||||
if [ ! -f /etc/docker/daemon.json ]; then
|
if [ ! -f /etc/docker/daemon.json ]; then
|
||||||
# ubuntu runners no longer have a default daemon.json present
|
# ubuntu 24.04 runners no longer have a default daemon.json present
|
||||||
sudo mkdir -p /etc/docker/
|
sudo mkdir -p /etc/docker/
|
||||||
echo '{"experimental": true}' | sudo tee /etc/docker/daemon.json
|
echo '{"experimental": true}' | sudo tee /etc/docker/daemon.json
|
||||||
else
|
else
|
||||||
@@ -73,7 +65,7 @@ jobs:
|
|||||||
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@v3
|
||||||
-
|
-
|
||||||
name: Run ${{ matrix.target }}
|
name: Run ${{ matrix.target }}
|
||||||
run: |
|
run: |
|
||||||
@@ -84,7 +76,7 @@ jobs:
|
|||||||
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@v4
|
||||||
with:
|
with:
|
||||||
files: ./build/coverage/coverage.txt
|
file: ./build/coverage/coverage.txt
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
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"
|
|
||||||
+22
-35
@@ -26,29 +26,24 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
ctn:
|
ctn:
|
||||||
runs-on: ubuntu-26.04
|
runs-on: ubuntu-24.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@v4
|
||||||
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@v3
|
||||||
-
|
-
|
||||||
name: Test
|
name: Test
|
||||||
uses: docker/bake-action@018cb6412ab401ebaa809aa5f85966b74628600f # v7.4.0
|
uses: docker/bake-action@v5
|
||||||
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@v4
|
||||||
with:
|
with:
|
||||||
files: ./build/coverage/coverage.txt
|
file: ./build/coverage/coverage.txt
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
|
||||||
host:
|
host:
|
||||||
@@ -61,45 +56,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@v4
|
||||||
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@v5
|
||||||
with:
|
with:
|
||||||
go-version: "1.26.8"
|
go-version: "1.22.12"
|
||||||
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@v4
|
||||||
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 }}
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
|||||||
@@ -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} ✓`);
|
|
||||||
@@ -11,50 +11,27 @@ permissions:
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
types: [opened, edited, labeled, unlabeled, synchronize]
|
types: [opened, edited, labeled, unlabeled]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check-labels:
|
check-area-label:
|
||||||
runs-on: ubuntu-26.04
|
runs-on: ubuntu-20.04
|
||||||
timeout-minutes: 120 # guardrails timeout for the whole job
|
|
||||||
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: Missing `area/` label
|
- name: Missing `area/` label
|
||||||
if: always() && contains(join(github.event.pull_request.labels.*.name, ','), 'impact/') && !contains(join(github.event.pull_request.labels.*.name, ','), 'area/')
|
if: contains(join(github.event.pull_request.labels.*.name, ','), 'impact/') && !contains(join(github.event.pull_request.labels.*.name, ','), 'area/')
|
||||||
run: |
|
run: |
|
||||||
echo "::error::Every PR with an 'impact/*' label should also have an 'area/*' label"
|
echo "::error::Every PR with an 'impact/*' label should also have an 'area/*' label"
|
||||||
exit 1
|
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
|
- name: OK
|
||||||
run: exit 0
|
run: exit 0
|
||||||
|
|
||||||
check-changelog:
|
check-changelog:
|
||||||
runs-on: ubuntu-26.04
|
if: contains(join(github.event.pull_request.labels.*.name, ','), 'impact/')
|
||||||
timeout-minutes: 120 # guardrails timeout for the whole job
|
runs-on: ubuntu-20.04
|
||||||
env:
|
env:
|
||||||
HAS_IMPACT_LABEL: ${{ contains(join(github.event.pull_request.labels.*.name, ','), 'impact/') }}
|
|
||||||
PR_BODY: |
|
PR_BODY: |
|
||||||
${{ github.event.pull_request.body }}
|
${{ github.event.pull_request.body }}
|
||||||
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: Check changelog description
|
- name: Check changelog description
|
||||||
run: |
|
run: |
|
||||||
# Extract the `markdown changelog` note code block
|
# Extract the `markdown changelog` note code block
|
||||||
@@ -63,42 +40,25 @@ jobs:
|
|||||||
# Strip empty lines
|
# Strip empty lines
|
||||||
desc=$(echo "$block" | awk NF)
|
desc=$(echo "$block" | awk NF)
|
||||||
|
|
||||||
if [ "$HAS_IMPACT_LABEL" = "true" ]; then
|
if [ -z "$desc" ]; then
|
||||||
if [ -z "$desc" ]; then
|
echo "::error::Changelog section is empty. Provide a description for the changelog."
|
||||||
echo "::error::Changelog section is empty. Please provide a description for the changelog."
|
exit 1
|
||||||
exit 1
|
fi
|
||||||
fi
|
|
||||||
|
|
||||||
len=$(echo -n "$desc" | wc -c)
|
len=$(echo -n "$desc" | wc -c)
|
||||||
if [[ $len -le 6 ]]; then
|
if [[ $len -le 6 ]]; then
|
||||||
echo "::error::Description looks too short: $desc"
|
echo "::error::Description looks too short: $desc"
|
||||||
exit 1
|
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
|
fi
|
||||||
|
|
||||||
echo "This PR will be included in the release notes with the following note:"
|
echo "This PR will be included in the release notes with the following note:"
|
||||||
echo "$desc"
|
echo "$desc"
|
||||||
|
|
||||||
check-pr-branch:
|
check-pr-branch:
|
||||||
runs-on: ubuntu-26.04
|
runs-on: ubuntu-20.04
|
||||||
timeout-minutes: 120 # guardrails timeout for the whole job
|
|
||||||
env:
|
env:
|
||||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||||
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
|
|
||||||
# Backports or PR that target a release branch directly should mention the target branch in the title, for example:
|
# 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 backport] Some change that needs backporting to X.Y
|
||||||
# [X.Y] Change directly targeting the X.Y branch
|
# [X.Y] Change directly targeting the X.Y branch
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
validate:
|
validate:
|
||||||
runs-on: ubuntu-26.04
|
runs-on: ubuntu-24.04
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -36,37 +36,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@v4
|
||||||
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@v5
|
||||||
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-24.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@v4
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
-
|
-
|
||||||
name: Generate
|
name: Generate
|
||||||
shell: 'script --return --quiet --command "bash {0}"'
|
shell: 'script --return --quiet --command "bash {0}"'
|
||||||
@@ -82,7 +67,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
validate-make:
|
validate-make:
|
||||||
runs-on: ubuntu-26.04
|
runs-on: ubuntu-24.04
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -90,54 +75,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@v4
|
||||||
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
|
|
||||||
+2
-2
@@ -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
|
||||||
|
|||||||
+140
-201
@@ -1,237 +1,176 @@
|
|||||||
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.
|
- copyloopvar # Detects places where loop variables are copied.
|
||||||
- depguard
|
- depguard
|
||||||
- dogsled # Detects assignments with too many blank identifiers.
|
- dogsled
|
||||||
- dupword # Detects duplicate words.
|
- dupword # Detects duplicate words.
|
||||||
- durationcheck # Detect cases where two time.Duration values are being multiplied in possibly erroneous ways.
|
- durationcheck
|
||||||
- errcheck
|
- errchkjson
|
||||||
- errchkjson # Detects unsupported types passed to json encoding functions and reports if checks for the returned error can be omitted.
|
- gocritic # Metalinter; detects bugs, performance, and styling issues.
|
||||||
- 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 # Detects whether code was gofumpt-ed.
|
||||||
|
- goimports
|
||||||
|
- gosec # Detects security problems.
|
||||||
|
- 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 # Detects commonly misspelled English words in comments.
|
||||||
- nakedret # Detects uses of naked returns.
|
- nakedret
|
||||||
- nilnesserr # Detects returning nil errors. It combines the features of nilness and nilerr,
|
- nilerr # Detects code that returns nil even if it checks that the error is not nil.
|
||||||
- nosprintfhostport # Detects misuse of Sprintf to construct a host with port in a URL.
|
- nolintlint # Detects ill-formed or insufficient nolint directives.
|
||||||
- nolintlint # Detects ill-formed or insufficient nolint directives.
|
- perfsprint # Detects fmt.Sprintf uses that can be replaced with a faster alternative.
|
||||||
- perfsprint # Detects fmt.Sprintf uses that can be replaced with a faster alternative.
|
- prealloc # Detects slice declarations that could potentially be pre-allocated.
|
||||||
- prealloc # Detects slice declarations that could potentially be pre-allocated.
|
- predeclared # Detects code that shadows one of Go's predeclared identifiers
|
||||||
- predeclared # Detects code that shadows one of Go's predeclared identifiers
|
- reassign
|
||||||
- reassign # Detects reassigning a top-level variable in another package.
|
- revive # Metalinter; drop-in replacement for golint.
|
||||||
- revive # Metalinter; drop-in replacement for golint.
|
|
||||||
- spancheck # Detects mistakes with OpenTelemetry/Census spans.
|
|
||||||
- staticcheck
|
- staticcheck
|
||||||
- thelper # Detects test helpers without t.Helper().
|
- stylecheck # Replacement for golint
|
||||||
- tparallel # Detects inappropriate usage of t.Parallel().
|
- tenv # Detects using os.Setenv instead of t.Setenv.
|
||||||
- unconvert # Detects unnecessary type conversions.
|
- thelper # Detects test helpers without t.Helper().
|
||||||
|
- tparallel # Detects inappropriate usage of t.Parallel().
|
||||||
|
- typecheck
|
||||||
|
- unconvert # Detects unnecessary type conversions.
|
||||||
- unparam
|
- unparam
|
||||||
- unused
|
- unused
|
||||||
- usestdlibvars # Detects the possibility to use variables/constants from the Go standard library.
|
- usestdlibvars
|
||||||
- usetesting # Reports uses of functions with replacement inside the testing package.
|
- vet
|
||||||
- wastedassign # Detects wasted assignment statements.
|
- wastedassign
|
||||||
|
|
||||||
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
|
||||||
|
gosec:
|
||||||
|
excludes:
|
||||||
|
- G104 # G104: Errors unhandled; (TODO: reduce unhandled errors, or explicitly ignore)
|
||||||
|
- G113 # G113: Potential uncontrolled memory consumption in Rat.SetString (CVE-2022-23772); (only affects go < 1.16.14. and go < 1.17.7)
|
||||||
|
- G115 # G115: integer overflow conversion; (TODO: verify these: https://github.com/docker/cli/issues/5584)
|
||||||
|
- G306 # G306: Expect WriteFile permissions to be 0600 or less (too restrictive; also flags "0o644" permissions)
|
||||||
|
- G307 # G307: Deferring unsafe method "*os.File" on type "Close" (also EXC0008); (TODO: evaluate these and fix where needed: G307: Deferring unsafe method "*os.File" on type "Close")
|
||||||
|
govet:
|
||||||
|
enable:
|
||||||
|
- shadow
|
||||||
|
settings:
|
||||||
|
shadow:
|
||||||
|
strict: true
|
||||||
|
lll:
|
||||||
|
line-length: 200
|
||||||
|
nakedret:
|
||||||
|
command: nakedret
|
||||||
|
pattern: ^(?P<path>.*?\\.go):(?P<line>\\d+)\\s*(?P<message>.*)$
|
||||||
|
|
||||||
gocyclo:
|
revive:
|
||||||
min-complexity: 16
|
rules:
|
||||||
|
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#import-shadowing
|
||||||
|
- name: import-shadowing
|
||||||
|
severity: warning
|
||||||
|
disabled: false
|
||||||
|
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#empty-block
|
||||||
|
- name: empty-block
|
||||||
|
severity: warning
|
||||||
|
disabled: false
|
||||||
|
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#empty-lines
|
||||||
|
- name: empty-lines
|
||||||
|
severity: warning
|
||||||
|
disabled: false
|
||||||
|
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#use-any
|
||||||
|
- name: use-any
|
||||||
|
severity: warning
|
||||||
|
disabled: false
|
||||||
|
|
||||||
gosec:
|
issues:
|
||||||
excludes:
|
# The default exclusion rules are a bit too permissive, so copying the relevant ones below
|
||||||
- G104 # G104: Errors unhandled; (TODO: reduce unhandled errors, or explicitly ignore)
|
exclude-use-default: false
|
||||||
- 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:
|
# This option has been defined when Go modules was not existed and when the
|
||||||
enable:
|
# golangci-lint core was different, this is not something we still recommend.
|
||||||
- shadow
|
exclude-dirs-use-default: false
|
||||||
settings:
|
|
||||||
shadow:
|
|
||||||
strict: true
|
|
||||||
|
|
||||||
lll:
|
exclude:
|
||||||
line-length: 200
|
- parameter .* always receives
|
||||||
|
|
||||||
importas:
|
exclude-files:
|
||||||
# Do not allow unaliased imports of aliased packages.
|
- cli/compose/schema/bindata.go
|
||||||
no-unaliased: true
|
- .*generated.*
|
||||||
|
|
||||||
alias:
|
exclude-rules:
|
||||||
# Should no longer be aliased, because we no longer allow moby/docker errdefs.
|
# We prefer to use an "exclude-list" so that new "default" exclusions are not
|
||||||
- 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:
|
# The default list of exclusions can be found at:
|
||||||
# https://golangci-lint.run/usage/false-positives/#default-exclusions
|
# 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
|
||||||
|
# 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
|
# TODO: make sure all packages have a description. Currently, there's 67 packages without.
|
||||||
- text: "Subprocess launch(ed with variable|ing should be audited)"
|
- 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
|
||||||
|
- text: "ST1000: at least one file in a package should have a package comment"
|
||||||
|
linters:
|
||||||
|
- stylecheck
|
||||||
|
|
||||||
# EXC0009
|
# Allow "err" and "ok" vars to shadow existing declarations, otherwise we get too many false positives.
|
||||||
- text: "(Expect directory permissions to be 0750 or less|Expect file permissions to be 0600 or less)"
|
- text: '^shadow: declaration of "(err|ok)" shadows declaration'
|
||||||
linters:
|
linters:
|
||||||
- gosec
|
- govet
|
||||||
|
|
||||||
# EXC0010
|
|
||||||
- text: "Potential file inclusion via variable"
|
|
||||||
linters:
|
|
||||||
- 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
|
|
||||||
|
|||||||
@@ -39,12 +39,10 @@ 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 +63,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>
|
||||||
@@ -154,8 +146,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>
|
||||||
David Karlsson <david.karlsson@docker.com> <35727626+dvdksn@users.noreply.github.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>
|
||||||
@@ -205,8 +195,6 @@ 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>
|
||||||
@@ -226,7 +214,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>
|
||||||
@@ -343,8 +330,6 @@ 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 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 +339,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>
|
||||||
@@ -405,7 +387,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 +396,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,7 +419,6 @@ 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>
|
||||||
@@ -519,7 +498,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>
|
||||||
@@ -575,8 +553,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,7 +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>
|
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>
|
||||||
@@ -43,15 +42,12 @@ 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>
|
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,8 +62,6 @@ 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>
|
||||||
@@ -87,16 +81,13 @@ 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>
|
Ashly Mathew <ashly.mathew@sap.com>
|
||||||
Ashwini Oruganti <ashwini.oruganti@gmail.com>
|
Ashwini Oruganti <ashwini.oruganti@gmail.com>
|
||||||
Aslam Ahemad <aslamahemad@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>
|
||||||
@@ -131,7 +122,6 @@ Brian Goff <cpuguy83@gmail.com>
|
|||||||
Brian Tracy <brian.tracy33@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>
|
||||||
@@ -142,12 +132,9 @@ 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>
|
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,7 +149,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 Chinchilla <chris@chrischinchilla.com>
|
||||||
Chris Couzens <ccouzens@gmail.com>
|
Chris Couzens <ccouzens@gmail.com>
|
||||||
Chris Gavin <chris@chrisgavin.me>
|
Chris Gavin <chris@chrisgavin.me>
|
||||||
@@ -184,7 +170,6 @@ 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>
|
||||||
@@ -204,7 +189,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>
|
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>
|
||||||
@@ -231,7 +215,7 @@ 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 le Blanc <systemmonkey42@users.noreply.github.com>
|
||||||
@@ -241,7 +225,6 @@ 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>
|
||||||
@@ -249,13 +232,11 @@ 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>
|
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,7 +250,6 @@ 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>
|
||||||
@@ -279,7 +259,6 @@ 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 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>
|
||||||
@@ -329,8 +308,6 @@ George MacRorie <gmacr31@gmail.com>
|
|||||||
George Margaritis <gmargaritis@protonmail.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>
|
||||||
@@ -360,7 +337,6 @@ 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 Chastel <Hugo-C@users.noreply.github.com>
|
||||||
@@ -368,7 +344,6 @@ 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>
|
||||||
@@ -418,7 +393,6 @@ 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 Chen <njucjc@gmail.com>
|
||||||
@@ -472,7 +446,6 @@ 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 Chadwell <me@jedevc.com>
|
||||||
Justin Cormack <justin.cormack@docker.com>
|
Justin Cormack <justin.cormack@docker.com>
|
||||||
@@ -481,7 +454,6 @@ 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>
|
||||||
@@ -518,22 +490,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 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 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>
|
||||||
Lihua Tang <lhtang@alauda.io>
|
Lihua Tang <lhtang@alauda.io>
|
||||||
Lily Guo <lily.guo@docker.com>
|
Lily Guo <lily.guo@docker.com>
|
||||||
@@ -546,17 +515,13 @@ 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>
|
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 +530,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>
|
||||||
@@ -593,19 +557,15 @@ 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>
|
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>
|
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 +578,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 +593,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>
|
||||||
@@ -680,11 +633,9 @@ 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>
|
Oded Arbel <oded@geek.co.il>
|
||||||
@@ -697,18 +648,15 @@ 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 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 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>
|
||||||
@@ -724,13 +672,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>
|
||||||
@@ -749,7 +697,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>
|
||||||
@@ -791,7 +738,6 @@ 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>
|
Saurabh Kumar <saurabhkumar0184@gmail.com>
|
||||||
@@ -800,7 +746,6 @@ 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 +770,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 +780,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>
|
||||||
@@ -901,10 +844,8 @@ Umesh Yadav <umesh4257@gmail.com>
|
|||||||
Vaclav Struhar <struharv@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>
|
||||||
@@ -914,7 +855,6 @@ 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 +867,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,7 +882,6 @@ 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>
|
||||||
@@ -971,4 +908,3 @@ Zhuo Zhi <h.dwwwwww@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>
|
|
||||||
|
|||||||
+7
-42
@@ -66,7 +66,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
|
||||||
|
|
||||||
@@ -124,7 +124,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 +134,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
|
||||||
@@ -267,7 +232,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`.
|
||||||
|
|||||||
+15
-32
@@ -1,43 +1,22 @@
|
|||||||
# syntax=docker/dockerfile:1
|
# syntax=docker/dockerfile:1
|
||||||
|
|
||||||
ARG BASE_VARIANT=alpine
|
ARG BASE_VARIANT=alpine
|
||||||
|
ARG ALPINE_VERSION=3.20
|
||||||
# ALPINE_VERSION sets the version of the alpine base image to use, including for the golang image.
|
|
||||||
# It must be a supported tag in the docker.io/library/alpine image repository
|
|
||||||
# that's also available as alpine image variant for the Golang version used.
|
|
||||||
ARG ALPINE_VERSION=3.23
|
|
||||||
ARG BASE_DEBIAN_DISTRO=bookworm
|
ARG BASE_DEBIAN_DISTRO=bookworm
|
||||||
|
|
||||||
ARG GO_VERSION=1.26.8
|
ARG GO_VERSION=1.22.12
|
||||||
|
ARG XX_VERSION=1.6.1
|
||||||
# XX_VERSION specifies the version of the xx utility to use.
|
ARG GOVERSIONINFO_VERSION=v1.3.0
|
||||||
# It must be a valid tag in the docker.io/tonistiigi/xx image repository.
|
ARG GOTESTSUM_VERSION=v1.10.0
|
||||||
ARG XX_VERSION=1.9.0
|
ARG BUILDX_VERSION=0.20.0
|
||||||
|
ARG COMPOSE_VERSION=v2.32.4
|
||||||
# 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
|
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
|
||||||
@@ -84,7 +63,7 @@ ARG PACKAGER_NAME
|
|||||||
COPY --link --from=goversioninfo /out/goversioninfo /usr/bin/goversioninfo
|
COPY --link --from=goversioninfo /out/goversioninfo /usr/bin/goversioninfo
|
||||||
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=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 +76,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
|
||||||
@@ -122,6 +101,10 @@ 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,7 +113,7 @@ 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 . .
|
||||||
|
|||||||
@@ -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.21 . ; \
|
||||||
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,46 @@ 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
|
.PHONY: completion
|
||||||
completion: shell-completion
|
completion: binary
|
||||||
completion: ## generate and install the shell-completion scripts
|
completion: /etc/bash_completion.d/docker
|
||||||
# Note: this uses system-wide paths, and so may overwrite completion
|
completion: ## generate and install the completion scripts
|
||||||
# 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
|
|
||||||
|
|
||||||
|
.PHONY: /etc/bash_completion.d/docker
|
||||||
build/docker:
|
/etc/bash_completion.d/docker: ## generate and install the bash-completion script
|
||||||
# This target is used by the "shell-completion" target, which requires either
|
mkdir -p /etc/bash_completion.d
|
||||||
# "binary" or "dynbinary" to have been built. We don't want to trigger those
|
docker completion bash > /etc/bash_completion.d/docker
|
||||||
# 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
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
# 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
|
||||||
|
|
||||||
@@ -51,18 +50,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,7 +61,7 @@ make help
|
|||||||
Start an interactive development environment:
|
Start an interactive development environment:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
make shell
|
make -f docker.Makefile shell
|
||||||
```
|
```
|
||||||
|
|
||||||
## Legal
|
## Legal
|
||||||
|
|||||||
-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,7 +38,7 @@ 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
|
||||||
},
|
},
|
||||||
@@ -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 optContext {
|
||||||
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()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,7 +97,7 @@ func main() {
|
|||||||
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 +1,18 @@
|
|||||||
package hooks
|
package hooks
|
||||||
|
|
||||||
import "io"
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
const (
|
"github.com/morikuni/aec"
|
||||||
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) {
|
func PrintNextSteps(out io.Writer, messages []string) {
|
||||||
if len(messages) == 0 {
|
if len(messages) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
fmt.Fprintln(out, aec.Bold.Apply("\nWhat's next:"))
|
||||||
_, _ = io.WriteString(out, whatsNext)
|
for _, n := range messages {
|
||||||
for _, msg := range messages {
|
_, _ = fmt.Fprintf(out, " %s\n", n)
|
||||||
_, _ = io.WriteString(out, indent)
|
|
||||||
_, _ = io.WriteString(out, msg)
|
|
||||||
_, _ = io.WriteString(out, "\n")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,45 +1,38 @@
|
|||||||
package hooks_test
|
package hooks
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
"bytes"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/docker/cli/cli-plugins/hooks"
|
"github.com/morikuni/aec"
|
||||||
"gotest.tools/v3/assert"
|
"gotest.tools/v3/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPrintHookMessages(t *testing.T) {
|
func TestPrintHookMessages(t *testing.T) {
|
||||||
const header = "\n\x1b[1mWhat's next:\x1b[0m\n"
|
testCases := []struct {
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
doc string
|
|
||||||
messages []string
|
messages []string
|
||||||
expectedOutput string
|
expectedOutput string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
doc: "no messages",
|
messages: []string{},
|
||||||
messages: nil,
|
|
||||||
expectedOutput: "",
|
expectedOutput: "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
doc: "single message",
|
|
||||||
messages: []string{"Bork!"},
|
messages: []string{"Bork!"},
|
||||||
expectedOutput: header +
|
expectedOutput: aec.Bold.Apply("\nWhat's next:") + "\n" +
|
||||||
" Bork!\n",
|
" Bork!\n",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
doc: "multiple messages",
|
|
||||||
messages: []string{"Foo", "bar"},
|
messages: []string{"Foo", "bar"},
|
||||||
expectedOutput: header +
|
expectedOutput: aec.Bold.Apply("\nWhat's next:") + "\n" +
|
||||||
" Foo\n" +
|
" Foo\n" +
|
||||||
" bar\n",
|
" bar\n",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tc := range tests {
|
|
||||||
t.Run(tc.doc, func(t *testing.T) {
|
for _, tc := range testCases {
|
||||||
var w strings.Builder
|
w := bytes.Buffer{}
|
||||||
hooks.PrintNextSteps(&w, tc.messages)
|
PrintNextSteps(&w, tc.messages)
|
||||||
assert.Equal(t, w.String(), tc.expectedOutput)
|
assert.Equal(t, w.String(), tc.expectedOutput)
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,95 +1,116 @@
|
|||||||
// 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
|
package hooks
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
const maxMessages = 10
|
type HookType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
NextSteps = iota
|
||||||
|
)
|
||||||
|
|
||||||
|
// HookMessage represents a plugin hook response. Plugins
|
||||||
|
// declaring support for CLI hooks need to print a json
|
||||||
|
// representation of this type when their hook subcommand
|
||||||
|
// is invoked.
|
||||||
|
type HookMessage struct {
|
||||||
|
Type HookType
|
||||||
|
Template string
|
||||||
|
}
|
||||||
|
|
||||||
|
// TemplateReplaceSubcommandName returns a hook template string
|
||||||
|
// that will be replaced by the CLI subcommand being executed
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// "you ran the subcommand: " + TemplateReplaceSubcommandName()
|
||||||
|
//
|
||||||
|
// when being executed after the command:
|
||||||
|
// `docker run --name "my-container" alpine`
|
||||||
|
// will result in the message:
|
||||||
|
// `you ran the subcommand: run`
|
||||||
|
func TemplateReplaceSubcommandName() string {
|
||||||
|
return hookTemplateCommandName
|
||||||
|
}
|
||||||
|
|
||||||
|
// TemplateReplaceFlagValue returns a hook template string
|
||||||
|
// that will be replaced by the flags value.
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// "you ran a container named: " + TemplateReplaceFlagValue("name")
|
||||||
|
//
|
||||||
|
// when being executed after the command:
|
||||||
|
// `docker run --name "my-container" alpine`
|
||||||
|
// will result in the message:
|
||||||
|
// `you ran a container named: my-container`
|
||||||
|
func TemplateReplaceFlagValue(flag string) string {
|
||||||
|
return fmt.Sprintf(hookTemplateFlagValue, flag)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TemplateReplaceArg takes an index i and returns a hook
|
||||||
|
// template string that the CLI will replace the template with
|
||||||
|
// the ith argument, after processing the passed flags.
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// "run this image with `docker run " + TemplateReplaceArg(0) + "`"
|
||||||
|
//
|
||||||
|
// when being executed after the command:
|
||||||
|
// `docker pull alpine`
|
||||||
|
// will result in the message:
|
||||||
|
// "Run this image with `docker run alpine`"
|
||||||
|
func TemplateReplaceArg(i int) string {
|
||||||
|
return fmt.Sprintf(hookTemplateArg, strconv.Itoa(i))
|
||||||
|
}
|
||||||
|
|
||||||
func ParseTemplate(hookTemplate string, cmd *cobra.Command) ([]string, error) {
|
func ParseTemplate(hookTemplate string, cmd *cobra.Command) ([]string, error) {
|
||||||
out := hookTemplate
|
tmpl := template.New("").Funcs(commandFunctions)
|
||||||
if strings.Contains(hookTemplate, "{{") {
|
tmpl, err := tmpl.Parse(hookTemplate)
|
||||||
// Message may be a template.
|
if err != nil {
|
||||||
msgContext := commandInfo{cmd: cmd}
|
return nil, err
|
||||||
|
|
||||||
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 {
|
b := bytes.Buffer{}
|
||||||
return nil, fmt.Errorf("hook template contains too many messages (%d): maximum is %d", n, maxMessages)
|
err = tmpl.Execute(&b, cmd)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
return strings.SplitN(out, "\n", maxMessages), nil
|
return strings.Split(b.String(), "\n"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var ErrHookTemplateParse = errors.New("failed to parse hook template")
|
var ErrHookTemplateParse = errors.New("failed to parse hook template")
|
||||||
|
|
||||||
// commandInfo provides info about the command for which the hook was invoked.
|
const (
|
||||||
// It is used for templated hook-messages.
|
hookTemplateCommandName = "{{.Name}}"
|
||||||
type commandInfo struct {
|
hookTemplateFlagValue = `{{flag . "%s"}}`
|
||||||
cmd *cobra.Command
|
hookTemplateArg = "{{arg . %s}}"
|
||||||
|
)
|
||||||
|
|
||||||
|
var commandFunctions = template.FuncMap{
|
||||||
|
"flag": getFlagValue,
|
||||||
|
"arg": getArgValue,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Name returns the name of the (sub)command for which the hook was invoked.
|
func getFlagValue(cmd *cobra.Command, flag string) (string, error) {
|
||||||
//
|
cmdFlag := cmd.Flag(flag)
|
||||||
// It's used for backward-compatibility with old templates.
|
if cmdFlag == nil {
|
||||||
func (c commandInfo) Name() string {
|
return "", ErrHookTemplateParse
|
||||||
return c.command()
|
}
|
||||||
|
return cmdFlag.Value.String(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// command returns the name of the (sub)command for which the hook was invoked.
|
func getArgValue(cmd *cobra.Command, i int) (string, error) {
|
||||||
func (c commandInfo) command() string {
|
flags := cmd.Flags()
|
||||||
if c.cmd == nil {
|
if flags == nil {
|
||||||
return ""
|
return "", ErrHookTemplateParse
|
||||||
}
|
}
|
||||||
return c.cmd.Name()
|
return flags.Arg(i), nil
|
||||||
}
|
|
||||||
|
|
||||||
// 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,67 +1,43 @@
|
|||||||
package hooks_test
|
package hooks
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/docker/cli/cli-plugins/hooks"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"gotest.tools/v3/assert"
|
"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) {
|
func TestParseTemplate(t *testing.T) {
|
||||||
type testFlag struct {
|
type testFlag struct {
|
||||||
name string
|
name string
|
||||||
value string
|
value string
|
||||||
}
|
}
|
||||||
tests := []struct {
|
testCases := []struct {
|
||||||
doc string
|
template string
|
||||||
template string // compatibility fixture; keep even if helpers emit a newer form
|
|
||||||
templateFunc func() string
|
|
||||||
flags []testFlag
|
flags []testFlag
|
||||||
args []string
|
args []string
|
||||||
expectedOutput []string
|
expectedOutput []string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
doc: "empty template",
|
|
||||||
template: "",
|
template: "",
|
||||||
expectedOutput: []string{""},
|
expectedOutput: []string{""},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
doc: "plain message",
|
|
||||||
template: "a plain template message",
|
template: "a plain template message",
|
||||||
expectedOutput: []string{"a plain template message"},
|
expectedOutput: []string{"a plain template message"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
doc: "subcommand name",
|
template: TemplateReplaceFlagValue("tag"),
|
||||||
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{
|
flags: []testFlag{
|
||||||
{name: "tag", value: "my-tag"},
|
{
|
||||||
|
name: "tag",
|
||||||
|
value: "my-tag",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
expectedOutput: []string{"my-tag"},
|
expectedOutput: []string{"my-tag"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
doc: "multiple flags",
|
template: TemplateReplaceFlagValue("test-one") + " " + TemplateReplaceFlagValue("test2"),
|
||||||
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{
|
flags: []testFlag{
|
||||||
{
|
{
|
||||||
name: "test-one",
|
name: "test-one",
|
||||||
@@ -75,51 +51,36 @@ func TestParseTemplate(t *testing.T) {
|
|||||||
expectedOutput: []string{"value value2"},
|
expectedOutput: []string{"value value2"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
doc: "multiple args",
|
template: TemplateReplaceArg(0) + " " + TemplateReplaceArg(1),
|
||||||
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"},
|
args: []string{"zero", "one"},
|
||||||
expectedOutput: []string{"zero one"},
|
expectedOutput: []string{"zero one"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
doc: "arg in sentence",
|
template: "You just pulled " + TemplateReplaceArg(0),
|
||||||
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"},
|
args: []string{"alpine"},
|
||||||
expectedOutput: []string{"You just pulled alpine"},
|
expectedOutput: []string{"You just pulled alpine"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
doc: "multiline output",
|
|
||||||
template: "one line\nanother line!",
|
template: "one line\nanother line!",
|
||||||
expectedOutput: []string{"one line", "another line!"},
|
expectedOutput: []string{"one line", "another line!"},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range tests {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.doc, func(t *testing.T) {
|
testCmd := &cobra.Command{
|
||||||
testCmd := &cobra.Command{
|
Use: "pull",
|
||||||
Use: "pull",
|
Args: cobra.ExactArgs(len(tc.args)),
|
||||||
Args: cobra.ExactArgs(len(tc.args)),
|
}
|
||||||
}
|
for _, f := range tc.flags {
|
||||||
for _, f := range tc.flags {
|
_ = testCmd.Flags().String(f.name, "", "")
|
||||||
_ = testCmd.Flags().String(f.name, "", "")
|
err := testCmd.Flag(f.name).Value.Set(f.value)
|
||||||
err := testCmd.Flag(f.name).Value.Set(f.value)
|
|
||||||
assert.NilError(t, err)
|
|
||||||
}
|
|
||||||
err := testCmd.Flags().Parse(tc.args)
|
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
|
}
|
||||||
|
err := testCmd.Flags().Parse(tc.args)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
// Validate using fixtures.
|
out, err := ParseTemplate(tc.template, testCmd)
|
||||||
out, err := hooks.ParseTemplate(tc.template, testCmd)
|
assert.NilError(t, err)
|
||||||
assert.NilError(t, err)
|
assert.DeepEqual(t, out, tc.expectedOutput)
|
||||||
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,10 +1,12 @@
|
|||||||
package manager
|
package manager
|
||||||
|
|
||||||
import (
|
import "os/exec"
|
||||||
"os/exec"
|
|
||||||
|
|
||||||
"github.com/docker/cli/cli-plugins/metadata"
|
// Candidate represents a possible plugin candidate, for mocking purposes
|
||||||
)
|
type Candidate interface {
|
||||||
|
Path() string
|
||||||
|
Metadata() ([]byte, error)
|
||||||
|
}
|
||||||
|
|
||||||
type candidate struct {
|
type candidate struct {
|
||||||
path string
|
path string
|
||||||
@@ -15,5 +17,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() // #nosec G204 -- ignore "Subprocess launched with a potential tainted input or cmd arguments"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,62 @@ 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 {
|
switch {
|
||||||
case tc.err != "":
|
case tc.err != "":
|
||||||
assert.ErrorContains(t, err, tc.err)
|
assert.ErrorContains(t, err, tc.err)
|
||||||
case tc.invalid != "":
|
case 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:
|
default:
|
||||||
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")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,12 +2,41 @@ package manager
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
"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"
|
||||||
|
"go.opentelemetry.io/otel/attribute"
|
||||||
|
)
|
||||||
|
|
||||||
|
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"
|
||||||
)
|
)
|
||||||
|
|
||||||
var pluginCommandStubsOnce sync.Once
|
var pluginCommandStubsOnce sync.Once
|
||||||
@@ -15,56 +44,53 @@ 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
|
||||||
}
|
}
|
||||||
@@ -79,3 +105,44 @@ func AddPluginCommandStubs(dockerCLI config.Provider, rootCmd *cobra.Command) (e
|
|||||||
})
|
})
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
dockerCliAttributePrefix = attribute.Key("docker.cli")
|
||||||
|
|
||||||
|
cobraCommandPath = attribute.Key("cobra.command_path")
|
||||||
|
)
|
||||||
|
|
||||||
|
func getPluginResourceAttributes(cmd *cobra.Command, plugin Plugin) attribute.Set {
|
||||||
|
commandPath := cmd.Annotations[CommandAnnotationPluginCommandPath]
|
||||||
|
if commandPath == "" {
|
||||||
|
commandPath = fmt.Sprintf("%s %s", cmd.CommandPath(), plugin.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
// values in environment variables need to be in baggage format
|
||||||
|
// otel/baggage package can be used after update to v1.22, currently it encodes incorrectly
|
||||||
|
attrsSlice := make([]string, attrs.Len())
|
||||||
|
for iter := attrs.Iter(); iter.Next(); {
|
||||||
|
i, v := iter.IndexedAttribute()
|
||||||
|
attrsSlice[i] = string(v.Key) + "=" + url.PathEscape(v.Value.AsString())
|
||||||
|
}
|
||||||
|
env = append(env, ResourceAttributesEnvvar+"="+strings.Join(attrsSlice, ","))
|
||||||
|
}
|
||||||
|
return env
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,10 @@
|
|||||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
||||||
//go:build go1.26
|
//go:build go1.22
|
||||||
|
|
||||||
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 +23,32 @@ 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)}
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
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 ...any) 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,19 +1,12 @@
|
|||||||
// 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"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/docker/cli/cli-plugins/hooks"
|
"github.com/docker/cli/cli-plugins/hooks"
|
||||||
"github.com/docker/cli/cli/config"
|
"github.com/docker/cli/cli/command"
|
||||||
"github.com/docker/cli/cli/config/configfile"
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
@@ -22,108 +15,97 @@ import (
|
|||||||
// HookPluginData is the type representing the information
|
// HookPluginData is the type representing the information
|
||||||
// that plugins declaring support for hooks get passed when
|
// that plugins declaring support for hooks get passed when
|
||||||
// being invoked following a CLI command execution.
|
// being invoked following a CLI command execution.
|
||||||
//
|
type HookPluginData struct {
|
||||||
// Deprecated: use [hooks.Request] instead.
|
// RootCmd is a string representing the matching hook configuration
|
||||||
//
|
// which is currently being invoked. If a hook for `docker context` is
|
||||||
//go:fix inline
|
// configured and the user executes `docker context ls`, the plugin will
|
||||||
type HookPluginData = hooks.Request
|
// be invoked with `context`.
|
||||||
|
RootCmd string
|
||||||
|
Flags map[string]string
|
||||||
|
CommandError string
|
||||||
|
}
|
||||||
|
|
||||||
// RunCLICommandHooks is the entrypoint into the hooks execution flow after
|
// RunCLICommandHooks is the entrypoint into the hooks execution flow after
|
||||||
// a main CLI command was executed. It calls the hook subcommand for all
|
// 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
|
// present CLI plugins that declare support for hooks in their metadata and
|
||||||
// parses/prints their responses.
|
// parses/prints their responses.
|
||||||
func RunCLICommandHooks(ctx context.Context, dockerCLI config.Provider, rootCmd, subCommand *cobra.Command, cmdErrorMessage string) {
|
func RunCLICommandHooks(ctx context.Context, dockerCli command.Cli, rootCmd, subCommand *cobra.Command, cmdErrorMessage string) {
|
||||||
commandName := strings.TrimPrefix(subCommand.CommandPath(), rootCmd.Name()+" ")
|
commandName := strings.TrimPrefix(subCommand.CommandPath(), rootCmd.Name()+" ")
|
||||||
flags := getCommandFlags(subCommand)
|
flags := getCommandFlags(subCommand)
|
||||||
|
|
||||||
runHooks(ctx, dockerCLI.ConfigFile(), rootCmd, subCommand, commandName, flags, cmdErrorMessage)
|
runHooks(ctx, dockerCli, rootCmd, subCommand, commandName, flags, cmdErrorMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RunPluginHooks is the entrypoint for the hooks execution flow
|
// RunPluginHooks is the entrypoint for the hooks execution flow
|
||||||
// after a plugin command was just executed by the CLI.
|
// 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) {
|
func RunPluginHooks(ctx context.Context, dockerCli command.Cli, rootCmd, subCommand *cobra.Command, args []string) {
|
||||||
commandName := strings.Join(args, " ")
|
commandName := strings.Join(args, " ")
|
||||||
flags := getNaiveFlags(args)
|
flags := getNaiveFlags(args)
|
||||||
|
|
||||||
runHooks(ctx, dockerCLI.ConfigFile(), rootCmd, subCommand, commandName, flags, cmdErrorMessage)
|
runHooks(ctx, dockerCli, rootCmd, subCommand, commandName, flags, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
func runHooks(ctx context.Context, cfg *configfile.ConfigFile, rootCmd, subCommand *cobra.Command, invokedCommand string, flags map[string]string, cmdErrorMessage string) {
|
func runHooks(ctx context.Context, dockerCli command.Cli, rootCmd, subCommand *cobra.Command, invokedCommand string, flags map[string]string, cmdErrorMessage string) {
|
||||||
nextSteps := invokeAndCollectHooks(ctx, cfg, rootCmd, subCommand, invokedCommand, flags, cmdErrorMessage)
|
nextSteps := invokeAndCollectHooks(ctx, dockerCli, rootCmd, subCommand, invokedCommand, flags, cmdErrorMessage)
|
||||||
hooks.PrintNextSteps(subCommand.ErrOrStderr(), nextSteps)
|
|
||||||
|
hooks.PrintNextSteps(dockerCli.Err(), nextSteps)
|
||||||
}
|
}
|
||||||
|
|
||||||
func invokeAndCollectHooks(ctx context.Context, cfg *configfile.ConfigFile, rootCmd, subCmd *cobra.Command, subCmdStr string, flags map[string]string, cmdErrorMessage string) []string {
|
func invokeAndCollectHooks(ctx context.Context, dockerCli command.Cli, rootCmd, subCmd *cobra.Command, subCmdStr string, flags map[string]string, cmdErrorMessage string) []string {
|
||||||
if ctx.Err() != nil {
|
// check if the context was cancelled before invoking hooks
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
return nil
|
return nil
|
||||||
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
pluginsCfg := cfg.Plugins
|
pluginsCfg := dockerCli.ConfigFile().Plugins
|
||||||
if pluginsCfg == nil {
|
if pluginsCfg == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
pluginDirs := getPluginDirs(cfg)
|
|
||||||
nextSteps := make([]string, 0, len(pluginsCfg))
|
nextSteps := make([]string, 0, len(pluginsCfg))
|
||||||
|
for pluginName, cfg := range pluginsCfg {
|
||||||
tryInvokeHook := func(pluginName string, pluginCfg map[string]string) (messages []string, ok bool, err error) {
|
match, ok := pluginMatch(cfg, subCmdStr)
|
||||||
match, matched := pluginMatch(pluginCfg, subCmdStr, cmdErrorMessage)
|
if !ok {
|
||||||
if !matched {
|
continue
|
||||||
return nil, false, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
p, err := getPlugin(pluginName, pluginDirs, rootCmd)
|
p, err := GetPlugin(pluginName, dockerCli, rootCmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, err
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := p.RunHook(ctx, hooks.Request{
|
hookReturn, err := p.RunHook(ctx, HookPluginData{
|
||||||
RootCmd: match,
|
RootCmd: match,
|
||||||
Flags: flags,
|
Flags: flags,
|
||||||
CommandError: cmdErrorMessage,
|
CommandError: cmdErrorMessage,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, err
|
// skip misbehaving plugins, but don't halt execution
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
var message hooks.Response
|
var hookMessageData hooks.HookMessage
|
||||||
if err := json.Unmarshal(resp, &message); err != nil {
|
err = json.Unmarshal(hookReturn, &hookMessageData)
|
||||||
return nil, false, fmt.Errorf("failed to unmarshal hook response (%q): %w", string(resp), err)
|
if err != nil {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// currently the only hook type
|
// currently the only hook type
|
||||||
if message.Type != hooks.NextSteps {
|
if hookMessageData.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
|
continue
|
||||||
}
|
}
|
||||||
if !ok {
|
|
||||||
|
processedHook, err := hooks.ParseTemplate(hookMessageData.Template, subCmd)
|
||||||
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
var appended bool
|
var appended bool
|
||||||
nextSteps, appended = appendNextSteps(nextSteps, messages)
|
nextSteps, appended = appendNextSteps(nextSteps, processedHook)
|
||||||
if !appended {
|
if !appended {
|
||||||
logrus.WithFields(logrus.Fields{
|
logrus.Debugf("Plugin %s responded with an empty hook message %q. Ignoring.", pluginName, string(hookReturn))
|
||||||
"plugin": pluginName,
|
|
||||||
}).Debug("Plugin responded with an empty hook message; ignoring")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nextSteps
|
return nextSteps
|
||||||
@@ -152,34 +134,14 @@ func appendNextSteps(nextSteps []string, processed []string) ([]string, bool) {
|
|||||||
// command being executed (such as 'image ls' – the root 'docker' is omitted)
|
// command being executed (such as 'image ls' – the root 'docker' is omitted)
|
||||||
// and, if the configuration includes a hook for the invoked command, returns
|
// and, if the configuration includes a hook for the invoked command, returns
|
||||||
// the configured hook string.
|
// the configured hook string.
|
||||||
//
|
func pluginMatch(pluginCfg map[string]string, subCmd string) (string, bool) {
|
||||||
// Plugins can declare two types of hooks in their configuration:
|
configuredPluginHooks, ok := pluginCfg["hooks"]
|
||||||
// - "hooks": fires on every command invocation (success or failure)
|
if !ok || configuredPluginHooks == "" {
|
||||||
// - "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
|
return "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
for hookCmd := range strings.SplitSeq(configuredHooks, ",") {
|
commands := strings.Split(configuredPluginHooks, ",")
|
||||||
|
for _, hookCmd := range commands {
|
||||||
if hookMatch(hookCmd, subCmd) {
|
if hookMatch(hookCmd, subCmd) {
|
||||||
return hookCmd, true
|
return hookCmd, true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,12 @@
|
|||||||
package manager
|
package manager
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/docker/cli/cli/config/configfile"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"gotest.tools/v3/assert"
|
"gotest.tools/v3/assert"
|
||||||
is "gotest.tools/v3/assert/cmp"
|
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) {
|
func TestGetNaiveFlags(t *testing.T) {
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
args []string
|
args []string
|
||||||
@@ -51,15 +40,12 @@ func TestGetNaiveFlags(t *testing.T) {
|
|||||||
|
|
||||||
func TestPluginMatch(t *testing.T) {
|
func TestPluginMatch(t *testing.T) {
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
doc string
|
commandString string
|
||||||
commandString string
|
pluginConfig map[string]string
|
||||||
pluginConfig map[string]string
|
expectedMatch string
|
||||||
cmdErrorMessage string
|
expectedOk bool
|
||||||
expectedMatch string
|
|
||||||
expectedOk bool
|
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
doc: "hooks prefix match",
|
|
||||||
commandString: "image ls",
|
commandString: "image ls",
|
||||||
pluginConfig: map[string]string{
|
pluginConfig: map[string]string{
|
||||||
"hooks": "image",
|
"hooks": "image",
|
||||||
@@ -68,7 +54,6 @@ func TestPluginMatch(t *testing.T) {
|
|||||||
expectedOk: true,
|
expectedOk: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
doc: "hooks no match",
|
|
||||||
commandString: "context ls",
|
commandString: "context ls",
|
||||||
pluginConfig: map[string]string{
|
pluginConfig: map[string]string{
|
||||||
"hooks": "build",
|
"hooks": "build",
|
||||||
@@ -77,7 +62,6 @@ func TestPluginMatch(t *testing.T) {
|
|||||||
expectedOk: false,
|
expectedOk: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
doc: "hooks exact match",
|
|
||||||
commandString: "context ls",
|
commandString: "context ls",
|
||||||
pluginConfig: map[string]string{
|
pluginConfig: map[string]string{
|
||||||
"hooks": "context ls",
|
"hooks": "context ls",
|
||||||
@@ -86,7 +70,6 @@ func TestPluginMatch(t *testing.T) {
|
|||||||
expectedOk: true,
|
expectedOk: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
doc: "hooks first match wins",
|
|
||||||
commandString: "image ls",
|
commandString: "image ls",
|
||||||
pluginConfig: map[string]string{
|
pluginConfig: map[string]string{
|
||||||
"hooks": "image ls,image",
|
"hooks": "image ls,image",
|
||||||
@@ -95,7 +78,6 @@ func TestPluginMatch(t *testing.T) {
|
|||||||
expectedOk: true,
|
expectedOk: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
doc: "hooks empty string",
|
|
||||||
commandString: "image ls",
|
commandString: "image ls",
|
||||||
pluginConfig: map[string]string{
|
pluginConfig: map[string]string{
|
||||||
"hooks": "",
|
"hooks": "",
|
||||||
@@ -104,7 +86,6 @@ func TestPluginMatch(t *testing.T) {
|
|||||||
expectedOk: false,
|
expectedOk: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
doc: "hooks partial token no match",
|
|
||||||
commandString: "image inspect",
|
commandString: "image inspect",
|
||||||
pluginConfig: map[string]string{
|
pluginConfig: map[string]string{
|
||||||
"hooks": "image i",
|
"hooks": "image i",
|
||||||
@@ -113,7 +94,6 @@ func TestPluginMatch(t *testing.T) {
|
|||||||
expectedOk: false,
|
expectedOk: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
doc: "hooks prefix token match",
|
|
||||||
commandString: "image inspect",
|
commandString: "image inspect",
|
||||||
pluginConfig: map[string]string{
|
pluginConfig: map[string]string{
|
||||||
"hooks": "image",
|
"hooks": "image",
|
||||||
@@ -121,140 +101,12 @@ func TestPluginMatch(t *testing.T) {
|
|||||||
expectedMatch: "image",
|
expectedMatch: "image",
|
||||||
expectedOk: true,
|
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 {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.doc, func(t *testing.T) {
|
match, ok := pluginMatch(tc.pluginConfig, tc.commandString)
|
||||||
match, ok := pluginMatch(tc.pluginConfig, tc.commandString, tc.cmdErrorMessage)
|
assert.Equal(t, ok, tc.expectedOk)
|
||||||
assert.Equal(t, ok, tc.expectedOk)
|
assert.Equal(t, match, tc.expectedMatch)
|
||||||
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)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -289,87 +141,3 @@ func TestAppendNextSteps(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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,37 +1,54 @@
|
|||||||
// 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"
|
"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/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"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ReexecEnvvar is the name of an ennvar which is set to the command
|
||||||
|
// used to originally invoke the docker CLI when executing a
|
||||||
|
// plugin. Assuming $PATH and $CWD remain unchanged this should allow
|
||||||
|
// the plugin to re-execute the original CLI.
|
||||||
|
ReexecEnvvar = "DOCKER_CLI_PLUGIN_ORIGINAL_CLI_COMMAND"
|
||||||
|
|
||||||
|
// ResourceAttributesEnvvar is the name of the envvar that includes additional
|
||||||
|
// resource attributes for OTEL.
|
||||||
|
ResourceAttributesEnvvar = "OTEL_RESOURCE_ATTRIBUTES"
|
||||||
|
)
|
||||||
|
|
||||||
// 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type notFound interface{ NotFound() }
|
||||||
|
|
||||||
|
// IsNotFound is true if the given error is due to a plugin not being found.
|
||||||
|
func IsNotFound(err error) bool {
|
||||||
|
if e, ok := err.(*pluginError); ok {
|
||||||
|
err = e.Cause()
|
||||||
|
}
|
||||||
|
_, ok := err.(notFound)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
// getPluginDirs returns the platform-specific locations to search for plugins
|
// getPluginDirs returns the platform-specific locations to search for plugins
|
||||||
// in order of preference.
|
// in order of preference.
|
||||||
//
|
//
|
||||||
@@ -42,16 +59,20 @@ func (e errPluginNotFound) Error() string {
|
|||||||
// 3. Platform-specific defaultSystemPluginDirs.
|
// 3. Platform-specific defaultSystemPluginDirs.
|
||||||
//
|
//
|
||||||
// [ConfigFile.CLIPluginsExtraDirs]: https://pkg.go.dev/github.com/docker/cli@v26.1.4+incompatible/cli/config/configfile#ConfigFile.CLIPluginsExtraDirs
|
// [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 {
|
func getPluginDirs(cfg *configfile.ConfigFile) ([]string, error) {
|
||||||
var pluginDirs []string
|
var pluginDirs []string
|
||||||
|
|
||||||
if cfg != nil {
|
if cfg != nil {
|
||||||
pluginDirs = append(pluginDirs, cfg.CLIPluginsExtraDirs...)
|
pluginDirs = append(pluginDirs, cfg.CLIPluginsExtraDirs...)
|
||||||
}
|
}
|
||||||
pluginDir := filepath.Join(config.Dir(), "cli-plugins")
|
pluginDir, err := config.Path("cli-plugins")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
pluginDirs = append(pluginDirs, pluginDir)
|
pluginDirs = append(pluginDirs, pluginDir)
|
||||||
pluginDirs = append(pluginDirs, defaultSystemPluginDirs...)
|
pluginDirs = append(pluginDirs, defaultSystemPluginDirs...)
|
||||||
return pluginDirs
|
return pluginDirs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func addPluginCandidatesFromDir(res map[string][]string, d string) {
|
func addPluginCandidatesFromDir(res map[string][]string, d string) {
|
||||||
@@ -62,26 +83,18 @@ func addPluginCandidatesFromDir(res map[string][]string, d string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
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
|
||||||
@@ -100,12 +113,12 @@ func listPluginCandidates(dirs []string) map[string][]string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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.ConfigFile())
|
||||||
return getPlugin(name, pluginDirs, rootcmd)
|
if err != nil {
|
||||||
}
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
func getPlugin(name string, pluginDirs []string, rootcmd *cobra.Command) (*Plugin, error) {
|
|
||||||
candidates := listPluginCandidates(pluginDirs)
|
candidates := listPluginCandidates(pluginDirs)
|
||||||
if paths, ok := candidates[name]; ok {
|
if paths, ok := candidates[name]; ok {
|
||||||
if len(paths) == 0 {
|
if len(paths) == 0 {
|
||||||
@@ -116,7 +129,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 +139,17 @@ 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.ConfigFile())
|
||||||
candidates := listPluginCandidates(pluginDirs)
|
if err != nil {
|
||||||
if len(candidates) == 0 {
|
return nil, err
|
||||||
return nil, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
candidates := listPluginCandidates(pluginDirs)
|
||||||
|
|
||||||
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 +162,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 +176,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.ConfigFile())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
for _, d := range pluginDirs {
|
for _, d := range pluginDirs {
|
||||||
path := filepath.Join(d, exename)
|
path := filepath.Join(d, exename)
|
||||||
@@ -221,7 +233,7 @@ 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 = append(cmd.Environ(), ReexecEnvvar+"="+os.Args[0])
|
||||||
cmd.Env = appendPluginResourceAttributesEnvvar(cmd.Env, rootcmd, plugin)
|
cmd.Env = appendPluginResourceAttributesEnvvar(cmd.Env, rootcmd, plugin)
|
||||||
|
|
||||||
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.
|
||||||
),
|
),
|
||||||
@@ -72,6 +70,9 @@ func TestListPluginCandidates(t *testing.T) {
|
|||||||
"hardlink2": {
|
"hardlink2": {
|
||||||
dir.Join("plugins2", "docker-hardlink2"),
|
dir.Join("plugins2", "docker-hardlink2"),
|
||||||
},
|
},
|
||||||
|
"brokensymlink": {
|
||||||
|
dir.Join("plugins3", "docker-brokensymlink"),
|
||||||
|
},
|
||||||
"symlinked": {
|
"symlinked": {
|
||||||
dir.Join("plugins3", "docker-symlinked"),
|
dir.Join("plugins3", "docker-symlinked"),
|
||||||
},
|
},
|
||||||
@@ -80,16 +81,10 @@ 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.
|
// Regression test for https://github.com/docker/cli/issues/5643.
|
||||||
// Check that inaccessible directories that come before accessible ones are ignored
|
// Check that inaccessible directories that come before accessible ones are ignored
|
||||||
// and do not prevent the latter from being processed.
|
// and do not prevent the latter from being processed.
|
||||||
func TestListPluginCandidatesInaccessibleDir(t *testing.T) {
|
func TestListPluginCandidatesInaccesibleDir(t *testing.T) {
|
||||||
dir := fs.NewDir(t, t.Name(),
|
dir := fs.NewDir(t, t.Name(),
|
||||||
fs.WithDir("no-perm", fs.WithMode(0)),
|
fs.WithDir("no-perm", fs.WithMode(0)),
|
||||||
fs.WithDir("plugins",
|
fs.WithDir("plugins",
|
||||||
@@ -129,7 +124,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 +159,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.ConfigFile())
|
||||||
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.ConfigFile())
|
||||||
assert.DeepEqual(t, expected, pluginDirs)
|
assert.DeepEqual(t, expected, pluginDirs)
|
||||||
|
assert.NilError(t, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,5 +16,6 @@ import (
|
|||||||
//
|
//
|
||||||
// [ConfigFile.CLIPluginsExtraDirs]: https://pkg.go.dev/github.com/docker/cli@v26.1.4+incompatible/cli/config/configfile#ConfigFile.CLIPluginsExtraDirs
|
// [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"),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package metadata
|
package manager
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// NamePrefix is the prefix required on all plugin binary names
|
// NamePrefix is the prefix required on all plugin binary names
|
||||||
@@ -13,12 +13,6 @@ const (
|
|||||||
// which must be implemented by plugins declaring support
|
// which must be implemented by plugins declaring support
|
||||||
// for hooks in their metadata.
|
// for hooks in their metadata.
|
||||||
HookSubcommandName = "docker-cli-plugin-hooks"
|
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.
|
// Metadata provided by the plugin.
|
||||||
@@ -33,6 +27,4 @@ type Metadata struct {
|
|||||||
ShortDescription string `json:",omitempty"`
|
ShortDescription string `json:",omitempty"`
|
||||||
// URL is a pointer to the plugin's homepage.
|
// URL is a pointer to the plugin's homepage.
|
||||||
URL string `json:",omitempty"`
|
URL string `json:",omitempty"`
|
||||||
// Hidden hides the plugin in completion and help message output.
|
|
||||||
Hidden bool `json:",omitempty"`
|
|
||||||
}
|
}
|
||||||
+24
-102
@@ -1,28 +1,23 @@
|
|||||||
// 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"
|
||||||
"encoding"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"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 +29,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 +44,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 +73,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 +93,32 @@ 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
|
// RunHook executes the plugin's hooks command
|
||||||
// and returns its unprocessed output.
|
// and returns its unprocessed output.
|
||||||
func (p *Plugin) RunHook(ctx context.Context, hookData hooks.Request) ([]byte, error) {
|
func (p *Plugin) RunHook(ctx context.Context, hookData HookPluginData) ([]byte, error) {
|
||||||
hDataBytes, err := json.Marshal(hookData)
|
hDataBytes, err := json.Marshal(hookData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, wrapAsPluginError(err, "failed to marshall hook data")
|
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 := exec.CommandContext(ctx, p.Path, p.Name, HookSubcommandName, string(hDataBytes)) // #nosec G204 -- ignore "Subprocess launched with a potential tainted input or cmd arguments"
|
||||||
pCmd.Env = os.Environ()
|
pCmd.Env = os.Environ()
|
||||||
pCmd.Env = append(pCmd.Env, metadata.ReexecEnvvar+"="+os.Args[0])
|
pCmd.Env = append(pCmd.Env, ReexecEnvvar+"="+os.Args[0])
|
||||||
|
hookCmdOutput, err := pCmd.Output()
|
||||||
out, err := pCmd.Output()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if _, ok := errors.AsType[*exec.ExitError](err); ok {
|
return nil, wrapAsPluginError(err, "failed to execute plugin hook subcommand")
|
||||||
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.
|
return hookCmdOutput, nil
|
||||||
// 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,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,23 +1,19 @@
|
|||||||
// 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"
|
"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-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/cli/cli/debug"
|
||||||
"github.com/moby/moby/client"
|
"github.com/docker/docker/client"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"go.opentelemetry.io/otel"
|
"go.opentelemetry.io/otel"
|
||||||
)
|
)
|
||||||
@@ -33,12 +29,12 @@ import (
|
|||||||
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(cmd *cobra.Command, _ []string) error {
|
||||||
var retErr error
|
var err error
|
||||||
persistentPreRunOnce.Do(func() {
|
persistentPreRunOnce.Do(func() {
|
||||||
ctx, cancel := context.WithCancel(cmd.Context())
|
ctx, cancel := context.WithCancel(cmd.Context())
|
||||||
cmd.SetContext(ctx)
|
cmd.SetContext(ctx)
|
||||||
@@ -50,7 +46,7 @@ func RunPlugin(dockerCli *command.DockerCli, plugin *cobra.Command, meta metadat
|
|||||||
opts = append(opts, withPluginClientConn(plugin.Name()))
|
opts = append(opts, withPluginClientConn(plugin.Name()))
|
||||||
}
|
}
|
||||||
opts = append(opts, command.WithEnableGlobalMeterProvider(), command.WithEnableGlobalTracerProvider())
|
opts = append(opts, command.WithEnableGlobalMeterProvider(), command.WithEnableGlobalTracerProvider())
|
||||||
retErr = tcmd.Initialize(opts...)
|
err = tcmd.Initialize(opts...)
|
||||||
ogRunE := cmd.RunE
|
ogRunE := cmd.RunE
|
||||||
if ogRunE == nil {
|
if ogRunE == nil {
|
||||||
ogRun := cmd.Run
|
ogRun := cmd.Run
|
||||||
@@ -70,7 +66,7 @@ func RunPlugin(dockerCli *command.DockerCli, plugin *cobra.Command, meta metadat
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return retErr
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd, args, err := tcmd.HandleGlobalFlags()
|
cmd, args, err := tcmd.HandleGlobalFlags()
|
||||||
@@ -83,41 +79,39 @@ 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
|
|
||||||
// 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)
|
otel.SetErrorHandler(debug.OTELErrorHandler)
|
||||||
|
|
||||||
dockerCLI, err := command.NewDockerCli(ops...)
|
dockerCli, err := command.NewDockerCli()
|
||||||
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.CLIOption {
|
||||||
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 +133,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,14 +158,9 @@ 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,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disable file-completion by default. Most commands and flags should not
|
|
||||||
// complete with filenames.
|
|
||||||
cmd.CompletionOptions.SetDefaultShellCompDirective(cobra.ShellCompDirectiveNoFileComp)
|
|
||||||
|
|
||||||
opts, _ := cli.SetupPluginRootCommand(cmd)
|
opts, _ := cli.SetupPluginRootCommand(cmd)
|
||||||
|
|
||||||
cmd.SetIn(dockerCli.In())
|
cmd.SetIn(dockerCli.In())
|
||||||
@@ -186,30 +172,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, cmd.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 +200,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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -93,7 +93,7 @@ func (pl *PluginServer) Addr() net.Addr {
|
|||||||
// Close ensures that the server is no longer accepting new connections and
|
// Close ensures that the server is no longer accepting new connections and
|
||||||
// closes all existing connections. Existing connections will receive [io.EOF].
|
// closes all existing connections. Existing connections will receive [io.EOF].
|
||||||
//
|
//
|
||||||
// The error value is that of the underlying [net.Listener.Close] call.
|
// The error value is that of the underlying [net.Listner.Close] call.
|
||||||
func (pl *PluginServer) Close() error {
|
func (pl *PluginServer) Close() error {
|
||||||
if pl == nil {
|
if pl == nil {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -47,16 +47,16 @@ func TestPluginServer(t *testing.T) {
|
|||||||
select {
|
select {
|
||||||
case err := <-done:
|
case err := <-done:
|
||||||
if !errors.Is(err, io.EOF) {
|
if !errors.Is(err, io.EOF) {
|
||||||
t.Fatalf("expected EOF error, got: %v", err)
|
t.Fatalf("exepcted EOF error, got: %v", err)
|
||||||
}
|
}
|
||||||
case <-time.After(10 * time.Millisecond):
|
case <-time.After(10 * time.Millisecond):
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("allows reconnects", func(t *testing.T) {
|
t.Run("allows reconnects", func(t *testing.T) {
|
||||||
var calls atomic.Int32
|
var calls int32
|
||||||
h := func(_ net.Conn) {
|
h := func(_ net.Conn) {
|
||||||
calls.Add(1)
|
atomic.AddInt32(&calls, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
srv, err := NewPluginServer(h)
|
srv, err := NewPluginServer(h)
|
||||||
@@ -70,7 +70,7 @@ func TestPluginServer(t *testing.T) {
|
|||||||
|
|
||||||
waitForCalls := func(n int) {
|
waitForCalls := func(n int) {
|
||||||
poll.WaitOn(t, func(t poll.LogT) poll.Result {
|
poll.WaitOn(t, func(t poll.LogT) poll.Result {
|
||||||
if calls.Load() == int32(n) {
|
if atomic.LoadInt32(&calls) == int32(n) {
|
||||||
return poll.Success()
|
return poll.Success()
|
||||||
}
|
}
|
||||||
return poll.Continue("waiting for handler to be called")
|
return poll.Continue("waiting for handler to be called")
|
||||||
@@ -178,39 +178,24 @@ func TestConnectAndWait(t *testing.T) {
|
|||||||
// TODO: this test cannot be executed with `t.Parallel()`, due to
|
// TODO: this test cannot be executed with `t.Parallel()`, due to
|
||||||
// relying on goroutine numbers to ensure correct behaviour
|
// relying on goroutine numbers to ensure correct behaviour
|
||||||
t.Run("connect goroutine exits after EOF", func(t *testing.T) {
|
t.Run("connect goroutine exits after EOF", func(t *testing.T) {
|
||||||
runtime.LockOSThread()
|
|
||||||
defer runtime.UnlockOSThread()
|
|
||||||
srv, err := NewPluginServer(nil)
|
srv, err := NewPluginServer(nil)
|
||||||
assert.NilError(t, err, "failed to setup server")
|
assert.NilError(t, err, "failed to setup server")
|
||||||
|
|
||||||
defer srv.Close()
|
defer srv.Close()
|
||||||
|
|
||||||
t.Setenv(EnvKey, srv.Addr().String())
|
t.Setenv(EnvKey, srv.Addr().String())
|
||||||
|
|
||||||
runtime.Gosched()
|
|
||||||
numGoroutines := runtime.NumGoroutine()
|
numGoroutines := runtime.NumGoroutine()
|
||||||
|
|
||||||
ConnectAndWait(func() {})
|
ConnectAndWait(func() {})
|
||||||
|
assert.Equal(t, runtime.NumGoroutine(), numGoroutines+1)
|
||||||
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()
|
srv.Close()
|
||||||
|
|
||||||
runtime.Gosched()
|
|
||||||
poll.WaitOn(t, func(t poll.LogT) poll.Result {
|
poll.WaitOn(t, func(t poll.LogT) poll.Result {
|
||||||
// +1 goroutine for the poll.WaitOn
|
|
||||||
if runtime.NumGoroutine() > numGoroutines+1 {
|
if runtime.NumGoroutine() > numGoroutines+1 {
|
||||||
return poll.Continue("waiting for connect goroutine to exit")
|
return poll.Continue("waiting for connect goroutine to exit")
|
||||||
}
|
}
|
||||||
return poll.Success()
|
return poll.Success()
|
||||||
}, poll.WithDelay(1*time.Millisecond), poll.WithTimeout(500*time.Millisecond))
|
}, poll.WithDelay(1*time.Millisecond), poll.WithTimeout(10*time.Millisecond))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
+64
-23
@@ -1,20 +1,21 @@
|
|||||||
// 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"
|
||||||
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"
|
||||||
)
|
)
|
||||||
@@ -61,6 +62,13 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Configure registry.CertsDir() when running in rootless-mode
|
||||||
|
if os.Getenv("ROOTLESSKIT_STATE_DIR") != "" {
|
||||||
|
if configHome, err := homedir.GetConfigHome(); err == nil {
|
||||||
|
registry.SetCertsDir(filepath.Join(configHome, "docker/certs.d"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return opts, helpCommand
|
return opts, helpCommand
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,8 +92,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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -169,6 +181,35 @@ func (tcmd *TopLevelCommand) Initialize(ops ...command.CLIOption) error {
|
|||||||
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 +218,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 +256,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 +294,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 +316,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 +341,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 +358,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 +392,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,7 +419,7 @@ 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:
|
const usageTemplate = `Usage:
|
||||||
@@ -481,4 +522,4 @@ Run '{{.CommandPath}} COMMAND --help' for more information on a command.
|
|||||||
`
|
`
|
||||||
|
|
||||||
const helpTemplate = `
|
const 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*")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,17 +3,18 @@ package builder
|
|||||||
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
|
||||||
builderPruneFunc func(ctx context.Context, opts client.BuildCachePruneOptions) (client.BuildCachePruneResult, error)
|
builderPruneFunc func(ctx context.Context, opts types.BuildCachePruneOptions) (*types.BuildCachePruneReport, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *fakeClient) BuildCachePrune(ctx context.Context, opts client.BuildCachePruneOptions) (client.BuildCachePruneResult, error) {
|
func (c *fakeClient) BuildCachePrune(ctx context.Context, opts types.BuildCachePruneOptions) (*types.BuildCachePruneReport, error) {
|
||||||
if c.builderPruneFunc != nil {
|
if c.builderPruneFunc != nil {
|
||||||
return c.builderPruneFunc(ctx, opts)
|
return c.builderPruneFunc(ctx, opts)
|
||||||
}
|
}
|
||||||
return client.BuildCachePruneResult{}, nil
|
return nil, 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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -8,30 +8,23 @@ 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/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"
|
"github.com/docker/docker/errdefs"
|
||||||
|
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 +32,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(cmd.Context(), 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
|
||||||
}
|
}
|
||||||
@@ -69,31 +61,32 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions) (spaceReclaimed uint64, output string, err error) {
|
func runPrune(ctx context.Context, 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 {
|
||||||
r, err := prompt.Confirm(ctx, dockerCli.In(), dockerCli.Out(), warning)
|
r, err := command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), warning)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, "", err
|
return 0, "", err
|
||||||
}
|
}
|
||||||
if !r {
|
if !r {
|
||||||
return 0, "", cancelledErr{errors.New("builder prune has been cancelled")}
|
return 0, "", errdefs.Cancelled(errors.New("builder prune has been cancelled"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := dockerCli.Client().BuildCachePrune(ctx, client.BuildCachePruneOptions{
|
report, err := dockerCli.Client().BuildCachePrune(ctx, 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 +100,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(ctx context.Context, dockerCli command.Cli, all bool, filter opts.FilterOpt) (uint64, string, error) {
|
||||||
func (cancelledErr) Cancelled() {}
|
return runPrune(ctx, 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,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"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"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestBuilderPromptTermination(t *testing.T) {
|
func TestBuilderPromptTermination(t *testing.T) {
|
||||||
@@ -15,11 +15,11 @@ func TestBuilderPromptTermination(t *testing.T) {
|
|||||||
t.Cleanup(cancel)
|
t.Cleanup(cancel)
|
||||||
|
|
||||||
cli := test.NewFakeCli(&fakeClient{
|
cli := test.NewFakeCli(&fakeClient{
|
||||||
builderPruneFunc: func(ctx context.Context, opts client.BuildCachePruneOptions) (client.BuildCachePruneResult, error) {
|
builderPruneFunc: func(ctx context.Context, opts types.BuildCachePruneOptions) (*types.BuildCachePruneReport, error) {
|
||||||
return client.BuildCachePruneResult{}, errors.New("fakeClient builderPruneFunc should not be called")
|
return nil, errors.New("fakeClient builderPruneFunc should not be called")
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
cmd := newPruneCommand(cli)
|
cmd := NewPruneCommand(cli)
|
||||||
cmd.SetOut(io.Discard)
|
cmd.SetOut(io.Discard)
|
||||||
cmd.SetErr(io.Discard)
|
cmd.SetErr(io.Discard)
|
||||||
test.TerminatePrompt(ctx, t, cmd, cli)
|
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/checkpoint"
|
||||||
|
"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 checkpoint.CreateOptions) error
|
||||||
checkpointDeleteFunc func(container string, options client.CheckpointRemoveOptions) (client.CheckpointRemoveResult, error)
|
checkpointDeleteFunc func(container string, options checkpoint.DeleteOptions) error
|
||||||
checkpointListFunc func(container string, options client.CheckpointListOptions) (client.CheckpointListResult, error)
|
checkpointListFunc func(container string, options checkpoint.ListOptions) ([]checkpoint.Summary, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cli *fakeClient) CheckpointCreate(_ context.Context, container string, options client.CheckpointCreateOptions) (client.CheckpointCreateResult, error) {
|
func (cli *fakeClient) CheckpointCreate(_ context.Context, container string, options checkpoint.CreateOptions) 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 checkpoint.DeleteOptions) 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 checkpoint.ListOptions) ([]checkpoint.Summary, error) {
|
||||||
if cli.checkpointListFunc != nil {
|
if cli.checkpointListFunc != nil {
|
||||||
return cli.checkpointListFunc(container, options)
|
return cli.checkpointListFunc(container, options)
|
||||||
}
|
}
|
||||||
return client.CheckpointListResult{}, nil
|
return []checkpoint.Summary{}, 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/checkpoint"
|
||||||
"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,10 +28,9 @@ 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(cmd.Context(), dockerCli, opts)
|
||||||
},
|
},
|
||||||
ValidArgsFunction: cobra.NoFileCompletions,
|
ValidArgsFunction: completion.NoComplete,
|
||||||
DisableFlagsInUseLine: true,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
flags := cmd.Flags()
|
flags := cmd.Flags()
|
||||||
@@ -40,8 +40,8 @@ func newCreateCommand(dockerCLI command.Cli) *cobra.Command {
|
|||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func runCreate(ctx context.Context, dockerCLI command.Cli, opts createOptions) error {
|
func runCreate(ctx context.Context, dockerCli command.Cli, opts createOptions) error {
|
||||||
_, err := dockerCLI.Client().CheckpointCreate(ctx, opts.container, client.CheckpointCreateOptions{
|
err := dockerCli.Client().CheckpointCreate(ctx, opts.container, checkpoint.CreateOptions{
|
||||||
CheckpointID: opts.checkpoint,
|
CheckpointID: opts.checkpoint,
|
||||||
CheckpointDir: opts.checkpointDir,
|
CheckpointDir: opts.checkpointDir,
|
||||||
Exit: !opts.leaveRunning,
|
Exit: !opts.leaveRunning,
|
||||||
@@ -50,6 +50,6 @@ func runCreate(ctx context.Context, dockerCLI command.Cli, opts createOptions) e
|
|||||||
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/checkpoint"
|
||||||
|
"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 checkpoint.CreateOptions) 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 checkpoint.CreateOptions) 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",
|
||||||
},
|
},
|
||||||
@@ -49,39 +48,26 @@ func TestCheckpointCreateErrors(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 checkpoint.CreateOptions) 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) {
|
cp := "checkpoint-bar"
|
||||||
actualContainerName = container
|
cmd.SetArgs([]string{"container-foo", cp})
|
||||||
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(cp, 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(cp, 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,7 +2,7 @@ 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/checkpoint"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -10,31 +10,25 @@ const (
|
|||||||
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 {
|
if source == 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 []checkpoint.Summary) error {
|
||||||
cpContext := &checkpointContext{
|
render := func(format func(subContext formatter.SubContext) error) error {
|
||||||
HeaderContext: formatter.HeaderContext{
|
|
||||||
Header: formatter.SubHeaderContext{
|
|
||||||
"Name": checkpointNameHeader,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return fmtCtx.Write(cpContext, func(format func(subContext formatter.SubContext) error) error {
|
|
||||||
for _, cp := range checkpoints {
|
for _, cp := range checkpoints {
|
||||||
if err := format(&checkpointContext{c: cp}); err != nil {
|
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 {
|
||||||
@@ -42,6 +36,14 @@ type checkpointContext struct {
|
|||||||
c checkpoint.Summary
|
c checkpoint.Summary
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
return formatter.MarshalJSON(c)
|
return formatter.MarshalJSON(c)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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/checkpoint"
|
||||||
"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:
|
||||||
@@ -41,7 +41,7 @@ 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, []checkpoint.Summary{
|
||||||
{Name: "checkpoint-1"},
|
{Name: "checkpoint-1"},
|
||||||
{Name: "checkpoint-2"},
|
{Name: "checkpoint-2"},
|
||||||
{Name: "checkpoint-3"},
|
{Name: "checkpoint-3"},
|
||||||
|
|||||||
@@ -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/checkpoint"
|
||||||
"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,10 +24,9 @@ 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(cmd.Context(), dockerCli, args[0], opts)
|
||||||
},
|
},
|
||||||
ValidArgsFunction: completion.ContainerNames(dockerCLI, false),
|
ValidArgsFunction: completion.ContainerNames(dockerCli, false),
|
||||||
DisableFlagsInUseLine: true,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
flags := cmd.Flags()
|
flags := cmd.Flags()
|
||||||
@@ -36,8 +35,8 @@ func newListCommand(dockerCLI command.Cli) *cobra.Command {
|
|||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func runList(ctx context.Context, dockerCLI command.Cli, container string, opts listOptions) error {
|
func runList(ctx context.Context, dockerCli command.Cli, container string, opts listOptions) error {
|
||||||
checkpoints, err := dockerCLI.Client().CheckpointList(ctx, container, client.CheckpointListOptions{
|
checkpoints, err := dockerCli.Client().CheckpointList(ctx, container, checkpoint.ListOptions{
|
||||||
CheckpointDir: opts.checkpointDir,
|
CheckpointDir: opts.checkpointDir,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -45,8 +44,8 @@ func runList(ctx context.Context, dockerCLI command.Cli, container string, opts
|
|||||||
}
|
}
|
||||||
|
|
||||||
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/checkpoint"
|
||||||
"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 checkpoint.ListOptions) ([]checkpoint.Summary, 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 checkpoint.ListOptions) ([]checkpoint.Summary, error) {
|
||||||
return client.CheckpointListResult{}, errors.New("error getting checkpoints for container foo")
|
return []checkpoint.Summary{}, errors.Errorf("error getting checkpoints for container foo")
|
||||||
},
|
},
|
||||||
expectedError: "error getting checkpoints for container foo",
|
expectedError: "error getting checkpoints for container foo",
|
||||||
},
|
},
|
||||||
@@ -51,19 +50,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 checkpoint.ListOptions) ([]checkpoint.Summary, error) {
|
||||||
containerID = container
|
containerID = container
|
||||||
checkpointDir = options.CheckpointDir
|
checkpointDir = options.CheckpointDir
|
||||||
return client.CheckpointListResult{
|
return []checkpoint.Summary{
|
||||||
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/checkpoint"
|
||||||
"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,14 +22,8 @@ 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(cmd.Context(), 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()
|
||||||
@@ -35,3 +31,10 @@ func newRemoveCommand(dockerCLI command.Cli) *cobra.Command {
|
|||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func runRemove(ctx context.Context, dockerCli command.Cli, container string, checkpointID string, opts removeOptions) error {
|
||||||
|
return dockerCli.Client().CheckpointDelete(ctx, container, checkpoint.DeleteOptions{
|
||||||
|
CheckpointID: checkpointID,
|
||||||
|
CheckpointDir: opts.checkpointDir,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -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/checkpoint"
|
||||||
|
"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 checkpoint.DeleteOptions) 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 checkpoint.DeleteOptions) error {
|
||||||
return client.CheckpointRemoveResult{}, errors.New("error deleting checkpoint")
|
return errors.Errorf("error deleting checkpoint")
|
||||||
},
|
},
|
||||||
expectedError: "error deleting checkpoint",
|
expectedError: "error deleting checkpoint",
|
||||||
},
|
},
|
||||||
@@ -49,16 +49,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 checkpoint.DeleteOptions) 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))
|
||||||
|
|||||||
+117
-148
@@ -1,14 +1,14 @@
|
|||||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
||||||
//go:build go1.26
|
//go:build go1.22
|
||||||
|
|
||||||
package command
|
package command
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -21,12 +21,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
|
||||||
@@ -43,9 +52,15 @@ type Cli interface {
|
|||||||
Client() client.APIClient
|
Client() client.APIClient
|
||||||
Streams
|
Streams
|
||||||
SetIn(in *streams.In)
|
SetIn(in *streams.In)
|
||||||
config.Provider
|
Apply(ops ...CLIOption) 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
|
||||||
@@ -54,24 +69,22 @@ type Cli interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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 *streams.Out
|
||||||
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
|
res telemetryResource
|
||||||
|
|
||||||
@@ -83,12 +96,17 @@ type DockerCli struct {
|
|||||||
enableGlobalMeter, enableGlobalTracer bool
|
enableGlobalMeter, enableGlobalTracer bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DefaultVersion returns api.defaultVersion.
|
||||||
|
func (cli *DockerCli) DefaultVersion() string {
|
||||||
|
return api.DefaultVersion
|
||||||
|
}
|
||||||
|
|
||||||
// CurrentVersion returns the API version currently negotiated, or the default
|
// CurrentVersion returns the API version currently negotiated, or the default
|
||||||
// version otherwise.
|
// version otherwise.
|
||||||
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 +114,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
|
||||||
@@ -147,13 +165,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
|
||||||
}
|
}
|
||||||
@@ -164,7 +188,7 @@ func (cli *DockerCli) BuildKitEnabled() (bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
si := cli.ServerInfo()
|
si := cli.ServerInfo()
|
||||||
if si.BuildkitVersion == build.BuilderBuildKit {
|
if si.BuildkitVersion == types.BuilderBuildKit {
|
||||||
// The daemon advertised BuildKit as the preferred builder; this may
|
// The daemon advertised BuildKit as the preferred builder; this may
|
||||||
// be either a Linux daemon or a Windows daemon with experimental
|
// be either a Linux daemon or a Windows daemon with experimental
|
||||||
// BuildKit support enabled.
|
// BuildKit support enabled.
|
||||||
@@ -178,16 +202,16 @@ func (cli *DockerCli) BuildKitEnabled() (bool, error) {
|
|||||||
|
|
||||||
// HooksEnabled returns whether plugin hooks are enabled.
|
// HooksEnabled returns whether plugin hooks are enabled.
|
||||||
func (cli *DockerCli) HooksEnabled() bool {
|
func (cli *DockerCli) HooksEnabled() bool {
|
||||||
// use DOCKER_CLI_HOOKS env var value if set and not empty
|
// legacy support DOCKER_CLI_HINTS env var
|
||||||
if v := os.Getenv("DOCKER_CLI_HOOKS"); v != "" {
|
if v := os.Getenv("DOCKER_CLI_HINTS"); v != "" {
|
||||||
enabled, err := strconv.ParseBool(v)
|
enabled, err := strconv.ParseBool(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return enabled
|
return enabled
|
||||||
}
|
}
|
||||||
// legacy support DOCKER_CLI_HINTS env var
|
// use DOCKER_CLI_HOOKS env var value if set and not empty
|
||||||
if v := os.Getenv("DOCKER_CLI_HINTS"); v != "" {
|
if v := os.Getenv("DOCKER_CLI_HOOKS"); v != "" {
|
||||||
enabled, err := strconv.ParseBool(v)
|
enabled, err := strconv.ParseBool(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
@@ -206,6 +230,30 @@ func (cli *DockerCli) HooksEnabled() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ManifestStore returns a store for local manifests
|
||||||
|
func (cli *DockerCli) ManifestStore() manifeststore.Store {
|
||||||
|
// TODO: support override default location from config file
|
||||||
|
return manifeststore.NewStore(filepath.Join(config.Dir(), "manifests"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegistryClient returns a client for communicating with a Docker distribution
|
||||||
|
// registry
|
||||||
|
func (cli *DockerCli) RegistryClient(allowInsecure bool) registryclient.RegistryClient {
|
||||||
|
resolver := func(ctx context.Context, index *registry.IndexInfo) registry.AuthConfig {
|
||||||
|
return ResolveAuthConfig(cli.ConfigFile(), index)
|
||||||
|
}
|
||||||
|
return registryclient.NewRegistryClient(resolver, UserAgent(), allowInsecure)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithInitializeClient is passed to DockerCli.Initialize by callers who wish to set a particular API Client for use by the CLI.
|
||||||
|
func WithInitializeClient(makeClient func(dockerCli *DockerCli) (client.APIClient, error)) CLIOption {
|
||||||
|
return func(dockerCli *DockerCli) error {
|
||||||
|
var err error
|
||||||
|
dockerCli.client, err = makeClient(dockerCli)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize the dockerCli runs initialization that must happen after command
|
// 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 ...CLIOption) error {
|
||||||
@@ -224,36 +272,16 @@ 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)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,18 +292,6 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...CLIOption)
|
|||||||
if cli.enableGlobalTracer {
|
if cli.enableGlobalTracer {
|
||||||
cli.createGlobalTracerProvider(cli.baseCtx)
|
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
|
||||||
}
|
}
|
||||||
@@ -283,24 +299,24 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...CLIOption)
|
|||||||
// 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()
|
opts, err := ep.ClientOpts()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -308,19 +324,8 @@ func newAPIClientFromEndpoint(ep docker.Endpoint, configFile *configfile.ConfigF
|
|||||||
if len(configFile.HTTPHeaders) > 0 {
|
if len(configFile.HTTPHeaders) > 0 {
|
||||||
opts = append(opts, client.WithHTTPHeaders(configFile.HTTPHeaders))
|
opts = append(opts, client.WithHTTPHeaders(configFile.HTTPHeaders))
|
||||||
}
|
}
|
||||||
withCustomHeaders, err := withCustomHeadersFromEnv()
|
opts = append(opts, withCustomHeadersFromEnv(), client.WithUserAgent(UserAgent()))
|
||||||
if err != nil {
|
return client.NewClientWithOpts(opts...)
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
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 +345,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
|
||||||
}
|
}
|
||||||
@@ -381,21 +383,29 @@ func (cli *DockerCli) initializeFromClient() {
|
|||||||
ctx, cancel := context.WithTimeout(cli.baseCtx, cli.getInitTimeout())
|
ctx, cancel := context.WithTimeout(cli.baseCtx, cli.getInitTimeout())
|
||||||
defer cancel()
|
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
|
||||||
@@ -465,7 +475,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,66 +488,15 @@ 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -549,12 +508,22 @@ func (cli *DockerCli) initialize() error {
|
|||||||
return cli.initErr
|
return cli.initErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply all the operation on the cli
|
||||||
|
func (cli *DockerCli) Apply(ops ...CLIOption) 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,42 +532,42 @@ 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 ...CLIOption) (*DockerCli, error) {
|
||||||
defaultOps := make([]CLIOption, 0, 3+len(ops))
|
defaultOps := []CLIOption{
|
||||||
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{baseCtx: context.Background()}
|
||||||
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("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 + ")"
|
||||||
}
|
}
|
||||||
|
|||||||
+69
-87
@@ -3,16 +3,17 @@ package command
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/csv"
|
"encoding/csv"
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/docker/cli/cli/streams"
|
"github.com/docker/cli/cli/streams"
|
||||||
"github.com/moby/moby/client"
|
"github.com/docker/docker/client"
|
||||||
|
"github.com/docker/docker/errdefs"
|
||||||
"github.com/moby/term"
|
"github.com/moby/term"
|
||||||
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CLIOption is a functional argument to apply options to a [DockerCli]. These
|
// CLIOption is a functional argument to apply options to a [DockerCli]. These
|
||||||
@@ -75,11 +76,32 @@ func WithErrorStream(err io.Writer) CLIOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithContentTrustFromEnv enables content trust on a cli from environment variable DOCKER_CONTENT_TRUST value.
|
||||||
|
func WithContentTrustFromEnv() CLIOption {
|
||||||
|
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) CLIOption {
|
||||||
|
return func(cli *DockerCli) error {
|
||||||
|
cli.contentTrust = enabled
|
||||||
|
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() CLIOption {
|
||||||
return func(cli *DockerCli) error {
|
return func(cli *DockerCli) error {
|
||||||
cfg := DefaultContextStoreConfig()
|
cli.contextStoreConfig = DefaultContextStoreConfig()
|
||||||
cli.contextStoreConfig = &cfg
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -92,28 +114,6 @@ func WithAPIClient(c client.APIClient) CLIOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
// 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
|
// 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
|
// variable is the equivalent to the HttpHeaders field in the configuration
|
||||||
@@ -168,70 +168,52 @@ const envOverrideHTTPHeaders = "DOCKER_CUSTOM_HEADERS"
|
|||||||
// override headers with the same name).
|
// 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.
|
// 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) {
|
func withCustomHeadersFromEnv() client.Opt {
|
||||||
value := os.Getenv(envOverrideHTTPHeaders)
|
return func(apiClient *client.Client) error {
|
||||||
if value == "" {
|
value := os.Getenv(envOverrideHTTPHeaders)
|
||||||
return nil, nil
|
if value == "" {
|
||||||
}
|
return nil
|
||||||
csvReader := csv.NewReader(strings.NewReader(value))
|
}
|
||||||
fields, err := csvReader.Read()
|
csvReader := csv.NewReader(strings.NewReader(value))
|
||||||
if err != nil {
|
fields, err := csvReader.Read()
|
||||||
return nil, invalidParameter(fmt.Errorf(
|
if err != nil {
|
||||||
"failed to parse custom headers from %s environment variable: value must be formatted as comma-separated key=value pairs",
|
return errdefs.InvalidParameter(errors.Errorf("failed to parse custom headers from %s environment variable: value must be formatted as comma-separated key=value pairs", envOverrideHTTPHeaders))
|
||||||
envOverrideHTTPHeaders,
|
}
|
||||||
))
|
if len(fields) == 0 {
|
||||||
}
|
return nil
|
||||||
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.
|
env := map[string]string{}
|
||||||
// This is something we could allow in future (e.g. to read value
|
for _, kv := range fields {
|
||||||
// from an environment variable with the same name). In the meantime,
|
k, v, hasValue := strings.Cut(kv, "=")
|
||||||
// produce an error to prevent users from depending on this.
|
|
||||||
if !hasValue {
|
// Only strip whitespace in keys; preserve whitespace in values.
|
||||||
return nil, invalidParameter(fmt.Errorf(
|
k = strings.TrimSpace(k)
|
||||||
`failed to set custom headers from %s environment variable: missing "=" in key=value pair: '%s'`,
|
|
||||||
envOverrideHTTPHeaders, kv,
|
if k == "" {
|
||||||
))
|
return errdefs.InvalidParameter(errors.Errorf(`failed to set custom headers from %s environment variable: value contains a key=value pair with an empty key: '%s'`, envOverrideHTTPHeaders, kv))
|
||||||
|
}
|
||||||
|
|
||||||
|
// We don't currently allow empty key=value pairs, and produce an error.
|
||||||
|
// This is something we could allow in future (e.g. to read value
|
||||||
|
// from an environment variable with the same name). In the meantime,
|
||||||
|
// produce an error to prevent users from depending on this.
|
||||||
|
if !hasValue {
|
||||||
|
return errdefs.InvalidParameter(errors.Errorf(`failed to set custom headers from %s environment variable: missing "=" in key=value pair: '%s'`, envOverrideHTTPHeaders, kv))
|
||||||
|
}
|
||||||
|
|
||||||
|
env[http.CanonicalHeaderKey(k)] = v
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
if len(env) == 0 {
|
// headers with an empty set.
|
||||||
// We should probably not hit this case, as we don't skip values
|
return nil
|
||||||
// (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
|
// TODO(thaJeztah): add a client.WithExtraHTTPHeaders() function to allow these headers to be _added_ to existing ones, instead of _replacing_
|
||||||
|
// see https://github.com/docker/cli/pull/5098#issuecomment-2147403871 (when updating, also update the WARNING in the function and env-var GoDoc)
|
||||||
|
return client.WithHTTPHeaders(env)(apiClient)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gotest.tools/v3/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func contentTrustEnabled(t *testing.T) bool {
|
||||||
|
t.Helper()
|
||||||
|
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))
|
||||||
|
}
|
||||||
+127
-174
@@ -3,7 +3,6 @@ package command
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
@@ -18,10 +17,14 @@ 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/cli/cli/streams"
|
||||||
|
"github.com/docker/docker/api"
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/client"
|
||||||
|
"github.com/pkg/errors"
|
||||||
"gotest.tools/v3/assert"
|
"gotest.tools/v3/assert"
|
||||||
|
"gotest.tools/v3/fs"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNewAPIClientFromFlags(t *testing.T) {
|
func TestNewAPIClientFromFlags(t *testing.T) {
|
||||||
@@ -33,7 +36,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 +49,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 +73,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,7 +82,7 @@ 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.NilError(t, err)
|
||||||
assert.DeepEqual(t, received, expectedHeaders)
|
assert.DeepEqual(t, received, expectedHeaders)
|
||||||
}
|
}
|
||||||
@@ -105,7 +108,7 @@ func TestNewAPIClientFromFlagsWithCustomHeadersFromEnv(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)
|
||||||
|
|
||||||
expectedHeaders := http.Header{
|
expectedHeaders := http.Header{
|
||||||
"One": []string{"one-value"},
|
"One": []string{"one-value"},
|
||||||
@@ -114,14 +117,13 @@ func TestNewAPIClientFromFlagsWithCustomHeadersFromEnv(t *testing.T) {
|
|||||||
"Four": []string{"four-value-override"},
|
"Four": []string{"four-value-override"},
|
||||||
"User-Agent": []string{UserAgent()},
|
"User-Agent": []string{UserAgent()},
|
||||||
}
|
}
|
||||||
_, err = apiClient.Ping(context.TODO(), client.PingOptions{})
|
_, err = apiClient.Ping(context.Background())
|
||||||
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 +131,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 +207,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: streams.NewOut(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,16 +338,14 @@ 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) {
|
func TestHooksEnabled(t *testing.T) {
|
||||||
t.Run("disabled by default", func(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()
|
cli, err := NewDockerCli()
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
|
|
||||||
@@ -340,11 +357,12 @@ func TestHooksEnabled(t *testing.T) {
|
|||||||
"features": {
|
"features": {
|
||||||
"hooks": "true"
|
"hooks": "true"
|
||||||
}}`
|
}}`
|
||||||
config.SetDir(t.TempDir())
|
dir := fs.NewDir(t, "", fs.WithFile("config.json", configFile))
|
||||||
err := os.WriteFile(filepath.Join(config.Dir(), "config.json"), []byte(configFile), 0o600)
|
defer dir.Remove()
|
||||||
assert.NilError(t, err)
|
|
||||||
cli, err := NewDockerCli()
|
cli, err := NewDockerCli()
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
|
config.SetDir(dir.Path())
|
||||||
|
|
||||||
assert.Check(t, cli.HooksEnabled())
|
assert.Check(t, cli.HooksEnabled())
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -354,11 +372,12 @@ func TestHooksEnabled(t *testing.T) {
|
|||||||
"hooks": "true"
|
"hooks": "true"
|
||||||
}}`
|
}}`
|
||||||
t.Setenv("DOCKER_CLI_HOOKS", "false")
|
t.Setenv("DOCKER_CLI_HOOKS", "false")
|
||||||
config.SetDir(t.TempDir())
|
dir := fs.NewDir(t, "", fs.WithFile("config.json", configFile))
|
||||||
err := os.WriteFile(filepath.Join(config.Dir(), "config.json"), []byte(configFile), 0o600)
|
defer dir.Remove()
|
||||||
assert.NilError(t, err)
|
|
||||||
cli, err := NewDockerCli()
|
cli, err := NewDockerCli()
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
|
config.SetDir(dir.Path())
|
||||||
|
|
||||||
assert.Check(t, !cli.HooksEnabled())
|
assert.Check(t, !cli.HooksEnabled())
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -368,78 +387,12 @@ func TestHooksEnabled(t *testing.T) {
|
|||||||
"hooks": "true"
|
"hooks": "true"
|
||||||
}}`
|
}}`
|
||||||
t.Setenv("DOCKER_CLI_HINTS", "false")
|
t.Setenv("DOCKER_CLI_HINTS", "false")
|
||||||
config.SetDir(t.TempDir())
|
dir := fs.NewDir(t, "", fs.WithFile("config.json", configFile))
|
||||||
err := os.WriteFile(filepath.Join(config.Dir(), "config.json"), []byte(configFile), 0o600)
|
defer dir.Remove()
|
||||||
assert.NilError(t, err)
|
|
||||||
cli, err := NewDockerCli()
|
cli, err := NewDockerCli()
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
|
config.SetDir(dir.Path())
|
||||||
|
|
||||||
assert.Check(t, !cli.HooksEnabled())
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,13 +4,20 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/distribution/reference"
|
"github.com/docker/cli/cli/command/formatter"
|
||||||
"github.com/moby/moby/api/types/container"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/moby/moby/client"
|
"github.com/docker/docker/api/types/container"
|
||||||
|
"github.com/docker/docker/api/types/image"
|
||||||
|
"github.com/docker/docker/api/types/network"
|
||||||
|
"github.com/docker/docker/api/types/volume"
|
||||||
|
"github.com/docker/docker/client"
|
||||||
"github.com/spf13/cobra"
|
"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
|
||||||
|
type ValidArgsFn func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective)
|
||||||
|
|
||||||
|
// APIClientProvider provides a method to get an [client.APIClient], initializing
|
||||||
// it if needed.
|
// it if needed.
|
||||||
//
|
//
|
||||||
// It's a smaller interface than [command.Cli], and used in situations where an
|
// It's a smaller interface than [command.Cli], and used in situations where an
|
||||||
@@ -21,62 +28,26 @@ type APIClientProvider interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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 APIClientProvider) 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(), image.ListOptions{})
|
||||||
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 _, img := range list {
|
||||||
names = append(names, img.RepoTags...)
|
names = append(names, img.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 APIClientProvider, 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(), container.ListOptions{
|
||||||
All: all,
|
All: all,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -86,7 +57,7 @@ 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 _, ctr := range list {
|
||||||
skip := false
|
skip := false
|
||||||
for _, fn := range filters {
|
for _, fn := range filters {
|
||||||
if fn != nil && !fn(ctr) {
|
if fn != nil && !fn(ctr) {
|
||||||
@@ -100,46 +71,40 @@ func ContainerNames(dockerCLI APIClientProvider, all bool, filters ...func(conta
|
|||||||
if showContainerIDs {
|
if showContainerIDs {
|
||||||
names = append(names, ctr.ID)
|
names = append(names, ctr.ID)
|
||||||
}
|
}
|
||||||
for _, n := range ctr.Names {
|
names = append(names, formatter.StripNamePrefix(ctr.Names)...)
|
||||||
// Skip legacy link names: "/linked-container/link-name"
|
|
||||||
if len(n) <= 1 || strings.IndexByte(n[1:], '/') != -1 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
names = append(names, strings.TrimPrefix(n, "/"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return names, cobra.ShellCompDirectiveNoFileComp
|
return names, cobra.ShellCompDirectiveNoFileComp
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// VolumeNames offers completion for volumes
|
// VolumeNames offers completion for volumes
|
||||||
func VolumeNames(dockerCLI APIClientProvider) cobra.CompletionFunc {
|
func VolumeNames(dockerCLI APIClientProvider) 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(), volume.ListOptions{})
|
||||||
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 _, vol := range list.Volumes {
|
||||||
names = append(names, vol.Name)
|
names = append(names, vol.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 APIClientProvider) 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(), network.ListOptions{})
|
||||||
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 _, nw := range list {
|
||||||
names = append(names, nw.Name)
|
names = append(names, nw.Name)
|
||||||
}
|
}
|
||||||
return names, cobra.ShellCompDirectiveNoFileComp
|
return names, cobra.ShellCompDirectiveNoFileComp
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// EnvVarNames offers completion for environment-variable names. This
|
// EnvVarNames offers completion for environment-variable names. This
|
||||||
@@ -155,55 +120,35 @@ func NetworkNames(dockerCLI APIClientProvider) cobra.CompletionFunc {
|
|||||||
// export MY_VAR=hello
|
// export MY_VAR=hello
|
||||||
// docker run --rm --env MY_VAR alpine printenv MY_VAR
|
// docker run --rm --env MY_VAR alpine printenv MY_VAR
|
||||||
// hello
|
// hello
|
||||||
func EnvVarNames() cobra.CompletionFunc {
|
func EnvVarNames(_ *cobra.Command, _ []string, _ string) (names []string, _ cobra.ShellCompDirective) {
|
||||||
return Unique(func(_ *cobra.Command, _ []string, _ string) (names []string, _ cobra.ShellCompDirective) {
|
envs := os.Environ()
|
||||||
envs := os.Environ()
|
names = make([]string, 0, len(envs))
|
||||||
names = make([]string, 0, len(envs))
|
for _, env := range envs {
|
||||||
for _, env := range envs {
|
name, _, _ := strings.Cut(env, "=")
|
||||||
name, _, _ := strings.Cut(env, "=")
|
names = append(names, name)
|
||||||
names = append(names, name)
|
}
|
||||||
}
|
return names, cobra.ShellCompDirectiveNoFileComp
|
||||||
return names, cobra.ShellCompDirectiveNoFileComp
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// FromList offers completion for the given list of options.
|
// FromList offers completion for the given list of options.
|
||||||
func FromList(options ...string) cobra.CompletionFunc {
|
func FromList(options ...string) ValidArgsFn {
|
||||||
return Unique(cobra.FixedCompletions(options, cobra.ShellCompDirectiveNoFileComp))
|
return 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],
|
// FileNames is a convenience function to use [cobra.ShellCompDirectiveDefault],
|
||||||
// which indicates to let the shell perform its default behavior after
|
// which indicates to let the shell perform its default behavior after
|
||||||
// completions have been provided.
|
// completions have been provided.
|
||||||
func FileNames() cobra.CompletionFunc {
|
func FileNames(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
|
||||||
return func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
|
return nil, cobra.ShellCompDirectiveDefault
|
||||||
return nil, cobra.ShellCompDirectiveDefault
|
}
|
||||||
}
|
|
||||||
|
// NoComplete is used for commands where there's no relevant completion
|
||||||
|
func NoComplete(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
}
|
}
|
||||||
|
|
||||||
var commonPlatforms = []string{
|
var commonPlatforms = []string{
|
||||||
|
"linux",
|
||||||
"linux/386",
|
"linux/386",
|
||||||
"linux/amd64",
|
"linux/amd64",
|
||||||
"linux/arm",
|
"linux/arm",
|
||||||
@@ -220,8 +165,10 @@ var commonPlatforms = []string{
|
|||||||
// Not yet supported
|
// Not yet supported
|
||||||
"linux/riscv64",
|
"linux/riscv64",
|
||||||
|
|
||||||
|
"windows",
|
||||||
"windows/amd64",
|
"windows/amd64",
|
||||||
|
|
||||||
|
"wasip1",
|
||||||
"wasip1/wasm",
|
"wasip1/wasm",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -240,43 +187,6 @@ var commonPlatforms = []string{
|
|||||||
// - we currently exclude architectures that may have unofficial builds,
|
// - we currently exclude architectures that may have unofficial builds,
|
||||||
// but don't have wide adoption (and no support), such as loong64, mipsXXX,
|
// but don't have wide adoption (and no support), such as loong64, mipsXXX,
|
||||||
// ppc64 (non-le) to prevent confusion.
|
// ppc64 (non-le) to prevent confusion.
|
||||||
func Platforms() cobra.CompletionFunc {
|
func Platforms(_ *cobra.Command, _ []string, _ string) (platforms []string, _ cobra.ShellCompDirective) {
|
||||||
return func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
|
return commonPlatforms, cobra.ShellCompDirectiveNoFileComp
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,14 +3,17 @@ package completion
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"sort"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/api/types/container"
|
||||||
|
"github.com/docker/docker/api/types/filters"
|
||||||
|
"github.com/docker/docker/api/types/image"
|
||||||
|
"github.com/docker/docker/api/types/network"
|
||||||
|
"github.com/docker/docker/api/types/volume"
|
||||||
|
"github.com/docker/docker/client"
|
||||||
"github.com/google/go-cmp/cmp/cmpopts"
|
"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"
|
"github.com/spf13/cobra"
|
||||||
"gotest.tools/v3/assert"
|
"gotest.tools/v3/assert"
|
||||||
is "gotest.tools/v3/assert/cmp"
|
is "gotest.tools/v3/assert/cmp"
|
||||||
@@ -28,48 +31,48 @@ func (c fakeCLI) Client() client.APIClient {
|
|||||||
|
|
||||||
type fakeClient struct {
|
type fakeClient struct {
|
||||||
client.Client
|
client.Client
|
||||||
containerListFunc func(context.Context, client.ContainerListOptions) (client.ContainerListResult, error)
|
containerListFunc func(options container.ListOptions) ([]types.Container, error)
|
||||||
imageListFunc func(context.Context, client.ImageListOptions) (client.ImageListResult, error)
|
imageListFunc func(options image.ListOptions) ([]image.Summary, error)
|
||||||
networkListFunc func(context.Context, client.NetworkListOptions) (client.NetworkListResult, error)
|
networkListFunc func(ctx context.Context, options network.ListOptions) ([]network.Summary, error)
|
||||||
volumeListFunc func(context.Context, client.VolumeListOptions) (client.VolumeListResult, error)
|
volumeListFunc func(filter filters.Args) (volume.ListResponse, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *fakeClient) ContainerList(ctx context.Context, options client.ContainerListOptions) (client.ContainerListResult, error) {
|
func (c *fakeClient) ContainerList(_ context.Context, options container.ListOptions) ([]types.Container, error) {
|
||||||
if c.containerListFunc != nil {
|
if c.containerListFunc != nil {
|
||||||
return c.containerListFunc(ctx, options)
|
return c.containerListFunc(options)
|
||||||
}
|
}
|
||||||
return client.ContainerListResult{}, nil
|
return []types.Container{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *fakeClient) ImageList(ctx context.Context, options client.ImageListOptions) (client.ImageListResult, error) {
|
func (c *fakeClient) ImageList(_ context.Context, options image.ListOptions) ([]image.Summary, error) {
|
||||||
if c.imageListFunc != nil {
|
if c.imageListFunc != nil {
|
||||||
return c.imageListFunc(ctx, options)
|
return c.imageListFunc(options)
|
||||||
}
|
}
|
||||||
return client.ImageListResult{}, nil
|
return []image.Summary{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *fakeClient) NetworkList(ctx context.Context, options client.NetworkListOptions) (client.NetworkListResult, error) {
|
func (c *fakeClient) NetworkList(ctx context.Context, options network.ListOptions) ([]network.Summary, error) {
|
||||||
if c.networkListFunc != nil {
|
if c.networkListFunc != nil {
|
||||||
return c.networkListFunc(ctx, options)
|
return c.networkListFunc(ctx, options)
|
||||||
}
|
}
|
||||||
return client.NetworkListResult{}, nil
|
return []network.Inspect{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *fakeClient) VolumeList(ctx context.Context, options client.VolumeListOptions) (client.VolumeListResult, error) {
|
func (c *fakeClient) VolumeList(_ context.Context, options volume.ListOptions) (volume.ListResponse, error) {
|
||||||
if c.volumeListFunc != nil {
|
if c.volumeListFunc != nil {
|
||||||
return c.volumeListFunc(ctx, options)
|
return c.volumeListFunc(options.Filters)
|
||||||
}
|
}
|
||||||
return client.VolumeListResult{}, nil
|
return volume.ListResponse{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCompleteContainerNames(t *testing.T) {
|
func TestCompleteContainerNames(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
doc string
|
doc string
|
||||||
showAll, showIDs bool
|
showAll, showIDs bool
|
||||||
filters []func(container.Summary) bool
|
filters []func(types.Container) bool
|
||||||
containers []container.Summary
|
containers []types.Container
|
||||||
expOut []string
|
expOut []string
|
||||||
expOpts client.ContainerListOptions
|
expOpts container.ListOptions
|
||||||
expDirective cobra.ShellCompDirective
|
expDirective cobra.ShellCompDirective
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
@@ -79,66 +82,66 @@ func TestCompleteContainerNames(t *testing.T) {
|
|||||||
{
|
{
|
||||||
doc: "all containers",
|
doc: "all containers",
|
||||||
showAll: true,
|
showAll: true,
|
||||||
containers: []container.Summary{
|
containers: []types.Container{
|
||||||
{ID: "id-c", State: container.StateRunning, Names: []string{"/container-c", "/container-c/link-b"}},
|
{ID: "id-c", State: "running", Names: []string{"/container-c", "/container-c/link-b"}},
|
||||||
{ID: "id-b", State: container.StateCreated, Names: []string{"/container-b"}},
|
{ID: "id-b", State: "created", Names: []string{"/container-b"}},
|
||||||
{ID: "id-a", State: container.StateExited, Names: []string{"/container-a"}},
|
{ID: "id-a", State: "exited", Names: []string{"/container-a"}},
|
||||||
},
|
},
|
||||||
expOut: []string{"container-c", "container-b", "container-a"},
|
expOut: []string{"container-c", "container-c/link-b", "container-b", "container-a"},
|
||||||
expOpts: client.ContainerListOptions{All: true},
|
expOpts: container.ListOptions{All: true},
|
||||||
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
doc: "all containers with ids",
|
doc: "all containers with ids",
|
||||||
showAll: true,
|
showAll: true,
|
||||||
showIDs: true,
|
showIDs: true,
|
||||||
containers: []container.Summary{
|
containers: []types.Container{
|
||||||
{ID: "id-c", State: container.StateRunning, Names: []string{"/container-c", "/container-c/link-b"}},
|
{ID: "id-c", State: "running", Names: []string{"/container-c", "/container-c/link-b"}},
|
||||||
{ID: "id-b", State: container.StateCreated, Names: []string{"/container-b"}},
|
{ID: "id-b", State: "created", Names: []string{"/container-b"}},
|
||||||
{ID: "id-a", State: container.StateExited, Names: []string{"/container-a"}},
|
{ID: "id-a", State: "exited", Names: []string{"/container-a"}},
|
||||||
},
|
},
|
||||||
expOut: []string{"id-c", "container-c", "id-b", "container-b", "id-a", "container-a"},
|
expOut: []string{"id-c", "container-c", "container-c/link-b", "id-b", "container-b", "id-a", "container-a"},
|
||||||
expOpts: client.ContainerListOptions{All: true},
|
expOpts: container.ListOptions{All: true},
|
||||||
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
doc: "only running containers",
|
doc: "only running containers",
|
||||||
showAll: false,
|
showAll: false,
|
||||||
containers: []container.Summary{
|
containers: []types.Container{
|
||||||
{ID: "id-c", State: container.StateRunning, Names: []string{"/container-c", "/container-c/link-b"}},
|
{ID: "id-c", State: "running", Names: []string{"/container-c", "/container-c/link-b"}},
|
||||||
},
|
},
|
||||||
expOut: []string{"container-c"},
|
expOut: []string{"container-c", "container-c/link-b"},
|
||||||
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
doc: "with filter",
|
doc: "with filter",
|
||||||
showAll: true,
|
showAll: true,
|
||||||
filters: []func(container.Summary) bool{
|
filters: []func(types.Container) bool{
|
||||||
func(ctr container.Summary) bool { return ctr.State == container.StateCreated },
|
func(container types.Container) bool { return container.State == "created" },
|
||||||
},
|
},
|
||||||
containers: []container.Summary{
|
containers: []types.Container{
|
||||||
{ID: "id-c", State: container.StateRunning, Names: []string{"/container-c"}},
|
{ID: "id-c", State: "running", Names: []string{"/container-c", "/container-c/link-b"}},
|
||||||
{ID: "id-b", State: container.StateCreated, Names: []string{"/container-b"}},
|
{ID: "id-b", State: "created", Names: []string{"/container-b"}},
|
||||||
{ID: "id-a", State: container.StateExited, Names: []string{"/container-a"}},
|
{ID: "id-a", State: "exited", Names: []string{"/container-a"}},
|
||||||
},
|
},
|
||||||
expOut: []string{"container-b"},
|
expOut: []string{"container-b"},
|
||||||
expOpts: client.ContainerListOptions{All: true},
|
expOpts: container.ListOptions{All: true},
|
||||||
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
doc: "multiple filters",
|
doc: "multiple filters",
|
||||||
showAll: true,
|
showAll: true,
|
||||||
filters: []func(container.Summary) bool{
|
filters: []func(types.Container) bool{
|
||||||
func(ctr container.Summary) bool { return ctr.ID == "id-a" },
|
func(container types.Container) bool { return container.ID == "id-a" },
|
||||||
func(ctr container.Summary) bool { return ctr.State == container.StateCreated },
|
func(container types.Container) bool { return container.State == "created" },
|
||||||
},
|
},
|
||||||
containers: []container.Summary{
|
containers: []types.Container{
|
||||||
{ID: "id-c", State: container.StateRunning, Names: []string{"/container-c"}},
|
{ID: "id-c", State: "running", Names: []string{"/container-c", "/container-c/link-b"}},
|
||||||
{ID: "id-b", State: container.StateCreated, Names: []string{"/container-b"}},
|
{ID: "id-b", State: "created", Names: []string{"/container-b"}},
|
||||||
{ID: "id-a", State: container.StateCreated, Names: []string{"/container-a"}},
|
{ID: "id-a", State: "created", Names: []string{"/container-a"}},
|
||||||
},
|
},
|
||||||
expOut: []string{"container-a"},
|
expOut: []string{"container-a"},
|
||||||
expOpts: client.ContainerListOptions{All: true},
|
expOpts: container.ListOptions{All: true},
|
||||||
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -148,17 +151,18 @@ func TestCompleteContainerNames(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
|
tc := tc
|
||||||
t.Run(tc.doc, func(t *testing.T) {
|
t.Run(tc.doc, func(t *testing.T) {
|
||||||
if tc.showIDs {
|
if tc.showIDs {
|
||||||
t.Setenv("DOCKER_COMPLETION_SHOW_CONTAINER_IDS", "yes")
|
t.Setenv("DOCKER_COMPLETION_SHOW_CONTAINER_IDS", "yes")
|
||||||
}
|
}
|
||||||
comp := ContainerNames(fakeCLI{&fakeClient{
|
comp := ContainerNames(fakeCLI{&fakeClient{
|
||||||
containerListFunc: func(_ context.Context, opts client.ContainerListOptions) (client.ContainerListResult, error) {
|
containerListFunc: func(opts container.ListOptions) ([]types.Container, error) {
|
||||||
assert.Check(t, is.DeepEqual(opts, tc.expOpts))
|
assert.Check(t, is.DeepEqual(opts, tc.expOpts, cmpopts.IgnoreUnexported(container.ListOptions{}, filters.Args{})))
|
||||||
if tc.expDirective == cobra.ShellCompDirectiveError {
|
if tc.expDirective == cobra.ShellCompDirectiveError {
|
||||||
return client.ContainerListResult{}, errors.New("some error occurred")
|
return nil, errors.New("some error occurred")
|
||||||
}
|
}
|
||||||
return client.ContainerListResult{Items: tc.containers}, nil
|
return tc.containers, nil
|
||||||
},
|
},
|
||||||
}}, tc.showAll, tc.filters...)
|
}}, tc.showAll, tc.filters...)
|
||||||
|
|
||||||
@@ -174,17 +178,16 @@ func TestCompleteEnvVarNames(t *testing.T) {
|
|||||||
"ENV_A": "hello-a",
|
"ENV_A": "hello-a",
|
||||||
"ENV_B": "hello-b",
|
"ENV_B": "hello-b",
|
||||||
})
|
})
|
||||||
values, directives := EnvVarNames()(nil, nil, "")
|
values, directives := EnvVarNames(nil, nil, "")
|
||||||
assert.Check(t, is.Equal(directives&cobra.ShellCompDirectiveNoFileComp, cobra.ShellCompDirectiveNoFileComp), "Should not perform file completion")
|
assert.Check(t, is.Equal(directives&cobra.ShellCompDirectiveNoFileComp, cobra.ShellCompDirectiveNoFileComp), "Should not perform file completion")
|
||||||
|
|
||||||
|
sort.Strings(values)
|
||||||
expected := []string{"ENV_A", "ENV_B"}
|
expected := []string{"ENV_A", "ENV_B"}
|
||||||
assert.Check(t, is.DeepEqual(values, expected, cmpopts.SortSlices(func(a, b string) bool {
|
assert.Check(t, is.DeepEqual(values, expected))
|
||||||
return a < b
|
|
||||||
})))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCompleteFileNames(t *testing.T) {
|
func TestCompleteFileNames(t *testing.T) {
|
||||||
values, directives := FileNames()(nil, nil, "")
|
values, directives := FileNames(nil, nil, "")
|
||||||
assert.Check(t, is.Equal(directives, cobra.ShellCompDirectiveDefault))
|
assert.Check(t, is.Equal(directives, cobra.ShellCompDirectiveDefault))
|
||||||
assert.Check(t, is.Len(values, 0))
|
assert.Check(t, is.Len(values, 0))
|
||||||
}
|
}
|
||||||
@@ -197,18 +200,6 @@ func TestCompleteFromList(t *testing.T) {
|
|||||||
assert.Check(t, is.DeepEqual(values, expected))
|
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) {
|
func TestCompleteImageNames(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
doc string
|
doc string
|
||||||
@@ -237,15 +228,16 @@ func TestCompleteImageNames(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
|
tc := tc
|
||||||
t.Run(tc.doc, func(t *testing.T) {
|
t.Run(tc.doc, func(t *testing.T) {
|
||||||
comp := ImageNames(fakeCLI{&fakeClient{
|
comp := ImageNames(fakeCLI{&fakeClient{
|
||||||
imageListFunc: func(context.Context, client.ImageListOptions) (client.ImageListResult, error) {
|
imageListFunc: func(options image.ListOptions) ([]image.Summary, error) {
|
||||||
if tc.expDirective == cobra.ShellCompDirectiveError {
|
if tc.expDirective == cobra.ShellCompDirectiveError {
|
||||||
return client.ImageListResult{}, errors.New("some error occurred")
|
return nil, errors.New("some error occurred")
|
||||||
}
|
}
|
||||||
return client.ImageListResult{Items: tc.images}, nil
|
return tc.images, nil
|
||||||
},
|
},
|
||||||
}}, -1)
|
}})
|
||||||
|
|
||||||
volumes, directives := comp(&cobra.Command{}, nil, "")
|
volumes, directives := comp(&cobra.Command{}, nil, "")
|
||||||
assert.Check(t, is.Equal(directives&tc.expDirective, tc.expDirective))
|
assert.Check(t, is.Equal(directives&tc.expDirective, tc.expDirective))
|
||||||
@@ -268,24 +260,9 @@ func TestCompleteNetworkNames(t *testing.T) {
|
|||||||
{
|
{
|
||||||
doc: "with results",
|
doc: "with results",
|
||||||
networks: []network.Summary{
|
networks: []network.Summary{
|
||||||
{
|
{ID: "nw-c", Name: "network-c"},
|
||||||
Network: network.Network{
|
{ID: "nw-b", Name: "network-b"},
|
||||||
ID: "nw-c",
|
{ID: "nw-a", Name: "network-a"},
|
||||||
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"},
|
expOut: []string{"network-c", "network-b", "network-a"},
|
||||||
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
||||||
@@ -297,13 +274,14 @@ func TestCompleteNetworkNames(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
|
tc := tc
|
||||||
t.Run(tc.doc, func(t *testing.T) {
|
t.Run(tc.doc, func(t *testing.T) {
|
||||||
comp := NetworkNames(fakeCLI{&fakeClient{
|
comp := NetworkNames(fakeCLI{&fakeClient{
|
||||||
networkListFunc: func(context.Context, client.NetworkListOptions) (client.NetworkListResult, error) {
|
networkListFunc: func(ctx context.Context, options network.ListOptions) ([]network.Summary, error) {
|
||||||
if tc.expDirective == cobra.ShellCompDirectiveError {
|
if tc.expDirective == cobra.ShellCompDirectiveError {
|
||||||
return client.NetworkListResult{}, errors.New("some error occurred")
|
return nil, errors.New("some error occurred")
|
||||||
}
|
}
|
||||||
return client.NetworkListResult{Items: tc.networks}, nil
|
return tc.networks, nil
|
||||||
},
|
},
|
||||||
}})
|
}})
|
||||||
|
|
||||||
@@ -314,8 +292,14 @@ func TestCompleteNetworkNames(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCompleteNoComplete(t *testing.T) {
|
||||||
|
values, directives := NoComplete(nil, nil, "")
|
||||||
|
assert.Check(t, is.Equal(directives, cobra.ShellCompDirectiveNoFileComp))
|
||||||
|
assert.Check(t, is.Len(values, 0))
|
||||||
|
}
|
||||||
|
|
||||||
func TestCompletePlatforms(t *testing.T) {
|
func TestCompletePlatforms(t *testing.T) {
|
||||||
values, directives := Platforms()(nil, nil, "")
|
values, directives := Platforms(nil, nil, "")
|
||||||
assert.Check(t, is.Equal(directives&cobra.ShellCompDirectiveNoFileComp, cobra.ShellCompDirectiveNoFileComp), "Should not perform file completion")
|
assert.Check(t, is.Equal(directives&cobra.ShellCompDirectiveNoFileComp, cobra.ShellCompDirectiveNoFileComp), "Should not perform file completion")
|
||||||
assert.Check(t, is.DeepEqual(values, commonPlatforms))
|
assert.Check(t, is.DeepEqual(values, commonPlatforms))
|
||||||
}
|
}
|
||||||
@@ -323,7 +307,7 @@ func TestCompletePlatforms(t *testing.T) {
|
|||||||
func TestCompleteVolumeNames(t *testing.T) {
|
func TestCompleteVolumeNames(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
doc string
|
doc string
|
||||||
volumes []volume.Volume
|
volumes []*volume.Volume
|
||||||
expOut []string
|
expOut []string
|
||||||
expDirective cobra.ShellCompDirective
|
expDirective cobra.ShellCompDirective
|
||||||
}{
|
}{
|
||||||
@@ -333,7 +317,7 @@ func TestCompleteVolumeNames(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
doc: "with results",
|
doc: "with results",
|
||||||
volumes: []volume.Volume{
|
volumes: []*volume.Volume{
|
||||||
{Name: "volume-c"},
|
{Name: "volume-c"},
|
||||||
{Name: "volume-b"},
|
{Name: "volume-b"},
|
||||||
{Name: "volume-a"},
|
{Name: "volume-a"},
|
||||||
@@ -348,13 +332,14 @@ func TestCompleteVolumeNames(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
|
tc := tc
|
||||||
t.Run(tc.doc, func(t *testing.T) {
|
t.Run(tc.doc, func(t *testing.T) {
|
||||||
comp := VolumeNames(fakeCLI{&fakeClient{
|
comp := VolumeNames(fakeCLI{&fakeClient{
|
||||||
volumeListFunc: func(context.Context, client.VolumeListOptions) (client.VolumeListResult, error) {
|
volumeListFunc: func(filter filters.Args) (volume.ListResponse, error) {
|
||||||
if tc.expDirective == cobra.ShellCompDirectiveError {
|
if tc.expDirective == cobra.ShellCompDirectiveError {
|
||||||
return client.VolumeListResult{}, errors.New("some error occurred")
|
return volume.ListResponse{}, errors.New("some error occurred")
|
||||||
}
|
}
|
||||||
return client.VolumeListResult{Items: tc.volumes}, nil
|
return volume.ListResponse{Volumes: tc.volumes}, nil
|
||||||
},
|
},
|
||||||
}})
|
}})
|
||||||
|
|
||||||
@@ -364,23 +349,3 @@ func TestCompleteVolumeNames(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 completion.APIClientProvider) 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,56 @@ func newConfigCreateCommand(dockerCLI command.Cli) *cobra.Command {
|
|||||||
Short: "Create a config from a file or STDIN",
|
Short: "Create a config from a file or STDIN",
|
||||||
Args: cli.ExactArgs(2),
|
Args: cli.ExactArgs(2),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
createOpts.name = args[0]
|
createOpts.Name = args[0]
|
||||||
createOpts.file = args[1]
|
createOpts.File = args[1]
|
||||||
return runCreate(cmd.Context(), dockerCLI, createOpts)
|
return RunConfigCreate(cmd.Context(), 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(ctx context.Context, dockerCli command.Cli, options CreateOptions) error {
|
||||||
apiClient := dockerCLI.Client()
|
client := dockerCli.Client()
|
||||||
|
|
||||||
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,26 +23,27 @@ 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 {
|
||||||
|
tc := tc
|
||||||
t.Run(tc.expectedError, func(t *testing.T) {
|
t.Run(tc.expectedError, func(t *testing.T) {
|
||||||
cmd := newConfigCreateCommand(
|
cmd := newConfigCreateCommand(
|
||||||
test.NewFakeCli(&fakeClient{
|
test.NewFakeCli(&fakeClient{
|
||||||
@@ -59,18 +59,18 @@ func TestConfigCreateErrors(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 +87,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 +101,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 +124,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 {
|
||||||
@@ -106,7 +105,6 @@ func (c *configContext) Labels() string {
|
|||||||
for k, v := range mapLabels {
|
for k, v := range mapLabels {
|
||||||
joinLabels = append(joinLabels, k+"="+v)
|
joinLabels = append(joinLabels, 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,5 +1,5 @@
|
|||||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
||||||
//go:build go1.26
|
//go:build go1.22
|
||||||
|
|
||||||
package config
|
package config
|
||||||
|
|
||||||
@@ -12,61 +12,61 @@ 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(cmd.Context(), 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(ctx context.Context, dockerCli command.Cli, opts InspectOptions) error {
|
||||||
apiClient := dockerCLI.Client()
|
client := dockerCli.Client()
|
||||||
|
|
||||||
if opts.pretty {
|
if opts.Pretty {
|
||||||
opts.format = "pretty"
|
opts.Format = "pretty"
|
||||||
}
|
}
|
||||||
|
|
||||||
getRef := func(id string) (any, []byte, error) {
|
getRef := func(id string) (any, []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",
|
||||||
},
|
},
|
||||||
@@ -72,34 +70,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 +102,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 +141,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
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
+27
-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,44 @@ 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(cmd.Context(), 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.StringVar(&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(ctx context.Context, dockerCli command.Cli, options ListOptions) error {
|
||||||
apiClient := dockerCLI.Client()
|
client := dockerCli.Client()
|
||||||
|
|
||||||
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",
|
||||||
},
|
},
|
||||||
@@ -48,28 +49,26 @@ func TestConfigListErrors(t *testing.T) {
|
|||||||
|
|
||||||
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 +79,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 +96,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 +115,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 +132,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,56 @@ 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(cmd.Context(), 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(ctx context.Context, dockerCli command.Cli, opts RemoveOptions) error {
|
||||||
apiClient := dockerCLI.Client()
|
client := dockerCli.Client()
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"io"
|
"io"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/docker/cli/internal/test"
|
"github.com/docker/cli/internal/test"
|
||||||
"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"
|
||||||
)
|
)
|
||||||
@@ -16,17 +14,17 @@ import (
|
|||||||
func TestConfigRemoveErrors(t *testing.T) {
|
func TestConfigRemoveErrors(t *testing.T) {
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
args []string
|
args []string
|
||||||
configRemoveFunc func(context.Context, string, client.ConfigRemoveOptions) (client.ConfigRemoveResult, error)
|
configRemoveFunc func(string) error
|
||||||
expectedError string
|
expectedError string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
args: []string{},
|
args: []string{},
|
||||||
expectedError: "requires at least 1 argument",
|
expectedError: "requires at least 1 argument.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
args: []string{"foo"},
|
args: []string{"foo"},
|
||||||
configRemoveFunc: func(ctx context.Context, name string, options client.ConfigRemoveOptions) (client.ConfigRemoveResult, error) {
|
configRemoveFunc: func(name string) error {
|
||||||
return client.ConfigRemoveResult{}, errors.New("error removing config")
|
return errors.Errorf("error removing config")
|
||||||
},
|
},
|
||||||
expectedError: "error removing config",
|
expectedError: "error removing config",
|
||||||
},
|
},
|
||||||
@@ -48,9 +46,9 @@ func TestConfigRemoveWithName(t *testing.T) {
|
|||||||
names := []string{"foo", "bar"}
|
names := []string{"foo", "bar"}
|
||||||
var removedConfigs []string
|
var removedConfigs []string
|
||||||
cli := test.NewFakeCli(&fakeClient{
|
cli := test.NewFakeCli(&fakeClient{
|
||||||
configRemoveFunc: func(_ context.Context, name string, _ client.ConfigRemoveOptions) (client.ConfigRemoveResult, error) {
|
configRemoveFunc: func(name string) error {
|
||||||
removedConfigs = append(removedConfigs, name)
|
removedConfigs = append(removedConfigs, name)
|
||||||
return client.ConfigRemoveResult{}, nil
|
return nil
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
cmd := newConfigRemoveCommand(cli)
|
cmd := newConfigRemoveCommand(cli)
|
||||||
@@ -65,12 +63,12 @@ func TestConfigRemoveContinueAfterError(t *testing.T) {
|
|||||||
var removedConfigs []string
|
var removedConfigs []string
|
||||||
|
|
||||||
cli := test.NewFakeCli(&fakeClient{
|
cli := test.NewFakeCli(&fakeClient{
|
||||||
configRemoveFunc: func(_ context.Context, name string, _ client.ConfigRemoveOptions) (client.ConfigRemoveResult, error) {
|
configRemoveFunc: func(name string) error {
|
||||||
removedConfigs = append(removedConfigs, name)
|
removedConfigs = append(removedConfigs, name)
|
||||||
if name == "foo" {
|
if name == "foo" {
|
||||||
return client.ConfigRemoveResult{}, errors.New("error removing config: " + name)
|
return errors.Errorf("error removing config: %s", name)
|
||||||
}
|
}
|
||||||
return client.ConfigRemoveResult{}, nil
|
return nil
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -2,15 +2,16 @@ package container
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"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/cli/command/completion"
|
||||||
"github.com/moby/moby/api/types/container"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/moby/moby/client"
|
"github.com/docker/docker/api/types/container"
|
||||||
|
"github.com/docker/docker/client"
|
||||||
"github.com/moby/sys/signal"
|
"github.com/moby/sys/signal"
|
||||||
|
"github.com/pkg/errors"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
@@ -22,26 +23,26 @@ type AttachOptions struct {
|
|||||||
DetachKeys string
|
DetachKeys string
|
||||||
}
|
}
|
||||||
|
|
||||||
func inspectContainerAndCheckState(ctx context.Context, apiClient client.APIClient, args string) (*container.InspectResponse, error) {
|
func inspectContainerAndCheckState(ctx context.Context, apiClient client.APIClient, args string) (*types.ContainerJSON, error) {
|
||||||
c, err := apiClient.ContainerInspect(ctx, args, client.ContainerInspectOptions{})
|
c, err := apiClient.ContainerInspect(ctx, args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if !c.Container.State.Running {
|
if !c.State.Running {
|
||||||
return nil, errors.New("cannot attach to a stopped container, start it first")
|
return nil, errors.New("You cannot attach to a stopped container, start it first")
|
||||||
}
|
}
|
||||||
if c.Container.State.Paused {
|
if c.State.Paused {
|
||||||
return nil, errors.New("cannot attach to a paused container, unpause it first")
|
return nil, errors.New("You cannot attach to a paused container, unpause it first")
|
||||||
}
|
}
|
||||||
if c.Container.State.Restarting {
|
if c.State.Restarting {
|
||||||
return nil, errors.New("cannot attach to a restarting container, wait until it is running")
|
return nil, errors.New("You cannot attach to a restarting container, wait until it is running")
|
||||||
}
|
}
|
||||||
|
|
||||||
return &c.Container, nil
|
return &c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// newAttachCommand creates a new cobra.Command for `docker attach`
|
// NewAttachCommand creates a new cobra.Command for `docker attach`
|
||||||
func newAttachCommand(dockerCLI command.Cli) *cobra.Command {
|
func NewAttachCommand(dockerCLI command.Cli) *cobra.Command {
|
||||||
var opts AttachOptions
|
var opts AttachOptions
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
@@ -55,10 +56,9 @@ func newAttachCommand(dockerCLI command.Cli) *cobra.Command {
|
|||||||
Annotations: map[string]string{
|
Annotations: map[string]string{
|
||||||
"aliases": "docker container attach, docker attach",
|
"aliases": "docker container attach, docker attach",
|
||||||
},
|
},
|
||||||
ValidArgsFunction: completion.ContainerNames(dockerCLI, false, func(ctr container.Summary) bool {
|
ValidArgsFunction: completion.ContainerNames(dockerCLI, false, func(ctr types.Container) bool {
|
||||||
return ctr.State != container.StatePaused
|
return ctr.State != "paused"
|
||||||
}),
|
}),
|
||||||
DisableFlagsInUseLine: true,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
flags := cmd.Flags()
|
flags := cmd.Flags()
|
||||||
@@ -70,19 +70,11 @@ func newAttachCommand(dockerCLI command.Cli) *cobra.Command {
|
|||||||
|
|
||||||
// RunAttach executes an `attach` command
|
// RunAttach executes an `attach` command
|
||||||
func RunAttach(ctx context.Context, dockerCLI command.Cli, containerID string, opts *AttachOptions) error {
|
func RunAttach(ctx context.Context, dockerCLI command.Cli, containerID string, opts *AttachOptions) error {
|
||||||
detachKeys := opts.DetachKeys
|
|
||||||
if detachKeys == "" {
|
|
||||||
detachKeys = dockerCLI.ConfigFile().DetachKeys
|
|
||||||
}
|
|
||||||
if err := validateDetachKeys(detachKeys); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
apiClient := dockerCLI.Client()
|
apiClient := dockerCLI.Client()
|
||||||
|
|
||||||
// request channel to wait for client
|
// request channel to wait for client
|
||||||
waitCtx := context.WithoutCancel(ctx)
|
waitCtx := context.WithoutCancel(ctx)
|
||||||
waitRes := apiClient.ContainerWait(waitCtx, containerID, client.ContainerWaitOptions{})
|
resultC, errC := apiClient.ContainerWait(waitCtx, containerID, "")
|
||||||
|
|
||||||
c, err := inspectContainerAndCheckState(ctx, apiClient, containerID)
|
c, err := inspectContainerAndCheckState(ctx, apiClient, containerID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -93,7 +85,12 @@ func RunAttach(ctx context.Context, dockerCLI command.Cli, containerID string, o
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
options := client.ContainerAttachOptions{
|
detachKeys := dockerCLI.ConfigFile().DetachKeys
|
||||||
|
if opts.DetachKeys != "" {
|
||||||
|
detachKeys = opts.DetachKeys
|
||||||
|
}
|
||||||
|
|
||||||
|
options := container.AttachOptions{
|
||||||
Stream: true,
|
Stream: true,
|
||||||
Stdin: !opts.NoStdin && c.Config.OpenStdin,
|
Stdin: !opts.NoStdin && c.Config.OpenStdin,
|
||||||
Stdout: true,
|
Stdout: true,
|
||||||
@@ -117,11 +114,11 @@ func RunAttach(ctx context.Context, dockerCLI command.Cli, containerID string, o
|
|||||||
defer signal.StopCatch(sigc)
|
defer signal.StopCatch(sigc)
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := apiClient.ContainerAttach(ctx, containerID, options)
|
resp, errAttach := apiClient.ContainerAttach(ctx, containerID, options)
|
||||||
if err != nil {
|
if errAttach != nil {
|
||||||
return err
|
return errAttach
|
||||||
}
|
}
|
||||||
defer res.HijackedResponse.Close()
|
defer resp.Close()
|
||||||
|
|
||||||
// If use docker attach command to attach to a stop container, it will return
|
// If use docker attach command to attach to a stop container, it will return
|
||||||
// "You cannot attach to a stopped container" error, it's ok, but when
|
// "You cannot attach to a stopped container" error, it's ok, but when
|
||||||
@@ -145,7 +142,7 @@ func RunAttach(ctx context.Context, dockerCLI command.Cli, containerID string, o
|
|||||||
inputStream: in,
|
inputStream: in,
|
||||||
outputStream: dockerCLI.Out(),
|
outputStream: dockerCLI.Out(),
|
||||||
errorStream: dockerCLI.Err(),
|
errorStream: dockerCLI.Err(),
|
||||||
resp: res.HijackedResponse,
|
resp: resp,
|
||||||
tty: c.Config.Tty,
|
tty: c.Config.Tty,
|
||||||
detachKeys: options.DetachKeys,
|
detachKeys: options.DetachKeys,
|
||||||
}
|
}
|
||||||
@@ -155,19 +152,22 @@ func RunAttach(ctx context.Context, dockerCLI command.Cli, containerID string, o
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return getExitStatus(waitRes)
|
return getExitStatus(errC, resultC)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getExitStatus(waitRes client.ContainerWaitResult) error {
|
func getExitStatus(errC <-chan error, resultC <-chan container.WaitResponse) error {
|
||||||
select {
|
select {
|
||||||
case result := <-waitRes.Result:
|
case result := <-resultC:
|
||||||
if result.Error != nil {
|
if result.Error != nil {
|
||||||
return errors.New(result.Error.Message)
|
return errors.New(result.Error.Message)
|
||||||
}
|
}
|
||||||
if result.StatusCode != 0 {
|
if result.StatusCode != 0 {
|
||||||
return cli.StatusError{StatusCode: int(result.StatusCode)}
|
return cli.StatusError{StatusCode: int(result.StatusCode)}
|
||||||
}
|
}
|
||||||
case err := <-waitRes.Error:
|
case err := <-errC:
|
||||||
|
if errors.Is(err, context.Canceled) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,7 +180,7 @@ func resizeTTY(ctx context.Context, dockerCli command.Cli, containerID string) {
|
|||||||
// terminal, the only way to get the shell prompt to display for attaches 2+ is to artificially
|
// terminal, the only way to get the shell prompt to display for attaches 2+ is to artificially
|
||||||
// resize it, then go back to normal. Without this, every attach after the first will
|
// resize it, then go back to normal. Without this, every attach after the first will
|
||||||
// require the user to manually resize or hit enter.
|
// require the user to manually resize or hit enter.
|
||||||
resizeTTYTo(ctx, dockerCli.Client(), containerID, height+1, width+1, false)
|
resizeTtyTo(ctx, dockerCli.Client(), containerID, height+1, width+1, false)
|
||||||
|
|
||||||
// After the above resizing occurs, the call to MonitorTtySize below will handle resetting back
|
// After the above resizing occurs, the call to MonitorTtySize below will handle resetting back
|
||||||
// to the actual size.
|
// to the actual size.
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
package container
|
package container
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"context"
|
||||||
"io"
|
"io"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/docker/cli/cli"
|
"github.com/docker/cli/cli"
|
||||||
"github.com/docker/cli/internal/test"
|
"github.com/docker/cli/internal/test"
|
||||||
"github.com/moby/moby/api/types/container"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/moby/moby/client"
|
"github.com/docker/docker/api/types/container"
|
||||||
|
"github.com/pkg/errors"
|
||||||
"gotest.tools/v3/assert"
|
"gotest.tools/v3/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -17,73 +18,61 @@ func TestNewAttachCommandErrors(t *testing.T) {
|
|||||||
name string
|
name string
|
||||||
args []string
|
args []string
|
||||||
expectedError string
|
expectedError string
|
||||||
containerInspectFunc func(img string) (client.ContainerInspectResult, error)
|
containerInspectFunc func(img string) (types.ContainerJSON, error)
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "client-error",
|
name: "client-error",
|
||||||
args: []string{"5cb5bb5e4a3b"},
|
args: []string{"5cb5bb5e4a3b"},
|
||||||
expectedError: "something went wrong",
|
expectedError: "something went wrong",
|
||||||
containerInspectFunc: func(containerID string) (client.ContainerInspectResult, error) {
|
containerInspectFunc: func(containerID string) (types.ContainerJSON, error) {
|
||||||
return client.ContainerInspectResult{}, errors.New("something went wrong")
|
return types.ContainerJSON{}, errors.Errorf("something went wrong")
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid-detach-keys",
|
|
||||||
args: []string{"--detach-keys", "shift-b", "5cb5bb5e4a3b"},
|
|
||||||
expectedError: "invalid detach keys (shift-b):",
|
|
||||||
containerInspectFunc: func(containerID string) (client.ContainerInspectResult, error) {
|
|
||||||
return client.ContainerInspectResult{}, errors.New("something went wrong")
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "client-stopped",
|
name: "client-stopped",
|
||||||
args: []string{"5cb5bb5e4a3b"},
|
args: []string{"5cb5bb5e4a3b"},
|
||||||
expectedError: "cannot attach to a stopped container",
|
expectedError: "You cannot attach to a stopped container",
|
||||||
containerInspectFunc: func(containerID string) (client.ContainerInspectResult, error) {
|
containerInspectFunc: func(containerID string) (types.ContainerJSON, error) {
|
||||||
return client.ContainerInspectResult{
|
c := types.ContainerJSON{}
|
||||||
Container: container.InspectResponse{
|
c.ContainerJSONBase = &types.ContainerJSONBase{}
|
||||||
State: &container.State{
|
c.ContainerJSONBase.State = &types.ContainerState{Running: false}
|
||||||
Running: false,
|
return c, nil
|
||||||
},
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "client-paused",
|
name: "client-paused",
|
||||||
args: []string{"5cb5bb5e4a3b"},
|
args: []string{"5cb5bb5e4a3b"},
|
||||||
expectedError: "cannot attach to a paused container",
|
expectedError: "You cannot attach to a paused container",
|
||||||
containerInspectFunc: func(containerID string) (client.ContainerInspectResult, error) {
|
containerInspectFunc: func(containerID string) (types.ContainerJSON, error) {
|
||||||
return client.ContainerInspectResult{
|
c := types.ContainerJSON{}
|
||||||
Container: container.InspectResponse{
|
c.ContainerJSONBase = &types.ContainerJSONBase{}
|
||||||
State: &container.State{
|
c.ContainerJSONBase.State = &types.ContainerState{
|
||||||
Running: true,
|
Running: true,
|
||||||
Paused: true,
|
Paused: true,
|
||||||
},
|
}
|
||||||
},
|
return c, nil
|
||||||
}, nil
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "client-restarting",
|
name: "client-restarting",
|
||||||
args: []string{"5cb5bb5e4a3b"},
|
args: []string{"5cb5bb5e4a3b"},
|
||||||
expectedError: "cannot attach to a restarting container",
|
expectedError: "You cannot attach to a restarting container",
|
||||||
containerInspectFunc: func(containerID string) (client.ContainerInspectResult, error) {
|
containerInspectFunc: func(containerID string) (types.ContainerJSON, error) {
|
||||||
return client.ContainerInspectResult{
|
c := types.ContainerJSON{}
|
||||||
Container: container.InspectResponse{
|
c.ContainerJSONBase = &types.ContainerJSONBase{}
|
||||||
State: &container.State{
|
c.ContainerJSONBase.State = &types.ContainerState{
|
||||||
Running: true,
|
Running: true,
|
||||||
Paused: false,
|
Paused: false,
|
||||||
Restarting: true,
|
Restarting: true,
|
||||||
},
|
}
|
||||||
},
|
return c, nil
|
||||||
}, nil
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
|
tc := tc
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
cmd := newAttachCommand(test.NewFakeCli(&fakeClient{inspectFunc: tc.containerInspectFunc}))
|
cmd := NewAttachCommand(test.NewFakeCli(&fakeClient{inspectFunc: tc.containerInspectFunc}))
|
||||||
cmd.SetOut(io.Discard)
|
cmd.SetOut(io.Discard)
|
||||||
cmd.SetErr(io.Discard)
|
cmd.SetErr(io.Discard)
|
||||||
cmd.SetArgs(tc.args)
|
cmd.SetArgs(tc.args)
|
||||||
@@ -121,6 +110,10 @@ func TestGetExitStatus(t *testing.T) {
|
|||||||
},
|
},
|
||||||
expectedError: cli.StatusError{StatusCode: 15},
|
expectedError: cli.StatusError{StatusCode: 15},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
err: context.Canceled,
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, testcase := range testcases {
|
for _, testcase := range testcases {
|
||||||
@@ -133,10 +126,7 @@ func TestGetExitStatus(t *testing.T) {
|
|||||||
resultC <- *testcase.result
|
resultC <- *testcase.result
|
||||||
}
|
}
|
||||||
|
|
||||||
err := getExitStatus(client.ContainerWaitResult{
|
err := getExitStatus(errC, resultC)
|
||||||
Result: resultC,
|
|
||||||
Error: errC,
|
|
||||||
})
|
|
||||||
|
|
||||||
if testcase.expectedError == nil {
|
if testcase.expectedError == nil {
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
package container
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/docker/cli/cli/config"
|
|
||||||
"github.com/docker/cli/cli/config/configfile"
|
|
||||||
"github.com/docker/cli/cli/config/types"
|
|
||||||
)
|
|
||||||
|
|
||||||
// readCredentials resolves auth-config from the current environment to be
|
|
||||||
// applied to the container if the `--use-api-socket` flag is set.
|
|
||||||
//
|
|
||||||
// - If a valid "DOCKER_AUTH_CONFIG" env-var is found, and it contains
|
|
||||||
// credentials, it's value is used.
|
|
||||||
// - If no "DOCKER_AUTH_CONFIG" env-var is found, or it does not contain
|
|
||||||
// credentials, it attempts to read from the CLI's credentials store.
|
|
||||||
//
|
|
||||||
// It returns an error if either the "DOCKER_AUTH_CONFIG" is incorrectly
|
|
||||||
// formatted, or when failing to read from the credentials store.
|
|
||||||
//
|
|
||||||
// A nil value is returned if neither option contained any credentials.
|
|
||||||
func readCredentials(dockerCLI config.Provider) (creds map[string]types.AuthConfig, _ error) {
|
|
||||||
if v, ok := os.LookupEnv("DOCKER_AUTH_CONFIG"); ok && v != "" {
|
|
||||||
// The results are expected to have been unmarshaled the same as
|
|
||||||
// when reading from a config-file, which includes decoding the
|
|
||||||
// base64-encoded "username:password" into the "UserName" and
|
|
||||||
// "Password" fields.
|
|
||||||
ac := &configfile.ConfigFile{}
|
|
||||||
if err := ac.LoadFromReader(strings.NewReader(v)); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to read credentials from DOCKER_AUTH_CONFIG: %w", err)
|
|
||||||
}
|
|
||||||
if len(ac.AuthConfigs) > 0 {
|
|
||||||
return ac.AuthConfigs, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve this here for later, ensuring we error our before we create the container.
|
|
||||||
creds, err := dockerCLI.ConfigFile().GetAllCredentials()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("resolving credentials failed: %w", err)
|
|
||||||
}
|
|
||||||
return creds, nil
|
|
||||||
}
|
|
||||||
@@ -3,244 +3,181 @@ package container
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/moby/moby/client"
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/api/types/container"
|
||||||
|
"github.com/docker/docker/api/types/filters"
|
||||||
|
"github.com/docker/docker/api/types/image"
|
||||||
|
"github.com/docker/docker/api/types/network"
|
||||||
|
"github.com/docker/docker/api/types/system"
|
||||||
|
"github.com/docker/docker/client"
|
||||||
|
specs "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
func mockContainerExportResult(content string) client.ContainerExportResult {
|
|
||||||
return io.NopCloser(strings.NewReader(content))
|
|
||||||
}
|
|
||||||
|
|
||||||
func mockContainerLogsResult(content string) client.ContainerLogsResult {
|
|
||||||
return io.NopCloser(strings.NewReader(content))
|
|
||||||
}
|
|
||||||
|
|
||||||
type fakeStreamResult struct {
|
|
||||||
io.ReadCloser
|
|
||||||
client.ImagePushResponse // same interface as [client.ImagePushResponse]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e fakeStreamResult) Read(p []byte) (int, error) { return e.ReadCloser.Read(p) }
|
|
||||||
func (e fakeStreamResult) Close() error { return e.ReadCloser.Close() }
|
|
||||||
|
|
||||||
type fakeClient struct {
|
type fakeClient struct {
|
||||||
client.Client
|
client.Client
|
||||||
inspectFunc func(string) (client.ContainerInspectResult, error)
|
inspectFunc func(string) (types.ContainerJSON, error)
|
||||||
execInspectFunc func(execID string) (client.ExecInspectResult, error)
|
execInspectFunc func(execID string) (container.ExecInspect, error)
|
||||||
execCreateFunc func(containerID string, options client.ExecCreateOptions) (client.ExecCreateResult, error)
|
execCreateFunc func(containerID string, options container.ExecOptions) (types.IDResponse, error)
|
||||||
createContainerFunc func(options client.ContainerCreateOptions) (client.ContainerCreateResult, error)
|
createContainerFunc func(config *container.Config,
|
||||||
containerStartFunc func(containerID string, options client.ContainerStartOptions) (client.ContainerStartResult, error)
|
hostConfig *container.HostConfig,
|
||||||
imagePullFunc func(ctx context.Context, parentReference string, options client.ImagePullOptions) (client.ImagePullResponse, error)
|
networkingConfig *network.NetworkingConfig,
|
||||||
infoFunc func() (client.SystemInfoResult, error)
|
platform *specs.Platform,
|
||||||
containerStatPathFunc func(containerID, path string) (client.ContainerStatPathResult, error)
|
containerName string) (container.CreateResponse, error)
|
||||||
containerCopyFromFunc func(containerID, srcPath string) (client.CopyFromContainerResult, error)
|
containerStartFunc func(containerID string, options container.StartOptions) error
|
||||||
containerCopyToFunc func(containerID string, options client.CopyToContainerOptions) (client.CopyToContainerResult, error)
|
imageCreateFunc func(ctx context.Context, parentReference string, options image.CreateOptions) (io.ReadCloser, error)
|
||||||
logFunc func(string, client.ContainerLogsOptions) (client.ContainerLogsResult, error)
|
infoFunc func() (system.Info, error)
|
||||||
waitFunc func(string) client.ContainerWaitResult
|
containerStatPathFunc func(containerID, path string) (container.PathStat, error)
|
||||||
containerListFunc func(client.ContainerListOptions) (client.ContainerListResult, error)
|
containerCopyFromFunc func(containerID, srcPath string) (io.ReadCloser, container.PathStat, error)
|
||||||
containerExportFunc func(string) (client.ContainerExportResult, error)
|
logFunc func(string, container.LogsOptions) (io.ReadCloser, error)
|
||||||
containerExecResizeFunc func(id string, options client.ExecResizeOptions) (client.ExecResizeResult, error)
|
waitFunc func(string) (<-chan container.WaitResponse, <-chan error)
|
||||||
containerRemoveFunc func(ctx context.Context, containerID string, options client.ContainerRemoveOptions) (client.ContainerRemoveResult, error)
|
containerListFunc func(container.ListOptions) ([]types.Container, error)
|
||||||
containerRestartFunc func(ctx context.Context, containerID string, options client.ContainerRestartOptions) (client.ContainerRestartResult, error)
|
containerExportFunc func(string) (io.ReadCloser, error)
|
||||||
containerStopFunc func(ctx context.Context, containerID string, options client.ContainerStopOptions) (client.ContainerStopResult, error)
|
containerExecResizeFunc func(id string, options container.ResizeOptions) error
|
||||||
containerKillFunc func(ctx context.Context, containerID string, options client.ContainerKillOptions) (client.ContainerKillResult, error)
|
containerRemoveFunc func(ctx context.Context, containerID string, options container.RemoveOptions) error
|
||||||
containerPruneFunc func(ctx context.Context, options client.ContainerPruneOptions) (client.ContainerPruneResult, error)
|
containerKillFunc func(ctx context.Context, containerID, signal string) error
|
||||||
containerAttachFunc func(ctx context.Context, containerID string, options client.ContainerAttachOptions) (client.ContainerAttachResult, error)
|
containerPruneFunc func(ctx context.Context, pruneFilters filters.Args) (container.PruneReport, error)
|
||||||
containerDiffFunc func(ctx context.Context, containerID string) (client.ContainerDiffResult, error)
|
containerAttachFunc func(ctx context.Context, containerID string, options container.AttachOptions) (types.HijackedResponse, error)
|
||||||
containerRenameFunc func(ctx context.Context, oldName, newName string) error
|
|
||||||
containerCommitFunc func(ctx context.Context, container string, options client.ContainerCommitOptions) (client.ContainerCommitResult, error)
|
|
||||||
containerPauseFunc func(ctx context.Context, container string, options client.ContainerPauseOptions) (client.ContainerPauseResult, error)
|
|
||||||
Version string
|
Version string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fakeClient) ContainerList(_ context.Context, options client.ContainerListOptions) (client.ContainerListResult, error) {
|
func (f *fakeClient) ContainerList(_ context.Context, options container.ListOptions) ([]types.Container, error) {
|
||||||
if f.containerListFunc != nil {
|
if f.containerListFunc != nil {
|
||||||
return f.containerListFunc(options)
|
return f.containerListFunc(options)
|
||||||
}
|
}
|
||||||
return client.ContainerListResult{}, nil
|
return []types.Container{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fakeClient) ContainerInspect(_ context.Context, containerID string, _ client.ContainerInspectOptions) (client.ContainerInspectResult, error) {
|
func (f *fakeClient) ContainerInspect(_ context.Context, containerID string) (types.ContainerJSON, error) {
|
||||||
if f.inspectFunc != nil {
|
if f.inspectFunc != nil {
|
||||||
return f.inspectFunc(containerID)
|
return f.inspectFunc(containerID)
|
||||||
}
|
}
|
||||||
return client.ContainerInspectResult{}, nil
|
return types.ContainerJSON{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fakeClient) ExecCreate(_ context.Context, containerID string, config client.ExecCreateOptions) (client.ExecCreateResult, error) {
|
func (f *fakeClient) ContainerExecCreate(_ context.Context, containerID string, config container.ExecOptions) (types.IDResponse, error) {
|
||||||
if f.execCreateFunc != nil {
|
if f.execCreateFunc != nil {
|
||||||
return f.execCreateFunc(containerID, config)
|
return f.execCreateFunc(containerID, config)
|
||||||
}
|
}
|
||||||
return client.ExecCreateResult{}, nil
|
return types.IDResponse{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fakeClient) ExecInspect(_ context.Context, execID string, _ client.ExecInspectOptions) (client.ExecInspectResult, error) {
|
func (f *fakeClient) ContainerExecInspect(_ context.Context, execID string) (container.ExecInspect, error) {
|
||||||
if f.execInspectFunc != nil {
|
if f.execInspectFunc != nil {
|
||||||
return f.execInspectFunc(execID)
|
return f.execInspectFunc(execID)
|
||||||
}
|
}
|
||||||
return client.ExecInspectResult{}, nil
|
return container.ExecInspect{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*fakeClient) ExecStart(context.Context, string, client.ExecStartOptions) (client.ExecStartResult, error) {
|
func (f *fakeClient) ContainerExecStart(context.Context, string, container.ExecStartOptions) error {
|
||||||
return client.ExecStartResult{}, nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fakeClient) ContainerCreate(_ context.Context, options client.ContainerCreateOptions) (client.ContainerCreateResult, error) {
|
func (f *fakeClient) ContainerCreate(
|
||||||
|
_ context.Context,
|
||||||
|
config *container.Config,
|
||||||
|
hostConfig *container.HostConfig,
|
||||||
|
networkingConfig *network.NetworkingConfig,
|
||||||
|
platform *specs.Platform,
|
||||||
|
containerName string,
|
||||||
|
) (container.CreateResponse, error) {
|
||||||
if f.createContainerFunc != nil {
|
if f.createContainerFunc != nil {
|
||||||
return f.createContainerFunc(options)
|
return f.createContainerFunc(config, hostConfig, networkingConfig, platform, containerName)
|
||||||
}
|
}
|
||||||
return client.ContainerCreateResult{}, nil
|
return container.CreateResponse{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fakeClient) ContainerRemove(ctx context.Context, containerID string, options client.ContainerRemoveOptions) (client.ContainerRemoveResult, error) {
|
func (f *fakeClient) ContainerRemove(ctx context.Context, containerID string, options container.RemoveOptions) error {
|
||||||
if f.containerRemoveFunc != nil {
|
if f.containerRemoveFunc != nil {
|
||||||
return f.containerRemoveFunc(ctx, containerID, options)
|
return f.containerRemoveFunc(ctx, containerID, options)
|
||||||
}
|
}
|
||||||
return client.ContainerRemoveResult{}, nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fakeClient) ImagePull(ctx context.Context, parentReference string, options client.ImagePullOptions) (client.ImagePullResponse, error) {
|
func (f *fakeClient) ImageCreate(ctx context.Context, parentReference string, options image.CreateOptions) (io.ReadCloser, error) {
|
||||||
if f.imagePullFunc != nil {
|
if f.imageCreateFunc != nil {
|
||||||
return f.imagePullFunc(ctx, parentReference, options)
|
return f.imageCreateFunc(ctx, parentReference, options)
|
||||||
}
|
}
|
||||||
return fakeStreamResult{}, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fakeClient) Info(context.Context, client.InfoOptions) (client.SystemInfoResult, error) {
|
func (f *fakeClient) Info(_ context.Context) (system.Info, error) {
|
||||||
if f.infoFunc != nil {
|
if f.infoFunc != nil {
|
||||||
return f.infoFunc()
|
return f.infoFunc()
|
||||||
}
|
}
|
||||||
return client.SystemInfoResult{}, nil
|
return system.Info{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fakeClient) ContainerStatPath(_ context.Context, containerID string, options client.ContainerStatPathOptions) (client.ContainerStatPathResult, error) {
|
func (f *fakeClient) ContainerStatPath(_ context.Context, containerID, path string) (container.PathStat, error) {
|
||||||
if f.containerStatPathFunc != nil {
|
if f.containerStatPathFunc != nil {
|
||||||
return f.containerStatPathFunc(containerID, options.Path)
|
return f.containerStatPathFunc(containerID, path)
|
||||||
}
|
}
|
||||||
return client.ContainerStatPathResult{}, nil
|
return container.PathStat{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fakeClient) CopyFromContainer(_ context.Context, containerID string, options client.CopyFromContainerOptions) (client.CopyFromContainerResult, error) {
|
func (f *fakeClient) CopyFromContainer(_ context.Context, containerID, srcPath string) (io.ReadCloser, container.PathStat, error) {
|
||||||
if f.containerCopyFromFunc != nil {
|
if f.containerCopyFromFunc != nil {
|
||||||
return f.containerCopyFromFunc(containerID, options.SourcePath)
|
return f.containerCopyFromFunc(containerID, srcPath)
|
||||||
}
|
}
|
||||||
return client.CopyFromContainerResult{}, nil
|
return nil, container.PathStat{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fakeClient) CopyToContainer(_ context.Context, containerID string, options client.CopyToContainerOptions) (client.CopyToContainerResult, error) {
|
func (f *fakeClient) ContainerLogs(_ context.Context, containerID string, options container.LogsOptions) (io.ReadCloser, error) {
|
||||||
if f.containerCopyToFunc != nil {
|
|
||||||
return f.containerCopyToFunc(containerID, options)
|
|
||||||
}
|
|
||||||
return client.CopyToContainerResult{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *fakeClient) ContainerLogs(_ context.Context, containerID string, options client.ContainerLogsOptions) (client.ContainerLogsResult, error) {
|
|
||||||
if f.logFunc != nil {
|
if f.logFunc != nil {
|
||||||
return f.logFunc(containerID, options)
|
return f.logFunc(containerID, options)
|
||||||
}
|
}
|
||||||
return http.NoBody, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fakeClient) ClientVersion() string {
|
func (f *fakeClient) ClientVersion() string {
|
||||||
return f.Version
|
return f.Version
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fakeClient) ContainerWait(_ context.Context, containerID string, _ client.ContainerWaitOptions) client.ContainerWaitResult {
|
func (f *fakeClient) ContainerWait(_ context.Context, containerID string, _ container.WaitCondition) (<-chan container.WaitResponse, <-chan error) {
|
||||||
if f.waitFunc != nil {
|
if f.waitFunc != nil {
|
||||||
return f.waitFunc(containerID)
|
return f.waitFunc(containerID)
|
||||||
}
|
}
|
||||||
return client.ContainerWaitResult{}
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fakeClient) ContainerStart(_ context.Context, containerID string, options client.ContainerStartOptions) (client.ContainerStartResult, error) {
|
func (f *fakeClient) ContainerStart(_ context.Context, containerID string, options container.StartOptions) error {
|
||||||
if f.containerStartFunc != nil {
|
if f.containerStartFunc != nil {
|
||||||
return f.containerStartFunc(containerID, options)
|
return f.containerStartFunc(containerID, options)
|
||||||
}
|
}
|
||||||
return client.ContainerStartResult{}, nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fakeClient) ContainerExport(_ context.Context, containerID string, _ client.ContainerExportOptions) (client.ContainerExportResult, error) {
|
func (f *fakeClient) ContainerExport(_ context.Context, containerID string) (io.ReadCloser, error) {
|
||||||
if f.containerExportFunc != nil {
|
if f.containerExportFunc != nil {
|
||||||
return f.containerExportFunc(containerID)
|
return f.containerExportFunc(containerID)
|
||||||
}
|
}
|
||||||
return http.NoBody, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fakeClient) ExecResize(_ context.Context, id string, options client.ExecResizeOptions) (client.ExecResizeResult, error) {
|
func (f *fakeClient) ContainerExecResize(_ context.Context, id string, options container.ResizeOptions) error {
|
||||||
if f.containerExecResizeFunc != nil {
|
if f.containerExecResizeFunc != nil {
|
||||||
return f.containerExecResizeFunc(id, options)
|
return f.containerExecResizeFunc(id, options)
|
||||||
}
|
}
|
||||||
return client.ExecResizeResult{}, nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fakeClient) ContainerKill(ctx context.Context, containerID string, options client.ContainerKillOptions) (client.ContainerKillResult, error) {
|
func (f *fakeClient) ContainerKill(ctx context.Context, containerID, signal string) error {
|
||||||
if f.containerKillFunc != nil {
|
if f.containerKillFunc != nil {
|
||||||
return f.containerKillFunc(ctx, containerID, options)
|
return f.containerKillFunc(ctx, containerID, signal)
|
||||||
}
|
}
|
||||||
return client.ContainerKillResult{}, nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fakeClient) ContainerPrune(ctx context.Context, options client.ContainerPruneOptions) (client.ContainerPruneResult, error) {
|
func (f *fakeClient) ContainersPrune(ctx context.Context, pruneFilters filters.Args) (container.PruneReport, error) {
|
||||||
if f.containerPruneFunc != nil {
|
if f.containerPruneFunc != nil {
|
||||||
return f.containerPruneFunc(ctx, options)
|
return f.containerPruneFunc(ctx, pruneFilters)
|
||||||
}
|
}
|
||||||
return client.ContainerPruneResult{}, nil
|
return container.PruneReport{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fakeClient) ContainerRestart(ctx context.Context, containerID string, options client.ContainerRestartOptions) (client.ContainerRestartResult, error) {
|
func (f *fakeClient) ContainerAttach(ctx context.Context, containerID string, options container.AttachOptions) (types.HijackedResponse, error) {
|
||||||
if f.containerRestartFunc != nil {
|
|
||||||
return f.containerRestartFunc(ctx, containerID, options)
|
|
||||||
}
|
|
||||||
return client.ContainerRestartResult{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *fakeClient) ContainerStop(ctx context.Context, containerID string, options client.ContainerStopOptions) (client.ContainerStopResult, error) {
|
|
||||||
if f.containerStopFunc != nil {
|
|
||||||
return f.containerStopFunc(ctx, containerID, options)
|
|
||||||
}
|
|
||||||
return client.ContainerStopResult{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *fakeClient) ContainerAttach(ctx context.Context, containerID string, options client.ContainerAttachOptions) (client.ContainerAttachResult, error) {
|
|
||||||
if f.containerAttachFunc != nil {
|
if f.containerAttachFunc != nil {
|
||||||
return f.containerAttachFunc(ctx, containerID, options)
|
return f.containerAttachFunc(ctx, containerID, options)
|
||||||
}
|
}
|
||||||
return client.ContainerAttachResult{}, nil
|
return types.HijackedResponse{}, nil
|
||||||
}
|
|
||||||
|
|
||||||
func (f *fakeClient) ContainerDiff(ctx context.Context, containerID string, _ client.ContainerDiffOptions) (client.ContainerDiffResult, error) {
|
|
||||||
if f.containerDiffFunc != nil {
|
|
||||||
return f.containerDiffFunc(ctx, containerID)
|
|
||||||
}
|
|
||||||
|
|
||||||
return client.ContainerDiffResult{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *fakeClient) ContainerRename(ctx context.Context, oldName string, options client.ContainerRenameOptions) (client.ContainerRenameResult, error) {
|
|
||||||
if f.containerRenameFunc != nil {
|
|
||||||
return client.ContainerRenameResult{}, f.containerRenameFunc(ctx, oldName, options.NewName)
|
|
||||||
}
|
|
||||||
|
|
||||||
return client.ContainerRenameResult{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *fakeClient) ContainerCommit(ctx context.Context, containerID string, options client.ContainerCommitOptions) (client.ContainerCommitResult, error) {
|
|
||||||
if f.containerCommitFunc != nil {
|
|
||||||
return f.containerCommitFunc(ctx, containerID, options)
|
|
||||||
}
|
|
||||||
return client.ContainerCommitResult{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *fakeClient) ContainerPause(ctx context.Context, containerID string, options client.ContainerPauseOptions) (client.ContainerPauseResult, error) {
|
|
||||||
if f.containerPauseFunc != nil {
|
|
||||||
return f.containerPauseFunc(ctx, containerID, options)
|
|
||||||
}
|
|
||||||
|
|
||||||
return client.ContainerPauseResult{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*fakeClient) Ping(_ context.Context, _ client.PingOptions) (client.PingResult, error) {
|
|
||||||
return client.PingResult{}, nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,73 +3,43 @@ package container
|
|||||||
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() {
|
// NewContainerCommand returns a cobra command for `container` subcommands
|
||||||
commands.Register(newRunCommand)
|
func NewContainerCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
commands.Register(newExecCommand)
|
|
||||||
commands.Register(newPsCommand)
|
|
||||||
commands.Register(newContainerCommand)
|
|
||||||
commands.RegisterLegacy(newAttachCommand)
|
|
||||||
commands.RegisterLegacy(newCommitCommand)
|
|
||||||
commands.RegisterLegacy(newCopyCommand)
|
|
||||||
commands.RegisterLegacy(newCreateCommand)
|
|
||||||
commands.RegisterLegacy(newDiffCommand)
|
|
||||||
commands.RegisterLegacy(newExportCommand)
|
|
||||||
commands.RegisterLegacy(newKillCommand)
|
|
||||||
commands.RegisterLegacy(newLogsCommand)
|
|
||||||
commands.RegisterLegacy(newPauseCommand)
|
|
||||||
commands.RegisterLegacy(newPortCommand)
|
|
||||||
commands.RegisterLegacy(newRenameCommand)
|
|
||||||
commands.RegisterLegacy(newRestartCommand)
|
|
||||||
commands.RegisterLegacy(newRmCommand)
|
|
||||||
commands.RegisterLegacy(newStartCommand)
|
|
||||||
commands.RegisterLegacy(newStatsCommand)
|
|
||||||
commands.RegisterLegacy(newStopCommand)
|
|
||||||
commands.RegisterLegacy(newTopCommand)
|
|
||||||
commands.RegisterLegacy(newUnpauseCommand)
|
|
||||||
commands.RegisterLegacy(newUpdateCommand)
|
|
||||||
commands.RegisterLegacy(newWaitCommand)
|
|
||||||
}
|
|
||||||
|
|
||||||
// newContainerCommand returns a cobra command for `container` subcommands
|
|
||||||
func newContainerCommand(dockerCLI command.Cli) *cobra.Command {
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "container",
|
Use: "container",
|
||||||
Short: "Manage containers",
|
Short: "Manage containers",
|
||||||
Args: cli.NoArgs,
|
Args: cli.NoArgs,
|
||||||
RunE: command.ShowHelp(dockerCLI.Err()),
|
RunE: command.ShowHelp(dockerCli.Err()),
|
||||||
|
|
||||||
DisableFlagsInUseLine: true,
|
|
||||||
}
|
}
|
||||||
cmd.AddCommand(
|
cmd.AddCommand(
|
||||||
newAttachCommand(dockerCLI),
|
NewAttachCommand(dockerCli),
|
||||||
newCommitCommand(dockerCLI),
|
NewCommitCommand(dockerCli),
|
||||||
newCopyCommand(dockerCLI),
|
NewCopyCommand(dockerCli),
|
||||||
newCreateCommand(dockerCLI),
|
NewCreateCommand(dockerCli),
|
||||||
newDiffCommand(dockerCLI),
|
NewDiffCommand(dockerCli),
|
||||||
newExecCommand(dockerCLI),
|
NewExecCommand(dockerCli),
|
||||||
newExportCommand(dockerCLI),
|
NewExportCommand(dockerCli),
|
||||||
newKillCommand(dockerCLI),
|
NewKillCommand(dockerCli),
|
||||||
newLogsCommand(dockerCLI),
|
NewLogsCommand(dockerCli),
|
||||||
newPauseCommand(dockerCLI),
|
NewPauseCommand(dockerCli),
|
||||||
newPortCommand(dockerCLI),
|
NewPortCommand(dockerCli),
|
||||||
newRenameCommand(dockerCLI),
|
NewRenameCommand(dockerCli),
|
||||||
newRestartCommand(dockerCLI),
|
NewRestartCommand(dockerCli),
|
||||||
newRemoveCommand(dockerCLI),
|
NewRmCommand(dockerCli),
|
||||||
newRunCommand(dockerCLI),
|
NewRunCommand(dockerCli),
|
||||||
newStartCommand(dockerCLI),
|
NewStartCommand(dockerCli),
|
||||||
newStatsCommand(dockerCLI),
|
NewStatsCommand(dockerCli),
|
||||||
newStopCommand(dockerCLI),
|
NewStopCommand(dockerCli),
|
||||||
newTopCommand(dockerCLI),
|
NewTopCommand(dockerCli),
|
||||||
newUnpauseCommand(dockerCLI),
|
NewUnpauseCommand(dockerCli),
|
||||||
newUpdateCommand(dockerCLI),
|
NewUpdateCommand(dockerCli),
|
||||||
newWaitCommand(dockerCLI),
|
NewWaitCommand(dockerCli),
|
||||||
newListCommand(dockerCLI),
|
newListCommand(dockerCli),
|
||||||
newInspectCommand(dockerCLI),
|
newInspectCommand(dockerCli),
|
||||||
newPruneCommand(dockerCLI),
|
NewPruneCommand(dockerCli),
|
||||||
)
|
)
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,14 +2,13 @@ package container
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"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/opts"
|
"github.com/docker/cli/opts"
|
||||||
"github.com/moby/moby/client"
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -18,14 +17,13 @@ type commitOptions struct {
|
|||||||
reference string
|
reference string
|
||||||
|
|
||||||
pause bool
|
pause bool
|
||||||
noPause bool
|
|
||||||
comment string
|
comment string
|
||||||
author string
|
author string
|
||||||
changes opts.ListOpts
|
changes opts.ListOpts
|
||||||
}
|
}
|
||||||
|
|
||||||
// newCommitCommand creates a new cobra.Command for `docker commit`
|
// NewCommitCommand creates a new cobra.Command for `docker commit`
|
||||||
func newCommitCommand(dockerCLI command.Cli) *cobra.Command {
|
func NewCommitCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
var options commitOptions
|
var options commitOptions
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
@@ -37,29 +35,18 @@ func newCommitCommand(dockerCLI command.Cli) *cobra.Command {
|
|||||||
if len(args) > 1 {
|
if len(args) > 1 {
|
||||||
options.reference = args[1]
|
options.reference = args[1]
|
||||||
}
|
}
|
||||||
if cmd.Flag("pause").Changed {
|
return runCommit(cmd.Context(), dockerCli, &options)
|
||||||
if cmd.Flag("no-pause").Changed {
|
|
||||||
return errors.New("conflicting options: --no-pause and --pause cannot be used together")
|
|
||||||
}
|
|
||||||
options.noPause = !options.pause
|
|
||||||
}
|
|
||||||
return runCommit(cmd.Context(), dockerCLI, &options)
|
|
||||||
},
|
},
|
||||||
Annotations: map[string]string{
|
Annotations: map[string]string{
|
||||||
"aliases": "docker container commit, docker commit",
|
"aliases": "docker container commit, docker commit",
|
||||||
},
|
},
|
||||||
ValidArgsFunction: completion.ContainerNames(dockerCLI, false),
|
ValidArgsFunction: completion.ContainerNames(dockerCli, false),
|
||||||
DisableFlagsInUseLine: true,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
flags := cmd.Flags()
|
flags := cmd.Flags()
|
||||||
flags.SetInterspersed(false)
|
flags.SetInterspersed(false)
|
||||||
|
|
||||||
// TODO(thaJeztah): Deprecated: the --pause flag was deprecated in v29 and can be removed in v30.
|
flags.BoolVarP(&options.pause, "pause", "p", true, "Pause container during commit")
|
||||||
flags.BoolVarP(&options.pause, "pause", "p", true, "Pause container during commit (deprecated: use --no-pause instead)")
|
|
||||||
_ = flags.MarkDeprecated("pause", "and enabled by default. Use --no-pause to disable pausing during commit.")
|
|
||||||
|
|
||||||
flags.BoolVar(&options.noPause, "no-pause", false, "Disable pausing container during commit")
|
|
||||||
flags.StringVarP(&options.comment, "message", "m", "", "Commit message")
|
flags.StringVarP(&options.comment, "message", "m", "", "Commit message")
|
||||||
flags.StringVarP(&options.author, "author", "a", "", `Author (e.g., "John Hannibal Smith <hannibal@a-team.com>")`)
|
flags.StringVarP(&options.author, "author", "a", "", `Author (e.g., "John Hannibal Smith <hannibal@a-team.com>")`)
|
||||||
|
|
||||||
@@ -70,17 +57,17 @@ func newCommitCommand(dockerCLI command.Cli) *cobra.Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func runCommit(ctx context.Context, dockerCli command.Cli, options *commitOptions) error {
|
func runCommit(ctx context.Context, dockerCli command.Cli, options *commitOptions) error {
|
||||||
response, err := dockerCli.Client().ContainerCommit(ctx, options.container, client.ContainerCommitOptions{
|
response, err := dockerCli.Client().ContainerCommit(ctx, options.container, container.CommitOptions{
|
||||||
Reference: options.reference,
|
Reference: options.reference,
|
||||||
Comment: options.comment,
|
Comment: options.comment,
|
||||||
Author: options.author,
|
Author: options.author,
|
||||||
Changes: options.changes.GetSlice(),
|
Changes: options.changes.GetAll(),
|
||||||
NoPause: options.noPause,
|
Pause: options.pause,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, _ = fmt.Fprintln(dockerCli.Out(), response.ID)
|
fmt.Fprintln(dockerCli.Out(), response.ID)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
package container
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/docker/cli/internal/test"
|
|
||||||
"github.com/moby/moby/client"
|
|
||||||
"gotest.tools/v3/assert"
|
|
||||||
is "gotest.tools/v3/assert/cmp"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestRunCommit(t *testing.T) {
|
|
||||||
cli := test.NewFakeCli(&fakeClient{
|
|
||||||
containerCommitFunc: func(ctx context.Context, ctr string, options client.ContainerCommitOptions) (client.ContainerCommitResult, error) {
|
|
||||||
assert.Check(t, is.Equal(options.Author, "Author Name <author@name.com>"))
|
|
||||||
assert.Check(t, is.DeepEqual(options.Changes, []string{"EXPOSE 80"}))
|
|
||||||
assert.Check(t, is.Equal(options.Comment, "commit message"))
|
|
||||||
assert.Check(t, is.Equal(options.NoPause, true))
|
|
||||||
assert.Check(t, is.Equal(ctr, "container-id"))
|
|
||||||
|
|
||||||
return client.ContainerCommitResult{ID: "image-id"}, nil
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
cmd := newCommitCommand(cli)
|
|
||||||
cmd.SetOut(io.Discard)
|
|
||||||
cmd.SetArgs(
|
|
||||||
[]string{
|
|
||||||
"--author", "Author Name <author@name.com>",
|
|
||||||
"--change", "EXPOSE 80",
|
|
||||||
"--message", "commit message",
|
|
||||||
"--no-pause",
|
|
||||||
"container-id",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
err := cmd.Execute()
|
|
||||||
assert.NilError(t, err)
|
|
||||||
|
|
||||||
assert.Assert(t, is.Equal(cli.OutBuffer().String(), "image-id\n"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRunCommitClientError(t *testing.T) {
|
|
||||||
clientError := errors.New("client error")
|
|
||||||
|
|
||||||
cli := test.NewFakeCli(&fakeClient{
|
|
||||||
containerCommitFunc: func(ctx context.Context, ctr string, options client.ContainerCommitOptions) (client.ContainerCommitResult, error) {
|
|
||||||
return client.ContainerCommitResult{}, clientError
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
cmd := newCommitCommand(cli)
|
|
||||||
cmd.SetOut(io.Discard)
|
|
||||||
cmd.SetErr(io.Discard)
|
|
||||||
cmd.SetArgs([]string{"container-id"})
|
|
||||||
|
|
||||||
err := cmd.Execute()
|
|
||||||
assert.ErrorIs(t, err, clientError)
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
||||||
//go:build go1.26
|
//go:build go1.22
|
||||||
|
// +build go1.22
|
||||||
|
|
||||||
package container
|
package container
|
||||||
|
|
||||||
@@ -8,8 +9,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/docker/cli/cli/command/completion"
|
"github.com/docker/cli/cli/command/completion"
|
||||||
"github.com/moby/moby/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/moby/moby/client"
|
|
||||||
"github.com/moby/sys/capability"
|
"github.com/moby/sys/capability"
|
||||||
"github.com/moby/sys/signal"
|
"github.com/moby/sys/signal"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@@ -57,7 +57,7 @@ var logDriverOptions = map[string][]string{
|
|||||||
"fluentd": {
|
"fluentd": {
|
||||||
"max-buffer-size", "mode", "env", "env-regex", "labels", "fluentd-address", "fluentd-async",
|
"max-buffer-size", "mode", "env", "env-regex", "labels", "fluentd-address", "fluentd-async",
|
||||||
"fluentd-buffer-limit", "fluentd-request-ack", "fluentd-retry-wait", "fluentd-max-retries",
|
"fluentd-buffer-limit", "fluentd-request-ack", "fluentd-retry-wait", "fluentd-max-retries",
|
||||||
"fluentd-sub-second-precision", "fluentd-write-timeout", "tag",
|
"fluentd-sub-second-precision", "tag",
|
||||||
},
|
},
|
||||||
"gcplogs": {
|
"gcplogs": {
|
||||||
"max-buffer-size", "mode", "env", "env-regex", "labels", "gcp-log-cmd", "gcp-meta-id", "gcp-meta-name",
|
"max-buffer-size", "mode", "env", "env-regex", "labels", "gcp-log-cmd", "gcp-meta-id", "gcp-meta-name",
|
||||||
@@ -123,15 +123,15 @@ func addCompletions(cmd *cobra.Command, dockerCLI completion.APIClientProvider)
|
|||||||
_ = cmd.RegisterFlagCompletionFunc("cap-add", completeLinuxCapabilityNames)
|
_ = cmd.RegisterFlagCompletionFunc("cap-add", completeLinuxCapabilityNames)
|
||||||
_ = cmd.RegisterFlagCompletionFunc("cap-drop", completeLinuxCapabilityNames)
|
_ = cmd.RegisterFlagCompletionFunc("cap-drop", completeLinuxCapabilityNames)
|
||||||
_ = cmd.RegisterFlagCompletionFunc("cgroupns", completeCgroupns())
|
_ = cmd.RegisterFlagCompletionFunc("cgroupns", completeCgroupns())
|
||||||
_ = cmd.RegisterFlagCompletionFunc("env", completion.EnvVarNames())
|
_ = cmd.RegisterFlagCompletionFunc("env", completion.EnvVarNames)
|
||||||
_ = cmd.RegisterFlagCompletionFunc("env-file", completion.FileNames())
|
_ = cmd.RegisterFlagCompletionFunc("env-file", completion.FileNames)
|
||||||
_ = cmd.RegisterFlagCompletionFunc("ipc", completeIpc(dockerCLI))
|
_ = cmd.RegisterFlagCompletionFunc("ipc", completeIpc(dockerCLI))
|
||||||
_ = cmd.RegisterFlagCompletionFunc("link", completeLink(dockerCLI))
|
_ = cmd.RegisterFlagCompletionFunc("link", completeLink(dockerCLI))
|
||||||
_ = cmd.RegisterFlagCompletionFunc("log-driver", completeLogDriver(dockerCLI))
|
_ = cmd.RegisterFlagCompletionFunc("log-driver", completeLogDriver(dockerCLI))
|
||||||
_ = cmd.RegisterFlagCompletionFunc("log-opt", completeLogOpt)
|
_ = cmd.RegisterFlagCompletionFunc("log-opt", completeLogOpt)
|
||||||
_ = cmd.RegisterFlagCompletionFunc("network", completion.NetworkNames(dockerCLI))
|
_ = cmd.RegisterFlagCompletionFunc("network", completion.NetworkNames(dockerCLI))
|
||||||
_ = cmd.RegisterFlagCompletionFunc("pid", completePid(dockerCLI))
|
_ = cmd.RegisterFlagCompletionFunc("pid", completePid(dockerCLI))
|
||||||
_ = cmd.RegisterFlagCompletionFunc("platform", completion.Platforms())
|
_ = cmd.RegisterFlagCompletionFunc("platform", completion.Platforms)
|
||||||
_ = cmd.RegisterFlagCompletionFunc("pull", completion.FromList(PullImageAlways, PullImageMissing, PullImageNever))
|
_ = cmd.RegisterFlagCompletionFunc("pull", completion.FromList(PullImageAlways, PullImageMissing, PullImageNever))
|
||||||
_ = cmd.RegisterFlagCompletionFunc("restart", completeRestartPolicies)
|
_ = cmd.RegisterFlagCompletionFunc("restart", completeRestartPolicies)
|
||||||
_ = cmd.RegisterFlagCompletionFunc("security-opt", completeSecurityOpt)
|
_ = cmd.RegisterFlagCompletionFunc("security-opt", completeSecurityOpt)
|
||||||
@@ -145,7 +145,7 @@ func addCompletions(cmd *cobra.Command, dockerCLI completion.APIClientProvider)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// completeCgroupns implements shell completion for the `--cgroupns` option of `run` and `create`.
|
// completeCgroupns implements shell completion for the `--cgroupns` option of `run` and `create`.
|
||||||
func completeCgroupns() cobra.CompletionFunc {
|
func completeCgroupns() completion.ValidArgsFn {
|
||||||
return completion.FromList(string(container.CgroupnsModeHost), string(container.CgroupnsModePrivate))
|
return completion.FromList(string(container.CgroupnsModeHost), string(container.CgroupnsModePrivate))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,7 +156,7 @@ func completeDetachKeys(_ *cobra.Command, _ []string, _ string) ([]string, cobra
|
|||||||
|
|
||||||
// completeIpc implements shell completion for the `--ipc` option of `run` and `create`.
|
// completeIpc implements shell completion for the `--ipc` option of `run` and `create`.
|
||||||
// The completion is partly composite.
|
// The completion is partly composite.
|
||||||
func completeIpc(dockerCLI completion.APIClientProvider) cobra.CompletionFunc {
|
func completeIpc(dockerCLI completion.APIClientProvider) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
if len(toComplete) > 0 && strings.HasPrefix("container", toComplete) { //nolint:gocritic // not swapped, matches partly typed "container"
|
if len(toComplete) > 0 && strings.HasPrefix("container", toComplete) { //nolint:gocritic // not swapped, matches partly typed "container"
|
||||||
return []string{"container:"}, cobra.ShellCompDirectiveNoSpace
|
return []string{"container:"}, cobra.ShellCompDirectiveNoSpace
|
||||||
@@ -176,51 +176,22 @@ func completeIpc(dockerCLI completion.APIClientProvider) cobra.CompletionFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// completeLink implements shell completion for the `--link` option of `run` and `create`.
|
// completeLink implements shell completion for the `--link` option of `run` and `create`.
|
||||||
func completeLink(dockerCLI completion.APIClientProvider) cobra.CompletionFunc {
|
func completeLink(dockerCLI completion.APIClientProvider) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
return postfixWith(":", containerNames(dockerCLI, cmd, args, toComplete)), cobra.ShellCompDirectiveNoSpace
|
return postfixWith(":", containerNames(dockerCLI, cmd, args, toComplete)), cobra.ShellCompDirectiveNoSpace
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// completeLinks implements shell completion for the `--link` option of `rm --link`.
|
|
||||||
//
|
|
||||||
// It contacts the API to get names of legacy links on containers.
|
|
||||||
// In case of an error, an empty list is returned.
|
|
||||||
func completeLinks(dockerCLI completion.APIClientProvider) cobra.CompletionFunc {
|
|
||||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
|
||||||
res, err := dockerCLI.Client().ContainerList(cmd.Context(), client.ContainerListOptions{
|
|
||||||
All: true,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, cobra.ShellCompDirectiveError
|
|
||||||
}
|
|
||||||
var names []string
|
|
||||||
for _, ctr := range res.Items {
|
|
||||||
if len(ctr.Names) <= 1 {
|
|
||||||
// Container has no links names.
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
for _, n := range ctr.Names {
|
|
||||||
// Skip legacy link names: "/linked-container/link-name"
|
|
||||||
if len(n) > 1 && strings.IndexByte(n[1:], '/') != -1 {
|
|
||||||
names = append(names, strings.TrimPrefix(n, "/"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return names, cobra.ShellCompDirectiveNoFileComp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// completeLogDriver implements shell completion for the `--log-driver` option of `run` and `create`.
|
// completeLogDriver implements shell completion for the `--log-driver` option of `run` and `create`.
|
||||||
// The log drivers are collected from a call to the Info endpoint with a fallback to a hard-coded list
|
// The log drivers are collected from a call to the Info endpoint with a fallback to a hard-coded list
|
||||||
// of the build-in log drivers.
|
// of the build-in log drivers.
|
||||||
func completeLogDriver(dockerCLI completion.APIClientProvider) cobra.CompletionFunc {
|
func completeLogDriver(dockerCLI completion.APIClientProvider) 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().Info(cmd.Context(), client.InfoOptions{})
|
info, err := dockerCLI.Client().Info(cmd.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return builtInLogDrivers(), cobra.ShellCompDirectiveNoFileComp
|
return builtInLogDrivers(), cobra.ShellCompDirectiveNoFileComp
|
||||||
}
|
}
|
||||||
drivers := res.Info.Plugins.Log
|
drivers := info.Plugins.Log
|
||||||
return drivers, cobra.ShellCompDirectiveNoFileComp
|
return drivers, cobra.ShellCompDirectiveNoFileComp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -236,7 +207,7 @@ func completeLogOpt(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.S
|
|||||||
}
|
}
|
||||||
|
|
||||||
// completePid implements shell completion for the `--pid` option of `run` and `create`.
|
// completePid implements shell completion for the `--pid` option of `run` and `create`.
|
||||||
func completePid(dockerCLI completion.APIClientProvider) cobra.CompletionFunc {
|
func completePid(dockerCLI completion.APIClientProvider) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
if len(toComplete) > 0 && strings.HasPrefix("container", toComplete) { //nolint:gocritic // not swapped, matches partly typed "container"
|
if len(toComplete) > 0 && strings.HasPrefix("container", toComplete) { //nolint:gocritic // not swapped, matches partly typed "container"
|
||||||
return []string{"container:"}, cobra.ShellCompDirectiveNoSpace
|
return []string{"container:"}, cobra.ShellCompDirectiveNoSpace
|
||||||
@@ -307,14 +278,14 @@ func completeUlimit(_ *cobra.Command, _ []string, _ string) ([]string, cobra.She
|
|||||||
}
|
}
|
||||||
|
|
||||||
// completeVolumeDriver contacts the API to get the built-in and installed volume drivers.
|
// completeVolumeDriver contacts the API to get the built-in and installed volume drivers.
|
||||||
func completeVolumeDriver(dockerCLI completion.APIClientProvider) cobra.CompletionFunc {
|
func completeVolumeDriver(dockerCLI completion.APIClientProvider) completion.ValidArgsFn {
|
||||||
return func(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
|
return func(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
|
||||||
res, err := dockerCLI.Client().Info(cmd.Context(), client.InfoOptions{})
|
info, err := dockerCLI.Client().Info(cmd.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// fallback: the built-in drivers
|
// fallback: the built-in drivers
|
||||||
return []string{"local"}, cobra.ShellCompDirectiveNoFileComp
|
return []string{"local"}, cobra.ShellCompDirectiveNoFileComp
|
||||||
}
|
}
|
||||||
drivers := res.Info.Plugins.Volume
|
drivers := info.Plugins.Volume
|
||||||
return drivers, cobra.ShellCompDirectiveNoFileComp
|
return drivers, cobra.ShellCompDirectiveNoFileComp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,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/api/types/container"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/moby/moby/client"
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/moby/sys/signal"
|
"github.com/moby/sys/signal"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"gotest.tools/v3/assert"
|
"gotest.tools/v3/assert"
|
||||||
@@ -27,7 +27,7 @@ func TestCompleteLinuxCapabilityNames(t *testing.T) {
|
|||||||
|
|
||||||
func TestCompletePid(t *testing.T) {
|
func TestCompletePid(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
containerListFunc func(client.ContainerListOptions) (client.ContainerListResult, error)
|
containerListFunc func(container.ListOptions) ([]types.Container, error)
|
||||||
toComplete string
|
toComplete string
|
||||||
expectedCompletions []string
|
expectedCompletions []string
|
||||||
expectedDirective cobra.ShellCompDirective
|
expectedDirective cobra.ShellCompDirective
|
||||||
@@ -43,12 +43,10 @@ func TestCompletePid(t *testing.T) {
|
|||||||
expectedDirective: cobra.ShellCompDirectiveNoSpace,
|
expectedDirective: cobra.ShellCompDirectiveNoSpace,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
containerListFunc: func(client.ContainerListOptions) (client.ContainerListResult, error) {
|
containerListFunc: func(container.ListOptions) ([]types.Container, error) {
|
||||||
return client.ContainerListResult{
|
return []types.Container{
|
||||||
Items: []container.Summary{
|
*builders.Container("c1"),
|
||||||
*builders.Container("c1"),
|
*builders.Container("c2"),
|
||||||
*builders.Container("c2"),
|
|
||||||
},
|
|
||||||
}, nil
|
}, nil
|
||||||
},
|
},
|
||||||
toComplete: "container:",
|
toComplete: "container:",
|
||||||
@@ -62,7 +60,7 @@ func TestCompletePid(t *testing.T) {
|
|||||||
cli := test.NewFakeCli(&fakeClient{
|
cli := test.NewFakeCli(&fakeClient{
|
||||||
containerListFunc: tc.containerListFunc,
|
containerListFunc: tc.containerListFunc,
|
||||||
})
|
})
|
||||||
completions, directive := completePid(cli)(newRunCommand(cli), nil, tc.toComplete)
|
completions, directive := completePid(cli)(NewRunCommand(cli), nil, tc.toComplete)
|
||||||
assert.Check(t, is.DeepEqual(completions, tc.expectedCompletions))
|
assert.Check(t, is.DeepEqual(completions, tc.expectedCompletions))
|
||||||
assert.Check(t, is.Equal(directive, tc.expectedDirective))
|
assert.Check(t, is.Equal(directive, tc.expectedDirective))
|
||||||
})
|
})
|
||||||
@@ -135,44 +133,3 @@ func TestCompleteSignals(t *testing.T) {
|
|||||||
assert.Check(t, len(values) > 1)
|
assert.Check(t, len(values) > 1)
|
||||||
assert.Check(t, is.Len(values, len(signal.SignalMap)))
|
assert.Check(t, is.Len(values, len(signal.SignalMap)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCompleteLinks(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
doc string
|
|
||||||
showAll, showIDs bool
|
|
||||||
filters []func(container.Summary) bool
|
|
||||||
containers []container.Summary
|
|
||||||
expOut []string
|
|
||||||
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", "/container-c/link-c"}},
|
|
||||||
{ID: "id-b", State: container.StateCreated, Names: []string{"/container-b", "/container-b/link-a"}},
|
|
||||||
{ID: "id-a", State: container.StateExited, Names: []string{"/container-a"}},
|
|
||||||
},
|
|
||||||
expOut: []string{"container-c/link-b", "container-c/link-c", "container-b/link-a"},
|
|
||||||
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range tests {
|
|
||||||
t.Run(tc.doc, func(t *testing.T) {
|
|
||||||
comp := completeLinks(test.NewFakeCli(&fakeClient{
|
|
||||||
containerListFunc: func(client.ContainerListOptions) (client.ContainerListResult, error) {
|
|
||||||
return client.ContainerListResult{Items: tc.containers}, nil
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
|
|
||||||
containers, directives := comp(&cobra.Command{}, nil, "")
|
|
||||||
assert.Check(t, is.Equal(directives&tc.expDirective, tc.expDirective))
|
|
||||||
assert.Check(t, is.DeepEqual(containers, tc.expOut))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+68
-135
@@ -3,7 +3,6 @@ package container
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
@@ -16,10 +15,11 @@ 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/streams"
|
"github.com/docker/cli/cli/streams"
|
||||||
"github.com/docker/go-units"
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/moby/go-archive"
|
"github.com/docker/docker/pkg/archive"
|
||||||
"github.com/moby/moby/client"
|
units "github.com/docker/go-units"
|
||||||
"github.com/morikuni/aec"
|
"github.com/morikuni/aec"
|
||||||
|
"github.com/pkg/errors"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -121,20 +121,21 @@ func copyProgress(ctx context.Context, dst io.Writer, header string, total *int6
|
|||||||
return restore, done
|
return restore, done
|
||||||
}
|
}
|
||||||
|
|
||||||
// newCopyCommand creates a new `docker cp` command
|
// NewCopyCommand creates a new `docker cp` command
|
||||||
func newCopyCommand(dockerCLI command.Cli) *cobra.Command {
|
func NewCopyCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
var opts copyOptions
|
var opts copyOptions
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: `cp [OPTIONS] CONTAINER:SRC_PATH DEST_PATH|-
|
Use: `cp [OPTIONS] CONTAINER:SRC_PATH DEST_PATH|-
|
||||||
docker cp [OPTIONS] SRC_PATH|- CONTAINER:DEST_PATH`,
|
docker cp [OPTIONS] SRC_PATH|- CONTAINER:DEST_PATH`,
|
||||||
Short: "Copy files/folders between a container and the local filesystem",
|
Short: "Copy files/folders between a container and the local filesystem",
|
||||||
Long: `Copy files/folders between a container and the local filesystem
|
Long: strings.Join([]string{
|
||||||
|
"Copy files/folders between a container and the local filesystem\n",
|
||||||
Use '-' as the source to read a tar archive from stdin
|
"\nUse '-' as the source to read a tar archive from stdin\n",
|
||||||
and extract it to a directory destination in a container.
|
"and extract it to a directory destination in a container.\n",
|
||||||
Use '-' as the destination to stream a tar archive of a
|
"Use '-' as the destination to stream a tar archive of a\n",
|
||||||
container source to stdout.`,
|
"container source to stdout.",
|
||||||
|
}, ""),
|
||||||
Args: cli.ExactArgs(2),
|
Args: cli.ExactArgs(2),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
if args[0] == "" {
|
if args[0] == "" {
|
||||||
@@ -147,18 +148,17 @@ container source to stdout.`,
|
|||||||
opts.destination = args[1]
|
opts.destination = args[1]
|
||||||
if !cmd.Flag("quiet").Changed {
|
if !cmd.Flag("quiet").Changed {
|
||||||
// User did not specify "quiet" flag; suppress output if no terminal is attached
|
// User did not specify "quiet" flag; suppress output if no terminal is attached
|
||||||
opts.quiet = !dockerCLI.Out().IsTerminal()
|
opts.quiet = !dockerCli.Out().IsTerminal()
|
||||||
}
|
}
|
||||||
return runCopy(cmd.Context(), dockerCLI, opts)
|
return runCopy(cmd.Context(), dockerCli, opts)
|
||||||
},
|
},
|
||||||
Annotations: map[string]string{
|
Annotations: map[string]string{
|
||||||
"aliases": "docker container cp, docker cp",
|
"aliases": "docker container cp, docker cp",
|
||||||
},
|
},
|
||||||
DisableFlagsInUseLine: true,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
flags := cmd.Flags()
|
flags := cmd.Flags()
|
||||||
flags.BoolVarP(&opts.followLink, "follow-link", "L", false, "Always follow symlinks in SRC_PATH")
|
flags.BoolVarP(&opts.followLink, "follow-link", "L", false, "Always follow symbol link in SRC_PATH")
|
||||||
flags.BoolVarP(&opts.copyUIDGID, "archive", "a", false, "Archive mode (copy all uid/gid information)")
|
flags.BoolVarP(&opts.copyUIDGID, "archive", "a", false, "Archive mode (copy all uid/gid information)")
|
||||||
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Suppress progress output during copy. Progress output is automatically suppressed if no terminal is attached")
|
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Suppress progress output during copy. Progress output is automatically suppressed if no terminal is attached")
|
||||||
return cmd
|
return cmd
|
||||||
@@ -168,50 +168,6 @@ func progressHumanSize(n int64) string {
|
|||||||
return units.HumanSizeWithPrecision(float64(n), 3)
|
return units.HumanSizeWithPrecision(float64(n), 3)
|
||||||
}
|
}
|
||||||
|
|
||||||
// localContentSize returns the total size of regular file content at path.
|
|
||||||
// For a regular file it returns the file size. For a directory it walks
|
|
||||||
// the tree and sums sizes of all regular files.
|
|
||||||
func localContentSize(path string) (int64, error) {
|
|
||||||
fi, err := os.Lstat(path)
|
|
||||||
if err != nil {
|
|
||||||
return -1, err
|
|
||||||
}
|
|
||||||
if !fi.IsDir() {
|
|
||||||
if fi.Mode().IsRegular() {
|
|
||||||
return fi.Size(), nil
|
|
||||||
}
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
var total int64
|
|
||||||
err = filepath.WalkDir(path, func(_ string, d os.DirEntry, err error) error {
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if d.Type().IsRegular() {
|
|
||||||
info, err := d.Info()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
total += info.Size()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
return total, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// copySummary formats the "Successfully copied ..." message.
|
|
||||||
// When contentSize differs from transferredSize, both values are shown.
|
|
||||||
func copySummary(contentSize, transferredSize int64, dest string) string {
|
|
||||||
if contentSize != transferredSize {
|
|
||||||
return fmt.Sprintf("Successfully copied %s (transferred %s) to %s\n",
|
|
||||||
progressHumanSize(contentSize), progressHumanSize(transferredSize), dest,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("Successfully copied %s to %s\n",
|
|
||||||
progressHumanSize(contentSize), dest,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func runCopy(ctx context.Context, dockerCli command.Cli, opts copyOptions) error {
|
func runCopy(ctx context.Context, dockerCli command.Cli, opts copyOptions) error {
|
||||||
srcContainer, srcPath := splitCpArg(opts.source)
|
srcContainer, srcPath := splitCpArg(opts.source)
|
||||||
destContainer, destPath := splitCpArg(opts.destination)
|
destContainer, destPath := splitCpArg(opts.destination)
|
||||||
@@ -246,15 +202,14 @@ func runCopy(ctx context.Context, dockerCli command.Cli, opts copyOptions) error
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func resolveLocalPath(localPath string) (absPath string, _ error) {
|
func resolveLocalPath(localPath string) (absPath string, err error) {
|
||||||
absPath, err := filepath.Abs(localPath)
|
if absPath, err = filepath.Abs(localPath); err != nil {
|
||||||
if err != nil {
|
return
|
||||||
return "", err
|
|
||||||
}
|
}
|
||||||
return archive.PreserveTrailingDotOrSeparator(absPath, localPath), nil
|
return archive.PreserveTrailingDotOrSeparator(absPath, localPath), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func copyFromContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cpConfig) (err error) {
|
func copyFromContainer(ctx context.Context, dockerCli command.Cli, copyConfig cpConfig) (err error) {
|
||||||
dstPath := copyConfig.destPath
|
dstPath := copyConfig.destPath
|
||||||
srcPath := copyConfig.sourcePath
|
srcPath := copyConfig.sourcePath
|
||||||
|
|
||||||
@@ -270,17 +225,15 @@ func copyFromContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cp
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
apiClient := dockerCLI.Client()
|
client := dockerCli.Client()
|
||||||
// if client requests to follow symlinks, then must decide target file to be copied
|
// if client requests to follow symbol link, then must decide target file to be copied
|
||||||
var rebaseName string
|
var rebaseName string
|
||||||
if copyConfig.followLink {
|
if copyConfig.followLink {
|
||||||
src, err := apiClient.ContainerStatPath(ctx, copyConfig.container, client.ContainerStatPathOptions{
|
srcStat, err := client.ContainerStatPath(ctx, copyConfig.container, srcPath)
|
||||||
Path: srcPath,
|
|
||||||
})
|
|
||||||
|
|
||||||
// If the destination is a symbolic link, we should follow it.
|
// If the destination is a symbolic link, we should follow it.
|
||||||
if err == nil && src.Stat.Mode&os.ModeSymlink != 0 {
|
if err == nil && srcStat.Mode&os.ModeSymlink != 0 {
|
||||||
linkTarget := src.Stat.LinkTarget
|
linkTarget := srcStat.LinkTarget
|
||||||
if !isAbs(linkTarget) {
|
if !isAbs(linkTarget) {
|
||||||
// Join with the parent directory.
|
// Join with the parent directory.
|
||||||
srcParent, _ := archive.SplitPathDirEntry(srcPath)
|
srcParent, _ := archive.SplitPathDirEntry(srcPath)
|
||||||
@@ -295,24 +248,21 @@ func copyFromContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cp
|
|||||||
ctx, cancel := signal.NotifyContext(ctx, os.Interrupt)
|
ctx, cancel := signal.NotifyContext(ctx, os.Interrupt)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
cpRes, err := apiClient.CopyFromContainer(ctx, copyConfig.container, client.CopyFromContainerOptions{
|
content, stat, err := client.CopyFromContainer(ctx, copyConfig.container, srcPath)
|
||||||
SourcePath: srcPath,
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
content := cpRes.Content
|
defer content.Close()
|
||||||
defer func() { _ = content.Close() }()
|
|
||||||
|
|
||||||
if dstPath == "-" {
|
if dstPath == "-" {
|
||||||
_, err = io.Copy(dockerCLI.Out(), content)
|
_, err = io.Copy(dockerCli.Out(), content)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
srcInfo := archive.CopyInfo{
|
srcInfo := archive.CopyInfo{
|
||||||
Path: srcPath,
|
Path: srcPath,
|
||||||
Exists: true,
|
Exists: true,
|
||||||
IsDir: cpRes.Stat.Mode.IsDir(),
|
IsDir: stat.Mode.IsDir(),
|
||||||
RebaseName: rebaseName,
|
RebaseName: rebaseName,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -334,16 +284,12 @@ func copyFromContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cp
|
|||||||
return archive.CopyTo(preArchive, srcInfo, dstPath)
|
return archive.CopyTo(preArchive, srcInfo, dstPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
restore, done := copyProgress(ctx, dockerCLI.Err(), copyFromContainerHeader, &copiedSize)
|
restore, done := copyProgress(ctx, dockerCli.Err(), copyFromContainerHeader, &copiedSize)
|
||||||
res := archive.CopyTo(preArchive, srcInfo, dstPath)
|
res := archive.CopyTo(preArchive, srcInfo, dstPath)
|
||||||
cancel()
|
cancel()
|
||||||
<-done
|
<-done
|
||||||
restore()
|
restore()
|
||||||
reportedSize := copiedSize
|
fmt.Fprintln(dockerCli.Err(), "Successfully copied", progressHumanSize(copiedSize), "to", dstPath)
|
||||||
if !cpRes.Stat.Mode.IsDir() {
|
|
||||||
reportedSize = cpRes.Stat.Size
|
|
||||||
}
|
|
||||||
_, _ = fmt.Fprint(dockerCLI.Err(), copySummary(reportedSize, copiedSize, dstPath))
|
|
||||||
|
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
@@ -352,66 +298,62 @@ func copyFromContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cp
|
|||||||
// about both the source and destination. The API is a simple tar
|
// about both the source and destination. The API is a simple tar
|
||||||
// archive/extract API but we can use the stat info header about the
|
// archive/extract API but we can use the stat info header about the
|
||||||
// destination to be more informed about exactly what the destination is.
|
// destination to be more informed about exactly what the destination is.
|
||||||
func copyToContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cpConfig) error {
|
func copyToContainer(ctx context.Context, dockerCli command.Cli, copyConfig cpConfig) (err error) {
|
||||||
srcPath := copyConfig.sourcePath
|
srcPath := copyConfig.sourcePath
|
||||||
dstPath := copyConfig.destPath
|
dstPath := copyConfig.destPath
|
||||||
|
|
||||||
if srcPath != "-" {
|
if srcPath != "-" {
|
||||||
// Get an absolute source path.
|
// Get an absolute source path.
|
||||||
p, err := resolveLocalPath(srcPath)
|
srcPath, err = resolveLocalPath(srcPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
srcPath = p
|
|
||||||
}
|
}
|
||||||
|
|
||||||
apiClient := dockerCLI.Client()
|
client := dockerCli.Client()
|
||||||
// Prepare destination copy info by stat-ing the container path.
|
// Prepare destination copy info by stat-ing the container path.
|
||||||
dstInfo := archive.CopyInfo{Path: dstPath}
|
dstInfo := archive.CopyInfo{Path: dstPath}
|
||||||
if dst, err := apiClient.ContainerStatPath(ctx, copyConfig.container, client.ContainerStatPathOptions{Path: dstPath}); err == nil {
|
dstStat, err := client.ContainerStatPath(ctx, copyConfig.container, dstPath)
|
||||||
// If the destination is a symbolic link, we should evaluate it.
|
|
||||||
if dst.Stat.Mode&os.ModeSymlink != 0 {
|
|
||||||
linkTarget := dst.Stat.LinkTarget
|
|
||||||
if !isAbs(linkTarget) {
|
|
||||||
// Join with the parent directory.
|
|
||||||
dstParent, _ := archive.SplitPathDirEntry(dstPath)
|
|
||||||
linkTarget = filepath.Join(dstParent, linkTarget)
|
|
||||||
}
|
|
||||||
|
|
||||||
dstInfo.Path = linkTarget
|
// If the destination is a symbolic link, we should evaluate it.
|
||||||
dst, err = apiClient.ContainerStatPath(ctx, copyConfig.container, client.ContainerStatPathOptions{Path: linkTarget})
|
if err == nil && dstStat.Mode&os.ModeSymlink != 0 {
|
||||||
}
|
linkTarget := dstStat.LinkTarget
|
||||||
// Validate the destination path
|
if !isAbs(linkTarget) {
|
||||||
if err == nil {
|
// Join with the parent directory.
|
||||||
if err := command.ValidateOutputPathFileMode(dst.Stat.Mode); err != nil {
|
dstParent, _ := archive.SplitPathDirEntry(dstPath)
|
||||||
return fmt.Errorf(`destination "%s:%s" must be a directory or a regular file: %w`, copyConfig.container, dstPath, err)
|
linkTarget = filepath.Join(dstParent, linkTarget)
|
||||||
}
|
|
||||||
dstInfo.Exists, dstInfo.IsDir = true, dst.Stat.Mode.IsDir()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ignore any error and assume that the parent directory of the destination
|
dstInfo.Path = linkTarget
|
||||||
// path exists, in which case the copy may still succeed. If there is any
|
dstStat, err = client.ContainerStatPath(ctx, copyConfig.container, linkTarget)
|
||||||
// type of conflict (e.g., non-directory overwriting an existing directory
|
}
|
||||||
// or vice versa) the extraction will fail. If the destination simply did
|
|
||||||
// not exist, but the parent directory does, the extraction will still
|
// Validate the destination path
|
||||||
// succeed.
|
if err := command.ValidateOutputPathFileMode(dstStat.Mode); err != nil {
|
||||||
_ = err // Intentionally ignore stat errors (see above)
|
return errors.Wrapf(err, `destination "%s:%s" must be a directory or a regular file`, copyConfig.container, dstPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore any error and assume that the parent directory of the destination
|
||||||
|
// path exists, in which case the copy may still succeed. If there is any
|
||||||
|
// type of conflict (e.g., non-directory overwriting an existing directory
|
||||||
|
// or vice versa) the extraction will fail. If the destination simply did
|
||||||
|
// not exist, but the parent directory does, the extraction will still
|
||||||
|
// succeed.
|
||||||
|
if err == nil {
|
||||||
|
dstInfo.Exists, dstInfo.IsDir = true, dstStat.Mode.IsDir()
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
content io.ReadCloser
|
content io.ReadCloser
|
||||||
resolvedDstPath string
|
resolvedDstPath string
|
||||||
copiedSize int64
|
copiedSize int64
|
||||||
contentSize int64
|
|
||||||
sizeErr error
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if srcPath == "-" {
|
if srcPath == "-" {
|
||||||
content = os.Stdin
|
content = os.Stdin
|
||||||
resolvedDstPath = dstInfo.Path
|
resolvedDstPath = dstInfo.Path
|
||||||
sizeErr = errors.New("content size not available for stdin")
|
|
||||||
if !dstInfo.IsDir {
|
if !dstInfo.IsDir {
|
||||||
return fmt.Errorf(`destination "%s:%s" must be a directory`, copyConfig.container, dstPath)
|
return errors.Errorf("destination \"%s:%s\" must be a directory", copyConfig.container, dstPath)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Prepare source copy info.
|
// Prepare source copy info.
|
||||||
@@ -420,8 +362,6 @@ func copyToContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cpCo
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
contentSize, sizeErr = localContentSize(srcInfo.Path)
|
|
||||||
|
|
||||||
srcArchive, err := archive.TarResource(srcInfo)
|
srcArchive, err := archive.TarResource(srcInfo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -456,31 +396,24 @@ func copyToContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cpCo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
options := client.CopyToContainerOptions{
|
options := container.CopyToContainerOptions{
|
||||||
DestinationPath: resolvedDstPath,
|
AllowOverwriteDirWithFile: false,
|
||||||
Content: content,
|
CopyUIDGID: copyConfig.copyUIDGID,
|
||||||
CopyUIDGID: copyConfig.copyUIDGID,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if copyConfig.quiet {
|
if copyConfig.quiet {
|
||||||
_, err := apiClient.CopyToContainer(ctx, copyConfig.container, options)
|
return client.CopyToContainer(ctx, copyConfig.container, resolvedDstPath, content, options)
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := signal.NotifyContext(ctx, os.Interrupt)
|
ctx, cancel := signal.NotifyContext(ctx, os.Interrupt)
|
||||||
restore, done := copyProgress(ctx, dockerCLI.Err(), copyToContainerHeader, &copiedSize)
|
restore, done := copyProgress(ctx, dockerCli.Err(), copyToContainerHeader, &copiedSize)
|
||||||
// TODO(thaJeztah): error-handling looks odd here; should it be handled differently?
|
res := client.CopyToContainer(ctx, copyConfig.container, resolvedDstPath, content, options)
|
||||||
_, err := apiClient.CopyToContainer(ctx, copyConfig.container, options)
|
|
||||||
cancel()
|
cancel()
|
||||||
<-done
|
<-done
|
||||||
restore()
|
restore()
|
||||||
reportedSize := copiedSize
|
fmt.Fprintln(dockerCli.Err(), "Successfully copied", progressHumanSize(copiedSize), "to", copyConfig.container+":"+dstInfo.Path)
|
||||||
if sizeErr == nil {
|
|
||||||
reportedSize = contentSize
|
|
||||||
}
|
|
||||||
_, _ = fmt.Fprint(dockerCLI.Err(), copySummary(reportedSize, copiedSize, copyConfig.container+":"+dstInfo.Path))
|
|
||||||
|
|
||||||
return err
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
// We use `:` as a delimiter between CONTAINER and PATH, but `:` could also be
|
// We use `:` as a delimiter between CONTAINER and PATH, but `:` could also be
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user