Compare commits

..

12 Commits

Author SHA1 Message Date
211e74b240 Merge pull request #5066 from vvoland/vendor-docker
Some checks failed
build / prepare-plugins (push) Has been cancelled
build / plugins (push) Has been cancelled
codeql / codeql (push) Has been cancelled
e2e / e2e (alpine, 23, connhelper-ssh) (push) Has been cancelled
e2e / e2e (alpine, 23, experimental) (push) Has been cancelled
e2e / e2e (alpine, 23, non-experimental) (push) Has been cancelled
e2e / e2e (alpine, 24, connhelper-ssh) (push) Has been cancelled
e2e / e2e (alpine, 24, experimental) (push) Has been cancelled
e2e / e2e (alpine, 24, non-experimental) (push) Has been cancelled
e2e / e2e (alpine, 25, connhelper-ssh) (push) Has been cancelled
e2e / e2e (alpine, 25, experimental) (push) Has been cancelled
e2e / e2e (alpine, 25, non-experimental) (push) Has been cancelled
e2e / e2e (debian, 23, connhelper-ssh) (push) Has been cancelled
e2e / e2e (debian, 23, experimental) (push) Has been cancelled
e2e / e2e (debian, 23, non-experimental) (push) Has been cancelled
e2e / e2e (debian, 24, connhelper-ssh) (push) Has been cancelled
e2e / e2e (debian, 24, experimental) (push) Has been cancelled
e2e / e2e (debian, 24, non-experimental) (push) Has been cancelled
e2e / e2e (debian, 25, connhelper-ssh) (push) Has been cancelled
e2e / e2e (debian, 25, experimental) (push) Has been cancelled
e2e / e2e (debian, 25, non-experimental) (push) Has been cancelled
test / ctn (push) Has been cancelled
test / host (macos-12) (push) Has been cancelled
validate / validate (lint) (push) Has been cancelled
validate / validate (shellcheck) (push) Has been cancelled
validate / validate (update-authors) (push) Has been cancelled
validate / validate (validate-vendor) (push) Has been cancelled
validate / validate-md (push) Has been cancelled
validate / validate-make (manpages) (push) Has been cancelled
validate / validate-make (yamldocs) (push) Has been cancelled
[26.1] vendor: github.com/docker/docker v26.1.2-dev (ef1912d8b6ae)
2024-05-08 11:31:07 +02:00
8beff78d85 Merge pull request #5065 from vvoland/v26.1-5064
[26.1 backport] update to go1.21.10
2024-05-08 11:30:55 +02:00
e64914c890 vendor: github.com/docker/docker v26.1.2-dev (ef1912d8b6ae)
- full diff: ac2de55998...ef1912d8b6

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-05-08 11:04:04 +02:00
c1d70d1fbb update to go1.21.10
These minor releases include 2 security fixes following the security policy:

- cmd/go: arbitrary code execution during build on darwin
On Darwin, building a Go module which contains CGO can trigger arbitrary code execution when using the Apple version of ld, due to
usage of the -lto_library flag in a "#cgo LDFLAGS" directive.
Thanks to Juho Forsén of Mattermost for reporting this issue.
This is CVE-2024-24787 and Go issue https://go.dev/issue/67119.

- net: malformed DNS message can cause infinite loop
A malformed DNS message in response to a query can cause the Lookup functions to get stuck in an infinite loop.
Thanks to long-name-let-people-remember-you on GitHub for reporting this issue, and to Mateusz Poliwczak for bringing the issue to
our attention.
This is CVE-2024-24788 and Go issue https://go.dev/issue/66754.

View the release notes for more information:
https://go.dev/doc/devel/release#go1.22.3

- https://github.com/golang/go/issues?q=milestone%3AGo1.21.10+label%3ACherryPickApproved
- full diff: https://github.com/golang/go/compare/go1.21.9...go1.21.10

**- Description for the changelog**

```markdown changelog
Update Go runtime to 1.21.10
```

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
(cherry picked from commit eb99994c75)
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-05-08 09:52:05 +02:00
53a3f0be18 Merge pull request #5062 from laurazard/cherry-pick-run-hang
[26.1 backport] Fix hang when container fails to start
2024-05-07 13:35:09 +02:00
4add46d686 Add e2e tests for run w/ bad entrypoint
Signed-off-by: Laura Brehm <laurabrehm@hey.com>
(cherry picked from commit 8d6e571c03)
Signed-off-by: Laura Brehm <laurabrehm@hey.com>
2024-05-07 11:57:36 +01:00
ccea7d8a30 Fix hang when container fails to start
Signed-off-by: Laura Brehm <laurabrehm@hey.com>
(cherry picked from commit 31644d5ea7)
Signed-off-by: Laura Brehm <laurabrehm@hey.com>
2024-05-07 11:57:24 +01:00
4cf5afaefa Merge pull request #5047 from vvoland/v26.1-5038
Some checks failed
build / prepare-plugins (push) Has been cancelled
build / plugins (push) Has been cancelled
codeql / codeql (push) Has been cancelled
e2e / e2e (alpine, 23, connhelper-ssh) (push) Has been cancelled
e2e / e2e (alpine, 23, experimental) (push) Has been cancelled
e2e / e2e (alpine, 23, non-experimental) (push) Has been cancelled
e2e / e2e (alpine, 24, connhelper-ssh) (push) Has been cancelled
e2e / e2e (alpine, 24, experimental) (push) Has been cancelled
e2e / e2e (alpine, 24, non-experimental) (push) Has been cancelled
e2e / e2e (alpine, 25, connhelper-ssh) (push) Has been cancelled
e2e / e2e (alpine, 25, experimental) (push) Has been cancelled
e2e / e2e (alpine, 25, non-experimental) (push) Has been cancelled
e2e / e2e (debian, 23, connhelper-ssh) (push) Has been cancelled
e2e / e2e (debian, 23, experimental) (push) Has been cancelled
e2e / e2e (debian, 23, non-experimental) (push) Has been cancelled
e2e / e2e (debian, 24, connhelper-ssh) (push) Has been cancelled
e2e / e2e (debian, 24, experimental) (push) Has been cancelled
e2e / e2e (debian, 24, non-experimental) (push) Has been cancelled
e2e / e2e (debian, 25, connhelper-ssh) (push) Has been cancelled
e2e / e2e (debian, 25, experimental) (push) Has been cancelled
e2e / e2e (debian, 25, non-experimental) (push) Has been cancelled
test / ctn (push) Has been cancelled
test / host (macos-12) (push) Has been cancelled
validate / validate (lint) (push) Has been cancelled
validate / validate (shellcheck) (push) Has been cancelled
validate / validate (update-authors) (push) Has been cancelled
validate / validate (validate-vendor) (push) Has been cancelled
validate / validate-md (push) Has been cancelled
validate / validate-make (manpages) (push) Has been cancelled
validate / validate-make (yamldocs) (push) Has been cancelled
[26.1 backport] cli-plugins: PluginRunCommand: use cmd.Environ instead of os.Environ
2024-04-30 13:40:03 +02:00
6c2b06d535 Merge pull request #5045 from vvoland/vendor-docker-26.1.1-dev
[26.1] vendor: github.com/docker/docker ac2de55998d4 (v26.1.1)
2024-04-30 13:12:54 +02:00
1c6a8ecf2e cli-plugins: PluginRunCommand: use cmd.Environ instead of os.Environ
Commit 5011759056 implemented a fix that
caused the current environment to be discarded, using `os.Environ()`.
On Windows, `os.Environ()` may produce an incorrect value for `PWD`,
for which a new function was added in go1.19;

- https://tip.golang.org/doc/go1.19#osexecpkgosexec
- https://go-review.googlesource.com/c/go/+/401340

Replace the use of `os.Environ()` with `cmd.Environ()` to address that.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 5ccb48459b)
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-04-30 13:03:15 +02:00
6d1c387af2 vendor: github.com/docker/docker ac2de55998d4 (v26.1.1)
full diff: c8af8ebe4a...ac2de55998

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-04-30 11:55:51 +02:00
1e6db5d24b Merge pull request #5044 from vvoland/wait-cancel-noerror
[26.1 backport] waitExitOrRemoved: Handle context cancellation
2024-04-29 17:46:55 +02:00
1614 changed files with 43220 additions and 78622 deletions

7
.github/CODEOWNERS vendored
View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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**

View File

@ -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:
@ -71,7 +61,7 @@ jobs:
uses: docker/setup-buildx-action@v3
-
name: Build
uses: docker/bake-action@v5
uses: docker/bake-action@v4
with:
targets: ${{ matrix.target }}
set: |
@ -87,20 +77,20 @@ jobs:
platformPair=${platform//\//-}
tar -cvzf "/tmp/out/docker-${platformPair}.tar.gz" .
if [ -z "${{ matrix.use_glibc }}" ]; then
echo "ARTIFACT_NAME=${{ matrix.target }}-${platformPair}" >> $GITHUB_ENV
echo "ARTIFACT_NAME=${{ matrix.target }}" >> $GITHUB_ENV
else
echo "ARTIFACT_NAME=${{ matrix.target }}-${platformPair}-glibc" >> $GITHUB_ENV
echo "ARTIFACT_NAME=${{ matrix.target }}-glibc" >> $GITHUB_ENV
fi
-
name: Upload artifacts
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: ${{ env.ARTIFACT_NAME }}
path: /tmp/out/*
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:
-
@ -132,7 +122,7 @@ jobs:
password: ${{ secrets.DOCKERHUB_CLIBIN_TOKEN }}
-
name: Build and push image
uses: docker/bake-action@v5
uses: docker/bake-action@v4
with:
files: |
./docker-bake.hcl
@ -144,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:
@ -162,7 +152,7 @@ jobs:
echo ${{ steps.platforms.outputs.matrix }}
plugins:
runs-on: ubuntu-24.04
runs-on: ubuntu-22.04
needs:
- prepare-plugins
strategy:
@ -178,7 +168,7 @@ jobs:
uses: docker/setup-buildx-action@v3
-
name: Build
uses: docker/bake-action@v5
uses: docker/bake-action@v4
with:
targets: plugins-cross
set: |

View File

@ -1,14 +1,5 @@
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:
@ -53,6 +44,16 @@ jobs:
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
@ -63,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.22.10"
-
name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: go
-
name: Autobuild
uses: github/codeql-action/autobuild@v3

View File

@ -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
@ -25,7 +16,7 @@ on:
jobs:
e2e:
runs-on: ubuntu-24.04
runs-on: ubuntu-22.04
strategy:
fail-fast: false
matrix:
@ -37,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
@ -49,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

View File

@ -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
@ -25,7 +16,7 @@ on:
jobs:
ctn:
runs-on: ubuntu-24.04
runs-on: ubuntu-22.04
steps:
-
name: Checkout
@ -35,7 +26,7 @@ jobs:
uses: docker/setup-buildx-action@v3
-
name: Test
uses: docker/bake-action@v5
uses: docker/bake-action@v4
with:
targets: test-coverage
-
@ -55,8 +46,7 @@ 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:
-
@ -74,7 +64,7 @@ jobs:
name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.22.10"
go-version: 1.21.10
-
name: Test
run: |

View File

@ -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]
@ -41,7 +32,7 @@ jobs:
desc=$(echo "$block" | awk NF)
if [ -z "$desc" ]; then
echo "::error::Changelog section is empty. Provide a description for the changelog."
echo "::error::Changelog section is empty. Please provide a description for the changelog."
exit 1
fi
@ -62,16 +53,10 @@ jobs:
# 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

View File

@ -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
@ -25,7 +16,7 @@ on:
jobs:
validate:
runs-on: ubuntu-24.04
runs-on: ubuntu-22.04
strategy:
fail-fast: false
matrix:
@ -40,13 +31,13 @@ jobs:
uses: actions/checkout@v4
-
name: Run
uses: docker/bake-action@v5
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
@ -66,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
View File

@ -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

View File

@ -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.
@ -44,6 +44,9 @@ linters:
run:
timeout: 5m
skip-files:
- cli/compose/schema/bindata.go
- .*generated.*
linters-settings:
depguard:
@ -54,16 +57,8 @@ linters-settings:
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
@ -96,17 +91,9 @@ 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
@ -117,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"
@ -137,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:
@ -146,6 +135,26 @@ 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:

View File

@ -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
View File

@ -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>

View File

@ -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:
@ -166,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
@ -269,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

View File

@ -1,15 +1,15 @@
# syntax=docker/dockerfile:1
ARG BASE_VARIANT=alpine
ARG ALPINE_VERSION=3.20
ARG ALPINE_VERSION=3.18
ARG BASE_DEBIAN_DISTRO=bookworm
ARG GO_VERSION=1.22.10
ARG XX_VERSION=1.6.1
ARG GO_VERSION=1.21.10
ARG XX_VERSION=1.4.0
ARG GOVERSIONINFO_VERSION=v1.3.0
ARG GOTESTSUM_VERSION=v1.10.0
ARG BUILDX_VERSION=0.18.0
ARG COMPOSE_VERSION=v2.30.3
ARG BUILDX_VERSION=0.12.1
ARG COMPOSE_VERSION=v2.24.3
FROM --platform=$BUILDPLATFORM tonistiigi/xx:${XX_VERSION} AS xx
@ -63,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 && \
@ -88,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/*

View File

@ -86,16 +86,6 @@ mod-outdated: ## check outdated dependencies
authors: ## generate AUTHORS file from git history
scripts/docs/generate-authors.sh
.PHONY: completion
completion: binary
completion: /etc/bash_completion.d/docker
completion: ## generate and install the completion scripts
.PHONY: /etc/bash_completion.d/docker
/etc/bash_completion.d/docker: ## generate and install the bash-completion script
mkdir -p /etc/bash_completion.d
docker completion bash > /etc/bash_completion.d/docker
.PHONY: manpages
manpages: ## generate man pages from go source and markdown
scripts/docs/generate-man.sh

2
NOTICE
View File

@ -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.

View File

@ -67,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.
@ -75,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

View File

@ -1 +1 @@
27.3.1-dev
25.0.0-dev

View File

@ -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()
}

View File

@ -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

View File

@ -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))

View File

@ -1,13 +1,11 @@
package manager
import (
"context"
"encoding/json"
"strings"
"github.com/docker/cli/cli-plugins/hooks"
"github.com/docker/cli/cli/command"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
@ -29,36 +27,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 +67,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,
@ -101,35 +92,11 @@ func invokeAndCollectHooks(ctx context.Context, dockerCli command.Cli, rootCmd,
if err != nil {
continue
}
var appended bool
nextSteps, appended = appendNextSteps(nextSteps, processedHook)
if !appended {
logrus.Debugf("Plugin %s responded with an empty hook message %q. Ignoring.", pluginName, string(hookReturn))
}
nextSteps = append(nextSteps, processedHook...)
}
return nextSteps
}
// appendNextSteps appends the processed hook output to the nextSteps slice.
// If the processed hook output is empty, it is not appended.
// Empty lines are not stripped if there's at least one non-empty line.
func appendNextSteps(nextSteps []string, processed []string) ([]string, bool) {
empty := true
for _, l := range processed {
if strings.TrimSpace(l) != "" {
empty = false
break
}
}
if empty {
return nextSteps, false
}
return append(nextSteps, processed...), true
}
// pluginMatch takes a plugin configuration and a string representing the
// command being executed (such as 'image ls' the root 'docker' is omitted)
// and, if the configuration includes a hook for the invoked command, returns

View File

@ -4,7 +4,6 @@ import (
"testing"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
func TestGetNaiveFlags(t *testing.T) {
@ -109,35 +108,3 @@ func TestPluginMatch(t *testing.T) {
assert.Equal(t, match, tc.expectedMatch)
}
}
func TestAppendNextSteps(t *testing.T) {
testCases := []struct {
processed []string
expectedOut []string
}{
{
processed: []string{},
expectedOut: []string{},
},
{
processed: []string{"", ""},
expectedOut: []string{},
},
{
processed: []string{"Some hint", "", "Some other hint"},
expectedOut: []string{"Some hint", "", "Some other hint"},
},
{
processed: []string{"Hint 1", "Hint 2"},
expectedOut: []string{"Hint 1", "Hint 2"},
},
}
for _, tc := range testCases {
t.Run("", func(t *testing.T) {
got, appended := appendNextSteps([]string{}, tc.processed)
assert.Check(t, is.DeepEqual(got, tc.expectedOut))
assert.Check(t, is.Equal(appended, len(got) > 0))
})
}
}

View File

@ -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

View File

@ -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", `

View File

@ -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",
}

View File

@ -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"),

View File

@ -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()

View File

@ -36,7 +36,13 @@ func RunPlugin(dockerCli *command.DockerCli, plugin *cobra.Command, meta manager
PersistentPreRunE = func(cmd *cobra.Command, _ []string) 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)
@ -45,26 +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())
err = tcmd.Initialize(opts...)
ogRunE := cmd.RunE
if ogRunE == nil {
ogRun := cmd.Run
// necessary because error will always be nil here
// see: https://github.com/golangci/golangci-lint/issues/1379
//nolint:unparam
ogRunE = func(cmd *cobra.Command, args []string) error {
ogRun(cmd, args)
return nil
}
cmd.Run = nil
}
cmd.RunE = func(cmd *cobra.Command, args []string) error {
stopInstrumentation := dockerCli.StartInstrumentation(cmd)
err := ogRunE(cmd, args)
stopInstrumentation(err)
return err
}
})
return err
}

View File

@ -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

View File

@ -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) {

View File

@ -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{

View File

@ -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)
}

View File

@ -42,7 +42,6 @@ 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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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,8 +92,6 @@ 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.
@ -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.
@ -284,15 +273,6 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...CLIOption)
return ResolveDefaultContext(cli.options, cli.contextStoreConfig)
},
}
// TODO(krissetto): pass ctx to the funcs instead of using this
if cli.enableGlobalMeter {
cli.createGlobalMeterProvider(cli.baseCtx)
}
if cli.enableGlobalTracer {
cli.createGlobalTracerProvider(cli.baseCtx)
}
return nil
}
@ -324,13 +304,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 {
@ -561,7 +541,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)

View File

@ -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,107 +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)
}
}

View File

@ -18,7 +18,6 @@ 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"
@ -87,41 +86,6 @@ 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) {
customVersion := "v3.3.3"
t.Setenv("DOCKER_API_VERSION", customVersion)
@ -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)
@ -289,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)

View File

@ -2,41 +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) ValidArgsFn {
func ImageNames(dockerCli command.Cli) ValidArgsFn {
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
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
}
@ -45,9 +33,9 @@ func ImageNames(dockerCLI APIClientProvider) 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(types.Container) 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 {
@ -57,10 +45,10 @@ func ContainerNames(dockerCLI APIClientProvider, all bool, filters ...func(types
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
}
@ -69,18 +57,18 @@ func ContainerNames(dockerCLI APIClientProvider, all bool, filters ...func(types
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
}
@ -93,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
}

View File

@ -1,351 +0,0 @@
package completion
import (
"context"
"errors"
"sort"
"testing"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/volume"
"github.com/docker/docker/client"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/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) ([]types.Container, 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) ([]types.Container, error) {
if c.containerListFunc != nil {
return c.containerListFunc(options)
}
return []types.Container{}, 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(types.Container) bool
containers []types.Container
expOut []string
expOpts container.ListOptions
expDirective cobra.ShellCompDirective
}{
{
doc: "no results",
expDirective: cobra.ShellCompDirectiveNoFileComp,
},
{
doc: "all containers",
showAll: true,
containers: []types.Container{
{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: []types.Container{
{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: []types.Container{
{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(types.Container) bool{
func(container types.Container) bool { return container.State == "created" },
},
containers: []types.Container{
{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(types.Container) bool{
func(container types.Container) bool { return container.ID == "id-a" },
func(container types.Container) bool { return container.State == "created" },
},
containers: []types.Container{
{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 {
tc := tc
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) ([]types.Container, 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 {
tc := tc
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
},
}})
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 {
tc := tc
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 {
tc := tc
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))
})
}
}

View File

@ -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
}

View File

@ -43,18 +43,14 @@ func TestConfigCreateErrors(t *testing.T) {
},
}
for _, tc := range testCases {
tc := tc
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)
}
}

View File

@ -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

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}
@ -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))
}

View File

@ -2,6 +2,7 @@ package container
import (
"context"
"fmt"
"io"
"github.com/docker/cli/cli"
@ -73,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 {
@ -105,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)
}
@ -147,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
}
@ -159,15 +153,12 @@ 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)}
}
case err := <-errC:
if errors.Is(err, context.Canceled) {
return nil
}
return err
}

View File

@ -1,7 +1,7 @@
package container
import (
"context"
"fmt"
"io"
"testing"
@ -70,19 +70,19 @@ func TestNewAttachCommandErrors(t *testing.T) {
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
cmd := NewAttachCommand(test.NewFakeCli(&fakeClient{inspectFunc: tc.containerInspectFunc}))
cmd.SetOut(io.Discard)
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
@ -110,24 +110,16 @@ func TestGetExitStatus(t *testing.T) {
},
expectedError: cli.StatusError{StatusCode: 15},
},
{
err: context.Canceled,
expectedError: nil,
},
}
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 {

View File

@ -17,18 +17,18 @@ import (
type fakeClient struct {
client.Client
inspectFunc func(string) (types.ContainerJSON, error)
execInspectFunc func(execID string) (container.ExecInspect, error)
execCreateFunc func(containerID string, options container.ExecOptions) (types.IDResponse, 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) ([]types.Container, error)
@ -36,8 +36,7 @@ type fakeClient struct {
containerExecResizeFunc func(id string, options container.ResizeOptions) error
containerRemoveFunc func(ctx context.Context, containerID string, options container.RemoveOptions) 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)
containerPruneFunc func(ctx context.Context, pruneFilters filters.Args) (types.ContainersPruneReport, error)
Version string
}
@ -55,21 +54,21 @@ func (f *fakeClient) ContainerInspect(_ context.Context, containerID string) (ty
return types.ContainerJSON{}, nil
}
func (f *fakeClient) ContainerExecCreate(_ context.Context, containerID string, config container.ExecOptions) (types.IDResponse, 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 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 (f *fakeClient) ContainerExecStart(context.Context, string, container.ExecStartOptions) error {
func (f *fakeClient) ContainerExecStart(context.Context, string, types.ExecStartCheck) error {
return nil
}
@ -94,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
}
@ -108,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) {
@ -168,16 +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) 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
return types.ContainersPruneReport{}, nil
}

View File

@ -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)
}

View File

@ -1,135 +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"
"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) ([]types.Container, 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) ([]types.Container, error) {
return []types.Container{
*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)))
}

View File

@ -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"
@ -234,7 +235,7 @@ func copyFromContainer(ctx context.Context, dockerCli command.Cli, copyConfig cp
// 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)
@ -318,7 +319,7 @@ func copyToContainer(ctx context.Context, dockerCli command.Cli, copyConfig cpCo
// 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)
@ -396,7 +397,7 @@ func copyToContainer(ctx context.Context, dockerCli command.Cli, copyConfig cpCo
}
}
options := container.CopyToContainerOptions{
options := types.CopyToContainerOptions{
AllowOverwriteDirWithFile: false,
CopyUIDGID: copyConfig.copyUIDGID,
}
@ -432,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
}

View File

@ -9,7 +9,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/docker/pkg/archive"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
@ -51,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()))
@ -98,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"))
}
@ -116,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" {
@ -130,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"
@ -179,24 +180,21 @@ func TestSplitCpArg(t *testing.T) {
expectedContainer: "container",
},
}
for _, tc := range testcases {
tc := tc
t.Run(tc.doc, func(t *testing.T) {
skip.If(t, tc.os == "windows" && runtime.GOOS != "windows" || tc.os == "linux" && runtime.GOOS == "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)

View File

@ -7,7 +7,7 @@ import (
"os"
"regexp"
"github.com/containerd/platforms"
"github.com/containerd/containerd/platforms"
"github.com/distribution/reference"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
@ -77,16 +77,6 @@ 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
}
@ -140,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 jsonmessage.DisplayJSONMessagesToStream(responseBody, out, nil)
return jsonmessage.DisplayJSONMessagesToStream(responseBody, streams.NewOut(out), nil)
}
type cidFile struct {
@ -249,7 +239,7 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *c
if options.platform != "" && versions.GreaterThanOrEqualTo(dockerCli.Client().ClientVersion(), "1.41") {
p, err := platforms.Parse(options.platform)
if err != nil {
return "", errors.Wrap(errdefs.InvalidParameter(err), "error parsing specified platform")
return "", errors.Wrap(err, "error parsing specified platform")
}
platform = &p
}

View File

@ -3,6 +3,7 @@ package container
import (
"context"
"errors"
"fmt"
"io"
"os"
"runtime"
@ -133,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
},
@ -230,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)

View File

@ -4,6 +4,7 @@ import (
"context"
"fmt"
"io"
"os"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
@ -11,8 +12,7 @@ import (
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/opts"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
apiclient "github.com/docker/docker/client"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
@ -43,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 types.Container) 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",
@ -78,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
}
@ -118,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
}
@ -178,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)
}
}
@ -197,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}
@ -218,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,
@ -230,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
}

View File

@ -11,7 +11,6 @@ import (
"github.com/docker/cli/internal/test"
"github.com/docker/cli/opts"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/pkg/errors"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
@ -38,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,
@ -49,7 +48,7 @@ TWO=2
options: withDefaultOpts(ExecOptions{}),
},
{
expected: container.ExecOptions{
expected: types.ExecConfig{
Cmd: []string{"command1", "command2"},
AttachStdout: true,
AttachStderr: true,
@ -64,7 +63,7 @@ TWO=2
TTY: true,
User: "uid",
}),
expected: container.ExecOptions{
expected: types.ExecConfig{
User: "uid",
AttachStdin: true,
AttachStdout: true,
@ -75,7 +74,7 @@ TWO=2
},
{
options: withDefaultOpts(ExecOptions{Detach: true}),
expected: container.ExecOptions{
expected: types.ExecConfig{
Detach: true,
Cmd: []string{"command"},
},
@ -86,7 +85,7 @@ TWO=2
Interactive: true,
Detach: true,
}),
expected: container.ExecOptions{
expected: types.ExecConfig{
Detach: true,
Tty: true,
Cmd: []string{"command"},
@ -95,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,
@ -107,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,
@ -127,7 +126,7 @@ TWO=2
}(),
},
{
expected: container.ExecOptions{
expected: types.ExecConfig{
Cmd: []string{"command"},
AttachStdout: true,
AttachStderr: true,
@ -162,7 +161,7 @@ func TestRunExec(t *testing.T) {
testcases := []struct {
doc string
options ExecOptions
client *fakeClient
client fakeClient
expectedError string
expectedOut string
expectedErr string
@ -172,12 +171,12 @@ 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{
client: fakeClient{
inspectFunc: func(string) (types.ContainerJSON, error) {
return types.ContainerJSON{}, errors.New("failed inspect")
},
@ -188,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 != "" {
@ -208,7 +206,7 @@ func TestRunExec(t *testing.T) {
}
}
func execCreateWithID(_ string, _ container.ExecOptions) (types.IDResponse, error) {
func execCreateWithID(_ string, _ types.ExecConfig) (types.IDResponse, error) {
return types.IDResponse{ID: "execid"}, nil
}
@ -237,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)

View File

@ -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()

View File

@ -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

View File

@ -38,9 +38,6 @@ 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
}
@ -53,7 +50,7 @@ func runKill(ctx context.Context, dockerCli command.Cli, opts *killOptions) erro
if err := <-errChan; err != nil {
errs = append(errs, err.Error())
} else {
_, _ = fmt.Fprintln(dockerCli.Out(), name)
fmt.Fprintln(dockerCli.Out(), name)
}
}
if len(errs) > 0 {

View File

@ -1,7 +1,7 @@
package container
import (
"errors"
"fmt"
"io"
"testing"
@ -128,6 +128,7 @@ func TestContainerListBuildContainerListOptions(t *testing.T) {
func TestContainerListErrors(t *testing.T) {
testCases := []struct {
args []string
flags map[string]string
containerListFunc func(container.ListOptions) ([]types.Container, error)
expectedError string
@ -146,7 +147,7 @@ func TestContainerListErrors(t *testing.T) {
},
{
containerListFunc: func(_ container.ListOptions) ([]types.Container, error) {
return nil, errors.New("error listing containers")
return nil, fmt.Errorf("error listing containers")
},
expectedError: "error listing containers",
},
@ -157,12 +158,11 @@ 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)
}
}
@ -180,9 +180,6 @@ 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")
}
@ -197,9 +194,6 @@ func TestContainerListNoTrunc(t *testing.T) {
},
})
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")
@ -216,9 +210,6 @@ func TestContainerListNamesMultipleTime(t *testing.T) {
},
})
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")
@ -235,9 +226,6 @@ func TestContainerListFormatTemplateWithArg(t *testing.T) {
},
})
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")
@ -287,9 +275,6 @@ func TestContainerListFormatSizeSetsOption(t *testing.T) {
},
})
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))
@ -312,9 +297,6 @@ 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")
}
@ -332,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")
@ -343,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())

View File

@ -30,7 +30,7 @@ func TestRunLogs(t *testing.T) {
testcases := []struct {
doc string
options *logsOptions
client *fakeClient
client fakeClient
expectedError string
expectedOut string
expectedErr string
@ -39,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 != "" {

View File

@ -208,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")
@ -574,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{

View File

@ -335,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)
}
}
@ -921,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)
}

View File

@ -2,11 +2,10 @@ package container
import (
"context"
"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"
)
@ -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)
}

View File

@ -43,9 +43,6 @@ 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, "time", "t", 0, "Seconds to wait before killing the container")
_ = cmd.RegisterFlagCompletionFunc("signal", completeSignals)
return cmd
}

View File

@ -8,7 +8,6 @@ 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/docker/docker/errdefs"
"github.com/pkg/errors"
@ -39,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 types.Container) bool {
return opts.force || ctr.State == "exited" || ctr.State == "created"
}),
ValidArgsFunction: completion.ContainerNames(dockerCli, true),
}
flags := cmd.Flags()

View File

@ -2,7 +2,7 @@ package container
import (
"context"
"errors"
"fmt"
"io"
"sort"
"sync"
@ -37,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
},
@ -45,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()

View File

@ -4,6 +4,7 @@ import (
"context"
"fmt"
"io"
"os"
"strings"
"syscall"
@ -69,16 +70,22 @@ 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
}
@ -133,6 +140,9 @@ 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 {
reportError(stderr, "run", err.Error(), true)
@ -140,18 +150,10 @@ func runContainer(ctx context.Context, dockerCli command.Cli, runOpts *runOption
}
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
@ -171,9 +173,6 @@ func runContainer(ctx context.Context, dockerCli command.Cli, runOpts *runOption
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,

View File

@ -2,25 +2,16 @@ 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"
@ -41,239 +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) {
select {
case <-ctx.Done():
return container.CreateResponse{}, ctx.Err()
default:
}
return container.CreateResponse{}, fakeNotFound{}
},
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{"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{
StatusCode: 125,
})
case <-time.After(10 * time.Second):
t.Fatal("cmd did not return before the timeout")
}
}
func TestRunCommandWithContentTrustErrors(t *testing.T) {
testCases := []struct {
name string
@ -301,27 +59,23 @@ func TestRunCommandWithContentTrustErrors(t *testing.T) {
},
}
for _, tc := range testCases {
tc := tc
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()
assert.Assert(t, err != nil)
assert.Assert(t, is.Contains(fakeCLI.ErrBuffer().String(), 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))
}
}

View File

@ -87,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)
}

View File

@ -1,7 +1,6 @@
package container
import (
"bytes"
"context"
"fmt"
"io"
@ -14,6 +13,7 @@ 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"
@ -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
@ -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.Fprint(&statsTextBuffer, line, "\033[K\n")
}
// 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
}

View File

@ -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)

View File

@ -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

View 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)
}
}

View File

@ -43,9 +43,6 @@ 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, "time", "t", 0, "Seconds to wait before killing the container")
_ = cmd.RegisterFlagCompletionFunc("signal", completeSignals)
return cmd
}

View File

@ -83,8 +83,6 @@ func NewUpdateCommand(dockerCli command.Cli) *cobra.Command {
flags.Var(&options.cpus, "cpus", "Number of CPUs")
flags.SetAnnotation("cpus", "version", []string{"1.29"})
_ = cmd.RegisterFlagCompletionFunc("restart", completeRestartPolicies)
return cmd
}

View File

@ -5,6 +5,7 @@ import (
"errors"
"strconv"
"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"
@ -67,11 +68,11 @@ func legacyWaitExitOrRemoved(ctx context.Context, apiClient client.APIClient, co
f := filters.NewArgs()
f.Add("type", "container")
f.Add("container", containerID)
eventCtx, cancel := context.WithCancel(ctx)
eventq, errq := apiClient.Events(eventCtx, events.ListOptions{
options := types.EventsOptions{
Filters: f,
})
}
eventCtx, cancel := context.WithCancel(ctx)
eventq, errq := apiClient.Events(eventCtx, options)
eventProcessor := func(e events.Message) bool {
stopProcessing := false

View File

@ -38,7 +38,7 @@ func waitFn(cid string) (<-chan container.WaitResponse, <-chan error) {
}
func TestWaitExitOrRemoved(t *testing.T) {
tests := []struct {
testcases := []struct {
cid string
exitCode int
}{
@ -61,11 +61,9 @@ func TestWaitExitOrRemoved(t *testing.T) {
}
client := &fakeClient{waitFunc: waitFn, Version: api.DefaultVersion}
for _, tc := range tests {
t.Run(tc.cid, func(t *testing.T) {
statusC := waitExitOrRemoved(context.Background(), client, tc.cid, true)
exitCode := <-statusC
assert.Check(t, is.Equal(tc.exitCode, exitCode))
})
for _, testcase := range testcases {
statusC := waitExitOrRemoved(context.Background(), client, testcase.cid, true)
exitCode := <-statusC
assert.Check(t, is.Equal(testcase.exitCode, exitCode))
}
}

View File

@ -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

View File

@ -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 context
@ -24,10 +24,6 @@ type CreateOptions struct {
Description string
Docker map[string]string
From string
// Additional Metadata to store in the context. This option is not
// currently exposed to the user.
metaData map[string]any
}
func longCreateDescription() string {
@ -98,8 +94,7 @@ func createNewContext(contextStore store.ReaderWriter, o *CreateOptions) error {
docker.DockerEndpoint: dockerEP,
},
Metadata: command.DockerContext{
Description: o.Description,
AdditionalFields: o.metaData,
Description: o.Description,
},
Name: o.Name,
}

View File

@ -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 context

View File

@ -8,18 +8,14 @@ import (
"path/filepath"
"testing"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/streams"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
func TestExportImportWithFile(t *testing.T) {
contextFile := filepath.Join(t.TempDir(), "exported")
cli := makeFakeCli(t)
createTestContext(t, cli, "test", map[string]any{
"MyCustomMetadata": t.Name(),
})
createTestContext(t, cli, "test")
cli.ErrBuffer().Reset()
assert.NilError(t, RunExport(cli, &ExportOptions{
ContextName: "test",
@ -33,26 +29,18 @@ func TestExportImportWithFile(t *testing.T) {
assert.NilError(t, err)
context2, err := cli.ContextStore().GetMetadata("test2")
assert.NilError(t, err)
assert.DeepEqual(t, context1.Endpoints, context2.Endpoints)
assert.DeepEqual(t, context1.Metadata, context2.Metadata)
assert.Equal(t, "test", context1.Name)
assert.Equal(t, "test2", context2.Name)
assert.Check(t, is.DeepEqual(context1.Metadata, command.DockerContext{
Description: "description of test",
AdditionalFields: map[string]any{"MyCustomMetadata": t.Name()},
}))
assert.Check(t, is.DeepEqual(context1.Endpoints, context2.Endpoints))
assert.Check(t, is.DeepEqual(context1.Metadata, context2.Metadata))
assert.Check(t, is.Equal("test", context1.Name))
assert.Check(t, is.Equal("test2", context2.Name))
assert.Check(t, is.Equal("test2\n", cli.OutBuffer().String()))
assert.Check(t, is.Equal("Successfully imported context \"test2\"\n", cli.ErrBuffer().String()))
assert.Equal(t, "test2\n", cli.OutBuffer().String())
assert.Equal(t, "Successfully imported context \"test2\"\n", cli.ErrBuffer().String())
}
func TestExportImportPipe(t *testing.T) {
cli := makeFakeCli(t)
createTestContext(t, cli, "test", map[string]any{
"MyCustomMetadata": t.Name(),
})
createTestContext(t, cli, "test")
cli.ErrBuffer().Reset()
cli.OutBuffer().Reset()
assert.NilError(t, RunExport(cli, &ExportOptions{
@ -68,19 +56,13 @@ func TestExportImportPipe(t *testing.T) {
assert.NilError(t, err)
context2, err := cli.ContextStore().GetMetadata("test2")
assert.NilError(t, err)
assert.DeepEqual(t, context1.Endpoints, context2.Endpoints)
assert.DeepEqual(t, context1.Metadata, context2.Metadata)
assert.Equal(t, "test", context1.Name)
assert.Equal(t, "test2", context2.Name)
assert.Check(t, is.DeepEqual(context1.Metadata, command.DockerContext{
Description: "description of test",
AdditionalFields: map[string]any{"MyCustomMetadata": t.Name()},
}))
assert.Check(t, is.DeepEqual(context1.Endpoints, context2.Endpoints))
assert.Check(t, is.DeepEqual(context1.Metadata, context2.Metadata))
assert.Check(t, is.Equal("test", context1.Name))
assert.Check(t, is.Equal("test2", context2.Name))
assert.Check(t, is.Equal("test2\n", cli.OutBuffer().String()))
assert.Check(t, is.Equal("Successfully imported context \"test2\"\n", cli.ErrBuffer().String()))
assert.Equal(t, "test2\n", cli.OutBuffer().String())
assert.Equal(t, "Successfully imported context \"test2\"\n", cli.ErrBuffer().String())
}
func TestExportExistingFile(t *testing.T) {

View File

@ -42,7 +42,7 @@ func writeTo(dockerCli command.Cli, reader io.Reader, dest string) error {
var printDest bool
if dest == "-" {
if dockerCli.Out().IsTerminal() {
return errors.New("cowardly refusing to export to a terminal, specify a file path")
return errors.New("cowardly refusing to export to a terminal, please specify a file path")
}
writer = dockerCli.Out()
} else {

View File

@ -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 context

View File

@ -10,9 +10,7 @@ import (
func TestInspect(t *testing.T) {
cli := makeFakeCli(t)
createTestContext(t, cli, "current", map[string]any{
"MyCustomMetadata": "MyCustomMetadataValue",
})
createTestContext(t, cli, "current")
cli.OutBuffer().Reset()
assert.NilError(t, runInspect(cli, inspectOptions{
refs: []string{"current"},

View File

@ -1,6 +1,3 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.22
package context
import (
@ -69,8 +66,6 @@ func runList(dockerCli command.Cli, opts *listOptions) error {
Name: rawMeta.Name,
Current: isCurrent,
Error: err.Error(),
ContextType: getContextType(nil, opts.format),
})
continue
}
@ -85,8 +80,6 @@ func runList(dockerCli command.Cli, opts *listOptions) error {
Description: meta.Description,
DockerEndpoint: dockerEndpoint.Host,
Error: errMsg,
ContextType: getContextType(meta.AdditionalFields, opts.format),
}
contexts = append(contexts, &desc)
}
@ -103,8 +96,6 @@ func runList(dockerCli command.Cli, opts *listOptions) error {
Name: curContext,
Current: true,
Error: errMsg,
ContextType: getContextType(nil, opts.format),
})
}
sort.Slice(contexts, func(i, j int) bool {
@ -120,30 +111,6 @@ func runList(dockerCli command.Cli, opts *listOptions) error {
return nil
}
// getContextType sets the LegacyContextType field for compatibility with
// Visual Studio, which depends on this field from the "cloud integration"
// wrapper.
//
// https://github.com/docker/compose-cli/blob/c156ce6da4c2b317174d42daf1b019efa87e9f92/api/context/store/contextmetadata.go#L28-L34
// https://github.com/docker/compose-cli/blob/c156ce6da4c2b317174d42daf1b019efa87e9f92/api/context/store/store.go#L34-L51
//
// TODO(thaJeztah): remove this and [ClientContext.ContextType] once Visual Studio is updated to no longer depend on this.
func getContextType(meta map[string]any, format string) string {
if format != formatter.JSONFormat && format != formatter.JSONFormatKey {
// We only need the ContextType field when formatting as JSON,
// which is the format-string used by Visual Studio to detect the
// context-type.
return ""
}
if ct, ok := meta["Type"]; ok {
// If the context on-disk has a context-type (ecs, aci), return it.
return ct.(string)
}
// Use the default context-type.
return "moby"
}
func format(dockerCli command.Cli, opts *listOptions, contexts []*formatter.ClientContext) error {
contextCtx := formatter.Context{
Output: dockerCli.Out(),

View File

@ -4,70 +4,36 @@ import (
"testing"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/formatter"
"gotest.tools/v3/assert"
"gotest.tools/v3/golden"
)
func createTestContexts(t *testing.T, cli command.Cli, name ...string) {
t.Helper()
for _, n := range name {
createTestContext(t, cli, n, nil)
}
}
func createTestContext(t *testing.T, cli command.Cli, name string, metaData map[string]any) {
func createTestContext(t *testing.T, cli command.Cli, name string) {
t.Helper()
err := RunCreate(cli, &CreateOptions{
Name: name,
Description: "description of " + name,
Docker: map[string]string{keyHost: "https://someswarmserver.example.com"},
metaData: metaData,
})
assert.NilError(t, err)
}
func TestList(t *testing.T) {
cli := makeFakeCli(t)
createTestContexts(t, cli, "current", "other", "unset")
createTestContext(t, cli, "current")
createTestContext(t, cli, "other")
createTestContext(t, cli, "unset")
cli.SetCurrentContext("current")
cli.OutBuffer().Reset()
assert.NilError(t, runList(cli, &listOptions{}))
golden.Assert(t, cli.OutBuffer().String(), "list.golden")
}
func TestListJSON(t *testing.T) {
cli := makeFakeCli(t)
createTestContext(t, cli, "current", nil)
createTestContext(t, cli, "context1", map[string]any{"Type": "aci"})
createTestContext(t, cli, "context2", map[string]any{"Type": "ecs"})
createTestContext(t, cli, "context3", map[string]any{"Type": "moby"})
cli.SetCurrentContext("current")
t.Run("format={{json .}}", func(t *testing.T) {
cli.OutBuffer().Reset()
assert.NilError(t, runList(cli, &listOptions{format: formatter.JSONFormat}))
golden.Assert(t, cli.OutBuffer().String(), "list-json.golden")
})
t.Run("format=json", func(t *testing.T) {
cli.OutBuffer().Reset()
assert.NilError(t, runList(cli, &listOptions{format: formatter.JSONFormatKey}))
golden.Assert(t, cli.OutBuffer().String(), "list-json.golden")
})
t.Run("format={{ json .Name }}", func(t *testing.T) {
cli.OutBuffer().Reset()
assert.NilError(t, runList(cli, &listOptions{format: `{{ json .Name }}`}))
golden.Assert(t, cli.OutBuffer().String(), "list-json-name.golden")
})
}
func TestListQuiet(t *testing.T) {
cli := makeFakeCli(t)
createTestContexts(t, cli, "current", "other")
createTestContext(t, cli, "current")
createTestContext(t, cli, "other")
cli.SetCurrentContext("current")
cli.OutBuffer().Reset()
assert.NilError(t, runList(cli, &listOptions{quiet: true}))

View File

@ -1,6 +1,7 @@
package context
import (
"fmt"
"strconv"
"strings"
@ -75,7 +76,7 @@ func validateConfig(config map[string]string, allowedKeys map[string]struct{}) e
var errs []string
for k := range config {
if _, ok := allowedKeys[k]; !ok {
errs = append(errs, "unrecognized config key: "+k)
errs = append(errs, fmt.Sprintf("%s: unrecognized config key", k))
}
}
if len(errs) == 0 {

View File

@ -13,7 +13,8 @@ import (
func TestRemove(t *testing.T) {
cli := makeFakeCli(t)
createTestContexts(t, cli, "current", "other")
createTestContext(t, cli, "current")
createTestContext(t, cli, "other")
assert.NilError(t, RunRemove(cli, RemoveOptions{}, []string{"other"}))
_, err := cli.ContextStore().GetMetadata("current")
assert.NilError(t, err)
@ -23,7 +24,8 @@ func TestRemove(t *testing.T) {
func TestRemoveNotAContext(t *testing.T) {
cli := makeFakeCli(t)
createTestContexts(t, cli, "current", "other")
createTestContext(t, cli, "current")
createTestContext(t, cli, "other")
err := RunRemove(cli, RemoveOptions{}, []string{"not-a-context"})
assert.ErrorContains(t, err, `context "not-a-context" does not exist`)
@ -33,7 +35,8 @@ func TestRemoveNotAContext(t *testing.T) {
func TestRemoveCurrent(t *testing.T) {
cli := makeFakeCli(t)
createTestContexts(t, cli, "current", "other")
createTestContext(t, cli, "current")
createTestContext(t, cli, "other")
cli.SetCurrentContext("current")
err := RunRemove(cli, RemoveOptions{}, []string{"current"})
assert.ErrorContains(t, err, `context "current" is in use, set -f flag to force remove`)
@ -47,7 +50,8 @@ func TestRemoveCurrentForce(t *testing.T) {
assert.NilError(t, testCfg.Save())
cli := makeFakeCli(t, withCliConfig(testCfg))
createTestContexts(t, cli, "current", "other")
createTestContext(t, cli, "current")
createTestContext(t, cli, "other")
cli.SetCurrentContext("current")
assert.NilError(t, RunRemove(cli, RemoveOptions{Force: true}, []string{"current"}))
reloadedConfig, err := config.Load(configDir)
@ -57,7 +61,7 @@ func TestRemoveCurrentForce(t *testing.T) {
func TestRemoveDefault(t *testing.T) {
cli := makeFakeCli(t)
createTestContext(t, cli, "other", nil)
createTestContext(t, cli, "other")
cli.SetCurrentContext("current")
err := RunRemove(cli, RemoveOptions{}, []string{"default"})
assert.ErrorContains(t, err, `default: context "default" cannot be removed`)

View File

@ -8,7 +8,7 @@ import (
func TestShow(t *testing.T) {
cli := makeFakeCli(t)
createTestContext(t, cli, "current", nil)
createTestContext(t, cli, "current")
cli.SetCurrentContext("current")
cli.OutBuffer().Reset()

View File

@ -2,8 +2,7 @@
{
"Name": "current",
"Metadata": {
"Description": "description of current",
"MyCustomMetadata": "MyCustomMetadataValue"
"Description": "description of current"
},
"Endpoints": {
"docker": {

View File

@ -1,5 +0,0 @@
"context1"
"context2"
"context3"
"current"
"default"

View File

@ -1,5 +0,0 @@
{"Name":"context1","Description":"description of context1","DockerEndpoint":"https://someswarmserver.example.com","Current":false,"Error":"","ContextType":"aci"}
{"Name":"context2","Description":"description of context2","DockerEndpoint":"https://someswarmserver.example.com","Current":false,"Error":"","ContextType":"ecs"}
{"Name":"context3","Description":"description of context3","DockerEndpoint":"https://someswarmserver.example.com","Current":false,"Error":"","ContextType":"moby"}
{"Name":"current","Description":"description of current","DockerEndpoint":"https://someswarmserver.example.com","Current":true,"Error":"","ContextType":"moby"}
{"Name":"default","Description":"Current DOCKER_HOST based configuration","DockerEndpoint":"unix:///var/run/docker.sock","Current":false,"Error":"","ContextType":"moby"}

View File

@ -6,7 +6,7 @@ import (
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/context/docker"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
"gotest.tools/v3/assert/cmp"
)
func TestUpdateDescriptionOnly(t *testing.T) {
@ -34,7 +34,7 @@ func TestUpdateDescriptionOnly(t *testing.T) {
func TestUpdateDockerOnly(t *testing.T) {
cli := makeFakeCli(t)
createTestContext(t, cli, "test", nil)
createTestContext(t, cli, "test")
assert.NilError(t, RunUpdate(cli, &UpdateOptions{
Name: "test",
Docker: map[string]string{
@ -46,7 +46,7 @@ func TestUpdateDockerOnly(t *testing.T) {
dc, err := command.GetDockerContext(c)
assert.NilError(t, err)
assert.Equal(t, dc.Description, "description of test")
assert.Check(t, is.Contains(c.Endpoints, docker.DockerEndpoint))
assert.Check(t, cmp.Contains(c.Endpoints, docker.DockerEndpoint))
assert.Equal(t, c.Endpoints[docker.DockerEndpoint].(docker.EndpointMeta).Host, "tcp://some-host")
}

View File

@ -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

View File

@ -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

Some files were not shown because too many files have changed in this diff Show More