Compare commits
49 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5650f9b102 | |||
| e8bc27d4ca | |||
| 2acb9c2e8f | |||
| 230d4d0bab | |||
| 6d47c06b0d | |||
| a6d757c0e3 | |||
| c463d968fc | |||
| a61a0c3ac9 | |||
| 0576b3dad7 | |||
| 60b13f1f80 | |||
| 5dab29765f | |||
| b600054c51 | |||
| 42912d4d68 | |||
| 28638aab95 | |||
| e89982aea9 | |||
| dafe63b085 | |||
| 7dfc99c17e | |||
| 079107e1bc | |||
| 14d7485d98 | |||
| 50bb59277a | |||
| c4767b3141 | |||
| 3f807ede91 | |||
| 836fc57e2a | |||
| 258e27438f | |||
| 29cd8591ee | |||
| e23a63eb4c | |||
| 1e52a86d42 | |||
| e382a5f225 | |||
| b72abbb6f0 | |||
| 26e9ff49c8 | |||
| 4021ee92fe | |||
| 326c7138bb | |||
| ee359a394b | |||
| 004e2925d7 | |||
| 6691085012 | |||
| 60f2d38d53 | |||
| 81b482ea5e | |||
| 211e74b240 | |||
| 8beff78d85 | |||
| e64914c890 | |||
| c1d70d1fbb | |||
| 53a3f0be18 | |||
| 4add46d686 | |||
| ccea7d8a30 | |||
| 4cf5afaefa | |||
| 6c2b06d535 | |||
| 1c6a8ecf2e | |||
| 6d1c387af2 | |||
| 1e6db5d24b |
11
.gitattributes
vendored
11
.gitattributes
vendored
@ -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
|
||||
|
||||
7
.github/CODEOWNERS
vendored
7
.github/CODEOWNERS
vendored
@ -1,6 +1,7 @@
|
||||
# GitHub code owners
|
||||
# See https://github.com/blog/2392-introducing-code-owners
|
||||
|
||||
cli/command/stack/** @silvin-lubecki @docker/runtime-owners
|
||||
contrib/completion/bash/** @albers @docker/runtime-owners
|
||||
docs/** @thaJeztah @docker/runtime-owners
|
||||
cli/command/stack/** @silvin-lubecki
|
||||
contrib/completion/bash/** @albers
|
||||
contrib/completion/zsh/** @sdurrheimer
|
||||
docs/** @thaJeztah
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
4
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@ -8,12 +8,12 @@ body:
|
||||
attributes:
|
||||
value: |
|
||||
Thank you for taking the time to report a bug!
|
||||
If this is a security issue report it to the [Docker Security team](mailto:security@docker.com).
|
||||
If this is a security issue please report it to the [Docker Security team](mailto:security@docker.com).
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Description
|
||||
description: Give a clear and concise description of the bug
|
||||
description: Please give a clear and concise description of the bug
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@ -4,7 +4,7 @@ contact_links:
|
||||
about: "Read guidelines and tips about contributing to Docker."
|
||||
url: "https://github.com/docker/cli/blob/master/CONTRIBUTING.md"
|
||||
- name: "Security and Vulnerabilities"
|
||||
about: "Report any security issues or vulnerabilities responsibly to the Docker security team. Do not use the public issue tracker."
|
||||
about: "Please report any security issues or vulnerabilities responsibly to the Docker security team. Please do not use the public issue tracker."
|
||||
url: "https://github.com/moby/moby/security/policy"
|
||||
- name: "General Support"
|
||||
about: "Get the help you need to build, share, and run your Docker applications"
|
||||
|
||||
11
.github/PULL_REQUEST_TEMPLATE.md
vendored
11
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -1,5 +1,5 @@
|
||||
<!--
|
||||
Make sure you've read and understood our contributing guidelines;
|
||||
Please make sure you've read and understood our contributing guidelines;
|
||||
https://github.com/docker/cli/blob/master/CONTRIBUTING.md
|
||||
|
||||
** Make sure all your commits include a signature generated with `git commit -s` **
|
||||
@ -10,7 +10,7 @@ guide https://docs.docker.com/opensource/code/
|
||||
If this is a bug fix, make sure your description includes "fixes #xxxx", or
|
||||
"closes #xxxx"
|
||||
|
||||
Provide the following information:
|
||||
Please provide the following information:
|
||||
-->
|
||||
|
||||
**- What I did**
|
||||
@ -19,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
|
||||
|
||||
|
||||
51
.github/workflows/build.yml
vendored
51
.github/workflows/build.yml
vendored
@ -1,14 +1,5 @@
|
||||
name: build
|
||||
|
||||
# Default to 'contents: read', which grants actions to read commits.
|
||||
#
|
||||
# If any permission is set, any permission not included in the list is
|
||||
# implicitly set to "none".
|
||||
#
|
||||
# see https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
@ -22,14 +13,13 @@ on:
|
||||
branches:
|
||||
- 'master'
|
||||
- '[0-9]+.[0-9]+'
|
||||
- '[0-9]+.x'
|
||||
tags:
|
||||
- 'v*'
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-22.04
|
||||
outputs:
|
||||
matrix: ${{ steps.platforms.outputs.matrix }}
|
||||
steps:
|
||||
@ -47,7 +37,7 @@ jobs:
|
||||
echo ${{ steps.platforms.outputs.matrix }}
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- prepare
|
||||
strategy:
|
||||
@ -61,12 +51,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@v4
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
set: |
|
||||
@ -95,16 +90,12 @@ jobs:
|
||||
if-no-files-found: error
|
||||
|
||||
bin-image:
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-22.04
|
||||
if: ${{ github.event_name != 'pull_request' && github.repository == 'docker/cli' }}
|
||||
steps:
|
||||
-
|
||||
name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_CLIBIN_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_CLIBIN_TOKEN }}
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
@ -122,13 +113,20 @@ jobs:
|
||||
type=ref,event=branch
|
||||
type=ref,event=pr
|
||||
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@v4
|
||||
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: |
|
||||
@ -136,7 +134,7 @@ jobs:
|
||||
*.cache-to=type=gha,scope=bin-image,mode=max
|
||||
|
||||
prepare-plugins:
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-22.04
|
||||
outputs:
|
||||
matrix: ${{ steps.platforms.outputs.matrix }}
|
||||
steps:
|
||||
@ -154,7 +152,7 @@ jobs:
|
||||
echo ${{ steps.platforms.outputs.matrix }}
|
||||
|
||||
plugins:
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- prepare-plugins
|
||||
strategy:
|
||||
@ -162,12 +160,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@v4
|
||||
with:
|
||||
targets: plugins-cross
|
||||
set: |
|
||||
|
||||
53
.github/workflows/codeql.yml
vendored
53
.github/workflows/codeql.yml
vendored
@ -1,25 +1,15 @@
|
||||
name: codeql
|
||||
|
||||
# Default to 'contents: read', which grants actions to read commits.
|
||||
#
|
||||
# If any permission is set, any permission not included in the list is
|
||||
# implicitly set to "none".
|
||||
#
|
||||
# see https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
- '[0-9]+.[0-9]+'
|
||||
- '[0-9]+.x'
|
||||
tags:
|
||||
- 'v*'
|
||||
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 +24,36 @@ 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
|
||||
-
|
||||
name: Update Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.21'
|
||||
-
|
||||
name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: go
|
||||
# CodeQL 2.16.4's auto-build added support for multi-module repositories,
|
||||
# and is trying to be smart by searching for modules in every directory,
|
||||
# including vendor directories. If no module is found, it's creating one
|
||||
@ -59,16 +64,6 @@ jobs:
|
||||
run: |
|
||||
ln -s vendor.mod go.mod
|
||||
ln -s vendor.sum go.sum
|
||||
-
|
||||
name: Update Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.23.6"
|
||||
-
|
||||
name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: go
|
||||
-
|
||||
name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
|
||||
31
.github/workflows/e2e.yml
vendored
31
.github/workflows/e2e.yml
vendored
@ -1,14 +1,5 @@
|
||||
name: e2e
|
||||
|
||||
# Default to 'contents: read', which grants actions to read commits.
|
||||
#
|
||||
# If any permission is set, any permission not included in the list is
|
||||
# implicitly set to "none".
|
||||
#
|
||||
# see https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
@ -19,14 +10,13 @@ on:
|
||||
branches:
|
||||
- 'master'
|
||||
- '[0-9]+.[0-9]+'
|
||||
- '[0-9]+.x'
|
||||
tags:
|
||||
- 'v*'
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
e2e:
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@ -38,8 +28,8 @@ jobs:
|
||||
- alpine
|
||||
- debian
|
||||
engine-version:
|
||||
- 27.0 # latest
|
||||
- 26.1 # latest - 1
|
||||
- 25.0 # latest
|
||||
- 24.0 # 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
|
||||
@ -50,15 +40,8 @@ jobs:
|
||||
-
|
||||
name: Update daemon.json
|
||||
run: |
|
||||
if [ ! -f /etc/docker/daemon.json ]; then
|
||||
# ubuntu 24.04 runners no longer have a default daemon.json present
|
||||
sudo mkdir -p /etc/docker/
|
||||
echo '{"experimental": true}' | sudo tee /etc/docker/daemon.json
|
||||
else
|
||||
# but if there is one; let's patch it to keep other options that may be set.
|
||||
sudo jq '.experimental = true' < /etc/docker/daemon.json > /tmp/docker.json
|
||||
sudo mv /tmp/docker.json /etc/docker/daemon.json
|
||||
fi
|
||||
sudo jq '.experimental = true' < /etc/docker/daemon.json > /tmp/docker.json
|
||||
sudo mv /tmp/docker.json /etc/docker/daemon.json
|
||||
sudo cat /etc/docker/daemon.json
|
||||
sudo service docker restart
|
||||
docker version
|
||||
@ -76,7 +59,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 }}
|
||||
|
||||
36
.github/workflows/test.yml
vendored
36
.github/workflows/test.yml
vendored
@ -1,14 +1,5 @@
|
||||
name: test
|
||||
|
||||
# Default to 'contents: read', which grants actions to read commits.
|
||||
#
|
||||
# If any permission is set, any permission not included in the list is
|
||||
# implicitly set to "none".
|
||||
#
|
||||
# see https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
@ -19,28 +10,30 @@ on:
|
||||
branches:
|
||||
- 'master'
|
||||
- '[0-9]+.[0-9]+'
|
||||
- '[0-9]+.x'
|
||||
tags:
|
||||
- 'v*'
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
ctn:
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-22.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@v4
|
||||
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:
|
||||
@ -53,10 +46,15 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os:
|
||||
- macos-13 # macOS 13 on Intel
|
||||
- macos-14 # macOS 14 on arm64 (Apple Silicon M1)
|
||||
- macos-12
|
||||
# - 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 +64,7 @@ jobs:
|
||||
name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.23.6"
|
||||
go-version: 1.21.11
|
||||
-
|
||||
name: Test
|
||||
run: |
|
||||
@ -76,8 +74,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 }}
|
||||
|
||||
54
.github/workflows/validate-pr.yml
vendored
54
.github/workflows/validate-pr.yml
vendored
@ -1,14 +1,5 @@
|
||||
name: validate-pr
|
||||
|
||||
# Default to 'contents: read', which grants actions to read commits.
|
||||
#
|
||||
# If any permission is set, any permission not included in the list is
|
||||
# implicitly set to "none".
|
||||
#
|
||||
# see https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, edited, labeled, unlabeled]
|
||||
@ -16,7 +7,6 @@ on:
|
||||
jobs:
|
||||
check-area-label:
|
||||
runs-on: ubuntu-20.04
|
||||
timeout-minutes: 120 # guardrails timeout for the whole job
|
||||
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 +17,9 @@ jobs:
|
||||
run: exit 0
|
||||
|
||||
check-changelog:
|
||||
if: contains(join(github.event.pull_request.labels.*.name, ','), 'impact/')
|
||||
runs-on: ubuntu-20.04
|
||||
timeout-minutes: 120 # guardrails timeout for the whole job
|
||||
env:
|
||||
HAS_IMPACT_LABEL: ${{ contains(join(github.event.pull_request.labels.*.name, ','), 'impact/') }}
|
||||
PR_BODY: |
|
||||
${{ github.event.pull_request.body }}
|
||||
steps:
|
||||
@ -42,23 +31,15 @@ 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. Please provide a description for the changelog."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
len=$(echo -n "$desc" | wc -c)
|
||||
if [[ $len -le 6 ]]; then
|
||||
echo "::error::Description looks too short: $desc"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
if [ -n "$desc" ]; then
|
||||
echo "::error::PR has a changelog description, but no changelog label"
|
||||
echo "::error::Please add the relevant 'impact/' label to the PR or remove the changelog description"
|
||||
exit 1
|
||||
fi
|
||||
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:"
|
||||
@ -66,23 +47,16 @@ jobs:
|
||||
|
||||
check-pr-branch:
|
||||
runs-on: ubuntu-20.04
|
||||
timeout-minutes: 120 # guardrails timeout for the whole job
|
||||
env:
|
||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||
steps:
|
||||
# Backports or PR that target a release branch directly should mention the target branch in the title, for example:
|
||||
# [X.Y backport] Some change that needs backporting to X.Y
|
||||
# [X.Y] Change directly targeting the X.Y branch
|
||||
- name: Check release branch
|
||||
- name: Get branch from PR title
|
||||
id: title_branch
|
||||
run: |
|
||||
# get the intended major version prefix ("[27.1 backport]" -> "27.") from the PR title.
|
||||
[[ "$PR_TITLE" =~ ^\[([0-9]*\.)[^]]*\] ]] && branch="${BASH_REMATCH[1]}"
|
||||
run: echo "$PR_TITLE" | sed -n 's/^\[\([0-9]*\.[0-9]*\)[^]]*\].*/branch=\1/p' >> $GITHUB_OUTPUT
|
||||
|
||||
# get major version prefix from the release branch ("27.x -> "27.")
|
||||
[[ "$GITHUB_BASE_REF" =~ ^([0-9]*\.) ]] && target_branch="${BASH_REMATCH[1]}" || target_branch="$GITHUB_BASE_REF"
|
||||
|
||||
if [[ "$target_branch" != "$branch" ]] && ! [[ "$GITHUB_BASE_REF" == "master" && "$branch" == "" ]]; then
|
||||
echo "::error::PR is opened against the $GITHUB_BASE_REF branch, but its title suggests otherwise."
|
||||
exit 1
|
||||
fi
|
||||
- name: Check release branch
|
||||
if: github.event.pull_request.base.ref != steps.title_branch.outputs.branch && !(github.event.pull_request.base.ref == 'master' && steps.title_branch.outputs.branch == '')
|
||||
run: echo "::error::PR title suggests targetting the ${{ steps.title_branch.outputs.branch }} branch, but is opened against ${{ github.event.pull_request.base.ref }}" && exit 1
|
||||
|
||||
21
.github/workflows/validate.yml
vendored
21
.github/workflows/validate.yml
vendored
@ -1,14 +1,5 @@
|
||||
name: validate
|
||||
|
||||
# Default to 'contents: read', which grants actions to read commits.
|
||||
#
|
||||
# If any permission is set, any permission not included in the list is
|
||||
# implicitly set to "none".
|
||||
#
|
||||
# see https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
@ -19,14 +10,13 @@ on:
|
||||
branches:
|
||||
- 'master'
|
||||
- '[0-9]+.[0-9]+'
|
||||
- '[0-9]+.x'
|
||||
tags:
|
||||
- 'v*'
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@ -36,15 +26,18 @@ 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@v4
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
# check that the generated Markdown and the checked-in files match
|
||||
validate-md:
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
@ -64,7 +57,7 @@ jobs:
|
||||
fi
|
||||
|
||||
validate-make:
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,5 +1,5 @@
|
||||
# if you want to ignore files created by your editor/tools,
|
||||
# consider a global .gitignore https://help.github.com/articles/ignoring-files
|
||||
# please consider a global .gitignore https://help.github.com/articles/ignoring-files
|
||||
*.exe
|
||||
*.exe~
|
||||
*.orig
|
||||
|
||||
110
.golangci.yml
110
.golangci.yml
@ -1,12 +1,12 @@
|
||||
linters:
|
||||
enable:
|
||||
- bodyclose
|
||||
- copyloopvar # Detects places where loop variables are copied.
|
||||
- depguard
|
||||
- dogsled
|
||||
- dupword # Detects duplicate words.
|
||||
- durationcheck
|
||||
- errchkjson
|
||||
- exportloopref # Detects pointers to enclosing loop variables.
|
||||
- gocritic # Metalinter; detects bugs, performance, and styling issues.
|
||||
- gocyclo
|
||||
- gofumpt # Detects whether code was gofumpt-ed.
|
||||
@ -15,8 +15,10 @@ linters:
|
||||
- gosimple
|
||||
- govet
|
||||
- ineffassign
|
||||
- lll
|
||||
- megacheck
|
||||
- misspell # Detects commonly misspelled English words in comments.
|
||||
- nakedret # Detects uses of naked returns.
|
||||
- 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.
|
||||
@ -26,6 +28,7 @@ linters:
|
||||
- revive # Metalinter; drop-in replacement for golint.
|
||||
- staticcheck
|
||||
- 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
|
||||
@ -33,61 +36,44 @@ linters:
|
||||
- unparam
|
||||
- unused
|
||||
- usestdlibvars
|
||||
- usetesting # Reports uses of functions with replacement inside the testing package.
|
||||
- vet
|
||||
- wastedassign
|
||||
|
||||
disable:
|
||||
- errcheck
|
||||
|
||||
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.23.6"
|
||||
timeout: 5m
|
||||
skip-files:
|
||||
- cli/compose/schema/bindata.go
|
||||
- .*generated.*
|
||||
|
||||
linters-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: "io/ioutil"
|
||||
- pkg: io/ioutil
|
||||
desc: The io/ioutil package has been deprecated, see https://go.dev/doc/go1.16#ioutil
|
||||
gocyclo:
|
||||
min-complexity: 16
|
||||
gosec:
|
||||
excludes:
|
||||
- G104 # G104: Errors unhandled; (TODO: reduce unhandled errors, or explicitly ignore)
|
||||
- G113 # G113: Potential uncontrolled memory consumption in Rat.SetString (CVE-2022-23772); (only affects go < 1.16.14. and go < 1.17.7)
|
||||
- G115 # G115: integer overflow conversion; (TODO: verify these: https://github.com/docker/cli/issues/5584)
|
||||
- G306 # G306: Expect WriteFile permissions to be 0600 or less (too restrictive; also flags "0o644" permissions)
|
||||
- G307 # G307: Deferring unsafe method "*os.File" on type "Close" (also EXC0008); (TODO: evaluate these and fix where needed: G307: Deferring unsafe method "*os.File" on type "Close")
|
||||
govet:
|
||||
enable:
|
||||
- shadow
|
||||
check-shadowing: true
|
||||
settings:
|
||||
shadow:
|
||||
strict: true
|
||||
lll:
|
||||
line-length: 200
|
||||
nakedret:
|
||||
# Disallow naked returns if func has more lines of code than this setting.
|
||||
# Default: 30
|
||||
max-func-lines: 0
|
||||
command: nakedret
|
||||
pattern: ^(?P<path>.*?\\.go):(?P<line>\\d+)\\s*(?P<message>.*)$
|
||||
|
||||
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
|
||||
@ -96,47 +82,18 @@ linters-settings:
|
||||
- name: empty-lines
|
||||
severity: warning
|
||||
disabled: false
|
||||
# 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#line-length-limit
|
||||
- name: line-length-limit
|
||||
severity: warning
|
||||
disabled: false
|
||||
arguments: [200]
|
||||
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unused-receiver
|
||||
- name: unused-receiver
|
||||
severity: warning
|
||||
disabled: false
|
||||
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#use-any
|
||||
- name: use-any
|
||||
severity: warning
|
||||
disabled: false
|
||||
|
||||
usetesting:
|
||||
# FIXME(thaJeztah): Disable `os.Chdir()` detections; should be automatically disabled on Go < 1.24; see https://github.com/docker/cli/pull/5835#issuecomment-2665302478
|
||||
os-chdir: 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-background: 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
|
||||
context-todo: false
|
||||
|
||||
issues:
|
||||
# The default exclusion rules are a bit too permissive, so copying the relevant ones below
|
||||
exclude-use-default: false
|
||||
|
||||
# This option has been defined when Go modules was not existed and when the
|
||||
# golangci-lint core was different, this is not something we still recommend.
|
||||
exclude-dirs-use-default: false
|
||||
|
||||
exclude:
|
||||
- parameter .* always receives
|
||||
|
||||
exclude-files:
|
||||
- cli/compose/schema/bindata.go
|
||||
- .*generated.*
|
||||
|
||||
exclude-rules:
|
||||
# We prefer to use an "exclude-list" so that new "default" exclusions are not
|
||||
# automatically inherited. We can decide whether or not to follow upstream
|
||||
@ -147,9 +104,6 @@ issues:
|
||||
#
|
||||
# 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
|
||||
#
|
||||
# The default list of exclusions can be found at:
|
||||
# https://golangci-lint.run/usage/false-positives/#default-exclusions
|
||||
|
||||
# EXC0001
|
||||
- text: "Error return value of .((os\\.)?std(out|err)\\..*|.*Close|.*Flush|os\\.Remove(All)?|.*print(f|ln)?|os\\.(Un)?Setenv). is not checked"
|
||||
@ -167,6 +121,11 @@ issues:
|
||||
- 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:
|
||||
@ -176,11 +135,34 @@ issues:
|
||||
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
|
||||
|
||||
# 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:
|
||||
|
||||
10
.mailmap
10
.mailmap
@ -22,10 +22,7 @@ Akihiro Matsushima <amatsusbit@gmail.com> <amatsus@users.noreply.github.com>
|
||||
Akihiro Suda <akihiro.suda.cz@hco.ntt.co.jp>
|
||||
Akihiro Suda <akihiro.suda.cz@hco.ntt.co.jp> <suda.akihiro@lab.ntt.co.jp>
|
||||
Akihiro Suda <akihiro.suda.cz@hco.ntt.co.jp> <suda.kyoto@gmail.com>
|
||||
Alano Terblanche <alano.terblanche@docker.com>
|
||||
Alano Terblanche <alano.terblanche@docker.com> <18033717+Benehiko@users.noreply.github.com>
|
||||
Albin Kerouanton <albinker@gmail.com>
|
||||
Albin Kerouanton <albinker@gmail.com> <557933+akerouanton@users.noreply.github.com>
|
||||
Albin Kerouanton <albinker@gmail.com> <albin@akerouanton.name>
|
||||
Aleksa Sarai <asarai@suse.de>
|
||||
Aleksa Sarai <asarai@suse.de> <asarai@suse.com>
|
||||
@ -91,7 +88,6 @@ Brian Goff <cpuguy83@gmail.com>
|
||||
Brian Goff <cpuguy83@gmail.com> <bgoff@cpuguy83-mbp.home>
|
||||
Brian Goff <cpuguy83@gmail.com> <bgoff@cpuguy83-mbp.local>
|
||||
Brian Tracy <brian.tracy33@gmail.com>
|
||||
Calvin Liu <flycalvin@qq.com>
|
||||
Carlos de Paula <me@carlosedp.com>
|
||||
Chad Faragher <wyckster@hotmail.com>
|
||||
Chander Govindarajan <chandergovind@gmail.com>
|
||||
@ -258,7 +254,6 @@ Jessica Frazelle <jess@oxide.computer> <jessfraz@google.com>
|
||||
Jessica Frazelle <jess@oxide.computer> <jfrazelle@users.noreply.github.com>
|
||||
Jessica Frazelle <jess@oxide.computer> <me@jessfraz.com>
|
||||
Jessica Frazelle <jess@oxide.computer> <princess@docker.com>
|
||||
Jim Chen <njucjc@gmail.com>
|
||||
Jim Galasyn <jim.galasyn@docker.com>
|
||||
Jiuyue Ma <majiuyue@huawei.com>
|
||||
Joey Geiger <jgeiger@gmail.com>
|
||||
@ -274,8 +269,6 @@ John Howard <github@lowenna.com> <jhowardmsft@users.noreply.github.com>
|
||||
John Howard <github@lowenna.com> <John.Howard@microsoft.com>
|
||||
John Howard <github@lowenna.com> <john.howard@microsoft.com>
|
||||
John Stephens <johnstep@docker.com> <johnstep@users.noreply.github.com>
|
||||
Jonathan A. Sternberg <jonathansternberg@gmail.com>
|
||||
Jonathan A. Sternberg <jonathansternberg@gmail.com> <jonathan.sternberg@docker.com>
|
||||
Jordan Arentsen <blissdev@gmail.com>
|
||||
Jordan Jennings <jjn2009@gmail.com> <jjn2009@users.noreply.github.com>
|
||||
Jorit Kleine-Möllhoff <joppich@bricknet.de> <joppich@users.noreply.github.com>
|
||||
@ -451,7 +444,6 @@ Roch Feuillade <roch.feuillade@pandobac.com>
|
||||
Roch Feuillade <roch.feuillade@pandobac.com> <46478807+rochfeu@users.noreply.github.com>
|
||||
Roman Dudin <katrmr@gmail.com> <decadent@users.noreply.github.com>
|
||||
Ross Boucher <rboucher@gmail.com>
|
||||
Rui JingAn <quiterace@gmail.com>
|
||||
Runshen Zhu <runshen.zhu@gmail.com>
|
||||
Ryan Stelly <ryan.stelly@live.com>
|
||||
Sakeven Jiang <jc5930@sina.cn>
|
||||
@ -532,8 +524,6 @@ Tim Bart <tim@fewagainstmany.com>
|
||||
Tim Bosse <taim@bosboot.org> <maztaim@users.noreply.github.com>
|
||||
Tim Ruffles <oi@truffles.me.uk> <timruffles@googlemail.com>
|
||||
Tim Terhorst <mynamewastaken+git@gmail.com>
|
||||
Tim Welsh <timothy.welsh@docker.com>
|
||||
Tim Welsh <timothy.welsh@docker.com> <84401379+twelsh-aw@users.noreply.github.com>
|
||||
Tim Zju <21651152@zju.edu.cn>
|
||||
Timothy Hobbs <timothyhobbs@seznam.cz>
|
||||
Toli Kuznets <toli@docker.com>
|
||||
|
||||
20
AUTHORS
20
AUTHORS
@ -26,7 +26,6 @@ Akhil Mohan <akhil.mohan@mayadata.io>
|
||||
Akihiro Suda <akihiro.suda.cz@hco.ntt.co.jp>
|
||||
Akim Demaille <akim.demaille@docker.com>
|
||||
Alan Thompson <cloojure@gmail.com>
|
||||
Alano Terblanche <alano.terblanche@docker.com>
|
||||
Albert Callarisa <shark234@gmail.com>
|
||||
Alberto Roura <mail@albertoroura.com>
|
||||
Albin Kerouanton <albinker@gmail.com>
|
||||
@ -66,7 +65,6 @@ Andrew Hsu <andrewhsu@docker.com>
|
||||
Andrew Macpherson <hopscotch23@gmail.com>
|
||||
Andrew McDonnell <bugs@andrewmcdonnell.net>
|
||||
Andrew Po <absourd.noise@gmail.com>
|
||||
Andrew-Zipperer <atzipperer@gmail.com>
|
||||
Andrey Petrov <andrey.petrov@shazow.net>
|
||||
Andrii Berehuliak <berkusandrew@gmail.com>
|
||||
André Martins <aanm90@gmail.com>
|
||||
@ -126,13 +124,11 @@ Bryan Bess <squarejaw@bsbess.com>
|
||||
Bryan Boreham <bjboreham@gmail.com>
|
||||
Bryan Murphy <bmurphy1976@gmail.com>
|
||||
bryfry <bryon.fryer@gmail.com>
|
||||
Calvin Liu <flycalvin@qq.com>
|
||||
Cameron Spear <cameronspear@gmail.com>
|
||||
Cao Weiwei <cao.weiwei30@zte.com.cn>
|
||||
Carlo Mion <mion00@gmail.com>
|
||||
Carlos Alexandro Becker <caarlos0@gmail.com>
|
||||
Carlos de Paula <me@carlosedp.com>
|
||||
Casey Korver <casey@korver.dev>
|
||||
Ce Gao <ce.gao@outlook.com>
|
||||
Cedric Davies <cedricda@microsoft.com>
|
||||
Cezar Sa Espinola <cezarsa@gmail.com>
|
||||
@ -164,8 +160,6 @@ Christophe Vidal <kriss@krizalys.com>
|
||||
Christopher Biscardi <biscarch@sketcht.com>
|
||||
Christopher Crone <christopher.crone@docker.com>
|
||||
Christopher Jones <tophj@linux.vnet.ibm.com>
|
||||
Christopher Petito <47751006+krissetto@users.noreply.github.com>
|
||||
Christopher Petito <chrisjpetito@gmail.com>
|
||||
Christopher Svensson <stoffus@stoffus.com>
|
||||
Christy Norman <christy@linux.vnet.ibm.com>
|
||||
Chun Chen <ramichen@tencent.com>
|
||||
@ -218,7 +212,6 @@ David Cramer <davcrame@cisco.com>
|
||||
David Dooling <dooling@gmail.com>
|
||||
David Gageot <david@gageot.net>
|
||||
David Karlsson <david.karlsson@docker.com>
|
||||
David le Blanc <systemmonkey42@users.noreply.github.com>
|
||||
David Lechner <david@lechnology.com>
|
||||
David Scott <dave@recoil.org>
|
||||
David Sheets <dsheets@docker.com>
|
||||
@ -305,7 +298,6 @@ Gang Qiao <qiaohai8866@gmail.com>
|
||||
Gary Schaetz <gary@schaetzkc.com>
|
||||
Genki Takiuchi <genki@s21g.com>
|
||||
George MacRorie <gmacr31@gmail.com>
|
||||
George Margaritis <gmargaritis@protonmail.com>
|
||||
George Xie <georgexsh@gmail.com>
|
||||
Gianluca Borello <g.borello@gmail.com>
|
||||
Gildas Cuisinier <gildas.cuisinier@gcuisinier.net>
|
||||
@ -314,7 +306,6 @@ Gleb Stsenov <gleb.stsenov@gmail.com>
|
||||
Goksu Toprak <goksu.toprak@docker.com>
|
||||
Gou Rao <gou@portworx.com>
|
||||
Govind Rai <raigovind93@gmail.com>
|
||||
Grace Choi <grace.54109@gmail.com>
|
||||
Graeme Wiebe <graeme.wiebe@gmail.com>
|
||||
Grant Reaber <grant.reaber@gmail.com>
|
||||
Greg Pflaum <gpflaum@users.noreply.github.com>
|
||||
@ -395,7 +386,6 @@ Jezeniel Zapanta <jpzapanta22@gmail.com>
|
||||
Jian Zhang <zhangjian.fnst@cn.fujitsu.com>
|
||||
Jie Luo <luo612@zju.edu.cn>
|
||||
Jilles Oldenbeuving <ojilles@gmail.com>
|
||||
Jim Chen <njucjc@gmail.com>
|
||||
Jim Galasyn <jim.galasyn@docker.com>
|
||||
Jim Lin <b04705003@ntu.edu.tw>
|
||||
Jimmy Leger <jimmy.leger@gmail.com>
|
||||
@ -426,7 +416,6 @@ John Willis <john.willis@docker.com>
|
||||
Jon Johnson <jonjohnson@google.com>
|
||||
Jon Zeolla <zeolla@gmail.com>
|
||||
Jonatas Baldin <jonatas.baldin@gmail.com>
|
||||
Jonathan A. Sternberg <jonathansternberg@gmail.com>
|
||||
Jonathan Boulle <jonathanboulle@gmail.com>
|
||||
Jonathan Lee <jonjohn1232009@gmail.com>
|
||||
Jonathan Lomas <jonathan@floatinglomas.ca>
|
||||
@ -481,7 +470,6 @@ Kevin Woblick <mail@kovah.de>
|
||||
khaled souf <khaled.souf@gmail.com>
|
||||
Kim Eik <kim@heldig.org>
|
||||
Kir Kolyshkin <kolyshkin@gmail.com>
|
||||
Kirill A. Korinsky <kirill@korins.ky>
|
||||
Kotaro Yoshimatsu <kotaro.yoshimatsu@gmail.com>
|
||||
Krasi Georgiev <krasi@vip-consult.solutions>
|
||||
Kris-Mikael Krister <krismikael@protonmail.com>
|
||||
@ -542,7 +530,6 @@ Marco Vedovati <mvedovati@suse.com>
|
||||
Marcus Martins <marcus@docker.com>
|
||||
Marianna Tessel <mtesselh@gmail.com>
|
||||
Marius Ileana <marius.ileana@gmail.com>
|
||||
Marius Meschter <marius@meschter.me>
|
||||
Marius Sturm <marius@graylog.com>
|
||||
Mark Oates <fl0yd@me.com>
|
||||
Marsh Macy <marsma@microsoft.com>
|
||||
@ -551,7 +538,6 @@ Mary Anthony <mary.anthony@docker.com>
|
||||
Mason Fish <mason.fish@docker.com>
|
||||
Mason Malone <mason.malone@gmail.com>
|
||||
Mateusz Major <apkd@users.noreply.github.com>
|
||||
Mathias Duedahl <64321057+Lussebullen@users.noreply.github.com>
|
||||
Mathieu Champlon <mathieu.champlon@docker.com>
|
||||
Mathieu Rollet <matletix@gmail.com>
|
||||
Matt Gucci <matt9ucci@gmail.com>
|
||||
@ -561,7 +547,6 @@ Matthew Heon <mheon@redhat.com>
|
||||
Matthieu Hauglustaine <matt.hauglustaine@gmail.com>
|
||||
Mauro Porras P <mauroporrasp@gmail.com>
|
||||
Max Shytikov <mshytikov@gmail.com>
|
||||
Max-Julian Pogner <max-julian@pogner.at>
|
||||
Maxime Petazzoni <max@signalfuse.com>
|
||||
Maximillian Fan Xavier <maximillianfx@gmail.com>
|
||||
Mei ChunTao <mei.chuntao@zte.com.cn>
|
||||
@ -625,7 +610,6 @@ Nathan McCauley <nathan.mccauley@docker.com>
|
||||
Neil Peterson <neilpeterson@outlook.com>
|
||||
Nick Adcock <nick.adcock@docker.com>
|
||||
Nick Santos <nick.santos@docker.com>
|
||||
Nick Sieger <nick@nicksieger.com>
|
||||
Nico Stapelbroek <nstapelbroek@gmail.com>
|
||||
Nicola Kabar <nicolaka@gmail.com>
|
||||
Nicolas Borboën <ponsfrilus@gmail.com>
|
||||
@ -720,7 +704,6 @@ Rory Hunter <roryhunter2@gmail.com>
|
||||
Ross Boucher <rboucher@gmail.com>
|
||||
Rubens Figueiredo <r.figueiredo.52@gmail.com>
|
||||
Rui Cao <ruicao@alauda.io>
|
||||
Rui JingAn <quiterace@gmail.com>
|
||||
Ryan Belgrave <rmb1993@gmail.com>
|
||||
Ryan Detzel <ryan.detzel@gmail.com>
|
||||
Ryan Stelly <ryan.stelly@live.com>
|
||||
@ -814,7 +797,6 @@ Tim Hockin <thockin@google.com>
|
||||
Tim Sampson <tim@sampson.fi>
|
||||
Tim Smith <timbot@google.com>
|
||||
Tim Waugh <twaugh@redhat.com>
|
||||
Tim Welsh <timothy.welsh@docker.com>
|
||||
Tim Wraight <tim.wraight@tangentlabs.co.uk>
|
||||
timfeirg <kkcocogogo@gmail.com>
|
||||
Timothy Hobbs <timothyhobbs@seznam.cz>
|
||||
@ -898,11 +880,9 @@ Zhang Wei <zhangwei555@huawei.com>
|
||||
Zhang Wentao <zhangwentao234@huawei.com>
|
||||
ZhangHang <stevezhang2014@gmail.com>
|
||||
zhenghenghuo <zhenghenghuo@zju.edu.cn>
|
||||
Zhiwei Liang <zliang@akamai.com>
|
||||
Zhou Hao <zhouhao@cn.fujitsu.com>
|
||||
Zhoulin Xie <zhoulin.xie@daocloud.io>
|
||||
Zhu Guihua <zhugh.fnst@cn.fujitsu.com>
|
||||
Zhuo Zhi <h.dwwwwww@gmail.com>
|
||||
Álex González <agonzalezro@gmail.com>
|
||||
Álvaro Lázaro <alvaro.lazaro.g@gmail.com>
|
||||
Átila Camurça Alves <camurca.home@gmail.com>
|
||||
|
||||
@ -16,9 +16,9 @@ start participating.
|
||||
## Reporting security issues
|
||||
|
||||
The Docker maintainers take security seriously. If you discover a security
|
||||
issue, bring it to their attention right away!
|
||||
issue, please bring it to their attention right away!
|
||||
|
||||
**DO NOT** file a public issue, instead send your report privately to
|
||||
Please **DO NOT** file a public issue, instead send your report privately to
|
||||
[security@docker.com](mailto:security@docker.com).
|
||||
|
||||
Security reports are greatly appreciated and we will publicly thank you for it.
|
||||
@ -39,7 +39,7 @@ If you find a match, you can use the "subscribe" button to get notified on
|
||||
updates. Do *not* leave random "+1" or "I have this too" comments, as they
|
||||
only clutter the discussion, and don't help resolving it. However, if you
|
||||
have ways to reproduce the issue or have additional information that may help
|
||||
resolving the issue, leave a comment.
|
||||
resolving the issue, please leave a comment.
|
||||
|
||||
When reporting issues, always include:
|
||||
|
||||
@ -66,7 +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
|
||||
@ -198,10 +166,10 @@ Include an issue reference like `Closes #XXXX` or `Fixes #XXXX` in the pull requ
|
||||
description that close an issue. Including references automatically closes the issue
|
||||
on a merge.
|
||||
|
||||
Do not add yourself to the `AUTHORS` file, as it is regenerated regularly
|
||||
Please do not add yourself to the `AUTHORS` file, as it is regenerated regularly
|
||||
from the Git history.
|
||||
|
||||
See the [Coding Style](#coding-style) for further guidelines.
|
||||
Please see the [Coding Style](#coding-style) for further guidelines.
|
||||
|
||||
### Merge approval
|
||||
|
||||
@ -301,8 +269,8 @@ guidelines for the community as a whole:
|
||||
|
||||
* Stay on topic: Make sure that you are posting to the correct channel and
|
||||
avoid off-topic discussions. Remember when you update an issue or respond
|
||||
to an email you are potentially sending to a large number of people. Consider
|
||||
this before you update. Also remember that nobody likes spam.
|
||||
to an email you are potentially sending to a large number of people. Please
|
||||
consider this before you update. Also remember that nobody likes spam.
|
||||
|
||||
* Don't send email to the maintainers: There's no need to send email to the
|
||||
maintainers to ask them to investigate an issue or to take a look at a
|
||||
|
||||
20
Dockerfile
20
Dockerfile
@ -1,19 +1,15 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
ARG BASE_VARIANT=alpine
|
||||
ARG ALPINE_VERSION=3.21
|
||||
ARG ALPINE_VERSION=3.20
|
||||
ARG BASE_DEBIAN_DISTRO=bookworm
|
||||
|
||||
ARG GO_VERSION=1.23.6
|
||||
ARG XX_VERSION=1.6.1
|
||||
ARG GOVERSIONINFO_VERSION=v1.4.1
|
||||
ARG GOTESTSUM_VERSION=v1.12.0
|
||||
|
||||
# BUILDX_VERSION sets the version of buildx to use for the e2e tests.
|
||||
# It must be a tag in the docker.io/docker/buildx-bin image repository
|
||||
# on Docker Hub.
|
||||
ARG BUILDX_VERSION=0.20.1
|
||||
ARG COMPOSE_VERSION=v2.32.4
|
||||
ARG GO_VERSION=1.21.11
|
||||
ARG XX_VERSION=1.4.0
|
||||
ARG GOVERSIONINFO_VERSION=v1.3.0
|
||||
ARG GOTESTSUM_VERSION=v1.10.0
|
||||
ARG BUILDX_VERSION=0.12.1
|
||||
ARG COMPOSE_VERSION=v2.24.3
|
||||
|
||||
FROM --platform=$BUILDPLATFORM tonistiigi/xx:${XX_VERSION} AS xx
|
||||
|
||||
@ -67,6 +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=from=dockercore/golang-cross:xx-sdk-extras,target=/xx-sdk,src=/xx-sdk \
|
||||
--mount=type=tmpfs,target=cli/winresources \
|
||||
# override the default behavior of go with xx-go
|
||||
xx-go --wrap && \
|
||||
@ -92,6 +89,7 @@ ARG GO_STRIP
|
||||
ARG CGO_ENABLED
|
||||
ARG VERSION
|
||||
RUN --mount=ro --mount=type=cache,target=/root/.cache \
|
||||
--mount=from=dockercore/golang-cross:xx-sdk-extras,target=/xx-sdk,src=/xx-sdk \
|
||||
xx-go --wrap && \
|
||||
TARGET=/out ./scripts/build/plugins e2e/cli-plugins/plugins/*
|
||||
|
||||
|
||||
29
Makefile
29
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
|
||||
@ -86,33 +86,6 @@ mod-outdated: ## check outdated dependencies
|
||||
authors: ## generate AUTHORS file from git history
|
||||
scripts/docs/generate-authors.sh
|
||||
|
||||
.PHONY: completion
|
||||
completion: shell-completion
|
||||
completion: ## generate and install the shell-completion scripts
|
||||
# Note: this uses system-wide paths, and so may overwrite completion
|
||||
# scripts installed as part of deb/rpm packages.
|
||||
#
|
||||
# Given that this target is intended to debug/test updated versions, we could
|
||||
# consider installing in per-user (~/.config, XDG_DATA_DIR) paths instead, but
|
||||
# this will add more complexity.
|
||||
#
|
||||
# See https://github.com/docker/cli/pull/5770#discussion_r1927772710
|
||||
install -D -p -m 0644 ./build/completion/bash/docker /usr/share/bash-completion/completions/docker
|
||||
install -D -p -m 0644 ./build/completion/fish/docker.fish debian/docker-ce-cli/usr/share/fish/vendor_completions.d/docker.fish
|
||||
install -D -p -m 0644 ./build/completion/zsh/_docker debian/docker-ce-cli/usr/share/zsh/vendor-completions/_docker
|
||||
|
||||
|
||||
build/docker:
|
||||
# This target is used by the "shell-completion" target, which requires either
|
||||
# "binary" or "dynbinary" to have been built. We don't want to trigger those
|
||||
# to prevent replacing a static binary with a dynamic one, or vice-versa.
|
||||
@echo "Run 'make binary' or 'make dynbinary' first" && exit 1
|
||||
|
||||
.PHONY: shell-completion
|
||||
shell-completion: build/docker # requires either "binary" or "dynbinary" to be built.
|
||||
shell-completion: ## generate shell-completion scripts
|
||||
@ ./scripts/build/shell-completion
|
||||
|
||||
.PHONY: manpages
|
||||
manpages: ## generate man pages from go source and markdown
|
||||
scripts/docs/generate-man.sh
|
||||
|
||||
2
NOTICE
2
NOTICE
@ -14,6 +14,6 @@ United States and other governments.
|
||||
It is your responsibility to ensure that your use and/or transfer does not
|
||||
violate applicable laws.
|
||||
|
||||
For more information, see https://www.bis.doc.gov
|
||||
For more information, please see https://www.bis.doc.gov
|
||||
|
||||
See also https://www.apache.org/dev/crypto.html and/or seek legal counsel.
|
||||
|
||||
@ -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
|
||||
@ -68,7 +67,7 @@ make -f docker.Makefile shell
|
||||
## Legal
|
||||
|
||||
*Brought to you courtesy of our legal counsel. For more context,
|
||||
see the [NOTICE](https://github.com/docker/cli/blob/master/NOTICE) document in this repo.*
|
||||
please see the [NOTICE](https://github.com/docker/cli/blob/master/NOTICE) document in this repo.*
|
||||
|
||||
Use and transfer of Docker may be subject to certain restrictions by the
|
||||
United States and other governments.
|
||||
@ -76,7 +75,7 @@ United States and other governments.
|
||||
It is your responsibility to ensure that your use and/or transfer does not
|
||||
violate applicable laws.
|
||||
|
||||
For more information, see https://www.bis.doc.gov
|
||||
For more information, please see https://www.bis.doc.gov
|
||||
|
||||
## Licensing
|
||||
|
||||
|
||||
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.
|
||||
@ -12,24 +12,24 @@ import (
|
||||
)
|
||||
|
||||
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()
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,5 +17,5 @@ func (c *candidate) Path() string {
|
||||
}
|
||||
|
||||
func (c *candidate) Metadata() ([]byte, error) {
|
||||
return exec.Command(c.path, MetadataSubcommandName).Output() // #nosec G204 -- ignore "Subprocess launched with a potential tainted input or cmd arguments"
|
||||
return exec.Command(c.path, MetadataSubcommandName).Output()
|
||||
}
|
||||
|
||||
@ -52,6 +52,7 @@ func AddPluginCommandStubs(dockerCli command.Cli, rootCmd *cobra.Command) (err e
|
||||
return
|
||||
}
|
||||
for _, p := range plugins {
|
||||
p := p
|
||||
vendor := p.Vendor
|
||||
if vendor == "" {
|
||||
vendor = "unknown"
|
||||
@ -81,7 +82,7 @@ func AddPluginCommandStubs(dockerCli command.Cli, rootCmd *cobra.Command) (err 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
|
||||
|
||||
@ -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.22
|
||||
//go:build go1.19
|
||||
|
||||
package manager
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@ package manager
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"gotest.tools/v3/assert"
|
||||
@ -13,7 +13,7 @@ func TestPluginError(t *testing.T) {
|
||||
err := NewPluginError("new error")
|
||||
assert.Check(t, is.Error(err, "new error"))
|
||||
|
||||
inner := errors.New("testing")
|
||||
inner := fmt.Errorf("testing")
|
||||
err = wrapAsPluginError(inner, "wrapping")
|
||||
assert.Check(t, is.Error(err, "wrapping: testing"))
|
||||
assert.Check(t, is.ErrorIs(err, inner))
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
@ -29,36 +28,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 command.Cli, rootCmd, subCommand *cobra.Command, cmdErrorMessage string) {
|
||||
func RunCLICommandHooks(dockerCli command.Cli, rootCmd, subCommand *cobra.Command, cmdErrorMessage string) {
|
||||
commandName := strings.TrimPrefix(subCommand.CommandPath(), rootCmd.Name()+" ")
|
||||
flags := getCommandFlags(subCommand)
|
||||
|
||||
runHooks(ctx, dockerCli, rootCmd, subCommand, commandName, flags, cmdErrorMessage)
|
||||
runHooks(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 command.Cli, rootCmd, subCommand *cobra.Command, args []string) {
|
||||
func RunPluginHooks(dockerCli command.Cli, rootCmd, subCommand *cobra.Command, args []string) {
|
||||
commandName := strings.Join(args, " ")
|
||||
flags := getNaiveFlags(args)
|
||||
|
||||
runHooks(ctx, dockerCli, rootCmd, subCommand, commandName, flags, "")
|
||||
runHooks(dockerCli, rootCmd, subCommand, commandName, flags, "")
|
||||
}
|
||||
|
||||
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)
|
||||
func runHooks(dockerCli command.Cli, rootCmd, subCommand *cobra.Command, invokedCommand string, flags map[string]string, cmdErrorMessage string) {
|
||||
nextSteps := invokeAndCollectHooks(dockerCli, rootCmd, subCommand, invokedCommand, flags, cmdErrorMessage)
|
||||
|
||||
hooks.PrintNextSteps(dockerCli.Err(), nextSteps)
|
||||
}
|
||||
|
||||
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():
|
||||
return nil
|
||||
default:
|
||||
}
|
||||
|
||||
func invokeAndCollectHooks(dockerCli command.Cli, rootCmd, subCmd *cobra.Command, subCmdStr string, flags map[string]string, cmdErrorMessage string) []string {
|
||||
pluginsCfg := dockerCli.ConfigFile().Plugins
|
||||
if pluginsCfg == nil {
|
||||
return nil
|
||||
@ -76,7 +68,7 @@ func invokeAndCollectHooks(ctx context.Context, dockerCli command.Cli, rootCmd,
|
||||
continue
|
||||
}
|
||||
|
||||
hookReturn, err := p.RunHook(ctx, HookPluginData{
|
||||
hookReturn, err := p.RunHook(HookPluginData{
|
||||
RootCmd: match,
|
||||
Flags: flags,
|
||||
CommandError: cmdErrorMessage,
|
||||
|
||||
@ -32,7 +32,7 @@ const (
|
||||
// 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)
|
||||
@ -49,16 +49,6 @@ func IsNotFound(err error) bool {
|
||||
return ok
|
||||
}
|
||||
|
||||
// getPluginDirs returns the platform-specific locations to search for plugins
|
||||
// in order of preference.
|
||||
//
|
||||
// Plugin-discovery is performed in the following order of preference:
|
||||
//
|
||||
// 1. The "cli-plugins" directory inside the CLIs [config.Path] (usually "~/.docker/cli-plugins").
|
||||
// 2. Additional plugin directories as configured through [ConfigFile.CLIPluginsExtraDirs].
|
||||
// 3. Platform-specific defaultSystemPluginDirs.
|
||||
//
|
||||
// [ConfigFile.CLIPluginsExtraDirs]: https://pkg.go.dev/github.com/docker/cli@v26.1.4+incompatible/cli/config/configfile#ConfigFile.CLIPluginsExtraDirs
|
||||
func getPluginDirs(cfg *configfile.ConfigFile) ([]string, error) {
|
||||
var pluginDirs []string
|
||||
|
||||
@ -75,12 +65,10 @@ func getPluginDirs(cfg *configfile.ConfigFile) ([]string, error) {
|
||||
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 {
|
||||
@ -101,15 +89,28 @@ func addPluginCandidatesFromDir(res map[string][]string, d string) {
|
||||
}
|
||||
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
|
||||
@ -119,7 +120,11 @@ func GetPlugin(name string, dockerCli command.Cli, rootcmd *cobra.Command) (*Plu
|
||||
return nil, err
|
||||
}
|
||||
|
||||
candidates := listPluginCandidates(pluginDirs)
|
||||
candidates, err := listPluginCandidates(pluginDirs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if paths, ok := candidates[name]; ok {
|
||||
if len(paths) == 0 {
|
||||
return nil, errPluginNotFound(name)
|
||||
@ -145,7 +150,10 @@ func ListPlugins(dockerCli command.Cli, rootcmd *cobra.Command) ([]Plugin, error
|
||||
return nil, err
|
||||
}
|
||||
|
||||
candidates := listPluginCandidates(pluginDirs)
|
||||
candidates, err := listPluginCandidates(pluginDirs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var plugins []Plugin
|
||||
var mu sync.Mutex
|
||||
@ -222,8 +230,7 @@ func PluginRunCommand(dockerCli command.Cli, name string, rootcmd *cobra.Command
|
||||
// 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
|
||||
|
||||
@ -51,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"),
|
||||
@ -81,29 +82,6 @@ func TestListPluginCandidates(t *testing.T) {
|
||||
assert.DeepEqual(t, candidates, exp)
|
||||
}
|
||||
|
||||
// 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", `
|
||||
|
||||
@ -2,19 +2,7 @@
|
||||
|
||||
package manager
|
||||
|
||||
// defaultSystemPluginDirs are the platform-specific locations to search
|
||||
// for plugins in order of preference.
|
||||
//
|
||||
// Plugin-discovery is performed in the following order of preference:
|
||||
//
|
||||
// 1. The "cli-plugins" directory inside the CLIs config-directory (usually "~/.docker/cli-plugins").
|
||||
// 2. Additional plugin directories as configured through [ConfigFile.CLIPluginsExtraDirs].
|
||||
// 3. Platform-specific defaultSystemPluginDirs (as defined below).
|
||||
//
|
||||
// [ConfigFile.CLIPluginsExtraDirs]: https://pkg.go.dev/github.com/docker/cli@v26.1.4+incompatible/cli/config/configfile#ConfigFile.CLIPluginsExtraDirs
|
||||
var defaultSystemPluginDirs = []string{
|
||||
"/usr/local/lib/docker/cli-plugins",
|
||||
"/usr/local/libexec/docker/cli-plugins",
|
||||
"/usr/lib/docker/cli-plugins",
|
||||
"/usr/libexec/docker/cli-plugins",
|
||||
"/usr/local/lib/docker/cli-plugins", "/usr/local/libexec/docker/cli-plugins",
|
||||
"/usr/lib/docker/cli-plugins", "/usr/libexec/docker/cli-plugins",
|
||||
}
|
||||
|
||||
@ -5,16 +5,6 @@ import (
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// defaultSystemPluginDirs are the platform-specific locations to search
|
||||
// for plugins in order of preference.
|
||||
//
|
||||
// Plugin-discovery is performed in the following order of preference:
|
||||
//
|
||||
// 1. The "cli-plugins" directory inside the CLIs config-directory (usually "~/.docker/cli-plugins").
|
||||
// 2. Additional plugin directories as configured through [ConfigFile.CLIPluginsExtraDirs].
|
||||
// 3. Platform-specific defaultSystemPluginDirs (as defined below).
|
||||
//
|
||||
// [ConfigFile.CLIPluginsExtraDirs]: https://pkg.go.dev/github.com/docker/cli@v26.1.4+incompatible/cli/config/configfile#ConfigFile.CLIPluginsExtraDirs
|
||||
var defaultSystemPluginDirs = []string{
|
||||
filepath.Join(os.Getenv("ProgramData"), "Docker", "cli-plugins"),
|
||||
filepath.Join(os.Getenv("ProgramFiles"), "Docker", "cli-plugins"),
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"os/exec"
|
||||
@ -106,13 +105,13 @@ func newPlugin(c Candidate, cmds []*cobra.Command) (Plugin, error) {
|
||||
|
||||
// RunHook executes the plugin's hooks command
|
||||
// and returns its unprocessed output.
|
||||
func (p *Plugin) RunHook(ctx context.Context, hookData HookPluginData) ([]byte, error) {
|
||||
func (p *Plugin) RunHook(hookData HookPluginData) ([]byte, error) {
|
||||
hDataBytes, err := json.Marshal(hookData)
|
||||
if err != nil {
|
||||
return nil, wrapAsPluginError(err, "failed to marshall hook data")
|
||||
}
|
||||
|
||||
pCmd := exec.CommandContext(ctx, p.Path, p.Name, HookSubcommandName, string(hDataBytes)) // #nosec G204 -- ignore "Subprocess launched with a potential tainted input or cmd arguments"
|
||||
pCmd := exec.Command(p.Path, p.Name, HookSubcommandName, string(hDataBytes))
|
||||
pCmd.Env = os.Environ()
|
||||
pCmd.Env = append(pCmd.Env, ReexecEnvvar+"="+os.Args[0])
|
||||
hookCmdOutput, err := pCmd.Output()
|
||||
|
||||
@ -3,7 +3,6 @@ package plugin
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
@ -35,9 +34,15 @@ func RunPlugin(dockerCli *command.DockerCli, plugin *cobra.Command, meta manager
|
||||
|
||||
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())
|
||||
cmdContext := cmd.Context()
|
||||
// TODO: revisit and make sure this check makes sense
|
||||
// see: https://github.com/docker/cli/pull/4599#discussion_r1422487271
|
||||
if cmdContext == nil {
|
||||
cmdContext = context.TODO()
|
||||
}
|
||||
ctx, cancel := context.WithCancel(cmdContext)
|
||||
cmd.SetContext(ctx)
|
||||
// Set up the context to cancel based on signalling via CLI socket.
|
||||
socket.ConnectAndWait(cancel)
|
||||
@ -46,8 +51,7 @@ func RunPlugin(dockerCli *command.DockerCli, plugin *cobra.Command, meta manager
|
||||
if os.Getenv("DOCKER_CLI_PLUGIN_USE_DIAL_STDIO") != "" {
|
||||
opts = append(opts, withPluginClientConn(plugin.Name()))
|
||||
}
|
||||
opts = append(opts, command.WithEnableGlobalMeterProvider(), command.WithEnableGlobalTracerProvider())
|
||||
retErr = tcmd.Initialize(opts...)
|
||||
err = tcmd.Initialize(opts...)
|
||||
ogRunE := cmd.RunE
|
||||
if ogRunE == nil {
|
||||
ogRun := cmd.Run
|
||||
@ -67,7 +71,7 @@ func RunPlugin(dockerCli *command.DockerCli, plugin *cobra.Command, meta manager
|
||||
return err
|
||||
}
|
||||
})
|
||||
return retErr
|
||||
return err
|
||||
}
|
||||
|
||||
cmd, args, err := tcmd.HandleGlobalFlags()
|
||||
@ -93,17 +97,18 @@ func Run(makeCmd func(command.Cli) *cobra.Command, meta manager.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)
|
||||
}
|
||||
}
|
||||
@ -158,7 +163,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)
|
||||
|
||||
@ -9,8 +9,6 @@ import (
|
||||
"os"
|
||||
"runtime"
|
||||
"sync"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// EnvKey represents the well-known environment variable used to pass the
|
||||
@ -32,7 +30,6 @@ func NewPluginServer(h func(net.Conn)) (*PluginServer, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
logrus.Trace("Plugin server listening on ", l.Addr())
|
||||
|
||||
if h == nil {
|
||||
h = func(net.Conn) {}
|
||||
@ -95,10 +92,6 @@ func (pl *PluginServer) Addr() net.Addr {
|
||||
//
|
||||
// The error value is that of the underlying [net.Listner.Close] call.
|
||||
func (pl *PluginServer) Close() error {
|
||||
if pl == nil {
|
||||
return nil
|
||||
}
|
||||
logrus.Trace("Closing plugin server")
|
||||
// Close connections first to ensure the connections get io.EOF instead
|
||||
// of a connection reset.
|
||||
pl.closeAllConns()
|
||||
@ -114,10 +107,6 @@ func (pl *PluginServer) closeAllConns() {
|
||||
pl.mu.Lock()
|
||||
defer pl.mu.Unlock()
|
||||
|
||||
if pl.closed {
|
||||
return
|
||||
}
|
||||
|
||||
// Prevent new connections from being accepted.
|
||||
pl.closed = true
|
||||
|
||||
|
||||
@ -117,18 +117,6 @@ func TestPluginServer(t *testing.T) {
|
||||
assert.NilError(t, err, "failed to dial returned server")
|
||||
checkDirNoNewPluginServer(t)
|
||||
})
|
||||
|
||||
t.Run("does not panic on Close if server is nil", func(t *testing.T) {
|
||||
var srv *PluginServer
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Errorf("panicked on Close")
|
||||
}
|
||||
}()
|
||||
|
||||
err := srv.Close()
|
||||
assert.NilError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func checkDirNoNewPluginServer(t *testing.T) {
|
||||
|
||||
14
cli/cobra.go
14
cli/cobra.go
@ -54,7 +54,7 @@ func setupCommonRootCommand(rootCmd *cobra.Command) (*cliflags.ClientOptions, *c
|
||||
rootCmd.SetHelpCommand(helpCommand)
|
||||
|
||||
rootCmd.PersistentFlags().BoolP("help", "h", false, "Print usage")
|
||||
rootCmd.PersistentFlags().MarkShorthandDeprecated("help", "use --help")
|
||||
rootCmd.PersistentFlags().MarkShorthandDeprecated("help", "please use --help")
|
||||
rootCmd.PersistentFlags().Lookup("help").Hidden = true
|
||||
|
||||
rootCmd.Annotations = map[string]string{
|
||||
@ -92,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,
|
||||
}
|
||||
}
|
||||
@ -337,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)
|
||||
}
|
||||
@ -520,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,6 @@ package builder
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
@ -20,7 +19,5 @@ func TestBuilderPromptTermination(t *testing.T) {
|
||||
},
|
||||
})
|
||||
cmd := NewPruneCommand(cli)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
test.TerminatePrompt(ctx, t, cmd, cli)
|
||||
}
|
||||
|
||||
@ -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/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",
|
||||
},
|
||||
@ -43,45 +42,31 @@ func TestCheckpointCreateErrors(t *testing.T) {
|
||||
cmd := newCreateCommand(cli)
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckpointCreateWithOptions(t *testing.T) {
|
||||
const (
|
||||
containerName = "container-foo"
|
||||
checkpointName = "checkpoint-bar"
|
||||
checkpointDir = "/dir/foo"
|
||||
)
|
||||
|
||||
for _, tc := range []bool{true, false} {
|
||||
leaveRunning := strconv.FormatBool(tc)
|
||||
t.Run("leave-running="+leaveRunning, func(t *testing.T) {
|
||||
var actualContainerName string
|
||||
var actualOptions 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())))
|
||||
}
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
package checkpoint
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"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",
|
||||
},
|
||||
@ -42,7 +42,6 @@ func TestCheckpointListErrors(t *testing.T) {
|
||||
cmd := newListCommand(cli)
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
package checkpoint
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"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",
|
||||
},
|
||||
@ -41,7 +41,6 @@ func TestCheckpointRemoveErrors(t *testing.T) {
|
||||
cmd := newRemoveCommand(cli)
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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.22
|
||||
//go:build go1.19
|
||||
|
||||
package command
|
||||
|
||||
@ -44,7 +44,7 @@ const defaultInitTimeout = 2 * time.Second
|
||||
type Streams interface {
|
||||
In() *streams.In
|
||||
Out() *streams.Out
|
||||
Err() *streams.Out
|
||||
Err() io.Writer
|
||||
}
|
||||
|
||||
// Cli represents the docker command line client.
|
||||
@ -75,7 +75,7 @@ type DockerCli struct {
|
||||
options *cliflags.ClientOptions
|
||||
in *streams.In
|
||||
out *streams.Out
|
||||
err *streams.Out
|
||||
err io.Writer
|
||||
client client.APIClient
|
||||
serverInfo ServerInfo
|
||||
contentTrust bool
|
||||
@ -92,12 +92,10 @@ type DockerCli struct {
|
||||
// this may be replaced by explicitly passing a context to functions that
|
||||
// need it.
|
||||
baseCtx context.Context
|
||||
|
||||
enableGlobalMeter, enableGlobalTracer bool
|
||||
}
|
||||
|
||||
// DefaultVersion returns api.defaultVersion.
|
||||
func (*DockerCli) DefaultVersion() string {
|
||||
func (cli *DockerCli) DefaultVersion() string {
|
||||
return api.DefaultVersion
|
||||
}
|
||||
|
||||
@ -114,7 +112,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
|
||||
@ -126,7 +124,7 @@ func (cli *DockerCli) Out() *streams.Out {
|
||||
}
|
||||
|
||||
// Err returns the writer used for stderr
|
||||
func (cli *DockerCli) Err() *streams.Out {
|
||||
func (cli *DockerCli) Err() io.Writer {
|
||||
return cli.err
|
||||
}
|
||||
|
||||
@ -186,18 +184,9 @@ func (cli *DockerCli) BuildKitEnabled() (bool, error) {
|
||||
if _, ok := aliasMap["builder"]; ok {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
si := cli.ServerInfo()
|
||||
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.
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// otherwise, assume BuildKit is enabled for Linux, but disabled for
|
||||
// Windows / WCOW, which does not yet support BuildKit by default.
|
||||
return si.OSType != "windows", nil
|
||||
// otherwise, assume BuildKit is enabled but
|
||||
// not if wcow reported from server side
|
||||
return cli.ServerInfo().OSType != "windows", nil
|
||||
}
|
||||
|
||||
// HooksEnabled returns whether plugin hooks are enabled.
|
||||
@ -231,7 +220,7 @@ func (cli *DockerCli) HooksEnabled() bool {
|
||||
}
|
||||
|
||||
// ManifestStore returns a store for local manifests
|
||||
func (*DockerCli) ManifestStore() manifeststore.Store {
|
||||
func (cli *DockerCli) ManifestStore() manifeststore.Store {
|
||||
// TODO: support override default location from config file
|
||||
return manifeststore.NewStore(filepath.Join(config.Dir(), "manifests"))
|
||||
}
|
||||
@ -272,7 +261,7 @@ 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")
|
||||
return errors.New("conflicting options: either specify --host or --context, not both")
|
||||
}
|
||||
|
||||
cli.options = opts
|
||||
@ -286,12 +275,8 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...CLIOption)
|
||||
}
|
||||
|
||||
// TODO(krissetto): pass ctx to the funcs instead of using this
|
||||
if cli.enableGlobalMeter {
|
||||
cli.createGlobalMeterProvider(cli.baseCtx)
|
||||
}
|
||||
if cli.enableGlobalTracer {
|
||||
cli.createGlobalTracerProvider(cli.baseCtx)
|
||||
}
|
||||
cli.createGlobalMeterProvider(cli.baseCtx)
|
||||
cli.createGlobalTracerProvider(cli.baseCtx)
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -299,7 +284,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()
|
||||
@ -324,13 +309,13 @@ func newAPIClientFromEndpoint(ep docker.Endpoint, configFile *configfile.ConfigF
|
||||
if len(configFile.HTTPHeaders) > 0 {
|
||||
opts = append(opts, client.WithHTTPHeaders(configFile.HTTPHeaders))
|
||||
}
|
||||
opts = append(opts, withCustomHeadersFromEnv(), client.WithUserAgent(UserAgent()))
|
||||
opts = append(opts, client.WithUserAgent(UserAgent()))
|
||||
return client.NewClientWithOpts(opts...)
|
||||
}
|
||||
|
||||
func resolveDockerEndpoint(s store.Reader, contextName string) (docker.Endpoint, error) {
|
||||
if s == nil {
|
||||
return docker.Endpoint{}, errors.New("no context store initialized")
|
||||
return docker.Endpoint{}, fmt.Errorf("no context store initialized")
|
||||
}
|
||||
ctxMeta, err := s.GetMetadata(contextName)
|
||||
if err != nil {
|
||||
@ -475,7 +460,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
|
||||
}
|
||||
@ -561,7 +546,7 @@ func getServerHost(hosts []string, tlsOptions *tlsconfig.Options) (string, error
|
||||
case 1:
|
||||
host = hosts[0]
|
||||
default:
|
||||
return "", errors.New("Specify only one -H")
|
||||
return "", errors.New("Please specify only one -H")
|
||||
}
|
||||
|
||||
return dopts.ParseHost(tlsOptions != nil, host)
|
||||
|
||||
@ -2,18 +2,13 @@ package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/csv"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/docker/docker/errdefs"
|
||||
"github.com/moby/term"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// CLIOption is a functional argument to apply options to a [DockerCli]. These
|
||||
@ -28,7 +23,7 @@ func WithStandardStreams() CLIOption {
|
||||
stdin, stdout, stderr := term.StdStreams()
|
||||
cli.in = streams.NewIn(stdin)
|
||||
cli.out = streams.NewOut(stdout)
|
||||
cli.err = streams.NewOut(stderr)
|
||||
cli.err = stderr
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@ -45,9 +40,8 @@ func WithBaseContext(ctx context.Context) CLIOption {
|
||||
// WithCombinedStreams uses the same stream for the output and error streams.
|
||||
func WithCombinedStreams(combined io.Writer) CLIOption {
|
||||
return func(cli *DockerCli) error {
|
||||
s := streams.NewOut(combined)
|
||||
cli.out = s
|
||||
cli.err = s
|
||||
cli.out = streams.NewOut(combined)
|
||||
cli.err = combined
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@ -71,7 +65,7 @@ func WithOutputStream(out io.Writer) CLIOption {
|
||||
// WithErrorStream sets a cli error stream.
|
||||
func WithErrorStream(err io.Writer) CLIOption {
|
||||
return func(cli *DockerCli) error {
|
||||
cli.err = streams.NewOut(err)
|
||||
cli.err = err
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@ -113,116 +107,3 @@ func WithAPIClient(c client.APIClient) CLIOption {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// envOverrideHTTPHeaders is the name of the environment-variable that can be
|
||||
// used to set custom HTTP headers to be sent by the client. This environment
|
||||
// variable is the equivalent to the HttpHeaders field in the configuration
|
||||
// file.
|
||||
//
|
||||
// WARNING: If both config and environment-variable are set, the environment
|
||||
// variable currently overrides all headers set in the configuration file.
|
||||
// This behavior may change in a future update, as we are considering the
|
||||
// environment variable to be appending to existing headers (and to only
|
||||
// override headers with the same name).
|
||||
//
|
||||
// While this env-var allows for custom headers to be set, it does not allow
|
||||
// for built-in headers (such as "User-Agent", if set) to be overridden.
|
||||
// Also see [client.WithHTTPHeaders] and [client.WithUserAgent].
|
||||
//
|
||||
// This environment variable can be used in situations where headers must be
|
||||
// set for a specific invocation of the CLI, but should not be set by default,
|
||||
// and therefore cannot be set in the config-file.
|
||||
//
|
||||
// envOverrideHTTPHeaders accepts a comma-separated (CSV) list of key=value pairs,
|
||||
// where key must be a non-empty, valid MIME header format. Whitespaces surrounding
|
||||
// the key are trimmed, and the key is normalised. Whitespaces in values are
|
||||
// preserved, but "key=value" pairs with an empty value (e.g. "key=") are ignored.
|
||||
// Tuples without a "=" produce an error.
|
||||
//
|
||||
// It follows CSV rules for escaping, allowing "key=value" pairs to be quoted
|
||||
// if they must contain commas, which allows for multiple values for a single
|
||||
// header to be set. If a key is repeated in the list, later values override
|
||||
// prior values.
|
||||
//
|
||||
// For example, the following value:
|
||||
//
|
||||
// one=one-value,"two=two,value","three= a value with whitespace ",four=,five=five=one,five=five-two
|
||||
//
|
||||
// Produces four headers (four is omitted as it has an empty value set):
|
||||
//
|
||||
// - one (value is "one-value")
|
||||
// - two (value is "two,value")
|
||||
// - three (value is " a value with whitespace ")
|
||||
// - five (value is "five-two", the later value has overridden the prior value)
|
||||
const envOverrideHTTPHeaders = "DOCKER_CUSTOM_HEADERS"
|
||||
|
||||
// withCustomHeadersFromEnv overriding custom HTTP headers to be sent by the
|
||||
// client through the [envOverrideHTTPHeaders] environment-variable. This
|
||||
// environment variable is the equivalent to the HttpHeaders field in the
|
||||
// configuration file.
|
||||
//
|
||||
// WARNING: If both config and environment-variable are set, the environment-
|
||||
// variable currently overrides all headers set in the configuration file.
|
||||
// This behavior may change in a future update, as we are considering the
|
||||
// environment-variable to be appending to existing headers (and to only
|
||||
// override headers with the same name).
|
||||
//
|
||||
// TODO(thaJeztah): this is a client Option, and should be moved to the client. It is non-exported for that reason.
|
||||
func withCustomHeadersFromEnv() client.Opt {
|
||||
return func(apiClient *client.Client) error {
|
||||
value := os.Getenv(envOverrideHTTPHeaders)
|
||||
if value == "" {
|
||||
return nil
|
||||
}
|
||||
csvReader := csv.NewReader(strings.NewReader(value))
|
||||
fields, err := csvReader.Read()
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
env := map[string]string{}
|
||||
for _, kv := range fields {
|
||||
k, v, hasValue := strings.Cut(kv, "=")
|
||||
|
||||
// Only strip whitespace in keys; preserve whitespace in values.
|
||||
k = strings.TrimSpace(k)
|
||||
|
||||
if k == "" {
|
||||
return errdefs.InvalidParameter(errors.Errorf(
|
||||
`failed to set custom headers from %s environment variable: value contains a key=value pair with an empty key: '%s'`,
|
||||
envOverrideHTTPHeaders, kv,
|
||||
))
|
||||
}
|
||||
|
||||
// We don't currently allow empty key=value pairs, and produce an error.
|
||||
// This is something we could allow in future (e.g. to read value
|
||||
// from an environment variable with the same name). In the meantime,
|
||||
// produce an error to prevent users from depending on this.
|
||||
if !hasValue {
|
||||
return errdefs.InvalidParameter(errors.Errorf(
|
||||
`failed to set custom headers from %s environment variable: missing "=" in key=value pair: '%s'`,
|
||||
envOverrideHTTPHeaders, kv,
|
||||
))
|
||||
}
|
||||
|
||||
env[http.CanonicalHeaderKey(k)] = v
|
||||
}
|
||||
|
||||
if len(env) == 0 {
|
||||
// We should probably not hit this case, as we don't skip values
|
||||
// (only return errors), but we don't want to discard existing
|
||||
// headers with an empty set.
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO(thaJeztah): add a client.WithExtraHTTPHeaders() function to allow these headers to be _added_ to existing ones, instead of _replacing_
|
||||
// see https://github.com/docker/cli/pull/5098#issuecomment-2147403871 (when updating, also update the WARNING in the function and env-var GoDoc)
|
||||
return client.WithHTTPHeaders(env)(apiClient)
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,7 +3,6 @@ package command
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
@ -19,10 +18,10 @@ import (
|
||||
"github.com/docker/cli/cli/config"
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
"github.com/docker/cli/cli/flags"
|
||||
"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"
|
||||
)
|
||||
@ -87,44 +86,8 @@ func TestNewAPIClientFromFlagsWithCustomHeaders(t *testing.T) {
|
||||
assert.DeepEqual(t, received, expectedHeaders)
|
||||
}
|
||||
|
||||
func TestNewAPIClientFromFlagsWithCustomHeadersFromEnv(t *testing.T) {
|
||||
var received http.Header
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
received = r.Header.Clone()
|
||||
_, _ = w.Write([]byte("OK"))
|
||||
}))
|
||||
defer ts.Close()
|
||||
host := strings.Replace(ts.URL, "http://", "tcp://", 1)
|
||||
opts := &flags.ClientOptions{Hosts: []string{host}}
|
||||
configFile := &configfile.ConfigFile{
|
||||
HTTPHeaders: map[string]string{
|
||||
"My-Header": "Custom-Value from config-file",
|
||||
},
|
||||
}
|
||||
|
||||
// envOverrideHTTPHeaders should override the HTTPHeaders from the config-file,
|
||||
// so "My-Header" should not be present.
|
||||
t.Setenv(envOverrideHTTPHeaders, `one=one-value,"two=two,value",three=,four=four-value,four=four-value-override`)
|
||||
apiClient, err := NewAPIClientFromFlags(opts, configFile)
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, apiClient.DaemonHost(), host)
|
||||
assert.Equal(t, apiClient.ClientVersion(), api.DefaultVersion)
|
||||
|
||||
expectedHeaders := http.Header{
|
||||
"One": []string{"one-value"},
|
||||
"Two": []string{"two,value"},
|
||||
"Three": []string{""},
|
||||
"Four": []string{"four-value-override"},
|
||||
"User-Agent": []string{UserAgent()},
|
||||
}
|
||||
_, err = apiClient.Ping(context.Background())
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, received, expectedHeaders)
|
||||
}
|
||||
|
||||
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")
|
||||
|
||||
@ -132,7 +95,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 {
|
||||
@ -188,18 +151,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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -228,7 +192,7 @@ func TestInitializeFromClientHangs(t *testing.T) {
|
||||
ts.Start()
|
||||
defer ts.Close()
|
||||
|
||||
opts := &flags.ClientOptions{Hosts: []string{"unix://" + socket}}
|
||||
opts := &flags.ClientOptions{Hosts: []string{fmt.Sprintf("unix://%s", socket)}}
|
||||
configFile := &configfile.ConfigFile{}
|
||||
apiClient, err := NewAPIClientFromFlags(opts, configFile)
|
||||
assert.NilError(t, err)
|
||||
@ -277,9 +241,10 @@ func TestExperimentalCLI(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testcases {
|
||||
t.Run(tc.doc, func(t *testing.T) {
|
||||
dir := fs.NewDir(t, tc.doc, fs.WithFile("config.json", tc.configfile))
|
||||
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,
|
||||
@ -288,7 +253,7 @@ func TestExperimentalCLI(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
cli := &DockerCli{client: apiclient, err: streams.NewOut(os.Stderr)}
|
||||
cli := &DockerCli{client: apiclient, err: os.Stderr}
|
||||
config.SetDir(dir.Path())
|
||||
err := cli.Initialize(flags.NewClientOptions())
|
||||
assert.NilError(t, err)
|
||||
|
||||
@ -2,43 +2,29 @@ package completion
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"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
|
||||
type ValidArgsFn func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective)
|
||||
|
||||
// APIClientProvider provides a method to get an [client.APIClient], initializing
|
||||
// it if needed.
|
||||
//
|
||||
// It's a smaller interface than [command.Cli], and used in situations where an
|
||||
// APIClient is needed, but we want to postpone initializing the client until
|
||||
// it's used.
|
||||
type APIClientProvider interface {
|
||||
Client() client.APIClient
|
||||
}
|
||||
|
||||
// ImageNames offers completion for images present within the local store
|
||||
func ImageNames(dockerCLI APIClientProvider, limit int) ValidArgsFn {
|
||||
func ImageNames(dockerCli command.Cli) 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{})
|
||||
list, err := dockerCli.Client().ImageList(cmd.Context(), image.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveError
|
||||
}
|
||||
var names []string
|
||||
for _, img := range list {
|
||||
names = append(names, img.RepoTags...)
|
||||
for _, image := range list {
|
||||
names = append(names, image.RepoTags...)
|
||||
}
|
||||
return names, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
@ -47,9 +33,9 @@ func ImageNames(dockerCLI APIClientProvider, limit int) ValidArgsFn {
|
||||
// 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) ValidArgsFn {
|
||||
func ContainerNames(dockerCli command.Cli, 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{
|
||||
list, err := dockerCli.Client().ContainerList(cmd.Context(), container.ListOptions{
|
||||
All: all,
|
||||
})
|
||||
if err != nil {
|
||||
@ -59,10 +45,10 @@ func ContainerNames(dockerCLI APIClientProvider, all bool, filters ...func(conta
|
||||
showContainerIDs := os.Getenv("DOCKER_COMPLETION_SHOW_CONTAINER_IDS") == "yes"
|
||||
|
||||
var names []string
|
||||
for _, ctr := range list {
|
||||
for _, container := range list {
|
||||
skip := false
|
||||
for _, fn := range filters {
|
||||
if fn != nil && !fn(ctr) {
|
||||
if !fn(container) {
|
||||
skip = true
|
||||
break
|
||||
}
|
||||
@ -71,18 +57,18 @@ func ContainerNames(dockerCLI APIClientProvider, all bool, filters ...func(conta
|
||||
continue
|
||||
}
|
||||
if showContainerIDs {
|
||||
names = append(names, ctr.ID)
|
||||
names = append(names, container.ID)
|
||||
}
|
||||
names = append(names, formatter.StripNamePrefix(ctr.Names)...)
|
||||
names = append(names, formatter.StripNamePrefix(container.Names)...)
|
||||
}
|
||||
return names, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
}
|
||||
|
||||
// VolumeNames offers completion for volumes
|
||||
func VolumeNames(dockerCLI APIClientProvider) ValidArgsFn {
|
||||
func VolumeNames(dockerCli command.Cli) ValidArgsFn {
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
list, err := dockerCLI.Client().VolumeList(cmd.Context(), volume.ListOptions{})
|
||||
list, err := dockerCli.Client().VolumeList(cmd.Context(), volume.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveError
|
||||
}
|
||||
@ -95,100 +81,21 @@ func VolumeNames(dockerCLI APIClientProvider) ValidArgsFn {
|
||||
}
|
||||
|
||||
// NetworkNames offers completion for networks
|
||||
func NetworkNames(dockerCLI APIClientProvider) ValidArgsFn {
|
||||
func NetworkNames(dockerCli command.Cli) ValidArgsFn {
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
list, err := dockerCLI.Client().NetworkList(cmd.Context(), network.ListOptions{})
|
||||
list, err := dockerCli.Client().NetworkList(cmd.Context(), types.NetworkListOptions{})
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveError
|
||||
}
|
||||
var names []string
|
||||
for _, nw := range list {
|
||||
names = append(names, nw.Name)
|
||||
for _, network := range list {
|
||||
names = append(names, network.Name)
|
||||
}
|
||||
return names, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
}
|
||||
|
||||
// EnvVarNames offers completion for environment-variable names. This
|
||||
// completion can be used for "--env" and "--build-arg" flags, which
|
||||
// allow obtaining the value of the given environment-variable if present
|
||||
// in the local environment, so we only should complete the names of the
|
||||
// environment variables, and not their value. This also prevents the
|
||||
// completion script from printing values of environment variables
|
||||
// containing sensitive values.
|
||||
//
|
||||
// For example;
|
||||
//
|
||||
// export MY_VAR=hello
|
||||
// docker run --rm --env MY_VAR alpine printenv MY_VAR
|
||||
// hello
|
||||
func EnvVarNames(_ *cobra.Command, _ []string, _ string) (names []string, _ cobra.ShellCompDirective) {
|
||||
envs := os.Environ()
|
||||
names = make([]string, 0, len(envs))
|
||||
for _, env := range envs {
|
||||
name, _, _ := strings.Cut(env, "=")
|
||||
names = append(names, name)
|
||||
}
|
||||
return names, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
|
||||
// FromList offers completion for the given list of options.
|
||||
func FromList(options ...string) ValidArgsFn {
|
||||
return cobra.FixedCompletions(options, cobra.ShellCompDirectiveNoFileComp)
|
||||
}
|
||||
|
||||
// FileNames is a convenience function to use [cobra.ShellCompDirectiveDefault],
|
||||
// which indicates to let the shell perform its default behavior after
|
||||
// completions have been provided.
|
||||
func FileNames(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
|
||||
return nil, cobra.ShellCompDirectiveDefault
|
||||
}
|
||||
|
||||
// NoComplete is used for commands where there's no relevant completion
|
||||
func NoComplete(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
|
||||
func NoComplete(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
|
||||
var commonPlatforms = []string{
|
||||
"linux",
|
||||
"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",
|
||||
"windows/amd64",
|
||||
|
||||
"wasip1",
|
||||
"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/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/docker/api/types/volume"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"github.com/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: "running", Names: []string{"/container-c", "/container-c/link-b"}},
|
||||
{ID: "id-b", State: "created", Names: []string{"/container-b"}},
|
||||
{ID: "id-a", State: "exited", 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: "running", Names: []string{"/container-c", "/container-c/link-b"}},
|
||||
{ID: "id-b", State: "created", Names: []string{"/container-b"}},
|
||||
{ID: "id-a", State: "exited", 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: "running", 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(container container.Summary) bool { return container.State == "created" },
|
||||
},
|
||||
containers: []container.Summary{
|
||||
{ID: "id-c", State: "running", Names: []string{"/container-c", "/container-c/link-b"}},
|
||||
{ID: "id-b", State: "created", Names: []string{"/container-b"}},
|
||||
{ID: "id-a", State: "exited", 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(container container.Summary) bool { return container.ID == "id-a" },
|
||||
func(container container.Summary) bool { return container.State == "created" },
|
||||
},
|
||||
containers: []container.Summary{
|
||||
{ID: "id-c", State: "running", Names: []string{"/container-c", "/container-c/link-b"}},
|
||||
{ID: "id-b", State: "created", Names: []string{"/container-b"}},
|
||||
{ID: "id-a", State: "created", 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))
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -30,9 +30,9 @@ func NewConfigCommand(dockerCli command.Cli) *cobra.Command {
|
||||
}
|
||||
|
||||
// completeNames offers completion for swarm configs
|
||||
func completeNames(dockerCLI completion.APIClientProvider) completion.ValidArgsFn {
|
||||
func completeNames(dockerCli command.Cli) completion.ValidArgsFn {
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
list, err := dockerCLI.Client().ConfigList(cmd.Context(), types.ConfigListOptions{})
|
||||
list, err := dockerCli.Client().ConfigList(cmd.Context(), types.ConfigListOptions{})
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveError
|
||||
}
|
||||
|
||||
@ -48,10 +48,10 @@ 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()
|
||||
|
||||
var in io.Reader = dockerCLI.In()
|
||||
var in io.Reader = dockerCli.In()
|
||||
if options.File != "-" {
|
||||
file, err := sequential.Open(options.File)
|
||||
if err != nil {
|
||||
@ -78,11 +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
|
||||
}
|
||||
|
||||
@ -2,8 +2,6 @@ package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@ -14,6 +12,7 @@ import (
|
||||
"github.com/docker/cli/internal/test"
|
||||
"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"
|
||||
@ -29,32 +28,29 @@ func TestConfigCreateErrors(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
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) (types.ConfigCreateResponse, error) {
|
||||
return types.ConfigCreateResponse{}, errors.New("error creating config")
|
||||
return types.ConfigCreateResponse{}, errors.Errorf("error creating config")
|
||||
},
|
||||
expectedError: "error creating config",
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.expectedError, func(t *testing.T) {
|
||||
cmd := newConfigCreateCommand(
|
||||
test.NewFakeCli(&fakeClient{
|
||||
configCreateFunc: tc.configCreateFunc,
|
||||
}),
|
||||
)
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
})
|
||||
cmd := newConfigCreateCommand(
|
||||
test.NewFakeCli(&fakeClient{
|
||||
configCreateFunc: tc.configCreateFunc,
|
||||
}),
|
||||
)
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.SetOut(io.Discard)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
@ -64,7 +60,7 @@ func TestConfigCreateWithName(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
configCreateFunc: func(_ context.Context, spec swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
|
||||
if spec.Name != name {
|
||||
return types.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
|
||||
@ -103,7 +99,7 @@ func TestConfigCreateWithLabels(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
configCreateFunc: func(_ context.Context, spec swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
|
||||
if !reflect.DeepEqual(spec, expected) {
|
||||
return types.ConfigCreateResponse{}, fmt.Errorf("expected %+v, got %+v", expected, spec)
|
||||
return types.ConfigCreateResponse{}, errors.Errorf("expected %+v, got %+v", expected, spec)
|
||||
}
|
||||
|
||||
return types.ConfigCreateResponse{
|
||||
@ -129,11 +125,11 @@ func TestConfigCreateWithTemplatingDriver(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
configCreateFunc: func(_ context.Context, spec swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
|
||||
if spec.Name != name {
|
||||
return types.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 types.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 types.ConfigCreateResponse{
|
||||
|
||||
@ -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.22
|
||||
//go:build go1.19
|
||||
|
||||
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"
|
||||
@ -11,6 +10,7 @@ import (
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/cli/internal/test/builders"
|
||||
"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",
|
||||
},
|
||||
@ -61,7 +61,6 @@ func TestConfigInspectErrors(t *testing.T) {
|
||||
assert.Check(t, cmd.Flags().Set(key, value))
|
||||
}
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
}
|
||||
}
|
||||
@ -77,7 +76,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
|
||||
},
|
||||
|
||||
@ -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, types.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"
|
||||
@ -12,6 +11,7 @@ import (
|
||||
"github.com/docker/cli/internal/test/builders"
|
||||
"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"
|
||||
@ -29,7 +29,7 @@ func TestConfigListErrors(t *testing.T) {
|
||||
},
|
||||
{
|
||||
configListFunc: func(_ context.Context, options types.ConfigListOptions) ([]swarm.Config, error) {
|
||||
return []swarm.Config{}, errors.New("error listing configs")
|
||||
return []swarm.Config{}, errors.Errorf("error listing configs")
|
||||
},
|
||||
expectedError: "error listing configs",
|
||||
},
|
||||
@ -42,7 +42,6 @@ func TestConfigListErrors(t *testing.T) {
|
||||
)
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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",
|
||||
},
|
||||
@ -37,7 +37,6 @@ func TestConfigRemoveErrors(t *testing.T) {
|
||||
)
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
}
|
||||
}
|
||||
@ -66,7 +65,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
|
||||
},
|
||||
@ -75,7 +74,6 @@ func TestConfigRemoveContinueAfterError(t *testing.T) {
|
||||
cmd := newConfigRemoveCommand(cli)
|
||||
cmd.SetArgs(names)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
assert.Error(t, cmd.Execute(), "error removing config: foo")
|
||||
assert.Check(t, is.DeepEqual(names, removedConfigs))
|
||||
}
|
||||
|
||||
@ -2,11 +2,13 @@ package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/moby/sys/signal"
|
||||
@ -22,7 +24,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,7 +57,7 @@ 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 {
|
||||
ValidArgsFunction: completion.ContainerNames(dockerCLI, false, func(ctr types.Container) bool {
|
||||
return ctr.State != "paused"
|
||||
}),
|
||||
}
|
||||
@ -72,8 +74,7 @@ func RunAttach(ctx context.Context, dockerCLI command.Cli, containerID string, o
|
||||
apiClient := dockerCLI.Client()
|
||||
|
||||
// request channel to wait for client
|
||||
waitCtx := context.WithoutCancel(ctx)
|
||||
resultC, errC := apiClient.ContainerWait(waitCtx, containerID, "")
|
||||
resultC, errC := apiClient.ContainerWait(ctx, containerID, "")
|
||||
|
||||
c, err := inspectContainerAndCheckState(ctx, apiClient, containerID)
|
||||
if err != nil {
|
||||
@ -104,12 +105,7 @@ func RunAttach(ctx context.Context, dockerCLI command.Cli, containerID string, o
|
||||
|
||||
if opts.Proxy && !c.Config.Tty {
|
||||
sigc := notifyAllSignals()
|
||||
// since we're explicitly setting up signal handling here, and the daemon will
|
||||
// get notified independently of the clients ctx cancellation, we use this context
|
||||
// but without cancellation to avoid ForwardAllSignals from returning
|
||||
// before all signals are forwarded.
|
||||
bgCtx := context.WithoutCancel(ctx)
|
||||
go ForwardAllSignals(bgCtx, apiClient, containerID, sigc)
|
||||
go ForwardAllSignals(ctx, apiClient, containerID, sigc)
|
||||
defer signal.StopCatch(sigc)
|
||||
}
|
||||
|
||||
@ -146,8 +142,7 @@ func RunAttach(ctx context.Context, dockerCLI command.Cli, containerID string, o
|
||||
detachKeys: options.DetachKeys,
|
||||
}
|
||||
|
||||
// if the context was canceled, this was likely intentional and we shouldn't return an error
|
||||
if err := streamer.stream(ctx); err != nil && !errors.Is(err, context.Canceled) {
|
||||
if err := streamer.stream(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -158,7 +153,7 @@ func getExitStatus(errC <-chan error, resultC <-chan container.WaitResponse) err
|
||||
select {
|
||||
case result := <-resultC:
|
||||
if result.Error != nil {
|
||||
return errors.New(result.Error.Message)
|
||||
return fmt.Errorf(result.Error.Message)
|
||||
}
|
||||
if result.StatusCode != 0 {
|
||||
return cli.StatusError{StatusCode: int(result.StatusCode)}
|
||||
|
||||
@ -1,13 +1,15 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/pkg/errors"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
@ -16,75 +18,71 @@ 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 {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cmd := NewAttachCommand(test.NewFakeCli(&fakeClient{inspectFunc: tc.containerInspectFunc}))
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
})
|
||||
cmd := NewAttachCommand(test.NewFakeCli(&fakeClient{inspectFunc: tc.containerInspectFunc}))
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetExitStatus(t *testing.T) {
|
||||
expectedErr := errors.New("unexpected error")
|
||||
var (
|
||||
expectedErr = fmt.Errorf("unexpected error")
|
||||
errC = make(chan error, 1)
|
||||
resultC = make(chan container.WaitResponse, 1)
|
||||
)
|
||||
|
||||
testcases := []struct {
|
||||
result *container.WaitResponse
|
||||
@ -115,17 +113,13 @@ func TestGetExitStatus(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, testcase := range testcases {
|
||||
errC := make(chan error, 1)
|
||||
resultC := make(chan container.WaitResponse, 1)
|
||||
if testcase.err != nil {
|
||||
errC <- testcase.err
|
||||
}
|
||||
if testcase.result != nil {
|
||||
resultC <- *testcase.result
|
||||
}
|
||||
|
||||
err := getExitStatus(errC, resultC)
|
||||
|
||||
if testcase.expectedError == nil {
|
||||
assert.NilError(t, err)
|
||||
} else {
|
||||
|
||||
@ -16,66 +16,59 @@ import (
|
||||
|
||||
type fakeClient struct {
|
||||
client.Client
|
||||
inspectFunc func(string) (container.InspectResponse, error)
|
||||
execInspectFunc func(execID string) (container.ExecInspect, error)
|
||||
execCreateFunc func(containerID string, options container.ExecOptions) (container.ExecCreateResponse, error)
|
||||
inspectFunc func(string) (types.ContainerJSON, error)
|
||||
execInspectFunc func(execID string) (types.ContainerExecInspect, error)
|
||||
execCreateFunc func(containerID string, config types.ExecConfig) (types.IDResponse, error)
|
||||
createContainerFunc func(config *container.Config,
|
||||
hostConfig *container.HostConfig,
|
||||
networkingConfig *network.NetworkingConfig,
|
||||
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)
|
||||
containerStatPathFunc func(containerID, path string) (types.ContainerPathStat, error)
|
||||
containerCopyFromFunc func(containerID, srcPath string) (io.ReadCloser, types.ContainerPathStat, 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
|
||||
containerPruneFunc func(ctx context.Context, pruneFilters filters.Args) (types.ContainersPruneReport, 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 types.ExecConfig) (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) {
|
||||
func (f *fakeClient) ContainerExecInspect(_ context.Context, execID string) (types.ContainerExecInspect, error) {
|
||||
if f.execInspectFunc != nil {
|
||||
return f.execInspectFunc(execID)
|
||||
}
|
||||
return container.ExecInspect{}, nil
|
||||
return types.ContainerExecInspect{}, nil
|
||||
}
|
||||
|
||||
func (*fakeClient) ContainerExecStart(context.Context, string, container.ExecStartOptions) error {
|
||||
func (f *fakeClient) ContainerExecStart(context.Context, string, types.ExecStartCheck) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -100,9 +93,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
|
||||
}
|
||||
@ -114,18 +107,18 @@ func (f *fakeClient) Info(_ context.Context) (system.Info, error) {
|
||||
return system.Info{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerStatPath(_ context.Context, containerID, path string) (container.PathStat, error) {
|
||||
func (f *fakeClient) ContainerStatPath(_ context.Context, containerID, path string) (types.ContainerPathStat, error) {
|
||||
if f.containerStatPathFunc != nil {
|
||||
return f.containerStatPathFunc(containerID, path)
|
||||
}
|
||||
return container.PathStat{}, nil
|
||||
return types.ContainerPathStat{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) CopyFromContainer(_ context.Context, containerID, srcPath string) (io.ReadCloser, container.PathStat, error) {
|
||||
func (f *fakeClient) CopyFromContainer(_ context.Context, containerID, srcPath string) (io.ReadCloser, types.ContainerPathStat, error) {
|
||||
if f.containerCopyFromFunc != nil {
|
||||
return f.containerCopyFromFunc(containerID, srcPath)
|
||||
}
|
||||
return nil, container.PathStat{}, nil
|
||||
return nil, types.ContainerPathStat{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerLogs(_ context.Context, containerID string, options container.LogsOptions) (io.ReadCloser, error) {
|
||||
@ -174,61 +167,9 @@ func (f *fakeClient) ContainerKill(ctx context.Context, containerID, signal stri
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainersPrune(ctx context.Context, pruneFilters filters.Args) (container.PruneReport, error) {
|
||||
func (f *fakeClient) ContainersPrune(ctx context.Context, pruneFilters filters.Args) (types.ContainersPruneReport, error) {
|
||||
if f.containerPruneFunc != nil {
|
||||
return f.containerPruneFunc(ctx, pruneFilters)
|
||||
}
|
||||
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
|
||||
return types.ContainersPruneReport{}, nil
|
||||
}
|
||||
|
||||
@ -1,70 +0,0 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/docker/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,336 +0,0 @@
|
||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
||||
//go:build go1.22
|
||||
// +build go1.22
|
||||
|
||||
package container
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/moby/sys/capability"
|
||||
"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.
|
||||
//
|
||||
// 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
|
||||
})
|
||||
|
||||
// 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", "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",
|
||||
},
|
||||
}
|
||||
|
||||
// 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")
|
||||
var restartPolicies = []string{
|
||||
string(container.RestartPolicyDisabled),
|
||||
string(container.RestartPolicyAlways),
|
||||
string(container.RestartPolicyOnFailure),
|
||||
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() completion.ValidArgsFn {
|
||||
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) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
if len(toComplete) > 0 && strings.HasPrefix("container", toComplete) { //nolint:gocritic // not swapped, matches partly typed "container"
|
||||
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) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
return postfixWith(":", containerNames(dockerCLI, cmd, args, toComplete)), cobra.ShellCompDirectiveNoSpace
|
||||
}
|
||||
}
|
||||
|
||||
// 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) completion.ValidArgsFn {
|
||||
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) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
if len(toComplete) > 0 && strings.HasPrefix("container", toComplete) { //nolint:gocritic // not swapped, matches partly typed "container"
|
||||
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) completion.ValidArgsFn {
|
||||
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)
|
||||
}
|
||||
|
||||
func completeRestartPolicies(cmd *cobra.Command, args []string, toComplete string) (names []string, _ cobra.ShellCompDirective) {
|
||||
return completion.FromList(restartPolicies...)(cmd, args, toComplete)
|
||||
}
|
||||
|
||||
func completeSignals(cmd *cobra.Command, args []string, toComplete string) (names []string, _ cobra.ShellCompDirective) {
|
||||
// TODO(thaJeztah): do we want to provide the full list here, or a subset?
|
||||
signalNames := make([]string, 0, len(signal.SignalMap))
|
||||
for k := range signal.SignalMap {
|
||||
signalNames = append(signalNames, k)
|
||||
}
|
||||
return completion.FromList(signalNames...)(cmd, args, toComplete)
|
||||
}
|
||||
@ -1,134 +0,0 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/cli/internal/test/builders"
|
||||
"github.com/docker/docker/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,8 +15,9 @@ import (
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types"
|
||||
"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"
|
||||
@ -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
|
||||
@ -397,22 +397,22 @@ func copyToContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cpCo
|
||||
}
|
||||
}
|
||||
|
||||
options := container.CopyToContainerOptions{
|
||||
options := types.CopyToContainerOptions{
|
||||
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,30 +433,18 @@ func copyToContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cpCo
|
||||
// so we have to check for a `/` or `.` prefix. Also, in the case of a Windows
|
||||
// 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) {
|
||||
func splitCpArg(arg string) (container, path string) {
|
||||
if system.IsAbs(arg) {
|
||||
// Explicit local absolute path, e.g., `C:\foo` or `/foo`.
|
||||
return "", arg
|
||||
}
|
||||
|
||||
ctr, path, ok := strings.Cut(arg, ":")
|
||||
if !ok || strings.HasPrefix(ctr, ".") {
|
||||
container, path, ok := strings.Cut(arg, ":")
|
||||
if !ok || strings.HasPrefix(container, ".") {
|
||||
// Either there's no `:` in the arg
|
||||
// OR it's an explicit local relative path like `./file:name.txt`.
|
||||
return "", arg
|
||||
}
|
||||
|
||||
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))
|
||||
return container, path
|
||||
}
|
||||
|
||||
@ -9,11 +9,12 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types"
|
||||
"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) {
|
||||
@ -50,39 +51,35 @@ func TestRunCopyWithInvalidArguments(t *testing.T) {
|
||||
func TestRunCopyFromContainerToStdout(t *testing.T) {
|
||||
tarContent := "the tar content"
|
||||
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
containerCopyFromFunc: func(ctr, srcPath string) (io.ReadCloser, container.PathStat, error) {
|
||||
assert.Check(t, is.Equal("container", ctr))
|
||||
return io.NopCloser(strings.NewReader(tarContent)), container.PathStat{}, nil
|
||||
fakeClient := &fakeClient{
|
||||
containerCopyFromFunc: func(container, srcPath string) (io.ReadCloser, types.ContainerPathStat, error) {
|
||||
assert.Check(t, is.Equal("container", container))
|
||||
return io.NopCloser(strings.NewReader(tarContent)), types.ContainerPathStat{}, nil
|
||||
},
|
||||
})
|
||||
err := runCopy(context.TODO(), cli, copyOptions{
|
||||
source: "container:/path",
|
||||
destination: "-",
|
||||
})
|
||||
}
|
||||
options := copyOptions{source: "container:/path", destination: "-"}
|
||||
cli := test.NewFakeCli(fakeClient)
|
||||
err := runCopy(context.TODO(), cli, options)
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, is.Equal(tarContent, cli.OutBuffer().String()))
|
||||
assert.Check(t, is.Equal("", cli.ErrBuffer().String()))
|
||||
}
|
||||
|
||||
func TestRunCopyFromContainerToFilesystem(t *testing.T) {
|
||||
srcDir := fs.NewDir(t, "cp-test",
|
||||
destDir := fs.NewDir(t, "cp-test",
|
||||
fs.WithFile("file1", "content\n"))
|
||||
defer destDir.Remove()
|
||||
|
||||
destDir := fs.NewDir(t, "cp-test")
|
||||
|
||||
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(), archive.Uncompressed)
|
||||
return readCloser, container.PathStat{}, err
|
||||
fakeClient := &fakeClient{
|
||||
containerCopyFromFunc: func(container, srcPath string) (io.ReadCloser, types.ContainerPathStat, error) {
|
||||
assert.Check(t, is.Equal("container", container))
|
||||
readCloser, err := archive.TarWithOptions(destDir.Path(), &archive.TarOptions{})
|
||||
return readCloser, types.ContainerPathStat{}, err
|
||||
},
|
||||
})
|
||||
err := runCopy(context.TODO(), cli, copyOptions{
|
||||
source: "container:/path",
|
||||
destination: destDir.Path(),
|
||||
quiet: true,
|
||||
})
|
||||
}
|
||||
options := copyOptions{source: "container:/path", destination: destDir.Path(), quiet: true}
|
||||
cli := test.NewFakeCli(fakeClient)
|
||||
err := runCopy(context.TODO(), cli, options)
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, is.Equal("", cli.OutBuffer().String()))
|
||||
assert.Check(t, is.Equal("", cli.ErrBuffer().String()))
|
||||
@ -97,17 +94,20 @@ func TestRunCopyFromContainerToFilesystemMissingDestinationDirectory(t *testing.
|
||||
fs.WithFile("file1", "content\n"))
|
||||
defer destDir.Remove()
|
||||
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
containerCopyFromFunc: func(ctr, srcPath string) (io.ReadCloser, container.PathStat, error) {
|
||||
assert.Check(t, is.Equal("container", ctr))
|
||||
fakeClient := &fakeClient{
|
||||
containerCopyFromFunc: func(container, srcPath string) (io.ReadCloser, types.ContainerPathStat, error) {
|
||||
assert.Check(t, is.Equal("container", container))
|
||||
readCloser, err := archive.TarWithOptions(destDir.Path(), &archive.TarOptions{})
|
||||
return readCloser, container.PathStat{}, err
|
||||
return readCloser, types.ContainerPathStat{}, err
|
||||
},
|
||||
})
|
||||
err := runCopy(context.TODO(), cli, copyOptions{
|
||||
}
|
||||
|
||||
options := copyOptions{
|
||||
source: "container:/path",
|
||||
destination: destDir.Join("missing", "foo"),
|
||||
})
|
||||
}
|
||||
cli := test.NewFakeCli(fakeClient)
|
||||
err := runCopy(context.TODO(), cli, options)
|
||||
assert.ErrorContains(t, err, destDir.Join("missing"))
|
||||
}
|
||||
|
||||
@ -115,11 +115,12 @@ func TestRunCopyToContainerFromFileWithTrailingSlash(t *testing.T) {
|
||||
srcFile := fs.NewFile(t, t.Name())
|
||||
defer srcFile.Remove()
|
||||
|
||||
cli := test.NewFakeCli(&fakeClient{})
|
||||
err := runCopy(context.TODO(), cli, copyOptions{
|
||||
options := copyOptions{
|
||||
source: srcFile.Path() + string(os.PathSeparator),
|
||||
destination: "container:/path",
|
||||
})
|
||||
}
|
||||
cli := test.NewFakeCli(&fakeClient{})
|
||||
err := runCopy(context.TODO(), cli, options)
|
||||
|
||||
expectedError := "not a directory"
|
||||
if runtime.GOOS == "windows" {
|
||||
@ -129,11 +130,12 @@ func TestRunCopyToContainerFromFileWithTrailingSlash(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRunCopyToContainerSourceDoesNotExist(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{})
|
||||
err := runCopy(context.TODO(), cli, copyOptions{
|
||||
options := copyOptions{
|
||||
source: "/does/not/exist",
|
||||
destination: "container:/path",
|
||||
})
|
||||
}
|
||||
cli := test.NewFakeCli(&fakeClient{})
|
||||
err := runCopy(context.TODO(), cli, options)
|
||||
expected := "no such file or directory"
|
||||
if runtime.GOOS == "windows" {
|
||||
expected = "cannot find the file specified"
|
||||
@ -151,7 +153,7 @@ func TestSplitCpArg(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
doc: "absolute path with colon",
|
||||
os: "unix",
|
||||
os: "linux",
|
||||
path: "/abs/path:withcolon",
|
||||
expectedPath: "/abs/path:withcolon",
|
||||
},
|
||||
@ -178,28 +180,21 @@ func TestSplitCpArg(t *testing.T) {
|
||||
expectedContainer: "container",
|
||||
},
|
||||
}
|
||||
for _, tc := range testcases {
|
||||
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")
|
||||
}
|
||||
for _, testcase := range testcases {
|
||||
t.Run(testcase.doc, func(t *testing.T) {
|
||||
skip.If(t, testcase.os != "" && testcase.os != runtime.GOOS)
|
||||
|
||||
ctr, path := splitCpArg(tc.path)
|
||||
assert.Check(t, is.Equal(tc.expectedContainer, ctr))
|
||||
assert.Check(t, is.Equal(tc.expectedPath, path))
|
||||
container, path := splitCpArg(testcase.path)
|
||||
assert.Check(t, is.Equal(testcase.expectedContainer, container))
|
||||
assert.Check(t, is.Equal(testcase.expectedPath, path))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunCopyFromContainerToFilesystemIrregularDestination(t *testing.T) {
|
||||
options := copyOptions{source: "container:/dev/null", destination: "/dev/random"}
|
||||
cli := test.NewFakeCli(nil)
|
||||
err := runCopy(context.TODO(), cli, copyOptions{
|
||||
source: "container:/dev/null",
|
||||
destination: "/dev/random",
|
||||
})
|
||||
err := runCopy(context.TODO(), cli, options)
|
||||
assert.Assert(t, err != nil)
|
||||
expected := `"/dev/random" must be a directory or a regular file`
|
||||
assert.ErrorContains(t, err, expected)
|
||||
|
||||
@ -13,13 +13,13 @@ import (
|
||||
"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/internal/jsonstream"
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/docker/cli/opts"
|
||||
"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"
|
||||
@ -60,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()
|
||||
@ -77,25 +77,13 @@ func NewCreateCommand(dockerCli command.Cli) *cobra.Command {
|
||||
command.AddPlatformFlag(flags, &options.platform)
|
||||
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)
|
||||
})
|
||||
|
||||
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.GetAll()))
|
||||
newEnv := []string{}
|
||||
@ -109,16 +97,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 {
|
||||
return cli.StatusError{
|
||||
Status: withHelp(err, "create").Error(),
|
||||
StatusCode: 125,
|
||||
}
|
||||
reportError(dockerCli.Err(), "create", err.Error(), true)
|
||||
return cli.StatusError{StatusCode: 125}
|
||||
}
|
||||
id, err := createContainer(ctx, dockerCli, containerCfg, options)
|
||||
if err != nil {
|
||||
@ -146,9 +130,9 @@ func pullImage(ctx context.Context, dockerCli command.Cli, img string, options *
|
||||
|
||||
out := dockerCli.Err()
|
||||
if options.quiet {
|
||||
out = streams.NewOut(io.Discard)
|
||||
out = io.Discard
|
||||
}
|
||||
return jsonstream.Display(ctx, responseBody, out)
|
||||
return jsonmessage.DisplayJSONMessagesToStream(responseBody, streams.NewOut(out), nil)
|
||||
}
|
||||
|
||||
type cidFile struct {
|
||||
@ -274,7 +258,7 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *c
|
||||
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 {
|
||||
@ -292,7 +276,7 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *c
|
||||
}
|
||||
|
||||
for _, w := range response.Warnings {
|
||||
_, _ = fmt.Fprintln(dockerCli.Err(), "WARNING:", w)
|
||||
_, _ = fmt.Fprintf(dockerCli.Err(), "WARNING: %s\n", w)
|
||||
}
|
||||
err = containerIDFile.Write(response.ID)
|
||||
return response.ID, err
|
||||
@ -300,7 +284,7 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *c
|
||||
|
||||
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.")
|
||||
fmt.Fprintln(stderr, "WARNING: Disabling the OOM killer on containers without setting a '-m/--memory' limit may be dangerous.")
|
||||
}
|
||||
}
|
||||
|
||||
@ -309,7 +293,7 @@ func warnOnOomKillDisable(hostConfig container.HostConfig, stderr io.Writer) {
|
||||
func warnOnLocalhostDNS(hostConfig container.HostConfig, stderr io.Writer) {
|
||||
for _, dnsIP := range hostConfig.DNS {
|
||||
if isLocalhost(dnsIP) {
|
||||
_, _ = fmt.Fprintf(stderr, "WARNING: Localhost DNS setting (--dns=%s) may fail in containers.\n", dnsIP)
|
||||
fmt.Fprintf(stderr, "WARNING: Localhost DNS setting (--dns=%s) may fail in containers.\n", dnsIP)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ package container
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"runtime"
|
||||
@ -113,6 +114,7 @@ func TestCreateContainerImagePullPolicy(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(tc.PullPolicy, func(t *testing.T) {
|
||||
pullCounter := 0
|
||||
|
||||
@ -132,7 +134,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 +177,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 +190,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,6 +223,7 @@ func TestNewCreateCommandWithContentTrustErrors(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
fakeCLI := test.NewFakeCli(&fakeClient{
|
||||
createContainerFunc: func(config *container.Config,
|
||||
hostConfig *container.HostConfig,
|
||||
@ -255,13 +231,12 @@ func TestNewCreateCommandWithContentTrustErrors(t *testing.T) {
|
||||
platform *specs.Platform,
|
||||
containerName string,
|
||||
) (container.CreateResponse, error) {
|
||||
return container.CreateResponse{}, errors.New("shouldn't try to pull image")
|
||||
return container.CreateResponse{}, fmt.Errorf("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)
|
||||
@ -308,8 +283,9 @@ 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,
|
||||
@ -319,15 +295,15 @@ func TestNewCreateCommandWithWarnings(t *testing.T) {
|
||||
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 {
|
||||
golden.Assert(t, fakeCLI.ErrBuffer().String(), tc.name+".golden")
|
||||
golden.Assert(t, cli.ErrBuffer().String(), tc.name+".golden")
|
||||
} else {
|
||||
assert.Equal(t, fakeCLI.ErrBuffer().String(), "")
|
||||
assert.Equal(t, cli.ErrBuffer().String(), "")
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -380,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" }
|
||||
|
||||
@ -1,93 +0,0 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/docker/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)
|
||||
}
|
||||
|
||||
func TestRunDiffEmptyContainerError(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{})
|
||||
|
||||
cmd := NewDiffCommand(cli)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
|
||||
containerID := ""
|
||||
cmd.SetArgs([]string{containerID})
|
||||
|
||||
err := cmd.Execute()
|
||||
assert.Error(t, err, "Container name cannot be empty")
|
||||
}
|
||||
@ -4,14 +4,15 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/docker/docker/api/types"
|
||||
apiclient "github.com/docker/docker/client"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
@ -42,18 +43,19 @@ func NewExecOptions() ExecOptions {
|
||||
// NewExecCommand creates a new cobra.Command for `docker exec`
|
||||
func NewExecCommand(dockerCli command.Cli) *cobra.Command {
|
||||
options := NewExecOptions()
|
||||
var container string
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "exec [OPTIONS] CONTAINER COMMAND [ARG...]",
|
||||
Short: "Execute a command in a running container",
|
||||
Args: cli.RequiresMinArgs(2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
containerIDorName := args[0]
|
||||
container = args[0]
|
||||
options.Command = args[1:]
|
||||
return RunExec(cmd.Context(), dockerCli, containerIDorName, options)
|
||||
return RunExec(cmd.Context(), dockerCli, container, options)
|
||||
},
|
||||
ValidArgsFunction: completion.ContainerNames(dockerCli, false, func(ctr container.Summary) bool {
|
||||
return ctr.State != "paused"
|
||||
ValidArgsFunction: completion.ContainerNames(dockerCli, false, func(container types.Container) bool {
|
||||
return container.State != "paused"
|
||||
}),
|
||||
Annotations: map[string]string{
|
||||
"category-top": "2",
|
||||
@ -77,37 +79,47 @@ func NewExecCommand(dockerCli command.Cli) *cobra.Command {
|
||||
flags.StringVarP(&options.Workdir, "workdir", "w", "", "Working directory inside the container")
|
||||
flags.SetAnnotation("workdir", "version", []string{"1.35"})
|
||||
|
||||
_ = cmd.RegisterFlagCompletionFunc("env", completion.EnvVarNames)
|
||||
_ = cmd.RegisterFlagCompletionFunc("env-file", completion.FileNames)
|
||||
cmd.RegisterFlagCompletionFunc(
|
||||
"env",
|
||||
func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
return os.Environ(), cobra.ShellCompDirectiveNoFileComp
|
||||
},
|
||||
)
|
||||
cmd.RegisterFlagCompletionFunc(
|
||||
"env-file",
|
||||
func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
return nil, cobra.ShellCompDirectiveDefault // _filedir
|
||||
},
|
||||
)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// 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, container string, options ExecOptions) error {
|
||||
execConfig, err := parseExec(options, dockerCli.ConfigFile())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
apiClient := dockerCLI.Client()
|
||||
client := 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
|
||||
// there's no easy way to clean those up). But also in order to make "not
|
||||
// exist" errors take precedence we do a dummy inspect first.
|
||||
if _, err := apiClient.ContainerInspect(ctx, containerIDorName); err != nil {
|
||||
if _, err := client.ContainerInspect(ctx, container); err != nil {
|
||||
return err
|
||||
}
|
||||
if !execOptions.Detach {
|
||||
if err := dockerCLI.In().CheckTty(execOptions.AttachStdin, execOptions.Tty); err != nil {
|
||||
if !execConfig.Detach {
|
||||
if err := dockerCli.In().CheckTty(execConfig.AttachStdin, execConfig.Tty); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
fillConsoleSize(execOptions, dockerCLI)
|
||||
fillConsoleSize(execConfig, dockerCli)
|
||||
|
||||
response, err := apiClient.ContainerExecCreate(ctx, containerIDorName, *execOptions)
|
||||
response, err := client.ContainerExecCreate(ctx, container, *execConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -117,50 +129,52 @@ func RunExec(ctx context.Context, dockerCLI command.Cli, containerIDorName strin
|
||||
return errors.New("exec ID empty")
|
||||
}
|
||||
|
||||
if execOptions.Detach {
|
||||
return apiClient.ContainerExecStart(ctx, execID, container.ExecStartOptions{
|
||||
Detach: execOptions.Detach,
|
||||
Tty: execOptions.Tty,
|
||||
ConsoleSize: execOptions.ConsoleSize,
|
||||
})
|
||||
if execConfig.Detach {
|
||||
execStartCheck := types.ExecStartCheck{
|
||||
Detach: execConfig.Detach,
|
||||
Tty: execConfig.Tty,
|
||||
ConsoleSize: execConfig.ConsoleSize,
|
||||
}
|
||||
return client.ContainerExecStart(ctx, execID, execStartCheck)
|
||||
}
|
||||
return interactiveExec(ctx, dockerCLI, execOptions, execID)
|
||||
return interactiveExec(ctx, dockerCli, execConfig, execID)
|
||||
}
|
||||
|
||||
func fillConsoleSize(execOptions *container.ExecOptions, dockerCli command.Cli) {
|
||||
if execOptions.Tty {
|
||||
func fillConsoleSize(execConfig *types.ExecConfig, dockerCli command.Cli) {
|
||||
if execConfig.Tty {
|
||||
height, width := dockerCli.Out().GetTtySize()
|
||||
execOptions.ConsoleSize = &[2]uint{height, width}
|
||||
execConfig.ConsoleSize = &[2]uint{height, width}
|
||||
}
|
||||
}
|
||||
|
||||
func interactiveExec(ctx context.Context, dockerCli command.Cli, execOptions *container.ExecOptions, execID string) error {
|
||||
func interactiveExec(ctx context.Context, dockerCli command.Cli, execConfig *types.ExecConfig, execID string) error {
|
||||
// Interactive exec requested.
|
||||
var (
|
||||
out, stderr io.Writer
|
||||
in io.ReadCloser
|
||||
)
|
||||
|
||||
if execOptions.AttachStdin {
|
||||
if execConfig.AttachStdin {
|
||||
in = dockerCli.In()
|
||||
}
|
||||
if execOptions.AttachStdout {
|
||||
if execConfig.AttachStdout {
|
||||
out = dockerCli.Out()
|
||||
}
|
||||
if execOptions.AttachStderr {
|
||||
if execOptions.Tty {
|
||||
if execConfig.AttachStderr {
|
||||
if execConfig.Tty {
|
||||
stderr = dockerCli.Out()
|
||||
} else {
|
||||
stderr = dockerCli.Err()
|
||||
}
|
||||
}
|
||||
fillConsoleSize(execOptions, dockerCli)
|
||||
fillConsoleSize(execConfig, dockerCli)
|
||||
|
||||
apiClient := dockerCli.Client()
|
||||
resp, err := apiClient.ContainerExecAttach(ctx, execID, container.ExecAttachOptions{
|
||||
Tty: execOptions.Tty,
|
||||
ConsoleSize: execOptions.ConsoleSize,
|
||||
})
|
||||
client := dockerCli.Client()
|
||||
execStartCheck := types.ExecStartCheck{
|
||||
Tty: execConfig.Tty,
|
||||
ConsoleSize: execConfig.ConsoleSize,
|
||||
}
|
||||
resp, err := client.ContainerExecAttach(ctx, execID, execStartCheck)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -177,17 +191,17 @@ func interactiveExec(ctx context.Context, dockerCli command.Cli, execOptions *co
|
||||
outputStream: out,
|
||||
errorStream: stderr,
|
||||
resp: resp,
|
||||
tty: execOptions.Tty,
|
||||
detachKeys: execOptions.DetachKeys,
|
||||
tty: execConfig.Tty,
|
||||
detachKeys: execConfig.DetachKeys,
|
||||
}
|
||||
|
||||
return streamer.stream(ctx)
|
||||
}()
|
||||
}()
|
||||
|
||||
if execOptions.Tty && dockerCli.In().IsTerminal() {
|
||||
if execConfig.Tty && dockerCli.In().IsTerminal() {
|
||||
if err := MonitorTtySize(ctx, dockerCli, execID, true); err != nil {
|
||||
_, _ = fmt.Fprintln(dockerCli.Err(), "Error monitoring TTY size:", err)
|
||||
fmt.Fprintln(dockerCli.Err(), "Error monitoring TTY size:", err)
|
||||
}
|
||||
}
|
||||
|
||||
@ -196,14 +210,14 @@ func interactiveExec(ctx context.Context, dockerCli command.Cli, execOptions *co
|
||||
return err
|
||||
}
|
||||
|
||||
return getExecExitStatus(ctx, apiClient, execID)
|
||||
return getExecExitStatus(ctx, client, execID)
|
||||
}
|
||||
|
||||
func getExecExitStatus(ctx context.Context, apiClient client.ContainerAPIClient, execID string) error {
|
||||
resp, err := apiClient.ContainerExecInspect(ctx, execID)
|
||||
func getExecExitStatus(ctx context.Context, client apiclient.ContainerAPIClient, execID string) error {
|
||||
resp, err := client.ContainerExecInspect(ctx, execID)
|
||||
if err != nil {
|
||||
// If we can't connect, then the daemon probably died.
|
||||
if !client.IsErrConnectionFailed(err) {
|
||||
if !apiclient.IsErrConnectionFailed(err) {
|
||||
return err
|
||||
}
|
||||
return cli.StatusError{StatusCode: -1}
|
||||
@ -217,8 +231,8 @@ func getExecExitStatus(ctx context.Context, apiClient client.ContainerAPIClient,
|
||||
|
||||
// parseExec parses the specified args for the specified command and generates
|
||||
// an ExecConfig from it.
|
||||
func parseExec(execOpts ExecOptions, configFile *configfile.ConfigFile) (*container.ExecOptions, error) {
|
||||
execOptions := &container.ExecOptions{
|
||||
func parseExec(execOpts ExecOptions, configFile *configfile.ConfigFile) (*types.ExecConfig, error) {
|
||||
execConfig := &types.ExecConfig{
|
||||
User: execOpts.User,
|
||||
Privileged: execOpts.Privileged,
|
||||
Tty: execOpts.TTY,
|
||||
@ -229,23 +243,23 @@ func parseExec(execOpts ExecOptions, configFile *configfile.ConfigFile) (*contai
|
||||
|
||||
// collect all the environment variables for the container
|
||||
var err error
|
||||
if execOptions.Env, err = opts.ReadKVEnvStrings(execOpts.EnvFile.GetAll(), execOpts.Env.GetAll()); err != nil {
|
||||
if execConfig.Env, err = opts.ReadKVEnvStrings(execOpts.EnvFile.GetAll(), execOpts.Env.GetAll()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If -d is not set, attach to everything by default
|
||||
if !execOpts.Detach {
|
||||
execOptions.AttachStdout = true
|
||||
execOptions.AttachStderr = true
|
||||
execConfig.AttachStdout = true
|
||||
execConfig.AttachStderr = true
|
||||
if execOpts.Interactive {
|
||||
execOptions.AttachStdin = true
|
||||
execConfig.AttachStdin = true
|
||||
}
|
||||
}
|
||||
|
||||
if execOpts.DetachKeys != "" {
|
||||
execOptions.DetachKeys = execOpts.DetachKeys
|
||||
execConfig.DetachKeys = execOpts.DetachKeys
|
||||
} else {
|
||||
execOptions.DetachKeys = configFile.DetachKeys
|
||||
execConfig.DetachKeys = configFile.DetachKeys
|
||||
}
|
||||
return execOptions, nil
|
||||
return execConfig, nil
|
||||
}
|
||||
|
||||
@ -2,7 +2,6 @@ package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"testing"
|
||||
@ -11,7 +10,8 @@ import (
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/pkg/errors"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
"gotest.tools/v3/fs"
|
||||
@ -37,10 +37,10 @@ TWO=2
|
||||
testcases := []struct {
|
||||
options ExecOptions
|
||||
configFile configfile.ConfigFile
|
||||
expected container.ExecOptions
|
||||
expected types.ExecConfig
|
||||
}{
|
||||
{
|
||||
expected: container.ExecOptions{
|
||||
expected: types.ExecConfig{
|
||||
Cmd: []string{"command"},
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
@ -48,7 +48,7 @@ TWO=2
|
||||
options: withDefaultOpts(ExecOptions{}),
|
||||
},
|
||||
{
|
||||
expected: container.ExecOptions{
|
||||
expected: types.ExecConfig{
|
||||
Cmd: []string{"command1", "command2"},
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
@ -63,7 +63,7 @@ TWO=2
|
||||
TTY: true,
|
||||
User: "uid",
|
||||
}),
|
||||
expected: container.ExecOptions{
|
||||
expected: types.ExecConfig{
|
||||
User: "uid",
|
||||
AttachStdin: true,
|
||||
AttachStdout: true,
|
||||
@ -74,7 +74,7 @@ TWO=2
|
||||
},
|
||||
{
|
||||
options: withDefaultOpts(ExecOptions{Detach: true}),
|
||||
expected: container.ExecOptions{
|
||||
expected: types.ExecConfig{
|
||||
Detach: true,
|
||||
Cmd: []string{"command"},
|
||||
},
|
||||
@ -85,7 +85,7 @@ TWO=2
|
||||
Interactive: true,
|
||||
Detach: true,
|
||||
}),
|
||||
expected: container.ExecOptions{
|
||||
expected: types.ExecConfig{
|
||||
Detach: true,
|
||||
Tty: true,
|
||||
Cmd: []string{"command"},
|
||||
@ -94,7 +94,7 @@ TWO=2
|
||||
{
|
||||
options: withDefaultOpts(ExecOptions{Detach: true}),
|
||||
configFile: configfile.ConfigFile{DetachKeys: "de"},
|
||||
expected: container.ExecOptions{
|
||||
expected: types.ExecConfig{
|
||||
Cmd: []string{"command"},
|
||||
DetachKeys: "de",
|
||||
Detach: true,
|
||||
@ -106,14 +106,14 @@ TWO=2
|
||||
DetachKeys: "ab",
|
||||
}),
|
||||
configFile: configfile.ConfigFile{DetachKeys: "de"},
|
||||
expected: container.ExecOptions{
|
||||
expected: types.ExecConfig{
|
||||
Cmd: []string{"command"},
|
||||
DetachKeys: "ab",
|
||||
Detach: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
expected: container.ExecOptions{
|
||||
expected: types.ExecConfig{
|
||||
Cmd: []string{"command"},
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
@ -126,7 +126,7 @@ TWO=2
|
||||
}(),
|
||||
},
|
||||
{
|
||||
expected: container.ExecOptions{
|
||||
expected: types.ExecConfig{
|
||||
Cmd: []string{"command"},
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
@ -161,7 +161,7 @@ func TestRunExec(t *testing.T) {
|
||||
testcases := []struct {
|
||||
doc string
|
||||
options ExecOptions
|
||||
client *fakeClient
|
||||
client fakeClient
|
||||
expectedError string
|
||||
expectedOut string
|
||||
expectedErr string
|
||||
@ -171,14 +171,14 @@ func TestRunExec(t *testing.T) {
|
||||
options: withDefaultOpts(ExecOptions{
|
||||
Detach: true,
|
||||
}),
|
||||
client: &fakeClient{execCreateFunc: execCreateWithID},
|
||||
client: fakeClient{execCreateFunc: execCreateWithID},
|
||||
},
|
||||
{
|
||||
doc: "inspect error",
|
||||
options: NewExecOptions(),
|
||||
client: &fakeClient{
|
||||
inspectFunc: func(string) (container.InspectResponse, error) {
|
||||
return container.InspectResponse{}, errors.New("failed inspect")
|
||||
client: fakeClient{
|
||||
inspectFunc: func(string) (types.ContainerJSON, error) {
|
||||
return types.ContainerJSON{}, errors.New("failed inspect")
|
||||
},
|
||||
},
|
||||
expectedError: "failed inspect",
|
||||
@ -187,13 +187,12 @@ func TestRunExec(t *testing.T) {
|
||||
doc: "missing exec ID",
|
||||
options: NewExecOptions(),
|
||||
expectedError: "exec ID empty",
|
||||
client: &fakeClient{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testcase := range testcases {
|
||||
t.Run(testcase.doc, func(t *testing.T) {
|
||||
fakeCLI := test.NewFakeCli(testcase.client)
|
||||
fakeCLI := test.NewFakeCli(&testcase.client)
|
||||
|
||||
err := RunExec(context.TODO(), fakeCLI, "thecontainer", testcase.options)
|
||||
if testcase.expectedError != "" {
|
||||
@ -207,8 +206,8 @@ func TestRunExec(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func execCreateWithID(_ string, _ container.ExecOptions) (container.ExecCreateResponse, error) {
|
||||
return container.ExecCreateResponse{ID: "execid"}, nil
|
||||
func execCreateWithID(_ string, _ types.ExecConfig) (types.IDResponse, error) {
|
||||
return types.IDResponse{ID: "execid"}, nil
|
||||
}
|
||||
|
||||
func TestGetExecExitStatus(t *testing.T) {
|
||||
@ -236,9 +235,9 @@ func TestGetExecExitStatus(t *testing.T) {
|
||||
|
||||
for _, testcase := range testcases {
|
||||
client := &fakeClient{
|
||||
execInspectFunc: func(id string) (container.ExecInspect, error) {
|
||||
execInspectFunc: func(id string) (types.ContainerExecInspect, error) {
|
||||
assert.Check(t, is.Equal(execID, id))
|
||||
return container.ExecInspect{ExitCode: testcase.exitCode}, testcase.inspectError
|
||||
return types.ContainerExecInspect{ExitCode: testcase.exitCode}, testcase.inspectError
|
||||
},
|
||||
}
|
||||
err := getExecExitStatus(context.Background(), client, execID)
|
||||
@ -251,14 +250,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")
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -39,7 +39,6 @@ func TestContainerExportOutputToIrregularFile(t *testing.T) {
|
||||
})
|
||||
cmd := NewExportCommand(cli)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
cmd.SetArgs([]string{"-o", "/dev/random", "container"})
|
||||
|
||||
err := cmd.Execute()
|
||||
|
||||
@ -47,6 +47,7 @@ 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
|
||||
|
||||
@ -22,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
|
||||
@ -171,7 +169,7 @@ func (c *statsContext) Name() string {
|
||||
if len(c.s.Name) > 1 {
|
||||
return c.s.Name[1:]
|
||||
}
|
||||
return noValue
|
||||
return "--"
|
||||
}
|
||||
|
||||
func (c *statsContext) ID() string {
|
||||
@ -183,7 +181,7 @@ func (c *statsContext) ID() string {
|
||||
|
||||
func (c *statsContext) CPUPerc() string {
|
||||
if c.s.IsInvalid {
|
||||
return noValue
|
||||
return "--"
|
||||
}
|
||||
return formatPercentage(c.s.CPUPercentage)
|
||||
}
|
||||
@ -200,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)
|
||||
}
|
||||
|
||||
@ -178,6 +178,7 @@ 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
|
||||
@ -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)
|
||||
|
||||
@ -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.22
|
||||
//go:build go1.19
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
@ -37,25 +38,23 @@ func NewKillCommand(dockerCli command.Cli) *cobra.Command {
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.StringVarP(&opts.signal, "signal", "s", "", "Signal to send to the container")
|
||||
|
||||
_ = cmd.RegisterFlagCompletionFunc("signal", completeSignals)
|
||||
|
||||
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"))
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
@ -9,6 +9,7 @@ import (
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/cli/internal/test/builders"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
@ -127,8 +128,9 @@ func TestContainerListBuildContainerListOptions(t *testing.T) {
|
||||
|
||||
func TestContainerListErrors(t *testing.T) {
|
||||
testCases := []struct {
|
||||
args []string
|
||||
flags map[string]string
|
||||
containerListFunc func(container.ListOptions) ([]container.Summary, error)
|
||||
containerListFunc func(container.ListOptions) ([]types.Container, error)
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
@ -144,8 +146,8 @@ func TestContainerListErrors(t *testing.T) {
|
||||
expectedError: `wrong number of args for join`,
|
||||
},
|
||||
{
|
||||
containerListFunc: func(_ container.ListOptions) ([]container.Summary, error) {
|
||||
return nil, errors.New("error listing containers")
|
||||
containerListFunc: func(_ container.ListOptions) ([]types.Container, error) {
|
||||
return nil, fmt.Errorf("error listing containers")
|
||||
},
|
||||
expectedError: "error listing containers",
|
||||
},
|
||||
@ -156,20 +158,19 @@ func TestContainerListErrors(t *testing.T) {
|
||||
containerListFunc: tc.containerListFunc,
|
||||
}),
|
||||
)
|
||||
cmd.SetArgs(tc.args)
|
||||
for key, value := range tc.flags {
|
||||
assert.Check(t, cmd.Flags().Set(key, value))
|
||||
}
|
||||
cmd.SetArgs([]string{})
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContainerListWithoutFormat(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
containerListFunc: func(_ container.ListOptions) ([]container.Summary, error) {
|
||||
return []container.Summary{
|
||||
containerListFunc: func(_ container.ListOptions) ([]types.Container, error) {
|
||||
return []types.Container{
|
||||
*builders.Container("c1"),
|
||||
*builders.Container("c2", builders.WithName("foo")),
|
||||
*builders.Container("c3", builders.WithPort(80, 80, builders.TCP), builders.WithPort(81, 81, builders.TCP), builders.WithPort(82, 82, builders.TCP)),
|
||||
@ -179,26 +180,20 @@ func TestContainerListWithoutFormat(t *testing.T) {
|
||||
},
|
||||
})
|
||||
cmd := newListCommand(cli)
|
||||
cmd.SetArgs([]string{})
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
assert.NilError(t, cmd.Execute())
|
||||
golden.Assert(t, cli.OutBuffer().String(), "container-list-without-format.golden")
|
||||
}
|
||||
|
||||
func TestContainerListNoTrunc(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
containerListFunc: func(_ container.ListOptions) ([]container.Summary, error) {
|
||||
return []container.Summary{
|
||||
containerListFunc: func(_ container.ListOptions) ([]types.Container, error) {
|
||||
return []types.Container{
|
||||
*builders.Container("c1"),
|
||||
*builders.Container("c2", builders.WithName("foo/bar")),
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
cmd := newListCommand(cli)
|
||||
cmd.SetArgs([]string{})
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
assert.Check(t, cmd.Flags().Set("no-trunc", "true"))
|
||||
assert.NilError(t, cmd.Execute())
|
||||
golden.Assert(t, cli.OutBuffer().String(), "container-list-without-format-no-trunc.golden")
|
||||
@ -207,17 +202,14 @@ func TestContainerListNoTrunc(t *testing.T) {
|
||||
// Test for GitHub issue docker/docker#21772
|
||||
func TestContainerListNamesMultipleTime(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
containerListFunc: func(_ container.ListOptions) ([]container.Summary, error) {
|
||||
return []container.Summary{
|
||||
containerListFunc: func(_ container.ListOptions) ([]types.Container, error) {
|
||||
return []types.Container{
|
||||
*builders.Container("c1"),
|
||||
*builders.Container("c2", builders.WithName("foo/bar")),
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
cmd := newListCommand(cli)
|
||||
cmd.SetArgs([]string{})
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
assert.Check(t, cmd.Flags().Set("format", "{{.Names}} {{.Names}}"))
|
||||
assert.NilError(t, cmd.Execute())
|
||||
golden.Assert(t, cli.OutBuffer().String(), "container-list-format-name-name.golden")
|
||||
@ -226,17 +218,14 @@ func TestContainerListNamesMultipleTime(t *testing.T) {
|
||||
// Test for GitHub issue docker/docker#30291
|
||||
func TestContainerListFormatTemplateWithArg(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
containerListFunc: func(_ container.ListOptions) ([]container.Summary, error) {
|
||||
return []container.Summary{
|
||||
containerListFunc: func(_ container.ListOptions) ([]types.Container, error) {
|
||||
return []types.Container{
|
||||
*builders.Container("c1", builders.WithLabel("some.label", "value")),
|
||||
*builders.Container("c2", builders.WithName("foo/bar"), builders.WithLabel("foo", "bar")),
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
cmd := newListCommand(cli)
|
||||
cmd.SetArgs([]string{})
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
assert.Check(t, cmd.Flags().Set("format", `{{.Names}} {{.Label "some.label"}}`))
|
||||
assert.NilError(t, cmd.Execute())
|
||||
golden.Assert(t, cli.OutBuffer().String(), "container-list-format-with-arg.golden")
|
||||
@ -277,17 +266,15 @@ func TestContainerListFormatSizeSetsOption(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(tc.doc, func(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
containerListFunc: func(options container.ListOptions) ([]container.Summary, error) {
|
||||
containerListFunc: func(options container.ListOptions) ([]types.Container, error) {
|
||||
assert.Check(t, is.Equal(options.Size, tc.sizeExpected))
|
||||
return []container.Summary{}, nil
|
||||
return []types.Container{}, nil
|
||||
},
|
||||
})
|
||||
cmd := newListCommand(cli)
|
||||
cmd.SetArgs([]string{})
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
assert.Check(t, cmd.Flags().Set("format", tc.format))
|
||||
if tc.sizeFlag != "" {
|
||||
assert.Check(t, cmd.Flags().Set("size", tc.sizeFlag))
|
||||
@ -299,8 +286,8 @@ func TestContainerListFormatSizeSetsOption(t *testing.T) {
|
||||
|
||||
func TestContainerListWithConfigFormat(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
containerListFunc: func(_ container.ListOptions) ([]container.Summary, error) {
|
||||
return []container.Summary{
|
||||
containerListFunc: func(_ container.ListOptions) ([]types.Container, error) {
|
||||
return []types.Container{
|
||||
*builders.Container("c1", builders.WithLabel("some.label", "value"), builders.WithSize(10700000)),
|
||||
*builders.Container("c2", builders.WithName("foo/bar"), builders.WithLabel("foo", "bar"), builders.WithSize(3200000)),
|
||||
}, nil
|
||||
@ -310,17 +297,14 @@ func TestContainerListWithConfigFormat(t *testing.T) {
|
||||
PsFormat: "{{ .Names }} {{ .Image }} {{ .Labels }} {{ .Size}}",
|
||||
})
|
||||
cmd := newListCommand(cli)
|
||||
cmd.SetArgs([]string{})
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
assert.NilError(t, cmd.Execute())
|
||||
golden.Assert(t, cli.OutBuffer().String(), "container-list-with-config-format.golden")
|
||||
}
|
||||
|
||||
func TestContainerListWithFormat(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
containerListFunc: func(_ container.ListOptions) ([]container.Summary, error) {
|
||||
return []container.Summary{
|
||||
containerListFunc: func(_ container.ListOptions) ([]types.Container, error) {
|
||||
return []types.Container{
|
||||
*builders.Container("c1", builders.WithLabel("some.label", "value")),
|
||||
*builders.Container("c2", builders.WithName("foo/bar"), builders.WithLabel("foo", "bar")),
|
||||
}, nil
|
||||
@ -330,9 +314,6 @@ func TestContainerListWithFormat(t *testing.T) {
|
||||
t.Run("with format", func(t *testing.T) {
|
||||
cli.OutBuffer().Reset()
|
||||
cmd := newListCommand(cli)
|
||||
cmd.SetArgs([]string{})
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
assert.Check(t, cmd.Flags().Set("format", "{{ .Names }} {{ .Image }} {{ .Labels }}"))
|
||||
assert.NilError(t, cmd.Execute())
|
||||
golden.Assert(t, cli.OutBuffer().String(), "container-list-with-format.golden")
|
||||
@ -341,9 +322,6 @@ func TestContainerListWithFormat(t *testing.T) {
|
||||
t.Run("with format and quiet", func(t *testing.T) {
|
||||
cli.OutBuffer().Reset()
|
||||
cmd := newListCommand(cli)
|
||||
cmd.SetArgs([]string{})
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
assert.Check(t, cmd.Flags().Set("format", "{{ .Names }} {{ .Image }} {{ .Labels }}"))
|
||||
assert.Check(t, cmd.Flags().Set("quiet", "true"))
|
||||
assert.NilError(t, cmd.Execute())
|
||||
|
||||
@ -7,6 +7,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
@ -19,17 +20,17 @@ var logFn = func(expectedOut string) func(string, container.LogsOptions) (io.Rea
|
||||
}
|
||||
|
||||
func TestRunLogs(t *testing.T) {
|
||||
inspectFn := func(containerID string) (container.InspectResponse, error) {
|
||||
return container.InspectResponse{
|
||||
inspectFn := func(containerID string) (types.ContainerJSON, error) {
|
||||
return types.ContainerJSON{
|
||||
Config: &container.Config{Tty: true},
|
||||
ContainerJSONBase: &container.ContainerJSONBase{State: &container.State{Running: false}},
|
||||
ContainerJSONBase: &types.ContainerJSONBase{State: &types.ContainerState{Running: false}},
|
||||
}, nil
|
||||
}
|
||||
|
||||
testcases := []struct {
|
||||
doc string
|
||||
options *logsOptions
|
||||
client *fakeClient
|
||||
client fakeClient
|
||||
expectedError string
|
||||
expectedOut string
|
||||
expectedErr string
|
||||
@ -38,13 +39,13 @@ func TestRunLogs(t *testing.T) {
|
||||
doc: "successful logs",
|
||||
expectedOut: "foo",
|
||||
options: &logsOptions{},
|
||||
client: &fakeClient{logFunc: logFn("foo"), inspectFunc: inspectFn},
|
||||
client: fakeClient{logFunc: logFn("foo"), inspectFunc: inspectFn},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testcase := range testcases {
|
||||
t.Run(testcase.doc, func(t *testing.T) {
|
||||
cli := test.NewFakeCli(testcase.client)
|
||||
cli := test.NewFakeCli(&testcase.client)
|
||||
|
||||
err := runLogs(context.TODO(), cli, testcase.options)
|
||||
if testcase.expectedError != "" {
|
||||
|
||||
@ -23,6 +23,7 @@ import (
|
||||
"github.com/docker/docker/errdefs"
|
||||
"github.com/docker/go-connections/nat"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/pflag"
|
||||
cdi "tags.cncf.io/container-device-interface/pkg/parser"
|
||||
)
|
||||
@ -207,7 +208,7 @@ func addFlags(flags *pflag.FlagSet) *containerOptions {
|
||||
flags.Var(copts.ulimits, "ulimit", "Ulimit options")
|
||||
flags.StringVarP(&copts.user, "user", "u", "", "Username or UID (format: <name|uid>[:<group|gid>])")
|
||||
flags.StringVarP(&copts.workingDir, "workdir", "w", "", "Working directory inside the container")
|
||||
flags.BoolVar(&copts.autoRemove, "rm", false, "Automatically remove the container and its associated anonymous volumes when it exits")
|
||||
flags.BoolVar(&copts.autoRemove, "rm", false, "Automatically remove the container when it exits")
|
||||
|
||||
// Security
|
||||
flags.Var(&copts.capAdd, "cap-add", "Add Linux capabilities")
|
||||
@ -363,6 +364,10 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
|
||||
return nil, errors.Errorf("invalid value: %d. Valid memory swappiness range is 0-100", swappiness)
|
||||
}
|
||||
|
||||
mounts := copts.mounts.Value()
|
||||
if len(mounts) > 0 && copts.volumeDriver != "" {
|
||||
logrus.Warn("`--volume-driver` is ignored for volumes specified via `--mount`. Use `--mount type=volume,volume-driver=...` instead.")
|
||||
}
|
||||
var binds []string
|
||||
volumes := copts.volumes.GetMap()
|
||||
// add any bind targets to the list of container volumes
|
||||
@ -569,10 +574,10 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
|
||||
return nil, errors.Errorf("--health-retries cannot be negative")
|
||||
}
|
||||
if copts.healthStartPeriod < 0 {
|
||||
return nil, errors.New("--health-start-period cannot be negative")
|
||||
return nil, fmt.Errorf("--health-start-period cannot be negative")
|
||||
}
|
||||
if copts.healthStartInterval < 0 {
|
||||
return nil, errors.New("--health-start-interval cannot be negative")
|
||||
return nil, fmt.Errorf("--health-start-interval cannot be negative")
|
||||
}
|
||||
|
||||
healthConfig = &container.HealthConfig{
|
||||
@ -692,14 +697,14 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
|
||||
Tmpfs: tmpfs,
|
||||
Sysctls: copts.sysctls.GetAll(),
|
||||
Runtime: copts.runtime,
|
||||
Mounts: copts.mounts.Value(),
|
||||
Mounts: mounts,
|
||||
MaskedPaths: maskedPaths,
|
||||
ReadonlyPaths: readonlyPaths,
|
||||
Annotations: copts.annotations.GetAll(),
|
||||
}
|
||||
|
||||
if copts.autoRemove && !hostConfig.RestartPolicy.IsNone() {
|
||||
return nil, errors.Errorf("conflicting options: cannot specify both --restart and --rm")
|
||||
return nil, errors.Errorf("Conflicting options: --restart and --rm")
|
||||
}
|
||||
|
||||
// only set this value if the user provided the flag, else it should default to nil
|
||||
@ -762,6 +767,7 @@ func parseNetworkOpts(copts *containerOptions) (map[string]*networktypes.Endpoin
|
||||
}
|
||||
|
||||
for i, n := range copts.netMode.Value() {
|
||||
n := n
|
||||
if container.NetworkMode(n.Target).IsUserDefined() {
|
||||
hasUserDefined = true
|
||||
} else {
|
||||
@ -825,9 +831,7 @@ func applyContainerOptions(n *opts.NetworkAttachmentOpts, copts *containerOption
|
||||
n.Aliases = make([]string, copts.aliases.Len())
|
||||
copy(n.Aliases, copts.aliases.GetAll())
|
||||
}
|
||||
// For a user-defined network, "--link" is an endpoint option, it creates an alias. But,
|
||||
// for the default bridge it defines a legacy-link.
|
||||
if container.NetworkMode(n.Target).IsUserDefined() && copts.links.Len() > 0 {
|
||||
if n.Target != "default" && copts.links.Len() > 0 {
|
||||
n.Links = make([]string, copts.links.Len())
|
||||
copy(n.Links, copts.links.GetAll())
|
||||
}
|
||||
@ -860,9 +864,7 @@ func parseNetworkAttachmentOpt(ep opts.NetworkAttachmentOpts) (*networktypes.End
|
||||
}
|
||||
}
|
||||
|
||||
epConfig := &networktypes.EndpointSettings{
|
||||
GwPriority: ep.GwPriority,
|
||||
}
|
||||
epConfig := &networktypes.EndpointSettings{}
|
||||
epConfig.Aliases = append(epConfig.Aliases, ep.Aliases...)
|
||||
if len(ep.DriverOpts) > 0 {
|
||||
epConfig.DriverOpts = make(map[string]string)
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
@ -13,6 +12,7 @@ import (
|
||||
"github.com/docker/docker/api/types/container"
|
||||
networktypes "github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/go-connections/nat"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/pflag"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
@ -72,75 +72,14 @@ func mustParse(t *testing.T, args string) (*container.Config, *container.HostCon
|
||||
}
|
||||
|
||||
func TestParseRunLinks(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expHostConfigLinks []string
|
||||
expNetConfigLinks map[string][]string
|
||||
}{
|
||||
// Default bridge - legacy links ...
|
||||
{
|
||||
name: "default/onelink",
|
||||
input: "--link a:b",
|
||||
expHostConfigLinks: []string{"a:b"},
|
||||
expNetConfigLinks: map[string][]string{"default": nil},
|
||||
},
|
||||
{
|
||||
name: "default/twolinks",
|
||||
input: "--link a:b --link c:d",
|
||||
expHostConfigLinks: []string{"a:b", "c:d"},
|
||||
expNetConfigLinks: map[string][]string{"default": nil},
|
||||
},
|
||||
{
|
||||
name: "bridge/onelink",
|
||||
input: "--network bridge --link a:b",
|
||||
expHostConfigLinks: []string{"a:b"},
|
||||
// expNetConfigLinks - no EndpointsConfig is created for a single named network with no options set.
|
||||
// See the "For backward compatibility" comment in parseNetworkOpts().
|
||||
},
|
||||
{
|
||||
name: "default/nolinks",
|
||||
expNetConfigLinks: map[string][]string{"default": nil},
|
||||
},
|
||||
|
||||
// User-defined bridge - links become DNS aliases ...
|
||||
{
|
||||
name: "userdefnet/onelink",
|
||||
input: "--network userdefnet --link a:b",
|
||||
expHostConfigLinks: []string{"a:b"},
|
||||
expNetConfigLinks: map[string][]string{"userdefnet": {"a:b"}},
|
||||
},
|
||||
{
|
||||
name: "userdefnet/twolinks",
|
||||
input: "--network userdefnet --link a:b --link c:d",
|
||||
expHostConfigLinks: []string{"a:b", "c:d"},
|
||||
expNetConfigLinks: map[string][]string{"userdefnet": {"a:b", "c:d"}},
|
||||
},
|
||||
{
|
||||
name: "userdefnet/nolinks",
|
||||
input: "--network userdefnet",
|
||||
},
|
||||
{
|
||||
// Link options are applied to the first network (and there's no "advanced syntax"
|
||||
// link key, like "--network name=userdefnet,link=a:b").
|
||||
name: "links apply to the first network",
|
||||
input: "--network userdefnet --link a:b --network bar --link c:d",
|
||||
expHostConfigLinks: []string{"a:b", "c:d"},
|
||||
expNetConfigLinks: map[string][]string{"userdefnet": {"a:b", "c:d"}, "bar": nil},
|
||||
},
|
||||
if _, hostConfig, _ := mustParse(t, "--link a:b"); len(hostConfig.Links) == 0 || hostConfig.Links[0] != "a:b" {
|
||||
t.Fatalf("Error parsing links. Expected []string{\"a:b\"}, received: %v", hostConfig.Links)
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
_, hostConfig, netConfig := mustParse(t, tc.input)
|
||||
assert.Check(t, is.DeepEqual(hostConfig.Links, tc.expHostConfigLinks))
|
||||
assert.Check(t, is.Len(netConfig.EndpointsConfig, len(tc.expNetConfigLinks)))
|
||||
for netName, expLinks := range tc.expNetConfigLinks {
|
||||
nc, ok := netConfig.EndpointsConfig[netName]
|
||||
assert.Assert(t, ok)
|
||||
assert.Check(t, is.DeepEqual(nc.Links, expLinks))
|
||||
}
|
||||
})
|
||||
if _, hostConfig, _ := mustParse(t, "--link a:b --link c:d"); len(hostConfig.Links) < 2 || hostConfig.Links[0] != "a:b" || hostConfig.Links[1] != "c:d" {
|
||||
t.Fatalf("Error parsing links. Expected []string{\"a:b\", \"c:d\"}, received: %v", hostConfig.Links)
|
||||
}
|
||||
if _, hostConfig, _ := mustParse(t, ""); len(hostConfig.Links) != 0 {
|
||||
t.Fatalf("Error parsing links. No link expected, received: %v", hostConfig.Links)
|
||||
}
|
||||
}
|
||||
|
||||
@ -187,6 +126,7 @@ func TestParseRunAttach(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(tc.input, func(t *testing.T) {
|
||||
config, _, _ := mustParse(t, tc.input)
|
||||
assert.Equal(t, config.AttachStdin, tc.expected.AttachStdin)
|
||||
@ -340,7 +280,7 @@ func compareRandomizedStrings(a, b, c, d string) error {
|
||||
if a == d && b == c {
|
||||
return nil
|
||||
}
|
||||
return errors.New("strings don't match")
|
||||
return errors.Errorf("strings don't match")
|
||||
}
|
||||
|
||||
// Simple parse with MacAddress validation
|
||||
@ -395,7 +335,7 @@ func TestParseHostname(t *testing.T) {
|
||||
hostnameWithDomain := "--hostname=hostname.domainname"
|
||||
hostnameWithDomainTld := "--hostname=hostname.domainname.tld"
|
||||
for hostname, expectedHostname := range validHostnames {
|
||||
if config, _, _ := mustParse(t, "--hostname="+hostname); config.Hostname != expectedHostname {
|
||||
if config, _, _ := mustParse(t, fmt.Sprintf("--hostname=%s", hostname)); config.Hostname != expectedHostname {
|
||||
t.Fatalf("Expected the config to have 'hostname' as %q, got %q", expectedHostname, config.Hostname)
|
||||
}
|
||||
}
|
||||
@ -862,6 +802,7 @@ func TestParseRestartPolicy(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(tc.input, func(t *testing.T) {
|
||||
_, hostConfig, _, err := parseRun([]string{"--restart=" + tc.input, "img", "cmd"})
|
||||
if tc.expectedErr != "" {
|
||||
@ -876,9 +817,11 @@ func TestParseRestartPolicy(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseRestartPolicyAutoRemove(t *testing.T) {
|
||||
expected := "Conflicting options: --restart and --rm"
|
||||
_, _, _, err := parseRun([]string{"--rm", "--restart=always", "img", "cmd"}) //nolint:dogsled
|
||||
const expected = "conflicting options: cannot specify both --restart and --rm"
|
||||
assert.Check(t, is.Error(err, expected))
|
||||
if err == nil || err.Error() != expected {
|
||||
t.Fatalf("Expected error %v, but got none", expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseHealth(t *testing.T) {
|
||||
@ -978,7 +921,7 @@ func TestParseEnvfileVariablesWithBOMUnicode(t *testing.T) {
|
||||
}
|
||||
|
||||
// UTF16 with BOM
|
||||
e := "invalid utf8 bytes at line"
|
||||
e := "contains invalid utf8 bytes at line"
|
||||
if _, _, _, err := parseRun([]string{"--env-file=testdata/utf16.env", "img", "cmd"}); err == nil || !strings.Contains(err.Error(), e) {
|
||||
t.Fatalf("Expected an error with message '%s', got %v", e, err)
|
||||
}
|
||||
@ -1016,8 +959,12 @@ func TestParseLabelfileVariables(t *testing.T) {
|
||||
|
||||
func TestParseEntryPoint(t *testing.T) {
|
||||
config, _, _, err := parseRun([]string{"--entrypoint=anything", "cmd", "img"})
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, is.DeepEqual([]string(config.Entrypoint), []string{"anything"}))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(config.Entrypoint) != 1 && config.Entrypoint[0] != "anything" {
|
||||
t.Fatalf("Expected entrypoint 'anything', got %v", config.Entrypoint)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateDevice(t *testing.T) {
|
||||
|
||||
@ -2,13 +2,14 @@ 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/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@ -31,23 +32,24 @@ func NewPauseCommand(dockerCli command.Cli) *cobra.Command {
|
||||
Annotations: map[string]string{
|
||||
"aliases": "docker container pause, docker pause",
|
||||
},
|
||||
ValidArgsFunction: completion.ContainerNames(dockerCli, false, func(ctr container.Summary) bool {
|
||||
return ctr.State != "paused"
|
||||
ValidArgsFunction: completion.ContainerNames(dockerCli, false, func(container types.Container) bool {
|
||||
return container.State != "paused"
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
func runPause(ctx context.Context, dockerCLI command.Cli, opts *pauseOptions) error {
|
||||
apiClient := dockerCLI.Client()
|
||||
errChan := parallelOperation(ctx, opts.containers, apiClient.ContainerPause)
|
||||
|
||||
var errs []error
|
||||
for _, ctr := range opts.containers {
|
||||
func runPause(ctx context.Context, dockerCli command.Cli, opts *pauseOptions) error {
|
||||
var errs []string
|
||||
errChan := parallelOperation(ctx, opts.containers, dockerCli.Client().ContainerPause)
|
||||
for _, container := range opts.containers {
|
||||
if err := <-errChan; err != nil {
|
||||
errs = append(errs, err)
|
||||
errs = append(errs, err.Error())
|
||||
continue
|
||||
}
|
||||
_, _ = fmt.Fprintln(dockerCLI.Out(), ctr)
|
||||
fmt.Fprintln(dockerCli.Out(), container)
|
||||
}
|
||||
return errors.Join(errs...)
|
||||
if len(errs) > 0 {
|
||||
return errors.New(strings.Join(errs, "\n"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -1,65 +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 TestRunPause(t *testing.T) {
|
||||
cli := test.NewFakeCli(
|
||||
&fakeClient{
|
||||
containerPauseFunc: func(ctx context.Context, container string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
cmd := NewPauseCommand(cli)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetArgs([]string{"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 TestRunPauseClientError(t *testing.T) {
|
||||
cli := test.NewFakeCli(
|
||||
&fakeClient{
|
||||
containerPauseFunc: func(ctx context.Context, container string) error {
|
||||
return fmt.Errorf("client error for container %s", container)
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
cmd := NewPauseCommand(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"))
|
||||
}
|
||||
@ -5,7 +5,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/go-connections/nat"
|
||||
"gotest.tools/v3/assert"
|
||||
"gotest.tools/v3/golden"
|
||||
@ -43,10 +43,11 @@ func TestNewPortCommandOutput(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
inspectFunc: func(string) (container.InspectResponse, error) {
|
||||
ci := container.InspectResponse{NetworkSettings: &container.NetworkSettings{}}
|
||||
inspectFunc: func(string) (types.ContainerJSON, error) {
|
||||
ci := types.ContainerJSON{NetworkSettings: &types.NetworkSettings{}}
|
||||
ci.NetworkSettings.Ports = nat.PortMap{
|
||||
"80/tcp": make([]nat.PortBinding, len(tc.ips)),
|
||||
"443/tcp": make([]nat.PortBinding, len(tc.ips)),
|
||||
|
||||
@ -2,13 +2,12 @@ package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func TestContainerPrunePromptTermination(t *testing.T) {
|
||||
@ -16,13 +15,10 @@ func TestContainerPrunePromptTermination(t *testing.T) {
|
||||
t.Cleanup(cancel)
|
||||
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
containerPruneFunc: func(ctx context.Context, pruneFilters filters.Args) (container.PruneReport, error) {
|
||||
return container.PruneReport{}, errors.New("fakeClient containerPruneFunc should not be called")
|
||||
containerPruneFunc: func(ctx context.Context, pruneFilters filters.Args) (types.ContainersPruneReport, error) {
|
||||
return types.ContainersPruneReport{}, errors.New("fakeClient containerPruneFunc should not be called")
|
||||
},
|
||||
})
|
||||
cmd := NewPruneCommand(cli)
|
||||
cmd.SetArgs([]string{})
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
test.TerminatePrompt(ctx, t, cmd, cli)
|
||||
}
|
||||
|
||||
@ -1,77 +0,0 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
|
||||
func TestRunRename(t *testing.T) {
|
||||
testcases := []struct {
|
||||
doc, oldName, newName, expectedErr string
|
||||
}{
|
||||
{
|
||||
doc: "success",
|
||||
oldName: "oldName",
|
||||
newName: "newName",
|
||||
expectedErr: "",
|
||||
},
|
||||
{
|
||||
doc: "empty old name",
|
||||
oldName: "",
|
||||
newName: "newName",
|
||||
expectedErr: "Error: Neither old nor new names may be empty",
|
||||
},
|
||||
{
|
||||
doc: "empty new name",
|
||||
oldName: "oldName",
|
||||
newName: "",
|
||||
expectedErr: "Error: Neither old nor new names may be empty",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testcases {
|
||||
t.Run(tc.doc, func(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
containerRenameFunc: func(ctx context.Context, oldName, newName string) error {
|
||||
return nil
|
||||
},
|
||||
})
|
||||
|
||||
cmd := NewRenameCommand(cli)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
cmd.SetArgs([]string{tc.oldName, tc.newName})
|
||||
|
||||
err := cmd.Execute()
|
||||
|
||||
if tc.expectedErr != "" {
|
||||
assert.ErrorContains(t, err, tc.expectedErr)
|
||||
} else {
|
||||
assert.NilError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunRenameClientError(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
containerRenameFunc: func(ctx context.Context, oldName, newName string) error {
|
||||
return errors.New("client error")
|
||||
},
|
||||
})
|
||||
|
||||
cmd := NewRenameCommand(cli)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
cmd.SetArgs([]string{"oldName", "newName"})
|
||||
|
||||
err := cmd.Execute()
|
||||
|
||||
assert.Check(t, is.Error(err, "Error: failed to rename container named oldName"))
|
||||
}
|
||||
@ -2,13 +2,14 @@ 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/docker/docker/api/types/container"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@ -29,11 +30,8 @@ func NewRestartCommand(dockerCli command.Cli) *cobra.Command {
|
||||
Short: "Restart one or more containers",
|
||||
Args: cli.RequiresMinArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if cmd.Flags().Changed("time") && cmd.Flags().Changed("timeout") {
|
||||
return errors.New("conflicting options: cannot specify both --timeout and --time")
|
||||
}
|
||||
opts.containers = args
|
||||
opts.timeoutChanged = cmd.Flags().Changed("timeout") || cmd.Flags().Changed("time")
|
||||
opts.timeoutChanged = cmd.Flags().Changed("time")
|
||||
return runRestart(cmd.Context(), dockerCli, &opts)
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
@ -44,36 +42,29 @@ func NewRestartCommand(dockerCli command.Cli) *cobra.Command {
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.StringVarP(&opts.signal, "signal", "s", "", "Signal to send to the container")
|
||||
flags.IntVarP(&opts.timeout, "timeout", "t", 0, "Seconds to wait before killing the container")
|
||||
|
||||
// The --time option is deprecated, but kept for backward compatibility.
|
||||
flags.IntVar(&opts.timeout, "time", 0, "Seconds to wait before killing the container (deprecated: use --timeout)")
|
||||
_ = flags.MarkDeprecated("time", "use --timeout instead")
|
||||
|
||||
_ = cmd.RegisterFlagCompletionFunc("signal", completeSignals)
|
||||
|
||||
flags.IntVarP(&opts.timeout, "time", "t", 0, "Seconds to wait before killing the container")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runRestart(ctx context.Context, dockerCLI command.Cli, opts *restartOptions) error {
|
||||
func runRestart(ctx context.Context, dockerCli command.Cli, opts *restartOptions) error {
|
||||
var errs []string
|
||||
var timeout *int
|
||||
if opts.timeoutChanged {
|
||||
timeout = &opts.timeout
|
||||
}
|
||||
|
||||
apiClient := dockerCLI.Client()
|
||||
var errs []error
|
||||
// TODO(thaJeztah): consider using parallelOperation for restart, similar to "stop" and "remove"
|
||||
for _, name := range opts.containers {
|
||||
err := apiClient.ContainerRestart(ctx, name, container.StopOptions{
|
||||
err := dockerCli.Client().ContainerRestart(ctx, name, container.StopOptions{
|
||||
Signal: opts.signal,
|
||||
Timeout: timeout,
|
||||
})
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
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.New(strings.Join(errs, "\n"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -1,95 +0,0 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"sort"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/errdefs"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
|
||||
func TestRestart(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
args []string
|
||||
restarted []string
|
||||
expectedOpts container.StopOptions
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "without options",
|
||||
args: []string{"container-1", "container-2"},
|
||||
restarted: []string{"container-1", "container-2"},
|
||||
},
|
||||
{
|
||||
name: "with unknown container",
|
||||
args: []string{"container-1", "nosuchcontainer", "container-2"},
|
||||
expectedErr: "no such container",
|
||||
restarted: []string{"container-1", "container-2"},
|
||||
},
|
||||
{
|
||||
name: "with -t",
|
||||
args: []string{"-t", "2", "container-1"},
|
||||
expectedOpts: container.StopOptions{Timeout: func(to int) *int { return &to }(2)},
|
||||
restarted: []string{"container-1"},
|
||||
},
|
||||
{
|
||||
name: "with --timeout",
|
||||
args: []string{"--timeout", "2", "container-1"},
|
||||
expectedOpts: container.StopOptions{Timeout: func(to int) *int { return &to }(2)},
|
||||
restarted: []string{"container-1"},
|
||||
},
|
||||
{
|
||||
name: "with --time",
|
||||
args: []string{"--time", "2", "container-1"},
|
||||
expectedOpts: container.StopOptions{Timeout: func(to int) *int { return &to }(2)},
|
||||
restarted: []string{"container-1"},
|
||||
},
|
||||
{
|
||||
name: "conflicting options",
|
||||
args: []string{"--timeout", "2", "--time", "2", "container-1"},
|
||||
expectedErr: "conflicting options: cannot specify both --timeout and --time",
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var restarted []string
|
||||
mutex := new(sync.Mutex)
|
||||
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
containerRestartFunc: func(ctx context.Context, containerID string, options container.StopOptions) error {
|
||||
assert.Check(t, is.DeepEqual(options, tc.expectedOpts))
|
||||
if containerID == "nosuchcontainer" {
|
||||
return errdefs.NotFound(errors.New("Error: no such container: " + containerID))
|
||||
}
|
||||
|
||||
// TODO(thaJeztah): consider using parallelOperation for restart, similar to "stop" and "remove"
|
||||
mutex.Lock()
|
||||
restarted = append(restarted, containerID)
|
||||
mutex.Unlock()
|
||||
return nil
|
||||
},
|
||||
Version: "1.36",
|
||||
})
|
||||
cmd := NewRestartCommand(cli)
|
||||
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))
|
||||
}
|
||||
sort.Strings(restarted)
|
||||
assert.Check(t, is.DeepEqual(restarted, tc.restarted))
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -2,7 +2,6 @@ package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
@ -11,6 +10,7 @@ import (
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/errdefs"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@ -38,9 +38,7 @@ func NewRmCommand(dockerCli command.Cli) *cobra.Command {
|
||||
Annotations: map[string]string{
|
||||
"aliases": "docker container rm, docker container remove, docker rm",
|
||||
},
|
||||
ValidArgsFunction: completion.ContainerNames(dockerCli, true, func(ctr container.Summary) bool {
|
||||
return opts.force || ctr.State == "exited" || ctr.State == "created"
|
||||
}),
|
||||
ValidArgsFunction: completion.ContainerNames(dockerCli, true),
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
@ -50,31 +48,33 @@ func NewRmCommand(dockerCli command.Cli) *cobra.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runRm(ctx context.Context, dockerCLI command.Cli, opts *rmOptions) error {
|
||||
apiClient := dockerCLI.Client()
|
||||
func runRm(ctx context.Context, dockerCli command.Cli, opts *rmOptions) error {
|
||||
var errs []string
|
||||
errChan := parallelOperation(ctx, opts.containers, func(ctx context.Context, ctrID string) error {
|
||||
ctrID = strings.Trim(ctrID, "/")
|
||||
if ctrID == "" {
|
||||
return errors.New("container name cannot be empty")
|
||||
return errors.New("Container name cannot be empty")
|
||||
}
|
||||
return apiClient.ContainerRemove(ctx, ctrID, container.RemoveOptions{
|
||||
return dockerCli.Client().ContainerRemove(ctx, ctrID, container.RemoveOptions{
|
||||
RemoveVolumes: opts.rmVolumes,
|
||||
RemoveLinks: opts.rmLink,
|
||||
Force: opts.force,
|
||||
})
|
||||
})
|
||||
|
||||
var errs []error
|
||||
for _, name := range opts.containers {
|
||||
if err := <-errChan; err != nil {
|
||||
if opts.force && errdefs.IsNotFound(err) {
|
||||
_, _ = fmt.Fprintln(dockerCLI.Err(), err)
|
||||
fmt.Fprintln(dockerCli.Err(), err)
|
||||
continue
|
||||
}
|
||||
errs = append(errs, err)
|
||||
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.New(strings.Join(errs, "\n"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@ package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"sync"
|
||||
@ -23,6 +23,7 @@ func TestRemoveForce(t *testing.T) {
|
||||
{name: "without force", args: []string{"nosuchcontainer", "mycontainer"}, expectedErr: "no such container"},
|
||||
{name: "with force", args: []string{"--force", "nosuchcontainer", "mycontainer"}, expectedErr: ""},
|
||||
} {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var removed []string
|
||||
mutex := new(sync.Mutex)
|
||||
@ -36,7 +37,7 @@ func TestRemoveForce(t *testing.T) {
|
||||
mutex.Unlock()
|
||||
|
||||
if container == "nosuchcontainer" {
|
||||
return errdefs.NotFound(errors.New("Error: no such container: " + container))
|
||||
return errdefs.NotFound(fmt.Errorf("Error: no such container: " + container))
|
||||
}
|
||||
return nil
|
||||
},
|
||||
@ -44,7 +45,6 @@ func TestRemoveForce(t *testing.T) {
|
||||
})
|
||||
cmd := NewRmCommand(cli)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
|
||||
err := cmd.Execute()
|
||||
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
@ -43,7 +44,7 @@ func NewRunCommand(dockerCli command.Cli) *cobra.Command {
|
||||
}
|
||||
return runRun(cmd.Context(), dockerCli, cmd.Flags(), &options, copts)
|
||||
},
|
||||
ValidArgsFunction: completion.ImageNames(dockerCli, 1),
|
||||
ValidArgsFunction: completion.ImageNames(dockerCli),
|
||||
Annotations: map[string]string{
|
||||
"category-top": "1",
|
||||
"aliases": "docker container run, docker run",
|
||||
@ -69,25 +70,29 @@ func NewRunCommand(dockerCli command.Cli) *cobra.Command {
|
||||
command.AddTrustVerificationFlags(flags, &options.untrusted, dockerCli.ContentTrustEnabled())
|
||||
copts = addFlags(flags)
|
||||
|
||||
_ = cmd.RegisterFlagCompletionFunc("detach-keys", completeDetachKeys)
|
||||
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(
|
||||
"env",
|
||||
func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
return os.Environ(), cobra.ShellCompDirectiveNoFileComp
|
||||
},
|
||||
)
|
||||
cmd.RegisterFlagCompletionFunc(
|
||||
"env-file",
|
||||
func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
return nil, cobra.ShellCompDirectiveDefault
|
||||
},
|
||||
)
|
||||
cmd.RegisterFlagCompletionFunc(
|
||||
"network",
|
||||
completion.NetworkNames(dockerCli),
|
||||
)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runRun(ctx context.Context, dockerCli command.Cli, flags *pflag.FlagSet, ropts *runOptions, copts *containerOptions) error {
|
||||
if err := validatePullOpt(ropts.pull); err != nil {
|
||||
return cli.StatusError{
|
||||
Status: withHelp(err, "run").Error(),
|
||||
StatusCode: 125,
|
||||
}
|
||||
reportError(dockerCli.Err(), "run", err.Error(), true)
|
||||
return cli.StatusError{StatusCode: 125}
|
||||
}
|
||||
proxyConfig := dockerCli.ConfigFile().ParseProxyConfig(dockerCli.Client().DaemonHost(), opts.ConvertKVStringsToMapWithNil(copts.env.GetAll()))
|
||||
newEnv := []string{}
|
||||
@ -102,16 +107,12 @@ func runRun(ctx context.Context, dockerCli command.Cli, flags *pflag.FlagSet, ro
|
||||
containerCfg, err := parse(flags, copts, dockerCli.ServerInfo().OSType)
|
||||
// just in case the parse does not exit
|
||||
if err != nil {
|
||||
return cli.StatusError{
|
||||
Status: withHelp(err, "run").Error(),
|
||||
StatusCode: 125,
|
||||
}
|
||||
reportError(dockerCli.Err(), "run", err.Error(), true)
|
||||
return cli.StatusError{StatusCode: 125}
|
||||
}
|
||||
if err = validateAPIVersion(containerCfg, dockerCli.CurrentVersion()); err != nil {
|
||||
return cli.StatusError{
|
||||
Status: withHelp(err, "run").Error(),
|
||||
StatusCode: 125,
|
||||
}
|
||||
reportError(dockerCli.Err(), "run", err.Error(), true)
|
||||
return cli.StatusError{StatusCode: 125}
|
||||
}
|
||||
return runContainer(ctx, dockerCli, ropts, copts, containerCfg)
|
||||
}
|
||||
@ -130,7 +131,7 @@ func runContainer(ctx context.Context, dockerCli command.Cli, runOpts *runOption
|
||||
}
|
||||
} else {
|
||||
if copts.attach.Len() != 0 {
|
||||
return errors.New("conflicting options: cannot specify both --attach and --detach")
|
||||
return errors.New("Conflicting options: -a and -d")
|
||||
}
|
||||
|
||||
config.AttachStdin = false
|
||||
@ -139,30 +140,25 @@ func runContainer(ctx context.Context, dockerCli command.Cli, runOpts *runOption
|
||||
config.StdinOnce = false
|
||||
}
|
||||
|
||||
ctx, cancelFun := context.WithCancel(ctx)
|
||||
defer cancelFun()
|
||||
|
||||
containerID, err := createContainer(ctx, dockerCli, containerCfg, &runOpts.createOptions)
|
||||
if err != nil {
|
||||
return toStatusError(err)
|
||||
reportError(stderr, "run", err.Error(), true)
|
||||
return runStartContainerErr(err)
|
||||
}
|
||||
if runOpts.sigProxy {
|
||||
sigc := notifyAllSignals()
|
||||
// since we're explicitly setting up signal handling here, and the daemon will
|
||||
// get notified independently of the clients ctx cancellation, we use this context
|
||||
// but without cancellation to avoid ForwardAllSignals from returning
|
||||
// before all signals are forwarded.
|
||||
bgCtx := context.WithoutCancel(ctx)
|
||||
go ForwardAllSignals(bgCtx, apiClient, containerID, sigc)
|
||||
go ForwardAllSignals(ctx, apiClient, containerID, sigc)
|
||||
defer signal.StopCatch(sigc)
|
||||
}
|
||||
|
||||
ctx, cancelFun := context.WithCancel(context.WithoutCancel(ctx))
|
||||
defer cancelFun()
|
||||
|
||||
var (
|
||||
waitDisplayID chan struct{}
|
||||
errCh chan error
|
||||
)
|
||||
attach := config.AttachStdin || config.AttachStdout || config.AttachStderr
|
||||
if !attach {
|
||||
if !config.AttachStdout && !config.AttachStderr {
|
||||
// Make this asynchronous to allow the client to write to stdin before having to read the ID
|
||||
waitDisplayID = make(chan struct{})
|
||||
go func() {
|
||||
@ -170,15 +166,13 @@ func runContainer(ctx context.Context, dockerCli command.Cli, runOpts *runOption
|
||||
_, _ = fmt.Fprintln(stdout, containerID)
|
||||
}()
|
||||
}
|
||||
attach := config.AttachStdin || config.AttachStdout || config.AttachStderr
|
||||
if attach {
|
||||
detachKeys := dockerCli.ConfigFile().DetachKeys
|
||||
if runOpts.detachKeys != "" {
|
||||
detachKeys = runOpts.detachKeys
|
||||
}
|
||||
|
||||
// ctx should not be cancellable here, as this would kill the stream to the container
|
||||
// and we want to keep the stream open until the process in the container exits or until
|
||||
// the user forcefully terminates the CLI.
|
||||
closeFn, err := attachContainer(ctx, dockerCli, containerID, &errCh, config, container.AttachOptions{
|
||||
Stream: true,
|
||||
Stdin: config.AttachStdin,
|
||||
@ -208,29 +202,22 @@ func runContainer(ctx context.Context, dockerCli command.Cli, runOpts *runOption
|
||||
<-errCh
|
||||
}
|
||||
|
||||
reportError(stderr, "run", err.Error(), false)
|
||||
if copts.autoRemove {
|
||||
// wait container to be removed
|
||||
<-statusChan
|
||||
}
|
||||
return toStatusError(err)
|
||||
return runStartContainerErr(err)
|
||||
}
|
||||
|
||||
// Detached mode: wait for the id to be displayed and return.
|
||||
if !attach {
|
||||
// Detached mode
|
||||
<-waitDisplayID
|
||||
return nil
|
||||
}
|
||||
|
||||
if config.Tty && dockerCli.Out().IsTerminal() {
|
||||
if (config.AttachStdin || config.AttachStdout || config.AttachStderr) && config.Tty && dockerCli.Out().IsTerminal() {
|
||||
if err := MonitorTtySize(ctx, dockerCli, containerID, false); err != nil {
|
||||
_, _ = fmt.Fprintln(stderr, "Error monitoring TTY size:", err)
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
case err := <-errCh:
|
||||
if err != nil {
|
||||
if errCh != nil {
|
||||
if err := <-errCh; err != nil {
|
||||
if _, ok := err.(term.EscapeError); ok {
|
||||
// The user entered the detach escape sequence.
|
||||
return nil
|
||||
@ -239,20 +226,19 @@ func runContainer(ctx context.Context, dockerCli command.Cli, runOpts *runOption
|
||||
logrus.Debugf("Error hijack: %s", err)
|
||||
return err
|
||||
}
|
||||
status := <-statusChan
|
||||
if status != 0 {
|
||||
return cli.StatusError{StatusCode: status}
|
||||
}
|
||||
case status := <-statusChan:
|
||||
// notify hijackedIOStreamer that we're exiting and wait
|
||||
// so that the terminal can be restored.
|
||||
cancelFun()
|
||||
<-errCh
|
||||
if status != 0 {
|
||||
return cli.StatusError{StatusCode: status}
|
||||
}
|
||||
}
|
||||
|
||||
// Detached mode: wait for the id to be displayed and return.
|
||||
if !config.AttachStdout && !config.AttachStderr {
|
||||
// Detached mode
|
||||
<-waitDisplayID
|
||||
return nil
|
||||
}
|
||||
|
||||
status := <-statusChan
|
||||
if status != 0 {
|
||||
return cli.StatusError{StatusCode: status}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -304,43 +290,30 @@ func attachContainer(ctx context.Context, dockerCli command.Cli, containerID str
|
||||
return resp.Close, nil
|
||||
}
|
||||
|
||||
// withHelp decorates the error with a suggestion to use "--help".
|
||||
func withHelp(err error, commandName string) error {
|
||||
return fmt.Errorf("docker: %w\n\nRun 'docker %s --help' for more information", err, commandName)
|
||||
// reportError is a utility method that prints a user-friendly message
|
||||
// containing the error that occurred during parsing and a suggestion to get help
|
||||
func reportError(stderr io.Writer, name string, str string, withHelp bool) {
|
||||
str = strings.TrimSuffix(str, ".") + "."
|
||||
if withHelp {
|
||||
str += "\nSee 'docker " + name + " --help'."
|
||||
}
|
||||
_, _ = fmt.Fprintln(stderr, "docker:", str)
|
||||
}
|
||||
|
||||
// toStatusError attempts to detect specific error-conditions to assign
|
||||
// an appropriate exit-code for situations where the problem originates
|
||||
// from the container. It returns [cli.StatusError] with the original
|
||||
// error message and the Status field set as follows:
|
||||
//
|
||||
// - 125: for generic failures sent back from the daemon
|
||||
// - 126: if container start fails with 'permission denied' error
|
||||
// - 127: if container start fails with 'not found'/'no such' error
|
||||
func toStatusError(err error) error {
|
||||
// TODO(thaJeztah): some of these errors originate from the container: should we actually suggest "--help" for those?
|
||||
|
||||
errMsg := err.Error()
|
||||
|
||||
if strings.Contains(errMsg, "executable file not found") || strings.Contains(errMsg, "no such file or directory") || strings.Contains(errMsg, "system cannot find the file specified") {
|
||||
return cli.StatusError{
|
||||
Cause: err,
|
||||
Status: withHelp(err, "run").Error(),
|
||||
StatusCode: 127,
|
||||
}
|
||||
// if container start fails with 'not found'/'no such' error, return 127
|
||||
// if container start fails with 'permission denied' error, return 126
|
||||
// return 125 for generic docker daemon failures
|
||||
func runStartContainerErr(err error) error {
|
||||
trimmedErr := strings.TrimPrefix(err.Error(), "Error response from daemon: ")
|
||||
statusError := cli.StatusError{StatusCode: 125}
|
||||
if strings.Contains(trimmedErr, "executable file not found") ||
|
||||
strings.Contains(trimmedErr, "no such file or directory") ||
|
||||
strings.Contains(trimmedErr, "system cannot find the file specified") {
|
||||
statusError = cli.StatusError{StatusCode: 127}
|
||||
} else if strings.Contains(trimmedErr, syscall.EACCES.Error()) ||
|
||||
strings.Contains(trimmedErr, syscall.EISDIR.Error()) {
|
||||
statusError = cli.StatusError{StatusCode: 126}
|
||||
}
|
||||
|
||||
if strings.Contains(errMsg, syscall.EACCES.Error()) || strings.Contains(errMsg, syscall.EISDIR.Error()) {
|
||||
return cli.StatusError{
|
||||
Cause: err,
|
||||
Status: withHelp(err, "run").Error(),
|
||||
StatusCode: 126,
|
||||
}
|
||||
}
|
||||
|
||||
return cli.StatusError{
|
||||
Cause: err,
|
||||
Status: withHelp(err, "run").Error(),
|
||||
StatusCode: 125,
|
||||
}
|
||||
return statusError
|
||||
}
|
||||
|
||||
@ -2,59 +2,22 @@ package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/creack/pty"
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/cli/internal/test/notary"
|
||||
"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/pkg/jsonmessage"
|
||||
specs "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/spf13/pflag"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
|
||||
func TestRunValidateFlags(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
args []string
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "with conflicting --attach, --detach",
|
||||
args: []string{"--attach", "stdin", "--detach", "myimage"},
|
||||
expectedErr: "conflicting options: cannot specify both --attach and --detach",
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cmd := NewRunCommand(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))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunLabel(t *testing.T) {
|
||||
fakeCLI := test.NewFakeCli(&fakeClient{
|
||||
createContainerFunc: func(_ *container.Config, _ *container.HostConfig, _ *network.NetworkingConfig, _ *specs.Platform, _ string) (container.CreateResponse, error) {
|
||||
@ -69,236 +32,6 @@ func TestRunLabel(t *testing.T) {
|
||||
assert.NilError(t, cmd.Execute())
|
||||
}
|
||||
|
||||
func TestRunAttach(t *testing.T) {
|
||||
p, tty, err := pty.Open()
|
||||
assert.NilError(t, err)
|
||||
defer func() {
|
||||
_ = tty.Close()
|
||||
_ = p.Close()
|
||||
}()
|
||||
|
||||
var conn net.Conn
|
||||
attachCh := make(chan struct{})
|
||||
fakeCLI := test.NewFakeCli(&fakeClient{
|
||||
createContainerFunc: func(_ *container.Config, _ *container.HostConfig, _ *network.NetworkingConfig, _ *specs.Platform, _ string) (container.CreateResponse, error) {
|
||||
return container.CreateResponse{
|
||||
ID: "id",
|
||||
}, nil
|
||||
},
|
||||
containerAttachFunc: func(ctx context.Context, containerID string, options container.AttachOptions) (types.HijackedResponse, error) {
|
||||
server, client := net.Pipe()
|
||||
conn = server
|
||||
t.Cleanup(func() {
|
||||
_ = server.Close()
|
||||
})
|
||||
attachCh <- struct{}{}
|
||||
return types.NewHijackedResponse(client, types.MediaTypeRawStream), nil
|
||||
},
|
||||
waitFunc: func(_ string) (<-chan container.WaitResponse, <-chan error) {
|
||||
responseChan := make(chan container.WaitResponse, 1)
|
||||
errChan := make(chan error)
|
||||
|
||||
responseChan <- container.WaitResponse{
|
||||
StatusCode: 33,
|
||||
}
|
||||
return responseChan, errChan
|
||||
},
|
||||
// use new (non-legacy) wait API
|
||||
// see: 38591f20d07795aaef45d400df89ca12f29c603b
|
||||
Version: "1.30",
|
||||
}, func(fc *test.FakeCli) {
|
||||
fc.SetOut(streams.NewOut(tty))
|
||||
fc.SetIn(streams.NewIn(tty))
|
||||
})
|
||||
|
||||
cmd := NewRunCommand(fakeCLI)
|
||||
cmd.SetArgs([]string{"-it", "busybox"})
|
||||
cmd.SilenceUsage = true
|
||||
cmdErrC := make(chan error, 1)
|
||||
go func() {
|
||||
cmdErrC <- cmd.Execute()
|
||||
}()
|
||||
|
||||
// run command should attempt to attach to the container
|
||||
select {
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("containerAttachFunc was not called before the 5 second timeout")
|
||||
case <-attachCh:
|
||||
}
|
||||
|
||||
// end stream from "container" so that we'll detach
|
||||
conn.Close()
|
||||
|
||||
select {
|
||||
case cmdErr := <-cmdErrC:
|
||||
assert.Equal(t, cmdErr, cli.StatusError{
|
||||
StatusCode: 33,
|
||||
})
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("cmd did not return within timeout")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunAttachTermination(t *testing.T) {
|
||||
p, tty, err := pty.Open()
|
||||
assert.NilError(t, err)
|
||||
defer func() {
|
||||
_ = tty.Close()
|
||||
_ = p.Close()
|
||||
}()
|
||||
|
||||
var conn net.Conn
|
||||
killCh := make(chan struct{})
|
||||
attachCh := make(chan struct{})
|
||||
fakeCLI := test.NewFakeCli(&fakeClient{
|
||||
createContainerFunc: func(_ *container.Config, _ *container.HostConfig, _ *network.NetworkingConfig, _ *specs.Platform, _ string) (container.CreateResponse, error) {
|
||||
return container.CreateResponse{
|
||||
ID: "id",
|
||||
}, nil
|
||||
},
|
||||
containerKillFunc: func(ctx context.Context, containerID, signal string) error {
|
||||
killCh <- struct{}{}
|
||||
return nil
|
||||
},
|
||||
containerAttachFunc: func(ctx context.Context, containerID string, options container.AttachOptions) (types.HijackedResponse, error) {
|
||||
server, client := net.Pipe()
|
||||
conn = server
|
||||
t.Cleanup(func() {
|
||||
_ = server.Close()
|
||||
})
|
||||
attachCh <- struct{}{}
|
||||
return types.NewHijackedResponse(client, types.MediaTypeRawStream), nil
|
||||
},
|
||||
waitFunc: func(_ string) (<-chan container.WaitResponse, <-chan error) {
|
||||
responseChan := make(chan container.WaitResponse, 1)
|
||||
errChan := make(chan error)
|
||||
|
||||
responseChan <- container.WaitResponse{
|
||||
StatusCode: 130,
|
||||
}
|
||||
return responseChan, errChan
|
||||
},
|
||||
// use new (non-legacy) wait API
|
||||
// see: 38591f20d07795aaef45d400df89ca12f29c603b
|
||||
Version: "1.30",
|
||||
}, func(fc *test.FakeCli) {
|
||||
fc.SetOut(streams.NewOut(tty))
|
||||
fc.SetIn(streams.NewIn(tty))
|
||||
})
|
||||
|
||||
cmd := NewRunCommand(fakeCLI)
|
||||
cmd.SetArgs([]string{"-it", "busybox"})
|
||||
cmd.SilenceUsage = true
|
||||
cmdErrC := make(chan error, 1)
|
||||
go func() {
|
||||
cmdErrC <- cmd.Execute()
|
||||
}()
|
||||
|
||||
// run command should attempt to attach to the container
|
||||
select {
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("containerAttachFunc was not called before the timeout")
|
||||
case <-attachCh:
|
||||
}
|
||||
|
||||
assert.NilError(t, syscall.Kill(syscall.Getpid(), syscall.SIGINT))
|
||||
// end stream from "container" so that we'll detach
|
||||
conn.Close()
|
||||
|
||||
select {
|
||||
case <-killCh:
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("containerKillFunc was not called before the timeout")
|
||||
}
|
||||
|
||||
select {
|
||||
case cmdErr := <-cmdErrC:
|
||||
assert.Equal(t, cmdErr, cli.StatusError{
|
||||
StatusCode: 130,
|
||||
})
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("cmd did not return before the timeout")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunPullTermination(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(cancel)
|
||||
|
||||
attachCh := make(chan struct{})
|
||||
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 create a container")
|
||||
},
|
||||
containerAttachFunc: func(ctx context.Context, containerID string, options container.AttachOptions) (types.HijackedResponse, error) {
|
||||
return types.HijackedResponse{}, errors.New("shouldn't try to attach to a container")
|
||||
},
|
||||
imageCreateFunc: func(ctx context.Context, parentReference string, options image.CreateOptions) (io.ReadCloser, error) {
|
||||
server, client := net.Pipe()
|
||||
t.Cleanup(func() {
|
||||
_ = server.Close()
|
||||
})
|
||||
go func() {
|
||||
enc := json.NewEncoder(server)
|
||||
for i := 0; i < 100; i++ {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
assert.NilError(t, server.Close(), "failed to close imageCreateFunc server")
|
||||
return
|
||||
default:
|
||||
assert.NilError(t, enc.Encode(jsonmessage.JSONMessage{
|
||||
Status: "Downloading",
|
||||
ID: fmt.Sprintf("id-%d", i),
|
||||
TimeNano: time.Now().UnixNano(),
|
||||
Time: time.Now().Unix(),
|
||||
Progress: &jsonmessage.JSONProgress{
|
||||
Current: int64(i),
|
||||
Total: 100,
|
||||
Start: 0,
|
||||
},
|
||||
}))
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
}()
|
||||
attachCh <- struct{}{}
|
||||
return client, nil
|
||||
},
|
||||
Version: "1.30",
|
||||
})
|
||||
|
||||
cmd := NewRunCommand(fakeCLI)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
cmd.SetArgs([]string{"--pull", "always", "foobar:latest"})
|
||||
|
||||
cmdErrC := make(chan error, 1)
|
||||
go func() {
|
||||
cmdErrC <- cmd.ExecuteContext(ctx)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("imageCreateFunc was not called before the timeout")
|
||||
case <-attachCh:
|
||||
}
|
||||
|
||||
cancel()
|
||||
|
||||
select {
|
||||
case cmdErr := <-cmdErrC:
|
||||
assert.Equal(t, cmdErr, cli.StatusError{
|
||||
Cause: context.Canceled,
|
||||
StatusCode: 125,
|
||||
Status: "docker: context canceled\n\nRun 'docker run --help' for more information",
|
||||
})
|
||||
case <-time.After(10 * time.Second):
|
||||
t.Fatal("cmd did not return before the timeout")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunCommandWithContentTrustErrors(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
@ -326,28 +59,23 @@ func TestRunCommandWithContentTrustErrors(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 *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 := NewRunCommand(fakeCLI)
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
err := cmd.Execute()
|
||||
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.expectedError))
|
||||
})
|
||||
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{}, fmt.Errorf("shouldn't try to pull image")
|
||||
},
|
||||
}, test.EnableContentTrust)
|
||||
fakeCLI.SetNotaryClient(tc.notaryFunc)
|
||||
cmd := NewRunCommand(fakeCLI)
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.SetOut(io.Discard)
|
||||
err := cmd.Execute()
|
||||
assert.Assert(t, err != nil)
|
||||
assert.Assert(t, is.Contains(fakeCLI.ErrBuffer().String(), tc.expectedError))
|
||||
}
|
||||
}
|
||||
|
||||
@ -366,6 +94,7 @@ func TestRunContainerImagePullPolicyInvalid(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(tc.PullPolicy, func(t *testing.T) {
|
||||
dockerCli := test.NewFakeCli(&fakeClient{})
|
||||
err := runRun(
|
||||
@ -378,8 +107,8 @@ func TestRunContainerImagePullPolicyInvalid(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))
|
||||
assert.Equal(t, statusErr.StatusCode, 125)
|
||||
assert.Check(t, is.Contains(dockerCli.ErrBuffer().String(), tc.ExpectedErrMsg))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,6 +9,7 @@ import (
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/moby/sys/signal"
|
||||
"github.com/moby/term"
|
||||
@ -42,8 +43,8 @@ func NewStartCommand(dockerCli command.Cli) *cobra.Command {
|
||||
Annotations: map[string]string{
|
||||
"aliases": "docker container start, docker start",
|
||||
},
|
||||
ValidArgsFunction: completion.ContainerNames(dockerCli, true, func(ctr container.Summary) bool {
|
||||
return ctr.State == "exited" || ctr.State == "created"
|
||||
ValidArgsFunction: completion.ContainerNames(dockerCli, true, func(container types.Container) bool {
|
||||
return container.State == "exited" || container.State == "created"
|
||||
}),
|
||||
}
|
||||
|
||||
@ -86,8 +87,7 @@ func RunStart(ctx context.Context, dockerCli command.Cli, opts *StartOptions) er
|
||||
// We always use c.ID instead of container to maintain consistency during `docker start`
|
||||
if !c.Config.Tty {
|
||||
sigc := notifyAllSignals()
|
||||
bgCtx := context.WithoutCancel(ctx)
|
||||
go ForwardAllSignals(bgCtx, dockerCli.Client(), c.ID, sigc)
|
||||
go ForwardAllSignals(ctx, dockerCli.Client(), c.ID, sigc)
|
||||
defer signal.StopCatch(sigc)
|
||||
}
|
||||
|
||||
|
||||
@ -1,9 +1,7 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
@ -15,9 +13,11 @@ import (
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
flagsHelper "github.com/docker/cli/cli/flags"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/events"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@ -164,7 +164,7 @@ func RunStats(ctx context.Context, dockerCLI command.Cli, options *StatsOptions)
|
||||
// is not valid for filtering containers.
|
||||
f := options.Filters.Clone()
|
||||
f.Add("type", string(events.ContainerEventType))
|
||||
eventChan, errChan := apiClient.Events(ctx, events.ListOptions{
|
||||
eventChan, errChan := apiClient.Events(ctx, types.EventsOptions{
|
||||
Filters: f,
|
||||
})
|
||||
|
||||
@ -219,7 +219,7 @@ func RunStats(ctx context.Context, dockerCLI command.Cli, options *StatsOptions)
|
||||
// with a list of container names/IDs.
|
||||
|
||||
if options.Filters != nil && options.Filters.Len() > 0 {
|
||||
return errors.New("filtering is not supported when specifying a list of containers")
|
||||
return fmt.Errorf("filtering is not supported when specifying a list of containers")
|
||||
}
|
||||
|
||||
// Create the list of containers, and start collecting stats for all
|
||||
@ -238,16 +238,16 @@ func RunStats(ctx context.Context, dockerCLI command.Cli, options *StatsOptions)
|
||||
// make sure each container get at least one valid stat data
|
||||
waitFirst.Wait()
|
||||
|
||||
var errs []error
|
||||
var errs []string
|
||||
cStats.mu.RLock()
|
||||
for _, c := range cStats.cs {
|
||||
if err := c.GetError(); err != nil {
|
||||
errs = append(errs, err)
|
||||
errs = append(errs, err.Error())
|
||||
}
|
||||
}
|
||||
cStats.mu.RUnlock()
|
||||
if err := errors.Join(errs...); err != nil {
|
||||
return err
|
||||
if len(errs) > 0 {
|
||||
return errors.New(strings.Join(errs, "\n"))
|
||||
}
|
||||
}
|
||||
|
||||
@ -265,50 +265,31 @@ func RunStats(ctx context.Context, dockerCLI command.Cli, options *StatsOptions)
|
||||
// so we unlikely hit this code in practice.
|
||||
daemonOSType = dockerCLI.ServerInfo().OSType
|
||||
}
|
||||
|
||||
// Buffer to store formatted stats text.
|
||||
// Once formatted, it will be printed in one write to avoid screen flickering.
|
||||
var statsTextBuffer bytes.Buffer
|
||||
|
||||
statsCtx := formatter.Context{
|
||||
Output: &statsTextBuffer,
|
||||
Output: dockerCLI.Out(),
|
||||
Format: NewStatsFormat(format, daemonOSType),
|
||||
}
|
||||
cleanScreen := func() {
|
||||
if !options.NoStream {
|
||||
_, _ = fmt.Fprint(dockerCLI.Out(), "\033[2J")
|
||||
_, _ = fmt.Fprint(dockerCLI.Out(), "\033[H")
|
||||
}
|
||||
}
|
||||
|
||||
var err error
|
||||
ticker := time.NewTicker(500 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
cleanScreen()
|
||||
var ccStats []StatsEntry
|
||||
cStats.mu.RLock()
|
||||
for _, c := range cStats.cs {
|
||||
ccStats = append(ccStats, c.GetStatistics())
|
||||
}
|
||||
cStats.mu.RUnlock()
|
||||
|
||||
if !options.NoStream {
|
||||
// Start by moving the cursor to the top-left
|
||||
_, _ = fmt.Fprint(&statsTextBuffer, "\033[H")
|
||||
}
|
||||
|
||||
if err = statsFormatWrite(statsCtx, ccStats, daemonOSType, !options.NoTrunc); err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
if !options.NoStream {
|
||||
for _, line := range strings.Split(statsTextBuffer.String(), "\n") {
|
||||
// In case the new text is shorter than the one we are writing over,
|
||||
// we'll append the "erase line" escape sequence to clear the remaining text.
|
||||
_, _ = fmt.Fprintln(&statsTextBuffer, line, "\033[K")
|
||||
}
|
||||
|
||||
// We might have fewer containers than before, so let's clear the remaining text
|
||||
_, _ = fmt.Fprint(&statsTextBuffer, "\033[J")
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprint(dockerCLI.Out(), statsTextBuffer.String())
|
||||
statsTextBuffer.Reset()
|
||||
|
||||
if len(cStats.cs) == 0 && !showAll {
|
||||
break
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
@ -50,7 +50,7 @@ func (s *stats) isKnownContainer(cid string) (int, bool) {
|
||||
return -1, false
|
||||
}
|
||||
|
||||
func collect(ctx context.Context, s *Stats, cli client.ContainerAPIClient, streamStats bool, waitFirst *sync.WaitGroup) {
|
||||
func collect(ctx context.Context, s *Stats, cli client.APIClient, streamStats bool, waitFirst *sync.WaitGroup) {
|
||||
logrus.Debugf("collecting stats for %s", s.Container)
|
||||
var (
|
||||
getFirst bool
|
||||
@ -78,7 +78,7 @@ func collect(ctx context.Context, s *Stats, cli client.ContainerAPIClient, strea
|
||||
go func() {
|
||||
for {
|
||||
var (
|
||||
v *container.StatsResponse
|
||||
v *types.StatsJSON
|
||||
memPercent, cpuPercent float64
|
||||
blkRead, blkWrite uint64 // Only used on Linux
|
||||
mem, memLimit float64
|
||||
@ -163,7 +163,7 @@ func collect(ctx context.Context, s *Stats, cli client.ContainerAPIClient, strea
|
||||
}
|
||||
}
|
||||
|
||||
func calculateCPUPercentUnix(previousCPU, previousSystem uint64, v *container.StatsResponse) float64 {
|
||||
func calculateCPUPercentUnix(previousCPU, previousSystem uint64, v *types.StatsJSON) float64 {
|
||||
var (
|
||||
cpuPercent = 0.0
|
||||
// calculate the change for the cpu usage of the container in between readings
|
||||
@ -182,7 +182,7 @@ func calculateCPUPercentUnix(previousCPU, previousSystem uint64, v *container.St
|
||||
return cpuPercent
|
||||
}
|
||||
|
||||
func calculateCPUPercentWindows(v *container.StatsResponse) float64 {
|
||||
func calculateCPUPercentWindows(v *types.StatsJSON) float64 {
|
||||
// Max number of 100ns intervals between the previous time read and now
|
||||
possIntervals := uint64(v.Read.Sub(v.PreRead).Nanoseconds()) // Start with number of ns intervals
|
||||
possIntervals /= 100 // Convert to number of 100ns intervals
|
||||
@ -198,7 +198,7 @@ func calculateCPUPercentWindows(v *container.StatsResponse) float64 {
|
||||
return 0.00
|
||||
}
|
||||
|
||||
func calculateBlockIO(blkio container.BlkioStats) (uint64, uint64) {
|
||||
func calculateBlockIO(blkio types.BlkioStats) (uint64, uint64) {
|
||||
var blkRead, blkWrite uint64
|
||||
for _, bioEntry := range blkio.IoServiceBytesRecursive {
|
||||
if len(bioEntry.Op) == 0 {
|
||||
@ -214,7 +214,7 @@ func calculateBlockIO(blkio container.BlkioStats) (uint64, uint64) {
|
||||
return blkRead, blkWrite
|
||||
}
|
||||
|
||||
func calculateNetwork(network map[string]container.NetworkStats) (float64, float64) {
|
||||
func calculateNetwork(network map[string]types.NetworkStats) (float64, float64) {
|
||||
var rx, tx float64
|
||||
|
||||
for _, v := range network {
|
||||
@ -236,7 +236,7 @@ func calculateNetwork(network map[string]container.NetworkStats) (float64, float
|
||||
//
|
||||
// On Docker 19.03 and older, the result was `mem.Usage - mem.Stats["cache"]`.
|
||||
// See https://github.com/moby/moby/issues/40727 for the background.
|
||||
func calculateMemUsageUnixNoCache(mem container.MemoryStats) float64 {
|
||||
func calculateMemUsageUnixNoCache(mem types.MemoryStats) float64 {
|
||||
// cgroup v1
|
||||
if v, isCgroup1 := mem.Stats["total_inactive_file"]; isCgroup1 && v < mem.Usage {
|
||||
return float64(mem.Usage - v)
|
||||
|
||||
@ -4,12 +4,18 @@ import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestCalculateMemUsageUnixNoCache(t *testing.T) {
|
||||
result := calculateMemUsageUnixNoCache(container.MemoryStats{Usage: 500, Stats: map[string]uint64{"total_inactive_file": 400}})
|
||||
// Given
|
||||
stats := types.MemoryStats{Usage: 500, Stats: map[string]uint64{"total_inactive_file": 400}}
|
||||
|
||||
// When
|
||||
result := calculateMemUsageUnixNoCache(stats)
|
||||
|
||||
// Then
|
||||
assert.Assert(t, inDelta(100.0, result, 1e-6))
|
||||
}
|
||||
|
||||
@ -30,28 +36,6 @@ func TestCalculateMemPercentUnixNoCache(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestCalculateBlockIO(t *testing.T) {
|
||||
blkRead, blkWrite := calculateBlockIO(container.BlkioStats{
|
||||
IoServiceBytesRecursive: []container.BlkioStatEntry{
|
||||
{Major: 8, Minor: 0, Op: "read", Value: 1234},
|
||||
{Major: 8, Minor: 1, Op: "read", Value: 4567},
|
||||
{Major: 8, Minor: 0, Op: "Read", Value: 6},
|
||||
{Major: 8, Minor: 1, Op: "Read", Value: 8},
|
||||
{Major: 8, Minor: 0, Op: "write", Value: 123},
|
||||
{Major: 8, Minor: 1, Op: "write", Value: 456},
|
||||
{Major: 8, Minor: 0, Op: "Write", Value: 6},
|
||||
{Major: 8, Minor: 1, Op: "Write", Value: 8},
|
||||
{Major: 8, Minor: 1, Op: "", Value: 456},
|
||||
},
|
||||
})
|
||||
if blkRead != 5815 {
|
||||
t.Fatalf("blkRead = %d, want 5815", blkRead)
|
||||
}
|
||||
if blkWrite != 593 {
|
||||
t.Fatalf("blkWrite = %d, want 593", blkWrite)
|
||||
}
|
||||
}
|
||||
|
||||
func inDelta(x, y, delta float64) func() (bool, string) {
|
||||
return func() (bool, string) {
|
||||
diff := x - y
|
||||
|
||||
30
cli/command/container/stats_unit_test.go
Normal file
30
cli/command/container/stats_unit_test.go
Normal file
@ -0,0 +1,30 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
)
|
||||
|
||||
func TestCalculateBlockIO(t *testing.T) {
|
||||
blkio := types.BlkioStats{
|
||||
IoServiceBytesRecursive: []types.BlkioStatEntry{
|
||||
{Major: 8, Minor: 0, Op: "read", Value: 1234},
|
||||
{Major: 8, Minor: 1, Op: "read", Value: 4567},
|
||||
{Major: 8, Minor: 0, Op: "Read", Value: 6},
|
||||
{Major: 8, Minor: 1, Op: "Read", Value: 8},
|
||||
{Major: 8, Minor: 0, Op: "write", Value: 123},
|
||||
{Major: 8, Minor: 1, Op: "write", Value: 456},
|
||||
{Major: 8, Minor: 0, Op: "Write", Value: 6},
|
||||
{Major: 8, Minor: 1, Op: "Write", Value: 8},
|
||||
{Major: 8, Minor: 1, Op: "", Value: 456},
|
||||
},
|
||||
}
|
||||
blkRead, blkWrite := calculateBlockIO(blkio)
|
||||
if blkRead != 5815 {
|
||||
t.Fatalf("blkRead = %d, want 5815", blkRead)
|
||||
}
|
||||
if blkWrite != 593 {
|
||||
t.Fatalf("blkWrite = %d, want 593", blkWrite)
|
||||
}
|
||||
}
|
||||
@ -2,13 +2,14 @@ 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/docker/docker/api/types/container"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@ -29,11 +30,8 @@ func NewStopCommand(dockerCli command.Cli) *cobra.Command {
|
||||
Short: "Stop one or more running containers",
|
||||
Args: cli.RequiresMinArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if cmd.Flags().Changed("time") && cmd.Flags().Changed("timeout") {
|
||||
return errors.New("conflicting options: cannot specify both --timeout and --time")
|
||||
}
|
||||
opts.containers = args
|
||||
opts.timeoutChanged = cmd.Flags().Changed("timeout") || cmd.Flags().Changed("time")
|
||||
opts.timeoutChanged = cmd.Flags().Changed("time")
|
||||
return runStop(cmd.Context(), dockerCli, &opts)
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
@ -44,37 +42,32 @@ func NewStopCommand(dockerCli command.Cli) *cobra.Command {
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.StringVarP(&opts.signal, "signal", "s", "", "Signal to send to the container")
|
||||
flags.IntVarP(&opts.timeout, "timeout", "t", 0, "Seconds to wait before killing the container")
|
||||
|
||||
// The --time option is deprecated, but kept for backward compatibility.
|
||||
flags.IntVar(&opts.timeout, "time", 0, "Seconds to wait before killing the container (deprecated: use --timeout)")
|
||||
_ = flags.MarkDeprecated("time", "use --timeout instead")
|
||||
|
||||
_ = cmd.RegisterFlagCompletionFunc("signal", completeSignals)
|
||||
|
||||
flags.IntVarP(&opts.timeout, "time", "t", 0, "Seconds to wait before killing the container")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runStop(ctx context.Context, dockerCLI command.Cli, opts *stopOptions) error {
|
||||
func runStop(ctx context.Context, dockerCli command.Cli, opts *stopOptions) error {
|
||||
var timeout *int
|
||||
if opts.timeoutChanged {
|
||||
timeout = &opts.timeout
|
||||
}
|
||||
|
||||
apiClient := dockerCLI.Client()
|
||||
errChan := parallelOperation(ctx, opts.containers, func(ctx context.Context, id string) error {
|
||||
return apiClient.ContainerStop(ctx, id, container.StopOptions{
|
||||
return dockerCli.Client().ContainerStop(ctx, id, container.StopOptions{
|
||||
Signal: opts.signal,
|
||||
Timeout: timeout,
|
||||
})
|
||||
})
|
||||
var errs []error
|
||||
var errs []string
|
||||
for _, ctr := range opts.containers {
|
||||
if err := <-errChan; err != nil {
|
||||
errs = append(errs, err)
|
||||
errs = append(errs, err.Error())
|
||||
continue
|
||||
}
|
||||
_, _ = fmt.Fprintln(dockerCLI.Out(), ctr)
|
||||
_, _ = fmt.Fprintln(dockerCli.Out(), ctr)
|
||||
}
|
||||
return errors.Join(errs...)
|
||||
if len(errs) > 0 {
|
||||
return errors.New(strings.Join(errs, "\n"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -1,96 +0,0 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"sort"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/errdefs"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
|
||||
func TestStop(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
args []string
|
||||
stopped []string
|
||||
expectedOpts container.StopOptions
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "without options",
|
||||
args: []string{"container-1", "container-2"},
|
||||
stopped: []string{"container-1", "container-2"},
|
||||
},
|
||||
{
|
||||
name: "with unknown container",
|
||||
args: []string{"container-1", "nosuchcontainer", "container-2"},
|
||||
expectedErr: "no such container",
|
||||
stopped: []string{"container-1", "container-2"},
|
||||
},
|
||||
{
|
||||
name: "with -t",
|
||||
args: []string{"-t", "2", "container-1"},
|
||||
expectedOpts: container.StopOptions{Timeout: func(to int) *int { return &to }(2)},
|
||||
stopped: []string{"container-1"},
|
||||
},
|
||||
{
|
||||
name: "with --timeout",
|
||||
args: []string{"--timeout", "2", "container-1"},
|
||||
expectedOpts: container.StopOptions{Timeout: func(to int) *int { return &to }(2)},
|
||||
stopped: []string{"container-1"},
|
||||
},
|
||||
{
|
||||
name: "with --time",
|
||||
args: []string{"--time", "2", "container-1"},
|
||||
expectedOpts: container.StopOptions{Timeout: func(to int) *int { return &to }(2)},
|
||||
stopped: []string{"container-1"},
|
||||
},
|
||||
{
|
||||
name: "conflicting options",
|
||||
args: []string{"--timeout", "2", "--time", "2", "container-1"},
|
||||
expectedErr: "conflicting options: cannot specify both --timeout and --time",
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var stopped []string
|
||||
mutex := new(sync.Mutex)
|
||||
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
containerStopFunc: func(ctx context.Context, containerID string, options container.StopOptions) error {
|
||||
assert.Check(t, is.DeepEqual(options, tc.expectedOpts))
|
||||
if containerID == "nosuchcontainer" {
|
||||
return errdefs.NotFound(errors.New("Error: no such container: " + containerID))
|
||||
}
|
||||
|
||||
// containerStopFunc is called in parallel for each container
|
||||
// so append must be synchronized.
|
||||
mutex.Lock()
|
||||
stopped = append(stopped, containerID)
|
||||
mutex.Unlock()
|
||||
return nil
|
||||
},
|
||||
Version: "1.36",
|
||||
})
|
||||
cmd := NewStopCommand(cli)
|
||||
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))
|
||||
}
|
||||
sort.Strings(stopped)
|
||||
assert.Check(t, is.DeepEqual(stopped, tc.stopped))
|
||||
})
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user