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/
|
||||
/cmd/docker/winresources/versioninfo.json
|
||||
/cmd/docker/winresources/*.syso
|
||||
/cli/winresources/versioninfo.json
|
||||
/cli/winresources/*.syso
|
||||
/man/man*/
|
||||
/man/vendor/
|
||||
/man/go.sum
|
||||
|
|
|
@ -1,14 +1,3 @@
|
|||
* text=auto
|
||||
|
||||
Dockerfile* linguist-language=Dockerfile
|
||||
vendor.mod linguist-language=Go-Module
|
||||
vendor.sum linguist-language=Go-Checksums
|
||||
|
||||
*.go -text diff=golang
|
||||
|
||||
# scripts directory contains shell scripts
|
||||
# without extensions, so we need to force
|
||||
scripts/** text=auto eol=lf
|
||||
|
||||
# shell scripts should always have LF
|
||||
*.sh text eol=lf
|
||||
|
|
|
@ -19,14 +19,11 @@ Provide the following information:
|
|||
|
||||
**- How to verify it**
|
||||
|
||||
**- Human readable description for the release notes**
|
||||
**- Description for the changelog**
|
||||
<!--
|
||||
Write a short (one line) summary that describes the changes in this
|
||||
pull request for inclusion in the changelog.
|
||||
It must be placed inside the below triple backticks section.
|
||||
|
||||
NOTE: Only fill this section if changes introduced in this PR are user-facing.
|
||||
The PR must have a relevant impact/ label.
|
||||
It must be placed inside the below triple backticks section:
|
||||
-->
|
||||
```markdown changelog
|
||||
|
||||
|
|
|
@ -22,7 +22,6 @@ on:
|
|||
branches:
|
||||
- 'master'
|
||||
- '[0-9]+.[0-9]+'
|
||||
- '[0-9]+.x'
|
||||
tags:
|
||||
- 'v*'
|
||||
pull_request:
|
||||
|
@ -61,12 +60,17 @@ jobs:
|
|||
- ""
|
||||
- glibc
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
-
|
||||
name: Build
|
||||
uses: docker/bake-action@v6
|
||||
uses: docker/bake-action@v5
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
set: |
|
||||
|
@ -99,12 +103,8 @@ jobs:
|
|||
if: ${{ github.event_name != 'pull_request' && github.repository == 'docker/cli' }}
|
||||
steps:
|
||||
-
|
||||
name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_CLIBIN_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_CLIBIN_TOKEN }}
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
@ -121,15 +121,21 @@ jobs:
|
|||
type=semver,pattern={{version}}
|
||||
type=ref,event=branch
|
||||
type=ref,event=pr
|
||||
type=semver,pattern={{major}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=sha
|
||||
-
|
||||
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
|
||||
uses: docker/bake-action@v6
|
||||
uses: docker/bake-action@v5
|
||||
with:
|
||||
files: |
|
||||
./docker-bake.hcl
|
||||
cwd://${{ steps.meta.outputs.bake-file }}
|
||||
${{ steps.meta.outputs.bake-file }}
|
||||
targets: bin-image-cross
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
set: |
|
||||
|
@ -163,12 +169,15 @@ jobs:
|
|||
matrix:
|
||||
platform: ${{ fromJson(needs.prepare-plugins.outputs.matrix) }}
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
-
|
||||
name: Build
|
||||
uses: docker/bake-action@v6
|
||||
uses: docker/bake-action@v5
|
||||
with:
|
||||
targets: plugins-cross
|
||||
set: |
|
||||
|
|
|
@ -11,15 +11,14 @@ permissions:
|
|||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
- '[0-9]+.[0-9]+'
|
||||
- '[0-9]+.x'
|
||||
tags:
|
||||
- 'v*'
|
||||
branches:
|
||||
- 'master'
|
||||
- '[0-9]+.[0-9]+'
|
||||
tags:
|
||||
- 'v*'
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: ["master"]
|
||||
branches: [ "master" ]
|
||||
schedule:
|
||||
# ┌───────────── minute (0 - 59)
|
||||
# │ ┌───────────── hour (0 - 23)
|
||||
|
@ -34,21 +33,26 @@ on:
|
|||
|
||||
jobs:
|
||||
codeql:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
runs-on: 'ubuntu-latest'
|
||||
timeout-minutes: 360
|
||||
env:
|
||||
DISABLE_WARN_OUTSIDE_CONTAINER: '1'
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
-
|
||||
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,
|
||||
# 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
|
||||
|
@ -63,7 +67,7 @@ jobs:
|
|||
name: Update Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.24.5"
|
||||
go-version: '1.21'
|
||||
-
|
||||
name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
|
|
|
@ -19,28 +19,29 @@ on:
|
|||
branches:
|
||||
- 'master'
|
||||
- '[0-9]+.[0-9]+'
|
||||
- '[0-9]+.x'
|
||||
tags:
|
||||
- 'v*'
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
e2e:
|
||||
runs-on: ubuntu-24.04
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
target:
|
||||
- local
|
||||
- non-experimental
|
||||
- experimental
|
||||
- connhelper-ssh
|
||||
base:
|
||||
- alpine
|
||||
- debian
|
||||
engine-version:
|
||||
- 28 # latest
|
||||
- 27 # latest - 1
|
||||
- 26 # github actions default
|
||||
- 23 # mirantis lts
|
||||
- 27.0 # latest
|
||||
- 26.1 # latest - 1
|
||||
- 23.0 # 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:
|
||||
-
|
||||
name: Checkout
|
||||
|
@ -74,7 +75,7 @@ jobs:
|
|||
TESTFLAGS: -coverprofile=/tmp/coverage/coverage.txt
|
||||
-
|
||||
name: Send to Codecov
|
||||
uses: codecov/codecov-action@v5
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
files: ./build/coverage/coverage.txt
|
||||
file: ./build/coverage/coverage.txt
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
|
|
@ -19,7 +19,6 @@ on:
|
|||
branches:
|
||||
- 'master'
|
||||
- '[0-9]+.[0-9]+'
|
||||
- '[0-9]+.x'
|
||||
tags:
|
||||
- 'v*'
|
||||
pull_request:
|
||||
|
@ -28,19 +27,22 @@ jobs:
|
|||
ctn:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
-
|
||||
name: Test
|
||||
uses: docker/bake-action@v6
|
||||
uses: docker/bake-action@v5
|
||||
with:
|
||||
targets: test-coverage
|
||||
-
|
||||
name: Send to Codecov
|
||||
uses: codecov/codecov-action@v5
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
files: ./build/coverage/coverage.txt
|
||||
file: ./build/coverage/coverage.txt
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
host:
|
||||
|
@ -57,6 +59,12 @@ jobs:
|
|||
- macos-14 # macOS 14 on arm64 (Apple Silicon M1)
|
||||
# - windows-2022 # FIXME: some tests are failing on the Windows runner, as well as on Appveyor since June 24, 2018: https://ci.appveyor.com/project/docker/cli/history
|
||||
steps:
|
||||
-
|
||||
name: Prepare git
|
||||
if: matrix.os == 'windows-latest'
|
||||
run: |
|
||||
git config --system core.autocrlf false
|
||||
git config --system core.eol lf
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
@ -66,7 +74,7 @@ jobs:
|
|||
name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.24.5"
|
||||
go-version: 1.22.7
|
||||
-
|
||||
name: Test
|
||||
run: |
|
||||
|
@ -76,8 +84,8 @@ jobs:
|
|||
shell: bash
|
||||
-
|
||||
name: Send to Codecov
|
||||
uses: codecov/codecov-action@v5
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
files: /tmp/coverage.txt
|
||||
file: /tmp/coverage.txt
|
||||
working-directory: ${{ env.GOPATH }}/src/github.com/docker/cli
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
|
|
@ -15,8 +15,7 @@ on:
|
|||
|
||||
jobs:
|
||||
check-area-label:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 120 # guardrails timeout for the whole job
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Missing `area/` label
|
||||
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
|
||||
|
||||
check-changelog:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 120 # guardrails timeout for the whole job
|
||||
if: contains(join(github.event.pull_request.labels.*.name, ','), 'impact/')
|
||||
runs-on: ubuntu-20.04
|
||||
env:
|
||||
HAS_IMPACT_LABEL: ${{ contains(join(github.event.pull_request.labels.*.name, ','), 'impact/') }}
|
||||
PR_BODY: |
|
||||
${{ github.event.pull_request.body }}
|
||||
steps:
|
||||
|
@ -42,31 +40,22 @@ jobs:
|
|||
# Strip empty lines
|
||||
desc=$(echo "$block" | awk NF)
|
||||
|
||||
if [ "$HAS_IMPACT_LABEL" = "true" ]; then
|
||||
if [ -z "$desc" ]; then
|
||||
echo "::error::Changelog section is empty. Please provide a description for the changelog."
|
||||
exit 1
|
||||
fi
|
||||
if [ -z "$desc" ]; then
|
||||
echo "::error::Changelog section is empty. Provide a description for the changelog."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
len=$(echo -n "$desc" | wc -c)
|
||||
if [[ $len -le 6 ]]; then
|
||||
echo "::error::Description looks too short: $desc"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
if [ -n "$desc" ]; then
|
||||
echo "::error::PR has a changelog description, but no changelog label"
|
||||
echo "::error::Please add the relevant 'impact/' label to the PR or remove the changelog description"
|
||||
exit 1
|
||||
fi
|
||||
len=$(echo -n "$desc" | wc -c)
|
||||
if [[ $len -le 6 ]]; then
|
||||
echo "::error::Description looks too short: $desc"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "This PR will be included in the release notes with the following note:"
|
||||
echo "$desc"
|
||||
|
||||
check-pr-branch:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 120 # guardrails timeout for the whole job
|
||||
runs-on: ubuntu-20.04
|
||||
env:
|
||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||
steps:
|
||||
|
|
|
@ -19,7 +19,6 @@ on:
|
|||
branches:
|
||||
- 'master'
|
||||
- '[0-9]+.[0-9]+'
|
||||
- '[0-9]+.x'
|
||||
tags:
|
||||
- 'v*'
|
||||
pull_request:
|
||||
|
@ -36,9 +35,12 @@ jobs:
|
|||
- validate-vendor
|
||||
- update-authors # ensure authors update target runs fine
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Run
|
||||
uses: docker/bake-action@v6
|
||||
uses: docker/bake-action@v5
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
|
|
|
@ -8,8 +8,8 @@
|
|||
Thumbs.db
|
||||
.editorconfig
|
||||
/build/
|
||||
/cmd/docker/winresources/versioninfo.json
|
||||
/cmd/docker/winresources/*.syso
|
||||
/cli/winresources/versioninfo.json
|
||||
/cli/winresources/*.syso
|
||||
profile.out
|
||||
|
||||
# top-level go.mod is not meant to be checked in
|
||||
|
|
347
.golangci.yml
347
.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:
|
||||
enable:
|
||||
- asasalint # Detects "[]any" used as argument for variadic "func(...any)".
|
||||
- bodyclose
|
||||
- copyloopvar # Detects places where loop variables are copied.
|
||||
- depguard
|
||||
- dogsled # Detects assignments with too many blank identifiers.
|
||||
- dupword # Detects duplicate words.
|
||||
- durationcheck # Detect cases where two time.Duration values are being multiplied in possibly erroneous ways.
|
||||
- errcheck
|
||||
- errchkjson # Detects unsupported types passed to json encoding functions and reports if checks for the returned error can be omitted.
|
||||
- exhaustive # Detects missing options in enum switch statements.
|
||||
- exptostd # Detects functions from golang.org/x/exp/ that can be replaced by std functions.
|
||||
- fatcontext # Detects nested contexts in loops and function literals.
|
||||
- forbidigo
|
||||
- gocheckcompilerdirectives # Detects invalid go compiler directive comments (//go:).
|
||||
- gocritic # Metalinter; detects bugs, performance, and styling issues.
|
||||
- dogsled
|
||||
- dupword # Detects duplicate words.
|
||||
- durationcheck
|
||||
- errchkjson
|
||||
- exportloopref # Detects pointers to enclosing loop variables.
|
||||
- gocritic # Metalinter; detects bugs, performance, and styling issues.
|
||||
- gocyclo
|
||||
- gosec # Detects security problems.
|
||||
- gofumpt # Detects whether code was gofumpt-ed.
|
||||
- goimports
|
||||
- gosec # Detects security problems.
|
||||
- gosimple
|
||||
- govet
|
||||
- iface # Detects incorrect use of interfaces. Currently only used for "identical" interfaces in the same package.
|
||||
- importas # Enforces consistent import aliases.
|
||||
- ineffassign
|
||||
- makezero # Finds slice declarations with non-zero initial length.
|
||||
- mirror # Detects wrong mirror patterns of bytes/strings usage.
|
||||
- misspell # Detects commonly misspelled English words in comments.
|
||||
- nakedret # Detects uses of naked returns.
|
||||
- nilnesserr # Detects returning nil errors. It combines the features of nilness and nilerr,
|
||||
- nosprintfhostport # Detects misuse of Sprintf to construct a host with port in a URL.
|
||||
- nolintlint # Detects ill-formed or insufficient nolint directives.
|
||||
- perfsprint # Detects fmt.Sprintf uses that can be replaced with a faster alternative.
|
||||
- prealloc # Detects slice declarations that could potentially be pre-allocated.
|
||||
- predeclared # Detects code that shadows one of Go's predeclared identifiers
|
||||
- reassign # Detects reassigning a top-level variable in another package.
|
||||
- revive # Metalinter; drop-in replacement for golint.
|
||||
- spancheck # Detects mistakes with OpenTelemetry/Census spans.
|
||||
- lll
|
||||
- megacheck
|
||||
- misspell # Detects commonly misspelled English words in comments.
|
||||
- nakedret
|
||||
- nilerr # Detects code that returns nil even if it checks that the error is not nil.
|
||||
- nolintlint # Detects ill-formed or insufficient nolint directives.
|
||||
- perfsprint # Detects fmt.Sprintf uses that can be replaced with a faster alternative.
|
||||
- prealloc # Detects slice declarations that could potentially be pre-allocated.
|
||||
- predeclared # Detects code that shadows one of Go's predeclared identifiers
|
||||
- reassign
|
||||
- revive # Metalinter; drop-in replacement for golint.
|
||||
- staticcheck
|
||||
- thelper # Detects test helpers without t.Helper().
|
||||
- tparallel # Detects inappropriate usage of t.Parallel().
|
||||
- unconvert # Detects unnecessary type conversions.
|
||||
- stylecheck # Replacement for golint
|
||||
- tenv # Detects using os.Setenv instead of t.Setenv.
|
||||
- thelper # Detects test helpers without t.Helper().
|
||||
- tparallel # Detects inappropriate usage of t.Parallel().
|
||||
- typecheck
|
||||
- unconvert # Detects unnecessary type conversions.
|
||||
- unparam
|
||||
- unused
|
||||
- usestdlibvars # Detects the possibility to use variables/constants from the Go standard library.
|
||||
- usetesting # Reports uses of functions with replacement inside the testing package.
|
||||
- wastedassign # Detects wasted assignment statements.
|
||||
- usestdlibvars
|
||||
- vet
|
||||
- wastedassign
|
||||
|
||||
disable:
|
||||
- errcheck
|
||||
|
||||
settings:
|
||||
depguard:
|
||||
rules:
|
||||
main:
|
||||
deny:
|
||||
- pkg: "github.com/containerd/containerd/errdefs"
|
||||
desc: The containerd errdefs package was migrated to a separate module. Use github.com/containerd/errdefs instead.
|
||||
- pkg: "github.com/containerd/containerd/log"
|
||||
desc: The containerd log package was migrated to a separate module. Use github.com/containerd/log instead.
|
||||
- pkg: "github.com/containerd/containerd/pkg/userns"
|
||||
desc: Use github.com/moby/sys/userns instead.
|
||||
- pkg: "github.com/containerd/containerd/platforms"
|
||||
desc: The containerd platforms package was migrated to a separate module. Use github.com/containerd/platforms instead.
|
||||
- pkg: "github.com/docker/docker/pkg/system"
|
||||
desc: This package should not be used unless strictly necessary.
|
||||
- pkg: "github.com/docker/distribution/uuid"
|
||||
desc: Use github.com/google/uuid instead.
|
||||
- pkg: "io/ioutil"
|
||||
desc: The io/ioutil package has been deprecated, see https://go.dev/doc/go1.16#ioutil
|
||||
run:
|
||||
timeout: 5m
|
||||
|
||||
forbidigo:
|
||||
forbid:
|
||||
- pkg: ^regexp$
|
||||
pattern: ^regexp\.MustCompile
|
||||
msg: Use internal/lazyregexp.New instead.
|
||||
linters-settings:
|
||||
depguard:
|
||||
rules:
|
||||
main:
|
||||
deny:
|
||||
- pkg: io/ioutil
|
||||
desc: The io/ioutil package has been deprecated, see https://go.dev/doc/go1.16#ioutil
|
||||
gocyclo:
|
||||
min-complexity: 16
|
||||
govet:
|
||||
enable:
|
||||
- shadow
|
||||
settings:
|
||||
shadow:
|
||||
strict: true
|
||||
lll:
|
||||
line-length: 200
|
||||
nakedret:
|
||||
command: nakedret
|
||||
pattern: ^(?P<path>.*?\\.go):(?P<line>\\d+)\\s*(?P<message>.*)$
|
||||
|
||||
gocyclo:
|
||||
min-complexity: 16
|
||||
revive:
|
||||
rules:
|
||||
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#import-shadowing
|
||||
- name: import-shadowing
|
||||
severity: warning
|
||||
disabled: false
|
||||
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#empty-block
|
||||
- name: empty-block
|
||||
severity: warning
|
||||
disabled: false
|
||||
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#empty-lines
|
||||
- name: empty-lines
|
||||
severity: warning
|
||||
disabled: false
|
||||
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#use-any
|
||||
- name: use-any
|
||||
severity: warning
|
||||
disabled: false
|
||||
|
||||
gosec:
|
||||
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")
|
||||
issues:
|
||||
# The default exclusion rules are a bit too permissive, so copying the relevant ones below
|
||||
exclude-use-default: false
|
||||
|
||||
govet:
|
||||
enable:
|
||||
- shadow
|
||||
settings:
|
||||
shadow:
|
||||
strict: true
|
||||
exclude:
|
||||
- parameter .* always receives
|
||||
|
||||
lll:
|
||||
line-length: 200
|
||||
exclude-files:
|
||||
- cli/compose/schema/bindata.go
|
||||
- .*generated.*
|
||||
|
||||
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:
|
||||
# Disallow naked returns if func has more lines of code than this setting.
|
||||
# Default: 30
|
||||
max-func-lines: 0
|
||||
|
||||
staticcheck:
|
||||
checks:
|
||||
- all
|
||||
- -QF1008 # Omit embedded fields from selector expression; https://staticcheck.dev/docs/checks/#QF1008
|
||||
|
||||
revive:
|
||||
rules:
|
||||
- name: empty-block # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#empty-block
|
||||
- name: empty-lines # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#empty-lines
|
||||
- name: import-shadowing # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#import-shadowing
|
||||
- name: line-length-limit # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#line-length-limit
|
||||
arguments: [200]
|
||||
- name: unused-receiver # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unused-receiver
|
||||
- name: use-any # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#use-any
|
||||
|
||||
usetesting:
|
||||
os-chdir: false # FIXME(thaJeztah): Disable `os.Chdir()` detections; should be automatically disabled on Go < 1.24; see https://github.com/docker/cli/pull/5835#issuecomment-2665302478
|
||||
context-background: false # FIXME(thaJeztah): Disable `context.Background()` detections; should be automatically disabled on Go < 1.24; see https://github.com/docker/cli/pull/5835#issuecomment-2665302478
|
||||
context-todo: false # FIXME(thaJeztah): Disable `context.TODO()` detections; should be automatically disabled on Go < 1.24; see https://github.com/docker/cli/pull/5835#issuecomment-2665302478
|
||||
|
||||
exclusions:
|
||||
# We prefer to use an "linters.exclusions.rules" so that new "default" exclusions are not
|
||||
exclude-rules:
|
||||
# We prefer to use an "exclude-list" so that new "default" exclusions are not
|
||||
# automatically inherited. We can decide whether or not to follow upstream
|
||||
# defaults when updating golang-ci-lint versions.
|
||||
# Unfortunately, this means we have to copy the whole exclusion pattern, as
|
||||
# (unlike the "include" option), the "exclude" option does not take exclusion
|
||||
# ID's.
|
||||
#
|
||||
# These exclusion patterns are copied from the default excludes at:
|
||||
# https://github.com/golangci/golangci-lint/blob/v1.61.0/pkg/config/issues.go#L11-L104
|
||||
#
|
||||
# The default list of exclusions can be found at:
|
||||
# https://golangci-lint.run/usage/false-positives/#default-exclusions
|
||||
generated: strict
|
||||
# These exclusion patterns are copied from the default excluses at:
|
||||
# https://github.com/golangci/golangci-lint/blob/v1.44.0/pkg/config/issues.go#L10-L104
|
||||
|
||||
rules:
|
||||
# EXC0003
|
||||
- text: "func name will be used as test\\.Test.* by other packages, and that stutters; consider calling this"
|
||||
linters:
|
||||
- revive
|
||||
# EXC0001
|
||||
- text: "Error return value of .((os\\.)?std(out|err)\\..*|.*Close|.*Flush|os\\.Remove(All)?|.*print(f|ln)?|os\\.(Un)?Setenv). is not checked"
|
||||
linters:
|
||||
- errcheck
|
||||
# EXC0003
|
||||
- text: "func name will be used as test\\.Test.* by other packages, and that stutters; consider calling this"
|
||||
linters:
|
||||
- revive
|
||||
# EXC0006
|
||||
- text: "Use of unsafe calls should be audited"
|
||||
linters:
|
||||
- gosec
|
||||
# EXC0007
|
||||
- text: "Subprocess launch(ed with variable|ing should be audited)"
|
||||
linters:
|
||||
- gosec
|
||||
# EXC0008
|
||||
# TODO: evaluate these and fix where needed: G307: Deferring unsafe method "*os.File" on type "Close" (gosec)
|
||||
- text: "G307"
|
||||
linters:
|
||||
- gosec
|
||||
# EXC0009
|
||||
- text: "(Expect directory permissions to be 0750 or less|Expect file permissions to be 0600 or less)"
|
||||
linters:
|
||||
- gosec
|
||||
# EXC0010
|
||||
- text: "Potential file inclusion via variable"
|
||||
linters:
|
||||
- gosec
|
||||
|
||||
# EXC0007
|
||||
- text: "Subprocess launch(ed with variable|ing should be audited)"
|
||||
linters:
|
||||
- gosec
|
||||
# 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
|
||||
|
||||
# EXC0009
|
||||
- text: "(Expect directory permissions to be 0750 or less|Expect file permissions to be 0600 or less)"
|
||||
linters:
|
||||
- gosec
|
||||
# TODO: make sure all packages have a description. Currently, there's 67 packages without.
|
||||
- text: "package-comments: should have a package comment"
|
||||
linters:
|
||||
- revive
|
||||
# FIXME temporarily suppress these (see https://github.com/gotestyourself/gotest.tools/issues/272)
|
||||
- text: "SA1019: (assert|cmp|is)\\.ErrorType is deprecated"
|
||||
linters:
|
||||
- staticcheck
|
||||
# Exclude some linters from running on tests files.
|
||||
- path: _test\.go
|
||||
linters:
|
||||
- errcheck
|
||||
- gosec
|
||||
- text: "ST1000: at least one file in a package should have a package comment"
|
||||
linters:
|
||||
- stylecheck
|
||||
|
||||
# EXC0010
|
||||
- text: "Potential file inclusion via variable"
|
||||
linters:
|
||||
- gosec
|
||||
# Allow "err" and "ok" vars to shadow existing declarations, otherwise we get too many false positives.
|
||||
- text: '^shadow: declaration of "(err|ok)" shadows declaration'
|
||||
linters:
|
||||
- govet
|
||||
|
||||
# TODO: make sure all packages have a description. Currently, there's 67 packages without.
|
||||
- text: "package-comments: should have a package comment"
|
||||
linters:
|
||||
- revive
|
||||
|
||||
# Exclude some linters from running on tests files.
|
||||
- path: _test\.go
|
||||
linters:
|
||||
- errcheck
|
||||
- gosec
|
||||
# Maximum issues count per one linter. Set to 0 to disable. Default is 50.
|
||||
max-issues-per-linter: 0
|
||||
|
||||
- text: "ST1000: at least one file in a package should have a package comment"
|
||||
linters:
|
||||
- staticcheck
|
||||
|
||||
# Allow "err" and "ok" vars to shadow existing declarations, otherwise we get too many false positives.
|
||||
- text: '^shadow: declaration of "(err|ok)" shadows declaration'
|
||||
linters:
|
||||
- govet
|
||||
|
||||
# Ignore for cli/command/formatter/tabwriter, which is forked from go stdlib, so we want to align with it.
|
||||
- text: '^(ST1020|ST1022): comment on exported'
|
||||
path: "cli/command/formatter/tabwriter"
|
||||
linters:
|
||||
- staticcheck
|
||||
|
||||
# Log a warning if an exclusion rule is unused.
|
||||
# Default: false
|
||||
warn-unused: true
|
||||
# Maximum count of issues with the same text. Set to 0 to disable. Default is 3.
|
||||
max-same-issues: 0
|
||||
|
|
13
.mailmap
13
.mailmap
|
@ -43,7 +43,6 @@ Alexis Couvreur <alexiscouvreur.pro@gmail.com>
|
|||
Alicia Lauerman <alicia@eta.im> <allydevour@me.com>
|
||||
Allen Sun <allensun.shl@alibaba-inc.com> <allen.sun@daocloud.io>
|
||||
Allen Sun <allensun.shl@alibaba-inc.com> <shlallen1990@gmail.com>
|
||||
Allie Sadler <allie.sadler@docker.com>
|
||||
Andrew Weiss <andrew.weiss@docker.com> <andrew.weiss@microsoft.com>
|
||||
Andrew Weiss <andrew.weiss@docker.com> <andrew.weiss@outlook.com>
|
||||
André Martins <aanm90@gmail.com> <martins@noironetworks.com>
|
||||
|
@ -66,9 +65,6 @@ Arnaud Porterie <icecrime@gmail.com>
|
|||
Arnaud Porterie <icecrime@gmail.com> <arnaud.porterie@docker.com>
|
||||
Arthur Gautier <baloo@gandi.net> <superbaloo+registrations.github@superbaloo.net>
|
||||
Arthur Peka <arthur.peka@outlook.com> <arthrp@users.noreply.github.com>
|
||||
Austin Vazquez <austin.vazquez.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>
|
||||
Ben Bonnefoy <frenchben@docker.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>
|
||||
George Kontridze <george@bugsnag.com>
|
||||
Gerwim Feiken <g.feiken@tfe.nl> <gerwim@gmail.com>
|
||||
Giau. Tran Minh <hello@giautm.dev>
|
||||
Giau. Tran Minh <hello@giautm.dev> <12751435+giautm@users.noreply.github.com>
|
||||
Giampaolo Mancini <giampaolo@trampolineup.com>
|
||||
Gopikannan Venugopalsamy <gopikannan.venugopalsamy@gmail.com>
|
||||
Gou Rao <gou@portworx.com> <gourao@users.noreply.github.com>
|
||||
|
@ -220,7 +214,6 @@ Günther Jungbluth <gunther@gameslabs.net>
|
|||
Hakan Özler <hakan.ozler@kodcu.com>
|
||||
Hao Shu Wei <haosw@cn.ibm.com>
|
||||
Hao Shu Wei <haosw@cn.ibm.com> <haoshuwei1989@163.com>
|
||||
Harald Albers <github@albersweb.de>
|
||||
Harald Albers <github@albersweb.de> <albers@users.noreply.github.com>
|
||||
Harold Cooper <hrldcpr@gmail.com>
|
||||
Harry Zhang <harryz@hyper.sh> <harryzhang@zju.edu.cn>
|
||||
|
@ -337,8 +330,6 @@ Lajos Papp <lajos.papp@sequenceiq.com> <lalyos@yahoo.com>
|
|||
Lei Jitang <leijitang@huawei.com>
|
||||
Lei Jitang <leijitang@huawei.com> <leijitang@gmail.com>
|
||||
Li Fu Bang <lifubang@acmcoder.com>
|
||||
Li Yi <denverdino@gmail.com>
|
||||
Li Yi <denverdino@gmail.com> <weiyuan.yl@alibaba-inc.com>
|
||||
Liang Mingqiang <mqliang.zju@gmail.com>
|
||||
Liang-Chi Hsieh <viirya@gmail.com>
|
||||
Liao Qingwei <liaoqingwei@huawei.com>
|
||||
|
@ -348,7 +339,6 @@ Lokesh Mandvekar <lsm5@fedoraproject.org> <lsm5@redhat.com>
|
|||
Lorenzo Fontana <lo@linux.com> <fontanalorenzo@me.com>
|
||||
Louis Opter <kalessin@kalessin.fr>
|
||||
Louis Opter <kalessin@kalessin.fr> <louis@dotcloud.com>
|
||||
Lovekesh Kumar <lovekesh.kumar@rtcamp.com>
|
||||
Luca Favatella <luca.favatella@erlang-solutions.com> <lucafavatella@users.noreply.github.com>
|
||||
Luke Marsden <me@lukemarsden.net> <luke@digital-crocus.com>
|
||||
Lyn <energylyn@zju.edu.cn>
|
||||
|
@ -406,7 +396,6 @@ Mike Dalton <mikedalton@github.com> <19153140+mikedalton@users.noreply.github.co
|
|||
Mike Goelzer <mike.goelzer@docker.com> <mgoelzer@docker.com>
|
||||
Milind Chawre <milindchawre@gmail.com>
|
||||
Misty Stanley-Jones <misty@docker.com> <misty@apache.org>
|
||||
Mohammad Hossein <mhm98035@gmail.com>
|
||||
Mohit Soni <mosoni@ebay.com> <mohitsoni1989@gmail.com>
|
||||
Moorthy RS <rsmoorthy@gmail.com> <rsmoorthy@users.noreply.github.com>
|
||||
Morten Hekkvang <morten.hekkvang@sbab.se>
|
||||
|
@ -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>
|
||||
Oliver Pomeroy <oppomeroy@gmail.com>
|
||||
Ouyang Liduo <oyld0210@163.com>
|
||||
Patrick St. laurent <patrick@saint-laurent.us>
|
||||
Patrick Stapleton <github@gdi2290.com>
|
||||
Paul Liljenberg <liljenberg.paul@gmail.com> <letters@paulnotcom.se>
|
||||
Pavel Tikhomirov <ptikhomirov@virtuozzo.com> <ptikhomirov@parallels.com>
|
||||
|
@ -510,7 +498,6 @@ Stephen Day <stevvooe@gmail.com> <stephen.day@docker.com>
|
|||
Stephen Day <stevvooe@gmail.com> <stevvooe@users.noreply.github.com>
|
||||
Steve Desmond <steve@vtsv.ca> <stevedesmond-ca@users.noreply.github.com>
|
||||
Steve Richards <steve.richards@docker.com> stevejr <>
|
||||
Stuart Williams <pid@pidster.com>
|
||||
Sun Gengze <690388648@qq.com>
|
||||
Sun Jianbo <wonderflow.sun@gmail.com>
|
||||
Sun Jianbo <wonderflow.sun@gmail.com> <wonderflow@zju.edu.cn>
|
||||
|
|
31
AUTHORS
31
AUTHORS
|
@ -48,7 +48,6 @@ Alfred Landrum <alfred.landrum@docker.com>
|
|||
Ali Rostami <rostami.ali@gmail.com>
|
||||
Alicia Lauerman <alicia@eta.im>
|
||||
Allen Sun <allensun.shl@alibaba-inc.com>
|
||||
Allie Sadler <allie.sadler@docker.com>
|
||||
Alvin Deng <alvin.q.deng@utexas.edu>
|
||||
Amen Belayneh <amenbelayneh@gmail.com>
|
||||
Amey Shrivastava <72866602+AmeyShrivastava@users.noreply.github.com>
|
||||
|
@ -82,7 +81,6 @@ Antonis Kalipetis <akalipetis@gmail.com>
|
|||
Anusha Ragunathan <anusha.ragunathan@docker.com>
|
||||
Ao Li <la9249@163.com>
|
||||
Arash Deshmeh <adeshmeh@ca.ibm.com>
|
||||
Archimedes Trajano <developer@trajano.net>
|
||||
Arko Dasgupta <arko@tetrate.io>
|
||||
Arnaud Porterie <icecrime@gmail.com>
|
||||
Arnaud Rebillout <elboulangero@gmail.com>
|
||||
|
@ -90,7 +88,6 @@ Arthur Peka <arthur.peka@outlook.com>
|
|||
Ashly Mathew <ashly.mathew@sap.com>
|
||||
Ashwini Oruganti <ashwini.oruganti@gmail.com>
|
||||
Aslam Ahemad <aslamahemad@gmail.com>
|
||||
Austin Vazquez <austin.vazquez.dev@gmail.com>
|
||||
Azat Khuyiyakhmetov <shadow_uz@mail.ru>
|
||||
Bardia Keyoumarsi <bkeyouma@ucsc.edu>
|
||||
Barnaby Gray <barnaby@pickle.me.uk>
|
||||
|
@ -135,7 +132,6 @@ Cao Weiwei <cao.weiwei30@zte.com.cn>
|
|||
Carlo Mion <mion00@gmail.com>
|
||||
Carlos Alexandro Becker <caarlos0@gmail.com>
|
||||
Carlos de Paula <me@carlosedp.com>
|
||||
Carston Schilds <Carston.Schilds@visier.com>
|
||||
Casey Korver <casey@korver.dev>
|
||||
Ce Gao <ce.gao@outlook.com>
|
||||
Cedric Davies <cedricda@microsoft.com>
|
||||
|
@ -193,7 +189,6 @@ Daisuke Ito <itodaisuke00@gmail.com>
|
|||
dalanlan <dalanlan925@gmail.com>
|
||||
Damien Nadé <github@livna.org>
|
||||
Dan Cotora <dan@bluevision.ro>
|
||||
Dan Wallis <dan@wallis.nz>
|
||||
Danial Gharib <danial.mail.gh@gmail.com>
|
||||
Daniel Artine <daniel.artine@ufrj.br>
|
||||
Daniel Cassidy <mail@danielcassidy.me.uk>
|
||||
|
@ -242,7 +237,6 @@ Deshi Xiao <dxiao@redhat.com>
|
|||
Dharmit Shah <shahdharmit@gmail.com>
|
||||
Dhawal Yogesh Bhanushali <dbhanushali@vmware.com>
|
||||
Dieter Reuter <dieter.reuter@me.com>
|
||||
Dilep Dev <34891655+DilepDev@users.noreply.github.com>
|
||||
Dima Stopel <dima@twistlock.com>
|
||||
Dimitry Andric <d.andric@activevideo.com>
|
||||
Ding Fei <dingfei@stars.org.cn>
|
||||
|
@ -314,8 +308,6 @@ George MacRorie <gmacr31@gmail.com>
|
|||
George Margaritis <gmargaritis@protonmail.com>
|
||||
George Xie <georgexsh@gmail.com>
|
||||
Gianluca Borello <g.borello@gmail.com>
|
||||
Giau. Tran Minh <hello@giautm.dev>
|
||||
Giedrius Jonikas <giedriusj1@gmail.com>
|
||||
Gildas Cuisinier <gildas.cuisinier@gcuisinier.net>
|
||||
Gio d'Amelio <giodamelio@gmail.com>
|
||||
Gleb Stsenov <gleb.stsenov@gmail.com>
|
||||
|
@ -352,7 +344,6 @@ Hugo Gabriel Eyherabide <hugogabriel.eyherabide@gmail.com>
|
|||
huqun <huqun@zju.edu.cn>
|
||||
Huu Nguyen <huu@prismskylabs.com>
|
||||
Hyzhou Zhy <hyzhou.zhy@alibaba-inc.com>
|
||||
Iain MacDonald <IJMacD@gmail.com>
|
||||
Iain Samuel McLean Elder <iain@isme.es>
|
||||
Ian Campbell <ian.campbell@docker.com>
|
||||
Ian Philpot <ian.philpot@microsoft.com>
|
||||
|
@ -402,7 +393,6 @@ Jesse Adametz <jesseadametz@gmail.com>
|
|||
Jessica Frazelle <jess@oxide.computer>
|
||||
Jezeniel Zapanta <jpzapanta22@gmail.com>
|
||||
Jian Zhang <zhangjian.fnst@cn.fujitsu.com>
|
||||
Jianyong Wu <wujianyong@hygon.cn>
|
||||
Jie Luo <luo612@zju.edu.cn>
|
||||
Jilles Oldenbeuving <ojilles@gmail.com>
|
||||
Jim Chen <njucjc@gmail.com>
|
||||
|
@ -456,7 +446,6 @@ Julian <gitea+julian@ic.thejulian.uk>
|
|||
Julien Barbier <write0@gmail.com>
|
||||
Julien Kassar <github@kassisol.com>
|
||||
Julien Maitrehenry <julien.maitrehenry@me.com>
|
||||
Julio Cesar Garcia <juliogarciamelgarejo@gmail.com>
|
||||
Justas Brazauskas <brazauskasjustas@gmail.com>
|
||||
Justin Chadwell <me@jedevc.com>
|
||||
Justin Cormack <justin.cormack@docker.com>
|
||||
|
@ -501,22 +490,19 @@ Kunal Kushwaha <kushwaha_kunal_v7@lab.ntt.co.jp>
|
|||
Kyle Mitofsky <Kylemit@gmail.com>
|
||||
Lachlan Cooper <lachlancooper@gmail.com>
|
||||
Lai Jiangshan <jiangshanlai@gmail.com>
|
||||
Lajos Papp <lajos.papp@sequenceiq.com>
|
||||
Lars Kellogg-Stedman <lars@redhat.com>
|
||||
Laura Brehm <laurabrehm@hey.com>
|
||||
Laura Frank <ljfrank@gmail.com>
|
||||
Laurent Erignoux <lerignoux@gmail.com>
|
||||
Laurent Goderre <laurent.goderre@docker.com>
|
||||
Lee Gaines <eightlimbed@gmail.com>
|
||||
Lei Jitang <leijitang@huawei.com>
|
||||
Lennie <github@consolejunkie.net>
|
||||
lentil32 <lentil32@icloud.com>
|
||||
Leo Gallucci <elgalu3@gmail.com>
|
||||
Leonid Skorospelov <leosko94@gmail.com>
|
||||
Lewis Daly <lewisdaly@me.com>
|
||||
Li Fu Bang <lifubang@acmcoder.com>
|
||||
Li Yi <denverdino@gmail.com>
|
||||
Li Zeghong <zeghong@hotmail.com>
|
||||
Li Yi <weiyuan.yl@alibaba-inc.com>
|
||||
Liang-Chi Hsieh <viirya@gmail.com>
|
||||
Lihua Tang <lhtang@alauda.io>
|
||||
Lily Guo <lily.guo@docker.com>
|
||||
|
@ -529,7 +515,6 @@ lixiaobing10051267 <li.xiaobing1@zte.com.cn>
|
|||
Lloyd Dewolf <foolswisdom@gmail.com>
|
||||
Lorenzo Fontana <lo@linux.com>
|
||||
Louis Opter <kalessin@kalessin.fr>
|
||||
Lovekesh Kumar <lovekesh.kumar@rtcamp.com>
|
||||
Luca Favatella <luca.favatella@erlang-solutions.com>
|
||||
Luca Marturana <lucamarturana@gmail.com>
|
||||
Lucas Chan <lucas-github@lucaschan.com>
|
||||
|
@ -574,7 +559,6 @@ Matt Robenolt <matt@ydekproductions.com>
|
|||
Matteo Orefice <matteo.orefice@bites4bits.software>
|
||||
Matthew Heon <mheon@redhat.com>
|
||||
Matthieu Hauglustaine <matt.hauglustaine@gmail.com>
|
||||
Matthieu MOREL <matthieu.morel35@gmail.com>
|
||||
Mauro Porras P <mauroporrasp@gmail.com>
|
||||
Max Shytikov <mshytikov@gmail.com>
|
||||
Max-Julian Pogner <max-julian@pogner.at>
|
||||
|
@ -582,7 +566,6 @@ Maxime Petazzoni <max@signalfuse.com>
|
|||
Maximillian Fan Xavier <maximillianfx@gmail.com>
|
||||
Mei ChunTao <mei.chuntao@zte.com.cn>
|
||||
Melroy van den Berg <melroy@melroy.org>
|
||||
Mert Şişmanoğlu <mert190737fb@gmail.com>
|
||||
Metal <2466052+tedhexaflow@users.noreply.github.com>
|
||||
Micah Zoltu <micah@newrelic.com>
|
||||
Michael A. Smith <michael@smith-li.com>
|
||||
|
@ -615,9 +598,7 @@ Mindaugas Rukas <momomg@gmail.com>
|
|||
Miroslav Gula <miroslav.gula@naytrolabs.com>
|
||||
Misty Stanley-Jones <misty@docker.com>
|
||||
Mohammad Banikazemi <mb@us.ibm.com>
|
||||
Mohammad Hossein <mhm98035@gmail.com>
|
||||
Mohammed Aaqib Ansari <maaquib@gmail.com>
|
||||
Mohammed Aminu Futa <mohammedfuta2000@gmail.com>
|
||||
Mohini Anne Dsouza <mohini3917@gmail.com>
|
||||
Moorthy RS <rsmoorthy@gmail.com>
|
||||
Morgan Bauer <mbauer@us.ibm.com>
|
||||
|
@ -652,11 +633,9 @@ Nicolas De Loof <nicolas.deloof@gmail.com>
|
|||
Nikhil Chawla <chawlanikhil24@gmail.com>
|
||||
Nikolas Garofil <nikolas.garofil@uantwerpen.be>
|
||||
Nikolay Milovanov <nmil@itransformers.net>
|
||||
NinaLua <iturf@sina.cn>
|
||||
Nir Soffer <nsoffer@redhat.com>
|
||||
Nishant Totla <nishanttotla@gmail.com>
|
||||
NIWA Hideyuki <niwa.niwa@nifty.ne.jp>
|
||||
Noah Silas <noah@hustle.com>
|
||||
Noah Treuhaft <noah.treuhaft@docker.com>
|
||||
O.S. Tezer <ostezer@gmail.com>
|
||||
Oded Arbel <oded@geek.co.il>
|
||||
|
@ -674,12 +653,10 @@ Patrick Böänziger <patrick.baenziger@bsi-software.com>
|
|||
Patrick Daigle <114765035+pdaig@users.noreply.github.com>
|
||||
Patrick Hemmer <patrick.hemmer@gmail.com>
|
||||
Patrick Lang <plang@microsoft.com>
|
||||
Patrick St. laurent <patrick@saint-laurent.us>
|
||||
Paul <paul9869@gmail.com>
|
||||
Paul Kehrer <paul.l.kehrer@gmail.com>
|
||||
Paul Lietar <paul@lietar.net>
|
||||
Paul Mulders <justinkb@gmail.com>
|
||||
Paul Rogalski <mail@paul-rogalski.de>
|
||||
Paul Seyfert <pseyfert.mathphys@gmail.com>
|
||||
Paul Weaver <pauweave@cisco.com>
|
||||
Pavel Pospisil <pospispa@gmail.com>
|
||||
|
@ -701,6 +678,7 @@ Philip Alexander Etling <paetling@gmail.com>
|
|||
Philipp Gillé <philipp.gille@gmail.com>
|
||||
Philipp Schmied <pschmied@schutzwerk.com>
|
||||
Phong Tran <tran.pho@northeastern.edu>
|
||||
pidster <pid@pidster.com>
|
||||
Pieter E Smit <diepes@github.com>
|
||||
pixelistik <pixelistik@users.noreply.github.com>
|
||||
Pratik Karki <prertik@outlook.com>
|
||||
|
@ -760,7 +738,6 @@ Samuel Cochran <sj26@sj26.com>
|
|||
Samuel Karp <skarp@amazon.com>
|
||||
Sandro Jäckel <sandro.jaeckel@gmail.com>
|
||||
Santhosh Manohar <santhosh@docker.com>
|
||||
Sarah Sanders <sarah.sanders@docker.com>
|
||||
Sargun Dhillon <sargun@netflix.com>
|
||||
Saswat Bhattacharya <sas.saswat@gmail.com>
|
||||
Saurabh Kumar <saurabhkumar0184@gmail.com>
|
||||
|
@ -793,7 +770,6 @@ Spencer Brown <spencer@spencerbrown.org>
|
|||
Spring Lee <xi.shuai@outlook.com>
|
||||
squeegels <lmscrewy@gmail.com>
|
||||
Srini Brahmaroutu <srbrahma@us.ibm.com>
|
||||
Stavros Panakakis <stavrospanakakis@gmail.com>
|
||||
Stefan S. <tronicum@user.github.com>
|
||||
Stefan Scherer <stefan.scherer@docker.com>
|
||||
Stefan Weil <sw@weilnetz.de>
|
||||
|
@ -804,7 +780,6 @@ Steve Durrheimer <s.durrheimer@gmail.com>
|
|||
Steve Richards <steve.richards@docker.com>
|
||||
Steven Burgess <steven.a.burgess@hotmail.com>
|
||||
Stoica-Marcu Floris-Andrei <floris.sm@gmail.com>
|
||||
Stuart Williams <pid@pidster.com>
|
||||
Subhajit Ghosh <isubuz.g@gmail.com>
|
||||
Sun Jianbo <wonderflow.sun@gmail.com>
|
||||
Sune Keller <absukl@almbrand.dk>
|
||||
|
@ -892,7 +867,6 @@ Wang Yumu <37442693@qq.com>
|
|||
Wataru Ishida <ishida.wataru@lab.ntt.co.jp>
|
||||
Wayne Song <wsong@docker.com>
|
||||
Wen Cheng Ma <wenchma@cn.ibm.com>
|
||||
Wenlong Zhang <zhangwenlong@loongson.cn>
|
||||
Wenzhi Liang <wenzhi.liang@gmail.com>
|
||||
Wes Morgan <cap10morgan@gmail.com>
|
||||
Wewang Xiaorenfine <wang.xiaoren@zte.com.cn>
|
||||
|
@ -934,4 +908,3 @@ Zhuo Zhi <h.dwwwwww@gmail.com>
|
|||
Átila Camurça Alves <camurca.home@gmail.com>
|
||||
Александр Менщиков <__Singleton__@hackerdom.ru>
|
||||
徐俊杰 <paco.xu@daocloud.io>
|
||||
林博仁 Buo-ren Lin <Buo.Ren.Lin@gmail.com>
|
||||
|
|
|
@ -66,7 +66,7 @@ anybody starts working on it.
|
|||
We are always thrilled to receive pull requests. We do our best to process them
|
||||
quickly. If your pull request is not accepted on the first try,
|
||||
don't get discouraged! Our contributor's guide explains [the review process we
|
||||
use for simple changes](https://github.com/docker/docker/blob/master/project/REVIEWING.md).
|
||||
use for simple changes](https://docs.docker.com/opensource/workflow/make-a-contribution/).
|
||||
|
||||
### Talking to other Docker users and contributors
|
||||
|
||||
|
@ -124,8 +124,8 @@ submitting a pull request.
|
|||
Update the documentation when creating or modifying features. Test your
|
||||
documentation changes for clarity, concision, and correctness, as well as a
|
||||
clean documentation build. See our contributors guide for [our style
|
||||
guide](https://docs.docker.com/contribute/style/grammar/) and instructions on [building
|
||||
the documentation](https://docs.docker.com/contribute/).
|
||||
guide](https://docs.docker.com/opensource/doc-style) and instructions on [building
|
||||
the documentation](https://docs.docker.com/opensource/project/test-and-docs/#build-and-test-the-documentation).
|
||||
|
||||
Write clean code. Universally formatted code promotes ease of writing, reading,
|
||||
and maintenance. Always run `gofmt -s -w file.go` on each changed file before
|
||||
|
@ -134,41 +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
|
||||
to all the issues that they address.
|
||||
|
||||
Commit messages must be written in the imperative mood (max. 72 chars), followed
|
||||
by an optional, more detailed explanatory text usually expanding on
|
||||
why the work is necessary. The explanatory text should be separated by an
|
||||
empty line.
|
||||
|
||||
The commit message *could* have a prefix scoping the change, however this is
|
||||
not enforced. Common prefixes are `docs: <message>`, `vendor: <message>`,
|
||||
`chore: <message>` or the package/area related to the change such as `pkg/foo: <message>`
|
||||
or `telemetry: <message>`.
|
||||
|
||||
A standard commit.
|
||||
```
|
||||
Fix the exploding flux capacitor
|
||||
|
||||
A call to function A causes the flux capacitor to blow up every time
|
||||
the sun and the moon align.
|
||||
```
|
||||
|
||||
Using a package as prefix.
|
||||
```
|
||||
pkg/foo: prevent panic in flux capacitor
|
||||
|
||||
Calling function A causes the flux capacitor to blow up every time
|
||||
the sun and the moon align.
|
||||
```
|
||||
|
||||
Updating a specific vendored package.
|
||||
```
|
||||
vendor: github.com/docker/docker 6ac445c42bad (master, v28.0-dev)
|
||||
```
|
||||
|
||||
Fixing a broken docs link.
|
||||
```
|
||||
docs: fix style/lint issues in deprecated.md
|
||||
```
|
||||
Commit messages must start with a capitalized and short summary (max. 50 chars)
|
||||
written in the imperative, followed by an optional, more detailed explanatory
|
||||
text which is separated from the summary by an empty line.
|
||||
|
||||
Code review comments may be added to your pull request. Discuss, then make the
|
||||
suggested modifications and push additional commits to your feature branch. Post
|
||||
|
|
35
Dockerfile
35
Dockerfile
|
@ -1,37 +1,22 @@
|
|||
# syntax=docker/dockerfile:1
|
||||
|
||||
ARG BASE_VARIANT=alpine
|
||||
|
||||
# ALPINE_VERSION sets the version of the alpine base image to use, including for the golang image.
|
||||
# It must be a supported tag in the docker.io/library/alpine image repository
|
||||
# that's also available as alpine image variant for the Golang version used.
|
||||
ARG ALPINE_VERSION=3.22
|
||||
ARG ALPINE_VERSION=3.20
|
||||
ARG BASE_DEBIAN_DISTRO=bookworm
|
||||
|
||||
ARG GO_VERSION=1.24.5
|
||||
ARG XX_VERSION=1.6.1
|
||||
ARG GOVERSIONINFO_VERSION=v1.4.1
|
||||
|
||||
# GOTESTSUM_VERSION sets the version of gotestsum to install in the dev container.
|
||||
# It must be a valid tag in the https://github.com/gotestyourself/gotestsum repository.
|
||||
ARG GOTESTSUM_VERSION=v1.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
|
||||
ARG GO_VERSION=1.22.7
|
||||
ARG XX_VERSION=1.5.0
|
||||
ARG GOVERSIONINFO_VERSION=v1.3.0
|
||||
ARG GOTESTSUM_VERSION=v1.10.0
|
||||
ARG BUILDX_VERSION=0.16.1
|
||||
ARG COMPOSE_VERSION=v2.29.0
|
||||
|
||||
FROM --platform=$BUILDPLATFORM tonistiigi/xx:${XX_VERSION} AS xx
|
||||
|
||||
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS build-base-alpine
|
||||
ENV GOTOOLCHAIN=local
|
||||
COPY --link --from=xx / /
|
||||
RUN apk add --no-cache bash clang lld llvm file git git-daemon
|
||||
RUN apk add --no-cache bash clang lld llvm file git
|
||||
WORKDIR /go/src/github.com/docker/cli
|
||||
|
||||
FROM build-base-alpine AS build-alpine
|
||||
|
@ -78,7 +63,7 @@ ARG PACKAGER_NAME
|
|||
COPY --link --from=goversioninfo /out/goversioninfo /usr/bin/goversioninfo
|
||||
RUN --mount=type=bind,target=.,ro \
|
||||
--mount=type=cache,target=/root/.cache \
|
||||
--mount=type=tmpfs,target=cmd/docker/winresources \
|
||||
--mount=type=tmpfs,target=cli/winresources \
|
||||
# override the default behavior of go with xx-go
|
||||
xx-go --wrap && \
|
||||
# export GOCACHE=$(go env GOCACHE)/$(xx-info)$([ -f /etc/alpine-release ] && echo "alpine") && \
|
||||
|
@ -128,7 +113,7 @@ COPY --link --from=compose /docker-compose /usr/libexec/docker/cli-plugins/docke
|
|||
COPY --link . .
|
||||
ENV DOCKER_BUILDKIT=1
|
||||
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
|
||||
COPY --link . .
|
||||
|
|
47
Makefile
47
Makefile
|
@ -52,7 +52,7 @@ shellcheck: ## run shellcheck validation
|
|||
.PHONY: fmt
|
||||
fmt: ## run gofumpt (if present) or gofmt
|
||||
@if command -v gofumpt > /dev/null; then \
|
||||
gofumpt -w -d -lang=1.23 . ; \
|
||||
gofumpt -w -d -lang=1.21 . ; \
|
||||
else \
|
||||
go list -f {{.Dir}} ./... | xargs gofmt -w -s -d ; \
|
||||
fi
|
||||
|
@ -67,63 +67,46 @@ dynbinary: ## build dynamically linked binary
|
|||
|
||||
.PHONY: plugins
|
||||
plugins: ## build example CLI plugins
|
||||
scripts/build/plugins
|
||||
./scripts/build/plugins
|
||||
|
||||
.PHONY: vendor
|
||||
vendor: ## update vendor with go modules
|
||||
rm -rf vendor
|
||||
scripts/with-go-mod.sh scripts/vendor update
|
||||
./scripts/vendor update
|
||||
|
||||
.PHONY: validate-vendor
|
||||
validate-vendor: ## validate vendor
|
||||
scripts/with-go-mod.sh scripts/vendor validate
|
||||
./scripts/vendor validate
|
||||
|
||||
.PHONY: mod-outdated
|
||||
mod-outdated: ## check outdated dependencies
|
||||
scripts/with-go-mod.sh scripts/vendor outdated
|
||||
./scripts/vendor outdated
|
||||
|
||||
.PHONY: authors
|
||||
authors: ## generate AUTHORS file from git history
|
||||
scripts/docs/generate-authors.sh
|
||||
|
||||
.PHONY: completion
|
||||
completion: shell-completion
|
||||
completion: ## generate and install the shell-completion scripts
|
||||
# Note: this uses system-wide paths, and so may overwrite completion
|
||||
# scripts installed as part of deb/rpm packages.
|
||||
#
|
||||
# Given that this target is intended to debug/test updated versions, we could
|
||||
# consider installing in per-user (~/.config, XDG_DATA_DIR) paths instead, but
|
||||
# this will add more complexity.
|
||||
#
|
||||
# See https://github.com/docker/cli/pull/5770#discussion_r1927772710
|
||||
install -D -p -m 0644 ./build/completion/bash/docker /usr/share/bash-completion/completions/docker
|
||||
install -D -p -m 0644 ./build/completion/fish/docker.fish debian/docker-ce-cli/usr/share/fish/vendor_completions.d/docker.fish
|
||||
install -D -p -m 0644 ./build/completion/zsh/_docker debian/docker-ce-cli/usr/share/zsh/vendor-completions/_docker
|
||||
completion: binary
|
||||
completion: /etc/bash_completion.d/docker
|
||||
completion: ## generate and install the completion scripts
|
||||
|
||||
|
||||
build/docker:
|
||||
# This target is used by the "shell-completion" target, which requires either
|
||||
# "binary" or "dynbinary" to have been built. We don't want to trigger those
|
||||
# to prevent replacing a static binary with a dynamic one, or vice-versa.
|
||||
@echo "Run 'make binary' or 'make dynbinary' first" && exit 1
|
||||
|
||||
.PHONY: shell-completion
|
||||
shell-completion: build/docker # requires either "binary" or "dynbinary" to be built.
|
||||
shell-completion: ## generate shell-completion scripts
|
||||
@ ./scripts/build/shell-completion
|
||||
.PHONY: /etc/bash_completion.d/docker
|
||||
/etc/bash_completion.d/docker: ## generate and install the bash-completion script
|
||||
mkdir -p /etc/bash_completion.d
|
||||
docker completion bash > /etc/bash_completion.d/docker
|
||||
|
||||
.PHONY: manpages
|
||||
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
|
||||
mddocs: ## generate markdown files from go source
|
||||
scripts/with-go-mod.sh scripts/docs/generate-md.sh
|
||||
scripts/docs/generate-md.sh
|
||||
|
||||
.PHONY: yamldocs
|
||||
yamldocs: ## generate documentation YAML files consumed by docs repo
|
||||
scripts/with-go-mod.sh scripts/docs/generate-yaml.sh
|
||||
scripts/docs/generate-yaml.sh
|
||||
|
||||
.PHONY: help
|
||||
help: ## print this help
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
# 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%3Atest)
|
||||
[](https://goreportcard.com/report/github.com/docker/cli)
|
||||
[](https://scorecard.dev/viewer/?uri=github.com/docker/cli)
|
||||
[](https://codecov.io/gh/docker/cli)
|
||||
|
||||
## 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"
|
||||
"os"
|
||||
|
||||
"github.com/docker/cli/cli-plugins/metadata"
|
||||
"github.com/docker/cli/cli-plugins/manager"
|
||||
"github.com/docker/cli/cli-plugins/plugin"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func main() {
|
||||
plugin.Run(func(dockerCLI command.Cli) *cobra.Command {
|
||||
plugin.Run(func(dockerCli command.Cli) *cobra.Command {
|
||||
goodbye := &cobra.Command{
|
||||
Use: "goodbye",
|
||||
Short: "Say Goodbye instead of Hello",
|
||||
Run: func(cmd *cobra.Command, _ []string) {
|
||||
_, _ = fmt.Fprintln(dockerCLI.Out(), "Goodbye World!")
|
||||
fmt.Fprintln(dockerCli.Out(), "Goodbye World!")
|
||||
},
|
||||
}
|
||||
apiversion := &cobra.Command{
|
||||
Use: "apiversion",
|
||||
Short: "Print the API version of the server",
|
||||
RunE: func(_ *cobra.Command, _ []string) error {
|
||||
apiClient := dockerCLI.Client()
|
||||
ping, err := apiClient.Ping(context.Background())
|
||||
cli := dockerCli.Client()
|
||||
ping, err := cli.Ping(context.Background())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, _ = fmt.Println(ping.APIVersion)
|
||||
fmt.Println(ping.APIVersion)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
@ -38,7 +38,7 @@ func main() {
|
|||
Use: "exitstatus2",
|
||||
Short: "Exit with status 2",
|
||||
RunE: func(_ *cobra.Command, _ []string) error {
|
||||
_, _ = fmt.Fprintln(dockerCLI.Err(), "Exiting with error status 2")
|
||||
fmt.Fprintln(dockerCli.Err(), "Exiting with error status 2")
|
||||
os.Exit(2)
|
||||
return nil
|
||||
},
|
||||
|
@ -56,33 +56,33 @@ func main() {
|
|||
return err
|
||||
}
|
||||
if preRun {
|
||||
_, _ = fmt.Fprintln(dockerCLI.Err(), "Plugin PersistentPreRunE called")
|
||||
fmt.Fprintf(dockerCli.Err(), "Plugin PersistentPreRunE called")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if debug {
|
||||
_, _ = fmt.Fprintln(dockerCLI.Err(), "Plugin debug mode enabled")
|
||||
fmt.Fprintf(dockerCli.Err(), "Plugin debug mode enabled")
|
||||
}
|
||||
|
||||
switch optContext {
|
||||
case "Christmas":
|
||||
_, _ = fmt.Fprintln(dockerCLI.Out(), "Merry Christmas!")
|
||||
fmt.Fprintf(dockerCli.Out(), "Merry Christmas!\n")
|
||||
return nil
|
||||
case "":
|
||||
// nothing
|
||||
}
|
||||
|
||||
if who == "" {
|
||||
who, _ = dockerCLI.ConfigFile().PluginConfig("helloworld", "who")
|
||||
who, _ = dockerCli.ConfigFile().PluginConfig("helloworld", "who")
|
||||
}
|
||||
if who == "" {
|
||||
who = "World"
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintln(dockerCLI.Out(), "Hello", who)
|
||||
dockerCLI.ConfigFile().SetPluginConfig("helloworld", "lastwho", who)
|
||||
return dockerCLI.ConfigFile().Save()
|
||||
fmt.Fprintf(dockerCli.Out(), "Hello %s!\n", who)
|
||||
dockerCli.ConfigFile().SetPluginConfig("helloworld", "lastwho", who)
|
||||
return dockerCli.ConfigFile().Save()
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -97,7 +97,7 @@ func main() {
|
|||
cmd.AddCommand(goodbye, apiversion, exitStatus2)
|
||||
return cmd
|
||||
},
|
||||
metadata.Metadata{
|
||||
manager.Metadata{
|
||||
SchemaVersion: "0.1.0",
|
||||
Vendor: "Docker Inc.",
|
||||
Version: "testing",
|
||||
|
|
|
@ -11,8 +11,8 @@ func PrintNextSteps(out io.Writer, messages []string) {
|
|||
if len(messages) == 0 {
|
||||
return
|
||||
}
|
||||
_, _ = fmt.Fprintln(out, aec.Bold.Apply("\nWhat's next:"))
|
||||
fmt.Fprintln(out, aec.Bold.Apply("\nWhat's next:"))
|
||||
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
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
|
||||
"github.com/docker/cli/cli-plugins/metadata"
|
||||
)
|
||||
import "os/exec"
|
||||
|
||||
// Candidate represents a possible plugin candidate, for mocking purposes
|
||||
type Candidate interface {
|
||||
|
@ -21,5 +17,5 @@ func (c *candidate) Path() string {
|
|||
}
|
||||
|
||||
func (c *candidate) Metadata() ([]byte, error) {
|
||||
return exec.Command(c.path, metadata.MetadataSubcommandName).Output() // #nosec G204 -- ignore "Subprocess launched with a potential tainted input or cmd arguments"
|
||||
return exec.Command(c.path, MetadataSubcommandName).Output()
|
||||
}
|
||||
|
|
|
@ -6,10 +6,9 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli-plugins/metadata"
|
||||
"github.com/spf13/cobra"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
"gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
|
||||
type fakeCandidate struct {
|
||||
|
@ -31,10 +30,10 @@ func (c *fakeCandidate) Metadata() ([]byte, error) {
|
|||
|
||||
func TestValidateCandidate(t *testing.T) {
|
||||
const (
|
||||
goodPluginName = metadata.NamePrefix + "goodplugin"
|
||||
goodPluginName = NamePrefix + "goodplugin"
|
||||
|
||||
builtinName = metadata.NamePrefix + "builtin"
|
||||
builtinAlias = metadata.NamePrefix + "alias"
|
||||
builtinName = NamePrefix + "builtin"
|
||||
builtinAlias = NamePrefix + "alias"
|
||||
|
||||
badPrefixPath = "/usr/local/libexec/cli-plugins/wobble"
|
||||
badNamePath = "/usr/local/libexec/cli-plugins/docker-123456"
|
||||
|
@ -44,9 +43,9 @@ func TestValidateCandidate(t *testing.T) {
|
|||
|
||||
fakeroot := &cobra.Command{Use: "docker"}
|
||||
fakeroot.AddCommand(&cobra.Command{
|
||||
Use: strings.TrimPrefix(builtinName, metadata.NamePrefix),
|
||||
Use: strings.TrimPrefix(builtinName, NamePrefix),
|
||||
Aliases: []string{
|
||||
strings.TrimPrefix(builtinAlias, metadata.NamePrefix),
|
||||
strings.TrimPrefix(builtinAlias, NamePrefix),
|
||||
},
|
||||
})
|
||||
|
||||
|
@ -60,7 +59,7 @@ func TestValidateCandidate(t *testing.T) {
|
|||
}{
|
||||
/* Each failing one of the tests */
|
||||
{name: "empty path", c: &fakeCandidate{path: ""}, err: "plugin candidate path cannot be empty"},
|
||||
{name: "bad prefix", c: &fakeCandidate{path: badPrefixPath}, err: fmt.Sprintf("does not have %q prefix", metadata.NamePrefix)},
|
||||
{name: "bad prefix", c: &fakeCandidate{path: badPrefixPath}, err: fmt.Sprintf("does not have %q prefix", NamePrefix)},
|
||||
{name: "bad path", c: &fakeCandidate{path: badNamePath}, invalid: "did not match"},
|
||||
{name: "builtin command", c: &fakeCandidate{path: builtinName}, invalid: `plugin "builtin" duplicates builtin command`},
|
||||
{name: "builtin alias", c: &fakeCandidate{path: builtinAlias}, invalid: `plugin "alias" duplicates an alias of builtin command "builtin"`},
|
||||
|
@ -81,11 +80,11 @@ func TestValidateCandidate(t *testing.T) {
|
|||
assert.ErrorContains(t, err, tc.err)
|
||||
case tc.invalid != "":
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, is.ErrorType(p.Err, reflect.TypeOf(&pluginError{})))
|
||||
assert.Assert(t, cmp.ErrorType(p.Err, reflect.TypeOf(&pluginError{})))
|
||||
assert.ErrorContains(t, p.Err, tc.invalid)
|
||||
default:
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, metadata.NamePrefix+p.Name, goodPluginName)
|
||||
assert.Equal(t, NamePrefix+p.Name, goodPluginName)
|
||||
assert.Equal(t, p.SchemaVersion, "0.1.0")
|
||||
assert.Equal(t, p.Vendor, "e2e-testing")
|
||||
}
|
||||
|
|
|
@ -2,12 +2,41 @@ package manager
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/docker/cli/cli-plugins/metadata"
|
||||
"github.com/docker/cli/cli/config"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"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
|
||||
|
@ -15,25 +44,26 @@ var pluginCommandStubsOnce sync.Once
|
|||
// AddPluginCommandStubs adds a stub cobra.Commands for each valid and invalid
|
||||
// plugin. The command stubs will have several annotations added, see
|
||||
// `CommandAnnotationPlugin*`.
|
||||
func AddPluginCommandStubs(dockerCLI config.Provider, rootCmd *cobra.Command) (err error) {
|
||||
func AddPluginCommandStubs(dockerCli command.Cli, rootCmd *cobra.Command) (err error) {
|
||||
pluginCommandStubsOnce.Do(func() {
|
||||
var plugins []Plugin
|
||||
plugins, err = ListPlugins(dockerCLI, rootCmd)
|
||||
plugins, err = ListPlugins(dockerCli, rootCmd)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, p := range plugins {
|
||||
p := p
|
||||
vendor := p.Vendor
|
||||
if vendor == "" {
|
||||
vendor = "unknown"
|
||||
}
|
||||
annotations := map[string]string{
|
||||
metadata.CommandAnnotationPlugin: "true",
|
||||
metadata.CommandAnnotationPluginVendor: vendor,
|
||||
metadata.CommandAnnotationPluginVersion: p.Version,
|
||||
CommandAnnotationPlugin: "true",
|
||||
CommandAnnotationPluginVendor: vendor,
|
||||
CommandAnnotationPluginVersion: p.Version,
|
||||
}
|
||||
if p.Err != nil {
|
||||
annotations[metadata.CommandAnnotationPluginInvalid] = p.Err.Error()
|
||||
annotations[CommandAnnotationPluginInvalid] = p.Err.Error()
|
||||
}
|
||||
rootCmd.AddCommand(&cobra.Command{
|
||||
Use: p.Name,
|
||||
|
@ -52,7 +82,7 @@ func AddPluginCommandStubs(dockerCLI config.Provider, rootCmd *cobra.Command) (e
|
|||
cmd.HelpFunc()(rootCmd, args)
|
||||
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) {
|
||||
// Delegate completion to plugin
|
||||
|
@ -60,7 +90,7 @@ func AddPluginCommandStubs(dockerCLI config.Provider, rootCmd *cobra.Command) (e
|
|||
cargs = append(cargs, args...)
|
||||
cargs = append(cargs, toComplete)
|
||||
os.Args = cargs
|
||||
runCommand, runErr := PluginRunCommand(dockerCLI, p.Name, cmd)
|
||||
runCommand, runErr := PluginRunCommand(dockerCli, p.Name, cmd)
|
||||
if runErr != nil {
|
||||
return nil, cobra.ShellCompDirectiveError
|
||||
}
|
||||
|
@ -75,3 +105,44 @@ func AddPluginCommandStubs(dockerCLI config.Provider, rootCmd *cobra.Command) (e
|
|||
})
|
||||
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:
|
||||
//go:build go1.23
|
||||
//go:build go1.21
|
||||
|
||||
package manager
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// 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
|
||||
// additional message.
|
||||
// additional message, analogous to errors.Wrapf.
|
||||
func wrapAsPluginError(err error, msg string) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
return &pluginError{cause: fmt.Errorf("%s: %w", msg, err)}
|
||||
return &pluginError{cause: errors.Wrap(err, msg)}
|
||||
}
|
||||
|
||||
// NewPluginError creates a new pluginError, analogous to
|
||||
// errors.Errorf.
|
||||
func NewPluginError(msg string, args ...any) error {
|
||||
return &pluginError{cause: fmt.Errorf(msg, args...)}
|
||||
return &pluginError{cause: errors.Errorf(msg, args...)}
|
||||
}
|
||||
|
|
|
@ -6,8 +6,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli-plugins/hooks"
|
||||
"github.com/docker/cli/cli/config"
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
|
@ -30,28 +29,29 @@ type HookPluginData struct {
|
|||
// a main CLI command was executed. It calls the hook subcommand for all
|
||||
// present CLI plugins that declare support for hooks in their metadata and
|
||||
// parses/prints their responses.
|
||||
func RunCLICommandHooks(ctx context.Context, dockerCLI config.Provider, rootCmd, subCommand *cobra.Command, cmdErrorMessage string) {
|
||||
func RunCLICommandHooks(ctx context.Context, dockerCli command.Cli, rootCmd, subCommand *cobra.Command, cmdErrorMessage string) {
|
||||
commandName := strings.TrimPrefix(subCommand.CommandPath(), rootCmd.Name()+" ")
|
||||
flags := getCommandFlags(subCommand)
|
||||
|
||||
runHooks(ctx, dockerCLI.ConfigFile(), rootCmd, subCommand, commandName, flags, cmdErrorMessage)
|
||||
runHooks(ctx, dockerCli, rootCmd, subCommand, commandName, flags, cmdErrorMessage)
|
||||
}
|
||||
|
||||
// RunPluginHooks is the entrypoint for the hooks execution flow
|
||||
// after a plugin command was just executed by the CLI.
|
||||
func RunPluginHooks(ctx context.Context, dockerCLI config.Provider, rootCmd, subCommand *cobra.Command, args []string) {
|
||||
func RunPluginHooks(ctx context.Context, dockerCli command.Cli, rootCmd, subCommand *cobra.Command, args []string) {
|
||||
commandName := strings.Join(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) {
|
||||
nextSteps := invokeAndCollectHooks(ctx, cfg, rootCmd, subCommand, invokedCommand, flags, cmdErrorMessage)
|
||||
hooks.PrintNextSteps(subCommand.ErrOrStderr(), nextSteps)
|
||||
func runHooks(ctx context.Context, dockerCli command.Cli, rootCmd, subCommand *cobra.Command, invokedCommand string, flags map[string]string, cmdErrorMessage string) {
|
||||
nextSteps := invokeAndCollectHooks(ctx, dockerCli, rootCmd, subCommand, invokedCommand, flags, cmdErrorMessage)
|
||||
|
||||
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
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
|
@ -59,20 +59,19 @@ func invokeAndCollectHooks(ctx context.Context, cfg *configfile.ConfigFile, root
|
|||
default:
|
||||
}
|
||||
|
||||
pluginsCfg := cfg.Plugins
|
||||
pluginsCfg := dockerCli.ConfigFile().Plugins
|
||||
if pluginsCfg == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
pluginDirs := getPluginDirs(cfg)
|
||||
nextSteps := make([]string, 0, len(pluginsCfg))
|
||||
for pluginName, pluginCfg := range pluginsCfg {
|
||||
match, ok := pluginMatch(pluginCfg, subCmdStr)
|
||||
for pluginName, cfg := range pluginsCfg {
|
||||
match, ok := pluginMatch(cfg, subCmdStr)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
p, err := getPlugin(pluginName, pluginDirs, rootCmd)
|
||||
p, err := GetPlugin(pluginName, dockerCli, rootCmd)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import (
|
|||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/docker/cli/cli-plugins/metadata"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/config"
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
"github.com/fvbommel/sortorder"
|
||||
|
@ -22,19 +22,17 @@ const (
|
|||
// used to originally invoke the docker CLI when executing a
|
||||
// plugin. Assuming $PATH and $CWD remain unchanged this should allow
|
||||
// the plugin to re-execute the original CLI.
|
||||
ReexecEnvvar = metadata.ReexecEnvvar
|
||||
ReexecEnvvar = "DOCKER_CLI_PLUGIN_ORIGINAL_CLI_COMMAND"
|
||||
|
||||
// ResourceAttributesEnvvar is the name of the envvar that includes additional
|
||||
// resource attributes for OTEL.
|
||||
//
|
||||
// Deprecated: The "OTEL_RESOURCE_ATTRIBUTES" env-var is part of the OpenTelemetry specification; users should define their own const for this. This const will be removed in the next release.
|
||||
ResourceAttributesEnvvar = "OTEL_RESOURCE_ATTRIBUTES"
|
||||
)
|
||||
|
||||
// errPluginNotFound is the error returned when a plugin could not be found.
|
||||
type errPluginNotFound string
|
||||
|
||||
func (errPluginNotFound) NotFound() {}
|
||||
func (e errPluginNotFound) NotFound() {}
|
||||
|
||||
func (e errPluginNotFound) Error() string {
|
||||
return "Error: No such CLI plugin: " + string(e)
|
||||
|
@ -61,27 +59,29 @@ func IsNotFound(err error) bool {
|
|||
// 3. Platform-specific defaultSystemPluginDirs.
|
||||
//
|
||||
// [ConfigFile.CLIPluginsExtraDirs]: https://pkg.go.dev/github.com/docker/cli@v26.1.4+incompatible/cli/config/configfile#ConfigFile.CLIPluginsExtraDirs
|
||||
func getPluginDirs(cfg *configfile.ConfigFile) []string {
|
||||
func getPluginDirs(cfg *configfile.ConfigFile) ([]string, error) {
|
||||
var pluginDirs []string
|
||||
|
||||
if cfg != nil {
|
||||
pluginDirs = append(pluginDirs, cfg.CLIPluginsExtraDirs...)
|
||||
}
|
||||
pluginDir := filepath.Join(config.Dir(), "cli-plugins")
|
||||
pluginDir, err := config.Path("cli-plugins")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pluginDirs = append(pluginDirs, pluginDir)
|
||||
pluginDirs = append(pluginDirs, defaultSystemPluginDirs...)
|
||||
return pluginDirs
|
||||
return pluginDirs, nil
|
||||
}
|
||||
|
||||
func addPluginCandidatesFromDir(res map[string][]string, d string) {
|
||||
func addPluginCandidatesFromDir(res map[string][]string, d string) error {
|
||||
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 {
|
||||
return
|
||||
return err
|
||||
}
|
||||
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:
|
||||
// Regular file or symlink, keep going
|
||||
default:
|
||||
|
@ -89,35 +89,52 @@ func addPluginCandidatesFromDir(res map[string][]string, d string) {
|
|||
continue
|
||||
}
|
||||
name := dentry.Name()
|
||||
if !strings.HasPrefix(name, metadata.NamePrefix) {
|
||||
if !strings.HasPrefix(name, NamePrefix) {
|
||||
continue
|
||||
}
|
||||
name = strings.TrimPrefix(name, metadata.NamePrefix)
|
||||
name = strings.TrimPrefix(name, NamePrefix)
|
||||
var err error
|
||||
if name, err = trimExeSuffix(name); err != nil {
|
||||
continue
|
||||
}
|
||||
res[name] = append(res[name], filepath.Join(d, dentry.Name()))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// listPluginCandidates returns a map from plugin name to the list of (unvalidated) Candidates. The list is in descending order of priority.
|
||||
func listPluginCandidates(dirs []string) map[string][]string {
|
||||
func listPluginCandidates(dirs []string) (map[string][]string, error) {
|
||||
result := make(map[string][]string)
|
||||
for _, d := range dirs {
|
||||
addPluginCandidatesFromDir(result, d)
|
||||
// Silently ignore any directories which we cannot
|
||||
// Stat (e.g. due to permissions or anything else) or
|
||||
// which is not a directory.
|
||||
if fi, err := os.Stat(d); err != nil || !fi.IsDir() {
|
||||
continue
|
||||
}
|
||||
if err := addPluginCandidatesFromDir(result, d); err != nil {
|
||||
// Silently ignore paths which don't exist.
|
||||
if os.IsNotExist(err) {
|
||||
continue
|
||||
}
|
||||
return nil, err // Or return partial result?
|
||||
}
|
||||
}
|
||||
return result
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetPlugin returns a plugin on the system by its name
|
||||
func GetPlugin(name string, dockerCLI config.Provider, rootcmd *cobra.Command) (*Plugin, error) {
|
||||
pluginDirs := getPluginDirs(dockerCLI.ConfigFile())
|
||||
return getPlugin(name, pluginDirs, rootcmd)
|
||||
}
|
||||
func GetPlugin(name string, dockerCli command.Cli, rootcmd *cobra.Command) (*Plugin, error) {
|
||||
pluginDirs, err := getPluginDirs(dockerCli.ConfigFile())
|
||||
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 len(paths) == 0 {
|
||||
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
|
||||
func ListPlugins(dockerCli config.Provider, rootcmd *cobra.Command) ([]Plugin, error) {
|
||||
pluginDirs := getPluginDirs(dockerCli.ConfigFile())
|
||||
candidates := listPluginCandidates(pluginDirs)
|
||||
if len(candidates) == 0 {
|
||||
return nil, nil
|
||||
func ListPlugins(dockerCli command.Cli, rootcmd *cobra.Command) ([]Plugin, error) {
|
||||
pluginDirs, err := getPluginDirs(dockerCli.ConfigFile())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
candidates, err := listPluginCandidates(pluginDirs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var plugins []Plugin
|
||||
var mu sync.Mutex
|
||||
ctx := rootcmd.Context()
|
||||
if ctx == nil {
|
||||
// Fallback, mostly for tests that pass a bare cobra.command
|
||||
ctx = context.Background()
|
||||
}
|
||||
eg, _ := errgroup.WithContext(ctx)
|
||||
eg, _ := errgroup.WithContext(context.TODO())
|
||||
cmds := rootcmd.Commands()
|
||||
for _, paths := range candidates {
|
||||
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.
|
||||
// The rootcmd argument is referenced to determine the set of builtin commands in order to detect conficts.
|
||||
// The error returned satisfies the IsNotFound() predicate if no plugin was found or if the first candidate plugin was invalid somehow.
|
||||
func PluginRunCommand(dockerCli config.Provider, name string, rootcmd *cobra.Command) (*exec.Cmd, error) {
|
||||
func PluginRunCommand(dockerCli command.Cli, name string, rootcmd *cobra.Command) (*exec.Cmd, error) {
|
||||
// This uses the full original args, not the args which may
|
||||
// have been provided by cobra to our caller. This is because
|
||||
// they lack e.g. global options which we must propagate here.
|
||||
|
@ -198,8 +214,11 @@ func PluginRunCommand(dockerCli config.Provider, name string, rootcmd *cobra.Com
|
|||
// fallback to their "invalid" command path.
|
||||
return nil, errPluginNotFound(name)
|
||||
}
|
||||
exename := addExeSuffix(metadata.NamePrefix + name)
|
||||
pluginDirs := getPluginDirs(dockerCli.ConfigFile())
|
||||
exename := addExeSuffix(NamePrefix + name)
|
||||
pluginDirs, err := getPluginDirs(dockerCli.ConfigFile())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, d := range pluginDirs {
|
||||
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?
|
||||
return nil, errPluginNotFound(name)
|
||||
}
|
||||
cmd := exec.Command(plugin.Path, args...) // #nosec G204 -- ignore "Subprocess launched with a potential tainted input or cmd arguments"
|
||||
|
||||
cmd := exec.Command(plugin.Path, args...)
|
||||
// Using dockerCli.{In,Out,Err}() here results in a hang until something is input.
|
||||
// See: - https://github.com/golang/go/issues/10338
|
||||
// - https://github.com/golang/go/commit/d000e8742a173aa0659584aa01b7ba2834ba28ab
|
||||
|
@ -232,7 +250,7 @@ func PluginRunCommand(dockerCli config.Provider, name string, rootcmd *cobra.Com
|
|||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
cmd.Env = append(cmd.Environ(), metadata.ReexecEnvvar+"="+os.Args[0])
|
||||
cmd.Env = append(cmd.Environ(), ReexecEnvvar+"="+os.Args[0])
|
||||
cmd.Env = appendPluginResourceAttributesEnvvar(cmd.Env, rootcmd, plugin)
|
||||
|
||||
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.
|
||||
func IsPluginCommand(cmd *cobra.Command) bool {
|
||||
return cmd.Annotations[metadata.CommandAnnotationPlugin] == "true"
|
||||
return cmd.Annotations[CommandAnnotationPlugin] == "true"
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package manager
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
|
@ -52,7 +51,8 @@ func TestListPluginCandidates(t *testing.T) {
|
|||
dirs = append(dirs, dir.Join(d))
|
||||
}
|
||||
|
||||
candidates := listPluginCandidates(dirs)
|
||||
candidates, err := listPluginCandidates(dirs)
|
||||
assert.NilError(t, err)
|
||||
exp := map[string][]string{
|
||||
"plugin1": {
|
||||
dir.Join("plugins1", "docker-plugin1"),
|
||||
|
@ -82,35 +82,6 @@ func TestListPluginCandidates(t *testing.T) {
|
|||
assert.DeepEqual(t, candidates, exp)
|
||||
}
|
||||
|
||||
func TestListPluginCandidatesEmpty(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
candidates := listPluginCandidates([]string{tmpDir, filepath.Join(tmpDir, "no-such-dir")})
|
||||
assert.Assert(t, len(candidates) == 0)
|
||||
}
|
||||
|
||||
// Regression test for https://github.com/docker/cli/issues/5643.
|
||||
// Check that inaccessible directories that come before accessible ones are ignored
|
||||
// and do not prevent the latter from being processed.
|
||||
func TestListPluginCandidatesInaccesibleDir(t *testing.T) {
|
||||
dir := fs.NewDir(t, t.Name(),
|
||||
fs.WithDir("no-perm", fs.WithMode(0)),
|
||||
fs.WithDir("plugins",
|
||||
fs.WithFile("docker-buildx", ""),
|
||||
),
|
||||
)
|
||||
defer dir.Remove()
|
||||
|
||||
candidates := listPluginCandidates([]string{
|
||||
dir.Join("no-perm"),
|
||||
dir.Join("plugins"),
|
||||
})
|
||||
assert.DeepEqual(t, candidates, map[string][]string{
|
||||
"buildx": {
|
||||
dir.Join("plugins", "docker-buildx"),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetPlugin(t *testing.T) {
|
||||
dir := fs.NewDir(t, t.Name(),
|
||||
fs.WithFile("docker-bbb", `
|
||||
|
@ -173,11 +144,14 @@ func TestErrPluginNotFound(t *testing.T) {
|
|||
func TestGetPluginDirs(t *testing.T) {
|
||||
cli := test.NewFakeCli(nil)
|
||||
|
||||
pluginDir := filepath.Join(config.Dir(), "cli-plugins")
|
||||
pluginDir, err := config.Path("cli-plugins")
|
||||
assert.NilError(t, err)
|
||||
expected := append([]string{pluginDir}, defaultSystemPluginDirs...)
|
||||
|
||||
pluginDirs := getPluginDirs(cli.ConfigFile())
|
||||
var pluginDirs []string
|
||||
pluginDirs, err = getPluginDirs(cli.ConfigFile())
|
||||
assert.Equal(t, strings.Join(expected, ":"), strings.Join(pluginDirs, ":"))
|
||||
assert.NilError(t, err)
|
||||
|
||||
extras := []string{
|
||||
"foo", "bar", "baz",
|
||||
|
@ -186,6 +160,7 @@ func TestGetPluginDirs(t *testing.T) {
|
|||
cli.SetConfigFile(&configfile.ConfigFile{
|
||||
CLIPluginsExtraDirs: extras,
|
||||
})
|
||||
pluginDirs = getPluginDirs(cli.ConfigFile())
|
||||
pluginDirs, err = getPluginDirs(cli.ConfigFile())
|
||||
assert.DeepEqual(t, expected, pluginDirs)
|
||||
assert.NilError(t, err)
|
||||
}
|
||||
|
|
|
@ -1,23 +1,30 @@
|
|||
package manager
|
||||
|
||||
import (
|
||||
"github.com/docker/cli/cli-plugins/metadata"
|
||||
)
|
||||
|
||||
const (
|
||||
// NamePrefix is the prefix required on all plugin binary names
|
||||
NamePrefix = metadata.NamePrefix
|
||||
NamePrefix = "docker-"
|
||||
|
||||
// MetadataSubcommandName is the name of the plugin subcommand
|
||||
// which must be supported by every plugin and returns the
|
||||
// plugin metadata.
|
||||
MetadataSubcommandName = metadata.MetadataSubcommandName
|
||||
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 = metadata.HookSubcommandName
|
||||
HookSubcommandName = "docker-cli-plugin-hooks"
|
||||
)
|
||||
|
||||
// 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 (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli-plugins/metadata"
|
||||
"github.com/docker/cli/internal/lazyregexp"
|
||||
"github.com/pkg/errors"
|
||||
"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.
|
||||
type Plugin struct {
|
||||
metadata.Metadata
|
||||
Metadata
|
||||
|
||||
Name 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.
|
||||
fullname := filepath.Base(path)
|
||||
if fullname == "." {
|
||||
return Plugin{}, fmt.Errorf("unable to determine basename of plugin candidate %q", path)
|
||||
return Plugin{}, errors.Errorf("unable to determine basename of plugin candidate %q", path)
|
||||
}
|
||||
var err error
|
||||
if fullname, err = trimExeSuffix(fullname); err != nil {
|
||||
return Plugin{}, fmt.Errorf("plugin candidate %q: %w", path, err)
|
||||
return Plugin{}, errors.Wrapf(err, "plugin candidate %q", path)
|
||||
}
|
||||
if !strings.HasPrefix(fullname, metadata.NamePrefix) {
|
||||
return Plugin{}, fmt.Errorf("plugin candidate %q: does not have %q prefix", path, metadata.NamePrefix)
|
||||
if !strings.HasPrefix(fullname, NamePrefix) {
|
||||
return Plugin{}, errors.Errorf("plugin candidate %q: does not have %q prefix", path, NamePrefix)
|
||||
}
|
||||
|
||||
p := Plugin{
|
||||
Name: strings.TrimPrefix(fullname, metadata.NamePrefix),
|
||||
Name: strings.TrimPrefix(fullname, NamePrefix),
|
||||
Path: path,
|
||||
}
|
||||
|
||||
|
@ -114,9 +112,9 @@ func (p *Plugin) RunHook(ctx context.Context, hookData HookPluginData) ([]byte,
|
|||
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 = append(pCmd.Env, metadata.ReexecEnvvar+"="+os.Args[0])
|
||||
pCmd.Env = append(pCmd.Env, ReexecEnvvar+"="+os.Args[0])
|
||||
hookCmdOutput, err := pCmd.Output()
|
||||
if err != nil {
|
||||
return nil, wrapAsPluginError(err, "failed to execute plugin hook subcommand")
|
||||
|
|
|
@ -1,16 +1,22 @@
|
|||
package manager
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// This is made slightly more complex due to needing to be case-insensitive.
|
||||
// This is made slightly more complex due to needing to be case insensitive.
|
||||
func trimExeSuffix(s string) (string, error) {
|
||||
ext := filepath.Ext(s)
|
||||
if ext == "" || !strings.EqualFold(ext, ".exe") {
|
||||
return "", fmt.Errorf("path %q lacks required file extension (.exe)", s)
|
||||
if ext == "" {
|
||||
return "", errors.Errorf("path %q lacks required file extension", s)
|
||||
}
|
||||
|
||||
exe := ".exe"
|
||||
if !strings.EqualFold(ext, exe) {
|
||||
return "", errors.Errorf("path %q lacks required %q suffix", s, exe)
|
||||
}
|
||||
return strings.TrimSuffix(s, ext), nil
|
||||
}
|
||||
|
|
|
@ -1,85 +0,0 @@
|
|||
package manager
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli-plugins/metadata"
|
||||
"github.com/spf13/cobra"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/baggage"
|
||||
)
|
||||
|
||||
const (
|
||||
// resourceAttributesEnvVar is the name of the envvar that includes additional
|
||||
// resource attributes for OTEL as defined in the [OpenTelemetry specification].
|
||||
//
|
||||
// [OpenTelemetry specification]: https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/#general-sdk-configuration
|
||||
resourceAttributesEnvVar = "OTEL_RESOURCE_ATTRIBUTES"
|
||||
|
||||
// dockerCLIAttributePrefix is the prefix for any docker cli OTEL attributes.
|
||||
//
|
||||
// It is a copy of the const defined in [command.dockerCLIAttributePrefix].
|
||||
dockerCLIAttributePrefix = "docker.cli."
|
||||
cobraCommandPath = attribute.Key("cobra.command_path")
|
||||
)
|
||||
|
||||
func getPluginResourceAttributes(cmd *cobra.Command, plugin Plugin) attribute.Set {
|
||||
commandPath := cmd.Annotations[metadata.CommandAnnotationPluginCommandPath]
|
||||
if commandPath == "" {
|
||||
commandPath = fmt.Sprintf("%s %s", cmd.CommandPath(), plugin.Name)
|
||||
}
|
||||
|
||||
attrSet := attribute.NewSet(
|
||||
cobraCommandPath.String(commandPath),
|
||||
)
|
||||
|
||||
kvs := make([]attribute.KeyValue, 0, attrSet.Len())
|
||||
for iter := attrSet.Iter(); iter.Next(); {
|
||||
attr := iter.Attribute()
|
||||
kvs = append(kvs, attribute.KeyValue{
|
||||
Key: dockerCLIAttributePrefix + attr.Key,
|
||||
Value: attr.Value,
|
||||
})
|
||||
}
|
||||
return attribute.NewSet(kvs...)
|
||||
}
|
||||
|
||||
func appendPluginResourceAttributesEnvvar(env []string, cmd *cobra.Command, plugin Plugin) []string {
|
||||
if attrs := getPluginResourceAttributes(cmd, plugin); attrs.Len() > 0 {
|
||||
// Construct baggage members for each of the attributes.
|
||||
// Ignore any failures as these aren't significant and
|
||||
// represent an internal issue.
|
||||
members := make([]baggage.Member, 0, attrs.Len())
|
||||
for iter := attrs.Iter(); iter.Next(); {
|
||||
attr := iter.Attribute()
|
||||
m, err := baggage.NewMemberRaw(string(attr.Key), attr.Value.AsString())
|
||||
if err != nil {
|
||||
otel.Handle(err)
|
||||
continue
|
||||
}
|
||||
members = append(members, m)
|
||||
}
|
||||
|
||||
// Combine plugin added resource attributes with ones found in the environment
|
||||
// variable. Our own attributes should be namespaced so there shouldn't be a
|
||||
// conflict. We do not parse the environment variable because we do not want
|
||||
// to handle errors in user configuration.
|
||||
attrsSlice := make([]string, 0, 2)
|
||||
if v := strings.TrimSpace(os.Getenv(resourceAttributesEnvVar)); v != "" {
|
||||
attrsSlice = append(attrsSlice, v)
|
||||
}
|
||||
if b, err := baggage.New(members...); err != nil {
|
||||
otel.Handle(err)
|
||||
} else if b.Len() > 0 {
|
||||
attrsSlice = append(attrsSlice, b.String())
|
||||
}
|
||||
|
||||
if len(attrsSlice) > 0 {
|
||||
env = append(env, resourceAttributesEnvVar+"="+strings.Join(attrsSlice, ","))
|
||||
}
|
||||
}
|
||||
return env
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
package metadata
|
||||
|
||||
const (
|
||||
// CommandAnnotationPlugin is added to every stub command added by
|
||||
// AddPluginCommandStubs with the value "true" and so can be
|
||||
// used to distinguish plugin stubs from regular commands.
|
||||
CommandAnnotationPlugin = "com.docker.cli.plugin"
|
||||
|
||||
// CommandAnnotationPluginVendor is added to every stub command
|
||||
// added by AddPluginCommandStubs and contains the vendor of
|
||||
// that plugin.
|
||||
CommandAnnotationPluginVendor = "com.docker.cli.plugin.vendor"
|
||||
|
||||
// CommandAnnotationPluginVersion is added to every stub command
|
||||
// added by AddPluginCommandStubs and contains the version of
|
||||
// that plugin.
|
||||
CommandAnnotationPluginVersion = "com.docker.cli.plugin.version"
|
||||
|
||||
// CommandAnnotationPluginInvalid is added to any stub command
|
||||
// added by AddPluginCommandStubs for an invalid command (that
|
||||
// is, one which failed it's candidate test) and contains the
|
||||
// reason for the failure.
|
||||
CommandAnnotationPluginInvalid = "com.docker.cli.plugin-invalid"
|
||||
|
||||
// CommandAnnotationPluginCommandPath is added to overwrite the
|
||||
// command path for a plugin invocation.
|
||||
CommandAnnotationPluginCommandPath = "com.docker.cli.plugin.command_path"
|
||||
)
|
|
@ -1,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 (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli-plugins/metadata"
|
||||
"github.com/docker/cli/cli-plugins/manager"
|
||||
"github.com/docker/cli/cli-plugins/socket"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/connhelper"
|
||||
"github.com/docker/cli/cli/debug"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/spf13/cobra"
|
||||
"go.opentelemetry.io/otel"
|
||||
)
|
||||
|
@ -30,12 +29,12 @@ import (
|
|||
var PersistentPreRunE func(*cobra.Command, []string) error
|
||||
|
||||
// RunPlugin executes the specified plugin command
|
||||
func RunPlugin(dockerCli *command.DockerCli, plugin *cobra.Command, meta metadata.Metadata) error {
|
||||
func RunPlugin(dockerCli *command.DockerCli, plugin *cobra.Command, meta manager.Metadata) error {
|
||||
tcmd := newPluginCommand(dockerCli, plugin, meta)
|
||||
|
||||
var persistentPreRunOnce sync.Once
|
||||
PersistentPreRunE = func(cmd *cobra.Command, _ []string) error {
|
||||
var retErr error
|
||||
var err error
|
||||
persistentPreRunOnce.Do(func() {
|
||||
ctx, cancel := context.WithCancel(cmd.Context())
|
||||
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, command.WithEnableGlobalMeterProvider(), command.WithEnableGlobalTracerProvider())
|
||||
retErr = tcmd.Initialize(opts...)
|
||||
err = tcmd.Initialize(opts...)
|
||||
ogRunE := cmd.RunE
|
||||
if ogRunE == nil {
|
||||
ogRun := cmd.Run
|
||||
|
@ -67,7 +66,7 @@ func RunPlugin(dockerCli *command.DockerCli, plugin *cobra.Command, meta metadat
|
|||
return err
|
||||
}
|
||||
})
|
||||
return retErr
|
||||
return err
|
||||
}
|
||||
|
||||
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.
|
||||
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)
|
||||
|
||||
dockerCli, err := command.NewDockerCli()
|
||||
|
@ -93,25 +92,26 @@ func Run(makeCmd func(command.Cli) *cobra.Command, meta metadata.Metadata) {
|
|||
plugin := makeCmd(dockerCli)
|
||||
|
||||
if err := RunPlugin(dockerCli, plugin, meta); err != nil {
|
||||
var stErr cli.StatusError
|
||||
if errors.As(err, &stErr) {
|
||||
if sterr, ok := err.(cli.StatusError); ok {
|
||||
if sterr.Status != "" {
|
||||
fmt.Fprintln(dockerCli.Err(), sterr.Status)
|
||||
}
|
||||
// StatusError should only be used for errors, and all errors should
|
||||
// have a non-zero exit status, so never exit with 0
|
||||
if stErr.StatusCode == 0 { // FIXME(thaJeztah): this should never be used with a zero status-code. Check if we do this anywhere.
|
||||
stErr.StatusCode = 1
|
||||
if sterr.StatusCode == 0 {
|
||||
os.Exit(1)
|
||||
}
|
||||
_, _ = fmt.Fprintln(dockerCli.Err(), stErr)
|
||||
os.Exit(stErr.StatusCode)
|
||||
os.Exit(sterr.StatusCode)
|
||||
}
|
||||
_, _ = fmt.Fprintln(dockerCli.Err(), err)
|
||||
fmt.Fprintln(dockerCli.Err(), err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func withPluginClientConn(name string) command.CLIOption {
|
||||
return func(cli *command.DockerCli) error {
|
||||
return command.WithInitializeClient(func(dockerCli *command.DockerCli) (client.APIClient, error) {
|
||||
cmd := "docker"
|
||||
if x := os.Getenv(metadata.ReexecEnvvar); x != "" {
|
||||
if x := os.Getenv(manager.ReexecEnvvar); x != "" {
|
||||
cmd = x
|
||||
}
|
||||
var flags []string
|
||||
|
@ -133,19 +133,16 @@ func withPluginClientConn(name string) command.CLIOption {
|
|||
|
||||
helper, err := connhelper.GetCommandConnectionHelper(cmd, flags...)
|
||||
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)
|
||||
}
|
||||
|
||||
return client.NewClientWithOpts(client.WithDialContext(helper.Dialer))
|
||||
})
|
||||
}
|
||||
|
||||
func newPluginCommand(dockerCli *command.DockerCli, plugin *cobra.Command, meta metadata.Metadata) *cli.TopLevelCommand {
|
||||
func newPluginCommand(dockerCli *command.DockerCli, plugin *cobra.Command, meta manager.Metadata) *cli.TopLevelCommand {
|
||||
name := plugin.Name()
|
||||
fullname := metadata.NamePrefix + name
|
||||
fullname := manager.NamePrefix + name
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: fmt.Sprintf("docker [OPTIONS] %s [ARG...]", name),
|
||||
|
@ -161,7 +158,7 @@ func newPluginCommand(dockerCli *command.DockerCli, plugin *cobra.Command, meta
|
|||
CompletionOptions: cobra.CompletionOptions{
|
||||
DisableDefaultCmd: false,
|
||||
HiddenDefaultCmd: true,
|
||||
DisableDescriptions: os.Getenv("DOCKER_CLI_DISABLE_COMPLETION_DESCRIPTION") != "",
|
||||
DisableDescriptions: true,
|
||||
},
|
||||
}
|
||||
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())
|
||||
}
|
||||
|
||||
func newMetadataSubcommand(plugin *cobra.Command, meta metadata.Metadata) *cobra.Command {
|
||||
func newMetadataSubcommand(plugin *cobra.Command, meta manager.Metadata) *cobra.Command {
|
||||
if meta.ShortDescription == "" {
|
||||
meta.ShortDescription = plugin.Short
|
||||
}
|
||||
cmd := &cobra.Command{
|
||||
Use: metadata.MetadataSubcommandName,
|
||||
Use: manager.MetadataSubcommandName,
|
||||
Hidden: true,
|
||||
// Suppress the global/parent PersistentPreRunE, which
|
||||
// needlessly initializes the client and tries to
|
||||
|
@ -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
|
||||
func RunningStandalone() bool {
|
||||
if os.Getenv(metadata.ReexecEnvvar) != "" {
|
||||
if os.Getenv(manager.ReexecEnvvar) != "" {
|
||||
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
|
||||
// relying on goroutine numbers to ensure correct behaviour
|
||||
t.Run("connect goroutine exits after EOF", func(t *testing.T) {
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
srv, err := NewPluginServer(nil)
|
||||
assert.NilError(t, err, "failed to setup server")
|
||||
|
||||
defer srv.Close()
|
||||
|
||||
t.Setenv(EnvKey, srv.Addr().String())
|
||||
|
||||
runtime.Gosched()
|
||||
numGoroutines := runtime.NumGoroutine()
|
||||
|
||||
ConnectAndWait(func() {})
|
||||
|
||||
runtime.Gosched()
|
||||
poll.WaitOn(t, func(t poll.LogT) poll.Result {
|
||||
// +1 goroutine for the poll.WaitOn
|
||||
// +1 goroutine for the connect goroutine
|
||||
if runtime.NumGoroutine() < numGoroutines+1+1 {
|
||||
return poll.Continue("waiting for connect goroutine to spawn")
|
||||
}
|
||||
return poll.Success()
|
||||
}, poll.WithDelay(1*time.Millisecond), poll.WithTimeout(500*time.Millisecond))
|
||||
assert.Equal(t, runtime.NumGoroutine(), numGoroutines+1)
|
||||
|
||||
srv.Close()
|
||||
|
||||
runtime.Gosched()
|
||||
poll.WaitOn(t, func(t poll.LogT) poll.Result {
|
||||
// +1 goroutine for the poll.WaitOn
|
||||
if runtime.NumGoroutine() > numGoroutines+1 {
|
||||
return poll.Continue("waiting for connect goroutine to exit")
|
||||
}
|
||||
return poll.Success()
|
||||
}, poll.WithDelay(1*time.Millisecond), poll.WithTimeout(500*time.Millisecond))
|
||||
}, 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 (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli-plugins/metadata"
|
||||
pluginmanager "github.com/docker/cli/cli-plugins/manager"
|
||||
"github.com/docker/cli/cli/command"
|
||||
cliflags "github.com/docker/cli/cli/flags"
|
||||
"github.com/docker/docker/pkg/homedir"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/fvbommel/sortorder"
|
||||
"github.com/moby/term"
|
||||
"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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
|
@ -82,8 +92,12 @@ func FlagErrorFunc(cmd *cobra.Command, err error) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
usage := ""
|
||||
if cmd.HasSubCommands() {
|
||||
usage = "\n\n" + cmd.UsageString()
|
||||
}
|
||||
return StatusError{
|
||||
Status: fmt.Sprintf("%s\n\nUsage: %s\n\nRun '%s --help' for more information", err, cmd.UseLine(), cmd.CommandPath()),
|
||||
Status: fmt.Sprintf("%s\nSee '%s --help'.%s", err, cmd.CommandPath(), usage),
|
||||
StatusCode: 125,
|
||||
}
|
||||
}
|
||||
|
@ -242,7 +256,7 @@ func hasAdditionalHelp(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 {
|
||||
|
@ -327,10 +341,8 @@ func operationSubCommands(cmd *cobra.Command) []*cobra.Command {
|
|||
return cmds
|
||||
}
|
||||
|
||||
const defaultTermWidth = 80
|
||||
|
||||
func wrappedFlagUsages(cmd *cobra.Command) string {
|
||||
width := defaultTermWidth
|
||||
width := 80
|
||||
if ws, err := term.GetWinsize(0); err == nil {
|
||||
width = int(ws.Width)
|
||||
}
|
||||
|
@ -346,9 +358,9 @@ func decoratedName(cmd *cobra.Command) string {
|
|||
}
|
||||
|
||||
func vendorAndVersion(cmd *cobra.Command) string {
|
||||
if vendor, ok := cmd.Annotations[metadata.CommandAnnotationPluginVendor]; ok && isPlugin(cmd) {
|
||||
if vendor, ok := cmd.Annotations[pluginmanager.CommandAnnotationPluginVendor]; ok && isPlugin(cmd) {
|
||||
version := ""
|
||||
if v, ok := cmd.Annotations[metadata.CommandAnnotationPluginVersion]; ok && v != "" {
|
||||
if v, ok := cmd.Annotations[pluginmanager.CommandAnnotationPluginVersion]; ok && v != "" {
|
||||
version = ", " + v
|
||||
}
|
||||
return fmt.Sprintf("(%s%s)", vendor, version)
|
||||
|
@ -407,7 +419,7 @@ func invalidPlugins(cmd *cobra.Command) []*cobra.Command {
|
|||
}
|
||||
|
||||
func invalidPluginReason(cmd *cobra.Command) string {
|
||||
return cmd.Annotations[metadata.CommandAnnotationPluginInvalid]
|
||||
return cmd.Annotations[pluginmanager.CommandAnnotationPluginInvalid]
|
||||
}
|
||||
|
||||
const usageTemplate = `Usage:
|
||||
|
@ -510,4 +522,4 @@ Run '{{.CommandPath}} COMMAND --help' for more information on a command.
|
|||
`
|
||||
|
||||
const helpTemplate = `
|
||||
{{- if or .Runnable .HasSubCommands}}{{.UsageString}}{{end}}`
|
||||
{{if or .Runnable .HasSubCommands}}{{.UsageString}}{{end}}`
|
||||
|
|
|
@ -3,7 +3,7 @@ package cli
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli-plugins/metadata"
|
||||
pluginmanager "github.com/docker/cli/cli-plugins/manager"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"github.com/spf13/cobra"
|
||||
"gotest.tools/v3/assert"
|
||||
|
@ -49,9 +49,9 @@ func TestVendorAndVersion(t *testing.T) {
|
|||
cmd := &cobra.Command{
|
||||
Use: "test",
|
||||
Annotations: map[string]string{
|
||||
metadata.CommandAnnotationPlugin: "true",
|
||||
metadata.CommandAnnotationPluginVendor: tc.vendor,
|
||||
metadata.CommandAnnotationPluginVersion: tc.version,
|
||||
pluginmanager.CommandAnnotationPlugin: "true",
|
||||
pluginmanager.CommandAnnotationPluginVendor: tc.vendor,
|
||||
pluginmanager.CommandAnnotationPluginVersion: tc.version,
|
||||
},
|
||||
}
|
||||
assert.Equal(t, vendorAndVersion(cmd), tc.expected)
|
||||
|
@ -69,8 +69,8 @@ func TestInvalidPlugin(t *testing.T) {
|
|||
assert.Assert(t, is.Len(invalidPlugins(root), 0))
|
||||
|
||||
sub1.Annotations = map[string]string{
|
||||
metadata.CommandAnnotationPlugin: "true",
|
||||
metadata.CommandAnnotationPluginInvalid: "foo",
|
||||
pluginmanager.CommandAnnotationPlugin: "true",
|
||||
pluginmanager.CommandAnnotationPluginInvalid: "foo",
|
||||
}
|
||||
root.AddCommand(sub1, sub2)
|
||||
sub1.AddCommand(sub1sub1, sub1sub2)
|
||||
|
@ -100,6 +100,6 @@ func TestDecoratedName(t *testing.T) {
|
|||
topLevelCommand := &cobra.Command{Use: "pluginTopLevelCommand"}
|
||||
root.AddCommand(topLevelCommand)
|
||||
assert.Equal(t, decoratedName(topLevelCommand), "pluginTopLevelCommand ")
|
||||
topLevelCommand.Annotations = map[string]string{metadata.CommandAnnotationPlugin: "true"}
|
||||
topLevelCommand.Annotations = map[string]string{pluginmanager.CommandAnnotationPlugin: "true"}
|
||||
assert.Equal(t, decoratedName(topLevelCommand), "pluginTopLevelCommand*")
|
||||
}
|
||||
|
|
|
@ -3,16 +3,16 @@ package builder
|
|||
import (
|
||||
"context"
|
||||
|
||||
"github.com/moby/moby/api/types/build"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/client"
|
||||
)
|
||||
|
||||
type fakeClient struct {
|
||||
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 {
|
||||
return c.builderPruneFunc(ctx, opts)
|
||||
}
|
||||
|
|
|
@ -23,22 +23,3 @@ func NewBuilderCommand(dockerCli command.Cli) *cobra.Command {
|
|||
)
|
||||
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/command"
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/docker/cli/internal/prompt"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/go-units"
|
||||
"github.com/moby/moby/api/types/build"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/errdefs"
|
||||
units "github.com/docker/go-units"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
@ -69,18 +69,18 @@ func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions)
|
|||
warning = allCacheWarning
|
||||
}
|
||||
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 {
|
||||
return 0, "", err
|
||||
}
|
||||
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,
|
||||
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,
|
||||
})
|
||||
if err != nil {
|
||||
|
@ -100,10 +100,6 @@ func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions)
|
|||
return report.SpaceReclaimed, output, nil
|
||||
}
|
||||
|
||||
type cancelledErr struct{ error }
|
||||
|
||||
func (cancelledErr) Cancelled() {}
|
||||
|
||||
// CachePrune executes a prune command for build cache
|
||||
func CachePrune(ctx context.Context, dockerCli command.Cli, all bool, filter opts.FilterOpt) (uint64, string, error) {
|
||||
return runPrune(ctx, dockerCli, pruneOptions{force: true, all: all, filter: filter})
|
||||
|
|
|
@ -7,7 +7,7 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/moby/moby/api/types/build"
|
||||
"github.com/docker/docker/api/types"
|
||||
)
|
||||
|
||||
func TestBuilderPromptTermination(t *testing.T) {
|
||||
|
@ -15,7 +15,7 @@ func TestBuilderPromptTermination(t *testing.T) {
|
|||
t.Cleanup(cancel)
|
||||
|
||||
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")
|
||||
},
|
||||
})
|
||||
|
|
|
@ -3,8 +3,8 @@ package checkpoint
|
|||
import (
|
||||
"context"
|
||||
|
||||
"github.com/moby/moby/api/types/checkpoint"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types/checkpoint"
|
||||
"github.com/docker/docker/client"
|
||||
)
|
||||
|
||||
type fakeClient struct {
|
||||
|
|
|
@ -7,7 +7,7 @@ import (
|
|||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"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"
|
||||
)
|
||||
|
||||
|
@ -40,8 +40,8 @@ func newCreateCommand(dockerCli command.Cli) *cobra.Command {
|
|||
return cmd
|
||||
}
|
||||
|
||||
func runCreate(ctx context.Context, dockerCLI command.Cli, opts createOptions) error {
|
||||
err := dockerCLI.Client().CheckpointCreate(ctx, opts.container, checkpoint.CreateOptions{
|
||||
func runCreate(ctx context.Context, dockerCli command.Cli, opts createOptions) error {
|
||||
err := dockerCli.Client().CheckpointCreate(ctx, opts.container, checkpoint.CreateOptions{
|
||||
CheckpointID: opts.checkpoint,
|
||||
CheckpointDir: opts.checkpointDir,
|
||||
Exit: !opts.leaveRunning,
|
||||
|
@ -50,6 +50,6 @@ func runCreate(ctx context.Context, dockerCLI command.Cli, opts createOptions) e
|
|||
return err
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintln(dockerCLI.Out(), opts.checkpoint)
|
||||
fmt.Fprintf(dockerCli.Out(), "%s\n", opts.checkpoint)
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
package checkpoint
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"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"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
|
@ -21,16 +20,16 @@ func TestCheckpointCreateErrors(t *testing.T) {
|
|||
}{
|
||||
{
|
||||
args: []string{"too-few-arguments"},
|
||||
expectedError: "requires 2 arguments",
|
||||
expectedError: "requires exactly 2 arguments",
|
||||
},
|
||||
{
|
||||
args: []string{"too", "many", "arguments"},
|
||||
expectedError: "requires 2 arguments",
|
||||
expectedError: "requires exactly 2 arguments",
|
||||
},
|
||||
{
|
||||
args: []string{"foo", "bar"},
|
||||
checkpointCreateFunc: func(container string, options checkpoint.CreateOptions) error {
|
||||
return errors.New("error creating checkpoint for container foo")
|
||||
return errors.Errorf("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) {
|
||||
const (
|
||||
containerName = "container-foo"
|
||||
checkpointName = "checkpoint-bar"
|
||||
checkpointDir = "/dir/foo"
|
||||
)
|
||||
|
||||
for _, tc := range []bool{true, false} {
|
||||
leaveRunning := strconv.FormatBool(tc)
|
||||
t.Run("leave-running="+leaveRunning, func(t *testing.T) {
|
||||
var actualContainerName string
|
||||
var actualOptions checkpoint.CreateOptions
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
checkpointCreateFunc: func(container string, options checkpoint.CreateOptions) error {
|
||||
actualContainerName = container
|
||||
actualOptions = options
|
||||
return nil
|
||||
},
|
||||
})
|
||||
cmd := newCreateCommand(cli)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
cmd.SetArgs([]string{containerName, checkpointName})
|
||||
assert.Check(t, cmd.Flags().Set("leave-running", leaveRunning))
|
||||
assert.Check(t, cmd.Flags().Set("checkpoint-dir", checkpointDir))
|
||||
assert.NilError(t, cmd.Execute())
|
||||
assert.Check(t, is.Equal(actualContainerName, containerName))
|
||||
expected := checkpoint.CreateOptions{
|
||||
CheckpointID: checkpointName,
|
||||
CheckpointDir: checkpointDir,
|
||||
Exit: !tc,
|
||||
}
|
||||
assert.Check(t, is.Equal(actualOptions, expected))
|
||||
assert.Check(t, is.Equal(strings.TrimSpace(cli.OutBuffer().String()), checkpointName))
|
||||
})
|
||||
}
|
||||
var containerID, checkpointID, checkpointDir string
|
||||
var exit bool
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
checkpointCreateFunc: func(container string, options checkpoint.CreateOptions) error {
|
||||
containerID = container
|
||||
checkpointID = options.CheckpointID
|
||||
checkpointDir = options.CheckpointDir
|
||||
exit = options.Exit
|
||||
return nil
|
||||
},
|
||||
})
|
||||
cmd := newCreateCommand(cli)
|
||||
cp := "checkpoint-bar"
|
||||
cmd.SetArgs([]string{"container-foo", cp})
|
||||
cmd.Flags().Set("leave-running", "true")
|
||||
cmd.Flags().Set("checkpoint-dir", "/dir/foo")
|
||||
assert.NilError(t, cmd.Execute())
|
||||
assert.Check(t, is.Equal("container-foo", containerID))
|
||||
assert.Check(t, is.Equal(cp, checkpointID))
|
||||
assert.Check(t, is.Equal("/dir/foo", checkpointDir))
|
||||
assert.Check(t, is.Equal(false, exit))
|
||||
assert.Check(t, is.Equal(cp, strings.TrimSpace(cli.OutBuffer().String())))
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ package checkpoint
|
|||
|
||||
import (
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/moby/moby/api/types/checkpoint"
|
||||
"github.com/docker/docker/api/types/checkpoint"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
|
@ -5,7 +5,7 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/moby/moby/api/types/checkpoint"
|
||||
"github.com/docker/docker/api/types/checkpoint"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ import (
|
|||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"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"
|
||||
)
|
||||
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
package checkpoint
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"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"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
"gotest.tools/v3/golden"
|
||||
|
@ -20,16 +20,16 @@ func TestCheckpointListErrors(t *testing.T) {
|
|||
}{
|
||||
{
|
||||
args: []string{},
|
||||
expectedError: "requires 1 argument",
|
||||
expectedError: "requires exactly 1 argument",
|
||||
},
|
||||
{
|
||||
args: []string{"too", "many", "arguments"},
|
||||
expectedError: "requires 1 argument",
|
||||
expectedError: "requires exactly 1 argument",
|
||||
},
|
||||
{
|
||||
args: []string{"foo"},
|
||||
checkpointListFunc: func(container string, options checkpoint.ListOptions) ([]checkpoint.Summary, error) {
|
||||
return []checkpoint.Summary{}, errors.New("error getting checkpoints for container foo")
|
||||
return []checkpoint.Summary{}, errors.Errorf("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/command"
|
||||
"github.com/moby/moby/api/types/checkpoint"
|
||||
"github.com/docker/docker/api/types/checkpoint"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
package checkpoint
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"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"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
|
@ -19,16 +19,16 @@ func TestCheckpointRemoveErrors(t *testing.T) {
|
|||
}{
|
||||
{
|
||||
args: []string{"too-few-arguments"},
|
||||
expectedError: "requires 2 arguments",
|
||||
expectedError: "requires exactly 2 arguments",
|
||||
},
|
||||
{
|
||||
args: []string{"too", "many", "arguments"},
|
||||
expectedError: "requires 2 arguments",
|
||||
expectedError: "requires exactly 2 arguments",
|
||||
},
|
||||
{
|
||||
args: []string{"foo", "bar"},
|
||||
checkpointDeleteFunc: func(container string, options checkpoint.DeleteOptions) error {
|
||||
return errors.New("error deleting checkpoint")
|
||||
return errors.Errorf("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:
|
||||
//go:build go1.23
|
||||
//go:build go1.21
|
||||
|
||||
package command
|
||||
|
||||
|
@ -8,6 +8,7 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
@ -20,14 +21,21 @@ import (
|
|||
"github.com/docker/cli/cli/context/store"
|
||||
"github.com/docker/cli/cli/debug"
|
||||
cliflags "github.com/docker/cli/cli/flags"
|
||||
manifeststore "github.com/docker/cli/cli/manifest/store"
|
||||
registryclient "github.com/docker/cli/cli/registry/client"
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/docker/cli/cli/trust"
|
||||
"github.com/docker/cli/cli/version"
|
||||
dopts "github.com/docker/cli/opts"
|
||||
"github.com/moby/moby/api/types/build"
|
||||
"github.com/moby/moby/api/types/swarm"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/registry"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/docker/go-connections/tlsconfig"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
notaryclient "github.com/theupdateframework/notary/client"
|
||||
)
|
||||
|
||||
const defaultInitTimeout = 2 * time.Second
|
||||
|
@ -45,10 +53,13 @@ type Cli interface {
|
|||
Streams
|
||||
SetIn(in *streams.In)
|
||||
Apply(ops ...CLIOption) error
|
||||
config.Provider
|
||||
ConfigFile() *configfile.ConfigFile
|
||||
ServerInfo() ServerInfo
|
||||
NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (notaryclient.Repository, error)
|
||||
DefaultVersion() string
|
||||
CurrentVersion() string
|
||||
ManifestStore() manifeststore.Store
|
||||
RegistryClient(bool) registryclient.RegistryClient
|
||||
ContentTrustEnabled() bool
|
||||
BuildKitEnabled() (bool, error)
|
||||
ContextStore() store.Store
|
||||
|
@ -58,9 +69,7 @@ type Cli interface {
|
|||
}
|
||||
|
||||
// DockerCli is an instance the docker command line client.
|
||||
// Instances of the client should be created using the [NewDockerCli]
|
||||
// constructor to make sure they are properly initialized with defaults
|
||||
// set.
|
||||
// Instances of the client can be returned from NewDockerCli.
|
||||
type DockerCli struct {
|
||||
configFile *configfile.ConfigFile
|
||||
options *cliflags.ClientOptions
|
||||
|
@ -75,7 +84,7 @@ type DockerCli struct {
|
|||
init sync.Once
|
||||
initErr error
|
||||
dockerEndpoint docker.Endpoint
|
||||
contextStoreConfig *store.Config
|
||||
contextStoreConfig store.Config
|
||||
initTimeout time.Duration
|
||||
res telemetryResource
|
||||
|
||||
|
@ -87,9 +96,9 @@ type DockerCli struct {
|
|||
enableGlobalMeter, enableGlobalTracer bool
|
||||
}
|
||||
|
||||
// DefaultVersion returns [client.DefaultAPIVersion].
|
||||
func (*DockerCli) DefaultVersion() string {
|
||||
return client.DefaultAPIVersion
|
||||
// DefaultVersion returns api.defaultVersion.
|
||||
func (cli *DockerCli) DefaultVersion() string {
|
||||
return api.DefaultVersion
|
||||
}
|
||||
|
||||
// CurrentVersion returns the API version currently negotiated, or the default
|
||||
|
@ -97,7 +106,7 @@ func (*DockerCli) DefaultVersion() string {
|
|||
func (cli *DockerCli) CurrentVersion() string {
|
||||
_ = cli.initialize()
|
||||
if cli.client == nil {
|
||||
return client.DefaultAPIVersion
|
||||
return api.DefaultVersion
|
||||
}
|
||||
return cli.client.ClientVersion()
|
||||
}
|
||||
|
@ -105,7 +114,7 @@ func (cli *DockerCli) CurrentVersion() string {
|
|||
// Client returns the APIClient
|
||||
func (cli *DockerCli) Client() client.APIClient {
|
||||
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)
|
||||
}
|
||||
return cli.client
|
||||
|
@ -179,7 +188,7 @@ func (cli *DockerCli) BuildKitEnabled() (bool, error) {
|
|||
}
|
||||
|
||||
si := cli.ServerInfo()
|
||||
if si.BuildkitVersion == build.BuilderBuildKit {
|
||||
if si.BuildkitVersion == types.BuilderBuildKit {
|
||||
// The daemon advertised BuildKit as the preferred builder; this may
|
||||
// be either a Linux daemon or a Windows daemon with experimental
|
||||
// BuildKit support enabled.
|
||||
|
@ -193,16 +202,16 @@ func (cli *DockerCli) BuildKitEnabled() (bool, error) {
|
|||
|
||||
// HooksEnabled returns whether plugin hooks are enabled.
|
||||
func (cli *DockerCli) HooksEnabled() bool {
|
||||
// use DOCKER_CLI_HOOKS env var value if set and not empty
|
||||
if v := os.Getenv("DOCKER_CLI_HOOKS"); v != "" {
|
||||
// legacy support DOCKER_CLI_HINTS env var
|
||||
if v := os.Getenv("DOCKER_CLI_HINTS"); v != "" {
|
||||
enabled, err := strconv.ParseBool(v)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return enabled
|
||||
}
|
||||
// legacy support DOCKER_CLI_HINTS env var
|
||||
if v := os.Getenv("DOCKER_CLI_HINTS"); v != "" {
|
||||
// use DOCKER_CLI_HOOKS env var value if set and not empty
|
||||
if v := os.Getenv("DOCKER_CLI_HOOKS"); v != "" {
|
||||
enabled, err := strconv.ParseBool(v)
|
||||
if err != nil {
|
||||
return false
|
||||
|
@ -221,6 +230,30 @@ func (cli *DockerCli) HooksEnabled() bool {
|
|||
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
|
||||
// line flags are parsed.
|
||||
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()
|
||||
}
|
||||
if opts.Context != "" && len(opts.Hosts) > 0 {
|
||||
return errors.New("conflicting options: cannot specify both --host and --context")
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
return errors.New("conflicting options: either specify --host or --context, not both")
|
||||
}
|
||||
|
||||
cli.options = opts
|
||||
cli.configFile = config.LoadDefaultConfigFile(cli.err)
|
||||
cli.currentContext = resolveContextName(cli.options, cli.configFile)
|
||||
cli.contextStore = &ContextStoreWithDefault{
|
||||
Store: store.New(config.ContextStoreDir(), *cli.contextStoreConfig),
|
||||
Store: store.New(config.ContextStoreDir(), cli.contextStoreConfig),
|
||||
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 {
|
||||
cli.createGlobalTracerProvider(cli.baseCtx)
|
||||
}
|
||||
filterResourceAttributesEnvvar()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -287,7 +299,7 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...CLIOption)
|
|||
// NewAPIClientFromFlags creates a new APIClient from command line flags
|
||||
func NewAPIClientFromFlags(opts *cliflags.ClientOptions, configFile *configfile.ConfigFile) (client.APIClient, error) {
|
||||
if opts.Context != "" && len(opts.Hosts) > 0 {
|
||||
return nil, errors.New("conflicting options: cannot specify both --host and --context")
|
||||
return nil, errors.New("conflicting options: either specify --host or --context, not both")
|
||||
}
|
||||
|
||||
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)
|
||||
func resolveDefaultDockerEndpoint(opts *cliflags.ClientOptions) (docker.Endpoint, error) {
|
||||
// defaultToTLS determines whether we should use a TLS host as default
|
||||
// if nothing was configured by the user.
|
||||
defaultToTLS := opts.TLSOptions != nil
|
||||
host, err := getServerHost(opts.Hosts, defaultToTLS)
|
||||
host, err := getServerHost(opts.Hosts, opts.TLSOptions)
|
||||
if err != nil {
|
||||
return docker.Endpoint{}, err
|
||||
}
|
||||
|
@ -394,6 +403,11 @@ func (cli *DockerCli) initializeFromClient() {
|
|||
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
|
||||
func (cli *DockerCli) ContextStore() store.Store {
|
||||
return cli.contextStore
|
||||
|
@ -461,7 +475,7 @@ func (cli *DockerCli) DockerEndpoint() docker.Endpoint {
|
|||
if err := cli.initialize(); err != nil {
|
||||
// Note that we're not terminating here, as this function may be used
|
||||
// in cases where we're able to continue.
|
||||
_, _ = fmt.Fprintln(cli.Err(), cli.initErr)
|
||||
_, _ = fmt.Fprintf(cli.Err(), "%v\n", cli.initErr)
|
||||
}
|
||||
return cli.dockerEndpoint
|
||||
}
|
||||
|
@ -509,7 +523,7 @@ func (cli *DockerCli) Apply(ops ...CLIOption) error {
|
|||
type ServerInfo struct {
|
||||
HasExperimental bool
|
||||
OSType string
|
||||
BuildkitVersion build.BuilderVersion
|
||||
BuildkitVersion types.BuilderVersion
|
||||
|
||||
// SwarmStatus provides information about the current swarm status of the
|
||||
// engine, obtained from the "Swarm" header in the API response.
|
||||
|
@ -539,15 +553,18 @@ func NewDockerCli(ops ...CLIOption) (*DockerCli, error) {
|
|||
return cli, nil
|
||||
}
|
||||
|
||||
func getServerHost(hosts []string, defaultToTLS bool) (string, error) {
|
||||
func getServerHost(hosts []string, tlsOptions *tlsconfig.Options) (string, error) {
|
||||
var host string
|
||||
switch len(hosts) {
|
||||
case 0:
|
||||
return dopts.ParseHost(defaultToTLS, os.Getenv(client.EnvOverrideHost))
|
||||
host = os.Getenv(client.EnvOverrideHost)
|
||||
case 1:
|
||||
return dopts.ParseHost(defaultToTLS, hosts[0])
|
||||
host = hosts[0]
|
||||
default:
|
||||
return "", errors.New("Specify only one -H")
|
||||
}
|
||||
|
||||
return dopts.ParseHost(tlsOptions != nil, host)
|
||||
}
|
||||
|
||||
// UserAgent returns the user agent string used for making API requests
|
||||
|
|
|
@ -10,7 +10,8 @@ import (
|
|||
"strings"
|
||||
|
||||
"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/pkg/errors"
|
||||
)
|
||||
|
@ -100,8 +101,7 @@ func WithContentTrust(enabled bool) CLIOption {
|
|||
// WithDefaultContextStoreConfig configures the cli to use the default context store configuration.
|
||||
func WithDefaultContextStoreConfig() CLIOption {
|
||||
return func(cli *DockerCli) error {
|
||||
cfg := DefaultContextStoreConfig()
|
||||
cli.contextStoreConfig = &cfg
|
||||
cli.contextStoreConfig = DefaultContextStoreConfig()
|
||||
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
|
||||
// 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
|
||||
|
@ -189,10 +177,7 @@ func withCustomHeadersFromEnv() client.Opt {
|
|||
csvReader := csv.NewReader(strings.NewReader(value))
|
||||
fields, err := csvReader.Read()
|
||||
if err != nil {
|
||||
return invalidParameter(errors.Errorf(
|
||||
"failed to parse custom headers from %s environment variable: value must be formatted as comma-separated key=value pairs",
|
||||
envOverrideHTTPHeaders,
|
||||
))
|
||||
return errdefs.InvalidParameter(errors.Errorf("failed to parse custom headers from %s environment variable: value must be formatted as comma-separated key=value pairs", envOverrideHTTPHeaders))
|
||||
}
|
||||
if len(fields) == 0 {
|
||||
return nil
|
||||
|
@ -206,10 +191,7 @@ func withCustomHeadersFromEnv() client.Opt {
|
|||
k = strings.TrimSpace(k)
|
||||
|
||||
if k == "" {
|
||||
return 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,
|
||||
))
|
||||
return errdefs.InvalidParameter(errors.Errorf(`failed to set custom headers from %s environment variable: value contains a key=value pair with an empty key: '%s'`, envOverrideHTTPHeaders, kv))
|
||||
}
|
||||
|
||||
// We don't currently allow empty key=value pairs, and produce an error.
|
||||
|
@ -217,10 +199,7 @@ func withCustomHeadersFromEnv() client.Opt {
|
|||
// from an environment variable with the same name). In the meantime,
|
||||
// produce an error to prevent users from depending on this.
|
||||
if !hasValue {
|
||||
return invalidParameter(errors.Errorf(
|
||||
`failed to set custom headers from %s environment variable: missing "=" in key=value pair: '%s'`,
|
||||
envOverrideHTTPHeaders, kv,
|
||||
))
|
||||
return errdefs.InvalidParameter(errors.Errorf(`failed to set custom headers from %s environment variable: missing "=" in key=value pair: '%s'`, envOverrideHTTPHeaders, kv))
|
||||
}
|
||||
|
||||
env[http.CanonicalHeaderKey(k)] = v
|
||||
|
|
|
@ -3,7 +3,6 @@ package command
|
|||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
|
@ -19,9 +18,13 @@ import (
|
|||
"github.com/docker/cli/cli/config"
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
"github.com/docker/cli/cli/flags"
|
||||
"github.com/moby/moby/api/types"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/docker/docker/api"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/pkg/errors"
|
||||
"gotest.tools/v3/assert"
|
||||
"gotest.tools/v3/fs"
|
||||
)
|
||||
|
||||
func TestNewAPIClientFromFlags(t *testing.T) {
|
||||
|
@ -33,7 +36,7 @@ func TestNewAPIClientFromFlags(t *testing.T) {
|
|||
apiClient, err := NewAPIClientFromFlags(opts, &configfile.ConfigFile{})
|
||||
assert.NilError(t, err)
|
||||
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) {
|
||||
|
@ -46,7 +49,7 @@ func TestNewAPIClientFromFlagsForDefaultSchema(t *testing.T) {
|
|||
apiClient, err := NewAPIClientFromFlags(opts, &configfile.ConfigFile{})
|
||||
assert.NilError(t, err)
|
||||
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) {
|
||||
|
@ -70,7 +73,7 @@ func TestNewAPIClientFromFlagsWithCustomHeaders(t *testing.T) {
|
|||
apiClient, err := NewAPIClientFromFlags(opts, configFile)
|
||||
assert.NilError(t, err)
|
||||
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
|
||||
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)
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, apiClient.DaemonHost(), host)
|
||||
assert.Equal(t, apiClient.ClientVersion(), client.DefaultAPIVersion)
|
||||
assert.Equal(t, apiClient.ClientVersion(), api.DefaultVersion)
|
||||
|
||||
expectedHeaders := http.Header{
|
||||
"One": []string{"one-value"},
|
||||
|
@ -120,8 +123,7 @@ func TestNewAPIClientFromFlagsWithCustomHeadersFromEnv(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestNewAPIClientFromFlagsWithAPIVersionFromEnv(t *testing.T) {
|
||||
const customVersion = "v3.3.3"
|
||||
const expectedVersion = "3.3.3"
|
||||
customVersion := "v3.3.3"
|
||||
t.Setenv("DOCKER_API_VERSION", customVersion)
|
||||
t.Setenv("DOCKER_HOST", ":2375")
|
||||
|
||||
|
@ -129,7 +131,7 @@ func TestNewAPIClientFromFlagsWithAPIVersionFromEnv(t *testing.T) {
|
|||
configFile := &configfile.ConfigFile{}
|
||||
apiclient, err := NewAPIClientFromFlags(opts, configFile)
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, apiclient.ClientVersion(), expectedVersion)
|
||||
assert.Equal(t, apiclient.ClientVersion(), customVersion)
|
||||
}
|
||||
|
||||
type fakeClient struct {
|
||||
|
@ -185,18 +187,19 @@ func TestInitializeFromClient(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
for _, tc := range testcases {
|
||||
t.Run(tc.doc, func(t *testing.T) {
|
||||
for _, testcase := range testcases {
|
||||
testcase := testcase
|
||||
t.Run(testcase.doc, func(t *testing.T) {
|
||||
apiclient := &fakeClient{
|
||||
pingFunc: tc.pingFunc,
|
||||
pingFunc: testcase.pingFunc,
|
||||
version: defaultVersion,
|
||||
}
|
||||
|
||||
cli := &DockerCli{client: apiclient}
|
||||
err := cli.Initialize(flags.NewClientOptions())
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, cli.ServerInfo(), tc.expectedServer)
|
||||
assert.Equal(t, apiclient.negotiated, tc.negotiated)
|
||||
assert.DeepEqual(t, cli.ServerInfo(), testcase.expectedServer)
|
||||
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.
|
||||
// https://github.com/docker/cli/issues/3652
|
||||
func TestInitializeFromClientHangs(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
socket := filepath.Join(tmpDir, "my.sock")
|
||||
dir := t.TempDir()
|
||||
socket := filepath.Join(dir, "my.sock")
|
||||
l, err := net.Listen("unix", socket)
|
||||
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) {
|
||||
// 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)
|
||||
// Check streams are initialized
|
||||
assert.Check(t, cli.In() != nil)
|
||||
assert.Check(t, cli.Out() != nil)
|
||||
assert.Check(t, cli.Err() != nil)
|
||||
inputStream, err := io.ReadAll(cli.In())
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, string(inputStream), "some input")
|
||||
assert.Equal(t, cli.ContentTrustEnabled(), true)
|
||||
|
||||
// Apply can modify a dockerCli after construction
|
||||
inbuf := bytes.NewBuffer([]byte("input"))
|
||||
outbuf := bytes.NewBuffer(nil)
|
||||
errbuf := bytes.NewBuffer(nil)
|
||||
err = cli.Apply(
|
||||
WithInputStream(io.NopCloser(strings.NewReader("input"))),
|
||||
WithInputStream(io.NopCloser(inbuf)),
|
||||
WithOutputStream(outbuf),
|
||||
WithErrorStream(errbuf),
|
||||
)
|
||||
assert.NilError(t, err)
|
||||
// Check input stream
|
||||
inputStream, err = io.ReadAll(cli.In())
|
||||
inputStream, err := io.ReadAll(cli.In())
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, string(inputStream), "input")
|
||||
// Check output stream
|
||||
_, err = fmt.Fprint(cli.Out(), "output")
|
||||
assert.NilError(t, err)
|
||||
fmt.Fprintf(cli.Out(), "output")
|
||||
outputStream, err := io.ReadAll(outbuf)
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, string(outputStream), "output")
|
||||
// Check error stream
|
||||
_, err = fmt.Fprint(cli.Err(), "error")
|
||||
assert.NilError(t, err)
|
||||
fmt.Fprintf(cli.Err(), "error")
|
||||
errStream, err := io.ReadAll(errbuf)
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, string(errStream), "error")
|
||||
|
@ -303,8 +346,6 @@ func TestInitializeShouldAlwaysCreateTheContextStore(t *testing.T) {
|
|||
|
||||
func TestHooksEnabled(t *testing.T) {
|
||||
t.Run("disabled by default", func(t *testing.T) {
|
||||
// Make sure we don't depend on any existing ~/.docker/config.json
|
||||
config.SetDir(t.TempDir())
|
||||
cli, err := NewDockerCli()
|
||||
assert.NilError(t, err)
|
||||
|
||||
|
@ -316,11 +357,12 @@ func TestHooksEnabled(t *testing.T) {
|
|||
"features": {
|
||||
"hooks": "true"
|
||||
}}`
|
||||
config.SetDir(t.TempDir())
|
||||
err := os.WriteFile(filepath.Join(config.Dir(), "config.json"), []byte(configFile), 0o600)
|
||||
assert.NilError(t, err)
|
||||
dir := fs.NewDir(t, "", fs.WithFile("config.json", configFile))
|
||||
defer dir.Remove()
|
||||
cli, err := NewDockerCli()
|
||||
assert.NilError(t, err)
|
||||
config.SetDir(dir.Path())
|
||||
|
||||
assert.Check(t, cli.HooksEnabled())
|
||||
})
|
||||
|
||||
|
@ -330,11 +372,12 @@ func TestHooksEnabled(t *testing.T) {
|
|||
"hooks": "true"
|
||||
}}`
|
||||
t.Setenv("DOCKER_CLI_HOOKS", "false")
|
||||
config.SetDir(t.TempDir())
|
||||
err := os.WriteFile(filepath.Join(config.Dir(), "config.json"), []byte(configFile), 0o600)
|
||||
assert.NilError(t, err)
|
||||
dir := fs.NewDir(t, "", fs.WithFile("config.json", configFile))
|
||||
defer dir.Remove()
|
||||
cli, err := NewDockerCli()
|
||||
assert.NilError(t, err)
|
||||
config.SetDir(dir.Path())
|
||||
|
||||
assert.Check(t, !cli.HooksEnabled())
|
||||
})
|
||||
|
||||
|
@ -344,11 +387,12 @@ func TestHooksEnabled(t *testing.T) {
|
|||
"hooks": "true"
|
||||
}}`
|
||||
t.Setenv("DOCKER_CLI_HINTS", "false")
|
||||
config.SetDir(t.TempDir())
|
||||
err := os.WriteFile(filepath.Join(config.Dir(), "config.json"), []byte(configFile), 0o600)
|
||||
assert.NilError(t, err)
|
||||
dir := fs.NewDir(t, "", fs.WithFile("config.json", configFile))
|
||||
defer dir.Remove()
|
||||
cli, err := NewDockerCli()
|
||||
assert.NilError(t, err)
|
||||
config.SetDir(dir.Path())
|
||||
|
||||
assert.Check(t, !cli.HooksEnabled())
|
||||
})
|
||||
}
|
||||
|
|
|
@ -43,7 +43,6 @@ func AddCommands(cmd *cobra.Command, dockerCli command.Cli) {
|
|||
system.NewInfoCommand(dockerCli),
|
||||
|
||||
// management commands
|
||||
builder.NewBakeStubCommand(dockerCli),
|
||||
builder.NewBuilderCommand(dockerCli),
|
||||
checkpoint.NewCheckpointCommand(dockerCli),
|
||||
container.NewContainerCommand(dockerCli),
|
||||
|
|
|
@ -5,18 +5,17 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/api/types/image"
|
||||
"github.com/moby/moby/api/types/network"
|
||||
"github.com/moby/moby/api/types/volume"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/docker/api/types/volume"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// ValidArgsFn a function to be used by cobra command as `ValidArgsFunction` to offer command line completion.
|
||||
//
|
||||
// Deprecated: use [cobra.CompletionFunc].
|
||||
type ValidArgsFn = cobra.CompletionFunc
|
||||
// ValidArgsFn a function to be used by cobra command as `ValidArgsFunction` to offer command line completion
|
||||
type ValidArgsFn func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective)
|
||||
|
||||
// APIClientProvider provides a method to get an [client.APIClient], initializing
|
||||
// it if needed.
|
||||
|
@ -29,11 +28,8 @@ type APIClientProvider interface {
|
|||
}
|
||||
|
||||
// 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) {
|
||||
if limit > 0 && len(args) >= limit {
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
list, err := dockerCLI.Client().ImageList(cmd.Context(), image.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveError
|
||||
|
@ -49,7 +45,7 @@ func ImageNames(dockerCLI APIClientProvider, limit int) cobra.CompletionFunc {
|
|||
// ContainerNames offers completion for container names and IDs
|
||||
// By default, only names are returned.
|
||||
// Set DOCKER_COMPLETION_SHOW_CONTAINER_IDS=yes to also complete IDs.
|
||||
func ContainerNames(dockerCLI APIClientProvider, all bool, filters ...func(container.Summary) bool) cobra.CompletionFunc {
|
||||
func ContainerNames(dockerCLI APIClientProvider, all bool, filters ...func(types.Container) bool) ValidArgsFn {
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
list, err := dockerCLI.Client().ContainerList(cmd.Context(), container.ListOptions{
|
||||
All: all,
|
||||
|
@ -64,7 +60,7 @@ func ContainerNames(dockerCLI APIClientProvider, all bool, filters ...func(conta
|
|||
for _, ctr := range list {
|
||||
skip := false
|
||||
for _, fn := range filters {
|
||||
if fn != nil && !fn(ctr) {
|
||||
if !fn(ctr) {
|
||||
skip = true
|
||||
break
|
||||
}
|
||||
|
@ -82,7 +78,7 @@ func ContainerNames(dockerCLI APIClientProvider, all bool, filters ...func(conta
|
|||
}
|
||||
|
||||
// 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) {
|
||||
list, err := dockerCLI.Client().VolumeList(cmd.Context(), volume.ListOptions{})
|
||||
if err != nil {
|
||||
|
@ -97,7 +93,7 @@ func VolumeNames(dockerCLI APIClientProvider) cobra.CompletionFunc {
|
|||
}
|
||||
|
||||
// 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) {
|
||||
list, err := dockerCLI.Client().NetworkList(cmd.Context(), network.ListOptions{})
|
||||
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.
|
||||
func FromList(options ...string) cobra.CompletionFunc {
|
||||
func FromList(options ...string) ValidArgsFn {
|
||||
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) {
|
||||
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 (
|
||||
"context"
|
||||
|
||||
"github.com/moby/moby/api/types/swarm"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/client"
|
||||
)
|
||||
|
||||
type fakeClient struct {
|
||||
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)
|
||||
configListFunc func(context.Context, swarm.ConfigListOptions) ([]swarm.Config, error)
|
||||
configListFunc func(context.Context, types.ConfigListOptions) ([]swarm.Config, 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 {
|
||||
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) {
|
||||
|
@ -29,7 +30,7 @@ func (c *fakeClient) ConfigInspectWithRaw(ctx context.Context, id string) (swarm
|
|||
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 {
|
||||
return c.configListFunc(ctx, options)
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import (
|
|||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/moby/moby/api/types/swarm"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
@ -30,9 +30,9 @@ func NewConfigCommand(dockerCli command.Cli) *cobra.Command {
|
|||
}
|
||||
|
||||
// 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) {
|
||||
list, err := dockerCLI.Client().ConfigList(cmd.Context(), swarm.ConfigListOptions{})
|
||||
list, err := dockerCLI.Client().ConfigList(cmd.Context(), types.ConfigListOptions{})
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveError
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import (
|
|||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"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/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
|
@ -48,10 +48,20 @@ func newConfigCreateCommand(dockerCli command.Cli) *cobra.Command {
|
|||
}
|
||||
|
||||
// RunConfigCreate creates a config with the given options.
|
||||
func RunConfigCreate(ctx context.Context, dockerCLI command.Cli, options CreateOptions) error {
|
||||
apiClient := dockerCLI.Client()
|
||||
func RunConfigCreate(ctx context.Context, dockerCli command.Cli, options CreateOptions) error {
|
||||
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 {
|
||||
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{
|
||||
Annotations: swarm.Annotations{
|
||||
Name: options.Name,
|
||||
Labels: opts.ConvertKVStringsToMap(options.Labels.GetSlice()),
|
||||
Labels: opts.ConvertKVStringsToMap(options.Labels.GetAll()),
|
||||
},
|
||||
Data: configData,
|
||||
}
|
||||
|
@ -68,59 +78,11 @@ func RunConfigCreate(ctx context.Context, dockerCLI command.Cli, options CreateO
|
|||
Name: options.TemplateDriver,
|
||||
}
|
||||
}
|
||||
r, err := apiClient.ConfigCreate(ctx, spec)
|
||||
r, err := client.ConfigCreate(ctx, spec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintln(dockerCLI.Out(), r.ID)
|
||||
fmt.Fprintln(dockerCli.Out(), r.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// maxConfigSize is the maximum byte length of the [swarm.ConfigSpec.Data] field,
|
||||
// as defined by [MaxConfigSize] in SwarmKit.
|
||||
//
|
||||
// [MaxConfigSize]: https://pkg.go.dev/github.com/moby/swarmkit/v2@v2.0.0-20250103191802-8c1959736554/manager/controlapi#MaxConfigSize
|
||||
const maxConfigSize = 1000 * 1024 // 1000KB
|
||||
|
||||
// readConfigData reads the config from either stdin or the given fileName.
|
||||
//
|
||||
// It reads up to twice the maximum size of the config ([maxConfigSize]),
|
||||
// just in case swarm's limit changes; this is only a safeguard to prevent
|
||||
// reading arbitrary files into memory.
|
||||
func readConfigData(in io.Reader, fileName string) ([]byte, error) {
|
||||
switch fileName {
|
||||
case "-":
|
||||
data, err := io.ReadAll(io.LimitReader(in, 2*maxConfigSize))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading from STDIN: %w", err)
|
||||
}
|
||||
if len(data) == 0 {
|
||||
return nil, errors.New("error reading from STDIN: data is empty")
|
||||
}
|
||||
return data, nil
|
||||
case "":
|
||||
return nil, errors.New("config file is required")
|
||||
default:
|
||||
// Open file with [FILE_FLAG_SEQUENTIAL_SCAN] on Windows, which
|
||||
// prevents Windows from aggressively caching it. We expect this
|
||||
// file to be only read once. Given that this is expected to be
|
||||
// a small file, this may not be a significant optimization, so
|
||||
// we could choose to omit this, and use a regular [os.Open].
|
||||
//
|
||||
// [FILE_FLAG_SEQUENTIAL_SCAN]: https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea#FILE_FLAG_SEQUENTIAL_SCAN
|
||||
f, err := sequential.Open(fileName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading from %s: %w", fileName, err)
|
||||
}
|
||||
defer f.Close()
|
||||
data, err := io.ReadAll(io.LimitReader(f, 2*maxConfigSize))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading from %s: %w", fileName, err)
|
||||
}
|
||||
if len(data) == 0 {
|
||||
return nil, fmt.Errorf("error reading from %s: data is empty", fileName)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,8 +2,6 @@ package config
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
@ -12,7 +10,9 @@ import (
|
|||
"testing"
|
||||
|
||||
"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"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
"gotest.tools/v3/golden"
|
||||
|
@ -23,26 +23,27 @@ const configDataFile = "config-create-with-name.golden"
|
|||
func TestConfigCreateErrors(t *testing.T) {
|
||||
testCases := []struct {
|
||||
args []string
|
||||
configCreateFunc func(context.Context, swarm.ConfigSpec) (swarm.ConfigCreateResponse, error)
|
||||
configCreateFunc func(context.Context, swarm.ConfigSpec) (types.ConfigCreateResponse, error)
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
args: []string{"too_few"},
|
||||
expectedError: "requires 2 arguments",
|
||||
expectedError: "requires exactly 2 arguments",
|
||||
},
|
||||
{
|
||||
args: []string{"too", "many", "arguments"},
|
||||
expectedError: "requires 2 arguments",
|
||||
expectedError: "requires exactly 2 arguments",
|
||||
},
|
||||
{
|
||||
args: []string{"name", filepath.Join("testdata", configDataFile)},
|
||||
configCreateFunc: func(_ context.Context, configSpec swarm.ConfigSpec) (swarm.ConfigCreateResponse, error) {
|
||||
return swarm.ConfigCreateResponse{}, errors.New("error creating config")
|
||||
configCreateFunc: func(_ context.Context, configSpec swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
|
||||
return types.ConfigCreateResponse{}, errors.Errorf("error creating config")
|
||||
},
|
||||
expectedError: "error creating config",
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
t.Run(tc.expectedError, func(t *testing.T) {
|
||||
cmd := newConfigCreateCommand(
|
||||
test.NewFakeCli(&fakeClient{
|
||||
|
@ -58,17 +59,17 @@ func TestConfigCreateErrors(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestConfigCreateWithName(t *testing.T) {
|
||||
const name = "config-with-name"
|
||||
name := "foo"
|
||||
var actual []byte
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
configCreateFunc: func(_ context.Context, spec swarm.ConfigSpec) (swarm.ConfigCreateResponse, error) {
|
||||
configCreateFunc: func(_ context.Context, spec swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
|
||||
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
|
||||
|
||||
return swarm.ConfigCreateResponse{
|
||||
return types.ConfigCreateResponse{
|
||||
ID: "ID-" + spec.Name,
|
||||
}, nil
|
||||
},
|
||||
|
@ -86,7 +87,7 @@ func TestConfigCreateWithLabels(t *testing.T) {
|
|||
"lbl1": "Label-foo",
|
||||
"lbl2": "Label-bar",
|
||||
}
|
||||
const name = "config-with-labels"
|
||||
name := "foo"
|
||||
|
||||
data, err := os.ReadFile(filepath.Join("testdata", configDataFile))
|
||||
assert.NilError(t, err)
|
||||
|
@ -100,12 +101,12 @@ func TestConfigCreateWithLabels(t *testing.T) {
|
|||
}
|
||||
|
||||
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) {
|
||||
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,
|
||||
}, nil
|
||||
},
|
||||
|
@ -123,19 +124,19 @@ func TestConfigCreateWithTemplatingDriver(t *testing.T) {
|
|||
expectedDriver := &swarm.Driver{
|
||||
Name: "template-driver",
|
||||
}
|
||||
const name = "config-with-template-driver"
|
||||
name := "foo"
|
||||
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
configCreateFunc: func(_ context.Context, spec swarm.ConfigSpec) (swarm.ConfigCreateResponse, error) {
|
||||
configCreateFunc: func(_ context.Context, spec swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
|
||||
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 {
|
||||
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,
|
||||
}, nil
|
||||
},
|
||||
|
|
|
@ -5,10 +5,11 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/docker/cli/cli/command/inspect"
|
||||
"github.com/docker/go-units"
|
||||
"github.com/moby/moby/api/types/swarm"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
units "github.com/docker/go-units"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -156,11 +157,11 @@ func (ctx *configInspectContext) Labels() map[string]string {
|
|||
}
|
||||
|
||||
func (ctx *configInspectContext) CreatedAt() string {
|
||||
return formatter.PrettyPrint(ctx.Config.CreatedAt)
|
||||
return command.PrettyPrint(ctx.Config.CreatedAt)
|
||||
}
|
||||
|
||||
func (ctx *configInspectContext) UpdatedAt() string {
|
||||
return formatter.PrettyPrint(ctx.Config.UpdatedAt)
|
||||
return command.PrettyPrint(ctx.Config.UpdatedAt)
|
||||
}
|
||||
|
||||
func (ctx *configInspectContext) Data() string {
|
||||
|
|
|
@ -6,7 +6,7 @@ import (
|
|||
"time"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
|
@ -61,6 +61,7 @@ id_rsa
|
|||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(string(tc.context.Format), func(t *testing.T) {
|
||||
var out bytes.Buffer
|
||||
tc.context.Output = &out
|
||||
|
|
|
@ -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:
|
||||
//go:build go1.23
|
||||
//go:build go1.21
|
||||
|
||||
package config
|
||||
|
||||
|
@ -43,15 +43,15 @@ func newConfigInspectCommand(dockerCli command.Cli) *cobra.Command {
|
|||
}
|
||||
|
||||
// RunConfigInspect inspects the given Swarm config.
|
||||
func RunConfigInspect(ctx context.Context, dockerCLI command.Cli, opts InspectOptions) error {
|
||||
apiClient := dockerCLI.Client()
|
||||
func RunConfigInspect(ctx context.Context, dockerCli command.Cli, opts InspectOptions) error {
|
||||
client := dockerCli.Client()
|
||||
|
||||
if opts.Pretty {
|
||||
opts.Format = "pretty"
|
||||
}
|
||||
|
||||
getRef := func(id string) (any, []byte, error) {
|
||||
return apiClient.ConfigInspectWithRaw(ctx, id)
|
||||
return client.ConfigInspectWithRaw(ctx, id)
|
||||
}
|
||||
f := opts.Format
|
||||
|
||||
|
@ -62,7 +62,7 @@ func RunConfigInspect(ctx context.Context, dockerCLI command.Cli, opts InspectOp
|
|||
}
|
||||
|
||||
configCtx := formatter.Context{
|
||||
Output: dockerCLI.Out(),
|
||||
Output: dockerCli.Out(),
|
||||
Format: NewFormat(f, false),
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,6 @@ package config
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"testing"
|
||||
|
@ -10,7 +9,8 @@ import (
|
|||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"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/golden"
|
||||
)
|
||||
|
@ -28,7 +28,7 @@ func TestConfigInspectErrors(t *testing.T) {
|
|||
{
|
||||
args: []string{"foo"},
|
||||
configInspectFunc: func(_ context.Context, configID string) (swarm.Config, []byte, error) {
|
||||
return swarm.Config{}, nil, errors.New("error while inspecting the config")
|
||||
return swarm.Config{}, nil, errors.Errorf("error while inspecting the config")
|
||||
},
|
||||
expectedError: "error while inspecting the config",
|
||||
},
|
||||
|
@ -45,7 +45,7 @@ func TestConfigInspectErrors(t *testing.T) {
|
|||
if configID == "foo" {
|
||||
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",
|
||||
},
|
||||
|
@ -77,7 +77,7 @@ func TestConfigInspectWithoutFormat(t *testing.T) {
|
|||
args: []string{"foo"},
|
||||
configInspectFunc: func(_ context.Context, name string) (swarm.Config, []byte, error) {
|
||||
if name != "foo" {
|
||||
return swarm.Config{}, nil, fmt.Errorf("invalid name, expected %s, got %s", "foo", name)
|
||||
return swarm.Config{}, nil, errors.Errorf("Invalid name, expected %s, got %s", "foo", name)
|
||||
}
|
||||
return *builders.Config(builders.ConfigID("ID-foo"), builders.ConfigName("foo")), nil, nil
|
||||
},
|
||||
|
|
|
@ -10,8 +10,8 @@ import (
|
|||
"github.com/docker/cli/cli/command/formatter"
|
||||
flagsHelper "github.com/docker/cli/cli/flags"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/fvbommel/sortorder"
|
||||
"github.com/moby/moby/api/types/swarm"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
@ -45,18 +45,18 @@ func newConfigListCommand(dockerCli command.Cli) *cobra.Command {
|
|||
}
|
||||
|
||||
// RunConfigList lists Swarm configs.
|
||||
func RunConfigList(ctx context.Context, dockerCLI command.Cli, options ListOptions) error {
|
||||
apiClient := dockerCLI.Client()
|
||||
func RunConfigList(ctx context.Context, dockerCli command.Cli, options ListOptions) error {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
|
||||
format := options.Format
|
||||
if len(format) == 0 {
|
||||
if len(dockerCLI.ConfigFile().ConfigFormat) > 0 && !options.Quiet {
|
||||
format = dockerCLI.ConfigFile().ConfigFormat
|
||||
if len(dockerCli.ConfigFile().ConfigFormat) > 0 && !options.Quiet {
|
||||
format = dockerCli.ConfigFile().ConfigFormat
|
||||
} else {
|
||||
format = formatter.TableFormatKey
|
||||
}
|
||||
|
@ -67,7 +67,7 @@ func RunConfigList(ctx context.Context, dockerCLI command.Cli, options ListOptio
|
|||
})
|
||||
|
||||
configCtx := formatter.Context{
|
||||
Output: dockerCLI.Out(),
|
||||
Output: dockerCli.Out(),
|
||||
Format: NewFormat(format, options.Quiet),
|
||||
}
|
||||
return FormatWrite(configCtx, configs)
|
||||
|
|
|
@ -2,7 +2,6 @@ package config
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"testing"
|
||||
"time"
|
||||
|
@ -10,7 +9,9 @@ import (
|
|||
"github.com/docker/cli/cli/config/configfile"
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/cli/internal/test/builders"
|
||||
"github.com/moby/moby/api/types/swarm"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/pkg/errors"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
"gotest.tools/v3/golden"
|
||||
|
@ -19,7 +20,7 @@ import (
|
|||
func TestConfigListErrors(t *testing.T) {
|
||||
testCases := []struct {
|
||||
args []string
|
||||
configListFunc func(context.Context, swarm.ConfigListOptions) ([]swarm.Config, error)
|
||||
configListFunc func(context.Context, types.ConfigListOptions) ([]swarm.Config, error)
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
|
@ -27,8 +28,8 @@ func TestConfigListErrors(t *testing.T) {
|
|||
expectedError: "accepts no argument",
|
||||
},
|
||||
{
|
||||
configListFunc: func(_ context.Context, options swarm.ConfigListOptions) ([]swarm.Config, error) {
|
||||
return []swarm.Config{}, errors.New("error listing configs")
|
||||
configListFunc: func(_ context.Context, options types.ConfigListOptions) ([]swarm.Config, error) {
|
||||
return []swarm.Config{}, errors.Errorf("error listing configs")
|
||||
},
|
||||
expectedError: "error listing configs",
|
||||
},
|
||||
|
@ -48,7 +49,7 @@ func TestConfigListErrors(t *testing.T) {
|
|||
|
||||
func TestConfigList(t *testing.T) {
|
||||
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{
|
||||
*builders.Config(builders.ConfigID("ID-1-foo"),
|
||||
builders.ConfigName("1-foo"),
|
||||
|
@ -78,7 +79,7 @@ func TestConfigList(t *testing.T) {
|
|||
|
||||
func TestConfigListWithQuietOption(t *testing.T) {
|
||||
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{
|
||||
*builders.Config(builders.ConfigID("ID-foo"), builders.ConfigName("foo")),
|
||||
*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) {
|
||||
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{
|
||||
*builders.Config(builders.ConfigID("ID-foo"), builders.ConfigName("foo")),
|
||||
*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) {
|
||||
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{
|
||||
*builders.Config(builders.ConfigID("ID-foo"), builders.ConfigName("foo")),
|
||||
*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) {
|
||||
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("lbl1=Label-bar", options.Filters.Get("label")[0]))
|
||||
return []swarm.Config{
|
||||
|
|
|
@ -2,11 +2,12 @@ package config
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
@ -34,17 +35,23 @@ func newConfigRemoveCommand(dockerCli command.Cli) *cobra.Command {
|
|||
}
|
||||
|
||||
// RunConfigRemove removes the given Swarm configs.
|
||||
func RunConfigRemove(ctx context.Context, dockerCLI command.Cli, opts RemoveOptions) error {
|
||||
apiClient := dockerCLI.Client()
|
||||
func RunConfigRemove(ctx context.Context, dockerCli command.Cli, opts RemoveOptions) error {
|
||||
client := dockerCli.Client()
|
||||
|
||||
var errs []string
|
||||
|
||||
var errs []error
|
||||
for _, name := range opts.Names {
|
||||
if err := apiClient.ConfigRemove(ctx, name); err != nil {
|
||||
errs = append(errs, err)
|
||||
if err := client.ConfigRemove(ctx, name); err != nil {
|
||||
errs = append(errs, err.Error())
|
||||
continue
|
||||
}
|
||||
_, _ = fmt.Fprintln(dockerCLI.Out(), name)
|
||||
|
||||
fmt.Fprintln(dockerCli.Out(), name)
|
||||
}
|
||||
|
||||
return errors.Join(errs...)
|
||||
if len(errs) > 0 {
|
||||
return errors.Errorf("%s", strings.Join(errs, "\n"))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/pkg/errors"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
|
@ -19,12 +19,12 @@ func TestConfigRemoveErrors(t *testing.T) {
|
|||
}{
|
||||
{
|
||||
args: []string{},
|
||||
expectedError: "requires at least 1 argument",
|
||||
expectedError: "requires at least 1 argument.",
|
||||
},
|
||||
{
|
||||
args: []string{"foo"},
|
||||
configRemoveFunc: func(name string) error {
|
||||
return errors.New("error removing config")
|
||||
return errors.Errorf("error removing config")
|
||||
},
|
||||
expectedError: "error removing config",
|
||||
},
|
||||
|
@ -66,7 +66,7 @@ func TestConfigRemoveContinueAfterError(t *testing.T) {
|
|||
configRemoveFunc: func(name string) error {
|
||||
removedConfigs = append(removedConfigs, name)
|
||||
if name == "foo" {
|
||||
return errors.New("error removing config: " + name)
|
||||
return errors.Errorf("error removing config: %s", name)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
|
|
|
@ -7,8 +7,9 @@ import (
|
|||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/moby/sys/signal"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
@ -22,7 +23,7 @@ type AttachOptions struct {
|
|||
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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -55,8 +56,8 @@ func NewAttachCommand(dockerCLI command.Cli) *cobra.Command {
|
|||
Annotations: map[string]string{
|
||||
"aliases": "docker container attach, docker attach",
|
||||
},
|
||||
ValidArgsFunction: completion.ContainerNames(dockerCLI, false, func(ctr container.Summary) bool {
|
||||
return ctr.State != container.StatePaused
|
||||
ValidArgsFunction: completion.ContainerNames(dockerCLI, false, func(ctr types.Container) bool {
|
||||
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)}
|
||||
}
|
||||
case err := <-errC:
|
||||
if errors.Is(err, context.Canceled) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"context"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"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"
|
||||
)
|
||||
|
||||
|
@ -16,63 +18,59 @@ func TestNewAttachCommandErrors(t *testing.T) {
|
|||
name string
|
||||
args []string
|
||||
expectedError string
|
||||
containerInspectFunc func(img string) (container.InspectResponse, error)
|
||||
containerInspectFunc func(img string) (types.ContainerJSON, error)
|
||||
}{
|
||||
{
|
||||
name: "client-error",
|
||||
args: []string{"5cb5bb5e4a3b"},
|
||||
expectedError: "something went wrong",
|
||||
containerInspectFunc: func(containerID string) (container.InspectResponse, error) {
|
||||
return container.InspectResponse{}, errors.New("something went wrong")
|
||||
containerInspectFunc: func(containerID string) (types.ContainerJSON, error) {
|
||||
return types.ContainerJSON{}, errors.Errorf("something went wrong")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "client-stopped",
|
||||
args: []string{"5cb5bb5e4a3b"},
|
||||
expectedError: "You cannot attach to a stopped container",
|
||||
containerInspectFunc: func(containerID string) (container.InspectResponse, error) {
|
||||
return container.InspectResponse{
|
||||
ContainerJSONBase: &container.ContainerJSONBase{
|
||||
State: &container.State{
|
||||
Running: false,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
containerInspectFunc: func(containerID string) (types.ContainerJSON, error) {
|
||||
c := types.ContainerJSON{}
|
||||
c.ContainerJSONBase = &types.ContainerJSONBase{}
|
||||
c.ContainerJSONBase.State = &types.ContainerState{Running: false}
|
||||
return c, nil
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "client-paused",
|
||||
args: []string{"5cb5bb5e4a3b"},
|
||||
expectedError: "You cannot attach to a paused container",
|
||||
containerInspectFunc: func(containerID string) (container.InspectResponse, error) {
|
||||
return container.InspectResponse{
|
||||
ContainerJSONBase: &container.ContainerJSONBase{
|
||||
State: &container.State{
|
||||
Running: true,
|
||||
Paused: true,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
containerInspectFunc: func(containerID string) (types.ContainerJSON, error) {
|
||||
c := types.ContainerJSON{}
|
||||
c.ContainerJSONBase = &types.ContainerJSONBase{}
|
||||
c.ContainerJSONBase.State = &types.ContainerState{
|
||||
Running: true,
|
||||
Paused: true,
|
||||
}
|
||||
return c, nil
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "client-restarting",
|
||||
args: []string{"5cb5bb5e4a3b"},
|
||||
expectedError: "You cannot attach to a restarting container",
|
||||
containerInspectFunc: func(containerID string) (container.InspectResponse, error) {
|
||||
return container.InspectResponse{
|
||||
ContainerJSONBase: &container.ContainerJSONBase{
|
||||
State: &container.State{
|
||||
Running: true,
|
||||
Paused: false,
|
||||
Restarting: true,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
containerInspectFunc: func(containerID string) (types.ContainerJSON, error) {
|
||||
c := types.ContainerJSON{}
|
||||
c.ContainerJSONBase = &types.ContainerJSONBase{}
|
||||
c.ContainerJSONBase.State = &types.ContainerState{
|
||||
Running: true,
|
||||
Paused: false,
|
||||
Restarting: true,
|
||||
}
|
||||
return c, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cmd := NewAttachCommand(test.NewFakeCli(&fakeClient{inspectFunc: tc.containerInspectFunc}))
|
||||
cmd.SetOut(io.Discard)
|
||||
|
@ -112,6 +110,10 @@ func TestGetExitStatus(t *testing.T) {
|
|||
},
|
||||
expectedError: cli.StatusError{StatusCode: 15},
|
||||
},
|
||||
{
|
||||
err: context.Canceled,
|
||||
expectedError: nil,
|
||||
},
|
||||
}
|
||||
|
||||
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"
|
||||
"io"
|
||||
|
||||
"github.com/moby/moby/api/types"
|
||||
"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/system"
|
||||
"github.com/moby/moby/client"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/docker/api/types/system"
|
||||
"github.com/docker/docker/client"
|
||||
specs "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
)
|
||||
|
||||
type fakeClient struct {
|
||||
client.Client
|
||||
inspectFunc func(string) (container.InspectResponse, error)
|
||||
inspectFunc func(string) (types.ContainerJSON, 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,
|
||||
hostConfig *container.HostConfig,
|
||||
networkingConfig *network.NetworkingConfig,
|
||||
platform *ocispec.Platform,
|
||||
platform *specs.Platform,
|
||||
containerName string) (container.CreateResponse, error)
|
||||
containerStartFunc func(containerID string, options container.StartOptions) error
|
||||
imageCreateFunc func(ctx context.Context, parentReference string, options image.CreateOptions) (io.ReadCloser, error)
|
||||
imageCreateFunc func(parentReference string, options image.CreateOptions) (io.ReadCloser, error)
|
||||
infoFunc func() (system.Info, error)
|
||||
containerStatPathFunc func(containerID, path string) (container.PathStat, error)
|
||||
containerCopyFromFunc func(containerID, srcPath string) (io.ReadCloser, container.PathStat, error)
|
||||
logFunc func(string, container.LogsOptions) (io.ReadCloser, error)
|
||||
waitFunc func(string) (<-chan container.WaitResponse, <-chan error)
|
||||
containerListFunc func(container.ListOptions) ([]container.Summary, error)
|
||||
containerListFunc func(container.ListOptions) ([]types.Container, error)
|
||||
containerExportFunc func(string) (io.ReadCloser, error)
|
||||
containerExecResizeFunc func(id string, options container.ResizeOptions) error
|
||||
containerRemoveFunc func(ctx context.Context, containerID string, options container.RemoveOptions) error
|
||||
containerRestartFunc func(ctx context.Context, containerID string, options container.StopOptions) error
|
||||
containerStopFunc func(ctx context.Context, containerID string, options container.StopOptions) error
|
||||
containerKillFunc func(ctx context.Context, containerID, signal string) error
|
||||
containerPruneFunc func(ctx context.Context, pruneFilters filters.Args) (container.PruneReport, error)
|
||||
containerAttachFunc func(ctx context.Context, containerID string, options container.AttachOptions) (types.HijackedResponse, error)
|
||||
containerDiffFunc func(ctx context.Context, containerID string) ([]container.FilesystemChange, error)
|
||||
containerRenameFunc func(ctx context.Context, oldName, newName string) error
|
||||
containerCommitFunc func(ctx context.Context, container string, options container.CommitOptions) (container.CommitResponse, error)
|
||||
containerPauseFunc func(ctx context.Context, container string) error
|
||||
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 {
|
||||
return f.containerListFunc(options)
|
||||
}
|
||||
return []container.Summary{}, nil
|
||||
return []types.Container{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerInspect(_ context.Context, containerID string) (container.InspectResponse, error) {
|
||||
func (f *fakeClient) ContainerInspect(_ context.Context, containerID string) (types.ContainerJSON, error) {
|
||||
if f.inspectFunc != nil {
|
||||
return f.inspectFunc(containerID)
|
||||
}
|
||||
return container.InspectResponse{}, nil
|
||||
return types.ContainerJSON{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerExecCreate(_ context.Context, containerID string, config container.ExecOptions) (container.ExecCreateResponse, error) {
|
||||
func (f *fakeClient) ContainerExecCreate(_ context.Context, containerID string, config container.ExecOptions) (types.IDResponse, error) {
|
||||
if f.execCreateFunc != nil {
|
||||
return f.execCreateFunc(containerID, config)
|
||||
}
|
||||
return container.ExecCreateResponse{}, nil
|
||||
return types.IDResponse{}, nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func (*fakeClient) ContainerExecStart(context.Context, string, container.ExecStartOptions) error {
|
||||
func (f *fakeClient) ContainerExecStart(context.Context, string, container.ExecStartOptions) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -84,7 +78,7 @@ func (f *fakeClient) ContainerCreate(
|
|||
config *container.Config,
|
||||
hostConfig *container.HostConfig,
|
||||
networkingConfig *network.NetworkingConfig,
|
||||
platform *ocispec.Platform,
|
||||
platform *specs.Platform,
|
||||
containerName string,
|
||||
) (container.CreateResponse, error) {
|
||||
if f.createContainerFunc != nil {
|
||||
|
@ -100,9 +94,9 @@ func (f *fakeClient) ContainerRemove(ctx context.Context, containerID string, op
|
|||
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 {
|
||||
return f.imageCreateFunc(ctx, parentReference, options)
|
||||
return f.imageCreateFunc(parentReference, options)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
@ -181,54 +175,9 @@ func (f *fakeClient) ContainersPrune(ctx context.Context, pruneFilters filters.A
|
|||
return container.PruneReport{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerRestart(ctx context.Context, containerID string, options container.StopOptions) error {
|
||||
if f.containerRestartFunc != nil {
|
||||
return f.containerRestartFunc(ctx, containerID, options)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerStop(ctx context.Context, containerID string, options container.StopOptions) error {
|
||||
if f.containerStopFunc != nil {
|
||||
return f.containerStopFunc(ctx, containerID, options)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerAttach(ctx context.Context, containerID string, options container.AttachOptions) (types.HijackedResponse, error) {
|
||||
if f.containerAttachFunc != nil {
|
||||
return f.containerAttachFunc(ctx, containerID, options)
|
||||
}
|
||||
return types.HijackedResponse{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerDiff(ctx context.Context, containerID string) ([]container.FilesystemChange, error) {
|
||||
if f.containerDiffFunc != nil {
|
||||
return f.containerDiffFunc(ctx, containerID)
|
||||
}
|
||||
|
||||
return []container.FilesystemChange{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerRename(ctx context.Context, oldName, newName string) error {
|
||||
if f.containerRenameFunc != nil {
|
||||
return f.containerRenameFunc(ctx, oldName, newName)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerCommit(ctx context.Context, containerID string, options container.CommitOptions) (container.CommitResponse, error) {
|
||||
if f.containerCommitFunc != nil {
|
||||
return f.containerCommitFunc(ctx, containerID, options)
|
||||
}
|
||||
return container.CommitResponse{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerPause(ctx context.Context, containerID string) error {
|
||||
if f.containerPauseFunc != nil {
|
||||
return f.containerPauseFunc(ctx, containerID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@ func NewContainerCommand(dockerCli command.Cli) *cobra.Command {
|
|||
NewPortCommand(dockerCli),
|
||||
NewRenameCommand(dockerCli),
|
||||
NewRestartCommand(dockerCli),
|
||||
newRemoveCommand(dockerCli),
|
||||
NewRmCommand(dockerCli),
|
||||
NewRunCommand(dockerCli),
|
||||
NewStartCommand(dockerCli),
|
||||
NewStatsCommand(dockerCli),
|
||||
|
|
|
@ -8,7 +8,7 @@ import (
|
|||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
@ -61,7 +61,7 @@ func runCommit(ctx context.Context, dockerCli command.Cli, options *commitOption
|
|||
Reference: options.reference,
|
||||
Comment: options.comment,
|
||||
Author: options.author,
|
||||
Changes: options.changes.GetSlice(),
|
||||
Changes: options.changes.GetAll(),
|
||||
Pause: options.pause,
|
||||
})
|
||||
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,111 +1,71 @@
|
|||
// 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
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/sys/capability"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/moby/sys/signal"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// allCaps is the magic value for "all capabilities".
|
||||
const allCaps = "ALL"
|
||||
|
||||
// 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): consider what casing we want to use for completion (see below);
|
||||
//
|
||||
// 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
|
||||
})
|
||||
var allLinuxCapabilities = []string{
|
||||
"ALL", // magic value for "all capabilities"
|
||||
|
||||
// logDriverOptions provides the options for each built-in logging driver.
|
||||
var logDriverOptions = map[string][]string{
|
||||
"awslogs": {
|
||||
"max-buffer-size", "mode", "awslogs-create-group", "awslogs-credentials-endpoint", "awslogs-datetime-format",
|
||||
"awslogs-group", "awslogs-multiline-pattern", "awslogs-region", "awslogs-stream", "tag",
|
||||
},
|
||||
"fluentd": {
|
||||
"max-buffer-size", "mode", "env", "env-regex", "labels", "fluentd-address", "fluentd-async",
|
||||
"fluentd-buffer-limit", "fluentd-request-ack", "fluentd-retry-wait", "fluentd-max-retries",
|
||||
"fluentd-sub-second-precision", "fluentd-write-timeout", "tag",
|
||||
},
|
||||
"gcplogs": {
|
||||
"max-buffer-size", "mode", "env", "env-regex", "labels", "gcp-log-cmd", "gcp-meta-id", "gcp-meta-name",
|
||||
"gcp-meta-zone", "gcp-project",
|
||||
},
|
||||
"gelf": {
|
||||
"max-buffer-size", "mode", "env", "env-regex", "labels", "gelf-address", "gelf-compression-level",
|
||||
"gelf-compression-type", "gelf-tcp-max-reconnect", "gelf-tcp-reconnect-delay", "tag",
|
||||
},
|
||||
"journald": {"max-buffer-size", "mode", "env", "env-regex", "labels", "tag"},
|
||||
"json-file": {"max-buffer-size", "mode", "env", "env-regex", "labels", "compress", "max-file", "max-size"},
|
||||
"local": {"max-buffer-size", "mode", "compress", "max-file", "max-size"},
|
||||
"none": {},
|
||||
"splunk": {
|
||||
"max-buffer-size", "mode", "env", "env-regex", "labels", "splunk-caname", "splunk-capath", "splunk-format",
|
||||
"splunk-gzip", "splunk-gzip-level", "splunk-index", "splunk-insecureskipverify", "splunk-source",
|
||||
"splunk-sourcetype", "splunk-token", "splunk-url", "splunk-verify-connection", "tag",
|
||||
},
|
||||
"syslog": {
|
||||
"max-buffer-size", "mode", "env", "env-regex", "labels", "syslog-address", "syslog-facility", "syslog-format",
|
||||
"syslog-tls-ca-cert", "syslog-tls-cert", "syslog-tls-key", "syslog-tls-skip-verify", "tag",
|
||||
},
|
||||
// caps35 is the caps of kernel 3.5 (37 entries)
|
||||
"CAP_CHOWN", // 2.2
|
||||
"CAP_DAC_OVERRIDE", // 2.2
|
||||
"CAP_DAC_READ_SEARCH", // 2.2
|
||||
"CAP_FOWNER", // 2.2
|
||||
"CAP_FSETID", // 2.2
|
||||
"CAP_KILL", // 2.2
|
||||
"CAP_SETGID", // 2.2
|
||||
"CAP_SETUID", // 2.2
|
||||
"CAP_SETPCAP", // 2.2
|
||||
"CAP_LINUX_IMMUTABLE", // 2.2
|
||||
"CAP_NET_BIND_SERVICE", // 2.2
|
||||
"CAP_NET_BROADCAST", // 2.2
|
||||
"CAP_NET_ADMIN", // 2.2
|
||||
"CAP_NET_RAW", // 2.2
|
||||
"CAP_IPC_LOCK", // 2.2
|
||||
"CAP_IPC_OWNER", // 2.2
|
||||
"CAP_SYS_MODULE", // 2.2
|
||||
"CAP_SYS_RAWIO", // 2.2
|
||||
"CAP_SYS_CHROOT", // 2.2
|
||||
"CAP_SYS_PTRACE", // 2.2
|
||||
"CAP_SYS_PACCT", // 2.2
|
||||
"CAP_SYS_ADMIN", // 2.2
|
||||
"CAP_SYS_BOOT", // 2.2
|
||||
"CAP_SYS_NICE", // 2.2
|
||||
"CAP_SYS_RESOURCE", // 2.2
|
||||
"CAP_SYS_TIME", // 2.2
|
||||
"CAP_SYS_TTY_CONFIG", // 2.2
|
||||
"CAP_MKNOD", // 2.4
|
||||
"CAP_LEASE", // 2.4
|
||||
"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
|
||||
|
||||
// caps316 is the caps of kernel 3.16 (38 entries)
|
||||
"CAP_AUDIT_READ",
|
||||
|
||||
// caps58 is the caps of kernel 5.8 (40 entries)
|
||||
"CAP_PERFMON",
|
||||
"CAP_BPF",
|
||||
|
||||
// caps59 is the caps of kernel 5.9 (41 entries)
|
||||
"CAP_CHECKPOINT_RESTORE",
|
||||
}
|
||||
|
||||
// builtInLogDrivers provides a list of the built-in logging drivers.
|
||||
var builtInLogDrivers = sync.OnceValue(func() []string {
|
||||
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.
|
||||
// The list does not contain duplicates.
|
||||
var allLogDriverOptions = sync.OnceValue(func() []string {
|
||||
var result []string
|
||||
seen := make(map[string]bool)
|
||||
for driver := range logDriverOptions {
|
||||
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..
|
||||
//
|
||||
// TODO(thaJeztah): add descriptions, and enable descriptions for our completion scripts (cobra.CompletionOptions.DisableDescriptions is currently set to "true")
|
||||
|
@ -116,209 +76,8 @@ var restartPolicies = []string{
|
|||
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) {
|
||||
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) {
|
||||
|
|
|
@ -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/command"
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/docker/go-units"
|
||||
"github.com/moby/go-archive"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/pkg/archive"
|
||||
"github.com/docker/docker/pkg/system"
|
||||
units "github.com/docker/go-units"
|
||||
"github.com/morikuni/aec"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
|
@ -129,12 +130,13 @@ func NewCopyCommand(dockerCli command.Cli) *cobra.Command {
|
|||
Use: `cp [OPTIONS] CONTAINER:SRC_PATH DEST_PATH|-
|
||||
docker cp [OPTIONS] SRC_PATH|- CONTAINER:DEST_PATH`,
|
||||
Short: "Copy files/folders between a container and the local filesystem",
|
||||
Long: `Copy files/folders between a container and the local filesystem
|
||||
|
||||
Use '-' as the source to read a tar archive from stdin
|
||||
and extract it to a directory destination in a container.
|
||||
Use '-' as the destination to stream a tar archive of a
|
||||
container source to stdout.`,
|
||||
Long: strings.Join([]string{
|
||||
"Copy files/folders between a container and the local filesystem\n",
|
||||
"\nUse '-' as the source to read a tar archive from stdin\n",
|
||||
"and extract it to a directory destination in a container.\n",
|
||||
"Use '-' as the destination to stream a tar archive of a\n",
|
||||
"container source to stdout.",
|
||||
}, ""),
|
||||
Args: cli.ExactArgs(2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
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) {
|
||||
absPath, err := filepath.Abs(localPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
func resolveLocalPath(localPath string) (absPath string, err error) {
|
||||
if absPath, err = filepath.Abs(localPath); err != nil {
|
||||
return
|
||||
}
|
||||
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
|
||||
srcPath := copyConfig.sourcePath
|
||||
|
||||
|
@ -225,16 +226,16 @@ func copyFromContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cp
|
|||
return err
|
||||
}
|
||||
|
||||
apiClient := dockerCLI.Client()
|
||||
client := dockerCli.Client()
|
||||
// if client requests to follow symbol link, then must decide target file to be copied
|
||||
var rebaseName string
|
||||
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 err == nil && srcStat.Mode&os.ModeSymlink != 0 {
|
||||
linkTarget := srcStat.LinkTarget
|
||||
if !isAbs(linkTarget) {
|
||||
if !system.IsAbs(linkTarget) {
|
||||
// Join with the parent directory.
|
||||
srcParent, _ := archive.SplitPathDirEntry(srcPath)
|
||||
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)
|
||||
defer cancel()
|
||||
|
||||
content, stat, err := apiClient.CopyFromContainer(ctx, copyConfig.container, srcPath)
|
||||
content, stat, err := client.CopyFromContainer(ctx, copyConfig.container, srcPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer content.Close()
|
||||
|
||||
if dstPath == "-" {
|
||||
_, err = io.Copy(dockerCLI.Out(), content)
|
||||
_, err = io.Copy(dockerCli.Out(), content)
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -284,12 +285,12 @@ func copyFromContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cp
|
|||
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)
|
||||
cancel()
|
||||
<-done
|
||||
restore()
|
||||
_, _ = fmt.Fprintln(dockerCLI.Err(), "Successfully copied", progressHumanSize(copiedSize), "to", dstPath)
|
||||
fmt.Fprintln(dockerCli.Err(), "Successfully copied", progressHumanSize(copiedSize), "to", dstPath)
|
||||
|
||||
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
|
||||
// archive/extract API but we can use the stat info header about the
|
||||
// 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
|
||||
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.
|
||||
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 err == nil && dstStat.Mode&os.ModeSymlink != 0 {
|
||||
linkTarget := dstStat.LinkTarget
|
||||
if !isAbs(linkTarget) {
|
||||
if !system.IsAbs(linkTarget) {
|
||||
// Join with the parent directory.
|
||||
dstParent, _ := archive.SplitPathDirEntry(dstPath)
|
||||
linkTarget = filepath.Join(dstParent, linkTarget)
|
||||
}
|
||||
|
||||
dstInfo.Path = linkTarget
|
||||
dstStat, err = apiClient.ContainerStatPath(ctx, copyConfig.container, linkTarget)
|
||||
// FIXME(thaJeztah): unhandled error (should this return?)
|
||||
dstStat, err = client.ContainerStatPath(ctx, copyConfig.container, linkTarget)
|
||||
}
|
||||
|
||||
// Validate the destination path
|
||||
|
@ -398,20 +398,21 @@ func copyToContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cpCo
|
|||
}
|
||||
|
||||
options := container.CopyToContainerOptions{
|
||||
CopyUIDGID: copyConfig.copyUIDGID,
|
||||
AllowOverwriteDirWithFile: false,
|
||||
CopyUIDGID: copyConfig.copyUIDGID,
|
||||
}
|
||||
|
||||
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)
|
||||
restore, done := copyProgress(ctx, dockerCLI.Err(), copyToContainerHeader, &copiedSize)
|
||||
res := apiClient.CopyToContainer(ctx, copyConfig.container, resolvedDstPath, content, options)
|
||||
restore, done := copyProgress(ctx, dockerCli.Err(), copyToContainerHeader, &copiedSize)
|
||||
res := client.CopyToContainer(ctx, copyConfig.container, resolvedDstPath, content, options)
|
||||
cancel()
|
||||
<-done
|
||||
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
|
||||
}
|
||||
|
@ -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
|
||||
// is immediately proceeded by a backslash.
|
||||
func splitCpArg(arg string) (ctr, path string) {
|
||||
if isAbs(arg) {
|
||||
if system.IsAbs(arg) {
|
||||
// Explicit local absolute path, e.g., `C:\foo` or `/foo`.
|
||||
return "", arg
|
||||
}
|
||||
|
@ -447,15 +448,3 @@ func splitCpArg(arg string) (ctr, path string) {
|
|||
|
||||
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"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/moby/go-archive"
|
||||
"github.com/moby/go-archive/compression"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/pkg/archive"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
"gotest.tools/v3/fs"
|
||||
"gotest.tools/v3/skip"
|
||||
)
|
||||
|
||||
func TestRunCopyWithInvalidArguments(t *testing.T) {
|
||||
|
@ -67,15 +67,14 @@ func TestRunCopyFromContainerToStdout(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"))
|
||||
|
||||
destDir := fs.NewDir(t, "cp-test")
|
||||
defer destDir.Remove()
|
||||
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
containerCopyFromFunc: func(ctr, srcPath string) (io.ReadCloser, container.PathStat, error) {
|
||||
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
|
||||
},
|
||||
})
|
||||
|
@ -152,7 +151,7 @@ func TestSplitCpArg(t *testing.T) {
|
|||
}{
|
||||
{
|
||||
doc: "absolute path with colon",
|
||||
os: "unix",
|
||||
os: "linux",
|
||||
path: "/abs/path:withcolon",
|
||||
expectedPath: "/abs/path:withcolon",
|
||||
},
|
||||
|
@ -180,13 +179,9 @@ func TestSplitCpArg(t *testing.T) {
|
|||
},
|
||||
}
|
||||
for _, tc := range testcases {
|
||||
tc := tc
|
||||
t.Run(tc.doc, func(t *testing.T) {
|
||||
if tc.os == "windows" && 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")
|
||||
}
|
||||
skip.If(t, tc.os == "windows" && runtime.GOOS != "windows" || tc.os == "linux" && runtime.GOOS == "windows")
|
||||
|
||||
ctr, path := splitCpArg(tc.path)
|
||||
assert.Check(t, is.Equal(tc.expectedContainer, ctr))
|
||||
|
|
|
@ -1,35 +1,26 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/netip"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"regexp"
|
||||
|
||||
cerrdefs "github.com/containerd/errdefs"
|
||||
"github.com/containerd/platforms"
|
||||
"github.com/distribution/reference"
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"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/trust"
|
||||
"github.com/docker/cli/internal/jsonstream"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
imagetypes "github.com/moby/moby/api/types/image"
|
||||
"github.com/moby/moby/api/types/mount"
|
||||
"github.com/moby/moby/api/types/versions"
|
||||
"github.com/moby/moby/client"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
imagetypes "github.com/docker/docker/api/types/image"
|
||||
"github.com/docker/docker/api/types/versions"
|
||||
"github.com/docker/docker/errdefs"
|
||||
"github.com/docker/docker/pkg/jsonmessage"
|
||||
specs "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
|
@ -43,12 +34,11 @@ const (
|
|||
)
|
||||
|
||||
type createOptions struct {
|
||||
name string
|
||||
platform string
|
||||
untrusted bool
|
||||
pull string // always, missing, never
|
||||
quiet bool
|
||||
useAPISocket bool
|
||||
name string
|
||||
platform string
|
||||
untrusted bool
|
||||
pull string // always, missing, never
|
||||
quiet bool
|
||||
}
|
||||
|
||||
// NewCreateCommand creates a new cobra.Command for `docker create`
|
||||
|
@ -70,7 +60,7 @@ func NewCreateCommand(dockerCli command.Cli) *cobra.Command {
|
|||
Annotations: map[string]string{
|
||||
"aliases": "docker container create, docker create",
|
||||
},
|
||||
ValidArgsFunction: completion.ImageNames(dockerCli, -1),
|
||||
ValidArgsFunction: completion.ImageNames(dockerCli),
|
||||
}
|
||||
|
||||
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.pull, "pull", PullImageMissing, `Pull image before creating ("`+PullImageAlways+`", "|`+PullImageMissing+`", "`+PullImageNever+`")`)
|
||||
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
|
||||
// with hostname
|
||||
|
@ -90,26 +78,24 @@ func NewCreateCommand(dockerCli command.Cli) *cobra.Command {
|
|||
command.AddTrustVerificationFlags(flags, &options.untrusted, dockerCli.ContentTrustEnabled())
|
||||
copts = addFlags(flags)
|
||||
|
||||
addCompletions(cmd, dockerCli)
|
||||
|
||||
flags.VisitAll(func(flag *pflag.Flag) {
|
||||
// Set a default completion function if none was set. We don't look
|
||||
// up if it does already have one set, because Cobra does this for
|
||||
// us, and returns an error (which we ignore for this reason).
|
||||
_ = cmd.RegisterFlagCompletionFunc(flag.Name, completion.NoComplete)
|
||||
})
|
||||
|
||||
_ = cmd.RegisterFlagCompletionFunc("cap-add", completeLinuxCapabilityNames)
|
||||
_ = cmd.RegisterFlagCompletionFunc("cap-drop", completeLinuxCapabilityNames)
|
||||
_ = cmd.RegisterFlagCompletionFunc("env", completion.EnvVarNames)
|
||||
_ = cmd.RegisterFlagCompletionFunc("env-file", completion.FileNames)
|
||||
_ = cmd.RegisterFlagCompletionFunc("network", completion.NetworkNames(dockerCli))
|
||||
_ = cmd.RegisterFlagCompletionFunc("pull", completion.FromList(PullImageAlways, PullImageMissing, PullImageNever))
|
||||
_ = cmd.RegisterFlagCompletionFunc("restart", completeRestartPolicies)
|
||||
_ = cmd.RegisterFlagCompletionFunc("stop-signal", completeSignals)
|
||||
_ = cmd.RegisterFlagCompletionFunc("volumes-from", completion.ContainerNames(dockerCli, true))
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runCreate(ctx context.Context, dockerCli command.Cli, flags *pflag.FlagSet, options *createOptions, copts *containerOptions) error {
|
||||
if err := validatePullOpt(options.pull); err != nil {
|
||||
return cli.StatusError{
|
||||
Status: withHelp(err, "create").Error(),
|
||||
StatusCode: 125,
|
||||
}
|
||||
reportError(dockerCli.Err(), "create", err.Error(), true)
|
||||
return cli.StatusError{StatusCode: 125}
|
||||
}
|
||||
proxyConfig := dockerCli.ConfigFile().ParseProxyConfig(dockerCli.Client().DaemonHost(), opts.ConvertKVStringsToMapWithNil(copts.env.GetSlice()))
|
||||
proxyConfig := dockerCli.ConfigFile().ParseProxyConfig(dockerCli.Client().DaemonHost(), opts.ConvertKVStringsToMapWithNil(copts.env.GetAll()))
|
||||
newEnv := []string{}
|
||||
for k, v := range proxyConfig {
|
||||
if v == nil {
|
||||
|
@ -121,10 +107,12 @@ func runCreate(ctx context.Context, dockerCli command.Cli, flags *pflag.FlagSet,
|
|||
copts.env = *opts.NewListOptsRef(&newEnv, nil)
|
||||
containerCfg, err := parse(flags, copts, dockerCli.ServerInfo().OSType)
|
||||
if err != nil {
|
||||
return cli.StatusError{
|
||||
Status: withHelp(err, "create").Error(),
|
||||
StatusCode: 125,
|
||||
}
|
||||
reportError(dockerCli.Err(), "create", err.Error(), true)
|
||||
return cli.StatusError{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)
|
||||
if err != nil {
|
||||
|
@ -154,7 +142,7 @@ func pullImage(ctx context.Context, dockerCli command.Cli, img string, options *
|
|||
if options.quiet {
|
||||
out = streams.NewOut(io.Discard)
|
||||
}
|
||||
return jsonstream.Display(ctx, responseBody, out)
|
||||
return jsonmessage.DisplayJSONMessagesToStream(responseBody, out, nil)
|
||||
}
|
||||
|
||||
type cidFile struct {
|
||||
|
@ -190,20 +178,20 @@ func (cid *cidFile) Write(id string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func newCIDFile(cidPath string) (*cidFile, error) {
|
||||
if cidPath == "" {
|
||||
func newCIDFile(path string) (*cidFile, error) {
|
||||
if path == "" {
|
||||
return &cidFile{}, nil
|
||||
}
|
||||
if _, err := os.Stat(cidPath); err == nil {
|
||||
return nil, errors.Errorf("container ID file found, make sure the other container isn't running or delete %s", cidPath)
|
||||
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", path)
|
||||
}
|
||||
|
||||
f, err := os.Create(cidPath)
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
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
|
||||
|
@ -212,6 +200,9 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *c
|
|||
hostConfig := containerCfg.HostConfig
|
||||
networkingConfig := containerCfg.NetworkingConfig
|
||||
|
||||
warnOnOomKillDisable(*hostConfig, dockerCli.Err())
|
||||
warnOnLocalhostDNS(*hostConfig, dockerCli.Err())
|
||||
|
||||
var (
|
||||
trustedRef reference.Canonical
|
||||
namedRef reference.Named
|
||||
|
@ -240,74 +231,17 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *c
|
|||
}
|
||||
}
|
||||
|
||||
const dockerConfigPathInContainer = "/run/secrets/docker/config.json"
|
||||
var apiSocketCreds map[string]types.AuthConfig
|
||||
|
||||
if options.useAPISocket {
|
||||
// We'll create two new mounts to handle this flag:
|
||||
// 1. Mount the actual docker socket.
|
||||
// 2. A synthezised ~/.docker/config.json with resolved tokens.
|
||||
|
||||
if dockerCli.ServerInfo().OSType == "windows" {
|
||||
return "", errors.New("flag --use-api-socket can't be used with a Windows Docker Engine")
|
||||
pullAndTagImage := func() error {
|
||||
if err := pullImage(ctx, dockerCli, config.Image, options); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// hard-code engine socket path until https://github.com/moby/moby/pull/43459 gives us a discovery mechanism
|
||||
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.
|
||||
}
|
||||
if taggedRef, ok := namedRef.(reference.NamedTagged); ok && trustedRef != nil {
|
||||
return image.TagTrusted(ctx, dockerCli, trustedRef, taggedRef)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var platform *ocispec.Platform
|
||||
var platform *specs.Platform
|
||||
// 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
|
||||
// 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") {
|
||||
p, err := platforms.Parse(options.platform)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(invalidParameter(err), "error parsing specified platform")
|
||||
return "", errors.Wrap(errdefs.InvalidParameter(err), "error parsing specified platform")
|
||||
}
|
||||
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 err := pullAndTagImage(); err != nil {
|
||||
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)
|
||||
if err != nil {
|
||||
// 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 {
|
||||
// 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 {
|
||||
|
@ -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 {
|
||||
_, _ = 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 {
|
||||
return "", fmt.Errorf("injecting docker config.json into container failed: %w", err)
|
||||
}
|
||||
func warnOnOomKillDisable(hostConfig container.HostConfig, stderr io.Writer) {
|
||||
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
|
||||
// they are trying to set a DNS to a localhost address.
|
||||
//
|
||||
// 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 {
|
||||
// they are trying to set a DNS to a localhost address
|
||||
func warnOnLocalhostDNS(hostConfig container.HostConfig, stderr io.Writer) {
|
||||
for _, dnsIP := range hostConfig.DNS {
|
||||
if addr, err := netip.ParseAddr(dnsIP); err == nil && addr.IsLoopback() {
|
||||
return fmt.Sprintf("Localhost DNS (%s) may fail in containers.", addr)
|
||||
if isLocalhost(dnsIP) {
|
||||
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 {
|
||||
|
@ -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/internal/test"
|
||||
"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/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/api/types/image"
|
||||
"github.com/moby/moby/api/types/network"
|
||||
"github.com/moby/moby/api/types/system"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
specs "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/spf13/pflag"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
|
@ -113,6 +113,7 @@ func TestCreateContainerImagePullPolicy(t *testing.T) {
|
|||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(tc.PullPolicy, func(t *testing.T) {
|
||||
pullCounter := 0
|
||||
|
||||
|
@ -121,7 +122,7 @@ func TestCreateContainerImagePullPolicy(t *testing.T) {
|
|||
config *container.Config,
|
||||
hostConfig *container.HostConfig,
|
||||
networkingConfig *network.NetworkingConfig,
|
||||
platform *ocispec.Platform,
|
||||
platform *specs.Platform,
|
||||
containerName string,
|
||||
) (container.CreateResponse, error) {
|
||||
defer func() { tc.ResponseCounter++ }()
|
||||
|
@ -132,7 +133,7 @@ func TestCreateContainerImagePullPolicy(t *testing.T) {
|
|||
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++ }()
|
||||
return io.NopCloser(strings.NewReader("")), nil
|
||||
},
|
||||
|
@ -175,6 +176,7 @@ func TestCreateContainerImagePullPolicyInvalid(t *testing.T) {
|
|||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(tc.PullPolicy, func(t *testing.T) {
|
||||
dockerCli := test.NewFakeCli(&fakeClient{})
|
||||
err := runCreate(
|
||||
|
@ -187,36 +189,8 @@ func TestCreateContainerImagePullPolicyInvalid(t *testing.T) {
|
|||
|
||||
statusErr := cli.StatusError{}
|
||||
assert.Check(t, errors.As(err, &statusErr))
|
||||
assert.Check(t, is.Equal(statusErr.StatusCode, 125))
|
||||
assert.Check(t, is.ErrorContains(err, 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))
|
||||
}
|
||||
assert.Equal(t, statusErr.StatusCode, 125)
|
||||
assert.Check(t, is.Contains(dockerCli.ErrBuffer().String(), tc.ExpectedErrMsg))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -248,48 +222,54 @@ func TestNewCreateCommandWithContentTrustErrors(t *testing.T) {
|
|||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
fakeCLI := test.NewFakeCli(&fakeClient{
|
||||
createContainerFunc: func(config *container.Config,
|
||||
hostConfig *container.HostConfig,
|
||||
networkingConfig *network.NetworkingConfig,
|
||||
platform *ocispec.Platform,
|
||||
containerName string,
|
||||
) (container.CreateResponse, error) {
|
||||
return container.CreateResponse{}, errors.New("shouldn't try to pull image")
|
||||
},
|
||||
}, test.EnableContentTrust)
|
||||
fakeCLI.SetNotaryClient(tc.notaryFunc)
|
||||
cmd := NewCreateCommand(fakeCLI)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
err := cmd.Execute()
|
||||
assert.ErrorContains(t, err, tc.expectedError)
|
||||
})
|
||||
tc := tc
|
||||
fakeCLI := test.NewFakeCli(&fakeClient{
|
||||
createContainerFunc: func(config *container.Config,
|
||||
hostConfig *container.HostConfig,
|
||||
networkingConfig *network.NetworkingConfig,
|
||||
platform *specs.Platform,
|
||||
containerName string,
|
||||
) (container.CreateResponse, error) {
|
||||
return container.CreateResponse{}, errors.New("shouldn't try to pull image")
|
||||
},
|
||||
}, test.EnableContentTrust)
|
||||
fakeCLI.SetNotaryClient(tc.notaryFunc)
|
||||
cmd := NewCreateCommand(fakeCLI)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
err := cmd.Execute()
|
||||
assert.ErrorContains(t, err, tc.expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewCreateCommandWithWarnings(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
args []string
|
||||
warnings []string
|
||||
warning bool
|
||||
name string
|
||||
args []string
|
||||
warning bool
|
||||
}{
|
||||
{
|
||||
name: "container-create-no-warnings",
|
||||
name: "container-create-without-oom-kill-disable",
|
||||
args: []string{"image:tag"},
|
||||
},
|
||||
{
|
||||
name: "container-create-daemon-single-warning",
|
||||
args: []string{"image:tag"},
|
||||
warnings: []string{"warning from daemon"},
|
||||
name: "container-create-oom-kill-disable-false",
|
||||
args: []string{"--oom-kill-disable=false", "image:tag"},
|
||||
},
|
||||
{
|
||||
name: "container-create-daemon-multiple-warnings",
|
||||
args: []string{"image:tag"},
|
||||
warnings: []string{"warning from daemon", "another warning from daemon"},
|
||||
name: "container-create-oom-kill-without-memory-limit",
|
||||
args: []string{"--oom-kill-disable", "image:tag"},
|
||||
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",
|
||||
|
@ -303,26 +283,27 @@ func TestNewCreateCommandWithWarnings(t *testing.T) {
|
|||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
fakeCLI := test.NewFakeCli(&fakeClient{
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
createContainerFunc: func(config *container.Config,
|
||||
hostConfig *container.HostConfig,
|
||||
networkingConfig *network.NetworkingConfig,
|
||||
platform *ocispec.Platform,
|
||||
platform *specs.Platform,
|
||||
containerName string,
|
||||
) (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.SetArgs(tc.args)
|
||||
err := cmd.Execute()
|
||||
assert.NilError(t, err)
|
||||
if tc.warning || len(tc.warnings) > 0 {
|
||||
golden.Assert(t, fakeCLI.ErrBuffer().String(), tc.name+".golden")
|
||||
if tc.warning {
|
||||
golden.Assert(t, cli.ErrBuffer().String(), tc.name+".golden")
|
||||
} 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,
|
||||
hostConfig *container.HostConfig,
|
||||
networkingConfig *network.NetworkingConfig,
|
||||
platform *ocispec.Platform,
|
||||
platform *specs.Platform,
|
||||
containerName string,
|
||||
) (container.CreateResponse, error) {
|
||||
sort.Strings(config.Env)
|
||||
|
@ -375,5 +356,5 @@ func TestCreateContainerWithProxyConfig(t *testing.T) {
|
|||
|
||||
type fakeNotFound struct{}
|
||||
|
||||
func (fakeNotFound) NotFound() {}
|
||||
func (fakeNotFound) Error() string { return "error fake not found" }
|
||||
func (f fakeNotFound) NotFound() {}
|
||||
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/completion"
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type diffOptions struct {
|
||||
container string
|
||||
}
|
||||
|
||||
// NewDiffCommand creates a new cobra.Command for `docker diff`
|
||||
func NewDiffCommand(dockerCli command.Cli) *cobra.Command {
|
||||
var opts diffOptions
|
||||
|
||||
return &cobra.Command{
|
||||
Use: "diff CONTAINER",
|
||||
Short: "Inspect changes to files or directories on a container's filesystem",
|
||||
Args: cli.ExactArgs(1),
|
||||
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{
|
||||
"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 {
|
||||
changes, err := dockerCLI.Client().ContainerDiff(ctx, containerID)
|
||||
func runDiff(ctx context.Context, dockerCli command.Cli, opts *diffOptions) error {
|
||||
if opts.container == "" {
|
||||
return errors.New("Container name cannot be empty")
|
||||
}
|
||||
changes, err := dockerCli.Client().ContainerDiff(ctx, opts.container)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
diffCtx := formatter.Context{
|
||||
Output: dockerCLI.Out(),
|
||||
Format: newDiffFormat("{{.Type}} {{.Path}}"),
|
||||
Output: dockerCli.Out(),
|
||||
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/config/configfile"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
|
@ -52,8 +53,8 @@ func NewExecCommand(dockerCli command.Cli) *cobra.Command {
|
|||
options.Command = args[1:]
|
||||
return RunExec(cmd.Context(), dockerCli, containerIDorName, options)
|
||||
},
|
||||
ValidArgsFunction: completion.ContainerNames(dockerCli, false, func(ctr container.Summary) bool {
|
||||
return ctr.State != container.StatePaused
|
||||
ValidArgsFunction: completion.ContainerNames(dockerCli, false, func(ctr types.Container) bool {
|
||||
return ctr.State != "paused"
|
||||
}),
|
||||
Annotations: map[string]string{
|
||||
"category-top": "2",
|
||||
|
@ -84,13 +85,13 @@ func NewExecCommand(dockerCli command.Cli) *cobra.Command {
|
|||
}
|
||||
|
||||
// RunExec executes an `exec` command
|
||||
func RunExec(ctx context.Context, dockerCLI command.Cli, containerIDorName string, options ExecOptions) error {
|
||||
execOptions, err := parseExec(options, dockerCLI.ConfigFile())
|
||||
func RunExec(ctx context.Context, dockerCli command.Cli, containerIDorName string, options ExecOptions) error {
|
||||
execOptions, err := parseExec(options, dockerCli.ConfigFile())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
apiClient := dockerCLI.Client()
|
||||
apiClient := dockerCli.Client()
|
||||
|
||||
// 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
|
||||
|
@ -99,13 +100,13 @@ func RunExec(ctx context.Context, dockerCLI command.Cli, containerIDorName strin
|
|||
if _, err := apiClient.ContainerInspect(ctx, containerIDorName); err != nil {
|
||||
return err
|
||||
}
|
||||
if !options.Detach {
|
||||
if err := dockerCLI.In().CheckTty(execOptions.AttachStdin, execOptions.Tty); err != nil {
|
||||
if !execOptions.Detach {
|
||||
if err := dockerCli.In().CheckTty(execOptions.AttachStdin, execOptions.Tty); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
fillConsoleSize(execOptions, dockerCLI)
|
||||
fillConsoleSize(execOptions, dockerCli)
|
||||
|
||||
response, err := apiClient.ContainerExecCreate(ctx, containerIDorName, *execOptions)
|
||||
if err != nil {
|
||||
|
@ -117,14 +118,14 @@ func RunExec(ctx context.Context, dockerCLI command.Cli, containerIDorName strin
|
|||
return errors.New("exec ID empty")
|
||||
}
|
||||
|
||||
if options.Detach {
|
||||
if execOptions.Detach {
|
||||
return apiClient.ContainerExecStart(ctx, execID, container.ExecStartOptions{
|
||||
Detach: options.Detach,
|
||||
Detach: execOptions.Detach,
|
||||
Tty: execOptions.Tty,
|
||||
ConsoleSize: execOptions.ConsoleSize,
|
||||
})
|
||||
}
|
||||
return interactiveExec(ctx, dockerCLI, execOptions, execID)
|
||||
return interactiveExec(ctx, dockerCli, execOptions, execID)
|
||||
}
|
||||
|
||||
func fillConsoleSize(execOptions *container.ExecOptions, dockerCli command.Cli) {
|
||||
|
@ -223,12 +224,13 @@ func parseExec(execOpts ExecOptions, configFile *configfile.ConfigFile) (*contai
|
|||
Privileged: execOpts.Privileged,
|
||||
Tty: execOpts.TTY,
|
||||
Cmd: execOpts.Command,
|
||||
Detach: execOpts.Detach,
|
||||
WorkingDir: execOpts.Workdir,
|
||||
}
|
||||
|
||||
// collect all the environment variables for the container
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -2,17 +2,17 @@ package container
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
"github.com/docker/cli/internal/test"
|
||||
"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"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
"gotest.tools/v3/fs"
|
||||
|
@ -76,7 +76,8 @@ TWO=2
|
|||
{
|
||||
options: withDefaultOpts(ExecOptions{Detach: true}),
|
||||
expected: container.ExecOptions{
|
||||
Cmd: []string{"command"},
|
||||
Detach: true,
|
||||
Cmd: []string{"command"},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -86,8 +87,9 @@ TWO=2
|
|||
Detach: true,
|
||||
}),
|
||||
expected: container.ExecOptions{
|
||||
Tty: true,
|
||||
Cmd: []string{"command"},
|
||||
Detach: true,
|
||||
Tty: true,
|
||||
Cmd: []string{"command"},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -96,6 +98,7 @@ TWO=2
|
|||
expected: container.ExecOptions{
|
||||
Cmd: []string{"command"},
|
||||
DetachKeys: "de",
|
||||
Detach: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -107,6 +110,7 @@ TWO=2
|
|||
expected: container.ExecOptions{
|
||||
Cmd: []string{"command"},
|
||||
DetachKeys: "ab",
|
||||
Detach: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -138,12 +142,10 @@ TWO=2
|
|||
},
|
||||
}
|
||||
|
||||
for i, testcase := range testcases {
|
||||
t.Run("test "+strconv.Itoa(i+1), func(t *testing.T) {
|
||||
execConfig, err := parseExec(testcase.options, &testcase.configFile)
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, is.DeepEqual(testcase.expected, *execConfig))
|
||||
})
|
||||
for _, testcase := range testcases {
|
||||
execConfig, err := parseExec(testcase.options, &testcase.configFile)
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, is.DeepEqual(testcase.expected, *execConfig))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -176,8 +178,8 @@ func TestRunExec(t *testing.T) {
|
|||
doc: "inspect error",
|
||||
options: NewExecOptions(),
|
||||
client: &fakeClient{
|
||||
inspectFunc: func(string) (container.InspectResponse, error) {
|
||||
return container.InspectResponse{}, errors.New("failed inspect")
|
||||
inspectFunc: func(string) (types.ContainerJSON, error) {
|
||||
return types.ContainerJSON{}, errors.New("failed inspect")
|
||||
},
|
||||
},
|
||||
expectedError: "failed inspect",
|
||||
|
@ -206,8 +208,8 @@ func TestRunExec(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func execCreateWithID(_ string, _ container.ExecOptions) (container.ExecCreateResponse, error) {
|
||||
return container.ExecCreateResponse{ID: "execid"}, nil
|
||||
func execCreateWithID(_ string, _ container.ExecOptions) (types.IDResponse, error) {
|
||||
return types.IDResponse{ID: "execid"}, nil
|
||||
}
|
||||
|
||||
func TestGetExecExitStatus(t *testing.T) {
|
||||
|
@ -250,14 +252,14 @@ func TestNewExecCommandErrors(t *testing.T) {
|
|||
name string
|
||||
args []string
|
||||
expectedError string
|
||||
containerInspectFunc func(img string) (container.InspectResponse, error)
|
||||
containerInspectFunc func(img string) (types.ContainerJSON, error)
|
||||
}{
|
||||
{
|
||||
name: "client-error",
|
||||
args: []string{"5cb5bb5e4a3b", "-t", "-i", "bash"},
|
||||
expectedError: "something went wrong",
|
||||
containerInspectFunc: func(containerID string) (container.InspectResponse, error) {
|
||||
return container.InspectResponse{}, errors.New("something went wrong")
|
||||
containerInspectFunc: func(containerID string) (types.ContainerJSON, error) {
|
||||
return types.ContainerJSON{}, errors.Errorf("something went wrong")
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -7,7 +7,6 @@ import (
|
|||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/moby/sys/atomicwriter"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
@ -42,28 +41,27 @@ func NewExportCommand(dockerCli command.Cli) *cobra.Command {
|
|||
return cmd
|
||||
}
|
||||
|
||||
func runExport(ctx context.Context, dockerCLI command.Cli, opts exportOptions) error {
|
||||
var output io.Writer
|
||||
if opts.output == "" {
|
||||
if dockerCLI.Out().IsTerminal() {
|
||||
return errors.New("cowardly refusing to save to a terminal. Use the -o flag or redirect")
|
||||
}
|
||||
output = dockerCLI.Out()
|
||||
} else {
|
||||
writer, err := atomicwriter.New(opts.output, 0o600)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to export container")
|
||||
}
|
||||
defer writer.Close()
|
||||
output = writer
|
||||
func runExport(ctx context.Context, dockerCli command.Cli, opts exportOptions) error {
|
||||
if opts.output == "" && dockerCli.Out().IsTerminal() {
|
||||
return errors.New("cowardly refusing to save to a terminal. Use the -o flag or redirect")
|
||||
}
|
||||
|
||||
responseBody, err := dockerCLI.Client().ContainerExport(ctx, opts.container)
|
||||
if err := command.ValidateOutputPath(opts.output); err != nil {
|
||||
return errors.Wrap(err, "failed to export container")
|
||||
}
|
||||
|
||||
clnt := dockerCli.Client()
|
||||
|
||||
responseBody, err := clnt.ContainerExport(ctx, opts.container)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer responseBody.Close()
|
||||
|
||||
_, err = io.Copy(output, responseBody)
|
||||
return err
|
||||
if opts.output == "" {
|
||||
_, err := io.Copy(dockerCli.Out(), responseBody)
|
||||
return err
|
||||
}
|
||||
|
||||
return command.CopyToFile(opts.output, responseBody)
|
||||
}
|
||||
|
|
|
@ -42,6 +42,8 @@ func TestContainerExportOutputToIrregularFile(t *testing.T) {
|
|||
cmd.SetErr(io.Discard)
|
||||
cmd.SetArgs([]string{"-o", "/dev/random", "container"})
|
||||
|
||||
const expected = `failed to export container: cannot write to a character device file`
|
||||
assert.Error(t, cmd.Execute(), expected)
|
||||
err := cmd.Execute()
|
||||
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 (
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -13,14 +13,7 @@ const (
|
|||
)
|
||||
|
||||
// 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 {
|
||||
return newDiffFormat(source)
|
||||
}
|
||||
|
||||
// newDiffFormat returns a format for use with a diff [formatter.Context].
|
||||
func newDiffFormat(source string) formatter.Format {
|
||||
if source == formatter.TableFormatKey {
|
||||
return defaultDiffTableFormat
|
||||
}
|
||||
|
@ -28,22 +21,16 @@ func newDiffFormat(source string) formatter.Format {
|
|||
}
|
||||
|
||||
// DiffFormatWrite writes formatted diff using the Context
|
||||
//
|
||||
// Deprecated: this function was only used internally and will be removed in the next release.
|
||||
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 {
|
||||
func DiffFormatWrite(ctx formatter.Context, changes []container.FilesystemChange) error {
|
||||
render := func(format func(subContext formatter.SubContext) error) error {
|
||||
for _, change := range changes {
|
||||
if err := format(&diffContext{c: change}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
return ctx.Write(newDiffContext(), render)
|
||||
}
|
||||
|
||||
type diffContext struct {
|
||||
|
@ -52,14 +39,12 @@ type diffContext struct {
|
|||
}
|
||||
|
||||
func newDiffContext() *diffContext {
|
||||
return &diffContext{
|
||||
HeaderContext: formatter.HeaderContext{
|
||||
Header: formatter.SubHeaderContext{
|
||||
"Type": changeTypeHeader,
|
||||
"Path": pathHeader,
|
||||
},
|
||||
},
|
||||
diffCtx := diffContext{}
|
||||
diffCtx.Header = formatter.SubHeaderContext{
|
||||
"Type": changeTypeHeader,
|
||||
"Path": pathHeader,
|
||||
}
|
||||
return &diffCtx
|
||||
}
|
||||
|
||||
func (d *diffContext) MarshalJSON() ([]byte, error) {
|
||||
|
|
|
@ -5,7 +5,7 @@ import (
|
|||
"testing"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
|
@ -16,7 +16,7 @@ func TestDiffContextFormatWrite(t *testing.T) {
|
|||
expected string
|
||||
}{
|
||||
{
|
||||
formatter.Context{Format: newDiffFormat("table")},
|
||||
formatter.Context{Format: NewDiffFormat("table")},
|
||||
`CHANGE TYPE PATH
|
||||
C /var/log/app.log
|
||||
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
|
||||
/var/log/app.log
|
||||
/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
|
||||
A: /usr/app/app.js
|
||||
D: /usr/app/old_app.js
|
||||
|
@ -47,10 +47,11 @@ D: /usr/app/old_app.js
|
|||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(string(tc.context.Format), func(t *testing.T) {
|
||||
out := bytes.NewBufferString("")
|
||||
tc.context.Output = out
|
||||
err := diffFormatWrite(tc.context, diffs)
|
||||
err := DiffFormatWrite(tc.context, diffs)
|
||||
if err != nil {
|
||||
assert.Error(t, err, tc.expected)
|
||||
} else {
|
||||
|
|
|
@ -5,7 +5,8 @@ import (
|
|||
"sync"
|
||||
|
||||
"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 (
|
||||
|
@ -21,8 +22,6 @@ const (
|
|||
winMemUseHeader = "PRIV WORKING SET" // Used only on Windows
|
||||
memUseHeader = "MEM USAGE / LIMIT" // Used only on Linux
|
||||
pidsHeader = "PIDS" // Used only on Linux
|
||||
|
||||
noValue = "--"
|
||||
)
|
||||
|
||||
// StatsEntry represents the statistics data collected from a container
|
||||
|
@ -170,19 +169,19 @@ func (c *statsContext) Name() string {
|
|||
if len(c.s.Name) > 1 {
|
||||
return c.s.Name[1:]
|
||||
}
|
||||
return noValue
|
||||
return "--"
|
||||
}
|
||||
|
||||
func (c *statsContext) ID() string {
|
||||
if c.trunc {
|
||||
return formatter.TruncateID(c.s.ID)
|
||||
return stringid.TruncateID(c.s.ID)
|
||||
}
|
||||
return c.s.ID
|
||||
}
|
||||
|
||||
func (c *statsContext) CPUPerc() string {
|
||||
if c.s.IsInvalid {
|
||||
return noValue
|
||||
return "--"
|
||||
}
|
||||
return formatPercentage(c.s.CPUPercentage)
|
||||
}
|
||||
|
@ -199,28 +198,28 @@ func (c *statsContext) MemUsage() string {
|
|||
|
||||
func (c *statsContext) MemPerc() string {
|
||||
if c.s.IsInvalid || c.os == winOSType {
|
||||
return noValue
|
||||
return "--"
|
||||
}
|
||||
return formatPercentage(c.s.MemoryPercentage)
|
||||
}
|
||||
|
||||
func (c *statsContext) NetIO() string {
|
||||
if c.s.IsInvalid {
|
||||
return noValue
|
||||
return "--"
|
||||
}
|
||||
return units.HumanSizeWithPrecision(c.s.NetworkRx, 3) + " / " + units.HumanSizeWithPrecision(c.s.NetworkTx, 3)
|
||||
}
|
||||
|
||||
func (c *statsContext) BlockIO() string {
|
||||
if c.s.IsInvalid {
|
||||
return noValue
|
||||
return "--"
|
||||
}
|
||||
return units.HumanSizeWithPrecision(c.s.BlockRead, 3) + " / " + units.HumanSizeWithPrecision(c.s.BlockWrite, 3)
|
||||
}
|
||||
|
||||
func (c *statsContext) PIDs() string {
|
||||
if c.s.IsInvalid || c.os == winOSType {
|
||||
return noValue
|
||||
return "--"
|
||||
}
|
||||
return strconv.FormatUint(c.s.PidsCurrent, 10)
|
||||
}
|
||||
|
|
|
@ -5,13 +5,13 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
|
||||
func TestContainerStatsContext(t *testing.T) {
|
||||
containerID := test.RandomID()
|
||||
containerID := stringid.GenerateRandomID()
|
||||
|
||||
var ctx statsContext
|
||||
tt := []struct {
|
||||
|
@ -148,7 +148,7 @@ container2 -- --
|
|||
`,
|
||||
},
|
||||
}
|
||||
entries := []StatsEntry{
|
||||
stats := []StatsEntry{
|
||||
{
|
||||
Container: "container1",
|
||||
CPUPercentage: 20,
|
||||
|
@ -178,10 +178,11 @@ container2 -- --
|
|||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(string(tc.context.Format), func(t *testing.T) {
|
||||
var out bytes.Buffer
|
||||
tc.context.Output = &out
|
||||
err := statsFormatWrite(tc.context, entries, "windows", false)
|
||||
err := statsFormatWrite(tc.context, stats, "windows", false)
|
||||
if err != nil {
|
||||
assert.Error(t, err, tc.expected)
|
||||
} else {
|
||||
|
@ -222,6 +223,7 @@ func TestContainerStatsContextWriteWithNoStats(t *testing.T) {
|
|||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(string(tc.context.Format), func(t *testing.T) {
|
||||
err := statsFormatWrite(tc.context, []StatsEntry{}, "linux", false)
|
||||
assert.NilError(t, err)
|
||||
|
@ -263,6 +265,7 @@ func TestContainerStatsContextWriteWithNoStatsWindows(t *testing.T) {
|
|||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(string(tc.context.Format), func(t *testing.T) {
|
||||
err := statsFormatWrite(tc.context, []StatsEntry{}, "windows", false)
|
||||
assert.NilError(t, err)
|
||||
|
@ -273,46 +276,45 @@ func TestContainerStatsContextWriteWithNoStatsWindows(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestContainerStatsContextWriteTrunc(t *testing.T) {
|
||||
tests := []struct {
|
||||
doc string
|
||||
var out bytes.Buffer
|
||||
|
||||
contexts := []struct {
|
||||
context formatter.Context
|
||||
trunc bool
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
doc: "non-truncated",
|
||||
context: formatter.Context{
|
||||
formatter.Context{
|
||||
Format: "{{.ID}}",
|
||||
Output: &out,
|
||||
},
|
||||
expected: "b95a83497c9161c9b444e3d70e1a9dfba0c1840d41720e146a95a08ebf938afc\n",
|
||||
false,
|
||||
"b95a83497c9161c9b444e3d70e1a9dfba0c1840d41720e146a95a08ebf938afc\n",
|
||||
},
|
||||
{
|
||||
doc: "truncated",
|
||||
context: formatter.Context{
|
||||
formatter.Context{
|
||||
Format: "{{.ID}}",
|
||||
Output: &out,
|
||||
},
|
||||
trunc: true,
|
||||
expected: "b95a83497c91\n",
|
||||
true,
|
||||
"b95a83497c91\n",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.doc, func(t *testing.T) {
|
||||
var out bytes.Buffer
|
||||
tc.context.Output = &out
|
||||
err := statsFormatWrite(tc.context, []StatsEntry{{ID: "b95a83497c9161c9b444e3d70e1a9dfba0c1840d41720e146a95a08ebf938afc"}}, "linux", tc.trunc)
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, is.Equal(tc.expected, out.String()))
|
||||
})
|
||||
for _, context := range contexts {
|
||||
statsFormatWrite(context.context, []StatsEntry{{ID: "b95a83497c9161c9b444e3d70e1a9dfba0c1840d41720e146a95a08ebf938afc"}}, "linux", context.trunc)
|
||||
assert.Check(t, is.Equal(context.expected, out.String()))
|
||||
// Clean buffer
|
||||
out.Reset()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkStatsFormat(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
entries := genStats()
|
||||
stats := genStats()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
for _, s := range entries {
|
||||
for _, s := range stats {
|
||||
_ = s.CPUPerc()
|
||||
_ = s.MemUsage()
|
||||
_ = s.MemPerc()
|
||||
|
@ -335,9 +337,9 @@ func genStats() []statsContext {
|
|||
NetworkTx: 987.654321,
|
||||
PidsCurrent: 123456789,
|
||||
}}
|
||||
entries := make([]statsContext, 0, 100)
|
||||
for range 100 {
|
||||
entries = append(entries, entry)
|
||||
stats := make([]statsContext, 100)
|
||||
for i := 0; i < 100; i++ {
|
||||
stats = append(stats, entry)
|
||||
}
|
||||
return entries
|
||||
return stats
|
||||
}
|
||||
|
|
|
@ -8,8 +8,9 @@ import (
|
|||
"sync"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/moby/moby/api/stdcopy"
|
||||
"github.com/moby/moby/api/types"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/pkg/ioutils"
|
||||
"github.com/docker/docker/pkg/stdcopy"
|
||||
"github.com/moby/term"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
@ -18,18 +19,6 @@ import (
|
|||
// TODO: This could be moved to `pkg/term`.
|
||||
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
|
||||
// connection.
|
||||
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
|
||||
// only restore the terminal once.
|
||||
restore = sync.OnceFunc(func() {
|
||||
_ = restoreTerminal(h.streams, h.inputStream)
|
||||
})
|
||||
var restoreOnce sync.Once
|
||||
restore = func() {
|
||||
restoreOnce.Do(func() {
|
||||
_ = restoreTerminal(h.streams, h.inputStream)
|
||||
})
|
||||
}
|
||||
|
||||
// Wrap the input to detect detach escape sequence.
|
||||
// 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{
|
||||
Reader: term.NewEscapeProxy(h.inputStream, escapeKeys),
|
||||
closer: h.inputStream.Close,
|
||||
}
|
||||
h.inputStream = ioutils.NewReadCloserWrapper(term.NewEscapeProxy(h.inputStream, escapeKeys), h.inputStream.Close)
|
||||
|
||||
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:
|
||||
//go:build go1.23
|
||||
//go:build go1.21
|
||||
|
||||
package container
|
||||
|
||||
|
@ -42,9 +42,11 @@ func newInspectCommand(dockerCli command.Cli) *cobra.Command {
|
|||
return cmd
|
||||
}
|
||||
|
||||
func runInspect(ctx context.Context, dockerCLI command.Cli, opts inspectOptions) error {
|
||||
apiClient := dockerCLI.Client()
|
||||
return inspect.Inspect(dockerCLI.Out(), opts.refs, opts.format, func(ref string) (any, []byte, error) {
|
||||
return apiClient.ContainerInspectWithRaw(ctx, ref, opts.size)
|
||||
})
|
||||
func runInspect(ctx context.Context, dockerCli command.Cli, opts inspectOptions) error {
|
||||
client := dockerCli.Client()
|
||||
|
||||
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 (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
@ -43,19 +44,20 @@ func NewKillCommand(dockerCli command.Cli) *cobra.Command {
|
|||
return cmd
|
||||
}
|
||||
|
||||
func runKill(ctx context.Context, dockerCLI command.Cli, opts *killOptions) error {
|
||||
apiClient := dockerCLI.Client()
|
||||
func runKill(ctx context.Context, dockerCli command.Cli, opts *killOptions) error {
|
||||
var errs []string
|
||||
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 {
|
||||
if err := <-errChan; err != nil {
|
||||
errs = append(errs, err)
|
||||
continue
|
||||
errs = append(errs, err.Error())
|
||||
} 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"
|
||||
"github.com/docker/cli/opts"
|
||||
"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/spf13/cobra"
|
||||
)
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue