mirror of https://github.com/docker/cli.git
Compare commits
207 Commits
master
...
v27.3.0-rc
Author | SHA1 | Date |
---|---|---|
|
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 |
|
@ -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
|
|
||||||
|
|
|
@ -19,14 +19,11 @@ Provide the following information:
|
||||||
|
|
||||||
**- How to verify it**
|
**- How to verify it**
|
||||||
|
|
||||||
**- Human readable description for the release notes**
|
**- Description for the changelog**
|
||||||
<!--
|
<!--
|
||||||
Write a short (one line) summary that describes the changes in this
|
Write a short (one line) summary that describes the changes in this
|
||||||
pull request for inclusion in the changelog.
|
pull request for inclusion in the changelog.
|
||||||
It must be placed inside the below triple backticks section.
|
It must be placed inside the below triple backticks section:
|
||||||
|
|
||||||
NOTE: Only fill this section if changes introduced in this PR are user-facing.
|
|
||||||
The PR must have a relevant impact/ label.
|
|
||||||
-->
|
-->
|
||||||
```markdown changelog
|
```markdown changelog
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,6 @@ on:
|
||||||
branches:
|
branches:
|
||||||
- 'master'
|
- 'master'
|
||||||
- '[0-9]+.[0-9]+'
|
- '[0-9]+.[0-9]+'
|
||||||
- '[0-9]+.x'
|
|
||||||
tags:
|
tags:
|
||||||
- 'v*'
|
- 'v*'
|
||||||
pull_request:
|
pull_request:
|
||||||
|
@ -61,12 +60,17 @@ jobs:
|
||||||
- ""
|
- ""
|
||||||
- glibc
|
- glibc
|
||||||
steps:
|
steps:
|
||||||
|
-
|
||||||
|
name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
-
|
-
|
||||||
name: Set up Docker Buildx
|
name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
-
|
-
|
||||||
name: Build
|
name: Build
|
||||||
uses: docker/bake-action@v6
|
uses: docker/bake-action@v5
|
||||||
with:
|
with:
|
||||||
targets: ${{ matrix.target }}
|
targets: ${{ matrix.target }}
|
||||||
set: |
|
set: |
|
||||||
|
@ -99,12 +103,8 @@ jobs:
|
||||||
if: ${{ github.event_name != 'pull_request' && github.repository == 'docker/cli' }}
|
if: ${{ github.event_name != 'pull_request' && github.repository == 'docker/cli' }}
|
||||||
steps:
|
steps:
|
||||||
-
|
-
|
||||||
name: Login to DockerHub
|
name: Checkout
|
||||||
if: github.event_name != 'pull_request'
|
uses: actions/checkout@v4
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKERHUB_CLIBIN_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKERHUB_CLIBIN_TOKEN }}
|
|
||||||
-
|
-
|
||||||
name: Set up QEMU
|
name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
|
@ -121,15 +121,21 @@ jobs:
|
||||||
type=semver,pattern={{version}}
|
type=semver,pattern={{version}}
|
||||||
type=ref,event=branch
|
type=ref,event=branch
|
||||||
type=ref,event=pr
|
type=ref,event=pr
|
||||||
type=semver,pattern={{major}}
|
type=sha
|
||||||
type=semver,pattern={{major}}.{{minor}}
|
-
|
||||||
|
name: Login to DockerHub
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_CLIBIN_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_CLIBIN_TOKEN }}
|
||||||
-
|
-
|
||||||
name: Build and push image
|
name: Build and push image
|
||||||
uses: docker/bake-action@v6
|
uses: docker/bake-action@v5
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
./docker-bake.hcl
|
./docker-bake.hcl
|
||||||
cwd://${{ steps.meta.outputs.bake-file }}
|
${{ steps.meta.outputs.bake-file }}
|
||||||
targets: bin-image-cross
|
targets: bin-image-cross
|
||||||
push: ${{ github.event_name != 'pull_request' }}
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
set: |
|
set: |
|
||||||
|
@ -163,12 +169,15 @@ jobs:
|
||||||
matrix:
|
matrix:
|
||||||
platform: ${{ fromJson(needs.prepare-plugins.outputs.matrix) }}
|
platform: ${{ fromJson(needs.prepare-plugins.outputs.matrix) }}
|
||||||
steps:
|
steps:
|
||||||
|
-
|
||||||
|
name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
-
|
-
|
||||||
name: Set up Docker Buildx
|
name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
-
|
-
|
||||||
name: Build
|
name: Build
|
||||||
uses: docker/bake-action@v6
|
uses: docker/bake-action@v5
|
||||||
with:
|
with:
|
||||||
targets: plugins-cross
|
targets: plugins-cross
|
||||||
set: |
|
set: |
|
||||||
|
|
|
@ -14,7 +14,6 @@ on:
|
||||||
branches:
|
branches:
|
||||||
- 'master'
|
- 'master'
|
||||||
- '[0-9]+.[0-9]+'
|
- '[0-9]+.[0-9]+'
|
||||||
- '[0-9]+.x'
|
|
||||||
tags:
|
tags:
|
||||||
- 'v*'
|
- 'v*'
|
||||||
pull_request:
|
pull_request:
|
||||||
|
@ -34,8 +33,8 @@ on:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
codeql:
|
codeql:
|
||||||
runs-on: ubuntu-24.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:
|
||||||
|
@ -49,6 +48,11 @@ jobs:
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
|
-
|
||||||
|
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
|
||||||
|
@ -63,7 +67,7 @@ jobs:
|
||||||
name: Update Go
|
name: Update Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: "1.24.5"
|
go-version: '1.21'
|
||||||
-
|
-
|
||||||
name: Initialize CodeQL
|
name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v3
|
uses: github/codeql-action/init@v3
|
||||||
|
|
|
@ -19,28 +19,29 @@ on:
|
||||||
branches:
|
branches:
|
||||||
- 'master'
|
- 'master'
|
||||||
- '[0-9]+.[0-9]+'
|
- '[0-9]+.[0-9]+'
|
||||||
- '[0-9]+.x'
|
|
||||||
tags:
|
tags:
|
||||||
- 'v*'
|
- 'v*'
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
tests:
|
e2e:
|
||||||
runs-on: ubuntu-24.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:
|
||||||
- 28 # latest
|
- 27.0 # latest
|
||||||
- 27 # latest - 1
|
- 26.1 # latest - 1
|
||||||
- 26 # github actions default
|
- 23.0 # mirantis lts
|
||||||
- 23 # 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: Checkout
|
name: Checkout
|
||||||
|
@ -74,7 +75,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@v5
|
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 }}
|
||||||
|
|
|
@ -19,7 +19,6 @@ on:
|
||||||
branches:
|
branches:
|
||||||
- 'master'
|
- 'master'
|
||||||
- '[0-9]+.[0-9]+'
|
- '[0-9]+.[0-9]+'
|
||||||
- '[0-9]+.x'
|
|
||||||
tags:
|
tags:
|
||||||
- 'v*'
|
- 'v*'
|
||||||
pull_request:
|
pull_request:
|
||||||
|
@ -28,19 +27,22 @@ jobs:
|
||||||
ctn:
|
ctn:
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
|
-
|
||||||
|
name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
-
|
-
|
||||||
name: Set up Docker Buildx
|
name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
-
|
-
|
||||||
name: Test
|
name: Test
|
||||||
uses: docker/bake-action@v6
|
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@v5
|
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:
|
||||||
|
@ -57,6 +59,12 @@ jobs:
|
||||||
- macos-14 # macOS 14 on arm64 (Apple Silicon M1)
|
- macos-14 # macOS 14 on arm64 (Apple Silicon M1)
|
||||||
# - windows-2022 # FIXME: some tests are failing on the Windows runner, as well as on Appveyor since June 24, 2018: https://ci.appveyor.com/project/docker/cli/history
|
# - 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: Prepare git
|
||||||
|
if: matrix.os == 'windows-latest'
|
||||||
|
run: |
|
||||||
|
git config --system core.autocrlf false
|
||||||
|
git config --system core.eol lf
|
||||||
-
|
-
|
||||||
name: Checkout
|
name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
@ -66,7 +74,7 @@ jobs:
|
||||||
name: Set up Go
|
name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: "1.24.5"
|
go-version: 1.22.7
|
||||||
-
|
-
|
||||||
name: Test
|
name: Test
|
||||||
run: |
|
run: |
|
||||||
|
@ -76,8 +84,8 @@ jobs:
|
||||||
shell: bash
|
shell: bash
|
||||||
-
|
-
|
||||||
name: Send to Codecov
|
name: Send to Codecov
|
||||||
uses: codecov/codecov-action@v5
|
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 }}
|
||||||
|
|
|
@ -15,8 +15,7 @@ on:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check-area-label:
|
check-area-label:
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-20.04
|
||||||
timeout-minutes: 120 # guardrails timeout for the whole job
|
|
||||||
steps:
|
steps:
|
||||||
- name: Missing `area/` label
|
- name: Missing `area/` label
|
||||||
if: 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/')
|
||||||
|
@ -27,10 +26,9 @@ jobs:
|
||||||
run: exit 0
|
run: exit 0
|
||||||
|
|
||||||
check-changelog:
|
check-changelog:
|
||||||
runs-on: ubuntu-24.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:
|
||||||
|
@ -42,9 +40,8 @@ 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. Please provide a description for the changelog."
|
echo "::error::Changelog section is empty. Provide a description for the changelog."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
@ -53,20 +50,12 @@ jobs:
|
||||||
echo "::error::Description looks too short: $desc"
|
echo "::error::Description looks too short: $desc"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
else
|
|
||||||
if [ -n "$desc" ]; then
|
|
||||||
echo "::error::PR has a changelog description, but no changelog label"
|
|
||||||
echo "::error::Please add the relevant 'impact/' label to the PR or remove the changelog description"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "This PR will be included in the release notes with the following note:"
|
echo "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-24.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:
|
||||||
|
|
|
@ -19,7 +19,6 @@ on:
|
||||||
branches:
|
branches:
|
||||||
- 'master'
|
- 'master'
|
||||||
- '[0-9]+.[0-9]+'
|
- '[0-9]+.[0-9]+'
|
||||||
- '[0-9]+.x'
|
|
||||||
tags:
|
tags:
|
||||||
- 'v*'
|
- 'v*'
|
||||||
pull_request:
|
pull_request:
|
||||||
|
@ -36,9 +35,12 @@ jobs:
|
||||||
- validate-vendor
|
- validate-vendor
|
||||||
- update-authors # ensure authors update target runs fine
|
- update-authors # ensure authors update target runs fine
|
||||||
steps:
|
steps:
|
||||||
|
-
|
||||||
|
name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
-
|
-
|
||||||
name: Run
|
name: Run
|
||||||
uses: docker/bake-action@v6
|
uses: docker/bake-action@v5
|
||||||
with:
|
with:
|
||||||
targets: ${{ matrix.target }}
|
targets: ${{ matrix.target }}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
235
.golangci.yml
235
.golangci.yml
|
@ -1,226 +1,187 @@
|
||||||
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.24.5"
|
|
||||||
|
|
||||||
timeout: 5m
|
|
||||||
|
|
||||||
issues:
|
|
||||||
# Maximum issues count per one linter. Set to 0 to disable. Default is 50.
|
|
||||||
max-issues-per-linter: 0
|
|
||||||
|
|
||||||
# Maximum count of issues with the same text. Set to 0 to disable. Default is 3.
|
|
||||||
max-same-issues: 0
|
|
||||||
|
|
||||||
formatters:
|
|
||||||
enable:
|
|
||||||
- gofumpt # Detects whether code was gofumpt-ed.
|
|
||||||
- goimports
|
|
||||||
|
|
||||||
exclusions:
|
|
||||||
generated: strict
|
|
||||||
|
|
||||||
linters:
|
linters:
|
||||||
enable:
|
enable:
|
||||||
- asasalint # Detects "[]any" used as argument for variadic "func(...any)".
|
|
||||||
- bodyclose
|
- bodyclose
|
||||||
- copyloopvar # Detects places where loop variables are copied.
|
|
||||||
- depguard
|
- depguard
|
||||||
- dogsled # Detects assignments with too many blank identifiers.
|
- dogsled
|
||||||
- dupword # Detects duplicate words.
|
- 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.
|
- exportloopref # Detects pointers to enclosing loop variables.
|
||||||
- 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.
|
- gocritic # Metalinter; detects bugs, performance, and styling issues.
|
||||||
- gocyclo
|
- gocyclo
|
||||||
|
- gofumpt # Detects whether code was gofumpt-ed.
|
||||||
|
- goimports
|
||||||
- gosec # Detects security problems.
|
- 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 # Detects reassigning a top-level variable in another package.
|
- reassign
|
||||||
- revive # Metalinter; drop-in replacement for golint.
|
- revive # Metalinter; drop-in replacement for golint.
|
||||||
- spancheck # Detects mistakes with OpenTelemetry/Census spans.
|
|
||||||
- staticcheck
|
- staticcheck
|
||||||
|
- stylecheck # Replacement for golint
|
||||||
|
- tenv # Detects using os.Setenv instead of t.Setenv.
|
||||||
- thelper # Detects test helpers without t.Helper().
|
- thelper # Detects test helpers without t.Helper().
|
||||||
- tparallel # Detects inappropriate usage of t.Parallel().
|
- tparallel # Detects inappropriate usage of t.Parallel().
|
||||||
|
- typecheck
|
||||||
- unconvert # Detects unnecessary type conversions.
|
- 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:
|
||||||
|
timeout: 5m
|
||||||
|
|
||||||
|
linters-settings:
|
||||||
depguard:
|
depguard:
|
||||||
rules:
|
rules:
|
||||||
main:
|
main:
|
||||||
deny:
|
deny:
|
||||||
- pkg: "github.com/containerd/containerd/errdefs"
|
- pkg: io/ioutil
|
||||||
desc: The containerd errdefs package was migrated to a separate module. Use github.com/containerd/errdefs instead.
|
|
||||||
- pkg: "github.com/containerd/containerd/log"
|
|
||||||
desc: The containerd log package was migrated to a separate module. Use github.com/containerd/log instead.
|
|
||||||
- pkg: "github.com/containerd/containerd/pkg/userns"
|
|
||||||
desc: Use github.com/moby/sys/userns instead.
|
|
||||||
- pkg: "github.com/containerd/containerd/platforms"
|
|
||||||
desc: The containerd platforms package was migrated to a separate module. Use github.com/containerd/platforms instead.
|
|
||||||
- pkg: "github.com/docker/docker/pkg/system"
|
|
||||||
desc: This package should not be used unless strictly necessary.
|
|
||||||
- pkg: "github.com/docker/distribution/uuid"
|
|
||||||
desc: Use github.com/google/uuid instead.
|
|
||||||
- pkg: "io/ioutil"
|
|
||||||
desc: The io/ioutil package has been deprecated, see https://go.dev/doc/go1.16#ioutil
|
desc: The io/ioutil package has been deprecated, see https://go.dev/doc/go1.16#ioutil
|
||||||
|
|
||||||
forbidigo:
|
|
||||||
forbid:
|
|
||||||
- pkg: ^regexp$
|
|
||||||
pattern: ^regexp\.MustCompile
|
|
||||||
msg: Use internal/lazyregexp.New instead.
|
|
||||||
|
|
||||||
gocyclo:
|
gocyclo:
|
||||||
min-complexity: 16
|
min-complexity: 16
|
||||||
|
|
||||||
gosec:
|
|
||||||
excludes:
|
|
||||||
- G104 # G104: Errors unhandled; (TODO: reduce unhandled errors, or explicitly ignore)
|
|
||||||
- G115 # G115: integer overflow conversion; (TODO: verify these: https://github.com/docker/cli/issues/5584)
|
|
||||||
- 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:
|
govet:
|
||||||
enable:
|
enable:
|
||||||
- shadow
|
- shadow
|
||||||
settings:
|
settings:
|
||||||
shadow:
|
shadow:
|
||||||
strict: true
|
strict: true
|
||||||
|
|
||||||
lll:
|
lll:
|
||||||
line-length: 200
|
line-length: 200
|
||||||
|
|
||||||
importas:
|
|
||||||
# Do not allow unaliased imports of aliased packages.
|
|
||||||
no-unaliased: true
|
|
||||||
|
|
||||||
alias:
|
|
||||||
# Enforce alias to prevent it accidentally being used instead of our
|
|
||||||
# own errdefs package (or vice-versa).
|
|
||||||
- pkg: github.com/containerd/errdefs
|
|
||||||
alias: cerrdefs
|
|
||||||
- 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:
|
nakedret:
|
||||||
# Disallow naked returns if func has more lines of code than this setting.
|
command: nakedret
|
||||||
# Default: 30
|
pattern: ^(?P<path>.*?\\.go):(?P<line>\\d+)\\s*(?P<message>.*)$
|
||||||
max-func-lines: 0
|
|
||||||
|
|
||||||
staticcheck:
|
|
||||||
checks:
|
|
||||||
- all
|
|
||||||
- -QF1008 # Omit embedded fields from selector expression; https://staticcheck.dev/docs/checks/#QF1008
|
|
||||||
|
|
||||||
revive:
|
revive:
|
||||||
rules:
|
rules:
|
||||||
- name: empty-block # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#empty-block
|
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#import-shadowing
|
||||||
- name: empty-lines # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#empty-lines
|
- name: import-shadowing
|
||||||
- name: import-shadowing # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#import-shadowing
|
severity: warning
|
||||||
- name: line-length-limit # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#line-length-limit
|
disabled: false
|
||||||
arguments: [200]
|
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#empty-block
|
||||||
- name: unused-receiver # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unused-receiver
|
- name: empty-block
|
||||||
- name: use-any # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#use-any
|
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
|
||||||
|
|
||||||
usetesting:
|
issues:
|
||||||
os-chdir: false # FIXME(thaJeztah): Disable `os.Chdir()` detections; should be automatically disabled on Go < 1.24; see https://github.com/docker/cli/pull/5835#issuecomment-2665302478
|
# The default exclusion rules are a bit too permissive, so copying the relevant ones below
|
||||||
context-background: false # FIXME(thaJeztah): Disable `context.Background()` detections; should be automatically disabled on Go < 1.24; see https://github.com/docker/cli/pull/5835#issuecomment-2665302478
|
exclude-use-default: false
|
||||||
context-todo: false # FIXME(thaJeztah): Disable `context.TODO()` detections; should be automatically disabled on Go < 1.24; see https://github.com/docker/cli/pull/5835#issuecomment-2665302478
|
|
||||||
|
|
||||||
exclusions:
|
exclude:
|
||||||
# We prefer to use an "linters.exclusions.rules" so that new "default" exclusions are not
|
- parameter .* always receives
|
||||||
|
|
||||||
|
exclude-files:
|
||||||
|
- cli/compose/schema/bindata.go
|
||||||
|
- .*generated.*
|
||||||
|
|
||||||
|
exclude-rules:
|
||||||
|
# We prefer to use an "exclude-list" so that new "default" exclusions are not
|
||||||
# automatically inherited. We can decide whether or not to follow upstream
|
# automatically inherited. We can decide whether or not to follow upstream
|
||||||
# defaults when updating golang-ci-lint versions.
|
# defaults when updating golang-ci-lint versions.
|
||||||
# Unfortunately, this means we have to copy the whole exclusion pattern, as
|
# Unfortunately, this means we have to copy the whole exclusion pattern, as
|
||||||
# (unlike the "include" option), the "exclude" option does not take exclusion
|
# (unlike the "include" option), the "exclude" option does not take exclusion
|
||||||
# ID's.
|
# ID's.
|
||||||
#
|
#
|
||||||
# These exclusion patterns are copied from the default excludes at:
|
# These exclusion patterns are copied from the default excluses at:
|
||||||
# https://github.com/golangci/golangci-lint/blob/v1.61.0/pkg/config/issues.go#L11-L104
|
# https://github.com/golangci/golangci-lint/blob/v1.44.0/pkg/config/issues.go#L10-L104
|
||||||
#
|
|
||||||
# The default list of exclusions can be found at:
|
|
||||||
# https://golangci-lint.run/usage/false-positives/#default-exclusions
|
|
||||||
generated: strict
|
|
||||||
|
|
||||||
rules:
|
# EXC0001
|
||||||
|
- text: "Error return value of .((os\\.)?std(out|err)\\..*|.*Close|.*Flush|os\\.Remove(All)?|.*print(f|ln)?|os\\.(Un)?Setenv). is not checked"
|
||||||
|
linters:
|
||||||
|
- errcheck
|
||||||
# EXC0003
|
# EXC0003
|
||||||
- text: "func name will be used as test\\.Test.* by other packages, and that stutters; consider calling this"
|
- text: "func name will be used as test\\.Test.* by other packages, and that stutters; consider calling this"
|
||||||
linters:
|
linters:
|
||||||
- revive
|
- revive
|
||||||
|
# EXC0006
|
||||||
|
- text: "Use of unsafe calls should be audited"
|
||||||
|
linters:
|
||||||
|
- gosec
|
||||||
# EXC0007
|
# EXC0007
|
||||||
- text: "Subprocess launch(ed with variable|ing should be audited)"
|
- text: "Subprocess launch(ed with variable|ing should be audited)"
|
||||||
linters:
|
linters:
|
||||||
- gosec
|
- gosec
|
||||||
|
# EXC0008
|
||||||
|
# TODO: evaluate these and fix where needed: G307: Deferring unsafe method "*os.File" on type "Close" (gosec)
|
||||||
|
- text: "G307"
|
||||||
|
linters:
|
||||||
|
- gosec
|
||||||
# EXC0009
|
# EXC0009
|
||||||
- text: "(Expect directory permissions to be 0750 or less|Expect file permissions to be 0600 or less)"
|
- text: "(Expect directory permissions to be 0750 or less|Expect file permissions to be 0600 or less)"
|
||||||
linters:
|
linters:
|
||||||
- gosec
|
- gosec
|
||||||
|
|
||||||
# EXC0010
|
# EXC0010
|
||||||
- text: "Potential file inclusion via variable"
|
- text: "Potential file inclusion via variable"
|
||||||
linters:
|
linters:
|
||||||
- gosec
|
- gosec
|
||||||
|
|
||||||
|
# G113 Potential uncontrolled memory consumption in Rat.SetString (CVE-2022-23772)
|
||||||
|
# only affects gp < 1.16.14. and go < 1.17.7
|
||||||
|
- text: "G113"
|
||||||
|
linters:
|
||||||
|
- gosec
|
||||||
|
# TODO: G104: Errors unhandled. (gosec)
|
||||||
|
- text: "G104"
|
||||||
|
linters:
|
||||||
|
- gosec
|
||||||
|
# Looks like the match in "EXC0007" above doesn't catch this one
|
||||||
|
# TODO: consider upstreaming this to golangci-lint's default exclusion rules
|
||||||
|
- text: "G204: Subprocess launched with a potential tainted input or cmd arguments"
|
||||||
|
linters:
|
||||||
|
- gosec
|
||||||
|
# Looks like the match in "EXC0009" above doesn't catch this one
|
||||||
|
# TODO: consider upstreaming this to golangci-lint's default exclusion rules
|
||||||
|
- text: "G306: Expect WriteFile permissions to be 0600 or less"
|
||||||
|
linters:
|
||||||
|
- gosec
|
||||||
|
|
||||||
# TODO: make sure all packages have a description. Currently, there's 67 packages without.
|
# TODO: make sure all packages have a description. Currently, there's 67 packages without.
|
||||||
- text: "package-comments: should have a package comment"
|
- text: "package-comments: should have a package comment"
|
||||||
linters:
|
linters:
|
||||||
- revive
|
- 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.
|
# Exclude some linters from running on tests files.
|
||||||
- path: _test\.go
|
- path: _test\.go
|
||||||
linters:
|
linters:
|
||||||
- errcheck
|
- errcheck
|
||||||
- gosec
|
- gosec
|
||||||
|
|
||||||
- text: "ST1000: at least one file in a package should have a package comment"
|
- text: "ST1000: at least one file in a package should have a package comment"
|
||||||
linters:
|
linters:
|
||||||
- staticcheck
|
- stylecheck
|
||||||
|
|
||||||
# Allow "err" and "ok" vars to shadow existing declarations, otherwise we get too many false positives.
|
# Allow "err" and "ok" vars to shadow existing declarations, otherwise we get too many false positives.
|
||||||
- text: '^shadow: declaration of "(err|ok)" shadows declaration'
|
- text: '^shadow: declaration of "(err|ok)" shadows declaration'
|
||||||
linters:
|
linters:
|
||||||
- govet
|
- 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
|
|
||||||
|
|
||||||
# Log a warning if an exclusion rule is unused.
|
# Maximum issues count per one linter. Set to 0 to disable. Default is 50.
|
||||||
# Default: false
|
max-issues-per-linter: 0
|
||||||
warn-unused: true
|
|
||||||
|
# Maximum count of issues with the same text. Set to 0 to disable. Default is 3.
|
||||||
|
max-same-issues: 0
|
||||||
|
|
13
.mailmap
13
.mailmap
|
@ -43,7 +43,6 @@ 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>
|
||||||
|
@ -66,9 +65,6 @@ Arnaud Porterie <icecrime@gmail.com>
|
||||||
Arnaud Porterie <icecrime@gmail.com> <arnaud.porterie@docker.com>
|
Arnaud Porterie <icecrime@gmail.com> <arnaud.porterie@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.dev@gmail.com>
|
|
||||||
Austin Vazquez <austin.vazquez.dev@gmail.com> <55906459+austinvazquez@users.noreply.github.com>
|
|
||||||
Austin Vazquez <austin.vazquez.dev@gmail.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>
|
||||||
|
@ -199,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>
|
||||||
|
@ -220,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>
|
||||||
|
@ -337,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>
|
||||||
|
@ -348,7 +339,6 @@ 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>
|
||||||
Luke Marsden <me@lukemarsden.net> <luke@digital-crocus.com>
|
Luke Marsden <me@lukemarsden.net> <luke@digital-crocus.com>
|
||||||
Lyn <energylyn@zju.edu.cn>
|
Lyn <energylyn@zju.edu.cn>
|
||||||
|
@ -406,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>
|
||||||
|
@ -430,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>
|
||||||
|
@ -510,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>
|
||||||
|
|
31
AUTHORS
31
AUTHORS
|
@ -48,7 +48,6 @@ 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>
|
||||||
|
@ -82,7 +81,6 @@ 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>
|
||||||
|
@ -90,7 +88,6 @@ 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.dev@gmail.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>
|
||||||
|
@ -135,7 +132,6 @@ 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>
|
||||||
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>
|
||||||
|
@ -193,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>
|
||||||
|
@ -242,7 +237,6 @@ 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>
|
||||||
|
@ -314,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>
|
||||||
|
@ -352,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>
|
||||||
|
@ -402,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>
|
||||||
|
@ -456,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>
|
||||||
|
@ -501,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>
|
||||||
|
@ -529,7 +515,6 @@ 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>
|
||||||
|
@ -574,7 +559,6 @@ Matt Robenolt <matt@ydekproductions.com>
|
||||||
Matteo Orefice <matteo.orefice@bites4bits.software>
|
Matteo Orefice <matteo.orefice@bites4bits.software>
|
||||||
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 Shytikov <mshytikov@gmail.com>
|
Max Shytikov <mshytikov@gmail.com>
|
||||||
Max-Julian Pogner <max-julian@pogner.at>
|
Max-Julian Pogner <max-julian@pogner.at>
|
||||||
|
@ -582,7 +566,6 @@ 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>
|
||||||
|
@ -615,9 +598,7 @@ 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>
|
|
||||||
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>
|
||||||
|
@ -652,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>
|
||||||
|
@ -674,12 +653,10 @@ 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>
|
||||||
|
@ -701,6 +678,7 @@ 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>
|
||||||
|
@ -760,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>
|
||||||
|
@ -793,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>
|
||||||
|
@ -804,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>
|
||||||
|
@ -892,7 +867,6 @@ 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>
|
||||||
|
@ -934,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>
|
|
||||||
|
|
|
@ -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/docker/docker/blob/master/project/REVIEWING.md).
|
use for simple changes](https://docs.docker.com/opensource/workflow/make-a-contribution/).
|
||||||
|
|
||||||
### Talking to other Docker users and contributors
|
### Talking to other Docker users and contributors
|
||||||
|
|
||||||
|
@ -124,8 +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://docs.docker.com/contribute/style/grammar/) and instructions on [building
|
guide](https://docs.docker.com/opensource/doc-style) and instructions on [building
|
||||||
the documentation](https://docs.docker.com/contribute/).
|
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
|
||||||
|
@ -134,41 +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 package.
|
|
||||||
```
|
|
||||||
vendor: github.com/docker/docker 6ac445c42bad (master, v28.0-dev)
|
|
||||||
```
|
|
||||||
|
|
||||||
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
|
||||||
|
|
35
Dockerfile
35
Dockerfile
|
@ -1,37 +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.22
|
|
||||||
ARG BASE_DEBIAN_DISTRO=bookworm
|
ARG BASE_DEBIAN_DISTRO=bookworm
|
||||||
|
|
||||||
ARG GO_VERSION=1.24.5
|
ARG GO_VERSION=1.22.7
|
||||||
ARG XX_VERSION=1.6.1
|
ARG XX_VERSION=1.5.0
|
||||||
ARG GOVERSIONINFO_VERSION=v1.4.1
|
ARG GOVERSIONINFO_VERSION=v1.3.0
|
||||||
|
ARG GOTESTSUM_VERSION=v1.10.0
|
||||||
# GOTESTSUM_VERSION sets the version of gotestsum to install in the dev container.
|
ARG BUILDX_VERSION=0.16.1
|
||||||
# It must be a valid tag in the https://github.com/gotestyourself/gotestsum repository.
|
ARG COMPOSE_VERSION=v2.29.0
|
||||||
ARG GOTESTSUM_VERSION=v1.12.3
|
|
||||||
|
|
||||||
# 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.25.0
|
|
||||||
|
|
||||||
# COMPOSE_VERSION is the version of compose to install in the dev container.
|
|
||||||
# It must be a tag in the docker.io/docker/compose-bin image repository
|
|
||||||
# on Docker Hub.
|
|
||||||
ARG COMPOSE_VERSION=v2.38.2
|
|
||||||
|
|
||||||
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
|
||||||
|
@ -78,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") && \
|
||||||
|
@ -128,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 . .
|
||||||
|
|
47
Makefile
47
Makefile
|
@ -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.23 . ; \
|
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,63 +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: 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,10 +1,9 @@
|
||||||
# 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://goreportcard.com/report/github.com/docker/cli)
|
[](https://goreportcard.com/report/github.com/docker/cli)
|
||||||
[](https://scorecard.dev/viewer/?uri=github.com/docker/cli)
|
|
||||||
[](https://codecov.io/gh/docker/cli)
|
[](https://codecov.io/gh/docker/cli)
|
||||||
|
|
||||||
## About
|
## About
|
||||||
|
|
44
SECURITY.md
44
SECURITY.md
|
@ -1,44 +0,0 @@
|
||||||
# Security Policy
|
|
||||||
|
|
||||||
The maintainers of the Docker CLI take security seriously. If you discover
|
|
||||||
a security issue, please bring it to their attention right away!
|
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
|
||||||
|
|
||||||
Please **DO NOT** file a public issue, instead send your report privately
|
|
||||||
to [security@docker.com](mailto:security@docker.com).
|
|
||||||
|
|
||||||
Reporter(s) can expect a response within 72 hours, acknowledging the issue was
|
|
||||||
received.
|
|
||||||
|
|
||||||
## Review Process
|
|
||||||
|
|
||||||
After receiving the report, an initial triage and technical analysis is
|
|
||||||
performed to confirm the report and determine its scope. We may request
|
|
||||||
additional information in this stage of the process.
|
|
||||||
|
|
||||||
Once a reviewer has confirmed the relevance of the report, a draft security
|
|
||||||
advisory will be created on GitHub. The draft advisory will be used to discuss
|
|
||||||
the issue with maintainers, the reporter(s), and where applicable, other
|
|
||||||
affected parties under embargo.
|
|
||||||
|
|
||||||
If the vulnerability is accepted, a timeline for developing a patch, public
|
|
||||||
disclosure, and patch release will be determined. If there is an embargo period
|
|
||||||
on public disclosure before the patch release, the reporter(s) are expected to
|
|
||||||
participate in the discussion of the timeline and abide by agreed upon dates
|
|
||||||
for public disclosure.
|
|
||||||
|
|
||||||
## Accreditation
|
|
||||||
|
|
||||||
Security reports are greatly appreciated and we will publicly thank you,
|
|
||||||
although we will keep your name confidential if you request it. We also like to
|
|
||||||
send gifts - if you're into swag, make sure to let us know. We do not currently
|
|
||||||
offer a paid security bounty program at this time.
|
|
||||||
|
|
||||||
## Supported Versions
|
|
||||||
|
|
||||||
This project uses long-lived branches to maintain releases, and follows
|
|
||||||
the maintenance cycle of the Moby project.
|
|
||||||
Refer to [BRANCHES-AND-TAGS.md](https://github.com/moby/moby/blob/master/project/BRANCHES-AND-TAGS.md)
|
|
||||||
in the default branch of the moby repository to learn about the current
|
|
||||||
maintenance status of each branch.
|
|
|
@ -5,31 +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/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())
|
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
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -38,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
|
||||||
},
|
},
|
||||||
|
@ -56,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()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -97,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",
|
||||||
|
|
|
@ -11,8 +11,8 @@ 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:"))
|
fmt.Fprintln(out, aec.Bold.Apply("\nWhat's next:"))
|
||||||
for _, n := range messages {
|
for _, n := range messages {
|
||||||
_, _ = fmt.Fprintln(out, " ", n)
|
_, _ = fmt.Fprintf(out, " %s\n", n)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,30 +0,0 @@
|
||||||
package manager
|
|
||||||
|
|
||||||
import "github.com/docker/cli/cli-plugins/metadata"
|
|
||||||
|
|
||||||
const (
|
|
||||||
// CommandAnnotationPlugin is added to every stub command added by
|
|
||||||
// AddPluginCommandStubs with the value "true" and so can be
|
|
||||||
// used to distinguish plugin stubs from regular commands.
|
|
||||||
CommandAnnotationPlugin = metadata.CommandAnnotationPlugin
|
|
||||||
|
|
||||||
// CommandAnnotationPluginVendor is added to every stub command
|
|
||||||
// added by AddPluginCommandStubs and contains the vendor of
|
|
||||||
// that plugin.
|
|
||||||
CommandAnnotationPluginVendor = metadata.CommandAnnotationPluginVendor
|
|
||||||
|
|
||||||
// CommandAnnotationPluginVersion is added to every stub command
|
|
||||||
// added by AddPluginCommandStubs and contains the version of
|
|
||||||
// that plugin.
|
|
||||||
CommandAnnotationPluginVersion = metadata.CommandAnnotationPluginVersion
|
|
||||||
|
|
||||||
// CommandAnnotationPluginInvalid is added to any stub command
|
|
||||||
// added by AddPluginCommandStubs for an invalid command (that
|
|
||||||
// is, one which failed it's candidate test) and contains the
|
|
||||||
// reason for the failure.
|
|
||||||
CommandAnnotationPluginInvalid = metadata.CommandAnnotationPluginInvalid
|
|
||||||
|
|
||||||
// CommandAnnotationPluginCommandPath is added to overwrite the
|
|
||||||
// command path for a plugin invocation.
|
|
||||||
CommandAnnotationPluginCommandPath = metadata.CommandAnnotationPluginCommandPath
|
|
||||||
)
|
|
|
@ -1,10 +1,6 @@
|
||||||
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
|
// Candidate represents a possible plugin candidate, for mocking purposes
|
||||||
type Candidate interface {
|
type Candidate interface {
|
||||||
|
@ -21,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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,10 +30,10 @@ 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"
|
builtinName = NamePrefix + "builtin"
|
||||||
builtinAlias = metadata.NamePrefix + "alias"
|
builtinAlias = NamePrefix + "alias"
|
||||||
|
|
||||||
badPrefixPath = "/usr/local/libexec/cli-plugins/wobble"
|
badPrefixPath = "/usr/local/libexec/cli-plugins/wobble"
|
||||||
badNamePath = "/usr/local/libexec/cli-plugins/docker-123456"
|
badNamePath = "/usr/local/libexec/cli-plugins/docker-123456"
|
||||||
|
@ -44,9 +43,9 @@ func TestValidateCandidate(t *testing.T) {
|
||||||
|
|
||||||
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),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -60,7 +59,7 @@ func TestValidateCandidate(t *testing.T) {
|
||||||
}{
|
}{
|
||||||
/* Each failing one of the tests */
|
/* Each failing one of the tests */
|
||||||
{name: "empty path", c: &fakeCandidate{path: ""}, err: "plugin candidate path cannot be empty"},
|
{name: "empty path", c: &fakeCandidate{path: ""}, err: "plugin candidate path cannot be empty"},
|
||||||
{name: "bad prefix", c: &fakeCandidate{path: badPrefixPath}, err: fmt.Sprintf("does not have %q prefix", metadata.NamePrefix)},
|
{name: "bad prefix", c: &fakeCandidate{path: badPrefixPath}, err: fmt.Sprintf("does not have %q prefix", NamePrefix)},
|
||||||
{name: "bad path", c: &fakeCandidate{path: badNamePath}, invalid: "did not match"},
|
{name: "bad path", c: &fakeCandidate{path: badNamePath}, invalid: "did not match"},
|
||||||
{name: "builtin command", c: &fakeCandidate{path: builtinName}, invalid: `plugin "builtin" duplicates builtin command`},
|
{name: "builtin 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: "builtin alias", c: &fakeCandidate{path: builtinAlias}, invalid: `plugin "alias" duplicates an alias of builtin command "builtin"`},
|
||||||
|
@ -81,11 +80,11 @@ func TestValidateCandidate(t *testing.T) {
|
||||||
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.TypeOf(&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, "0.1.0")
|
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,25 +44,26 @@ 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,
|
||||||
|
@ -52,7 +82,7 @@ func AddPluginCommandStubs(dockerCLI config.Provider, rootCmd *cobra.Command) (e
|
||||||
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
|
||||||
|
@ -60,7 +90,7 @@ func AddPluginCommandStubs(dockerCLI config.Provider, rootCmd *cobra.Command) (e
|
||||||
cargs = append(cargs, args...)
|
cargs = append(cargs, args...)
|
||||||
cargs = append(cargs, toComplete)
|
cargs = append(cargs, toComplete)
|
||||||
os.Args = cargs
|
os.Args = cargs
|
||||||
runCommand, runErr := PluginRunCommand(dockerCLI, p.Name, cmd)
|
runCommand, runErr := PluginRunCommand(dockerCli, p.Name, cmd)
|
||||||
if runErr != nil {
|
if runErr != nil {
|
||||||
return nil, cobra.ShellCompDirectiveError
|
return nil, cobra.ShellCompDirectiveError
|
||||||
}
|
}
|
||||||
|
@ -75,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,26 +0,0 @@
|
||||||
package manager
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"gotest.tools/v3/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestPluginResourceAttributesEnvvar(t *testing.T) {
|
|
||||||
cmd := &cobra.Command{
|
|
||||||
Annotations: map[string]string{
|
|
||||||
cobra.CommandDisplayNameAnnotation: "docker",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure basic usage is fine.
|
|
||||||
env := appendPluginResourceAttributesEnvvar(nil, cmd, Plugin{Name: "compose"})
|
|
||||||
assert.DeepEqual(t, []string{"OTEL_RESOURCE_ATTRIBUTES=docker.cli.cobra.command_path=docker%20compose"}, env)
|
|
||||||
|
|
||||||
// Add a user-based environment variable to OTEL_RESOURCE_ATTRIBUTES.
|
|
||||||
t.Setenv("OTEL_RESOURCE_ATTRIBUTES", "a.b.c=foo")
|
|
||||||
|
|
||||||
env = appendPluginResourceAttributesEnvvar(nil, cmd, Plugin{Name: "compose"})
|
|
||||||
assert.DeepEqual(t, []string{"OTEL_RESOURCE_ATTRIBUTES=a.b.c=foo,docker.cli.cobra.command_path=docker%20compose"}, env)
|
|
||||||
}
|
|
|
@ -1,10 +1,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.23
|
//go:build go1.21
|
||||||
|
|
||||||
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
|
||||||
|
@ -39,16 +39,16 @@ func (e *pluginError) MarshalText() (text []byte, err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return &pluginError{cause: fmt.Errorf("%s: %w", msg, err)}
|
return &pluginError{cause: errors.Wrap(err, msg)}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewPluginError creates a new pluginError, analogous to
|
// 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...)}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,8 +6,7 @@ import (
|
||||||
"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"
|
||||||
|
@ -30,28 +29,29 @@ type HookPluginData struct {
|
||||||
// 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) {
|
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, "")
|
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 {
|
||||||
// check if the context was cancelled before invoking hooks
|
// check if the context was cancelled before invoking hooks
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
|
@ -59,20 +59,19 @@ func invokeAndCollectHooks(ctx context.Context, cfg *configfile.ConfigFile, root
|
||||||
default:
|
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, pluginCfg := range pluginsCfg {
|
for pluginName, cfg := range pluginsCfg {
|
||||||
match, ok := pluginMatch(pluginCfg, subCmdStr)
|
match, ok := pluginMatch(cfg, subCmdStr)
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
p, err := getPlugin(pluginName, pluginDirs, rootCmd)
|
p, err := GetPlugin(pluginName, dockerCli, rootCmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/docker/cli/cli-plugins/metadata"
|
"github.com/docker/cli/cli/command"
|
||||||
"github.com/docker/cli/cli/config"
|
"github.com/docker/cli/cli/config"
|
||||||
"github.com/docker/cli/cli/config/configfile"
|
"github.com/docker/cli/cli/config/configfile"
|
||||||
"github.com/fvbommel/sortorder"
|
"github.com/fvbommel/sortorder"
|
||||||
|
@ -22,19 +22,17 @@ const (
|
||||||
// used to originally invoke the docker CLI when executing a
|
// used to originally invoke the docker CLI when executing a
|
||||||
// plugin. Assuming $PATH and $CWD remain unchanged this should allow
|
// plugin. Assuming $PATH and $CWD remain unchanged this should allow
|
||||||
// the plugin to re-execute the original CLI.
|
// the plugin to re-execute the original CLI.
|
||||||
ReexecEnvvar = metadata.ReexecEnvvar
|
ReexecEnvvar = "DOCKER_CLI_PLUGIN_ORIGINAL_CLI_COMMAND"
|
||||||
|
|
||||||
// ResourceAttributesEnvvar is the name of the envvar that includes additional
|
// ResourceAttributesEnvvar is the name of the envvar that includes additional
|
||||||
// resource attributes for OTEL.
|
// resource attributes for OTEL.
|
||||||
//
|
|
||||||
// Deprecated: The "OTEL_RESOURCE_ATTRIBUTES" env-var is part of the OpenTelemetry specification; users should define their own const for this. This const will be removed in the next release.
|
|
||||||
ResourceAttributesEnvvar = "OTEL_RESOURCE_ATTRIBUTES"
|
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)
|
||||||
|
@ -61,27 +59,29 @@ func IsNotFound(err error) bool {
|
||||||
// 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")
|
||||||
pluginDirs = append(pluginDirs, pluginDir)
|
if err != nil {
|
||||||
pluginDirs = append(pluginDirs, defaultSystemPluginDirs...)
|
return nil, err
|
||||||
return pluginDirs
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func addPluginCandidatesFromDir(res map[string][]string, d string) {
|
pluginDirs = append(pluginDirs, pluginDir)
|
||||||
|
pluginDirs = append(pluginDirs, defaultSystemPluginDirs...)
|
||||||
|
return pluginDirs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func addPluginCandidatesFromDir(res map[string][]string, d string) error {
|
||||||
dentries, err := os.ReadDir(d)
|
dentries, err := os.ReadDir(d)
|
||||||
// Silently ignore any directories which we cannot list (e.g. due to
|
|
||||||
// permissions or anything else) or which is not a directory
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
for _, dentry := range dentries {
|
for _, dentry := range dentries {
|
||||||
switch dentry.Type() & os.ModeType { //nolint:exhaustive,nolintlint // no need to include all possible file-modes in this list
|
switch dentry.Type() & os.ModeType {
|
||||||
case 0, os.ModeSymlink:
|
case 0, os.ModeSymlink:
|
||||||
// Regular file or symlink, keep going
|
// Regular file or symlink, keep going
|
||||||
default:
|
default:
|
||||||
|
@ -89,35 +89,52 @@ func addPluginCandidatesFromDir(res map[string][]string, d string) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
name := dentry.Name()
|
name := dentry.Name()
|
||||||
if !strings.HasPrefix(name, metadata.NamePrefix) {
|
if !strings.HasPrefix(name, NamePrefix) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
name = strings.TrimPrefix(name, metadata.NamePrefix)
|
name = strings.TrimPrefix(name, NamePrefix)
|
||||||
var err error
|
var err error
|
||||||
if name, err = trimExeSuffix(name); err != nil {
|
if name, err = trimExeSuffix(name); err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
res[name] = append(res[name], filepath.Join(d, dentry.Name()))
|
res[name] = append(res[name], filepath.Join(d, dentry.Name()))
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// listPluginCandidates returns a map from plugin name to the list of (unvalidated) Candidates. The list is in descending order of priority.
|
// listPluginCandidates returns a map from plugin name to the list of (unvalidated) Candidates. The list is in descending order of priority.
|
||||||
func listPluginCandidates(dirs []string) map[string][]string {
|
func listPluginCandidates(dirs []string) (map[string][]string, error) {
|
||||||
result := make(map[string][]string)
|
result := make(map[string][]string)
|
||||||
for _, d := range dirs {
|
for _, d := range dirs {
|
||||||
addPluginCandidatesFromDir(result, d)
|
// Silently ignore any directories which we cannot
|
||||||
|
// Stat (e.g. due to permissions or anything else) or
|
||||||
|
// which is not a directory.
|
||||||
|
if fi, err := os.Stat(d); err != nil || !fi.IsDir() {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
return result
|
if err := addPluginCandidatesFromDir(result, d); err != nil {
|
||||||
|
// Silently ignore paths which don't exist.
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return nil, err // Or return partial result?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPlugin returns a plugin on the system by its name
|
// GetPlugin returns a plugin on the system by its name
|
||||||
func GetPlugin(name string, dockerCLI config.Provider, rootcmd *cobra.Command) (*Plugin, error) {
|
func GetPlugin(name string, dockerCli command.Cli, rootcmd *cobra.Command) (*Plugin, error) {
|
||||||
pluginDirs := getPluginDirs(dockerCLI.ConfigFile())
|
pluginDirs, err := getPluginDirs(dockerCli.ConfigFile())
|
||||||
return getPlugin(name, pluginDirs, rootcmd)
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
candidates, err := listPluginCandidates(pluginDirs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func getPlugin(name string, pluginDirs []string, rootcmd *cobra.Command) (*Plugin, error) {
|
|
||||||
candidates := listPluginCandidates(pluginDirs)
|
|
||||||
if paths, ok := candidates[name]; ok {
|
if paths, ok := candidates[name]; ok {
|
||||||
if len(paths) == 0 {
|
if len(paths) == 0 {
|
||||||
return nil, errPluginNotFound(name)
|
return nil, errPluginNotFound(name)
|
||||||
|
@ -137,21 +154,20 @@ func getPlugin(name string, pluginDirs []string, rootcmd *cobra.Command) (*Plugi
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListPlugins produces a list of the plugins available on the system
|
// ListPlugins produces a list of the plugins available on the system
|
||||||
func ListPlugins(dockerCli config.Provider, rootcmd *cobra.Command) ([]Plugin, error) {
|
func ListPlugins(dockerCli command.Cli, rootcmd *cobra.Command) ([]Plugin, error) {
|
||||||
pluginDirs := getPluginDirs(dockerCli.ConfigFile())
|
pluginDirs, err := getPluginDirs(dockerCli.ConfigFile())
|
||||||
candidates := listPluginCandidates(pluginDirs)
|
if err != nil {
|
||||||
if len(candidates) == 0 {
|
return nil, err
|
||||||
return nil, nil
|
}
|
||||||
|
|
||||||
|
candidates, err := listPluginCandidates(pluginDirs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var plugins []Plugin
|
var plugins []Plugin
|
||||||
var mu sync.Mutex
|
var mu sync.Mutex
|
||||||
ctx := rootcmd.Context()
|
eg, _ := errgroup.WithContext(context.TODO())
|
||||||
if ctx == nil {
|
|
||||||
// Fallback, mostly for tests that pass a bare cobra.command
|
|
||||||
ctx = context.Background()
|
|
||||||
}
|
|
||||||
eg, _ := errgroup.WithContext(ctx)
|
|
||||||
cmds := rootcmd.Commands()
|
cmds := rootcmd.Commands()
|
||||||
for _, paths := range candidates {
|
for _, paths := range candidates {
|
||||||
func(paths []string) {
|
func(paths []string) {
|
||||||
|
@ -188,7 +204,7 @@ func ListPlugins(dockerCli config.Provider, rootcmd *cobra.Command) ([]Plugin, e
|
||||||
// PluginRunCommand returns an "os/exec".Cmd which when .Run() will execute the named plugin.
|
// PluginRunCommand returns an "os/exec".Cmd which when .Run() will execute the named plugin.
|
||||||
// The rootcmd argument is referenced to determine the set of builtin commands in order to detect conficts.
|
// The rootcmd argument is referenced to determine the set of builtin commands in order to detect conficts.
|
||||||
// The error returned satisfies the IsNotFound() predicate if no plugin was found or if the first candidate plugin was invalid somehow.
|
// 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.
|
||||||
|
@ -198,8 +214,11 @@ func PluginRunCommand(dockerCli config.Provider, name string, rootcmd *cobra.Com
|
||||||
// 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,8 +240,7 @@ func PluginRunCommand(dockerCli config.Provider, name string, rootcmd *cobra.Com
|
||||||
// TODO: why are we not returning plugin.Err?
|
// TODO: why are we not returning plugin.Err?
|
||||||
return nil, errPluginNotFound(name)
|
return nil, errPluginNotFound(name)
|
||||||
}
|
}
|
||||||
cmd := exec.Command(plugin.Path, args...) // #nosec G204 -- ignore "Subprocess launched with a potential tainted input or cmd arguments"
|
cmd := exec.Command(plugin.Path, args...)
|
||||||
|
|
||||||
// Using dockerCli.{In,Out,Err}() here results in a hang until something is input.
|
// Using dockerCli.{In,Out,Err}() here results in a hang until something is input.
|
||||||
// See: - https://github.com/golang/go/issues/10338
|
// See: - https://github.com/golang/go/issues/10338
|
||||||
// - https://github.com/golang/go/commit/d000e8742a173aa0659584aa01b7ba2834ba28ab
|
// - https://github.com/golang/go/commit/d000e8742a173aa0659584aa01b7ba2834ba28ab
|
||||||
|
@ -232,7 +250,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
|
||||||
|
@ -242,5 +260,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,7 +1,6 @@
|
||||||
package manager
|
package manager
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
@ -52,7 +51,8 @@ func TestListPluginCandidates(t *testing.T) {
|
||||||
dirs = append(dirs, dir.Join(d))
|
dirs = append(dirs, dir.Join(d))
|
||||||
}
|
}
|
||||||
|
|
||||||
candidates := listPluginCandidates(dirs)
|
candidates, err := listPluginCandidates(dirs)
|
||||||
|
assert.NilError(t, err)
|
||||||
exp := map[string][]string{
|
exp := map[string][]string{
|
||||||
"plugin1": {
|
"plugin1": {
|
||||||
dir.Join("plugins1", "docker-plugin1"),
|
dir.Join("plugins1", "docker-plugin1"),
|
||||||
|
@ -82,35 +82,6 @@ func TestListPluginCandidates(t *testing.T) {
|
||||||
assert.DeepEqual(t, candidates, exp)
|
assert.DeepEqual(t, candidates, exp)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestListPluginCandidatesEmpty(t *testing.T) {
|
|
||||||
tmpDir := t.TempDir()
|
|
||||||
candidates := listPluginCandidates([]string{tmpDir, filepath.Join(tmpDir, "no-such-dir")})
|
|
||||||
assert.Assert(t, len(candidates) == 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Regression test for https://github.com/docker/cli/issues/5643.
|
|
||||||
// Check that inaccessible directories that come before accessible ones are ignored
|
|
||||||
// and do not prevent the latter from being processed.
|
|
||||||
func TestListPluginCandidatesInaccesibleDir(t *testing.T) {
|
|
||||||
dir := fs.NewDir(t, t.Name(),
|
|
||||||
fs.WithDir("no-perm", fs.WithMode(0)),
|
|
||||||
fs.WithDir("plugins",
|
|
||||||
fs.WithFile("docker-buildx", ""),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
defer dir.Remove()
|
|
||||||
|
|
||||||
candidates := listPluginCandidates([]string{
|
|
||||||
dir.Join("no-perm"),
|
|
||||||
dir.Join("plugins"),
|
|
||||||
})
|
|
||||||
assert.DeepEqual(t, candidates, map[string][]string{
|
|
||||||
"buildx": {
|
|
||||||
dir.Join("plugins", "docker-buildx"),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetPlugin(t *testing.T) {
|
func TestGetPlugin(t *testing.T) {
|
||||||
dir := fs.NewDir(t, t.Name(),
|
dir := fs.NewDir(t, t.Name(),
|
||||||
fs.WithFile("docker-bbb", `
|
fs.WithFile("docker-bbb", `
|
||||||
|
@ -173,11 +144,14 @@ func TestErrPluginNotFound(t *testing.T) {
|
||||||
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 := []string{
|
extras := []string{
|
||||||
"foo", "bar", "baz",
|
"foo", "bar", "baz",
|
||||||
|
@ -186,6 +160,7 @@ func TestGetPluginDirs(t *testing.T) {
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,23 +1,30 @@
|
||||||
package manager
|
package manager
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/docker/cli/cli-plugins/metadata"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// NamePrefix is the prefix required on all plugin binary names
|
// NamePrefix is the prefix required on all plugin binary names
|
||||||
NamePrefix = metadata.NamePrefix
|
NamePrefix = "docker-"
|
||||||
|
|
||||||
// MetadataSubcommandName is the name of the plugin subcommand
|
// MetadataSubcommandName is the name of the plugin subcommand
|
||||||
// which must be supported by every plugin and returns the
|
// which must be supported by every plugin and returns the
|
||||||
// plugin metadata.
|
// plugin metadata.
|
||||||
MetadataSubcommandName = metadata.MetadataSubcommandName
|
MetadataSubcommandName = "docker-cli-plugin-metadata"
|
||||||
|
|
||||||
// HookSubcommandName is the name of the plugin subcommand
|
// HookSubcommandName is the name of the plugin subcommand
|
||||||
// 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 = metadata.HookSubcommandName
|
HookSubcommandName = "docker-cli-plugin-hooks"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Metadata provided by the plugin.
|
// Metadata provided by the plugin.
|
||||||
type Metadata = metadata.Metadata
|
type Metadata struct {
|
||||||
|
// SchemaVersion describes the version of this struct. Mandatory, must be "0.1.0"
|
||||||
|
SchemaVersion string `json:",omitempty"`
|
||||||
|
// Vendor is the name of the plugin vendor. Mandatory
|
||||||
|
Vendor string `json:",omitempty"`
|
||||||
|
// Version is the optional version of this plugin.
|
||||||
|
Version string `json:",omitempty"`
|
||||||
|
// ShortDescription should be suitable for a single line help message.
|
||||||
|
ShortDescription string `json:",omitempty"`
|
||||||
|
// URL is a pointer to the plugin's homepage.
|
||||||
|
URL string `json:",omitempty"`
|
||||||
|
}
|
||||||
|
|
|
@ -3,23 +3,21 @@ package manager
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/docker/cli/cli-plugins/metadata"
|
"github.com/pkg/errors"
|
||||||
"github.com/docker/cli/internal/lazyregexp"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
var pluginNameRe = lazyregexp.New("^[a-z][a-z0-9]*$")
|
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"`
|
||||||
|
@ -46,18 +44,18 @@ func newPlugin(c Candidate, 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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -114,9 +112,9 @@ func (p *Plugin) RunHook(ctx context.Context, hookData HookPluginData) ([]byte,
|
||||||
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))
|
||||||
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()
|
hookCmdOutput, err := pCmd.Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, wrapAsPluginError(err, "failed to execute plugin hook subcommand")
|
return nil, wrapAsPluginError(err, "failed to execute plugin hook subcommand")
|
||||||
|
|
|
@ -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,36 +0,0 @@
|
||||||
package metadata
|
|
||||||
|
|
||||||
const (
|
|
||||||
// NamePrefix is the prefix required on all plugin binary names
|
|
||||||
NamePrefix = "docker-"
|
|
||||||
|
|
||||||
// MetadataSubcommandName is the name of the plugin subcommand
|
|
||||||
// which must be supported by every plugin and returns the
|
|
||||||
// plugin metadata.
|
|
||||||
MetadataSubcommandName = "docker-cli-plugin-metadata"
|
|
||||||
|
|
||||||
// HookSubcommandName is the name of the plugin subcommand
|
|
||||||
// which must be implemented by plugins declaring support
|
|
||||||
// for hooks in their metadata.
|
|
||||||
HookSubcommandName = "docker-cli-plugin-hooks"
|
|
||||||
|
|
||||||
// ReexecEnvvar is the name of an ennvar which is set to the command
|
|
||||||
// used to originally invoke the docker CLI when executing a
|
|
||||||
// plugin. Assuming $PATH and $CWD remain unchanged this should allow
|
|
||||||
// the plugin to re-execute the original CLI.
|
|
||||||
ReexecEnvvar = "DOCKER_CLI_PLUGIN_ORIGINAL_CLI_COMMAND"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Metadata provided by the plugin.
|
|
||||||
type Metadata struct {
|
|
||||||
// SchemaVersion describes the version of this struct. Mandatory, must be "0.1.0"
|
|
||||||
SchemaVersion string `json:",omitempty"`
|
|
||||||
// Vendor is the name of the plugin vendor. Mandatory
|
|
||||||
Vendor string `json:",omitempty"`
|
|
||||||
// Version is the optional version of this plugin.
|
|
||||||
Version string `json:",omitempty"`
|
|
||||||
// ShortDescription should be suitable for a single line help message.
|
|
||||||
ShortDescription string `json:",omitempty"`
|
|
||||||
// URL is a pointer to the plugin's homepage.
|
|
||||||
URL string `json:",omitempty"`
|
|
||||||
}
|
|
|
@ -3,18 +3,17 @@ 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"
|
||||||
)
|
)
|
||||||
|
@ -30,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)
|
||||||
|
@ -47,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
|
||||||
|
@ -67,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()
|
||||||
|
@ -81,7 +80,7 @@ func RunPlugin(dockerCli *command.DockerCli, plugin *cobra.Command, meta metadat
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run is the top-level entry point to the CLI plugin framework. It should be called from your plugin's `main()` function.
|
// Run is the top-level entry point to the CLI plugin framework. It should be called from your plugin's `main()` function.
|
||||||
func Run(makeCmd func(command.Cli) *cobra.Command, meta metadata.Metadata) {
|
func Run(makeCmd func(command.Cli) *cobra.Command, meta manager.Metadata) {
|
||||||
otel.SetErrorHandler(debug.OTELErrorHandler)
|
otel.SetErrorHandler(debug.OTELErrorHandler)
|
||||||
|
|
||||||
dockerCli, err := command.NewDockerCli()
|
dockerCli, err := command.NewDockerCli()
|
||||||
|
@ -93,25 +92,26 @@ func Run(makeCmd func(command.Cli) *cobra.Command, meta metadata.Metadata) {
|
||||||
plugin := makeCmd(dockerCli)
|
plugin := makeCmd(dockerCli)
|
||||||
|
|
||||||
if err := RunPlugin(dockerCli, plugin, meta); err != nil {
|
if err := RunPlugin(dockerCli, plugin, meta); err != nil {
|
||||||
var stErr cli.StatusError
|
if sterr, ok := err.(cli.StatusError); ok {
|
||||||
if errors.As(err, &stErr) {
|
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
|
||||||
|
@ -133,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.NewClientWithOpts(client.WithDialContext(helper.Dialer))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return command.WithAPIClient(apiClient)(cli)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func newPluginCommand(dockerCli *command.DockerCli, plugin *cobra.Command, meta metadata.Metadata) *cli.TopLevelCommand {
|
return client.NewClientWithOpts(client.WithDialContext(helper.Dialer))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
@ -161,7 +158,7 @@ func newPluginCommand(dockerCli *command.DockerCli, plugin *cobra.Command, meta
|
||||||
CompletionOptions: cobra.CompletionOptions{
|
CompletionOptions: cobra.CompletionOptions{
|
||||||
DisableDefaultCmd: false,
|
DisableDefaultCmd: false,
|
||||||
HiddenDefaultCmd: true,
|
HiddenDefaultCmd: true,
|
||||||
DisableDescriptions: os.Getenv("DOCKER_CLI_DISABLE_COMPLETION_DESCRIPTION") != "",
|
DisableDescriptions: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
opts, _ := cli.SetupPluginRootCommand(cmd)
|
opts, _ := cli.SetupPluginRootCommand(cmd)
|
||||||
|
@ -180,12 +177,12 @@ func newPluginCommand(dockerCli *command.DockerCli, plugin *cobra.Command, meta
|
||||||
return cli.NewTopLevelCommand(cmd, dockerCli, opts, cmd.Flags())
|
return cli.NewTopLevelCommand(cmd, dockerCli, opts, cmd.Flags())
|
||||||
}
|
}
|
||||||
|
|
||||||
func newMetadataSubcommand(plugin *cobra.Command, meta metadata.Metadata) *cobra.Command {
|
func newMetadataSubcommand(plugin *cobra.Command, meta manager.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
|
||||||
|
@ -203,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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
32
cli/cobra.go
32
cli/cobra.go
|
@ -3,12 +3,15 @@ package cli
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"sort"
|
"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"
|
||||||
|
@ -59,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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -242,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 {
|
||||||
|
@ -327,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)
|
||||||
}
|
}
|
||||||
|
@ -346,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)
|
||||||
|
@ -407,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:
|
||||||
|
@ -510,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}}`
|
||||||
|
|
|
@ -3,7 +3,7 @@ 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"
|
||||||
|
@ -49,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)
|
||||||
|
@ -69,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)
|
||||||
|
@ -100,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,16 +3,16 @@ package builder
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/moby/moby/api/types/build"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/moby/moby/client"
|
"github.com/docker/docker/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
type fakeClient struct {
|
type fakeClient struct {
|
||||||
client.Client
|
client.Client
|
||||||
builderPruneFunc func(ctx context.Context, opts build.CachePruneOptions) (*build.CachePruneReport, error)
|
builderPruneFunc func(ctx context.Context, opts types.BuildCachePruneOptions) (*types.BuildCachePruneReport, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *fakeClient) BuildCachePrune(ctx context.Context, opts build.CachePruneOptions) (*build.CachePruneReport, 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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,22 +23,3 @@ func NewBuilderCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
)
|
)
|
||||||
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",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -9,10 +9,10 @@ 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/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/api/types/build"
|
"github.com/docker/docker/errdefs"
|
||||||
|
units "github.com/docker/go-units"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -69,18 +69,18 @@ func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions)
|
||||||
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"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
report, err := dockerCli.Client().BuildCachePrune(ctx, build.CachePruneOptions{
|
report, err := dockerCli.Client().BuildCachePrune(ctx, types.BuildCachePruneOptions{
|
||||||
All: options.all,
|
All: options.all,
|
||||||
KeepStorage: options.keepStorage.Value(), // FIXME(thaJeztah): rewrite to use new options; see https://github.com/moby/moby/pull/48720
|
KeepStorage: options.keepStorage.Value(),
|
||||||
Filters: pruneFilters,
|
Filters: pruneFilters,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -100,10 +100,6 @@ func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions)
|
||||||
return report.SpaceReclaimed, output, nil
|
return report.SpaceReclaimed, output, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type cancelledErr struct{ error }
|
|
||||||
|
|
||||||
func (cancelledErr) Cancelled() {}
|
|
||||||
|
|
||||||
// CachePrune executes a prune command for build cache
|
// 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 CachePrune(ctx context.Context, dockerCli command.Cli, all bool, filter opts.FilterOpt) (uint64, string, error) {
|
||||||
return runPrune(ctx, dockerCli, pruneOptions{force: true, all: all, filter: filter})
|
return runPrune(ctx, dockerCli, pruneOptions{force: true, all: all, filter: filter})
|
||||||
|
|
|
@ -7,7 +7,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/docker/cli/internal/test"
|
"github.com/docker/cli/internal/test"
|
||||||
"github.com/moby/moby/api/types/build"
|
"github.com/docker/docker/api/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestBuilderPromptTermination(t *testing.T) {
|
func TestBuilderPromptTermination(t *testing.T) {
|
||||||
|
@ -15,7 +15,7 @@ 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 build.CachePruneOptions) (*build.CachePruneReport, error) {
|
builderPruneFunc: func(ctx context.Context, opts types.BuildCachePruneOptions) (*types.BuildCachePruneReport, error) {
|
||||||
return nil, errors.New("fakeClient builderPruneFunc should not be called")
|
return nil, errors.New("fakeClient builderPruneFunc should not be called")
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -3,8 +3,8 @@ package checkpoint
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/moby/moby/api/types/checkpoint"
|
"github.com/docker/docker/api/types/checkpoint"
|
||||||
"github.com/moby/moby/client"
|
"github.com/docker/docker/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
type fakeClient struct {
|
type fakeClient struct {
|
||||||
|
|
|
@ -7,7 +7,7 @@ import (
|
||||||
"github.com/docker/cli/cli"
|
"github.com/docker/cli/cli"
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
"github.com/docker/cli/cli/command/completion"
|
"github.com/docker/cli/cli/command/completion"
|
||||||
"github.com/moby/moby/api/types/checkpoint"
|
"github.com/docker/docker/api/types/checkpoint"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -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, checkpoint.CreateOptions{
|
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/api/types/checkpoint"
|
"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"
|
||||||
)
|
)
|
||||||
|
@ -21,16 +20,16 @@ func TestCheckpointCreateErrors(t *testing.T) {
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
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 checkpoint.CreateOptions) error {
|
checkpointCreateFunc: func(container string, options checkpoint.CreateOptions) error {
|
||||||
return 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"
|
|
||||||
checkpointDir = "/dir/foo"
|
|
||||||
)
|
|
||||||
|
|
||||||
for _, tc := range []bool{true, false} {
|
|
||||||
leaveRunning := strconv.FormatBool(tc)
|
|
||||||
t.Run("leave-running="+leaveRunning, func(t *testing.T) {
|
|
||||||
var actualContainerName string
|
|
||||||
var actualOptions checkpoint.CreateOptions
|
|
||||||
cli := test.NewFakeCli(&fakeClient{
|
cli := test.NewFakeCli(&fakeClient{
|
||||||
checkpointCreateFunc: func(container string, options checkpoint.CreateOptions) error {
|
checkpointCreateFunc: func(container string, options checkpoint.CreateOptions) error {
|
||||||
actualContainerName = container
|
containerID = container
|
||||||
actualOptions = options
|
checkpointID = options.CheckpointID
|
||||||
|
checkpointDir = options.CheckpointDir
|
||||||
|
exit = options.Exit
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
cmd := newCreateCommand(cli)
|
cmd := newCreateCommand(cli)
|
||||||
cmd.SetOut(io.Discard)
|
cp := "checkpoint-bar"
|
||||||
cmd.SetErr(io.Discard)
|
cmd.SetArgs([]string{"container-foo", cp})
|
||||||
cmd.SetArgs([]string{containerName, checkpointName})
|
cmd.Flags().Set("leave-running", "true")
|
||||||
assert.Check(t, cmd.Flags().Set("leave-running", leaveRunning))
|
cmd.Flags().Set("checkpoint-dir", "/dir/foo")
|
||||||
assert.Check(t, cmd.Flags().Set("checkpoint-dir", checkpointDir))
|
|
||||||
assert.NilError(t, cmd.Execute())
|
assert.NilError(t, cmd.Execute())
|
||||||
assert.Check(t, is.Equal(actualContainerName, containerName))
|
assert.Check(t, is.Equal("container-foo", containerID))
|
||||||
expected := checkpoint.CreateOptions{
|
assert.Check(t, is.Equal(cp, checkpointID))
|
||||||
CheckpointID: checkpointName,
|
assert.Check(t, is.Equal("/dir/foo", checkpointDir))
|
||||||
CheckpointDir: checkpointDir,
|
assert.Check(t, is.Equal(false, exit))
|
||||||
Exit: !tc,
|
assert.Check(t, is.Equal(cp, strings.TrimSpace(cli.OutBuffer().String())))
|
||||||
}
|
|
||||||
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 (
|
||||||
|
|
|
@ -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"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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/api/types/checkpoint"
|
"github.com/docker/docker/api/types/checkpoint"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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/api/types/checkpoint"
|
"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"
|
||||||
"gotest.tools/v3/golden"
|
"gotest.tools/v3/golden"
|
||||||
|
@ -20,16 +20,16 @@ func TestCheckpointListErrors(t *testing.T) {
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
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 checkpoint.ListOptions) ([]checkpoint.Summary, error) {
|
checkpointListFunc: func(container string, options checkpoint.ListOptions) ([]checkpoint.Summary, error) {
|
||||||
return []checkpoint.Summary{}, 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",
|
||||||
},
|
},
|
||||||
|
|
|
@ -5,7 +5,7 @@ import (
|
||||||
|
|
||||||
"github.com/docker/cli/cli"
|
"github.com/docker/cli/cli"
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
"github.com/moby/moby/api/types/checkpoint"
|
"github.com/docker/docker/api/types/checkpoint"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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/api/types/checkpoint"
|
"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"
|
||||||
)
|
)
|
||||||
|
@ -19,16 +19,16 @@ func TestCheckpointRemoveErrors(t *testing.T) {
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
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 checkpoint.DeleteOptions) error {
|
checkpointDeleteFunc: func(container string, options checkpoint.DeleteOptions) error {
|
||||||
return errors.New("error deleting checkpoint")
|
return errors.Errorf("error deleting checkpoint")
|
||||||
},
|
},
|
||||||
expectedError: "error deleting checkpoint",
|
expectedError: "error deleting checkpoint",
|
||||||
},
|
},
|
||||||
|
|
|
@ -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.23
|
//go:build go1.21
|
||||||
|
|
||||||
package command
|
package command
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
|
@ -20,14 +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/api/types/swarm"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/moby/moby/client"
|
"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/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
|
||||||
|
@ -45,10 +53,13 @@ type Cli interface {
|
||||||
Streams
|
Streams
|
||||||
SetIn(in *streams.In)
|
SetIn(in *streams.In)
|
||||||
Apply(ops ...CLIOption) error
|
Apply(ops ...CLIOption) error
|
||||||
config.Provider
|
ConfigFile() *configfile.ConfigFile
|
||||||
ServerInfo() ServerInfo
|
ServerInfo() ServerInfo
|
||||||
|
NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (notaryclient.Repository, error)
|
||||||
DefaultVersion() string
|
DefaultVersion() string
|
||||||
CurrentVersion() string
|
CurrentVersion() string
|
||||||
|
ManifestStore() manifeststore.Store
|
||||||
|
RegistryClient(bool) registryclient.RegistryClient
|
||||||
ContentTrustEnabled() bool
|
ContentTrustEnabled() bool
|
||||||
BuildKitEnabled() (bool, error)
|
BuildKitEnabled() (bool, error)
|
||||||
ContextStore() store.Store
|
ContextStore() store.Store
|
||||||
|
@ -58,9 +69,7 @@ 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
|
||||||
|
@ -75,7 +84,7 @@ type DockerCli struct {
|
||||||
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
|
||||||
|
|
||||||
|
@ -87,9 +96,9 @@ type DockerCli struct {
|
||||||
enableGlobalMeter, enableGlobalTracer bool
|
enableGlobalMeter, enableGlobalTracer bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultVersion returns [client.DefaultAPIVersion].
|
// DefaultVersion returns api.defaultVersion.
|
||||||
func (*DockerCli) DefaultVersion() string {
|
func (cli *DockerCli) DefaultVersion() string {
|
||||||
return client.DefaultAPIVersion
|
return api.DefaultVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
// CurrentVersion returns the API version currently negotiated, or the default
|
// CurrentVersion returns the API version currently negotiated, or the default
|
||||||
|
@ -97,7 +106,7 @@ func (*DockerCli) DefaultVersion() string {
|
||||||
func (cli *DockerCli) CurrentVersion() string {
|
func (cli *DockerCli) CurrentVersion() string {
|
||||||
_ = cli.initialize()
|
_ = cli.initialize()
|
||||||
if cli.client == nil {
|
if cli.client == nil {
|
||||||
return client.DefaultAPIVersion
|
return api.DefaultVersion
|
||||||
}
|
}
|
||||||
return cli.client.ClientVersion()
|
return cli.client.ClientVersion()
|
||||||
}
|
}
|
||||||
|
@ -105,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
|
||||||
|
@ -179,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.
|
||||||
|
@ -193,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
|
||||||
|
@ -221,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 {
|
||||||
|
@ -239,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)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -279,7 +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()
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -287,7 +299,7 @@ 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()
|
||||||
|
@ -333,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
|
||||||
}
|
}
|
||||||
|
@ -394,6 +403,11 @@ func (cli *DockerCli) initializeFromClient() {
|
||||||
cli.client.NegotiateAPIVersionPing(ping)
|
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
|
||||||
func (cli *DockerCli) ContextStore() store.Store {
|
func (cli *DockerCli) ContextStore() store.Store {
|
||||||
return cli.contextStore
|
return cli.contextStore
|
||||||
|
@ -461,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
|
||||||
}
|
}
|
||||||
|
@ -509,7 +523,7 @@ func (cli *DockerCli) Apply(ops ...CLIOption) error {
|
||||||
type ServerInfo struct {
|
type ServerInfo struct {
|
||||||
HasExperimental bool
|
HasExperimental bool
|
||||||
OSType string
|
OSType string
|
||||||
BuildkitVersion build.BuilderVersion
|
BuildkitVersion types.BuilderVersion
|
||||||
|
|
||||||
// SwarmStatus provides information about the current swarm status of the
|
// 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.
|
||||||
|
@ -539,15 +553,18 @@ func NewDockerCli(ops ...CLIOption) (*DockerCli, error) {
|
||||||
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 user agent string used for making API requests
|
// UserAgent returns the user agent string used for making API requests
|
||||||
|
|
|
@ -10,7 +10,8 @@ import (
|
||||||
"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"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
@ -100,8 +101,7 @@ func WithContentTrust(enabled bool) CLIOption {
|
||||||
// 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -114,18 +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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
||||||
|
@ -189,10 +177,7 @@ func withCustomHeadersFromEnv() client.Opt {
|
||||||
csvReader := csv.NewReader(strings.NewReader(value))
|
csvReader := csv.NewReader(strings.NewReader(value))
|
||||||
fields, err := csvReader.Read()
|
fields, err := csvReader.Read()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return invalidParameter(errors.Errorf(
|
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))
|
||||||
"failed to parse custom headers from %s environment variable: value must be formatted as comma-separated key=value pairs",
|
|
||||||
envOverrideHTTPHeaders,
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
if len(fields) == 0 {
|
if len(fields) == 0 {
|
||||||
return nil
|
return nil
|
||||||
|
@ -206,10 +191,7 @@ func withCustomHeadersFromEnv() client.Opt {
|
||||||
k = strings.TrimSpace(k)
|
k = strings.TrimSpace(k)
|
||||||
|
|
||||||
if k == "" {
|
if k == "" {
|
||||||
return invalidParameter(errors.Errorf(
|
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))
|
||||||
`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.
|
// We don't currently allow empty key=value pairs, and produce an error.
|
||||||
|
@ -217,10 +199,7 @@ func withCustomHeadersFromEnv() client.Opt {
|
||||||
// from an environment variable with the same name). In the meantime,
|
// from an environment variable with the same name). In the meantime,
|
||||||
// produce an error to prevent users from depending on this.
|
// produce an error to prevent users from depending on this.
|
||||||
if !hasValue {
|
if !hasValue {
|
||||||
return invalidParameter(errors.Errorf(
|
return errdefs.InvalidParameter(errors.Errorf(`failed to set custom headers from %s environment variable: missing "=" in key=value pair: '%s'`, envOverrideHTTPHeaders, kv))
|
||||||
`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
|
||||||
|
|
|
@ -3,7 +3,6 @@ package command
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
|
@ -19,9 +18,13 @@ import (
|
||||||
"github.com/docker/cli/cli/config"
|
"github.com/docker/cli/cli/config"
|
||||||
"github.com/docker/cli/cli/config/configfile"
|
"github.com/docker/cli/cli/config/configfile"
|
||||||
"github.com/docker/cli/cli/flags"
|
"github.com/docker/cli/cli/flags"
|
||||||
"github.com/moby/moby/api/types"
|
"github.com/docker/cli/cli/streams"
|
||||||
"github.com/moby/moby/client"
|
"github.com/docker/docker/api"
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/client"
|
||||||
|
"github.com/pkg/errors"
|
||||||
"gotest.tools/v3/assert"
|
"gotest.tools/v3/assert"
|
||||||
|
"gotest.tools/v3/fs"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNewAPIClientFromFlags(t *testing.T) {
|
func TestNewAPIClientFromFlags(t *testing.T) {
|
||||||
|
@ -33,7 +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.DefaultAPIVersion)
|
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.DefaultAPIVersion)
|
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.DefaultAPIVersion)
|
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"})
|
||||||
|
@ -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.DefaultAPIVersion)
|
assert.Equal(t, apiClient.ClientVersion(), api.DefaultVersion)
|
||||||
|
|
||||||
expectedHeaders := http.Header{
|
expectedHeaders := http.Header{
|
||||||
"One": []string{"one-value"},
|
"One": []string{"one-value"},
|
||||||
|
@ -120,8 +123,7 @@ func TestNewAPIClientFromFlagsWithCustomHeadersFromEnv(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNewAPIClientFromFlagsWithAPIVersionFromEnv(t *testing.T) {
|
func TestNewAPIClientFromFlagsWithAPIVersionFromEnv(t *testing.T) {
|
||||||
const customVersion = "v3.3.3"
|
customVersion := "v3.3.3"
|
||||||
const expectedVersion = "3.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,7 +131,7 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
type fakeClient struct {
|
type fakeClient struct {
|
||||||
|
@ -185,18 +187,19 @@ func TestInitializeFromClient(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range testcases {
|
for _, testcase := range testcases {
|
||||||
t.Run(tc.doc, func(t *testing.T) {
|
testcase := testcase
|
||||||
|
t.Run(testcase.doc, func(t *testing.T) {
|
||||||
apiclient := &fakeClient{
|
apiclient := &fakeClient{
|
||||||
pingFunc: tc.pingFunc,
|
pingFunc: testcase.pingFunc,
|
||||||
version: defaultVersion,
|
version: defaultVersion,
|
||||||
}
|
}
|
||||||
|
|
||||||
cli := &DockerCli{client: apiclient}
|
cli := &DockerCli{client: apiclient}
|
||||||
err := cli.Initialize(flags.NewClientOptions())
|
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)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -204,8 +207,8 @@ 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) {
|
||||||
tmpDir := t.TempDir()
|
dir := t.TempDir()
|
||||||
socket := filepath.Join(tmpDir, "my.sock")
|
socket := filepath.Join(dir, "my.sock")
|
||||||
l, err := net.Listen("unix", socket)
|
l, err := net.Listen("unix", socket)
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
@ -253,40 +256,80 @@ func TestInitializeFromClientHangs(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
// Test default operations and also overriding default ones
|
// Test default operations and also overriding default ones
|
||||||
cli, err := NewDockerCli(WithInputStream(io.NopCloser(strings.NewReader("some input"))))
|
cli, err := NewDockerCli(
|
||||||
|
WithContentTrust(true),
|
||||||
|
)
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
// Check streams are initialized
|
// Check streams are initialized
|
||||||
assert.Check(t, cli.In() != nil)
|
assert.Check(t, cli.In() != nil)
|
||||||
assert.Check(t, cli.Out() != nil)
|
assert.Check(t, cli.Out() != nil)
|
||||||
assert.Check(t, cli.Err() != nil)
|
assert.Check(t, cli.Err() != nil)
|
||||||
inputStream, err := io.ReadAll(cli.In())
|
assert.Equal(t, cli.ContentTrustEnabled(), true)
|
||||||
assert.NilError(t, err)
|
|
||||||
assert.Equal(t, string(inputStream), "some input")
|
|
||||||
|
|
||||||
// Apply can modify a dockerCli after construction
|
// Apply can modify a dockerCli after construction
|
||||||
|
inbuf := bytes.NewBuffer([]byte("input"))
|
||||||
outbuf := bytes.NewBuffer(nil)
|
outbuf := bytes.NewBuffer(nil)
|
||||||
errbuf := bytes.NewBuffer(nil)
|
errbuf := bytes.NewBuffer(nil)
|
||||||
err = cli.Apply(
|
err = cli.Apply(
|
||||||
WithInputStream(io.NopCloser(strings.NewReader("input"))),
|
WithInputStream(io.NopCloser(inbuf)),
|
||||||
WithOutputStream(outbuf),
|
WithOutputStream(outbuf),
|
||||||
WithErrorStream(errbuf),
|
WithErrorStream(errbuf),
|
||||||
)
|
)
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
// Check input stream
|
// 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), "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")
|
||||||
|
@ -303,8 +346,6 @@ func TestInitializeShouldAlwaysCreateTheContextStore(t *testing.T) {
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
|
@ -316,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())
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -330,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())
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -344,11 +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())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,7 +43,6 @@ func AddCommands(cmd *cobra.Command, dockerCli command.Cli) {
|
||||||
system.NewInfoCommand(dockerCli),
|
system.NewInfoCommand(dockerCli),
|
||||||
|
|
||||||
// management commands
|
// management commands
|
||||||
builder.NewBakeStubCommand(dockerCli),
|
|
||||||
builder.NewBuilderCommand(dockerCli),
|
builder.NewBuilderCommand(dockerCli),
|
||||||
checkpoint.NewCheckpointCommand(dockerCli),
|
checkpoint.NewCheckpointCommand(dockerCli),
|
||||||
container.NewContainerCommand(dockerCli),
|
container.NewContainerCommand(dockerCli),
|
||||||
|
|
|
@ -5,18 +5,17 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/docker/cli/cli/command/formatter"
|
"github.com/docker/cli/cli/command/formatter"
|
||||||
"github.com/moby/moby/api/types/container"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/moby/moby/api/types/image"
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/moby/moby/api/types/network"
|
"github.com/docker/docker/api/types/image"
|
||||||
"github.com/moby/moby/api/types/volume"
|
"github.com/docker/docker/api/types/network"
|
||||||
"github.com/moby/moby/client"
|
"github.com/docker/docker/api/types/volume"
|
||||||
|
"github.com/docker/docker/client"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ValidArgsFn a function to be used by cobra command as `ValidArgsFunction` to offer command line completion.
|
// 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)
|
||||||
// Deprecated: use [cobra.CompletionFunc].
|
|
||||||
type ValidArgsFn = cobra.CompletionFunc
|
|
||||||
|
|
||||||
// APIClientProvider provides a method to get an [client.APIClient], initializing
|
// APIClientProvider provides a method to get an [client.APIClient], initializing
|
||||||
// it if needed.
|
// it if needed.
|
||||||
|
@ -29,11 +28,8 @@ 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 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 {
|
|
||||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
|
||||||
}
|
|
||||||
list, err := dockerCLI.Client().ImageList(cmd.Context(), image.ListOptions{})
|
list, err := dockerCLI.Client().ImageList(cmd.Context(), image.ListOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, cobra.ShellCompDirectiveError
|
return nil, cobra.ShellCompDirectiveError
|
||||||
|
@ -49,7 +45,7 @@ func ImageNames(dockerCLI APIClientProvider, limit int) cobra.CompletionFunc {
|
||||||
// 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 func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
list, err := dockerCLI.Client().ContainerList(cmd.Context(), container.ListOptions{
|
list, err := dockerCLI.Client().ContainerList(cmd.Context(), container.ListOptions{
|
||||||
All: all,
|
All: all,
|
||||||
|
@ -64,7 +60,7 @@ func ContainerNames(dockerCLI APIClientProvider, all bool, filters ...func(conta
|
||||||
for _, ctr := range list {
|
for _, ctr := range list {
|
||||||
skip := false
|
skip := false
|
||||||
for _, fn := range filters {
|
for _, fn := range filters {
|
||||||
if fn != nil && !fn(ctr) {
|
if !fn(ctr) {
|
||||||
skip = true
|
skip = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -82,7 +78,7 @@ func ContainerNames(dockerCLI APIClientProvider, all bool, filters ...func(conta
|
||||||
}
|
}
|
||||||
|
|
||||||
// VolumeNames offers completion for volumes
|
// VolumeNames offers completion for volumes
|
||||||
func VolumeNames(dockerCLI APIClientProvider) cobra.CompletionFunc {
|
func VolumeNames(dockerCLI APIClientProvider) 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) {
|
||||||
list, err := dockerCLI.Client().VolumeList(cmd.Context(), volume.ListOptions{})
|
list, err := dockerCLI.Client().VolumeList(cmd.Context(), volume.ListOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -97,7 +93,7 @@ func VolumeNames(dockerCLI APIClientProvider) cobra.CompletionFunc {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NetworkNames offers completion for networks
|
// NetworkNames offers completion for networks
|
||||||
func NetworkNames(dockerCLI APIClientProvider) cobra.CompletionFunc {
|
func NetworkNames(dockerCLI APIClientProvider) 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) {
|
||||||
list, err := dockerCLI.Client().NetworkList(cmd.Context(), network.ListOptions{})
|
list, err := dockerCLI.Client().NetworkList(cmd.Context(), network.ListOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -135,7 +131,7 @@ func EnvVarNames(_ *cobra.Command, _ []string, _ string) (names []string, _ cobr
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 cobra.FixedCompletions(options, cobra.ShellCompDirectiveNoFileComp)
|
return cobra.FixedCompletions(options, cobra.ShellCompDirectiveNoFileComp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -150,44 +146,3 @@ func FileNames(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCom
|
||||||
func NoComplete(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
|
func NoComplete(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
|
||||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
}
|
}
|
||||||
|
|
||||||
var commonPlatforms = []string{
|
|
||||||
"linux/386",
|
|
||||||
"linux/amd64",
|
|
||||||
"linux/arm",
|
|
||||||
"linux/arm/v5",
|
|
||||||
"linux/arm/v6",
|
|
||||||
"linux/arm/v7",
|
|
||||||
"linux/arm64",
|
|
||||||
"linux/arm64/v8",
|
|
||||||
|
|
||||||
// IBM power and z platforms
|
|
||||||
"linux/ppc64le",
|
|
||||||
"linux/s390x",
|
|
||||||
|
|
||||||
// Not yet supported
|
|
||||||
"linux/riscv64",
|
|
||||||
|
|
||||||
"windows/amd64",
|
|
||||||
|
|
||||||
"wasip1/wasm",
|
|
||||||
}
|
|
||||||
|
|
||||||
// Platforms offers completion for platform-strings. It provides a non-exhaustive
|
|
||||||
// list of platforms to be used for completion. Platform-strings are based on
|
|
||||||
// [runtime.GOOS] and [runtime.GOARCH], but with (optional) variants added. A
|
|
||||||
// list of recognised os/arch combinations from the Go runtime can be obtained
|
|
||||||
// through "go tool dist list".
|
|
||||||
//
|
|
||||||
// Some noteworthy exclusions from this list:
|
|
||||||
//
|
|
||||||
// - arm64 images ("windows/arm64", "windows/arm64/v8") do not yet exist for windows.
|
|
||||||
// - we don't (yet) include `os-variant` for completion (as can be used for Windows images)
|
|
||||||
// - we don't (yet) include platforms for which we don't build binaries, such as
|
|
||||||
// BSD platforms (freebsd, netbsd, openbsd), android, macOS (darwin).
|
|
||||||
// - we currently exclude architectures that may have unofficial builds,
|
|
||||||
// but don't have wide adoption (and no support), such as loong64, mipsXXX,
|
|
||||||
// ppc64 (non-le) to prevent confusion.
|
|
||||||
func Platforms(_ *cobra.Command, _ []string, _ string) (platforms []string, _ cobra.ShellCompDirective) {
|
|
||||||
return commonPlatforms, cobra.ShellCompDirectiveNoFileComp
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,346 +0,0 @@
|
||||||
package completion
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"sort"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp/cmpopts"
|
|
||||||
"github.com/moby/moby/api/types/container"
|
|
||||||
"github.com/moby/moby/api/types/filters"
|
|
||||||
"github.com/moby/moby/api/types/image"
|
|
||||||
"github.com/moby/moby/api/types/network"
|
|
||||||
"github.com/moby/moby/api/types/volume"
|
|
||||||
"github.com/moby/moby/client"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"gotest.tools/v3/assert"
|
|
||||||
is "gotest.tools/v3/assert/cmp"
|
|
||||||
"gotest.tools/v3/env"
|
|
||||||
)
|
|
||||||
|
|
||||||
type fakeCLI struct {
|
|
||||||
*fakeClient
|
|
||||||
}
|
|
||||||
|
|
||||||
// Client implements [APIClientProvider].
|
|
||||||
func (c fakeCLI) Client() client.APIClient {
|
|
||||||
return c.fakeClient
|
|
||||||
}
|
|
||||||
|
|
||||||
type fakeClient struct {
|
|
||||||
client.Client
|
|
||||||
containerListFunc func(options container.ListOptions) ([]container.Summary, error)
|
|
||||||
imageListFunc func(options image.ListOptions) ([]image.Summary, error)
|
|
||||||
networkListFunc func(ctx context.Context, options network.ListOptions) ([]network.Summary, error)
|
|
||||||
volumeListFunc func(filter filters.Args) (volume.ListResponse, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *fakeClient) ContainerList(_ context.Context, options container.ListOptions) ([]container.Summary, error) {
|
|
||||||
if c.containerListFunc != nil {
|
|
||||||
return c.containerListFunc(options)
|
|
||||||
}
|
|
||||||
return []container.Summary{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *fakeClient) ImageList(_ context.Context, options image.ListOptions) ([]image.Summary, error) {
|
|
||||||
if c.imageListFunc != nil {
|
|
||||||
return c.imageListFunc(options)
|
|
||||||
}
|
|
||||||
return []image.Summary{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *fakeClient) NetworkList(ctx context.Context, options network.ListOptions) ([]network.Summary, error) {
|
|
||||||
if c.networkListFunc != nil {
|
|
||||||
return c.networkListFunc(ctx, options)
|
|
||||||
}
|
|
||||||
return []network.Inspect{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *fakeClient) VolumeList(_ context.Context, options volume.ListOptions) (volume.ListResponse, error) {
|
|
||||||
if c.volumeListFunc != nil {
|
|
||||||
return c.volumeListFunc(options.Filters)
|
|
||||||
}
|
|
||||||
return volume.ListResponse{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCompleteContainerNames(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
doc string
|
|
||||||
showAll, showIDs bool
|
|
||||||
filters []func(container.Summary) bool
|
|
||||||
containers []container.Summary
|
|
||||||
expOut []string
|
|
||||||
expOpts container.ListOptions
|
|
||||||
expDirective cobra.ShellCompDirective
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
doc: "no results",
|
|
||||||
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
doc: "all containers",
|
|
||||||
showAll: true,
|
|
||||||
containers: []container.Summary{
|
|
||||||
{ID: "id-c", State: container.StateRunning, Names: []string{"/container-c", "/container-c/link-b"}},
|
|
||||||
{ID: "id-b", State: container.StateCreated, Names: []string{"/container-b"}},
|
|
||||||
{ID: "id-a", State: container.StateExited, Names: []string{"/container-a"}},
|
|
||||||
},
|
|
||||||
expOut: []string{"container-c", "container-c/link-b", "container-b", "container-a"},
|
|
||||||
expOpts: container.ListOptions{All: true},
|
|
||||||
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
doc: "all containers with ids",
|
|
||||||
showAll: true,
|
|
||||||
showIDs: true,
|
|
||||||
containers: []container.Summary{
|
|
||||||
{ID: "id-c", State: container.StateRunning, Names: []string{"/container-c", "/container-c/link-b"}},
|
|
||||||
{ID: "id-b", State: container.StateCreated, Names: []string{"/container-b"}},
|
|
||||||
{ID: "id-a", State: container.StateExited, Names: []string{"/container-a"}},
|
|
||||||
},
|
|
||||||
expOut: []string{"id-c", "container-c", "container-c/link-b", "id-b", "container-b", "id-a", "container-a"},
|
|
||||||
expOpts: container.ListOptions{All: true},
|
|
||||||
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
doc: "only running containers",
|
|
||||||
showAll: false,
|
|
||||||
containers: []container.Summary{
|
|
||||||
{ID: "id-c", State: container.StateRunning, Names: []string{"/container-c", "/container-c/link-b"}},
|
|
||||||
},
|
|
||||||
expOut: []string{"container-c", "container-c/link-b"},
|
|
||||||
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
doc: "with filter",
|
|
||||||
showAll: true,
|
|
||||||
filters: []func(container.Summary) bool{
|
|
||||||
func(ctr container.Summary) bool { return ctr.State == container.StateCreated },
|
|
||||||
},
|
|
||||||
containers: []container.Summary{
|
|
||||||
{ID: "id-c", State: container.StateRunning, Names: []string{"/container-c", "/container-c/link-b"}},
|
|
||||||
{ID: "id-b", State: container.StateCreated, Names: []string{"/container-b"}},
|
|
||||||
{ID: "id-a", State: container.StateExited, Names: []string{"/container-a"}},
|
|
||||||
},
|
|
||||||
expOut: []string{"container-b"},
|
|
||||||
expOpts: container.ListOptions{All: true},
|
|
||||||
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
doc: "multiple filters",
|
|
||||||
showAll: true,
|
|
||||||
filters: []func(container.Summary) bool{
|
|
||||||
func(ctr container.Summary) bool { return ctr.ID == "id-a" },
|
|
||||||
func(ctr container.Summary) bool { return ctr.State == container.StateCreated },
|
|
||||||
},
|
|
||||||
containers: []container.Summary{
|
|
||||||
{ID: "id-c", State: container.StateRunning, Names: []string{"/container-c", "/container-c/link-b"}},
|
|
||||||
{ID: "id-b", State: container.StateCreated, Names: []string{"/container-b"}},
|
|
||||||
{ID: "id-a", State: container.StateCreated, Names: []string{"/container-a"}},
|
|
||||||
},
|
|
||||||
expOut: []string{"container-a"},
|
|
||||||
expOpts: container.ListOptions{All: true},
|
|
||||||
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
doc: "with error",
|
|
||||||
expDirective: cobra.ShellCompDirectiveError,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range tests {
|
|
||||||
t.Run(tc.doc, func(t *testing.T) {
|
|
||||||
if tc.showIDs {
|
|
||||||
t.Setenv("DOCKER_COMPLETION_SHOW_CONTAINER_IDS", "yes")
|
|
||||||
}
|
|
||||||
comp := ContainerNames(fakeCLI{&fakeClient{
|
|
||||||
containerListFunc: func(opts container.ListOptions) ([]container.Summary, error) {
|
|
||||||
assert.Check(t, is.DeepEqual(opts, tc.expOpts, cmpopts.IgnoreUnexported(container.ListOptions{}, filters.Args{})))
|
|
||||||
if tc.expDirective == cobra.ShellCompDirectiveError {
|
|
||||||
return nil, errors.New("some error occurred")
|
|
||||||
}
|
|
||||||
return tc.containers, nil
|
|
||||||
},
|
|
||||||
}}, tc.showAll, tc.filters...)
|
|
||||||
|
|
||||||
containers, directives := comp(&cobra.Command{}, nil, "")
|
|
||||||
assert.Check(t, is.Equal(directives&tc.expDirective, tc.expDirective))
|
|
||||||
assert.Check(t, is.DeepEqual(containers, tc.expOut))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCompleteEnvVarNames(t *testing.T) {
|
|
||||||
env.PatchAll(t, map[string]string{
|
|
||||||
"ENV_A": "hello-a",
|
|
||||||
"ENV_B": "hello-b",
|
|
||||||
})
|
|
||||||
values, directives := EnvVarNames(nil, nil, "")
|
|
||||||
assert.Check(t, is.Equal(directives&cobra.ShellCompDirectiveNoFileComp, cobra.ShellCompDirectiveNoFileComp), "Should not perform file completion")
|
|
||||||
|
|
||||||
sort.Strings(values)
|
|
||||||
expected := []string{"ENV_A", "ENV_B"}
|
|
||||||
assert.Check(t, is.DeepEqual(values, expected))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCompleteFileNames(t *testing.T) {
|
|
||||||
values, directives := FileNames(nil, nil, "")
|
|
||||||
assert.Check(t, is.Equal(directives, cobra.ShellCompDirectiveDefault))
|
|
||||||
assert.Check(t, is.Len(values, 0))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCompleteFromList(t *testing.T) {
|
|
||||||
expected := []string{"one", "two", "three"}
|
|
||||||
|
|
||||||
values, directives := FromList(expected...)(nil, nil, "")
|
|
||||||
assert.Check(t, is.Equal(directives&cobra.ShellCompDirectiveNoFileComp, cobra.ShellCompDirectiveNoFileComp), "Should not perform file completion")
|
|
||||||
assert.Check(t, is.DeepEqual(values, expected))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCompleteImageNames(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
doc string
|
|
||||||
images []image.Summary
|
|
||||||
expOut []string
|
|
||||||
expDirective cobra.ShellCompDirective
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
doc: "no results",
|
|
||||||
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
doc: "with results",
|
|
||||||
images: []image.Summary{
|
|
||||||
{RepoTags: []string{"image-c:latest", "image-c:other"}},
|
|
||||||
{RepoTags: []string{"image-b:latest", "image-b:other"}},
|
|
||||||
{RepoTags: []string{"image-a:latest", "image-a:other"}},
|
|
||||||
},
|
|
||||||
expOut: []string{"image-c:latest", "image-c:other", "image-b:latest", "image-b:other", "image-a:latest", "image-a:other"},
|
|
||||||
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
doc: "with error",
|
|
||||||
expDirective: cobra.ShellCompDirectiveError,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range tests {
|
|
||||||
t.Run(tc.doc, func(t *testing.T) {
|
|
||||||
comp := ImageNames(fakeCLI{&fakeClient{
|
|
||||||
imageListFunc: func(options image.ListOptions) ([]image.Summary, error) {
|
|
||||||
if tc.expDirective == cobra.ShellCompDirectiveError {
|
|
||||||
return nil, errors.New("some error occurred")
|
|
||||||
}
|
|
||||||
return tc.images, nil
|
|
||||||
},
|
|
||||||
}}, -1)
|
|
||||||
|
|
||||||
volumes, directives := comp(&cobra.Command{}, nil, "")
|
|
||||||
assert.Check(t, is.Equal(directives&tc.expDirective, tc.expDirective))
|
|
||||||
assert.Check(t, is.DeepEqual(volumes, tc.expOut))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCompleteNetworkNames(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
doc string
|
|
||||||
networks []network.Summary
|
|
||||||
expOut []string
|
|
||||||
expDirective cobra.ShellCompDirective
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
doc: "no results",
|
|
||||||
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
doc: "with results",
|
|
||||||
networks: []network.Summary{
|
|
||||||
{ID: "nw-c", Name: "network-c"},
|
|
||||||
{ID: "nw-b", Name: "network-b"},
|
|
||||||
{ID: "nw-a", Name: "network-a"},
|
|
||||||
},
|
|
||||||
expOut: []string{"network-c", "network-b", "network-a"},
|
|
||||||
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
doc: "with error",
|
|
||||||
expDirective: cobra.ShellCompDirectiveError,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range tests {
|
|
||||||
t.Run(tc.doc, func(t *testing.T) {
|
|
||||||
comp := NetworkNames(fakeCLI{&fakeClient{
|
|
||||||
networkListFunc: func(ctx context.Context, options network.ListOptions) ([]network.Summary, error) {
|
|
||||||
if tc.expDirective == cobra.ShellCompDirectiveError {
|
|
||||||
return nil, errors.New("some error occurred")
|
|
||||||
}
|
|
||||||
return tc.networks, nil
|
|
||||||
},
|
|
||||||
}})
|
|
||||||
|
|
||||||
volumes, directives := comp(&cobra.Command{}, nil, "")
|
|
||||||
assert.Check(t, is.Equal(directives&tc.expDirective, tc.expDirective))
|
|
||||||
assert.Check(t, is.DeepEqual(volumes, tc.expOut))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCompleteNoComplete(t *testing.T) {
|
|
||||||
values, directives := NoComplete(nil, nil, "")
|
|
||||||
assert.Check(t, is.Equal(directives, cobra.ShellCompDirectiveNoFileComp))
|
|
||||||
assert.Check(t, is.Len(values, 0))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCompletePlatforms(t *testing.T) {
|
|
||||||
values, directives := Platforms(nil, nil, "")
|
|
||||||
assert.Check(t, is.Equal(directives&cobra.ShellCompDirectiveNoFileComp, cobra.ShellCompDirectiveNoFileComp), "Should not perform file completion")
|
|
||||||
assert.Check(t, is.DeepEqual(values, commonPlatforms))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCompleteVolumeNames(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
doc string
|
|
||||||
volumes []*volume.Volume
|
|
||||||
expOut []string
|
|
||||||
expDirective cobra.ShellCompDirective
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
doc: "no results",
|
|
||||||
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
doc: "with results",
|
|
||||||
volumes: []*volume.Volume{
|
|
||||||
{Name: "volume-c"},
|
|
||||||
{Name: "volume-b"},
|
|
||||||
{Name: "volume-a"},
|
|
||||||
},
|
|
||||||
expOut: []string{"volume-c", "volume-b", "volume-a"},
|
|
||||||
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
doc: "with error",
|
|
||||||
expDirective: cobra.ShellCompDirectiveError,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range tests {
|
|
||||||
t.Run(tc.doc, func(t *testing.T) {
|
|
||||||
comp := VolumeNames(fakeCLI{&fakeClient{
|
|
||||||
volumeListFunc: func(filter filters.Args) (volume.ListResponse, error) {
|
|
||||||
if tc.expDirective == cobra.ShellCompDirectiveError {
|
|
||||||
return volume.ListResponse{}, errors.New("some error occurred")
|
|
||||||
}
|
|
||||||
return volume.ListResponse{Volumes: tc.volumes}, nil
|
|
||||||
},
|
|
||||||
}})
|
|
||||||
|
|
||||||
volumes, directives := comp(&cobra.Command{}, nil, "")
|
|
||||||
assert.Check(t, is.Equal(directives&tc.expDirective, tc.expDirective))
|
|
||||||
assert.Check(t, is.DeepEqual(volumes, tc.expOut))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -3,23 +3,24 @@ package config
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"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/docker/docker/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
type fakeClient struct {
|
type fakeClient struct {
|
||||||
client.Client
|
client.Client
|
||||||
configCreateFunc func(context.Context, swarm.ConfigSpec) (swarm.ConfigCreateResponse, error)
|
configCreateFunc func(context.Context, swarm.ConfigSpec) (types.ConfigCreateResponse, error)
|
||||||
configInspectFunc func(context.Context, string) (swarm.Config, []byte, error)
|
configInspectFunc func(context.Context, string) (swarm.Config, []byte, error)
|
||||||
configListFunc func(context.Context, swarm.ConfigListOptions) ([]swarm.Config, error)
|
configListFunc func(context.Context, types.ConfigListOptions) ([]swarm.Config, error)
|
||||||
configRemoveFunc func(string) error
|
configRemoveFunc func(string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *fakeClient) ConfigCreate(ctx context.Context, spec swarm.ConfigSpec) (swarm.ConfigCreateResponse, 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, spec)
|
return c.configCreateFunc(ctx, spec)
|
||||||
}
|
}
|
||||||
return swarm.ConfigCreateResponse{}, nil
|
return types.ConfigCreateResponse{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *fakeClient) ConfigInspectWithRaw(ctx context.Context, id string) (swarm.Config, []byte, error) {
|
func (c *fakeClient) ConfigInspectWithRaw(ctx context.Context, id string) (swarm.Config, []byte, error) {
|
||||||
|
@ -29,7 +30,7 @@ func (c *fakeClient) ConfigInspectWithRaw(ctx context.Context, id string) (swarm
|
||||||
return swarm.Config{}, nil, nil
|
return swarm.Config{}, nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *fakeClient) ConfigList(ctx context.Context, options swarm.ConfigListOptions) ([]swarm.Config, 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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ import (
|
||||||
"github.com/docker/cli/cli"
|
"github.com/docker/cli/cli"
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
"github.com/docker/cli/cli/command/completion"
|
"github.com/docker/cli/cli/command/completion"
|
||||||
"github.com/moby/moby/api/types/swarm"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -30,9 +30,9 @@ func NewConfigCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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) {
|
||||||
list, err := dockerCLI.Client().ConfigList(cmd.Context(), swarm.ConfigListOptions{})
|
list, err := dockerCLI.Client().ConfigList(cmd.Context(), types.ConfigListOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, cobra.ShellCompDirectiveError
|
return nil, cobra.ShellCompDirectiveError
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,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/opts"
|
"github.com/docker/cli/opts"
|
||||||
"github.com/moby/moby/api/types/swarm"
|
"github.com/docker/docker/api/types/swarm"
|
||||||
"github.com/moby/sys/sequential"
|
"github.com/moby/sys/sequential"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
@ -48,10 +48,20 @@ func newConfigCreateCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
// RunConfigCreate creates a config with the given options.
|
// RunConfigCreate creates a config with the given options.
|
||||||
func RunConfigCreate(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 errors.Errorf("Error reading content from %q: %v", options.File, err)
|
return errors.Errorf("Error reading content from %q: %v", options.File, err)
|
||||||
}
|
}
|
||||||
|
@ -59,7 +69,7 @@ func RunConfigCreate(ctx context.Context, dockerCLI command.Cli, options CreateO
|
||||||
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,
|
||||||
}
|
}
|
||||||
|
@ -68,59 +78,11 @@ func RunConfigCreate(ctx context.Context, dockerCLI command.Cli, options CreateO
|
||||||
Name: options.TemplateDriver,
|
Name: options.TemplateDriver,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
r, err := apiClient.ConfigCreate(ctx, spec)
|
r, err := client.ConfigCreate(ctx, 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,7 +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/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"
|
||||||
|
@ -23,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, swarm.ConfigSpec) (swarm.ConfigCreateResponse, 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, configSpec swarm.ConfigSpec) (swarm.ConfigCreateResponse, error) {
|
configCreateFunc: func(_ context.Context, configSpec swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
|
||||||
return swarm.ConfigCreateResponse{}, 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{
|
||||||
|
@ -58,17 +59,17 @@ 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, spec swarm.ConfigSpec) (swarm.ConfigCreateResponse, error) {
|
configCreateFunc: func(_ context.Context, spec swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
|
||||||
if spec.Name != name {
|
if spec.Name != name {
|
||||||
return swarm.ConfigCreateResponse{}, fmt.Errorf("expected name %q, got %q", name, spec.Name)
|
return types.ConfigCreateResponse{}, errors.Errorf("expected name %q, got %q", name, spec.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
actual = spec.Data
|
actual = spec.Data
|
||||||
|
|
||||||
return swarm.ConfigCreateResponse{
|
return types.ConfigCreateResponse{
|
||||||
ID: "ID-" + spec.Name,
|
ID: "ID-" + spec.Name,
|
||||||
}, nil
|
}, nil
|
||||||
},
|
},
|
||||||
|
@ -86,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)
|
||||||
|
@ -100,12 +101,12 @@ func TestConfigCreateWithLabels(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
cli := test.NewFakeCli(&fakeClient{
|
cli := test.NewFakeCli(&fakeClient{
|
||||||
configCreateFunc: func(_ context.Context, spec swarm.ConfigSpec) (swarm.ConfigCreateResponse, error) {
|
configCreateFunc: func(_ context.Context, spec swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
|
||||||
if !reflect.DeepEqual(spec, expected) {
|
if !reflect.DeepEqual(spec, expected) {
|
||||||
return swarm.ConfigCreateResponse{}, fmt.Errorf("expected %+v, got %+v", expected, spec)
|
return types.ConfigCreateResponse{}, errors.Errorf("expected %+v, got %+v", expected, spec)
|
||||||
}
|
}
|
||||||
|
|
||||||
return swarm.ConfigCreateResponse{
|
return types.ConfigCreateResponse{
|
||||||
ID: "ID-" + spec.Name,
|
ID: "ID-" + spec.Name,
|
||||||
}, nil
|
}, nil
|
||||||
},
|
},
|
||||||
|
@ -123,19 +124,19 @@ 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, spec swarm.ConfigSpec) (swarm.ConfigCreateResponse, error) {
|
configCreateFunc: func(_ context.Context, spec swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
|
||||||
if spec.Name != name {
|
if spec.Name != name {
|
||||||
return swarm.ConfigCreateResponse{}, fmt.Errorf("expected name %q, got %q", name, spec.Name)
|
return types.ConfigCreateResponse{}, errors.Errorf("expected name %q, got %q", name, spec.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
if spec.Templating.Name != expectedDriver.Name {
|
if spec.Templating.Name != expectedDriver.Name {
|
||||||
return swarm.ConfigCreateResponse{}, fmt.Errorf("expected driver %v, got %v", expectedDriver, spec.Labels)
|
return types.ConfigCreateResponse{}, errors.Errorf("expected driver %v, got %v", expectedDriver, spec.Labels)
|
||||||
}
|
}
|
||||||
|
|
||||||
return swarm.ConfigCreateResponse{
|
return types.ConfigCreateResponse{
|
||||||
ID: "ID-" + spec.Name,
|
ID: "ID-" + spec.Name,
|
||||||
}, nil
|
}, nil
|
||||||
},
|
},
|
||||||
|
|
|
@ -5,10 +5,11 @@ import (
|
||||||
"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"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -156,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,7 +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"
|
||||||
"gotest.tools/v3/assert"
|
"gotest.tools/v3/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -61,6 +61,7 @@ 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
|
||||||
|
|
|
@ -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.23
|
//go:build go1.21
|
||||||
|
|
||||||
package config
|
package config
|
||||||
|
|
||||||
|
@ -43,15 +43,15 @@ func newConfigInspectCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
// RunConfigInspect inspects the given Swarm config.
|
// RunConfigInspect inspects the given Swarm config.
|
||||||
func RunConfigInspect(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) {
|
||||||
return apiClient.ConfigInspectWithRaw(ctx, id)
|
return client.ConfigInspectWithRaw(ctx, id)
|
||||||
}
|
}
|
||||||
f := opts.Format
|
f := opts.Format
|
||||||
|
|
||||||
|
@ -62,7 +62,7 @@ func RunConfigInspect(ctx context.Context, dockerCLI command.Cli, opts InspectOp
|
||||||
}
|
}
|
||||||
|
|
||||||
configCtx := formatter.Context{
|
configCtx := formatter.Context{
|
||||||
Output: dockerCLI.Out(),
|
Output: dockerCli.Out(),
|
||||||
Format: NewFormat(f, false),
|
Format: NewFormat(f, false),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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/api/types/swarm"
|
"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"
|
||||||
)
|
)
|
||||||
|
@ -28,7 +28,7 @@ func TestConfigInspectErrors(t *testing.T) {
|
||||||
{
|
{
|
||||||
args: []string{"foo"},
|
args: []string{"foo"},
|
||||||
configInspectFunc: func(_ context.Context, configID string) (swarm.Config, []byte, error) {
|
configInspectFunc: func(_ context.Context, configID string) (swarm.Config, []byte, error) {
|
||||||
return swarm.Config{}, nil, errors.New("error while inspecting the config")
|
return swarm.Config{}, nil, errors.Errorf("error while inspecting the config")
|
||||||
},
|
},
|
||||||
expectedError: "error while inspecting the config",
|
expectedError: "error while inspecting the config",
|
||||||
},
|
},
|
||||||
|
@ -45,7 +45,7 @@ func TestConfigInspectErrors(t *testing.T) {
|
||||||
if configID == "foo" {
|
if configID == "foo" {
|
||||||
return *builders.Config(builders.ConfigName("foo")), nil, nil
|
return *builders.Config(builders.ConfigName("foo")), nil, nil
|
||||||
}
|
}
|
||||||
return swarm.Config{}, nil, errors.New("error while inspecting the config")
|
return swarm.Config{}, nil, errors.Errorf("error while inspecting the config")
|
||||||
},
|
},
|
||||||
expectedError: "error while inspecting the config",
|
expectedError: "error while inspecting the config",
|
||||||
},
|
},
|
||||||
|
@ -77,7 +77,7 @@ func TestConfigInspectWithoutFormat(t *testing.T) {
|
||||||
args: []string{"foo"},
|
args: []string{"foo"},
|
||||||
configInspectFunc: func(_ context.Context, name string) (swarm.Config, []byte, error) {
|
configInspectFunc: func(_ context.Context, name string) (swarm.Config, []byte, error) {
|
||||||
if name != "foo" {
|
if name != "foo" {
|
||||||
return swarm.Config{}, nil, fmt.Errorf("invalid name, expected %s, got %s", "foo", name)
|
return swarm.Config{}, nil, errors.Errorf("Invalid name, expected %s, got %s", "foo", name)
|
||||||
}
|
}
|
||||||
return *builders.Config(builders.ConfigID("ID-foo"), builders.ConfigName("foo")), nil, nil
|
return *builders.Config(builders.ConfigID("ID-foo"), builders.ConfigName("foo")), nil, nil
|
||||||
},
|
},
|
||||||
|
|
|
@ -10,8 +10,8 @@ import (
|
||||||
"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/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -45,18 +45,18 @@ func newConfigListCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
// RunConfigList lists Swarm configs.
|
// RunConfigList lists Swarm configs.
|
||||||
func RunConfigList(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()
|
||||||
|
|
||||||
configs, err := apiClient.ConfigList(ctx, swarm.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
|
||||||
}
|
}
|
||||||
|
@ -67,7 +67,7 @@ func RunConfigList(ctx context.Context, dockerCLI command.Cli, options ListOptio
|
||||||
})
|
})
|
||||||
|
|
||||||
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, configs)
|
return FormatWrite(configCtx, configs)
|
||||||
|
|
|
@ -2,7 +2,6 @@ package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"io"
|
"io"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
@ -10,7 +9,9 @@ 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/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"
|
||||||
|
@ -19,7 +20,7 @@ import (
|
||||||
func TestConfigListErrors(t *testing.T) {
|
func TestConfigListErrors(t *testing.T) {
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
args []string
|
args []string
|
||||||
configListFunc func(context.Context, swarm.ConfigListOptions) ([]swarm.Config, 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 swarm.ConfigListOptions) ([]swarm.Config, error) {
|
configListFunc: func(_ context.Context, options types.ConfigListOptions) ([]swarm.Config, error) {
|
||||||
return []swarm.Config{}, errors.New("error listing configs")
|
return []swarm.Config{}, errors.Errorf("error listing configs")
|
||||||
},
|
},
|
||||||
expectedError: "error listing configs",
|
expectedError: "error listing configs",
|
||||||
},
|
},
|
||||||
|
@ -48,7 +49,7 @@ 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 swarm.ConfigListOptions) ([]swarm.Config, error) {
|
configListFunc: func(_ context.Context, options types.ConfigListOptions) ([]swarm.Config, error) {
|
||||||
return []swarm.Config{
|
return []swarm.Config{
|
||||||
*builders.Config(builders.ConfigID("ID-1-foo"),
|
*builders.Config(builders.ConfigID("ID-1-foo"),
|
||||||
builders.ConfigName("1-foo"),
|
builders.ConfigName("1-foo"),
|
||||||
|
@ -78,7 +79,7 @@ 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 swarm.ConfigListOptions) ([]swarm.Config, error) {
|
configListFunc: func(_ context.Context, options types.ConfigListOptions) ([]swarm.Config, error) {
|
||||||
return []swarm.Config{
|
return []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{
|
||||||
|
@ -95,7 +96,7 @@ 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 swarm.ConfigListOptions) ([]swarm.Config, error) {
|
configListFunc: func(_ context.Context, options types.ConfigListOptions) ([]swarm.Config, error) {
|
||||||
return []swarm.Config{
|
return []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{
|
||||||
|
@ -114,7 +115,7 @@ 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 swarm.ConfigListOptions) ([]swarm.Config, error) {
|
configListFunc: func(_ context.Context, options types.ConfigListOptions) ([]swarm.Config, error) {
|
||||||
return []swarm.Config{
|
return []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{
|
||||||
|
@ -131,7 +132,7 @@ 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 swarm.ConfigListOptions) ([]swarm.Config, error) {
|
configListFunc: func(_ context.Context, options types.ConfigListOptions) ([]swarm.Config, error) {
|
||||||
assert.Check(t, is.Equal("foo", options.Filters.Get("name")[0]))
|
assert.Check(t, is.Equal("foo", options.Filters.Get("name")[0]))
|
||||||
assert.Check(t, is.Equal("lbl1=Label-bar", options.Filters.Get("label")[0]))
|
assert.Check(t, is.Equal("lbl1=Label-bar", options.Filters.Get("label")[0]))
|
||||||
return []swarm.Config{
|
return []swarm.Config{
|
||||||
|
|
|
@ -2,11 +2,12 @@ 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/pkg/errors"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -34,17 +35,23 @@ func newConfigRemoveCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
// RunConfigRemove removes the given Swarm configs.
|
// RunConfigRemove removes the given Swarm configs.
|
||||||
func RunConfigRemove(ctx context.Context, dockerCLI command.Cli, opts RemoveOptions) error {
|
func RunConfigRemove(ctx context.Context, dockerCli command.Cli, opts RemoveOptions) error {
|
||||||
apiClient := dockerCLI.Client()
|
client := dockerCli.Client()
|
||||||
|
|
||||||
|
var errs []string
|
||||||
|
|
||||||
var errs []error
|
|
||||||
for _, name := range opts.Names {
|
for _, name := range opts.Names {
|
||||||
if err := apiClient.ConfigRemove(ctx, name); err != nil {
|
if err := client.ConfigRemove(ctx, name); err != nil {
|
||||||
errs = append(errs, err)
|
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,12 +1,12 @@
|
||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"io"
|
"io"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/docker/cli/internal/test"
|
"github.com/docker/cli/internal/test"
|
||||||
|
"github.com/pkg/errors"
|
||||||
"gotest.tools/v3/assert"
|
"gotest.tools/v3/assert"
|
||||||
is "gotest.tools/v3/assert/cmp"
|
is "gotest.tools/v3/assert/cmp"
|
||||||
)
|
)
|
||||||
|
@ -19,12 +19,12 @@ func TestConfigRemoveErrors(t *testing.T) {
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
args: []string{},
|
args: []string{},
|
||||||
expectedError: "requires at least 1 argument",
|
expectedError: "requires at least 1 argument.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
args: []string{"foo"},
|
args: []string{"foo"},
|
||||||
configRemoveFunc: func(name string) error {
|
configRemoveFunc: func(name string) error {
|
||||||
return errors.New("error removing config")
|
return errors.Errorf("error removing config")
|
||||||
},
|
},
|
||||||
expectedError: "error removing config",
|
expectedError: "error removing config",
|
||||||
},
|
},
|
||||||
|
@ -66,7 +66,7 @@ func TestConfigRemoveContinueAfterError(t *testing.T) {
|
||||||
configRemoveFunc: func(name string) error {
|
configRemoveFunc: func(name string) error {
|
||||||
removedConfigs = append(removedConfigs, name)
|
removedConfigs = append(removedConfigs, name)
|
||||||
if name == "foo" {
|
if name == "foo" {
|
||||||
return errors.New("error removing config: " + name)
|
return errors.Errorf("error removing config: %s", name)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
|
|
|
@ -7,8 +7,9 @@ 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/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/pkg/errors"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
@ -22,7 +23,7 @@ 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)
|
c, err := apiClient.ContainerInspect(ctx, args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -55,8 +56,8 @@ 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"
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -164,6 +165,9 @@ func getExitStatus(errC <-chan error, resultC <-chan container.WaitResponse) err
|
||||||
return cli.StatusError{StatusCode: int(result.StatusCode)}
|
return cli.StatusError{StatusCode: int(result.StatusCode)}
|
||||||
}
|
}
|
||||||
case err := <-errC:
|
case err := <-errC:
|
||||||
|
if errors.Is(err, context.Canceled) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,13 +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/docker/docker/api/types/container"
|
||||||
|
"github.com/pkg/errors"
|
||||||
"gotest.tools/v3/assert"
|
"gotest.tools/v3/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -16,63 +18,59 @@ func TestNewAttachCommandErrors(t *testing.T) {
|
||||||
name string
|
name string
|
||||||
args []string
|
args []string
|
||||||
expectedError string
|
expectedError string
|
||||||
containerInspectFunc func(img string) (container.InspectResponse, 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) (container.InspectResponse, error) {
|
containerInspectFunc: func(containerID string) (types.ContainerJSON, error) {
|
||||||
return container.InspectResponse{}, errors.New("something went wrong")
|
return types.ContainerJSON{}, errors.Errorf("something went wrong")
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "client-stopped",
|
name: "client-stopped",
|
||||||
args: []string{"5cb5bb5e4a3b"},
|
args: []string{"5cb5bb5e4a3b"},
|
||||||
expectedError: "You cannot attach to a stopped container",
|
expectedError: "You cannot attach to a stopped container",
|
||||||
containerInspectFunc: func(containerID string) (container.InspectResponse, error) {
|
containerInspectFunc: func(containerID string) (types.ContainerJSON, error) {
|
||||||
return container.InspectResponse{
|
c := types.ContainerJSON{}
|
||||||
ContainerJSONBase: &container.ContainerJSONBase{
|
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: "You cannot attach to a paused container",
|
expectedError: "You cannot attach to a paused container",
|
||||||
containerInspectFunc: func(containerID string) (container.InspectResponse, error) {
|
containerInspectFunc: func(containerID string) (types.ContainerJSON, error) {
|
||||||
return container.InspectResponse{
|
c := types.ContainerJSON{}
|
||||||
ContainerJSONBase: &container.ContainerJSONBase{
|
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: "You cannot attach to a restarting container",
|
expectedError: "You cannot attach to a restarting container",
|
||||||
containerInspectFunc: func(containerID string) (container.InspectResponse, error) {
|
containerInspectFunc: func(containerID string) (types.ContainerJSON, error) {
|
||||||
return container.InspectResponse{
|
c := types.ContainerJSON{}
|
||||||
ContainerJSONBase: &container.ContainerJSONBase{
|
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)
|
||||||
|
@ -112,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 {
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -4,68 +4,62 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
"github.com/moby/moby/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/moby/moby/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/moby/moby/api/types/filters"
|
"github.com/docker/docker/api/types/filters"
|
||||||
"github.com/moby/moby/api/types/image"
|
"github.com/docker/docker/api/types/image"
|
||||||
"github.com/moby/moby/api/types/network"
|
"github.com/docker/docker/api/types/network"
|
||||||
"github.com/moby/moby/api/types/system"
|
"github.com/docker/docker/api/types/system"
|
||||||
"github.com/moby/moby/client"
|
"github.com/docker/docker/client"
|
||||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
specs "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
type fakeClient struct {
|
type fakeClient struct {
|
||||||
client.Client
|
client.Client
|
||||||
inspectFunc func(string) (container.InspectResponse, error)
|
inspectFunc func(string) (types.ContainerJSON, error)
|
||||||
execInspectFunc func(execID string) (container.ExecInspect, error)
|
execInspectFunc func(execID string) (container.ExecInspect, error)
|
||||||
execCreateFunc func(containerID string, options container.ExecOptions) (container.ExecCreateResponse, error)
|
execCreateFunc func(containerID string, options container.ExecOptions) (types.IDResponse, error)
|
||||||
createContainerFunc func(config *container.Config,
|
createContainerFunc func(config *container.Config,
|
||||||
hostConfig *container.HostConfig,
|
hostConfig *container.HostConfig,
|
||||||
networkingConfig *network.NetworkingConfig,
|
networkingConfig *network.NetworkingConfig,
|
||||||
platform *ocispec.Platform,
|
platform *specs.Platform,
|
||||||
containerName string) (container.CreateResponse, error)
|
containerName string) (container.CreateResponse, error)
|
||||||
containerStartFunc func(containerID string, options container.StartOptions) error
|
containerStartFunc func(containerID string, options container.StartOptions) error
|
||||||
imageCreateFunc func(ctx context.Context, parentReference string, options image.CreateOptions) (io.ReadCloser, error)
|
imageCreateFunc func(parentReference string, options image.CreateOptions) (io.ReadCloser, error)
|
||||||
infoFunc func() (system.Info, error)
|
infoFunc func() (system.Info, error)
|
||||||
containerStatPathFunc func(containerID, path string) (container.PathStat, error)
|
containerStatPathFunc func(containerID, path string) (container.PathStat, error)
|
||||||
containerCopyFromFunc func(containerID, srcPath string) (io.ReadCloser, container.PathStat, error)
|
containerCopyFromFunc func(containerID, srcPath string) (io.ReadCloser, container.PathStat, error)
|
||||||
logFunc func(string, container.LogsOptions) (io.ReadCloser, error)
|
logFunc func(string, container.LogsOptions) (io.ReadCloser, error)
|
||||||
waitFunc func(string) (<-chan container.WaitResponse, <-chan error)
|
waitFunc func(string) (<-chan container.WaitResponse, <-chan error)
|
||||||
containerListFunc func(container.ListOptions) ([]container.Summary, error)
|
containerListFunc func(container.ListOptions) ([]types.Container, error)
|
||||||
containerExportFunc func(string) (io.ReadCloser, error)
|
containerExportFunc func(string) (io.ReadCloser, error)
|
||||||
containerExecResizeFunc func(id string, options container.ResizeOptions) error
|
containerExecResizeFunc func(id string, options container.ResizeOptions) error
|
||||||
containerRemoveFunc func(ctx context.Context, containerID string, options container.RemoveOptions) error
|
containerRemoveFunc func(ctx context.Context, containerID string, options container.RemoveOptions) error
|
||||||
containerRestartFunc func(ctx context.Context, containerID string, options container.StopOptions) error
|
|
||||||
containerStopFunc func(ctx context.Context, containerID string, options container.StopOptions) error
|
|
||||||
containerKillFunc func(ctx context.Context, containerID, signal string) error
|
containerKillFunc func(ctx context.Context, containerID, signal string) error
|
||||||
containerPruneFunc func(ctx context.Context, pruneFilters filters.Args) (container.PruneReport, error)
|
containerPruneFunc func(ctx context.Context, pruneFilters filters.Args) (container.PruneReport, error)
|
||||||
containerAttachFunc func(ctx context.Context, containerID string, options container.AttachOptions) (types.HijackedResponse, error)
|
containerAttachFunc func(ctx context.Context, containerID string, options container.AttachOptions) (types.HijackedResponse, error)
|
||||||
containerDiffFunc func(ctx context.Context, containerID string) ([]container.FilesystemChange, error)
|
|
||||||
containerRenameFunc func(ctx context.Context, oldName, newName string) error
|
|
||||||
containerCommitFunc func(ctx context.Context, container string, options container.CommitOptions) (container.CommitResponse, error)
|
|
||||||
containerPauseFunc func(ctx context.Context, container string) error
|
|
||||||
Version string
|
Version string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fakeClient) ContainerList(_ context.Context, options container.ListOptions) ([]container.Summary, 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 []container.Summary{}, nil
|
return []types.Container{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fakeClient) ContainerInspect(_ context.Context, containerID string) (container.InspectResponse, error) {
|
func (f *fakeClient) ContainerInspect(_ context.Context, containerID string) (types.ContainerJSON, error) {
|
||||||
if f.inspectFunc != nil {
|
if f.inspectFunc != nil {
|
||||||
return f.inspectFunc(containerID)
|
return f.inspectFunc(containerID)
|
||||||
}
|
}
|
||||||
return container.InspectResponse{}, nil
|
return types.ContainerJSON{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fakeClient) ContainerExecCreate(_ context.Context, containerID string, config container.ExecOptions) (container.ExecCreateResponse, error) {
|
func (f *fakeClient) ContainerExecCreate(_ context.Context, 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 container.ExecCreateResponse{}, nil
|
return types.IDResponse{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fakeClient) ContainerExecInspect(_ context.Context, execID string) (container.ExecInspect, error) {
|
func (f *fakeClient) ContainerExecInspect(_ context.Context, execID string) (container.ExecInspect, error) {
|
||||||
|
@ -75,7 +69,7 @@ func (f *fakeClient) ContainerExecInspect(_ context.Context, execID string) (con
|
||||||
return container.ExecInspect{}, nil
|
return container.ExecInspect{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*fakeClient) ContainerExecStart(context.Context, string, container.ExecStartOptions) error {
|
func (f *fakeClient) ContainerExecStart(context.Context, string, container.ExecStartOptions) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -84,7 +78,7 @@ func (f *fakeClient) ContainerCreate(
|
||||||
config *container.Config,
|
config *container.Config,
|
||||||
hostConfig *container.HostConfig,
|
hostConfig *container.HostConfig,
|
||||||
networkingConfig *network.NetworkingConfig,
|
networkingConfig *network.NetworkingConfig,
|
||||||
platform *ocispec.Platform,
|
platform *specs.Platform,
|
||||||
containerName string,
|
containerName string,
|
||||||
) (container.CreateResponse, error) {
|
) (container.CreateResponse, error) {
|
||||||
if f.createContainerFunc != nil {
|
if f.createContainerFunc != nil {
|
||||||
|
@ -100,9 +94,9 @@ func (f *fakeClient) ContainerRemove(ctx context.Context, containerID string, op
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fakeClient) ImageCreate(ctx context.Context, parentReference string, options image.CreateOptions) (io.ReadCloser, error) {
|
func (f *fakeClient) ImageCreate(_ context.Context, parentReference string, options image.CreateOptions) (io.ReadCloser, error) {
|
||||||
if f.imageCreateFunc != nil {
|
if f.imageCreateFunc != nil {
|
||||||
return f.imageCreateFunc(ctx, parentReference, options)
|
return f.imageCreateFunc(parentReference, options)
|
||||||
}
|
}
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
@ -181,54 +175,9 @@ func (f *fakeClient) ContainersPrune(ctx context.Context, pruneFilters filters.A
|
||||||
return container.PruneReport{}, nil
|
return container.PruneReport{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fakeClient) ContainerRestart(ctx context.Context, containerID string, options container.StopOptions) error {
|
|
||||||
if f.containerRestartFunc != nil {
|
|
||||||
return f.containerRestartFunc(ctx, containerID, options)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *fakeClient) ContainerStop(ctx context.Context, containerID string, options container.StopOptions) error {
|
|
||||||
if f.containerStopFunc != nil {
|
|
||||||
return f.containerStopFunc(ctx, containerID, options)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *fakeClient) ContainerAttach(ctx context.Context, containerID string, options container.AttachOptions) (types.HijackedResponse, error) {
|
func (f *fakeClient) ContainerAttach(ctx context.Context, containerID string, options container.AttachOptions) (types.HijackedResponse, error) {
|
||||||
if f.containerAttachFunc != nil {
|
if f.containerAttachFunc != nil {
|
||||||
return f.containerAttachFunc(ctx, containerID, options)
|
return f.containerAttachFunc(ctx, containerID, options)
|
||||||
}
|
}
|
||||||
return types.HijackedResponse{}, nil
|
return types.HijackedResponse{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fakeClient) ContainerDiff(ctx context.Context, containerID string) ([]container.FilesystemChange, error) {
|
|
||||||
if f.containerDiffFunc != nil {
|
|
||||||
return f.containerDiffFunc(ctx, containerID)
|
|
||||||
}
|
|
||||||
|
|
||||||
return []container.FilesystemChange{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *fakeClient) ContainerRename(ctx context.Context, oldName, newName string) error {
|
|
||||||
if f.containerRenameFunc != nil {
|
|
||||||
return f.containerRenameFunc(ctx, oldName, newName)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *fakeClient) ContainerCommit(ctx context.Context, containerID string, options container.CommitOptions) (container.CommitResponse, error) {
|
|
||||||
if f.containerCommitFunc != nil {
|
|
||||||
return f.containerCommitFunc(ctx, containerID, options)
|
|
||||||
}
|
|
||||||
return container.CommitResponse{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *fakeClient) ContainerPause(ctx context.Context, containerID string) error {
|
|
||||||
if f.containerPauseFunc != nil {
|
|
||||||
return f.containerPauseFunc(ctx, containerID)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
|
@ -28,7 +28,7 @@ func NewContainerCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
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),
|
||||||
|
|
|
@ -8,7 +8,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/opts"
|
"github.com/docker/cli/opts"
|
||||||
"github.com/moby/moby/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -61,7 +61,7 @@ func runCommit(ctx context.Context, dockerCli command.Cli, options *commitOption
|
||||||
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(),
|
||||||
Pause: options.pause,
|
Pause: options.pause,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -1,70 +0,0 @@
|
||||||
package container
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/docker/cli/internal/test"
|
|
||||||
"github.com/moby/moby/api/types/container"
|
|
||||||
"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 container.CommitOptions,
|
|
||||||
) (container.CommitResponse, 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.Pause, false))
|
|
||||||
assert.Check(t, is.Equal(ctr, "container-id"))
|
|
||||||
|
|
||||||
return container.CommitResponse{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",
|
|
||||||
"--pause=false",
|
|
||||||
"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 container.CommitOptions,
|
|
||||||
) (container.CommitResponse, error) {
|
|
||||||
return container.CommitResponse{}, 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,110 +1,70 @@
|
||||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
|
||||||
//go:build go1.23
|
|
||||||
|
|
||||||
package container
|
package container
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
|
||||||
"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/sys/capability"
|
|
||||||
"github.com/moby/sys/signal"
|
"github.com/moby/sys/signal"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
// allCaps is the magic value for "all capabilities".
|
|
||||||
const allCaps = "ALL"
|
|
||||||
|
|
||||||
// allLinuxCapabilities is a list of all known Linux capabilities.
|
// allLinuxCapabilities is a list of all known Linux capabilities.
|
||||||
//
|
//
|
||||||
|
// This list was based on the containerd pkg/cap package;
|
||||||
|
// https://github.com/containerd/containerd/blob/v1.7.19/pkg/cap/cap_linux.go#L133-L181
|
||||||
|
//
|
||||||
// TODO(thaJeztah): add descriptions, and enable descriptions for our completion scripts (cobra.CompletionOptions.DisableDescriptions is currently set to "true")
|
// TODO(thaJeztah): add descriptions, and enable descriptions for our completion scripts (cobra.CompletionOptions.DisableDescriptions is currently set to "true")
|
||||||
// TODO(thaJeztah): consider what casing we want to use for completion (see below);
|
var allLinuxCapabilities = []string{
|
||||||
//
|
"ALL", // magic value for "all capabilities"
|
||||||
// We need to consider what format is most convenient; currently we use the
|
|
||||||
// canonical name (uppercase and "CAP_" prefix), however, tab-completion is
|
|
||||||
// case-sensitive by default, so requires the user to type uppercase letters
|
|
||||||
// to filter the list of options.
|
|
||||||
//
|
|
||||||
// Bash completion provides a `completion-ignore-case on` option to make completion
|
|
||||||
// case-insensitive (https://askubuntu.com/a/87066), but it looks to be a global
|
|
||||||
// option; the current cobra.CompletionOptions also don't provide this as an option
|
|
||||||
// to be used in the generated completion-script.
|
|
||||||
//
|
|
||||||
// Fish completion has `smartcase` (by default?) which matches any case if
|
|
||||||
// all of the input is lowercase.
|
|
||||||
//
|
|
||||||
// Zsh does not appear have a dedicated option, but allows setting matching-rules
|
|
||||||
// (see https://superuser.com/a/1092328).
|
|
||||||
var allLinuxCapabilities = sync.OnceValue(func() []string {
|
|
||||||
caps := capability.ListKnown()
|
|
||||||
out := make([]string, 0, len(caps)+1)
|
|
||||||
out = append(out, allCaps)
|
|
||||||
for _, c := range caps {
|
|
||||||
out = append(out, "CAP_"+strings.ToUpper(c.String()))
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
})
|
|
||||||
|
|
||||||
// logDriverOptions provides the options for each built-in logging driver.
|
// caps35 is the caps of kernel 3.5 (37 entries)
|
||||||
var logDriverOptions = map[string][]string{
|
"CAP_CHOWN", // 2.2
|
||||||
"awslogs": {
|
"CAP_DAC_OVERRIDE", // 2.2
|
||||||
"max-buffer-size", "mode", "awslogs-create-group", "awslogs-credentials-endpoint", "awslogs-datetime-format",
|
"CAP_DAC_READ_SEARCH", // 2.2
|
||||||
"awslogs-group", "awslogs-multiline-pattern", "awslogs-region", "awslogs-stream", "tag",
|
"CAP_FOWNER", // 2.2
|
||||||
},
|
"CAP_FSETID", // 2.2
|
||||||
"fluentd": {
|
"CAP_KILL", // 2.2
|
||||||
"max-buffer-size", "mode", "env", "env-regex", "labels", "fluentd-address", "fluentd-async",
|
"CAP_SETGID", // 2.2
|
||||||
"fluentd-buffer-limit", "fluentd-request-ack", "fluentd-retry-wait", "fluentd-max-retries",
|
"CAP_SETUID", // 2.2
|
||||||
"fluentd-sub-second-precision", "fluentd-write-timeout", "tag",
|
"CAP_SETPCAP", // 2.2
|
||||||
},
|
"CAP_LINUX_IMMUTABLE", // 2.2
|
||||||
"gcplogs": {
|
"CAP_NET_BIND_SERVICE", // 2.2
|
||||||
"max-buffer-size", "mode", "env", "env-regex", "labels", "gcp-log-cmd", "gcp-meta-id", "gcp-meta-name",
|
"CAP_NET_BROADCAST", // 2.2
|
||||||
"gcp-meta-zone", "gcp-project",
|
"CAP_NET_ADMIN", // 2.2
|
||||||
},
|
"CAP_NET_RAW", // 2.2
|
||||||
"gelf": {
|
"CAP_IPC_LOCK", // 2.2
|
||||||
"max-buffer-size", "mode", "env", "env-regex", "labels", "gelf-address", "gelf-compression-level",
|
"CAP_IPC_OWNER", // 2.2
|
||||||
"gelf-compression-type", "gelf-tcp-max-reconnect", "gelf-tcp-reconnect-delay", "tag",
|
"CAP_SYS_MODULE", // 2.2
|
||||||
},
|
"CAP_SYS_RAWIO", // 2.2
|
||||||
"journald": {"max-buffer-size", "mode", "env", "env-regex", "labels", "tag"},
|
"CAP_SYS_CHROOT", // 2.2
|
||||||
"json-file": {"max-buffer-size", "mode", "env", "env-regex", "labels", "compress", "max-file", "max-size"},
|
"CAP_SYS_PTRACE", // 2.2
|
||||||
"local": {"max-buffer-size", "mode", "compress", "max-file", "max-size"},
|
"CAP_SYS_PACCT", // 2.2
|
||||||
"none": {},
|
"CAP_SYS_ADMIN", // 2.2
|
||||||
"splunk": {
|
"CAP_SYS_BOOT", // 2.2
|
||||||
"max-buffer-size", "mode", "env", "env-regex", "labels", "splunk-caname", "splunk-capath", "splunk-format",
|
"CAP_SYS_NICE", // 2.2
|
||||||
"splunk-gzip", "splunk-gzip-level", "splunk-index", "splunk-insecureskipverify", "splunk-source",
|
"CAP_SYS_RESOURCE", // 2.2
|
||||||
"splunk-sourcetype", "splunk-token", "splunk-url", "splunk-verify-connection", "tag",
|
"CAP_SYS_TIME", // 2.2
|
||||||
},
|
"CAP_SYS_TTY_CONFIG", // 2.2
|
||||||
"syslog": {
|
"CAP_MKNOD", // 2.4
|
||||||
"max-buffer-size", "mode", "env", "env-regex", "labels", "syslog-address", "syslog-facility", "syslog-format",
|
"CAP_LEASE", // 2.4
|
||||||
"syslog-tls-ca-cert", "syslog-tls-cert", "syslog-tls-key", "syslog-tls-skip-verify", "tag",
|
"CAP_AUDIT_WRITE", // 2.6.11
|
||||||
},
|
"CAP_AUDIT_CONTROL", // 2.6.11
|
||||||
}
|
"CAP_SETFCAP", // 2.6.24
|
||||||
|
"CAP_MAC_OVERRIDE", // 2.6.25
|
||||||
|
"CAP_MAC_ADMIN", // 2.6.25
|
||||||
|
"CAP_SYSLOG", // 2.6.37
|
||||||
|
"CAP_WAKE_ALARM", // 3.0
|
||||||
|
"CAP_BLOCK_SUSPEND", // 3.5
|
||||||
|
|
||||||
// builtInLogDrivers provides a list of the built-in logging drivers.
|
// caps316 is the caps of kernel 3.16 (38 entries)
|
||||||
var builtInLogDrivers = sync.OnceValue(func() []string {
|
"CAP_AUDIT_READ",
|
||||||
drivers := make([]string, 0, len(logDriverOptions))
|
|
||||||
for driver := range logDriverOptions {
|
|
||||||
drivers = append(drivers, driver)
|
|
||||||
}
|
|
||||||
return drivers
|
|
||||||
})
|
|
||||||
|
|
||||||
// allLogDriverOptions provides all options of the built-in logging drivers.
|
// caps58 is the caps of kernel 5.8 (40 entries)
|
||||||
// The list does not contain duplicates.
|
"CAP_PERFMON",
|
||||||
var allLogDriverOptions = sync.OnceValue(func() []string {
|
"CAP_BPF",
|
||||||
var result []string
|
|
||||||
seen := make(map[string]bool)
|
// caps59 is the caps of kernel 5.9 (41 entries)
|
||||||
for driver := range logDriverOptions {
|
"CAP_CHECKPOINT_RESTORE",
|
||||||
for _, opt := range logDriverOptions[driver] {
|
|
||||||
if !seen[opt] {
|
|
||||||
seen[opt] = true
|
|
||||||
result = append(result, opt)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
})
|
|
||||||
|
|
||||||
// restartPolicies is a list of all valid restart-policies..
|
// restartPolicies is a list of all valid restart-policies..
|
||||||
//
|
//
|
||||||
|
@ -116,209 +76,8 @@ var restartPolicies = []string{
|
||||||
string(container.RestartPolicyUnlessStopped),
|
string(container.RestartPolicyUnlessStopped),
|
||||||
}
|
}
|
||||||
|
|
||||||
// addCompletions adds the completions that `run` and `create` have in common.
|
|
||||||
func addCompletions(cmd *cobra.Command, dockerCLI completion.APIClientProvider) {
|
|
||||||
_ = cmd.RegisterFlagCompletionFunc("attach", completion.FromList("stderr", "stdin", "stdout"))
|
|
||||||
_ = cmd.RegisterFlagCompletionFunc("cap-add", completeLinuxCapabilityNames)
|
|
||||||
_ = cmd.RegisterFlagCompletionFunc("cap-drop", completeLinuxCapabilityNames)
|
|
||||||
_ = cmd.RegisterFlagCompletionFunc("cgroupns", completeCgroupns())
|
|
||||||
_ = cmd.RegisterFlagCompletionFunc("env", completion.EnvVarNames)
|
|
||||||
_ = cmd.RegisterFlagCompletionFunc("env-file", completion.FileNames)
|
|
||||||
_ = cmd.RegisterFlagCompletionFunc("ipc", completeIpc(dockerCLI))
|
|
||||||
_ = cmd.RegisterFlagCompletionFunc("link", completeLink(dockerCLI))
|
|
||||||
_ = cmd.RegisterFlagCompletionFunc("log-driver", completeLogDriver(dockerCLI))
|
|
||||||
_ = cmd.RegisterFlagCompletionFunc("log-opt", completeLogOpt)
|
|
||||||
_ = cmd.RegisterFlagCompletionFunc("network", completion.NetworkNames(dockerCLI))
|
|
||||||
_ = cmd.RegisterFlagCompletionFunc("pid", completePid(dockerCLI))
|
|
||||||
_ = cmd.RegisterFlagCompletionFunc("platform", completion.Platforms)
|
|
||||||
_ = cmd.RegisterFlagCompletionFunc("pull", completion.FromList(PullImageAlways, PullImageMissing, PullImageNever))
|
|
||||||
_ = cmd.RegisterFlagCompletionFunc("restart", completeRestartPolicies)
|
|
||||||
_ = cmd.RegisterFlagCompletionFunc("security-opt", completeSecurityOpt)
|
|
||||||
_ = cmd.RegisterFlagCompletionFunc("stop-signal", completeSignals)
|
|
||||||
_ = cmd.RegisterFlagCompletionFunc("storage-opt", completeStorageOpt)
|
|
||||||
_ = cmd.RegisterFlagCompletionFunc("ulimit", completeUlimit)
|
|
||||||
_ = cmd.RegisterFlagCompletionFunc("userns", completion.FromList("host"))
|
|
||||||
_ = cmd.RegisterFlagCompletionFunc("uts", completion.FromList("host"))
|
|
||||||
_ = cmd.RegisterFlagCompletionFunc("volume-driver", completeVolumeDriver(dockerCLI))
|
|
||||||
_ = cmd.RegisterFlagCompletionFunc("volumes-from", completion.ContainerNames(dockerCLI, true))
|
|
||||||
}
|
|
||||||
|
|
||||||
// completeCgroupns implements shell completion for the `--cgroupns` option of `run` and `create`.
|
|
||||||
func completeCgroupns() cobra.CompletionFunc {
|
|
||||||
return completion.FromList(string(container.CgroupnsModeHost), string(container.CgroupnsModePrivate))
|
|
||||||
}
|
|
||||||
|
|
||||||
// completeDetachKeys implements shell completion for the `--detach-keys` option of `run` and `create`.
|
|
||||||
func completeDetachKeys(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
|
|
||||||
return []string{"ctrl-"}, cobra.ShellCompDirectiveNoSpace
|
|
||||||
}
|
|
||||||
|
|
||||||
// completeIpc implements shell completion for the `--ipc` option of `run` and `create`.
|
|
||||||
// The completion is partly composite.
|
|
||||||
func completeIpc(dockerCLI completion.APIClientProvider) cobra.CompletionFunc {
|
|
||||||
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"
|
|
||||||
return []string{"container:"}, cobra.ShellCompDirectiveNoSpace
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(toComplete, "container:") {
|
|
||||||
names, _ := completion.ContainerNames(dockerCLI, true)(cmd, args, toComplete)
|
|
||||||
return prefixWith("container:", names), cobra.ShellCompDirectiveNoFileComp
|
|
||||||
}
|
|
||||||
return []string{
|
|
||||||
string(container.IPCModeContainer + ":"),
|
|
||||||
string(container.IPCModeHost),
|
|
||||||
string(container.IPCModeNone),
|
|
||||||
string(container.IPCModePrivate),
|
|
||||||
string(container.IPCModeShareable),
|
|
||||||
}, cobra.ShellCompDirectiveNoFileComp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// completeLink implements shell completion for the `--link` option of `run` and `create`.
|
|
||||||
func completeLink(dockerCLI completion.APIClientProvider) cobra.CompletionFunc {
|
|
||||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
|
||||||
return postfixWith(":", containerNames(dockerCLI, cmd, args, toComplete)), cobra.ShellCompDirectiveNoSpace
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
// of the build-in log drivers.
|
|
||||||
func completeLogDriver(dockerCLI completion.APIClientProvider) cobra.CompletionFunc {
|
|
||||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
|
||||||
info, err := dockerCLI.Client().Info(cmd.Context())
|
|
||||||
if err != nil {
|
|
||||||
return builtInLogDrivers(), cobra.ShellCompDirectiveNoFileComp
|
|
||||||
}
|
|
||||||
drivers := info.Plugins.Log
|
|
||||||
return drivers, cobra.ShellCompDirectiveNoFileComp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// completeLogOpt implements shell completion for the `--log-opt` option of `run` and `create`.
|
|
||||||
// If the user supplied a log-driver, only options for that driver are returned.
|
|
||||||
func completeLogOpt(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
|
|
||||||
driver, _ := cmd.Flags().GetString("log-driver")
|
|
||||||
if options, exists := logDriverOptions[driver]; exists {
|
|
||||||
return postfixWith("=", options), cobra.ShellCompDirectiveNoSpace | cobra.ShellCompDirectiveNoFileComp
|
|
||||||
}
|
|
||||||
return postfixWith("=", allLogDriverOptions()), cobra.ShellCompDirectiveNoSpace
|
|
||||||
}
|
|
||||||
|
|
||||||
// completePid implements shell completion for the `--pid` option of `run` and `create`.
|
|
||||||
func completePid(dockerCLI completion.APIClientProvider) cobra.CompletionFunc {
|
|
||||||
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"
|
|
||||||
return []string{"container:"}, cobra.ShellCompDirectiveNoSpace
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(toComplete, "container:") {
|
|
||||||
names, _ := completion.ContainerNames(dockerCLI, true)(cmd, args, toComplete)
|
|
||||||
return prefixWith("container:", names), cobra.ShellCompDirectiveNoFileComp
|
|
||||||
}
|
|
||||||
return []string{"container:", "host"}, cobra.ShellCompDirectiveNoFileComp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// completeSecurityOpt implements shell completion for the `--security-opt` option of `run` and `create`.
|
|
||||||
// The completion is partly composite.
|
|
||||||
func completeSecurityOpt(_ *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
|
||||||
if len(toComplete) > 0 && strings.HasPrefix("apparmor=", toComplete) { //nolint:gocritic // not swapped, matches partly typed "apparmor="
|
|
||||||
return []string{"apparmor="}, cobra.ShellCompDirectiveNoSpace
|
|
||||||
}
|
|
||||||
if len(toComplete) > 0 && strings.HasPrefix("label", toComplete) { //nolint:gocritic // not swapped, matches partly typed "label"
|
|
||||||
return []string{"label="}, cobra.ShellCompDirectiveNoSpace
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(toComplete, "label=") {
|
|
||||||
if strings.HasPrefix(toComplete, "label=d") {
|
|
||||||
return []string{"label=disable"}, cobra.ShellCompDirectiveNoFileComp
|
|
||||||
}
|
|
||||||
labels := []string{"disable", "level:", "role:", "type:", "user:"}
|
|
||||||
return prefixWith("label=", labels), cobra.ShellCompDirectiveNoSpace | cobra.ShellCompDirectiveNoFileComp
|
|
||||||
}
|
|
||||||
// length must be > 1 here so that completion of "s" falls through.
|
|
||||||
if len(toComplete) > 1 && strings.HasPrefix("seccomp", toComplete) { //nolint:gocritic // not swapped, matches partly typed "seccomp"
|
|
||||||
return []string{"seccomp="}, cobra.ShellCompDirectiveNoSpace
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(toComplete, "seccomp=") {
|
|
||||||
return []string{"seccomp=unconfined"}, cobra.ShellCompDirectiveNoFileComp
|
|
||||||
}
|
|
||||||
return []string{"apparmor=", "label=", "no-new-privileges", "seccomp=", "systempaths=unconfined"}, cobra.ShellCompDirectiveNoFileComp
|
|
||||||
}
|
|
||||||
|
|
||||||
// completeStorageOpt implements shell completion for the `--storage-opt` option of `run` and `create`.
|
|
||||||
func completeStorageOpt(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
|
|
||||||
return []string{"size="}, cobra.ShellCompDirectiveNoSpace
|
|
||||||
}
|
|
||||||
|
|
||||||
// completeUlimit implements shell completion for the `--ulimit` option of `run` and `create`.
|
|
||||||
func completeUlimit(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
|
|
||||||
limits := []string{
|
|
||||||
"as",
|
|
||||||
"chroot",
|
|
||||||
"core",
|
|
||||||
"cpu",
|
|
||||||
"data",
|
|
||||||
"fsize",
|
|
||||||
"locks",
|
|
||||||
"maxlogins",
|
|
||||||
"maxsyslogins",
|
|
||||||
"memlock",
|
|
||||||
"msgqueue",
|
|
||||||
"nice",
|
|
||||||
"nofile",
|
|
||||||
"nproc",
|
|
||||||
"priority",
|
|
||||||
"rss",
|
|
||||||
"rtprio",
|
|
||||||
"sigpending",
|
|
||||||
"stack",
|
|
||||||
}
|
|
||||||
return postfixWith("=", limits), cobra.ShellCompDirectiveNoSpace
|
|
||||||
}
|
|
||||||
|
|
||||||
// completeVolumeDriver contacts the API to get the built-in and installed volume drivers.
|
|
||||||
func completeVolumeDriver(dockerCLI completion.APIClientProvider) cobra.CompletionFunc {
|
|
||||||
return func(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
|
|
||||||
info, err := dockerCLI.Client().Info(cmd.Context())
|
|
||||||
if err != nil {
|
|
||||||
// fallback: the built-in drivers
|
|
||||||
return []string{"local"}, cobra.ShellCompDirectiveNoFileComp
|
|
||||||
}
|
|
||||||
drivers := info.Plugins.Volume
|
|
||||||
return drivers, cobra.ShellCompDirectiveNoFileComp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// containerNames contacts the API to get names and optionally IDs of containers.
|
|
||||||
// In case of an error, an empty list is returned.
|
|
||||||
func containerNames(dockerCLI completion.APIClientProvider, cmd *cobra.Command, args []string, toComplete string) []string {
|
|
||||||
names, _ := completion.ContainerNames(dockerCLI, true)(cmd, args, toComplete)
|
|
||||||
if names == nil {
|
|
||||||
return []string{}
|
|
||||||
}
|
|
||||||
return names
|
|
||||||
}
|
|
||||||
|
|
||||||
// prefixWith prefixes every element in the slice with the given prefix.
|
|
||||||
func prefixWith(prefix string, values []string) []string {
|
|
||||||
result := make([]string, len(values))
|
|
||||||
for i, v := range values {
|
|
||||||
result[i] = prefix + v
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// postfixWith appends postfix to every element in the slice.
|
|
||||||
func postfixWith(postfix string, values []string) []string {
|
|
||||||
result := make([]string, len(values))
|
|
||||||
for i, v := range values {
|
|
||||||
result[i] = v + postfix
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func completeLinuxCapabilityNames(cmd *cobra.Command, args []string, toComplete string) (names []string, _ cobra.ShellCompDirective) {
|
func completeLinuxCapabilityNames(cmd *cobra.Command, args []string, toComplete string) (names []string, _ cobra.ShellCompDirective) {
|
||||||
return completion.FromList(allLinuxCapabilities()...)(cmd, args, toComplete)
|
return completion.FromList(allLinuxCapabilities...)(cmd, args, toComplete)
|
||||||
}
|
}
|
||||||
|
|
||||||
func completeRestartPolicies(cmd *cobra.Command, args []string, toComplete string) (names []string, _ cobra.ShellCompDirective) {
|
func completeRestartPolicies(cmd *cobra.Command, args []string, toComplete string) (names []string, _ cobra.ShellCompDirective) {
|
||||||
|
|
|
@ -1,134 +0,0 @@
|
||||||
package container
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/docker/cli/internal/test"
|
|
||||||
"github.com/docker/cli/internal/test/builders"
|
|
||||||
"github.com/moby/moby/api/types/container"
|
|
||||||
"github.com/moby/sys/signal"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"gotest.tools/v3/assert"
|
|
||||||
is "gotest.tools/v3/assert/cmp"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestCompleteLinuxCapabilityNames(t *testing.T) {
|
|
||||||
names, directives := completeLinuxCapabilityNames(nil, nil, "")
|
|
||||||
assert.Check(t, is.Equal(directives&cobra.ShellCompDirectiveNoFileComp, cobra.ShellCompDirectiveNoFileComp), "Should not perform file completion")
|
|
||||||
assert.Assert(t, len(names) > 1)
|
|
||||||
assert.Check(t, names[0] == allCaps)
|
|
||||||
for _, name := range names[1:] {
|
|
||||||
assert.Check(t, strings.HasPrefix(name, "CAP_"))
|
|
||||||
assert.Check(t, is.Equal(name, strings.ToUpper(name)), "Should be formatted uppercase")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCompletePid(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
containerListFunc func(container.ListOptions) ([]container.Summary, error)
|
|
||||||
toComplete string
|
|
||||||
expectedCompletions []string
|
|
||||||
expectedDirective cobra.ShellCompDirective
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
toComplete: "",
|
|
||||||
expectedCompletions: []string{"container:", "host"},
|
|
||||||
expectedDirective: cobra.ShellCompDirectiveNoFileComp,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
toComplete: "c",
|
|
||||||
expectedCompletions: []string{"container:"},
|
|
||||||
expectedDirective: cobra.ShellCompDirectiveNoSpace,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
containerListFunc: func(container.ListOptions) ([]container.Summary, error) {
|
|
||||||
return []container.Summary{
|
|
||||||
*builders.Container("c1"),
|
|
||||||
*builders.Container("c2"),
|
|
||||||
}, nil
|
|
||||||
},
|
|
||||||
toComplete: "container:",
|
|
||||||
expectedCompletions: []string{"container:c1", "container:c2"},
|
|
||||||
expectedDirective: cobra.ShellCompDirectiveNoFileComp,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range tests {
|
|
||||||
t.Run(tc.toComplete, func(t *testing.T) {
|
|
||||||
cli := test.NewFakeCli(&fakeClient{
|
|
||||||
containerListFunc: tc.containerListFunc,
|
|
||||||
})
|
|
||||||
completions, directive := completePid(cli)(NewRunCommand(cli), nil, tc.toComplete)
|
|
||||||
assert.Check(t, is.DeepEqual(completions, tc.expectedCompletions))
|
|
||||||
assert.Check(t, is.Equal(directive, tc.expectedDirective))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCompleteRestartPolicies(t *testing.T) {
|
|
||||||
values, directives := completeRestartPolicies(nil, nil, "")
|
|
||||||
assert.Check(t, is.Equal(directives&cobra.ShellCompDirectiveNoFileComp, cobra.ShellCompDirectiveNoFileComp), "Should not perform file completion")
|
|
||||||
expected := restartPolicies
|
|
||||||
assert.Check(t, is.DeepEqual(values, expected))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCompleteSecurityOpt(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
toComplete string
|
|
||||||
expectedCompletions []string
|
|
||||||
expectedDirective cobra.ShellCompDirective
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
toComplete: "",
|
|
||||||
expectedCompletions: []string{"apparmor=", "label=", "no-new-privileges", "seccomp=", "systempaths=unconfined"},
|
|
||||||
expectedDirective: cobra.ShellCompDirectiveNoFileComp,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
toComplete: "apparmor=",
|
|
||||||
expectedCompletions: []string{"apparmor="},
|
|
||||||
expectedDirective: cobra.ShellCompDirectiveNoSpace,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
toComplete: "label=",
|
|
||||||
expectedCompletions: []string{"label=disable", "label=level:", "label=role:", "label=type:", "label=user:"},
|
|
||||||
expectedDirective: cobra.ShellCompDirectiveNoSpace | cobra.ShellCompDirectiveNoFileComp,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
toComplete: "s",
|
|
||||||
// We do not filter matching completions but delegate this task to the shell script.
|
|
||||||
expectedCompletions: []string{"apparmor=", "label=", "no-new-privileges", "seccomp=", "systempaths=unconfined"},
|
|
||||||
expectedDirective: cobra.ShellCompDirectiveNoFileComp,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
toComplete: "se",
|
|
||||||
expectedCompletions: []string{"seccomp="},
|
|
||||||
expectedDirective: cobra.ShellCompDirectiveNoSpace,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
toComplete: "seccomp=",
|
|
||||||
expectedCompletions: []string{"seccomp=unconfined"},
|
|
||||||
expectedDirective: cobra.ShellCompDirectiveNoFileComp,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
toComplete: "sy",
|
|
||||||
expectedCompletions: []string{"apparmor=", "label=", "no-new-privileges", "seccomp=", "systempaths=unconfined"},
|
|
||||||
expectedDirective: cobra.ShellCompDirectiveNoFileComp,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range tests {
|
|
||||||
t.Run(tc.toComplete, func(t *testing.T) {
|
|
||||||
completions, directive := completeSecurityOpt(nil, nil, tc.toComplete)
|
|
||||||
assert.Check(t, is.DeepEqual(completions, tc.expectedCompletions))
|
|
||||||
assert.Check(t, is.Equal(directive, tc.expectedDirective))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCompleteSignals(t *testing.T) {
|
|
||||||
values, directives := completeSignals(nil, nil, "")
|
|
||||||
assert.Check(t, is.Equal(directives&cobra.ShellCompDirectiveNoFileComp, cobra.ShellCompDirectiveNoFileComp), "Should not perform file completion")
|
|
||||||
assert.Check(t, len(values) > 1)
|
|
||||||
assert.Check(t, is.Len(values, len(signal.SignalMap)))
|
|
||||||
}
|
|
|
@ -15,9 +15,10 @@ 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/api/types/container"
|
"github.com/docker/docker/pkg/system"
|
||||||
|
units "github.com/docker/go-units"
|
||||||
"github.com/morikuni/aec"
|
"github.com/morikuni/aec"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
@ -129,12 +130,13 @@ func NewCopyCommand(dockerCli command.Cli) *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] == "" {
|
||||||
|
@ -201,15 +203,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
|
||||||
|
|
||||||
|
@ -225,16 +226,16 @@ 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 symbol link, 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 {
|
||||||
srcStat, err := apiClient.ContainerStatPath(ctx, copyConfig.container, srcPath)
|
srcStat, err := client.ContainerStatPath(ctx, copyConfig.container, 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 && srcStat.Mode&os.ModeSymlink != 0 {
|
if err == nil && srcStat.Mode&os.ModeSymlink != 0 {
|
||||||
linkTarget := srcStat.LinkTarget
|
linkTarget := srcStat.LinkTarget
|
||||||
if !isAbs(linkTarget) {
|
if !system.IsAbs(linkTarget) {
|
||||||
// Join with the parent directory.
|
// Join with the parent directory.
|
||||||
srcParent, _ := archive.SplitPathDirEntry(srcPath)
|
srcParent, _ := archive.SplitPathDirEntry(srcPath)
|
||||||
linkTarget = filepath.Join(srcParent, linkTarget)
|
linkTarget = filepath.Join(srcParent, linkTarget)
|
||||||
|
@ -248,14 +249,14 @@ 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()
|
||||||
|
|
||||||
content, stat, err := apiClient.CopyFromContainer(ctx, copyConfig.container, srcPath)
|
content, stat, err := client.CopyFromContainer(ctx, copyConfig.container, srcPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer content.Close()
|
defer content.Close()
|
||||||
|
|
||||||
if dstPath == "-" {
|
if dstPath == "-" {
|
||||||
_, err = io.Copy(dockerCLI.Out(), content)
|
_, err = io.Copy(dockerCli.Out(), content)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -284,12 +285,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()
|
||||||
_, _ = fmt.Fprintln(dockerCLI.Err(), "Successfully copied", progressHumanSize(copiedSize), "to", dstPath)
|
fmt.Fprintln(dockerCli.Err(), "Successfully copied", progressHumanSize(copiedSize), "to", dstPath)
|
||||||
|
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
@ -298,7 +299,7 @@ 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) (err 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
|
||||||
|
|
||||||
|
@ -310,23 +311,22 @@ func copyToContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cpCo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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}
|
||||||
dstStat, err := apiClient.ContainerStatPath(ctx, copyConfig.container, dstPath)
|
dstStat, err := client.ContainerStatPath(ctx, copyConfig.container, dstPath)
|
||||||
|
|
||||||
// If the destination is a symbolic link, we should evaluate it.
|
// If the destination is a symbolic link, we should evaluate it.
|
||||||
if err == nil && dstStat.Mode&os.ModeSymlink != 0 {
|
if err == nil && dstStat.Mode&os.ModeSymlink != 0 {
|
||||||
linkTarget := dstStat.LinkTarget
|
linkTarget := dstStat.LinkTarget
|
||||||
if !isAbs(linkTarget) {
|
if !system.IsAbs(linkTarget) {
|
||||||
// Join with the parent directory.
|
// Join with the parent directory.
|
||||||
dstParent, _ := archive.SplitPathDirEntry(dstPath)
|
dstParent, _ := archive.SplitPathDirEntry(dstPath)
|
||||||
linkTarget = filepath.Join(dstParent, linkTarget)
|
linkTarget = filepath.Join(dstParent, linkTarget)
|
||||||
}
|
}
|
||||||
|
|
||||||
dstInfo.Path = linkTarget
|
dstInfo.Path = linkTarget
|
||||||
dstStat, err = apiClient.ContainerStatPath(ctx, copyConfig.container, linkTarget)
|
dstStat, err = client.ContainerStatPath(ctx, copyConfig.container, linkTarget)
|
||||||
// FIXME(thaJeztah): unhandled error (should this return?)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate the destination path
|
// Validate the destination path
|
||||||
|
@ -398,20 +398,21 @@ func copyToContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cpCo
|
||||||
}
|
}
|
||||||
|
|
||||||
options := container.CopyToContainerOptions{
|
options := container.CopyToContainerOptions{
|
||||||
|
AllowOverwriteDirWithFile: false,
|
||||||
CopyUIDGID: copyConfig.copyUIDGID,
|
CopyUIDGID: copyConfig.copyUIDGID,
|
||||||
}
|
}
|
||||||
|
|
||||||
if copyConfig.quiet {
|
if copyConfig.quiet {
|
||||||
return apiClient.CopyToContainer(ctx, copyConfig.container, resolvedDstPath, content, options)
|
return client.CopyToContainer(ctx, copyConfig.container, resolvedDstPath, content, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
res := apiClient.CopyToContainer(ctx, copyConfig.container, resolvedDstPath, content, options)
|
res := client.CopyToContainer(ctx, copyConfig.container, resolvedDstPath, content, options)
|
||||||
cancel()
|
cancel()
|
||||||
<-done
|
<-done
|
||||||
restore()
|
restore()
|
||||||
fmt.Fprintln(dockerCLI.Err(), "Successfully copied", progressHumanSize(copiedSize), "to", copyConfig.container+":"+dstInfo.Path)
|
fmt.Fprintln(dockerCli.Err(), "Successfully copied", progressHumanSize(copiedSize), "to", copyConfig.container+":"+dstInfo.Path)
|
||||||
|
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
@ -433,7 +434,7 @@ func copyToContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cpCo
|
||||||
// client, a `:` could be part of an absolute Windows path, in which case it
|
// client, a `:` could be part of an absolute Windows path, in which case it
|
||||||
// is immediately proceeded by a backslash.
|
// is immediately proceeded by a backslash.
|
||||||
func splitCpArg(arg string) (ctr, path string) {
|
func splitCpArg(arg string) (ctr, path string) {
|
||||||
if isAbs(arg) {
|
if system.IsAbs(arg) {
|
||||||
// Explicit local absolute path, e.g., `C:\foo` or `/foo`.
|
// Explicit local absolute path, e.g., `C:\foo` or `/foo`.
|
||||||
return "", arg
|
return "", arg
|
||||||
}
|
}
|
||||||
|
@ -447,15 +448,3 @@ func splitCpArg(arg string) (ctr, path string) {
|
||||||
|
|
||||||
return ctr, path
|
return ctr, path
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsAbs is a platform-agnostic wrapper for filepath.IsAbs.
|
|
||||||
//
|
|
||||||
// On Windows, golang filepath.IsAbs does not consider a path \windows\system32
|
|
||||||
// as absolute as it doesn't start with a drive-letter/colon combination. However,
|
|
||||||
// in docker we need to verify things such as WORKDIR /windows/system32 in
|
|
||||||
// a Dockerfile (which gets translated to \windows\system32 when being processed
|
|
||||||
// by the daemon). This SHOULD be treated as absolute from a docker processing
|
|
||||||
// perspective.
|
|
||||||
func isAbs(path string) bool {
|
|
||||||
return filepath.IsAbs(path) || strings.HasPrefix(path, string(os.PathSeparator))
|
|
||||||
}
|
|
||||||
|
|
|
@ -9,12 +9,12 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/docker/cli/internal/test"
|
"github.com/docker/cli/internal/test"
|
||||||
"github.com/moby/go-archive"
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/moby/go-archive/compression"
|
"github.com/docker/docker/pkg/archive"
|
||||||
"github.com/moby/moby/api/types/container"
|
|
||||||
"gotest.tools/v3/assert"
|
"gotest.tools/v3/assert"
|
||||||
is "gotest.tools/v3/assert/cmp"
|
is "gotest.tools/v3/assert/cmp"
|
||||||
"gotest.tools/v3/fs"
|
"gotest.tools/v3/fs"
|
||||||
|
"gotest.tools/v3/skip"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestRunCopyWithInvalidArguments(t *testing.T) {
|
func TestRunCopyWithInvalidArguments(t *testing.T) {
|
||||||
|
@ -67,15 +67,14 @@ func TestRunCopyFromContainerToStdout(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRunCopyFromContainerToFilesystem(t *testing.T) {
|
func TestRunCopyFromContainerToFilesystem(t *testing.T) {
|
||||||
srcDir := fs.NewDir(t, "cp-test",
|
destDir := fs.NewDir(t, "cp-test",
|
||||||
fs.WithFile("file1", "content\n"))
|
fs.WithFile("file1", "content\n"))
|
||||||
|
defer destDir.Remove()
|
||||||
destDir := fs.NewDir(t, "cp-test")
|
|
||||||
|
|
||||||
cli := test.NewFakeCli(&fakeClient{
|
cli := test.NewFakeCli(&fakeClient{
|
||||||
containerCopyFromFunc: func(ctr, srcPath string) (io.ReadCloser, container.PathStat, error) {
|
containerCopyFromFunc: func(ctr, srcPath string) (io.ReadCloser, container.PathStat, error) {
|
||||||
assert.Check(t, is.Equal("container", ctr))
|
assert.Check(t, is.Equal("container", ctr))
|
||||||
readCloser, err := archive.Tar(srcDir.Path(), compression.None)
|
readCloser, err := archive.TarWithOptions(destDir.Path(), &archive.TarOptions{})
|
||||||
return readCloser, container.PathStat{}, err
|
return readCloser, container.PathStat{}, err
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -152,7 +151,7 @@ func TestSplitCpArg(t *testing.T) {
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
doc: "absolute path with colon",
|
doc: "absolute path with colon",
|
||||||
os: "unix",
|
os: "linux",
|
||||||
path: "/abs/path:withcolon",
|
path: "/abs/path:withcolon",
|
||||||
expectedPath: "/abs/path:withcolon",
|
expectedPath: "/abs/path:withcolon",
|
||||||
},
|
},
|
||||||
|
@ -180,13 +179,9 @@ func TestSplitCpArg(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tc := range testcases {
|
for _, tc := range testcases {
|
||||||
|
tc := tc
|
||||||
t.Run(tc.doc, func(t *testing.T) {
|
t.Run(tc.doc, func(t *testing.T) {
|
||||||
if tc.os == "windows" && runtime.GOOS != "windows" {
|
skip.If(t, tc.os == "windows" && runtime.GOOS != "windows" || tc.os == "linux" && runtime.GOOS == "windows")
|
||||||
t.Skip("skipping windows test on non-windows platform")
|
|
||||||
}
|
|
||||||
if tc.os == "unix" && runtime.GOOS == "windows" {
|
|
||||||
t.Skip("skipping unix test on windows")
|
|
||||||
}
|
|
||||||
|
|
||||||
ctr, path := splitCpArg(tc.path)
|
ctr, path := splitCpArg(tc.path)
|
||||||
assert.Check(t, is.Equal(tc.expectedContainer, ctr))
|
assert.Check(t, is.Equal(tc.expectedContainer, ctr))
|
||||||
|
|
|
@ -1,35 +1,26 @@
|
||||||
package container
|
package container
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"archive/tar"
|
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/netip"
|
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"regexp"
|
||||||
"strings"
|
|
||||||
|
|
||||||
cerrdefs "github.com/containerd/errdefs"
|
|
||||||
"github.com/containerd/platforms"
|
"github.com/containerd/platforms"
|
||||||
"github.com/distribution/reference"
|
"github.com/distribution/reference"
|
||||||
"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/cli/command/image"
|
"github.com/docker/cli/cli/command/image"
|
||||||
"github.com/docker/cli/cli/config/configfile"
|
|
||||||
"github.com/docker/cli/cli/config/types"
|
|
||||||
"github.com/docker/cli/cli/streams"
|
"github.com/docker/cli/cli/streams"
|
||||||
"github.com/docker/cli/cli/trust"
|
|
||||||
"github.com/docker/cli/internal/jsonstream"
|
|
||||||
"github.com/docker/cli/opts"
|
"github.com/docker/cli/opts"
|
||||||
"github.com/moby/moby/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
imagetypes "github.com/moby/moby/api/types/image"
|
imagetypes "github.com/docker/docker/api/types/image"
|
||||||
"github.com/moby/moby/api/types/mount"
|
"github.com/docker/docker/api/types/versions"
|
||||||
"github.com/moby/moby/api/types/versions"
|
"github.com/docker/docker/errdefs"
|
||||||
"github.com/moby/moby/client"
|
"github.com/docker/docker/pkg/jsonmessage"
|
||||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
specs "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
|
@ -48,7 +39,6 @@ type createOptions struct {
|
||||||
untrusted bool
|
untrusted bool
|
||||||
pull string // always, missing, never
|
pull string // always, missing, never
|
||||||
quiet bool
|
quiet bool
|
||||||
useAPISocket bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewCreateCommand creates a new cobra.Command for `docker create`
|
// NewCreateCommand creates a new cobra.Command for `docker create`
|
||||||
|
@ -70,7 +60,7 @@ func NewCreateCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
Annotations: map[string]string{
|
Annotations: map[string]string{
|
||||||
"aliases": "docker container create, docker create",
|
"aliases": "docker container create, docker create",
|
||||||
},
|
},
|
||||||
ValidArgsFunction: completion.ImageNames(dockerCli, -1),
|
ValidArgsFunction: completion.ImageNames(dockerCli),
|
||||||
}
|
}
|
||||||
|
|
||||||
flags := cmd.Flags()
|
flags := cmd.Flags()
|
||||||
|
@ -79,8 +69,6 @@ func NewCreateCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
flags.StringVar(&options.name, "name", "", "Assign a name to the container")
|
flags.StringVar(&options.name, "name", "", "Assign a name to the container")
|
||||||
flags.StringVar(&options.pull, "pull", PullImageMissing, `Pull image before creating ("`+PullImageAlways+`", "|`+PullImageMissing+`", "`+PullImageNever+`")`)
|
flags.StringVar(&options.pull, "pull", PullImageMissing, `Pull image before creating ("`+PullImageAlways+`", "|`+PullImageMissing+`", "`+PullImageNever+`")`)
|
||||||
flags.BoolVarP(&options.quiet, "quiet", "q", false, "Suppress the pull output")
|
flags.BoolVarP(&options.quiet, "quiet", "q", false, "Suppress the pull output")
|
||||||
flags.BoolVarP(&options.useAPISocket, "use-api-socket", "", false, "Bind mount Docker API socket and required auth")
|
|
||||||
flags.SetAnnotation("use-api-socket", "experimentalCLI", nil) // Marks flag as experimental for now.
|
|
||||||
|
|
||||||
// Add an explicit help that doesn't have a `-h` to prevent the conflict
|
// Add an explicit help that doesn't have a `-h` to prevent the conflict
|
||||||
// with hostname
|
// with hostname
|
||||||
|
@ -90,26 +78,24 @@ func NewCreateCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
command.AddTrustVerificationFlags(flags, &options.untrusted, dockerCli.ContentTrustEnabled())
|
command.AddTrustVerificationFlags(flags, &options.untrusted, dockerCli.ContentTrustEnabled())
|
||||||
copts = addFlags(flags)
|
copts = addFlags(flags)
|
||||||
|
|
||||||
addCompletions(cmd, dockerCli)
|
_ = cmd.RegisterFlagCompletionFunc("cap-add", completeLinuxCapabilityNames)
|
||||||
|
_ = cmd.RegisterFlagCompletionFunc("cap-drop", completeLinuxCapabilityNames)
|
||||||
flags.VisitAll(func(flag *pflag.Flag) {
|
_ = cmd.RegisterFlagCompletionFunc("env", completion.EnvVarNames)
|
||||||
// Set a default completion function if none was set. We don't look
|
_ = cmd.RegisterFlagCompletionFunc("env-file", completion.FileNames)
|
||||||
// up if it does already have one set, because Cobra does this for
|
_ = cmd.RegisterFlagCompletionFunc("network", completion.NetworkNames(dockerCli))
|
||||||
// us, and returns an error (which we ignore for this reason).
|
_ = cmd.RegisterFlagCompletionFunc("pull", completion.FromList(PullImageAlways, PullImageMissing, PullImageNever))
|
||||||
_ = cmd.RegisterFlagCompletionFunc(flag.Name, completion.NoComplete)
|
_ = cmd.RegisterFlagCompletionFunc("restart", completeRestartPolicies)
|
||||||
})
|
_ = cmd.RegisterFlagCompletionFunc("stop-signal", completeSignals)
|
||||||
|
_ = cmd.RegisterFlagCompletionFunc("volumes-from", completion.ContainerNames(dockerCli, true))
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func runCreate(ctx context.Context, dockerCli command.Cli, flags *pflag.FlagSet, options *createOptions, copts *containerOptions) error {
|
func runCreate(ctx context.Context, dockerCli command.Cli, flags *pflag.FlagSet, options *createOptions, copts *containerOptions) error {
|
||||||
if err := validatePullOpt(options.pull); err != nil {
|
if err := validatePullOpt(options.pull); err != nil {
|
||||||
return cli.StatusError{
|
reportError(dockerCli.Err(), "create", err.Error(), true)
|
||||||
Status: withHelp(err, "create").Error(),
|
return cli.StatusError{StatusCode: 125}
|
||||||
StatusCode: 125,
|
|
||||||
}
|
}
|
||||||
}
|
proxyConfig := dockerCli.ConfigFile().ParseProxyConfig(dockerCli.Client().DaemonHost(), opts.ConvertKVStringsToMapWithNil(copts.env.GetAll()))
|
||||||
proxyConfig := dockerCli.ConfigFile().ParseProxyConfig(dockerCli.Client().DaemonHost(), opts.ConvertKVStringsToMapWithNil(copts.env.GetSlice()))
|
|
||||||
newEnv := []string{}
|
newEnv := []string{}
|
||||||
for k, v := range proxyConfig {
|
for k, v := range proxyConfig {
|
||||||
if v == nil {
|
if v == nil {
|
||||||
|
@ -121,10 +107,12 @@ func runCreate(ctx context.Context, dockerCli command.Cli, flags *pflag.FlagSet,
|
||||||
copts.env = *opts.NewListOptsRef(&newEnv, nil)
|
copts.env = *opts.NewListOptsRef(&newEnv, nil)
|
||||||
containerCfg, err := parse(flags, copts, dockerCli.ServerInfo().OSType)
|
containerCfg, err := parse(flags, copts, dockerCli.ServerInfo().OSType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cli.StatusError{
|
reportError(dockerCli.Err(), "create", err.Error(), true)
|
||||||
Status: withHelp(err, "create").Error(),
|
return cli.StatusError{StatusCode: 125}
|
||||||
StatusCode: 125,
|
|
||||||
}
|
}
|
||||||
|
if err = validateAPIVersion(containerCfg, dockerCli.Client().ClientVersion()); err != nil {
|
||||||
|
reportError(dockerCli.Err(), "create", err.Error(), true)
|
||||||
|
return cli.StatusError{StatusCode: 125}
|
||||||
}
|
}
|
||||||
id, err := createContainer(ctx, dockerCli, containerCfg, options)
|
id, err := createContainer(ctx, dockerCli, containerCfg, options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -154,7 +142,7 @@ func pullImage(ctx context.Context, dockerCli command.Cli, img string, options *
|
||||||
if options.quiet {
|
if options.quiet {
|
||||||
out = streams.NewOut(io.Discard)
|
out = streams.NewOut(io.Discard)
|
||||||
}
|
}
|
||||||
return jsonstream.Display(ctx, responseBody, out)
|
return jsonmessage.DisplayJSONMessagesToStream(responseBody, out, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
type cidFile struct {
|
type cidFile struct {
|
||||||
|
@ -190,20 +178,20 @@ func (cid *cidFile) Write(id string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func newCIDFile(cidPath string) (*cidFile, error) {
|
func newCIDFile(path string) (*cidFile, error) {
|
||||||
if cidPath == "" {
|
if path == "" {
|
||||||
return &cidFile{}, nil
|
return &cidFile{}, nil
|
||||||
}
|
}
|
||||||
if _, err := os.Stat(cidPath); err == nil {
|
if _, err := os.Stat(path); err == nil {
|
||||||
return nil, errors.Errorf("container ID file found, make sure the other container isn't running or delete %s", cidPath)
|
return nil, errors.Errorf("container ID file found, make sure the other container isn't running or delete %s", path)
|
||||||
}
|
}
|
||||||
|
|
||||||
f, err := os.Create(cidPath)
|
f, err := os.Create(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "failed to create the container ID file")
|
return nil, errors.Wrap(err, "failed to create the container ID file")
|
||||||
}
|
}
|
||||||
|
|
||||||
return &cidFile{path: cidPath, file: f}, nil
|
return &cidFile{path: path, file: f}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
//nolint:gocyclo
|
//nolint:gocyclo
|
||||||
|
@ -212,6 +200,9 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *c
|
||||||
hostConfig := containerCfg.HostConfig
|
hostConfig := containerCfg.HostConfig
|
||||||
networkingConfig := containerCfg.NetworkingConfig
|
networkingConfig := containerCfg.NetworkingConfig
|
||||||
|
|
||||||
|
warnOnOomKillDisable(*hostConfig, dockerCli.Err())
|
||||||
|
warnOnLocalhostDNS(*hostConfig, dockerCli.Err())
|
||||||
|
|
||||||
var (
|
var (
|
||||||
trustedRef reference.Canonical
|
trustedRef reference.Canonical
|
||||||
namedRef reference.Named
|
namedRef reference.Named
|
||||||
|
@ -240,74 +231,17 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *c
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const dockerConfigPathInContainer = "/run/secrets/docker/config.json"
|
pullAndTagImage := func() error {
|
||||||
var apiSocketCreds map[string]types.AuthConfig
|
if err := pullImage(ctx, dockerCli, config.Image, options); err != nil {
|
||||||
|
return err
|
||||||
if options.useAPISocket {
|
}
|
||||||
// We'll create two new mounts to handle this flag:
|
if taggedRef, ok := namedRef.(reference.NamedTagged); ok && trustedRef != nil {
|
||||||
// 1. Mount the actual docker socket.
|
return image.TagTrusted(ctx, dockerCli, trustedRef, taggedRef)
|
||||||
// 2. A synthezised ~/.docker/config.json with resolved tokens.
|
}
|
||||||
|
return nil
|
||||||
if dockerCli.ServerInfo().OSType == "windows" {
|
|
||||||
return "", errors.New("flag --use-api-socket can't be used with a Windows Docker Engine")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// hard-code engine socket path until https://github.com/moby/moby/pull/43459 gives us a discovery mechanism
|
var platform *specs.Platform
|
||||||
containerCfg.HostConfig.Mounts = append(containerCfg.HostConfig.Mounts, mount.Mount{
|
|
||||||
Type: mount.TypeBind,
|
|
||||||
Source: "/var/run/docker.sock",
|
|
||||||
Target: "/var/run/docker.sock",
|
|
||||||
BindOptions: &mount.BindOptions{},
|
|
||||||
})
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
Ideally, we'd like to copy the config into a tmpfs but unfortunately,
|
|
||||||
the mounts won't be in place until we start the container. This can
|
|
||||||
leave around the config if the container doesn't get deleted.
|
|
||||||
|
|
||||||
We are using the most compose-secret-compatible approach,
|
|
||||||
which is implemented at
|
|
||||||
https://github.com/docker/compose/blob/main/pkg/compose/convergence.go#L737
|
|
||||||
|
|
||||||
// Prepare a tmpfs mount for our credentials so they go away after the
|
|
||||||
// container exits. We'll copy into this mount after the container is
|
|
||||||
// created.
|
|
||||||
containerCfg.HostConfig.Mounts = append(containerCfg.HostConfig.Mounts, mount.Mount{
|
|
||||||
Type: mount.TypeTmpfs,
|
|
||||||
Target: "/docker/",
|
|
||||||
TmpfsOptions: &mount.TmpfsOptions{
|
|
||||||
SizeBytes: 1 << 20, // only need a small partition
|
|
||||||
Mode: 0o600,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
*/
|
|
||||||
|
|
||||||
var envvarPresent bool
|
|
||||||
for _, envvar := range containerCfg.Config.Env {
|
|
||||||
if strings.HasPrefix(envvar, "DOCKER_CONFIG=") {
|
|
||||||
envvarPresent = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the DOCKER_CONFIG env var is already present, we assume the client knows
|
|
||||||
// what they're doing and don't inject the creds.
|
|
||||||
if !envvarPresent {
|
|
||||||
// Resolve this here for later, ensuring we error our before we create the container.
|
|
||||||
creds, err := readCredentials(dockerCli)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("resolving credentials failed: %w", err)
|
|
||||||
}
|
|
||||||
if len(creds) > 0 {
|
|
||||||
// Set our special little location for the config file.
|
|
||||||
containerCfg.Config.Env = append(containerCfg.Config.Env, "DOCKER_CONFIG="+path.Dir(dockerConfigPathInContainer))
|
|
||||||
|
|
||||||
apiSocketCreds = creds // inject these after container creation.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var platform *ocispec.Platform
|
|
||||||
// Engine API version 1.41 first introduced the option to specify platform on
|
// Engine API version 1.41 first introduced the option to specify platform on
|
||||||
// create. It will produce an error if you try to set a platform on older API
|
// create. It will produce an error if you try to set a platform on older API
|
||||||
// versions, so check the API version here to maintain backwards
|
// versions, so check the API version here to maintain backwards
|
||||||
|
@ -315,21 +249,11 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *c
|
||||||
if options.platform != "" && versions.GreaterThanOrEqualTo(dockerCli.Client().ClientVersion(), "1.41") {
|
if options.platform != "" && versions.GreaterThanOrEqualTo(dockerCli.Client().ClientVersion(), "1.41") {
|
||||||
p, err := platforms.Parse(options.platform)
|
p, err := platforms.Parse(options.platform)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", errors.Wrap(invalidParameter(err), "error parsing specified platform")
|
return "", errors.Wrap(errdefs.InvalidParameter(err), "error parsing specified platform")
|
||||||
}
|
}
|
||||||
platform = &p
|
platform = &p
|
||||||
}
|
}
|
||||||
|
|
||||||
pullAndTagImage := func() error {
|
|
||||||
if err := pullImage(ctx, dockerCli, config.Image, options); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if taggedRef, ok := namedRef.(reference.NamedTagged); ok && trustedRef != nil {
|
|
||||||
return trust.TagTrusted(ctx, dockerCli.Client(), dockerCli.Err(), trustedRef, taggedRef)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if options.pull == PullImageAlways {
|
if options.pull == PullImageAlways {
|
||||||
if err := pullAndTagImage(); err != nil {
|
if err := pullAndTagImage(); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
|
@ -341,10 +265,10 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *c
|
||||||
response, err := dockerCli.Client().ContainerCreate(ctx, config, hostConfig, networkingConfig, platform, options.name)
|
response, err := dockerCli.Client().ContainerCreate(ctx, config, hostConfig, networkingConfig, platform, options.name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Pull image if it does not exist locally and we have the PullImageMissing option. Default behavior.
|
// Pull image if it does not exist locally and we have the PullImageMissing option. Default behavior.
|
||||||
if cerrdefs.IsNotFound(err) && namedRef != nil && options.pull == PullImageMissing {
|
if errdefs.IsNotFound(err) && namedRef != nil && options.pull == PullImageMissing {
|
||||||
if !options.quiet {
|
if !options.quiet {
|
||||||
// we don't want to write to stdout anything apart from container.ID
|
// we don't want to write to stdout anything apart from container.ID
|
||||||
_, _ = fmt.Fprintf(dockerCli.Err(), "Unable to find image '%s' locally\n", reference.FamiliarString(namedRef))
|
fmt.Fprintf(dockerCli.Err(), "Unable to find image '%s' locally\n", reference.FamiliarString(namedRef))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := pullAndTagImage(); err != nil {
|
if err := pullAndTagImage(); err != nil {
|
||||||
|
@ -361,41 +285,40 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *c
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if warn := localhostDNSWarning(*hostConfig); warn != "" {
|
|
||||||
response.Warnings = append(response.Warnings, warn)
|
|
||||||
}
|
|
||||||
|
|
||||||
containerID = response.ID
|
|
||||||
for _, w := range response.Warnings {
|
for _, w := range response.Warnings {
|
||||||
_, _ = fmt.Fprintln(dockerCli.Err(), "WARNING:", w)
|
_, _ = fmt.Fprintf(dockerCli.Err(), "WARNING: %s\n", w)
|
||||||
}
|
}
|
||||||
err = containerIDFile.Write(containerID)
|
err = containerIDFile.Write(response.ID)
|
||||||
|
return response.ID, err
|
||||||
if options.useAPISocket && len(apiSocketCreds) > 0 {
|
|
||||||
// Create a new config file with just the auth.
|
|
||||||
newConfig := &configfile.ConfigFile{
|
|
||||||
AuthConfigs: apiSocketCreds,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := copyDockerConfigIntoContainer(ctx, dockerCli.Client(), containerID, dockerConfigPathInContainer, newConfig); err != nil {
|
func warnOnOomKillDisable(hostConfig container.HostConfig, stderr io.Writer) {
|
||||||
return "", fmt.Errorf("injecting docker config.json into container failed: %w", err)
|
if hostConfig.OomKillDisable != nil && *hostConfig.OomKillDisable && hostConfig.Memory == 0 {
|
||||||
|
fmt.Fprintln(stderr, "WARNING: Disabling the OOM killer on containers without setting a '-m/--memory' limit may be dangerous.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return containerID, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// check the DNS settings passed via --dns against localhost regexp to warn if
|
// check the DNS settings passed via --dns against localhost regexp to warn if
|
||||||
// they are trying to set a DNS to a localhost address.
|
// they are trying to set a DNS to a localhost address
|
||||||
//
|
func warnOnLocalhostDNS(hostConfig container.HostConfig, stderr io.Writer) {
|
||||||
// TODO(thaJeztah): move this to the daemon, which can make a better call if it will work or not (depending on networking mode).
|
|
||||||
func localhostDNSWarning(hostConfig container.HostConfig) string {
|
|
||||||
for _, dnsIP := range hostConfig.DNS {
|
for _, dnsIP := range hostConfig.DNS {
|
||||||
if addr, err := netip.ParseAddr(dnsIP); err == nil && addr.IsLoopback() {
|
if isLocalhost(dnsIP) {
|
||||||
return fmt.Sprintf("Localhost DNS (%s) may fail in containers.", addr)
|
fmt.Fprintf(stderr, "WARNING: Localhost DNS setting (--dns=%s) may fail in containers.\n", dnsIP)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ""
|
}
|
||||||
|
|
||||||
|
// IPLocalhost is a regex pattern for IPv4 or IPv6 loopback range.
|
||||||
|
const ipLocalhost = `((127\.([0-9]{1,3}\.){2}[0-9]{1,3})|(::1)$)`
|
||||||
|
|
||||||
|
var localhostIPRegexp = regexp.MustCompile(ipLocalhost)
|
||||||
|
|
||||||
|
// IsLocalhost returns true if ip matches the localhost IP regular expression.
|
||||||
|
// Used for determining if nameserver settings are being passed which are
|
||||||
|
// localhost addresses
|
||||||
|
func isLocalhost(ip string) bool {
|
||||||
|
return localhostIPRegexp.MatchString(ip)
|
||||||
}
|
}
|
||||||
|
|
||||||
func validatePullOpt(val string) error {
|
func validatePullOpt(val string) error {
|
||||||
|
@ -413,39 +336,3 @@ func validatePullOpt(val string) error {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// copyDockerConfigIntoContainer takes the client configuration and copies it
|
|
||||||
// into the container.
|
|
||||||
//
|
|
||||||
// The path should be an absolute path in the container, commonly
|
|
||||||
// /root/.docker/config.json.
|
|
||||||
func copyDockerConfigIntoContainer(ctx context.Context, dockerAPI client.APIClient, containerID string, configPath string, config *configfile.ConfigFile) error {
|
|
||||||
var configBuf bytes.Buffer
|
|
||||||
if err := config.SaveToWriter(&configBuf); err != nil {
|
|
||||||
return fmt.Errorf("saving creds: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// We don't need to get super fancy with the tar creation.
|
|
||||||
var tarBuf bytes.Buffer
|
|
||||||
tarWriter := tar.NewWriter(&tarBuf)
|
|
||||||
tarWriter.WriteHeader(&tar.Header{
|
|
||||||
Name: configPath,
|
|
||||||
Size: int64(configBuf.Len()),
|
|
||||||
Mode: 0o600,
|
|
||||||
})
|
|
||||||
|
|
||||||
if _, err := io.Copy(tarWriter, &configBuf); err != nil {
|
|
||||||
return fmt.Errorf("writing config to tar file for config copy: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := tarWriter.Close(); err != nil {
|
|
||||||
return fmt.Errorf("closing tar for config copy failed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := dockerAPI.CopyToContainer(ctx, containerID, "/",
|
|
||||||
&tarBuf, container.CopyToContainerOptions{}); err != nil {
|
|
||||||
return fmt.Errorf("copying config.json into container failed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
|
@ -14,12 +14,12 @@ 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/notary"
|
"github.com/docker/cli/internal/test/notary"
|
||||||
|
"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/system"
|
||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
"github.com/moby/moby/api/types/container"
|
specs "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
"github.com/moby/moby/api/types/image"
|
|
||||||
"github.com/moby/moby/api/types/network"
|
|
||||||
"github.com/moby/moby/api/types/system"
|
|
||||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
"gotest.tools/v3/assert"
|
"gotest.tools/v3/assert"
|
||||||
is "gotest.tools/v3/assert/cmp"
|
is "gotest.tools/v3/assert/cmp"
|
||||||
|
@ -113,6 +113,7 @@ func TestCreateContainerImagePullPolicy(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
|
tc := tc
|
||||||
t.Run(tc.PullPolicy, func(t *testing.T) {
|
t.Run(tc.PullPolicy, func(t *testing.T) {
|
||||||
pullCounter := 0
|
pullCounter := 0
|
||||||
|
|
||||||
|
@ -121,7 +122,7 @@ func TestCreateContainerImagePullPolicy(t *testing.T) {
|
||||||
config *container.Config,
|
config *container.Config,
|
||||||
hostConfig *container.HostConfig,
|
hostConfig *container.HostConfig,
|
||||||
networkingConfig *network.NetworkingConfig,
|
networkingConfig *network.NetworkingConfig,
|
||||||
platform *ocispec.Platform,
|
platform *specs.Platform,
|
||||||
containerName string,
|
containerName string,
|
||||||
) (container.CreateResponse, error) {
|
) (container.CreateResponse, error) {
|
||||||
defer func() { tc.ResponseCounter++ }()
|
defer func() { tc.ResponseCounter++ }()
|
||||||
|
@ -132,7 +133,7 @@ func TestCreateContainerImagePullPolicy(t *testing.T) {
|
||||||
return container.CreateResponse{ID: containerID}, nil
|
return container.CreateResponse{ID: containerID}, nil
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
imageCreateFunc: func(ctx context.Context, parentReference string, options image.CreateOptions) (io.ReadCloser, error) {
|
imageCreateFunc: func(parentReference string, options image.CreateOptions) (io.ReadCloser, error) {
|
||||||
defer func() { pullCounter++ }()
|
defer func() { pullCounter++ }()
|
||||||
return io.NopCloser(strings.NewReader("")), nil
|
return io.NopCloser(strings.NewReader("")), nil
|
||||||
},
|
},
|
||||||
|
@ -175,6 +176,7 @@ func TestCreateContainerImagePullPolicyInvalid(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
|
tc := tc
|
||||||
t.Run(tc.PullPolicy, func(t *testing.T) {
|
t.Run(tc.PullPolicy, func(t *testing.T) {
|
||||||
dockerCli := test.NewFakeCli(&fakeClient{})
|
dockerCli := test.NewFakeCli(&fakeClient{})
|
||||||
err := runCreate(
|
err := runCreate(
|
||||||
|
@ -187,36 +189,8 @@ func TestCreateContainerImagePullPolicyInvalid(t *testing.T) {
|
||||||
|
|
||||||
statusErr := cli.StatusError{}
|
statusErr := cli.StatusError{}
|
||||||
assert.Check(t, errors.As(err, &statusErr))
|
assert.Check(t, errors.As(err, &statusErr))
|
||||||
assert.Check(t, is.Equal(statusErr.StatusCode, 125))
|
assert.Equal(t, statusErr.StatusCode, 125)
|
||||||
assert.Check(t, is.ErrorContains(err, tc.ExpectedErrMsg))
|
assert.Check(t, is.Contains(dockerCli.ErrBuffer().String(), tc.ExpectedErrMsg))
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCreateContainerValidateFlags(t *testing.T) {
|
|
||||||
for _, tc := range []struct {
|
|
||||||
name string
|
|
||||||
args []string
|
|
||||||
expectedErr string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "with invalid --attach value",
|
|
||||||
args: []string{"--attach", "STDINFO", "myimage"},
|
|
||||||
expectedErr: `invalid argument "STDINFO" for "-a, --attach" flag: valid streams are STDIN, STDOUT and STDERR`,
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
cmd := NewCreateCommand(test.NewFakeCli(&fakeClient{}))
|
|
||||||
cmd.SetOut(io.Discard)
|
|
||||||
cmd.SetErr(io.Discard)
|
|
||||||
cmd.SetArgs(tc.args)
|
|
||||||
|
|
||||||
err := cmd.Execute()
|
|
||||||
if tc.expectedErr != "" {
|
|
||||||
assert.Check(t, is.ErrorContains(err, tc.expectedErr))
|
|
||||||
} else {
|
|
||||||
assert.Check(t, is.Nil(err))
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -248,12 +222,12 @@ func TestNewCreateCommandWithContentTrustErrors(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
tc := tc
|
||||||
fakeCLI := test.NewFakeCli(&fakeClient{
|
fakeCLI := test.NewFakeCli(&fakeClient{
|
||||||
createContainerFunc: func(config *container.Config,
|
createContainerFunc: func(config *container.Config,
|
||||||
hostConfig *container.HostConfig,
|
hostConfig *container.HostConfig,
|
||||||
networkingConfig *network.NetworkingConfig,
|
networkingConfig *network.NetworkingConfig,
|
||||||
platform *ocispec.Platform,
|
platform *specs.Platform,
|
||||||
containerName string,
|
containerName string,
|
||||||
) (container.CreateResponse, error) {
|
) (container.CreateResponse, error) {
|
||||||
return container.CreateResponse{}, errors.New("shouldn't try to pull image")
|
return container.CreateResponse{}, errors.New("shouldn't try to pull image")
|
||||||
|
@ -266,7 +240,6 @@ func TestNewCreateCommandWithContentTrustErrors(t *testing.T) {
|
||||||
cmd.SetArgs(tc.args)
|
cmd.SetArgs(tc.args)
|
||||||
err := cmd.Execute()
|
err := cmd.Execute()
|
||||||
assert.ErrorContains(t, err, tc.expectedError)
|
assert.ErrorContains(t, err, tc.expectedError)
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -274,22 +247,29 @@ func TestNewCreateCommandWithWarnings(t *testing.T) {
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
args []string
|
args []string
|
||||||
warnings []string
|
|
||||||
warning bool
|
warning bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "container-create-no-warnings",
|
name: "container-create-without-oom-kill-disable",
|
||||||
args: []string{"image:tag"},
|
args: []string{"image:tag"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "container-create-daemon-single-warning",
|
name: "container-create-oom-kill-disable-false",
|
||||||
args: []string{"image:tag"},
|
args: []string{"--oom-kill-disable=false", "image:tag"},
|
||||||
warnings: []string{"warning from daemon"},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "container-create-daemon-multiple-warnings",
|
name: "container-create-oom-kill-without-memory-limit",
|
||||||
args: []string{"image:tag"},
|
args: []string{"--oom-kill-disable", "image:tag"},
|
||||||
warnings: []string{"warning from daemon", "another warning from daemon"},
|
warning: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "container-create-oom-kill-true-without-memory-limit",
|
||||||
|
args: []string{"--oom-kill-disable=true", "image:tag"},
|
||||||
|
warning: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "container-create-oom-kill-true-with-memory-limit",
|
||||||
|
args: []string{"--oom-kill-disable=true", "--memory=100M", "image:tag"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "container-create-localhost-dns",
|
name: "container-create-localhost-dns",
|
||||||
|
@ -303,26 +283,27 @@ func TestNewCreateCommandWithWarnings(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
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) {
|
||||||
fakeCLI := test.NewFakeCli(&fakeClient{
|
cli := test.NewFakeCli(&fakeClient{
|
||||||
createContainerFunc: func(config *container.Config,
|
createContainerFunc: func(config *container.Config,
|
||||||
hostConfig *container.HostConfig,
|
hostConfig *container.HostConfig,
|
||||||
networkingConfig *network.NetworkingConfig,
|
networkingConfig *network.NetworkingConfig,
|
||||||
platform *ocispec.Platform,
|
platform *specs.Platform,
|
||||||
containerName string,
|
containerName string,
|
||||||
) (container.CreateResponse, error) {
|
) (container.CreateResponse, error) {
|
||||||
return container.CreateResponse{Warnings: tc.warnings}, nil
|
return container.CreateResponse{}, nil
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
cmd := NewCreateCommand(fakeCLI)
|
cmd := NewCreateCommand(cli)
|
||||||
cmd.SetOut(io.Discard)
|
cmd.SetOut(io.Discard)
|
||||||
cmd.SetArgs(tc.args)
|
cmd.SetArgs(tc.args)
|
||||||
err := cmd.Execute()
|
err := cmd.Execute()
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
if tc.warning || len(tc.warnings) > 0 {
|
if tc.warning {
|
||||||
golden.Assert(t, fakeCLI.ErrBuffer().String(), tc.name+".golden")
|
golden.Assert(t, cli.ErrBuffer().String(), tc.name+".golden")
|
||||||
} else {
|
} else {
|
||||||
assert.Equal(t, fakeCLI.ErrBuffer().String(), "")
|
assert.Equal(t, cli.ErrBuffer().String(), "")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -347,7 +328,7 @@ func TestCreateContainerWithProxyConfig(t *testing.T) {
|
||||||
createContainerFunc: func(config *container.Config,
|
createContainerFunc: func(config *container.Config,
|
||||||
hostConfig *container.HostConfig,
|
hostConfig *container.HostConfig,
|
||||||
networkingConfig *network.NetworkingConfig,
|
networkingConfig *network.NetworkingConfig,
|
||||||
platform *ocispec.Platform,
|
platform *specs.Platform,
|
||||||
containerName string,
|
containerName string,
|
||||||
) (container.CreateResponse, error) {
|
) (container.CreateResponse, error) {
|
||||||
sort.Strings(config.Env)
|
sort.Strings(config.Env)
|
||||||
|
@ -375,5 +356,5 @@ func TestCreateContainerWithProxyConfig(t *testing.T) {
|
||||||
|
|
||||||
type fakeNotFound struct{}
|
type fakeNotFound struct{}
|
||||||
|
|
||||||
func (fakeNotFound) NotFound() {}
|
func (f fakeNotFound) NotFound() {}
|
||||||
func (fakeNotFound) Error() string { return "error fake not found" }
|
func (f fakeNotFound) Error() string { return "error fake not found" }
|
||||||
|
|
|
@ -7,17 +7,25 @@ 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/pkg/errors"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type diffOptions struct {
|
||||||
|
container string
|
||||||
|
}
|
||||||
|
|
||||||
// NewDiffCommand creates a new cobra.Command for `docker diff`
|
// NewDiffCommand creates a new cobra.Command for `docker diff`
|
||||||
func NewDiffCommand(dockerCli command.Cli) *cobra.Command {
|
func NewDiffCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
|
var opts diffOptions
|
||||||
|
|
||||||
return &cobra.Command{
|
return &cobra.Command{
|
||||||
Use: "diff CONTAINER",
|
Use: "diff CONTAINER",
|
||||||
Short: "Inspect changes to files or directories on a container's filesystem",
|
Short: "Inspect changes to files or directories on a container's filesystem",
|
||||||
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 runDiff(cmd.Context(), dockerCli, args[0])
|
opts.container = args[0]
|
||||||
|
return runDiff(cmd.Context(), dockerCli, &opts)
|
||||||
},
|
},
|
||||||
Annotations: map[string]string{
|
Annotations: map[string]string{
|
||||||
"aliases": "docker container diff, docker diff",
|
"aliases": "docker container diff, docker diff",
|
||||||
|
@ -26,14 +34,17 @@ func NewDiffCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func runDiff(ctx context.Context, dockerCLI command.Cli, containerID string) error {
|
func runDiff(ctx context.Context, dockerCli command.Cli, opts *diffOptions) error {
|
||||||
changes, err := dockerCLI.Client().ContainerDiff(ctx, containerID)
|
if opts.container == "" {
|
||||||
|
return errors.New("Container name cannot be empty")
|
||||||
|
}
|
||||||
|
changes, err := dockerCli.Client().ContainerDiff(ctx, opts.container)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
diffCtx := formatter.Context{
|
diffCtx := formatter.Context{
|
||||||
Output: dockerCLI.Out(),
|
Output: dockerCli.Out(),
|
||||||
Format: newDiffFormat("{{.Type}} {{.Path}}"),
|
Format: NewDiffFormat("{{.Type}} {{.Path}}"),
|
||||||
}
|
}
|
||||||
return diffFormatWrite(diffCtx, changes)
|
return DiffFormatWrite(diffCtx, changes)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,79 +0,0 @@
|
||||||
package container
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/docker/cli/internal/test"
|
|
||||||
"github.com/moby/moby/api/types/container"
|
|
||||||
"gotest.tools/v3/assert"
|
|
||||||
is "gotest.tools/v3/assert/cmp"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestRunDiff(t *testing.T) {
|
|
||||||
cli := test.NewFakeCli(&fakeClient{
|
|
||||||
containerDiffFunc: func(
|
|
||||||
ctx context.Context,
|
|
||||||
containerID string,
|
|
||||||
) ([]container.FilesystemChange, error) {
|
|
||||||
return []container.FilesystemChange{
|
|
||||||
{
|
|
||||||
Kind: container.ChangeModify,
|
|
||||||
Path: "/path/to/file0",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Kind: container.ChangeAdd,
|
|
||||||
Path: "/path/to/file1",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Kind: container.ChangeDelete,
|
|
||||||
Path: "/path/to/file2",
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
cmd := NewDiffCommand(cli)
|
|
||||||
cmd.SetOut(io.Discard)
|
|
||||||
|
|
||||||
cmd.SetArgs([]string{"container-id"})
|
|
||||||
|
|
||||||
err := cmd.Execute()
|
|
||||||
assert.NilError(t, err)
|
|
||||||
|
|
||||||
diff := strings.SplitN(cli.OutBuffer().String(), "\n", 3)
|
|
||||||
assert.Assert(t, is.Len(diff, 3))
|
|
||||||
|
|
||||||
file0 := strings.TrimSpace(diff[0])
|
|
||||||
file1 := strings.TrimSpace(diff[1])
|
|
||||||
file2 := strings.TrimSpace(diff[2])
|
|
||||||
|
|
||||||
assert.Check(t, is.Equal(file0, "C /path/to/file0"))
|
|
||||||
assert.Check(t, is.Equal(file1, "A /path/to/file1"))
|
|
||||||
assert.Check(t, is.Equal(file2, "D /path/to/file2"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRunDiffClientError(t *testing.T) {
|
|
||||||
clientError := errors.New("client error")
|
|
||||||
|
|
||||||
cli := test.NewFakeCli(&fakeClient{
|
|
||||||
containerDiffFunc: func(
|
|
||||||
ctx context.Context,
|
|
||||||
containerID string,
|
|
||||||
) ([]container.FilesystemChange, error) {
|
|
||||||
return nil, clientError
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
cmd := NewDiffCommand(cli)
|
|
||||||
cmd.SetOut(io.Discard)
|
|
||||||
cmd.SetErr(io.Discard)
|
|
||||||
|
|
||||||
cmd.SetArgs([]string{"container-id"})
|
|
||||||
|
|
||||||
err := cmd.Execute()
|
|
||||||
assert.ErrorIs(t, err, clientError)
|
|
||||||
}
|
|
|
@ -1,31 +0,0 @@
|
||||||
package container
|
|
||||||
|
|
||||||
import cerrdefs "github.com/containerd/errdefs"
|
|
||||||
|
|
||||||
func invalidParameter(err error) error {
|
|
||||||
if err == nil || cerrdefs.IsInvalidArgument(err) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return invalidParameterErr{err}
|
|
||||||
}
|
|
||||||
|
|
||||||
type invalidParameterErr struct{ error }
|
|
||||||
|
|
||||||
func (invalidParameterErr) InvalidParameter() {}
|
|
||||||
func (e invalidParameterErr) Unwrap() error {
|
|
||||||
return e.error
|
|
||||||
}
|
|
||||||
|
|
||||||
func notFound(err error) error {
|
|
||||||
if err == nil || cerrdefs.IsNotFound(err) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return notFoundErr{err}
|
|
||||||
}
|
|
||||||
|
|
||||||
type notFoundErr struct{ error }
|
|
||||||
|
|
||||||
func (notFoundErr) NotFound() {}
|
|
||||||
func (e notFoundErr) Unwrap() error {
|
|
||||||
return e.error
|
|
||||||
}
|
|
|
@ -10,8 +10,9 @@ import (
|
||||||
"github.com/docker/cli/cli/command/completion"
|
"github.com/docker/cli/cli/command/completion"
|
||||||
"github.com/docker/cli/cli/config/configfile"
|
"github.com/docker/cli/cli/config/configfile"
|
||||||
"github.com/docker/cli/opts"
|
"github.com/docker/cli/opts"
|
||||||
"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/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
@ -52,8 +53,8 @@ func NewExecCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
options.Command = args[1:]
|
options.Command = args[1:]
|
||||||
return RunExec(cmd.Context(), dockerCli, containerIDorName, options)
|
return RunExec(cmd.Context(), dockerCli, containerIDorName, options)
|
||||||
},
|
},
|
||||||
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"
|
||||||
}),
|
}),
|
||||||
Annotations: map[string]string{
|
Annotations: map[string]string{
|
||||||
"category-top": "2",
|
"category-top": "2",
|
||||||
|
@ -84,13 +85,13 @@ func NewExecCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
// RunExec executes an `exec` command
|
// RunExec executes an `exec` command
|
||||||
func RunExec(ctx context.Context, dockerCLI command.Cli, containerIDorName string, options ExecOptions) error {
|
func RunExec(ctx context.Context, dockerCli command.Cli, containerIDorName string, options ExecOptions) error {
|
||||||
execOptions, err := parseExec(options, dockerCLI.ConfigFile())
|
execOptions, err := parseExec(options, dockerCli.ConfigFile())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
apiClient := dockerCLI.Client()
|
apiClient := dockerCli.Client()
|
||||||
|
|
||||||
// We need to check the tty _before_ we do the ContainerExecCreate, because
|
// We need to check the tty _before_ we do the ContainerExecCreate, because
|
||||||
// otherwise if we error out we will leak execIDs on the server (and
|
// otherwise if we error out we will leak execIDs on the server (and
|
||||||
|
@ -99,13 +100,13 @@ func RunExec(ctx context.Context, dockerCLI command.Cli, containerIDorName strin
|
||||||
if _, err := apiClient.ContainerInspect(ctx, containerIDorName); err != nil {
|
if _, err := apiClient.ContainerInspect(ctx, containerIDorName); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if !options.Detach {
|
if !execOptions.Detach {
|
||||||
if err := dockerCLI.In().CheckTty(execOptions.AttachStdin, execOptions.Tty); err != nil {
|
if err := dockerCli.In().CheckTty(execOptions.AttachStdin, execOptions.Tty); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fillConsoleSize(execOptions, dockerCLI)
|
fillConsoleSize(execOptions, dockerCli)
|
||||||
|
|
||||||
response, err := apiClient.ContainerExecCreate(ctx, containerIDorName, *execOptions)
|
response, err := apiClient.ContainerExecCreate(ctx, containerIDorName, *execOptions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -117,14 +118,14 @@ func RunExec(ctx context.Context, dockerCLI command.Cli, containerIDorName strin
|
||||||
return errors.New("exec ID empty")
|
return errors.New("exec ID empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
if options.Detach {
|
if execOptions.Detach {
|
||||||
return apiClient.ContainerExecStart(ctx, execID, container.ExecStartOptions{
|
return apiClient.ContainerExecStart(ctx, execID, container.ExecStartOptions{
|
||||||
Detach: options.Detach,
|
Detach: execOptions.Detach,
|
||||||
Tty: execOptions.Tty,
|
Tty: execOptions.Tty,
|
||||||
ConsoleSize: execOptions.ConsoleSize,
|
ConsoleSize: execOptions.ConsoleSize,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return interactiveExec(ctx, dockerCLI, execOptions, execID)
|
return interactiveExec(ctx, dockerCli, execOptions, execID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func fillConsoleSize(execOptions *container.ExecOptions, dockerCli command.Cli) {
|
func fillConsoleSize(execOptions *container.ExecOptions, dockerCli command.Cli) {
|
||||||
|
@ -223,12 +224,13 @@ func parseExec(execOpts ExecOptions, configFile *configfile.ConfigFile) (*contai
|
||||||
Privileged: execOpts.Privileged,
|
Privileged: execOpts.Privileged,
|
||||||
Tty: execOpts.TTY,
|
Tty: execOpts.TTY,
|
||||||
Cmd: execOpts.Command,
|
Cmd: execOpts.Command,
|
||||||
|
Detach: execOpts.Detach,
|
||||||
WorkingDir: execOpts.Workdir,
|
WorkingDir: execOpts.Workdir,
|
||||||
}
|
}
|
||||||
|
|
||||||
// collect all the environment variables for the container
|
// collect all the environment variables for the container
|
||||||
var err error
|
var err error
|
||||||
if execOptions.Env, err = opts.ReadKVEnvStrings(execOpts.EnvFile.GetSlice(), execOpts.Env.GetSlice()); err != nil {
|
if execOptions.Env, err = opts.ReadKVEnvStrings(execOpts.EnvFile.GetAll(), execOpts.Env.GetAll()); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,17 +2,17 @@ package container
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/docker/cli/cli"
|
"github.com/docker/cli/cli"
|
||||||
"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/opts"
|
"github.com/docker/cli/opts"
|
||||||
"github.com/moby/moby/api/types/container"
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/api/types/container"
|
||||||
|
"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/fs"
|
"gotest.tools/v3/fs"
|
||||||
|
@ -76,6 +76,7 @@ TWO=2
|
||||||
{
|
{
|
||||||
options: withDefaultOpts(ExecOptions{Detach: true}),
|
options: withDefaultOpts(ExecOptions{Detach: true}),
|
||||||
expected: container.ExecOptions{
|
expected: container.ExecOptions{
|
||||||
|
Detach: true,
|
||||||
Cmd: []string{"command"},
|
Cmd: []string{"command"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -86,6 +87,7 @@ TWO=2
|
||||||
Detach: true,
|
Detach: true,
|
||||||
}),
|
}),
|
||||||
expected: container.ExecOptions{
|
expected: container.ExecOptions{
|
||||||
|
Detach: true,
|
||||||
Tty: true,
|
Tty: true,
|
||||||
Cmd: []string{"command"},
|
Cmd: []string{"command"},
|
||||||
},
|
},
|
||||||
|
@ -96,6 +98,7 @@ TWO=2
|
||||||
expected: container.ExecOptions{
|
expected: container.ExecOptions{
|
||||||
Cmd: []string{"command"},
|
Cmd: []string{"command"},
|
||||||
DetachKeys: "de",
|
DetachKeys: "de",
|
||||||
|
Detach: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -107,6 +110,7 @@ TWO=2
|
||||||
expected: container.ExecOptions{
|
expected: container.ExecOptions{
|
||||||
Cmd: []string{"command"},
|
Cmd: []string{"command"},
|
||||||
DetachKeys: "ab",
|
DetachKeys: "ab",
|
||||||
|
Detach: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -138,12 +142,10 @@ TWO=2
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, testcase := range testcases {
|
for _, testcase := range testcases {
|
||||||
t.Run("test "+strconv.Itoa(i+1), func(t *testing.T) {
|
|
||||||
execConfig, err := parseExec(testcase.options, &testcase.configFile)
|
execConfig, err := parseExec(testcase.options, &testcase.configFile)
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
assert.Check(t, is.DeepEqual(testcase.expected, *execConfig))
|
assert.Check(t, is.DeepEqual(testcase.expected, *execConfig))
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -176,8 +178,8 @@ func TestRunExec(t *testing.T) {
|
||||||
doc: "inspect error",
|
doc: "inspect error",
|
||||||
options: NewExecOptions(),
|
options: NewExecOptions(),
|
||||||
client: &fakeClient{
|
client: &fakeClient{
|
||||||
inspectFunc: func(string) (container.InspectResponse, error) {
|
inspectFunc: func(string) (types.ContainerJSON, error) {
|
||||||
return container.InspectResponse{}, errors.New("failed inspect")
|
return types.ContainerJSON{}, errors.New("failed inspect")
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expectedError: "failed inspect",
|
expectedError: "failed inspect",
|
||||||
|
@ -206,8 +208,8 @@ func TestRunExec(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func execCreateWithID(_ string, _ container.ExecOptions) (container.ExecCreateResponse, error) {
|
func execCreateWithID(_ string, _ container.ExecOptions) (types.IDResponse, error) {
|
||||||
return container.ExecCreateResponse{ID: "execid"}, nil
|
return types.IDResponse{ID: "execid"}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetExecExitStatus(t *testing.T) {
|
func TestGetExecExitStatus(t *testing.T) {
|
||||||
|
@ -250,14 +252,14 @@ func TestNewExecCommandErrors(t *testing.T) {
|
||||||
name string
|
name string
|
||||||
args []string
|
args []string
|
||||||
expectedError string
|
expectedError string
|
||||||
containerInspectFunc func(img string) (container.InspectResponse, error)
|
containerInspectFunc func(img string) (types.ContainerJSON, error)
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "client-error",
|
name: "client-error",
|
||||||
args: []string{"5cb5bb5e4a3b", "-t", "-i", "bash"},
|
args: []string{"5cb5bb5e4a3b", "-t", "-i", "bash"},
|
||||||
expectedError: "something went wrong",
|
expectedError: "something went wrong",
|
||||||
containerInspectFunc: func(containerID string) (container.InspectResponse, error) {
|
containerInspectFunc: func(containerID string) (types.ContainerJSON, error) {
|
||||||
return container.InspectResponse{}, errors.New("something went wrong")
|
return types.ContainerJSON{}, errors.Errorf("something went wrong")
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,6 @@ 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/moby/sys/atomicwriter"
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
@ -42,28 +41,27 @@ func NewExportCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func runExport(ctx context.Context, dockerCLI command.Cli, opts exportOptions) error {
|
func runExport(ctx context.Context, dockerCli command.Cli, opts exportOptions) error {
|
||||||
var output io.Writer
|
if opts.output == "" && dockerCli.Out().IsTerminal() {
|
||||||
if opts.output == "" {
|
|
||||||
if dockerCLI.Out().IsTerminal() {
|
|
||||||
return errors.New("cowardly refusing to save to a terminal. Use the -o flag or redirect")
|
return errors.New("cowardly refusing to save to a terminal. Use the -o flag or redirect")
|
||||||
}
|
}
|
||||||
output = dockerCLI.Out()
|
|
||||||
} else {
|
if err := command.ValidateOutputPath(opts.output); err != nil {
|
||||||
writer, err := atomicwriter.New(opts.output, 0o600)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "failed to export container")
|
return errors.Wrap(err, "failed to export container")
|
||||||
}
|
}
|
||||||
defer writer.Close()
|
|
||||||
output = writer
|
|
||||||
}
|
|
||||||
|
|
||||||
responseBody, err := dockerCLI.Client().ContainerExport(ctx, opts.container)
|
clnt := dockerCli.Client()
|
||||||
|
|
||||||
|
responseBody, err := clnt.ContainerExport(ctx, opts.container)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer responseBody.Close()
|
defer responseBody.Close()
|
||||||
|
|
||||||
_, err = io.Copy(output, responseBody)
|
if opts.output == "" {
|
||||||
|
_, err := io.Copy(dockerCli.Out(), responseBody)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return command.CopyToFile(opts.output, responseBody)
|
||||||
|
}
|
||||||
|
|
|
@ -42,6 +42,8 @@ func TestContainerExportOutputToIrregularFile(t *testing.T) {
|
||||||
cmd.SetErr(io.Discard)
|
cmd.SetErr(io.Discard)
|
||||||
cmd.SetArgs([]string{"-o", "/dev/random", "container"})
|
cmd.SetArgs([]string{"-o", "/dev/random", "container"})
|
||||||
|
|
||||||
const expected = `failed to export container: cannot write to a character device file`
|
err := cmd.Execute()
|
||||||
assert.Error(t, cmd.Execute(), expected)
|
assert.Assert(t, err != nil)
|
||||||
|
expected := `"/dev/random" must be a directory or a regular file`
|
||||||
|
assert.ErrorContains(t, err, expected)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ package container
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/docker/cli/cli/command/formatter"
|
"github.com/docker/cli/cli/command/formatter"
|
||||||
"github.com/moby/moby/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -13,14 +13,7 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewDiffFormat returns a format for use with a diff Context
|
// NewDiffFormat returns a format for use with a diff Context
|
||||||
//
|
|
||||||
// Deprecated: this function was only used internally and will be removed in the next release.
|
|
||||||
func NewDiffFormat(source string) formatter.Format {
|
func NewDiffFormat(source string) formatter.Format {
|
||||||
return newDiffFormat(source)
|
|
||||||
}
|
|
||||||
|
|
||||||
// newDiffFormat returns a format for use with a diff [formatter.Context].
|
|
||||||
func newDiffFormat(source string) formatter.Format {
|
|
||||||
if source == formatter.TableFormatKey {
|
if source == formatter.TableFormatKey {
|
||||||
return defaultDiffTableFormat
|
return defaultDiffTableFormat
|
||||||
}
|
}
|
||||||
|
@ -28,22 +21,16 @@ func newDiffFormat(source string) formatter.Format {
|
||||||
}
|
}
|
||||||
|
|
||||||
// DiffFormatWrite writes formatted diff using the Context
|
// DiffFormatWrite writes formatted diff using the Context
|
||||||
//
|
func DiffFormatWrite(ctx formatter.Context, changes []container.FilesystemChange) error {
|
||||||
// Deprecated: this function was only used internally and will be removed in the next release.
|
render := func(format func(subContext formatter.SubContext) error) error {
|
||||||
func DiffFormatWrite(fmtCtx formatter.Context, changes []container.FilesystemChange) error {
|
|
||||||
return diffFormatWrite(fmtCtx, changes)
|
|
||||||
}
|
|
||||||
|
|
||||||
// diffFormatWrite writes formatted diff using the [formatter.Context].
|
|
||||||
func diffFormatWrite(fmtCtx formatter.Context, changes []container.FilesystemChange) error {
|
|
||||||
return fmtCtx.Write(newDiffContext(), func(format func(subContext formatter.SubContext) error) error {
|
|
||||||
for _, change := range changes {
|
for _, change := range changes {
|
||||||
if err := format(&diffContext{c: change}); err != nil {
|
if err := format(&diffContext{c: change}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
}
|
||||||
|
return ctx.Write(newDiffContext(), render)
|
||||||
}
|
}
|
||||||
|
|
||||||
type diffContext struct {
|
type diffContext struct {
|
||||||
|
@ -52,14 +39,12 @@ type diffContext struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func newDiffContext() *diffContext {
|
func newDiffContext() *diffContext {
|
||||||
return &diffContext{
|
diffCtx := diffContext{}
|
||||||
HeaderContext: formatter.HeaderContext{
|
diffCtx.Header = formatter.SubHeaderContext{
|
||||||
Header: formatter.SubHeaderContext{
|
|
||||||
"Type": changeTypeHeader,
|
"Type": changeTypeHeader,
|
||||||
"Path": pathHeader,
|
"Path": pathHeader,
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
return &diffCtx
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *diffContext) MarshalJSON() ([]byte, error) {
|
func (d *diffContext) MarshalJSON() ([]byte, error) {
|
||||||
|
|
|
@ -5,7 +5,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/docker/cli/cli/command/formatter"
|
"github.com/docker/cli/cli/command/formatter"
|
||||||
"github.com/moby/moby/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
"gotest.tools/v3/assert"
|
"gotest.tools/v3/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@ func TestDiffContextFormatWrite(t *testing.T) {
|
||||||
expected string
|
expected string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
formatter.Context{Format: newDiffFormat("table")},
|
formatter.Context{Format: NewDiffFormat("table")},
|
||||||
`CHANGE TYPE PATH
|
`CHANGE TYPE PATH
|
||||||
C /var/log/app.log
|
C /var/log/app.log
|
||||||
A /usr/app/app.js
|
A /usr/app/app.js
|
||||||
|
@ -24,7 +24,7 @@ D /usr/app/old_app.js
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
formatter.Context{Format: newDiffFormat("table {{.Path}}")},
|
formatter.Context{Format: NewDiffFormat("table {{.Path}}")},
|
||||||
`PATH
|
`PATH
|
||||||
/var/log/app.log
|
/var/log/app.log
|
||||||
/usr/app/app.js
|
/usr/app/app.js
|
||||||
|
@ -32,7 +32,7 @@ D /usr/app/old_app.js
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
formatter.Context{Format: newDiffFormat("{{.Type}}: {{.Path}}")},
|
formatter.Context{Format: NewDiffFormat("{{.Type}}: {{.Path}}")},
|
||||||
`C: /var/log/app.log
|
`C: /var/log/app.log
|
||||||
A: /usr/app/app.js
|
A: /usr/app/app.js
|
||||||
D: /usr/app/old_app.js
|
D: /usr/app/old_app.js
|
||||||
|
@ -47,10 +47,11 @@ D: /usr/app/old_app.js
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
out := bytes.NewBufferString("")
|
out := bytes.NewBufferString("")
|
||||||
tc.context.Output = out
|
tc.context.Output = out
|
||||||
err := diffFormatWrite(tc.context, diffs)
|
err := DiffFormatWrite(tc.context, diffs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
assert.Error(t, err, tc.expected)
|
assert.Error(t, err, tc.expected)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -5,7 +5,8 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/docker/cli/cli/command/formatter"
|
"github.com/docker/cli/cli/command/formatter"
|
||||||
"github.com/docker/go-units"
|
"github.com/docker/docker/pkg/stringid"
|
||||||
|
units "github.com/docker/go-units"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -21,8 +22,6 @@ const (
|
||||||
winMemUseHeader = "PRIV WORKING SET" // Used only on Windows
|
winMemUseHeader = "PRIV WORKING SET" // Used only on Windows
|
||||||
memUseHeader = "MEM USAGE / LIMIT" // Used only on Linux
|
memUseHeader = "MEM USAGE / LIMIT" // Used only on Linux
|
||||||
pidsHeader = "PIDS" // Used only on Linux
|
pidsHeader = "PIDS" // Used only on Linux
|
||||||
|
|
||||||
noValue = "--"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// StatsEntry represents the statistics data collected from a container
|
// StatsEntry represents the statistics data collected from a container
|
||||||
|
@ -170,19 +169,19 @@ func (c *statsContext) Name() string {
|
||||||
if len(c.s.Name) > 1 {
|
if len(c.s.Name) > 1 {
|
||||||
return c.s.Name[1:]
|
return c.s.Name[1:]
|
||||||
}
|
}
|
||||||
return noValue
|
return "--"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *statsContext) ID() string {
|
func (c *statsContext) ID() string {
|
||||||
if c.trunc {
|
if c.trunc {
|
||||||
return formatter.TruncateID(c.s.ID)
|
return stringid.TruncateID(c.s.ID)
|
||||||
}
|
}
|
||||||
return c.s.ID
|
return c.s.ID
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *statsContext) CPUPerc() string {
|
func (c *statsContext) CPUPerc() string {
|
||||||
if c.s.IsInvalid {
|
if c.s.IsInvalid {
|
||||||
return noValue
|
return "--"
|
||||||
}
|
}
|
||||||
return formatPercentage(c.s.CPUPercentage)
|
return formatPercentage(c.s.CPUPercentage)
|
||||||
}
|
}
|
||||||
|
@ -199,28 +198,28 @@ func (c *statsContext) MemUsage() string {
|
||||||
|
|
||||||
func (c *statsContext) MemPerc() string {
|
func (c *statsContext) MemPerc() string {
|
||||||
if c.s.IsInvalid || c.os == winOSType {
|
if c.s.IsInvalid || c.os == winOSType {
|
||||||
return noValue
|
return "--"
|
||||||
}
|
}
|
||||||
return formatPercentage(c.s.MemoryPercentage)
|
return formatPercentage(c.s.MemoryPercentage)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *statsContext) NetIO() string {
|
func (c *statsContext) NetIO() string {
|
||||||
if c.s.IsInvalid {
|
if c.s.IsInvalid {
|
||||||
return noValue
|
return "--"
|
||||||
}
|
}
|
||||||
return units.HumanSizeWithPrecision(c.s.NetworkRx, 3) + " / " + units.HumanSizeWithPrecision(c.s.NetworkTx, 3)
|
return units.HumanSizeWithPrecision(c.s.NetworkRx, 3) + " / " + units.HumanSizeWithPrecision(c.s.NetworkTx, 3)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *statsContext) BlockIO() string {
|
func (c *statsContext) BlockIO() string {
|
||||||
if c.s.IsInvalid {
|
if c.s.IsInvalid {
|
||||||
return noValue
|
return "--"
|
||||||
}
|
}
|
||||||
return units.HumanSizeWithPrecision(c.s.BlockRead, 3) + " / " + units.HumanSizeWithPrecision(c.s.BlockWrite, 3)
|
return units.HumanSizeWithPrecision(c.s.BlockRead, 3) + " / " + units.HumanSizeWithPrecision(c.s.BlockWrite, 3)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *statsContext) PIDs() string {
|
func (c *statsContext) PIDs() string {
|
||||||
if c.s.IsInvalid || c.os == winOSType {
|
if c.s.IsInvalid || c.os == winOSType {
|
||||||
return noValue
|
return "--"
|
||||||
}
|
}
|
||||||
return strconv.FormatUint(c.s.PidsCurrent, 10)
|
return strconv.FormatUint(c.s.PidsCurrent, 10)
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,13 +5,13 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/docker/cli/cli/command/formatter"
|
"github.com/docker/cli/cli/command/formatter"
|
||||||
"github.com/docker/cli/internal/test"
|
"github.com/docker/docker/pkg/stringid"
|
||||||
"gotest.tools/v3/assert"
|
"gotest.tools/v3/assert"
|
||||||
is "gotest.tools/v3/assert/cmp"
|
is "gotest.tools/v3/assert/cmp"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestContainerStatsContext(t *testing.T) {
|
func TestContainerStatsContext(t *testing.T) {
|
||||||
containerID := test.RandomID()
|
containerID := stringid.GenerateRandomID()
|
||||||
|
|
||||||
var ctx statsContext
|
var ctx statsContext
|
||||||
tt := []struct {
|
tt := []struct {
|
||||||
|
@ -148,7 +148,7 @@ container2 -- --
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
entries := []StatsEntry{
|
stats := []StatsEntry{
|
||||||
{
|
{
|
||||||
Container: "container1",
|
Container: "container1",
|
||||||
CPUPercentage: 20,
|
CPUPercentage: 20,
|
||||||
|
@ -178,10 +178,11 @@ container2 -- --
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
err := statsFormatWrite(tc.context, entries, "windows", false)
|
err := statsFormatWrite(tc.context, stats, "windows", false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
assert.Error(t, err, tc.expected)
|
assert.Error(t, err, tc.expected)
|
||||||
} else {
|
} else {
|
||||||
|
@ -222,6 +223,7 @@ func TestContainerStatsContextWriteWithNoStats(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
err := statsFormatWrite(tc.context, []StatsEntry{}, "linux", false)
|
err := statsFormatWrite(tc.context, []StatsEntry{}, "linux", false)
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
|
@ -263,6 +265,7 @@ func TestContainerStatsContextWriteWithNoStatsWindows(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
err := statsFormatWrite(tc.context, []StatsEntry{}, "windows", false)
|
err := statsFormatWrite(tc.context, []StatsEntry{}, "windows", false)
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
|
@ -273,46 +276,45 @@ func TestContainerStatsContextWriteWithNoStatsWindows(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestContainerStatsContextWriteTrunc(t *testing.T) {
|
func TestContainerStatsContextWriteTrunc(t *testing.T) {
|
||||||
tests := []struct {
|
var out bytes.Buffer
|
||||||
doc string
|
|
||||||
|
contexts := []struct {
|
||||||
context formatter.Context
|
context formatter.Context
|
||||||
trunc bool
|
trunc bool
|
||||||
expected string
|
expected string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
doc: "non-truncated",
|
formatter.Context{
|
||||||
context: formatter.Context{
|
|
||||||
Format: "{{.ID}}",
|
Format: "{{.ID}}",
|
||||||
|
Output: &out,
|
||||||
},
|
},
|
||||||
expected: "b95a83497c9161c9b444e3d70e1a9dfba0c1840d41720e146a95a08ebf938afc\n",
|
false,
|
||||||
|
"b95a83497c9161c9b444e3d70e1a9dfba0c1840d41720e146a95a08ebf938afc\n",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
doc: "truncated",
|
formatter.Context{
|
||||||
context: formatter.Context{
|
|
||||||
Format: "{{.ID}}",
|
Format: "{{.ID}}",
|
||||||
|
Output: &out,
|
||||||
},
|
},
|
||||||
trunc: true,
|
true,
|
||||||
expected: "b95a83497c91\n",
|
"b95a83497c91\n",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range tests {
|
for _, context := range contexts {
|
||||||
t.Run(tc.doc, func(t *testing.T) {
|
statsFormatWrite(context.context, []StatsEntry{{ID: "b95a83497c9161c9b444e3d70e1a9dfba0c1840d41720e146a95a08ebf938afc"}}, "linux", context.trunc)
|
||||||
var out bytes.Buffer
|
assert.Check(t, is.Equal(context.expected, out.String()))
|
||||||
tc.context.Output = &out
|
// Clean buffer
|
||||||
err := statsFormatWrite(tc.context, []StatsEntry{{ID: "b95a83497c9161c9b444e3d70e1a9dfba0c1840d41720e146a95a08ebf938afc"}}, "linux", tc.trunc)
|
out.Reset()
|
||||||
assert.NilError(t, err)
|
|
||||||
assert.Check(t, is.Equal(tc.expected, out.String()))
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkStatsFormat(b *testing.B) {
|
func BenchmarkStatsFormat(b *testing.B) {
|
||||||
b.ReportAllocs()
|
b.ReportAllocs()
|
||||||
entries := genStats()
|
stats := genStats()
|
||||||
|
|
||||||
for i := 0; i < b.N; i++ {
|
for i := 0; i < b.N; i++ {
|
||||||
for _, s := range entries {
|
for _, s := range stats {
|
||||||
_ = s.CPUPerc()
|
_ = s.CPUPerc()
|
||||||
_ = s.MemUsage()
|
_ = s.MemUsage()
|
||||||
_ = s.MemPerc()
|
_ = s.MemPerc()
|
||||||
|
@ -335,9 +337,9 @@ func genStats() []statsContext {
|
||||||
NetworkTx: 987.654321,
|
NetworkTx: 987.654321,
|
||||||
PidsCurrent: 123456789,
|
PidsCurrent: 123456789,
|
||||||
}}
|
}}
|
||||||
entries := make([]statsContext, 0, 100)
|
stats := make([]statsContext, 100)
|
||||||
for range 100 {
|
for i := 0; i < 100; i++ {
|
||||||
entries = append(entries, entry)
|
stats = append(stats, entry)
|
||||||
}
|
}
|
||||||
return entries
|
return stats
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,8 +8,9 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
"github.com/moby/moby/api/stdcopy"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/moby/moby/api/types"
|
"github.com/docker/docker/pkg/ioutils"
|
||||||
|
"github.com/docker/docker/pkg/stdcopy"
|
||||||
"github.com/moby/term"
|
"github.com/moby/term"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
@ -18,18 +19,6 @@ import (
|
||||||
// TODO: This could be moved to `pkg/term`.
|
// TODO: This could be moved to `pkg/term`.
|
||||||
var defaultEscapeKeys = []byte{16, 17}
|
var defaultEscapeKeys = []byte{16, 17}
|
||||||
|
|
||||||
// readCloserWrapper wraps an io.Reader, and implements an io.ReadCloser
|
|
||||||
// It calls the given callback function when closed.
|
|
||||||
type readCloserWrapper struct {
|
|
||||||
io.Reader
|
|
||||||
closer func() error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close calls back the passed closer function
|
|
||||||
func (r *readCloserWrapper) Close() error {
|
|
||||||
return r.closer()
|
|
||||||
}
|
|
||||||
|
|
||||||
// A hijackedIOStreamer handles copying input to and output from streams to the
|
// A hijackedIOStreamer handles copying input to and output from streams to the
|
||||||
// connection.
|
// connection.
|
||||||
type hijackedIOStreamer struct {
|
type hijackedIOStreamer struct {
|
||||||
|
@ -95,9 +84,12 @@ func (h *hijackedIOStreamer) setupInput() (restore func(), err error) {
|
||||||
|
|
||||||
// Use sync.Once so we may call restore multiple times but ensure we
|
// Use sync.Once so we may call restore multiple times but ensure we
|
||||||
// only restore the terminal once.
|
// only restore the terminal once.
|
||||||
restore = sync.OnceFunc(func() {
|
var restoreOnce sync.Once
|
||||||
|
restore = func() {
|
||||||
|
restoreOnce.Do(func() {
|
||||||
_ = restoreTerminal(h.streams, h.inputStream)
|
_ = restoreTerminal(h.streams, h.inputStream)
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Wrap the input to detect detach escape sequence.
|
// Wrap the input to detect detach escape sequence.
|
||||||
// Use default escape keys if an invalid sequence is given.
|
// Use default escape keys if an invalid sequence is given.
|
||||||
|
@ -111,10 +103,7 @@ func (h *hijackedIOStreamer) setupInput() (restore func(), err error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
h.inputStream = &readCloserWrapper{
|
h.inputStream = ioutils.NewReadCloserWrapper(term.NewEscapeProxy(h.inputStream, escapeKeys), h.inputStream.Close)
|
||||||
Reader: term.NewEscapeProxy(h.inputStream, escapeKeys),
|
|
||||||
closer: h.inputStream.Close,
|
|
||||||
}
|
|
||||||
|
|
||||||
return restore, nil
|
return restore, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.23
|
//go:build go1.21
|
||||||
|
|
||||||
package container
|
package container
|
||||||
|
|
||||||
|
@ -42,9 +42,11 @@ func newInspectCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func runInspect(ctx context.Context, dockerCLI command.Cli, opts inspectOptions) error {
|
func runInspect(ctx context.Context, dockerCli command.Cli, opts inspectOptions) error {
|
||||||
apiClient := dockerCLI.Client()
|
client := dockerCli.Client()
|
||||||
return inspect.Inspect(dockerCLI.Out(), opts.refs, opts.format, func(ref string) (any, []byte, error) {
|
|
||||||
return apiClient.ContainerInspectWithRaw(ctx, ref, opts.size)
|
getRefFunc := func(ref string) (any, []byte, error) {
|
||||||
})
|
return client.ContainerInspectWithRaw(ctx, ref, opts.size)
|
||||||
|
}
|
||||||
|
return inspect.Inspect(dockerCli.Out(), opts.refs, opts.format, getRefFunc)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,12 +2,13 @@ package container
|
||||||
|
|
||||||
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/docker/cli/cli/command/completion"
|
"github.com/docker/cli/cli/command/completion"
|
||||||
|
"github.com/pkg/errors"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -43,19 +44,20 @@ func NewKillCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func runKill(ctx context.Context, dockerCLI command.Cli, opts *killOptions) error {
|
func runKill(ctx context.Context, dockerCli command.Cli, opts *killOptions) error {
|
||||||
apiClient := dockerCLI.Client()
|
var errs []string
|
||||||
errChan := parallelOperation(ctx, opts.containers, func(ctx context.Context, container string) error {
|
errChan := parallelOperation(ctx, opts.containers, func(ctx context.Context, container string) error {
|
||||||
return apiClient.ContainerKill(ctx, container, opts.signal)
|
return dockerCli.Client().ContainerKill(ctx, container, opts.signal)
|
||||||
})
|
})
|
||||||
|
|
||||||
var errs []error
|
|
||||||
for _, name := range opts.containers {
|
for _, name := range opts.containers {
|
||||||
if err := <-errChan; err != nil {
|
if err := <-errChan; err != nil {
|
||||||
errs = append(errs, err)
|
errs = append(errs, err.Error())
|
||||||
continue
|
} else {
|
||||||
|
_, _ = fmt.Fprintln(dockerCli.Out(), name)
|
||||||
}
|
}
|
||||||
_, _ = fmt.Fprintln(dockerCLI.Out(), name)
|
|
||||||
}
|
}
|
||||||
return errors.Join(errs...)
|
if len(errs) > 0 {
|
||||||
|
return errors.New(strings.Join(errs, "\n"))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,74 +0,0 @@
|
||||||
package container
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/docker/cli/internal/test"
|
|
||||||
"gotest.tools/v3/assert"
|
|
||||||
is "gotest.tools/v3/assert/cmp"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestRunKill(t *testing.T) {
|
|
||||||
cli := test.NewFakeCli(&fakeClient{
|
|
||||||
containerKillFunc: func(
|
|
||||||
ctx context.Context,
|
|
||||||
container string,
|
|
||||||
signal string,
|
|
||||||
) error {
|
|
||||||
assert.Assert(t, is.Equal(signal, "STOP"))
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
cmd := NewKillCommand(cli)
|
|
||||||
cmd.SetOut(io.Discard)
|
|
||||||
|
|
||||||
cmd.SetArgs([]string{
|
|
||||||
"--signal", "STOP",
|
|
||||||
"container-id-1",
|
|
||||||
"container-id-2",
|
|
||||||
})
|
|
||||||
err := cmd.Execute()
|
|
||||||
assert.NilError(t, err)
|
|
||||||
|
|
||||||
containerIDs := strings.SplitN(cli.OutBuffer().String(), "\n", 2)
|
|
||||||
assert.Assert(t, is.Len(containerIDs, 2))
|
|
||||||
|
|
||||||
containerID1 := strings.TrimSpace(containerIDs[0])
|
|
||||||
containerID2 := strings.TrimSpace(containerIDs[1])
|
|
||||||
|
|
||||||
assert.Check(t, is.Equal(containerID1, "container-id-1"))
|
|
||||||
assert.Check(t, is.Equal(containerID2, "container-id-2"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRunKillClientError(t *testing.T) {
|
|
||||||
cli := test.NewFakeCli(&fakeClient{
|
|
||||||
containerKillFunc: func(
|
|
||||||
ctx context.Context,
|
|
||||||
container string,
|
|
||||||
signal string,
|
|
||||||
) error {
|
|
||||||
return fmt.Errorf("client error for container %s", container)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
cmd := NewKillCommand(cli)
|
|
||||||
cmd.SetOut(io.Discard)
|
|
||||||
cmd.SetErr(io.Discard)
|
|
||||||
|
|
||||||
cmd.SetArgs([]string{"container-id-1", "container-id-2"})
|
|
||||||
err := cmd.Execute()
|
|
||||||
|
|
||||||
errs := strings.SplitN(err.Error(), "\n", 2)
|
|
||||||
assert.Assert(t, is.Len(errs, 2))
|
|
||||||
|
|
||||||
errContainerID1 := errs[0]
|
|
||||||
errContainerID2 := errs[1]
|
|
||||||
|
|
||||||
assert.Assert(t, is.Equal(errContainerID1, "client error for container container-id-1"))
|
|
||||||
assert.Assert(t, is.Equal(errContainerID2, "client error for container container-id-2"))
|
|
||||||
}
|
|
|
@ -11,7 +11,7 @@ import (
|
||||||
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/cli/templates"
|
"github.com/docker/cli/templates"
|
||||||
"github.com/moby/moby/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue