Merge branch 'docker:master' into fix-swarm-init-help

Signed-off-by: Hernan Garcia <hernandanielg@gmail.com>
This commit is contained in:
Hernan Garcia 2024-04-19 14:35:02 -05:00
commit b0cd429db7
No known key found for this signature in database
GPG Key ID: 0234C5EF98912C93
2087 changed files with 146066 additions and 31537 deletions

View File

@ -1,19 +0,0 @@
# This is a dummy CircleCI config file to avoid GitHub status failures reported
# on branches that don't use CircleCI. This file should be deleted when all
# branches are no longer dependent on CircleCI.
version: 2
jobs:
dummy:
docker:
- image: busybox
steps:
- run:
name: "dummy"
command: echo "dummy job"
workflows:
version: 2
ci:
jobs:
- dummy

View File

@ -22,9 +22,13 @@ Please provide the following information:
**- Description for the changelog** **- Description for the changelog**
<!-- <!--
Write a short (one line) summary that describes the changes in this Write a short (one line) summary that describes the changes in this
pull request for inclusion in the changelog: pull request for inclusion in the changelog.
It must be placed inside the below triple backticks section:
--> -->
```markdown changelog
```
**- A picture of a cute animal (not mandatory but encouraged)** **- A picture of a cute animal (not mandatory but encouraged)**

View File

@ -4,6 +4,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true cancel-in-progress: true
env:
VERSION: ${{ github.ref }}
on: on:
workflow_dispatch: workflow_dispatch:
push: push:
@ -16,13 +19,13 @@ on:
jobs: jobs:
prepare: prepare:
runs-on: ubuntu-20.04 runs-on: ubuntu-22.04
outputs: outputs:
matrix: ${{ steps.platforms.outputs.matrix }} matrix: ${{ steps.platforms.outputs.matrix }}
steps: steps:
- -
name: Checkout name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
- -
name: Create matrix name: Create matrix
id: platforms id: platforms
@ -34,7 +37,7 @@ jobs:
echo ${{ steps.platforms.outputs.matrix }} echo ${{ steps.platforms.outputs.matrix }}
build: build:
runs-on: ubuntu-20.04 runs-on: ubuntu-22.04
needs: needs:
- prepare - prepare
strategy: strategy:
@ -50,15 +53,15 @@ jobs:
steps: steps:
- -
name: Checkout name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- -
name: Set up Docker Buildx name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v3
- -
name: Build name: Build
uses: docker/bake-action@v3 uses: docker/bake-action@v4
with: with:
targets: ${{ matrix.target }} targets: ${{ matrix.target }}
set: | set: |
@ -86,14 +89,58 @@ jobs:
path: /tmp/out/* path: /tmp/out/*
if-no-files-found: error if-no-files-found: error
bin-image:
runs-on: ubuntu-22.04
if: ${{ github.event_name != 'pull_request' && github.repository == 'docker/cli' }}
steps:
-
name: Checkout
uses: actions/checkout@v4
-
name: Set up QEMU
uses: docker/setup-qemu-action@v3
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
-
name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: dockereng/cli-bin
tags: |
type=semver,pattern={{version}}
type=ref,event=branch
type=ref,event=pr
type=sha
-
name: Login to DockerHub
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_CLIBIN_USERNAME }}
password: ${{ secrets.DOCKERHUB_CLIBIN_TOKEN }}
-
name: Build and push image
uses: docker/bake-action@v4
with:
files: |
./docker-bake.hcl
${{ steps.meta.outputs.bake-file }}
targets: bin-image-cross
push: ${{ github.event_name != 'pull_request' }}
set: |
*.cache-from=type=gha,scope=bin-image
*.cache-to=type=gha,scope=bin-image,mode=max
prepare-plugins: prepare-plugins:
runs-on: ubuntu-20.04 runs-on: ubuntu-22.04
outputs: outputs:
matrix: ${{ steps.platforms.outputs.matrix }} matrix: ${{ steps.platforms.outputs.matrix }}
steps: steps:
- -
name: Checkout name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
- -
name: Create matrix name: Create matrix
id: platforms id: platforms
@ -105,7 +152,7 @@ jobs:
echo ${{ steps.platforms.outputs.matrix }} echo ${{ steps.platforms.outputs.matrix }}
plugins: plugins:
runs-on: ubuntu-20.04 runs-on: ubuntu-22.04
needs: needs:
- prepare-plugins - prepare-plugins
strategy: strategy:
@ -115,13 +162,13 @@ jobs:
steps: steps:
- -
name: Checkout name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
- -
name: Set up Docker Buildx name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v3
- -
name: Build name: Build
uses: docker/bake-action@v3 uses: docker/bake-action@v4
with: with:
targets: plugins-cross targets: plugins-cross
set: | set: |

View File

@ -1,40 +0,0 @@
name: codeql
on:
schedule:
# ┌───────────── minute (0 - 59)
# │ ┌───────────── hour (0 - 23)
# │ │ ┌───────────── day of the month (1 - 31)
# │ │ │ ┌───────────── month (1 - 12)
# │ │ │ │ ┌───────────── day of the week (0 - 6) (Sunday to Saturday)
# │ │ │ │ │
# │ │ │ │ │
# │ │ │ │ │
# * * * * *
- cron: '0 9 * * 4'
jobs:
codeql:
runs-on: ubuntu-20.04
steps:
-
name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 2
-
name: Checkout HEAD on PR
if: ${{ github.event_name == 'pull_request' }}
run: |
git checkout HEAD^2
-
name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: go
-
name: Autobuild
uses: github/codeql-action/autobuild@v2
-
name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2

74
.github/workflows/codeql.yml vendored Normal file
View File

@ -0,0 +1,74 @@
name: codeql
on:
push:
branches:
- 'master'
- '[0-9]+.[0-9]+'
tags:
- 'v*'
pull_request:
# The branches below must be a subset of the branches above
branches: [ "master" ]
schedule:
# ┌───────────── minute (0 - 59)
# │ ┌───────────── hour (0 - 23)
# │ │ ┌───────────── day of the month (1 - 31)
# │ │ │ ┌───────────── month (1 - 12)
# │ │ │ │ ┌───────────── day of the week (0 - 6) (Sunday to Saturday)
# │ │ │ │ │
# │ │ │ │ │
# │ │ │ │ │
# * * * * *
- cron: '0 9 * * 4'
jobs:
codeql:
runs-on: 'ubuntu-latest'
timeout-minutes: 360
env:
DISABLE_WARN_OUTSIDE_CONTAINER: '1'
permissions:
actions: read
contents: read
security-events: write
steps:
-
name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 2
-
name: Checkout HEAD on PR
if: ${{ github.event_name == 'pull_request' }}
run: |
git checkout HEAD^2
-
name: Update Go
uses: actions/setup-go@v5
with:
go-version: '1.21'
-
name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: go
# CodeQL 2.16.4's auto-build added support for multi-module repositories,
# and is trying to be smart by searching for modules in every directory,
# including vendor directories. If no module is found, it's creating one
# which is ... not what we want, so let's give it a "go.mod".
# see: https://github.com/docker/cli/pull/4944#issuecomment-2002034698
-
name: Create go.mod
run: |
ln -s vendor.mod go.mod
ln -s vendor.sum go.sum
-
name: Autobuild
uses: github/codeql-action/autobuild@v3
-
name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:go"

View File

@ -16,7 +16,7 @@ on:
jobs: jobs:
e2e: e2e:
runs-on: ubuntu-20.04 runs-on: ubuntu-22.04
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@ -26,17 +26,17 @@ jobs:
- connhelper-ssh - connhelper-ssh
base: base:
- alpine - alpine
- bullseye - debian
engine-version: engine-version:
# - 20.10-dind # FIXME: Fails on 20.10 - 25.0 # latest
- stable-dind # TODO: Use 20.10-dind, stable-dind is deprecated - 24.0 # latest - 1
include: - 23.0 # mirantis lts
- target: non-experimental # TODO(krissetto) 19.03 needs a look, doesn't work ubuntu 22.04 (cgroup errors).
engine-version: 19.03-dind # we could have a separate job that tests it against ubuntu 20.04
steps: steps:
- -
name: Checkout name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
- -
name: Update daemon.json name: Update daemon.json
run: | run: |
@ -48,17 +48,18 @@ jobs:
docker info docker info
- -
name: Set up Docker Buildx name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v3
- -
name: Run ${{ matrix.target }} name: Run ${{ matrix.target }}
run: | run: |
make -f docker.Makefile test-e2e-${{ matrix.target }} make -f docker.Makefile test-e2e-${{ matrix.target }}
env: env:
BASE_VARIANT: ${{ matrix.base }} BASE_VARIANT: ${{ matrix.base }}
E2E_ENGINE_VERSION: ${{ matrix.engine-version }} ENGINE_VERSION: ${{ matrix.engine-version }}
TESTFLAGS: -coverprofile=/tmp/coverage/coverage.txt TESTFLAGS: -coverprofile=/tmp/coverage/coverage.txt
- -
name: Send to Codecov name: Send to Codecov
uses: codecov/codecov-action@v3 uses: codecov/codecov-action@v4
with: with:
file: ./build/coverage/coverage.txt file: ./build/coverage/coverage.txt
token: ${{ secrets.CODECOV_TOKEN }}

View File

@ -16,24 +16,25 @@ on:
jobs: jobs:
ctn: ctn:
runs-on: ubuntu-20.04 runs-on: ubuntu-22.04
steps: steps:
- -
name: Checkout name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
- -
name: Set up Docker Buildx name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v3
- -
name: Test name: Test
uses: docker/bake-action@v3 uses: docker/bake-action@v4
with: with:
targets: test-coverage targets: test-coverage
- -
name: Send to Codecov name: Send to Codecov
uses: codecov/codecov-action@v3 uses: codecov/codecov-action@v4
with: with:
file: ./build/coverage/coverage.txt file: ./build/coverage/coverage.txt
token: ${{ secrets.CODECOV_TOKEN }}
host: host:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
@ -45,7 +46,7 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
os: os:
- macos-11 - 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 # - windows-2022 # FIXME: some tests are failing on the Windows runner, as well as on Appveyor since June 24, 2018: https://ci.appveyor.com/project/docker/cli/history
steps: steps:
- -
@ -56,14 +57,14 @@ jobs:
git config --system core.eol lf git config --system core.eol lf
- -
name: Checkout name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
path: ${{ env.GOPATH }}/src/github.com/docker/cli path: ${{ env.GOPATH }}/src/github.com/docker/cli
- -
name: Set up Go name: Set up Go
uses: actions/setup-go@v4 uses: actions/setup-go@v5
with: with:
go-version: 1.20.5 go-version: 1.21.9
- -
name: Test name: Test
run: | run: |
@ -73,7 +74,8 @@ jobs:
shell: bash shell: bash
- -
name: Send to Codecov name: Send to Codecov
uses: codecov/codecov-action@v3 uses: codecov/codecov-action@v4
with: with:
file: /tmp/coverage.txt file: /tmp/coverage.txt
working-directory: ${{ env.GOPATH }}/src/github.com/docker/cli working-directory: ${{ env.GOPATH }}/src/github.com/docker/cli
token: ${{ secrets.CODECOV_TOKEN }}

62
.github/workflows/validate-pr.yml vendored Normal file
View File

@ -0,0 +1,62 @@
name: validate-pr
on:
pull_request:
types: [opened, edited, labeled, unlabeled]
jobs:
check-area-label:
runs-on: ubuntu-20.04
steps:
- name: Missing `area/` label
if: contains(join(github.event.pull_request.labels.*.name, ','), 'impact/') && !contains(join(github.event.pull_request.labels.*.name, ','), 'area/')
run: |
echo "::error::Every PR with an 'impact/*' label should also have an 'area/*' label"
exit 1
- name: OK
run: exit 0
check-changelog:
if: contains(join(github.event.pull_request.labels.*.name, ','), 'impact/')
runs-on: ubuntu-20.04
env:
PR_BODY: |
${{ github.event.pull_request.body }}
steps:
- name: Check changelog description
run: |
# Extract the `markdown changelog` note code block
block=$(echo -n "$PR_BODY" | tr -d '\r' | awk '/^```markdown changelog$/{flag=1;next}/^```$/{flag=0}flag')
# Strip empty lines
desc=$(echo "$block" | awk NF)
if [ -z "$desc" ]; then
echo "::error::Changelog section is empty. Please provide a description for the changelog."
exit 1
fi
len=$(echo -n "$desc" | wc -c)
if [[ $len -le 6 ]]; then
echo "::error::Description looks too short: $desc"
exit 1
fi
echo "This PR will be included in the release notes with the following note:"
echo "$desc"
check-pr-branch:
runs-on: ubuntu-20.04
env:
PR_TITLE: ${{ github.event.pull_request.title }}
steps:
# Backports or PR that target a release branch directly should mention the target branch in the title, for example:
# [X.Y backport] Some change that needs backporting to X.Y
# [X.Y] Change directly targeting the X.Y branch
- name: Get branch from PR title
id: title_branch
run: echo "$PR_TITLE" | sed -n 's/^\[\([0-9]*\.[0-9]*\)[^]]*\].*/branch=\1/p' >> $GITHUB_OUTPUT
- 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

@ -16,7 +16,7 @@ on:
jobs: jobs:
validate: validate:
runs-on: ubuntu-20.04 runs-on: ubuntu-22.04
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@ -28,20 +28,20 @@ jobs:
steps: steps:
- -
name: Checkout name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
- -
name: Run name: Run
uses: docker/bake-action@v3 uses: docker/bake-action@v4
with: with:
targets: ${{ matrix.target }} targets: ${{ matrix.target }}
# check that the generated Markdown and the checked-in files match # check that the generated Markdown and the checked-in files match
validate-md: validate-md:
runs-on: ubuntu-20.04 runs-on: ubuntu-22.04
steps: steps:
- -
name: Checkout name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
- -
name: Generate name: Generate
shell: 'script --return --quiet --command "bash {0}"' shell: 'script --return --quiet --command "bash {0}"'
@ -57,7 +57,7 @@ jobs:
fi fi
validate-make: validate-make:
runs-on: ubuntu-20.04 runs-on: ubuntu-22.04
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@ -67,7 +67,7 @@ jobs:
steps: steps:
- -
name: Checkout name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
- -
name: Run name: Run
shell: 'script --return --quiet --command "bash {0}"' shell: 'script --return --quiet --command "bash {0}"'

View File

@ -3,23 +3,41 @@ linters:
- bodyclose - bodyclose
- depguard - depguard
- dogsled - dogsled
- dupword # Detects duplicate words.
- durationcheck
- errchkjson
- exportloopref # Detects pointers to enclosing loop variables.
- gocritic # Metalinter; detects bugs, performance, and styling issues.
- gocyclo - gocyclo
- gofumpt - gofumpt # Detects whether code was gofumpt-ed.
- goimports - goimports
- gosec - gosec # Detects security problems.
- gosimple - gosimple
- govet - govet
- ineffassign - ineffassign
- lll - lll
- megacheck - megacheck
- misspell - misspell # Detects commonly misspelled English words in comments.
- nakedret - nakedret
- revive - nilerr # Detects code that returns nil even if it checks that the error is not nil.
- nolintlint # Detects ill-formed or insufficient nolint directives.
- perfsprint # Detects fmt.Sprintf uses that can be replaced with a faster alternative.
- prealloc # Detects slice declarations that could potentially be pre-allocated.
- predeclared # Detects code that shadows one of Go's predeclared identifiers
- reassign
- revive # Metalinter; drop-in replacement for golint.
- staticcheck - staticcheck
- stylecheck # Replacement for golint
- tenv # Detects using os.Setenv instead of t.Setenv.
- thelper # Detects test helpers without t.Helper().
- tparallel # Detects inappropriate usage of t.Parallel().
- typecheck - typecheck
- unconvert - unconvert # Detects unnecessary type conversions.
- unparam - unparam
- unused - unused
- usestdlibvars
- vet
- wastedassign
disable: disable:
- errcheck - errcheck
@ -32,22 +50,43 @@ run:
linters-settings: linters-settings:
depguard: depguard:
list-type: blacklist rules:
include-go-root: true main:
packages: deny:
# The io/ioutil package has been deprecated. - pkg: io/ioutil
# https://go.dev/doc/go1.16#ioutil desc: The io/ioutil package has been deprecated, see https://go.dev/doc/go1.16#ioutil
- io/ioutil
gocyclo: gocyclo:
min-complexity: 16 min-complexity: 16
govet: govet:
check-shadowing: false check-shadowing: true
settings:
shadow:
strict: true
lll: lll:
line-length: 200 line-length: 200
nakedret: nakedret:
command: nakedret command: nakedret
pattern: ^(?P<path>.*?\\.go):(?P<line>\\d+)\\s*(?P<message>.*)$ pattern: ^(?P<path>.*?\\.go):(?P<line>\\d+)\\s*(?P<message>.*)$
revive:
rules:
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#import-shadowing
- name: import-shadowing
severity: warning
disabled: false
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#empty-block
- name: empty-block
severity: warning
disabled: false
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#empty-lines
- name: empty-lines
severity: warning
disabled: false
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#use-any
- name: use-any
severity: warning
disabled: false
issues: issues:
# The default exclusion rules are a bit too permissive, so copying the relevant ones below # The default exclusion rules are a bit too permissive, so copying the relevant ones below
exclude-use-default: false exclude-use-default: false
@ -84,7 +123,7 @@ issues:
- gosec - gosec
# EXC0008 # EXC0008
# TODO: evaluate these and fix where needed: G307: Deferring unsafe method "*os.File" on type "Close" (gosec) # TODO: evaluate these and fix where needed: G307: Deferring unsafe method "*os.File" on type "Close" (gosec)
- text: "(G104|G307)" - text: "G307"
linters: linters:
- gosec - gosec
# EXC0009 # EXC0009
@ -98,10 +137,13 @@ issues:
# G113 Potential uncontrolled memory consumption in Rat.SetString (CVE-2022-23772) # G113 Potential uncontrolled memory consumption in Rat.SetString (CVE-2022-23772)
# only affects gp < 1.16.14. and go < 1.17.7 # only affects gp < 1.16.14. and go < 1.17.7
- text: "(G113)" - text: "G113"
linters:
- gosec
# TODO: G104: Errors unhandled. (gosec)
- text: "G104"
linters: linters:
- gosec - gosec
# Looks like the match in "EXC0007" above doesn't catch this one # Looks like the match in "EXC0007" above doesn't catch this one
# TODO: consider upstreaming this to golangci-lint's default exclusion rules # TODO: consider upstreaming this to golangci-lint's default exclusion rules
- text: "G204: Subprocess launched with a potential tainted input or cmd arguments" - text: "G204: Subprocess launched with a potential tainted input or cmd arguments"
@ -117,12 +159,24 @@ issues:
- text: "package-comments: should have a package comment" - text: "package-comments: should have a package comment"
linters: linters:
- revive - revive
# FIXME temporarily suppress these (see https://github.com/gotestyourself/gotest.tools/issues/272)
- text: "SA1019: (assert|cmp|is)\\.ErrorType is deprecated"
linters:
- staticcheck
# Exclude some linters from running on tests files. # Exclude some linters from running on tests files.
- path: _test\.go - path: _test\.go
linters: linters:
- errcheck - errcheck
- gosec - gosec
- text: "ST1000: at least one file in a package should have a package comment"
linters:
- stylecheck
# Allow "err" and "ok" vars to shadow existing declarations, otherwise we get too many false positives.
- text: '^shadow: declaration of "(err|ok)" shadows declaration'
linters:
- govet
# Maximum issues count per one linter. Set to 0 to disable. Default is 50. # Maximum issues count per one linter. Set to 0 to disable. Default is 50.
max-issues-per-linter: 0 max-issues-per-linter: 0

View File

@ -22,6 +22,8 @@ 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>
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.akihiro@lab.ntt.co.jp>
Akihiro Suda <akihiro.suda.cz@hco.ntt.co.jp> <suda.kyoto@gmail.com> Akihiro Suda <akihiro.suda.cz@hco.ntt.co.jp> <suda.kyoto@gmail.com>
Albin Kerouanton <albinker@gmail.com>
Albin Kerouanton <albinker@gmail.com> <albin@akerouanton.name>
Aleksa Sarai <asarai@suse.de> Aleksa Sarai <asarai@suse.de>
Aleksa Sarai <asarai@suse.de> <asarai@suse.com> Aleksa Sarai <asarai@suse.de> <asarai@suse.com>
Aleksa Sarai <asarai@suse.de> <cyphar@cyphar.com> Aleksa Sarai <asarai@suse.de> <cyphar@cyphar.com>
@ -29,6 +31,7 @@ Aleksandrs Fadins <aleks@s-ko.net>
Alessandro Boch <aboch@tetrationanalytics.com> <aboch@docker.com> Alessandro Boch <aboch@tetrationanalytics.com> <aboch@docker.com>
Alex Chen <alexchenunix@gmail.com> <root@localhost.localdomain> Alex Chen <alexchenunix@gmail.com> <root@localhost.localdomain>
Alex Ellis <alexellis2@gmail.com> Alex Ellis <alexellis2@gmail.com>
Alexander Chneerov <achneerov@gmail.com>
Alexander Larsson <alexl@redhat.com> <alexander.larsson@gmail.com> Alexander Larsson <alexl@redhat.com> <alexander.larsson@gmail.com>
Alexander Morozov <lk4d4math@gmail.com> Alexander Morozov <lk4d4math@gmail.com>
Alexander Morozov <lk4d4math@gmail.com> <lk4d4@docker.com> Alexander Morozov <lk4d4math@gmail.com> <lk4d4@docker.com>
@ -72,6 +75,9 @@ Bill Wang <ozbillwang@gmail.com> <SydOps@users.noreply.github.com>
Bin Liu <liubin0329@gmail.com> Bin Liu <liubin0329@gmail.com>
Bin Liu <liubin0329@gmail.com> <liubin0329@users.noreply.github.com> Bin Liu <liubin0329@gmail.com> <liubin0329@users.noreply.github.com>
Bingshen Wang <bingshen.wbs@alibaba-inc.com> Bingshen Wang <bingshen.wbs@alibaba-inc.com>
Bjorn Neergaard <bjorn.neergaard@docker.com>
Bjorn Neergaard <bjorn.neergaard@docker.com> <bjorn@neersighted.com>
Bjorn Neergaard <bjorn.neergaard@docker.com> <bneergaard@mirantis.com>
Boaz Shuster <ripcurld.github@gmail.com> Boaz Shuster <ripcurld.github@gmail.com>
Brad Baker <brad@brad.fi> Brad Baker <brad@brad.fi>
Brad Baker <brad@brad.fi> <88946291+brdbkr@users.noreply.github.com> Brad Baker <brad@brad.fi> <88946291+brdbkr@users.noreply.github.com>
@ -81,6 +87,7 @@ Brent Salisbury <brent.salisbury@docker.com> <brent@docker.com>
Brian Goff <cpuguy83@gmail.com> Brian Goff <cpuguy83@gmail.com>
Brian Goff <cpuguy83@gmail.com> <bgoff@cpuguy83-mbp.home> Brian Goff <cpuguy83@gmail.com> <bgoff@cpuguy83-mbp.home>
Brian Goff <cpuguy83@gmail.com> <bgoff@cpuguy83-mbp.local> Brian Goff <cpuguy83@gmail.com> <bgoff@cpuguy83-mbp.local>
Brian Tracy <brian.tracy33@gmail.com>
Carlos de Paula <me@carlosedp.com> Carlos de Paula <me@carlosedp.com>
Chad Faragher <wyckster@hotmail.com> Chad Faragher <wyckster@hotmail.com>
Chander Govindarajan <chandergovind@gmail.com> Chander Govindarajan <chandergovind@gmail.com>
@ -91,6 +98,8 @@ Chen Chuanliang <chen.chuanliang@zte.com.cn>
Chen Mingjie <chenmingjie0828@163.com> Chen Mingjie <chenmingjie0828@163.com>
Chen Qiu <cheney-90@hotmail.com> Chen Qiu <cheney-90@hotmail.com>
Chen Qiu <cheney-90@hotmail.com> <21321229@zju.edu.cn> Chen Qiu <cheney-90@hotmail.com> <21321229@zju.edu.cn>
Chris Chinchilla <chris@chrischinchilla.com>
Chris Chinchilla <chris@chrischinchilla.com> <chris.ward@docker.com>
Chris Dias <cdias@microsoft.com> Chris Dias <cdias@microsoft.com>
Chris McKinnel <chris.mckinnel@tangentlabs.co.uk> Chris McKinnel <chris.mckinnel@tangentlabs.co.uk>
Christopher Biscardi <biscarch@sketcht.com> Christopher Biscardi <biscarch@sketcht.com>
@ -101,6 +110,7 @@ Chun Chen <ramichen@tencent.com> <chenchun.feed@gmail.com>
Comical Derskeal <27731088+derskeal@users.noreply.github.com> Comical Derskeal <27731088+derskeal@users.noreply.github.com>
Corbin Coleman <corbin.coleman@docker.com> Corbin Coleman <corbin.coleman@docker.com>
Cory Bennet <cbennett@netflix.com> Cory Bennet <cbennett@netflix.com>
Craig Osterhout <craig.osterhout@docker.com>
Cristian Staretu <cristian.staretu@gmail.com> Cristian Staretu <cristian.staretu@gmail.com>
Cristian Staretu <cristian.staretu@gmail.com> <unclejack@users.noreply.github.com> Cristian Staretu <cristian.staretu@gmail.com> <unclejack@users.noreply.github.com>
Cristian Staretu <cristian.staretu@gmail.com> <unclejacksons@gmail.com> Cristian Staretu <cristian.staretu@gmail.com> <unclejacksons@gmail.com>
@ -110,6 +120,7 @@ Daehyeok Mun <daehyeok@gmail.com> <daehyeok@daehyeok-ui-MacBook-Air.local>
Daehyeok Mun <daehyeok@gmail.com> <daehyeok@daehyeokui-MacBook-Air.local> Daehyeok Mun <daehyeok@gmail.com> <daehyeok@daehyeokui-MacBook-Air.local>
Daisuke Ito <itodaisuke00@gmail.com> Daisuke Ito <itodaisuke00@gmail.com>
Dan Feldman <danf@jfrog.com> Dan Feldman <danf@jfrog.com>
Danial Gharib <danial.mail.gh@gmail.com>
Daniel Dao <dqminh@cloudflare.com> Daniel Dao <dqminh@cloudflare.com>
Daniel Dao <dqminh@cloudflare.com> <dqminh89@gmail.com> Daniel Dao <dqminh@cloudflare.com> <dqminh89@gmail.com>
Daniel Garcia <daniel@danielgarcia.info> Daniel Garcia <daniel@danielgarcia.info>
@ -131,6 +142,8 @@ Dave Henderson <dhenderson@gmail.com> <Dave.Henderson@ca.ibm.com>
Dave Tucker <dt@docker.com> <dave@dtucker.co.uk> Dave Tucker <dt@docker.com> <dave@dtucker.co.uk>
David Alvarez <david.alvarez@flyeralarm.com> David Alvarez <david.alvarez@flyeralarm.com>
David Alvarez <david.alvarez@flyeralarm.com> <busilezas@gmail.com> David Alvarez <david.alvarez@flyeralarm.com> <busilezas@gmail.com>
David Karlsson <david.karlsson@docker.com>
David Karlsson <david.karlsson@docker.com> <35727626+dvdksn@users.noreply.github.com>
David M. Karr <davidmichaelkarr@gmail.com> David M. Karr <davidmichaelkarr@gmail.com>
David Sheets <dsheets@docker.com> <sheets@alum.mit.edu> David Sheets <dsheets@docker.com> <sheets@alum.mit.edu>
David Sissitka <me@dsissitka.com> David Sissitka <me@dsissitka.com>
@ -181,6 +194,8 @@ Gerwim Feiken <g.feiken@tfe.nl> <gerwim@gmail.com>
Giampaolo Mancini <giampaolo@trampolineup.com> Giampaolo Mancini <giampaolo@trampolineup.com>
Gopikannan Venugopalsamy <gopikannan.venugopalsamy@gmail.com> Gopikannan Venugopalsamy <gopikannan.venugopalsamy@gmail.com>
Gou Rao <gou@portworx.com> <gourao@users.noreply.github.com> Gou Rao <gou@portworx.com> <gourao@users.noreply.github.com>
Graeme Wiebe <graeme.wiebe@gmail.com>
Graeme Wiebe <graeme.wiebe@gmail.com> <79593869+TheRealGramdalf@users.noreply.github.com>
Greg Stephens <greg@udon.org> Greg Stephens <greg@udon.org>
Guillaume J. Charmes <guillaume.charmes@docker.com> <charmes.guillaume@gmail.com> Guillaume J. Charmes <guillaume.charmes@docker.com> <charmes.guillaume@gmail.com>
Guillaume J. Charmes <guillaume.charmes@docker.com> <guillaume.charmes@dotcloud.com> Guillaume J. Charmes <guillaume.charmes@docker.com> <guillaume.charmes@dotcloud.com>
@ -289,7 +304,8 @@ Kelton Bassingthwaite <KeltonBassingthwaite@gmail.com> <github@bassingthwaite.or
Ken Cochrane <kencochrane@gmail.com> <KenCochrane@gmail.com> Ken Cochrane <kencochrane@gmail.com> <KenCochrane@gmail.com>
Ken Herner <kherner@progress.com> <chosenken@gmail.com> Ken Herner <kherner@progress.com> <chosenken@gmail.com>
Kenfe-Mickaël Laventure <mickael.laventure@gmail.com> Kenfe-Mickaël Laventure <mickael.laventure@gmail.com>
Kevin Alvarez <crazy-max@users.noreply.github.com> Kevin Alvarez <github@crazymax.dev>
Kevin Alvarez <github@crazymax.dev> <crazy-max@users.noreply.github.com>
Kevin Feyrer <kevin.feyrer@btinternet.com> <kevinfeyrer@users.noreply.github.com> Kevin Feyrer <kevin.feyrer@btinternet.com> <kevinfeyrer@users.noreply.github.com>
Kevin Kern <kaiwentan@harmonycloud.cn> Kevin Kern <kaiwentan@harmonycloud.cn>
Kevin Meredith <kevin.m.meredith@gmail.com> Kevin Meredith <kevin.m.meredith@gmail.com>
@ -306,6 +322,7 @@ Kyle Mitofsky <Kylemit@gmail.com>
Lajos Papp <lajos.papp@sequenceiq.com> <lalyos@yahoo.com> Lajos Papp <lajos.papp@sequenceiq.com> <lalyos@yahoo.com>
Lei Jitang <leijitang@huawei.com> Lei Jitang <leijitang@huawei.com>
Lei Jitang <leijitang@huawei.com> <leijitang@gmail.com> Lei Jitang <leijitang@huawei.com> <leijitang@gmail.com>
Li Fu Bang <lifubang@acmcoder.com>
Liang Mingqiang <mqliang.zju@gmail.com> Liang Mingqiang <mqliang.zju@gmail.com>
Liang-Chi Hsieh <viirya@gmail.com> Liang-Chi Hsieh <viirya@gmail.com>
Liao Qingwei <liaoqingwei@huawei.com> Liao Qingwei <liaoqingwei@huawei.com>
@ -330,6 +347,7 @@ Mansi Nahar <mmn4185@rit.edu> <mansi.nahar@macbookpro-mansinahar.local>
Mansi Nahar <mmn4185@rit.edu> <mansinahar@users.noreply.github.com> Mansi Nahar <mmn4185@rit.edu> <mansinahar@users.noreply.github.com>
Marc Abramowitz <marc@marc-abramowitz.com> <msabramo@gmail.com> Marc Abramowitz <marc@marc-abramowitz.com> <msabramo@gmail.com>
Marcelo Horacio Fortino <info@fortinux.com> <fortinux@users.noreply.github.com> Marcelo Horacio Fortino <info@fortinux.com> <fortinux@users.noreply.github.com>
Marco Spiess <marco.spiess@hotmail.de>
Marcus Linke <marcus.linke@gmx.de> Marcus Linke <marcus.linke@gmx.de>
Marianna Tessel <mtesselh@gmail.com> Marianna Tessel <mtesselh@gmail.com>
Marius Ileana <marius.ileana@gmail.com> Marius Ileana <marius.ileana@gmail.com>
@ -399,6 +417,9 @@ Paul Liljenberg <liljenberg.paul@gmail.com> <letters@paulnotcom.se>
Pavel Tikhomirov <ptikhomirov@virtuozzo.com> <ptikhomirov@parallels.com> Pavel Tikhomirov <ptikhomirov@virtuozzo.com> <ptikhomirov@parallels.com>
Pawel Konczalski <mail@konczalski.de> Pawel Konczalski <mail@konczalski.de>
Paweł Pokrywka <pepawel@users.noreply.github.com> Paweł Pokrywka <pepawel@users.noreply.github.com>
Per Lundberg <perlun@gmail.com>
Per Lundberg <perlun@gmail.com> <per.lundberg@ecraft.com>
Per Lundberg <perlun@gmail.com> <per.lundberg@hibox.tv>
Peter Choi <phkchoi89@gmail.com> <reikani@Peters-MacBook-Pro.local> Peter Choi <phkchoi89@gmail.com> <reikani@Peters-MacBook-Pro.local>
Peter Dave Hello <hsu@peterdavehello.org> <PeterDaveHello@users.noreply.github.com> Peter Dave Hello <hsu@peterdavehello.org> <PeterDaveHello@users.noreply.github.com>
Peter Hsu <shhsu@microsoft.com> Peter Hsu <shhsu@microsoft.com>
@ -414,6 +435,8 @@ Qiang Huang <h.huangqiang@huawei.com>
Qiang Huang <h.huangqiang@huawei.com> <qhuang@10.0.2.15> Qiang Huang <h.huangqiang@huawei.com> <qhuang@10.0.2.15>
Ray Tsang <rayt@google.com> <saturnism@users.noreply.github.com> Ray Tsang <rayt@google.com> <saturnism@users.noreply.github.com>
Renaud Gaubert <rgaubert@nvidia.com> <renaud.gaubert@gmail.com> Renaud Gaubert <rgaubert@nvidia.com> <renaud.gaubert@gmail.com>
Rob Murray <rob.murray@docker.com>
Rob Murray <rob.murray@docker.com> <148866618+robmry@users.noreply.github.com>
Robert Terhaar <rterhaar@atlanticdynamic.com> <robbyt@users.noreply.github.com> Robert Terhaar <rterhaar@atlanticdynamic.com> <robbyt@users.noreply.github.com>
Roberto G. Hashioka <roberto.hashioka@docker.com> <roberto_hashioka@hotmail.com> Roberto G. Hashioka <roberto.hashioka@docker.com> <roberto_hashioka@hotmail.com>
Roberto Muñoz Fernández <robertomf@gmail.com> <roberto.munoz.fernandez.contractor@bbva.com> Roberto Muñoz Fernández <robertomf@gmail.com> <roberto.munoz.fernandez.contractor@bbva.com>
@ -429,6 +452,7 @@ Sandeep Bansal <sabansal@microsoft.com>
Sandeep Bansal <sabansal@microsoft.com> <msabansal@microsoft.com> Sandeep Bansal <sabansal@microsoft.com> <msabansal@microsoft.com>
Sandro Jäckel <sandro.jaeckel@gmail.com> Sandro Jäckel <sandro.jaeckel@gmail.com>
Sargun Dhillon <sargun@netflix.com> <sargun@sargun.me> Sargun Dhillon <sargun@netflix.com> <sargun@sargun.me>
Saurabh Kumar <saurabhkumar0184@gmail.com>
Sean Lee <seanlee@tw.ibm.com> <scaleoutsean@users.noreply.github.com> Sean Lee <seanlee@tw.ibm.com> <scaleoutsean@users.noreply.github.com>
Sebastiaan van Stijn <github@gone.nl> <sebastiaan@ws-key-sebas3.dpi1.dpi> Sebastiaan van Stijn <github@gone.nl> <sebastiaan@ws-key-sebas3.dpi1.dpi>
Sebastiaan van Stijn <github@gone.nl> <thaJeztah@users.noreply.github.com> Sebastiaan van Stijn <github@gone.nl> <thaJeztah@users.noreply.github.com>

46
AUTHORS
View File

@ -2,6 +2,7 @@
# This file lists all contributors to the repository. # This file lists all contributors to the repository.
# See scripts/docs/generate-authors.sh to make modifications. # See scripts/docs/generate-authors.sh to make modifications.
A. Lester Buck III <github-reg@nbolt.com>
Aanand Prasad <aanand.prasad@gmail.com> Aanand Prasad <aanand.prasad@gmail.com>
Aaron L. Xu <liker.xu@foxmail.com> Aaron L. Xu <liker.xu@foxmail.com>
Aaron Lehmann <alehmann@netflix.com> Aaron Lehmann <alehmann@netflix.com>
@ -16,6 +17,7 @@ Adolfo Ochagavía <aochagavia92@gmail.com>
Adrian Plata <adrian.plata@docker.com> Adrian Plata <adrian.plata@docker.com>
Adrien Duermael <adrien@duermael.com> Adrien Duermael <adrien@duermael.com>
Adrien Folie <folie.adrien@gmail.com> Adrien Folie <folie.adrien@gmail.com>
Adyanth Hosavalike <ahosavalike@ucsd.edu>
Ahmet Alp Balkan <ahmetb@microsoft.com> Ahmet Alp Balkan <ahmetb@microsoft.com>
Aidan Feldman <aidan.feldman@gmail.com> Aidan Feldman <aidan.feldman@gmail.com>
Aidan Hobson Sayers <aidanhs@cantab.net> Aidan Hobson Sayers <aidanhs@cantab.net>
@ -26,7 +28,7 @@ Akim Demaille <akim.demaille@docker.com>
Alan Thompson <cloojure@gmail.com> Alan Thompson <cloojure@gmail.com>
Albert Callarisa <shark234@gmail.com> Albert Callarisa <shark234@gmail.com>
Alberto Roura <mail@albertoroura.com> Alberto Roura <mail@albertoroura.com>
Albin Kerouanton <albin@akerouanton.name> Albin Kerouanton <albinker@gmail.com>
Aleksa Sarai <asarai@suse.de> Aleksa Sarai <asarai@suse.de>
Aleksander Piotrowski <apiotrowski312@gmail.com> Aleksander Piotrowski <apiotrowski312@gmail.com>
Alessandro Boch <aboch@tetrationanalytics.com> Alessandro Boch <aboch@tetrationanalytics.com>
@ -34,6 +36,7 @@ Alex Couture-Beil <alex@earthly.dev>
Alex Mavrogiannis <alex.mavrogiannis@docker.com> Alex Mavrogiannis <alex.mavrogiannis@docker.com>
Alex Mayer <amayer5125@gmail.com> Alex Mayer <amayer5125@gmail.com>
Alexander Boyd <alex@opengroove.org> Alexander Boyd <alex@opengroove.org>
Alexander Chneerov <achneerov@gmail.com>
Alexander Larsson <alexl@redhat.com> Alexander Larsson <alexl@redhat.com>
Alexander Morozov <lk4d4math@gmail.com> Alexander Morozov <lk4d4math@gmail.com>
Alexander Ryabov <i@sepa.spb.ru> Alexander Ryabov <i@sepa.spb.ru>
@ -41,6 +44,7 @@ Alexandre González <agonzalezro@gmail.com>
Alexey Igrychev <alexey.igrychev@flant.com> Alexey Igrychev <alexey.igrychev@flant.com>
Alexis Couvreur <alexiscouvreur.pro@gmail.com> Alexis Couvreur <alexiscouvreur.pro@gmail.com>
Alfred Landrum <alfred.landrum@docker.com> Alfred Landrum <alfred.landrum@docker.com>
Ali Rostami <rostami.ali@gmail.com>
Alicia Lauerman <alicia@eta.im> Alicia Lauerman <alicia@eta.im>
Allen Sun <allensun.shl@alibaba-inc.com> Allen Sun <allensun.shl@alibaba-inc.com>
Alvin Deng <alvin.q.deng@utexas.edu> Alvin Deng <alvin.q.deng@utexas.edu>
@ -79,7 +83,9 @@ Arko Dasgupta <arko@tetrate.io>
Arnaud Porterie <icecrime@gmail.com> Arnaud Porterie <icecrime@gmail.com>
Arnaud Rebillout <elboulangero@gmail.com> Arnaud Rebillout <elboulangero@gmail.com>
Arthur Peka <arthur.peka@outlook.com> Arthur Peka <arthur.peka@outlook.com>
Ashly Mathew <ashly.mathew@sap.com>
Ashwini Oruganti <ashwini.oruganti@gmail.com> Ashwini Oruganti <ashwini.oruganti@gmail.com>
Aslam Ahemad <aslamahemad@gmail.com>
Azat Khuyiyakhmetov <shadow_uz@mail.ru> Azat Khuyiyakhmetov <shadow_uz@mail.ru>
Bardia Keyoumarsi <bkeyouma@ucsc.edu> Bardia Keyoumarsi <bkeyouma@ucsc.edu>
Barnaby Gray <barnaby@pickle.me.uk> Barnaby Gray <barnaby@pickle.me.uk>
@ -98,7 +104,9 @@ Bill Wang <ozbillwang@gmail.com>
Bin Liu <liubin0329@gmail.com> Bin Liu <liubin0329@gmail.com>
Bingshen Wang <bingshen.wbs@alibaba-inc.com> Bingshen Wang <bingshen.wbs@alibaba-inc.com>
Bishal Das <bishalhnj127@gmail.com> Bishal Das <bishalhnj127@gmail.com>
Bjorn Neergaard <bjorn.neergaard@docker.com>
Boaz Shuster <ripcurld.github@gmail.com> Boaz Shuster <ripcurld.github@gmail.com>
Boban Acimovic <boban.acimovic@gmail.com>
Bogdan Anton <contact@bogdananton.ro> Bogdan Anton <contact@bogdananton.ro>
Boris Pruessmann <boris@pruessmann.org> Boris Pruessmann <boris@pruessmann.org>
Brad Baker <brad@brad.fi> Brad Baker <brad@brad.fi>
@ -109,6 +117,7 @@ Brent Salisbury <brent.salisbury@docker.com>
Bret Fisher <bret@bretfisher.com> Bret Fisher <bret@bretfisher.com>
Brian (bex) Exelbierd <bexelbie@redhat.com> Brian (bex) Exelbierd <bexelbie@redhat.com>
Brian Goff <cpuguy83@gmail.com> Brian Goff <cpuguy83@gmail.com>
Brian Tracy <brian.tracy33@gmail.com>
Brian Wieder <brian@4wieders.com> Brian Wieder <brian@4wieders.com>
Bruno Sousa <bruno.sousa@docker.com> Bruno Sousa <bruno.sousa@docker.com>
Bryan Bess <squarejaw@bsbess.com> Bryan Bess <squarejaw@bsbess.com>
@ -136,6 +145,7 @@ Chen Chuanliang <chen.chuanliang@zte.com.cn>
Chen Hanxiao <chenhanxiao@cn.fujitsu.com> Chen Hanxiao <chenhanxiao@cn.fujitsu.com>
Chen Mingjie <chenmingjie0828@163.com> Chen Mingjie <chenmingjie0828@163.com>
Chen Qiu <cheney-90@hotmail.com> Chen Qiu <cheney-90@hotmail.com>
Chris Chinchilla <chris@chrischinchilla.com>
Chris Couzens <ccouzens@gmail.com> Chris Couzens <ccouzens@gmail.com>
Chris Gavin <chris@chrisgavin.me> Chris Gavin <chris@chrisgavin.me>
Chris Gibson <chris@chrisg.io> Chris Gibson <chris@chrisg.io>
@ -163,6 +173,8 @@ Conner Crosby <conner@cavcrosby.tech>
Corey Farrell <git@cfware.com> Corey Farrell <git@cfware.com>
Corey Quon <corey.quon@docker.com> Corey Quon <corey.quon@docker.com>
Cory Bennet <cbennett@netflix.com> Cory Bennet <cbennett@netflix.com>
Cory Snider <csnider@mirantis.com>
Craig Osterhout <craig.osterhout@docker.com>
Craig Wilhite <crwilhit@microsoft.com> Craig Wilhite <crwilhit@microsoft.com>
Cristian Staretu <cristian.staretu@gmail.com> Cristian Staretu <cristian.staretu@gmail.com>
Daehyeok Mun <daehyeok@gmail.com> Daehyeok Mun <daehyeok@gmail.com>
@ -171,6 +183,7 @@ Daisuke Ito <itodaisuke00@gmail.com>
dalanlan <dalanlan925@gmail.com> dalanlan <dalanlan925@gmail.com>
Damien Nadé <github@livna.org> Damien Nadé <github@livna.org>
Dan Cotora <dan@bluevision.ro> Dan Cotora <dan@bluevision.ro>
Danial Gharib <danial.mail.gh@gmail.com>
Daniel Artine <daniel.artine@ufrj.br> Daniel Artine <daniel.artine@ufrj.br>
Daniel Cassidy <mail@danielcassidy.me.uk> Daniel Cassidy <mail@danielcassidy.me.uk>
Daniel Dao <dqminh@cloudflare.com> Daniel Dao <dqminh@cloudflare.com>
@ -210,6 +223,7 @@ Denis Defreyne <denis@soundcloud.com>
Denis Gladkikh <denis@gladkikh.email> Denis Gladkikh <denis@gladkikh.email>
Denis Ollier <larchunix@users.noreply.github.com> Denis Ollier <larchunix@users.noreply.github.com>
Dennis Docter <dennis@d23.nl> Dennis Docter <dennis@d23.nl>
dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Derek McGowan <derek@mcg.dev> Derek McGowan <derek@mcg.dev>
Des Preston <despreston@gmail.com> Des Preston <despreston@gmail.com>
Deshi Xiao <dxiao@redhat.com> Deshi Xiao <dxiao@redhat.com>
@ -232,11 +246,13 @@ DongGeon Lee <secmatth1996@gmail.com>
Doug Davis <dug@us.ibm.com> Doug Davis <dug@us.ibm.com>
Drew Erny <derny@mirantis.com> Drew Erny <derny@mirantis.com>
Ed Costello <epc@epcostello.com> Ed Costello <epc@epcostello.com>
Ed Morley <501702+edmorley@users.noreply.github.com>
Elango Sivanandam <elango.siva@docker.com> Elango Sivanandam <elango.siva@docker.com>
Eli Uriegas <eli.uriegas@docker.com> Eli Uriegas <eli.uriegas@docker.com>
Eli Uriegas <seemethere101@gmail.com> Eli Uriegas <seemethere101@gmail.com>
Elias Faxö <elias.faxo@tre.se> Elias Faxö <elias.faxo@tre.se>
Elliot Luo <956941328@qq.com> Elliot Luo <956941328@qq.com>
Eric Bode <eric.bode@foundries.io>
Eric Curtin <ericcurtin17@gmail.com> Eric Curtin <ericcurtin17@gmail.com>
Eric Engestrom <eric@engestrom.ch> Eric Engestrom <eric@engestrom.ch>
Eric G. Noriega <enoriega@vizuri.com> Eric G. Noriega <enoriega@vizuri.com>
@ -254,6 +270,7 @@ Eugene Yakubovich <eugene.yakubovich@coreos.com>
Evan Allrich <evan@unguku.com> Evan Allrich <evan@unguku.com>
Evan Hazlett <ejhazlett@gmail.com> Evan Hazlett <ejhazlett@gmail.com>
Evan Krall <krall@yelp.com> Evan Krall <krall@yelp.com>
Evan Lezar <elezar@nvidia.com>
Evelyn Xu <evelynhsu21@gmail.com> Evelyn Xu <evelynhsu21@gmail.com>
Everett Toews <everett.toews@rackspace.com> Everett Toews <everett.toews@rackspace.com>
Fabio Falci <fabiofalci@gmail.com> Fabio Falci <fabiofalci@gmail.com>
@ -275,6 +292,7 @@ Frederik Nordahl Jul Sabroe <frederikns@gmail.com>
Frieder Bluemle <frieder.bluemle@gmail.com> Frieder Bluemle <frieder.bluemle@gmail.com>
Gabriel Gore <gabgore@cisco.com> Gabriel Gore <gabgore@cisco.com>
Gabriel Nicolas Avellaneda <avellaneda.gabriel@gmail.com> Gabriel Nicolas Avellaneda <avellaneda.gabriel@gmail.com>
Gabriela Georgieva <gabriela.georgieva@docker.com>
Gaetan de Villele <gdevillele@gmail.com> Gaetan de Villele <gdevillele@gmail.com>
Gang Qiao <qiaohai8866@gmail.com> Gang Qiao <qiaohai8866@gmail.com>
Gary Schaetz <gary@schaetzkc.com> Gary Schaetz <gary@schaetzkc.com>
@ -288,6 +306,7 @@ Gleb Stsenov <gleb.stsenov@gmail.com>
Goksu Toprak <goksu.toprak@docker.com> Goksu Toprak <goksu.toprak@docker.com>
Gou Rao <gou@portworx.com> Gou Rao <gou@portworx.com>
Govind Rai <raigovind93@gmail.com> Govind Rai <raigovind93@gmail.com>
Graeme Wiebe <graeme.wiebe@gmail.com>
Grant Reaber <grant.reaber@gmail.com> Grant Reaber <grant.reaber@gmail.com>
Greg Pflaum <gpflaum@users.noreply.github.com> Greg Pflaum <gpflaum@users.noreply.github.com>
Gsealy <jiaojingwei1001@hotmail.com> Gsealy <jiaojingwei1001@hotmail.com>
@ -311,6 +330,7 @@ Hernan Garcia <hernandanielg@gmail.com>
Hongbin Lu <hongbin034@gmail.com> Hongbin Lu <hongbin034@gmail.com>
Hu Keping <hukeping@huawei.com> Hu Keping <hukeping@huawei.com>
Huayi Zhang <irachex@gmail.com> Huayi Zhang <irachex@gmail.com>
Hugo Chastel <Hugo-C@users.noreply.github.com>
Hugo Gabriel Eyherabide <hugogabriel.eyherabide@gmail.com> Hugo Gabriel Eyherabide <hugogabriel.eyherabide@gmail.com>
huqun <huqun@zju.edu.cn> huqun <huqun@zju.edu.cn>
Huu Nguyen <huu@prismskylabs.com> Huu Nguyen <huu@prismskylabs.com>
@ -329,9 +349,12 @@ Ivan Grund <ivan.grund@gmail.com>
Ivan Markin <sw@nogoegst.net> Ivan Markin <sw@nogoegst.net>
Jacob Atzen <jacob@jacobatzen.dk> Jacob Atzen <jacob@jacobatzen.dk>
Jacob Tomlinson <jacob@tom.linson.uk> Jacob Tomlinson <jacob@tom.linson.uk>
Jacopo Rigoli <rigoli.jacopo@gmail.com>
Jaivish Kothari <janonymous.codevulture@gmail.com> Jaivish Kothari <janonymous.codevulture@gmail.com>
Jake Lambert <jake.lambert@volusion.com> Jake Lambert <jake.lambert@volusion.com>
Jake Sanders <jsand@google.com> Jake Sanders <jsand@google.com>
Jake Stokes <contactjake@developerjake.com>
Jakub Panek <me@panekj.dev>
James Nesbitt <james.nesbitt@wunderkraut.com> James Nesbitt <james.nesbitt@wunderkraut.com>
James Turnbull <james@lovedthanlost.net> James Turnbull <james@lovedthanlost.net>
Jamie Hannaford <jamie@limetree.org> Jamie Hannaford <jamie@limetree.org>
@ -408,10 +431,12 @@ Josh Chorlton <jchorlton@gmail.com>
Josh Hawn <josh.hawn@docker.com> Josh Hawn <josh.hawn@docker.com>
Josh Horwitz <horwitz@addthis.com> Josh Horwitz <horwitz@addthis.com>
Josh Soref <jsoref@gmail.com> Josh Soref <jsoref@gmail.com>
Julian <gitea+julian@ic.thejulian.uk>
Julien Barbier <write0@gmail.com> Julien Barbier <write0@gmail.com>
Julien Kassar <github@kassisol.com> Julien Kassar <github@kassisol.com>
Julien Maitrehenry <julien.maitrehenry@me.com> Julien Maitrehenry <julien.maitrehenry@me.com>
Justas Brazauskas <brazauskasjustas@gmail.com> Justas Brazauskas <brazauskasjustas@gmail.com>
Justin Chadwell <me@jedevc.com>
Justin Cormack <justin.cormack@docker.com> Justin Cormack <justin.cormack@docker.com>
Justin Simonelis <justin.p.simonelis@gmail.com> Justin Simonelis <justin.p.simonelis@gmail.com>
Justyn Temme <justyntemme@gmail.com> Justyn Temme <justyntemme@gmail.com>
@ -434,7 +459,7 @@ Kelton Bassingthwaite <KeltonBassingthwaite@gmail.com>
Ken Cochrane <kencochrane@gmail.com> Ken Cochrane <kencochrane@gmail.com>
Ken ICHIKAWA <ichikawa.ken@jp.fujitsu.com> Ken ICHIKAWA <ichikawa.ken@jp.fujitsu.com>
Kenfe-Mickaël Laventure <mickael.laventure@gmail.com> Kenfe-Mickaël Laventure <mickael.laventure@gmail.com>
Kevin Alvarez <crazy-max@users.noreply.github.com> Kevin Alvarez <github@crazymax.dev>
Kevin Burke <kev@inburke.com> Kevin Burke <kev@inburke.com>
Kevin Feyrer <kevin.feyrer@btinternet.com> Kevin Feyrer <kevin.feyrer@btinternet.com>
Kevin Kern <kaiwentan@harmonycloud.cn> Kevin Kern <kaiwentan@harmonycloud.cn>
@ -454,6 +479,7 @@ Kyle Mitofsky <Kylemit@gmail.com>
Lachlan Cooper <lachlancooper@gmail.com> Lachlan Cooper <lachlancooper@gmail.com>
Lai Jiangshan <jiangshanlai@gmail.com> Lai Jiangshan <jiangshanlai@gmail.com>
Lars Kellogg-Stedman <lars@redhat.com> Lars Kellogg-Stedman <lars@redhat.com>
Laura Brehm <laurabrehm@hey.com>
Laura Frank <ljfrank@gmail.com> Laura Frank <ljfrank@gmail.com>
Laurent Erignoux <lerignoux@gmail.com> Laurent Erignoux <lerignoux@gmail.com>
Lee Gaines <eightlimbed@gmail.com> Lee Gaines <eightlimbed@gmail.com>
@ -462,10 +488,10 @@ Lennie <github@consolejunkie.net>
Leo Gallucci <elgalu3@gmail.com> Leo Gallucci <elgalu3@gmail.com>
Leonid Skorospelov <leosko94@gmail.com> Leonid Skorospelov <leosko94@gmail.com>
Lewis Daly <lewisdaly@me.com> Lewis Daly <lewisdaly@me.com>
Li Fu Bang <lifubang@acmcoder.com>
Li Yi <denverdino@gmail.com> Li Yi <denverdino@gmail.com>
Li Yi <weiyuan.yl@alibaba-inc.com> Li Yi <weiyuan.yl@alibaba-inc.com>
Liang-Chi Hsieh <viirya@gmail.com> Liang-Chi Hsieh <viirya@gmail.com>
Lifubang <lifubang@acmcoder.com>
Lihua Tang <lhtang@alauda.io> Lihua Tang <lhtang@alauda.io>
Lily Guo <lily.guo@docker.com> Lily Guo <lily.guo@docker.com>
Lin Lu <doraalin@163.com> Lin Lu <doraalin@163.com>
@ -480,6 +506,7 @@ Louis Opter <kalessin@kalessin.fr>
Luca Favatella <luca.favatella@erlang-solutions.com> Luca Favatella <luca.favatella@erlang-solutions.com>
Luca Marturana <lucamarturana@gmail.com> Luca Marturana <lucamarturana@gmail.com>
Lucas Chan <lucas-github@lucaschan.com> Lucas Chan <lucas-github@lucaschan.com>
Luis Henrique Mulinari <luis.mulinari@gmail.com>
Luka Hartwig <mail@lukahartwig.de> Luka Hartwig <mail@lukahartwig.de>
Lukas Heeren <lukas-heeren@hotmail.com> Lukas Heeren <lukas-heeren@hotmail.com>
Lukasz Zajaczkowski <Lukasz.Zajaczkowski@ts.fujitsu.com> Lukasz Zajaczkowski <Lukasz.Zajaczkowski@ts.fujitsu.com>
@ -498,6 +525,7 @@ mapk0y <mapk0y@gmail.com>
Marc Bihlmaier <marc.bihlmaier@reddoxx.com> Marc Bihlmaier <marc.bihlmaier@reddoxx.com>
Marc Cornellà <hello@mcornella.com> Marc Cornellà <hello@mcornella.com>
Marco Mariani <marco.mariani@alterway.fr> Marco Mariani <marco.mariani@alterway.fr>
Marco Spiess <marco.spiess@hotmail.de>
Marco Vedovati <mvedovati@suse.com> Marco Vedovati <mvedovati@suse.com>
Marcus Martins <marcus@docker.com> Marcus Martins <marcus@docker.com>
Marianna Tessel <mtesselh@gmail.com> Marianna Tessel <mtesselh@gmail.com>
@ -522,6 +550,7 @@ Max Shytikov <mshytikov@gmail.com>
Maxime Petazzoni <max@signalfuse.com> Maxime Petazzoni <max@signalfuse.com>
Maximillian Fan Xavier <maximillianfx@gmail.com> Maximillian Fan Xavier <maximillianfx@gmail.com>
Mei ChunTao <mei.chuntao@zte.com.cn> Mei ChunTao <mei.chuntao@zte.com.cn>
Melroy van den Berg <melroy@melroy.org>
Metal <2466052+tedhexaflow@users.noreply.github.com> Metal <2466052+tedhexaflow@users.noreply.github.com>
Micah Zoltu <micah@newrelic.com> Micah Zoltu <micah@newrelic.com>
Michael A. Smith <michael@smith-li.com> Michael A. Smith <michael@smith-li.com>
@ -593,6 +622,7 @@ Nishant Totla <nishanttotla@gmail.com>
NIWA Hideyuki <niwa.niwa@nifty.ne.jp> NIWA Hideyuki <niwa.niwa@nifty.ne.jp>
Noah Treuhaft <noah.treuhaft@docker.com> Noah Treuhaft <noah.treuhaft@docker.com>
O.S. Tezer <ostezer@gmail.com> O.S. Tezer <ostezer@gmail.com>
Oded Arbel <oded@geek.co.il>
Odin Ugedal <odin@ugedal.com> Odin Ugedal <odin@ugedal.com>
ohmystack <jun.jiang02@ele.me> ohmystack <jun.jiang02@ele.me>
OKA Naoya <git@okanaoya.com> OKA Naoya <git@okanaoya.com>
@ -604,19 +634,21 @@ Otto Kekäläinen <otto@seravo.fi>
Ovidio Mallo <ovidio.mallo@gmail.com> Ovidio Mallo <ovidio.mallo@gmail.com>
Pascal Borreli <pascal@borreli.com> Pascal Borreli <pascal@borreli.com>
Patrick Böänziger <patrick.baenziger@bsi-software.com> Patrick Böänziger <patrick.baenziger@bsi-software.com>
Patrick Daigle <114765035+pdaig@users.noreply.github.com>
Patrick Hemmer <patrick.hemmer@gmail.com> Patrick Hemmer <patrick.hemmer@gmail.com>
Patrick Lang <plang@microsoft.com> Patrick Lang <plang@microsoft.com>
Paul <paul9869@gmail.com> Paul <paul9869@gmail.com>
Paul Kehrer <paul.l.kehrer@gmail.com> Paul Kehrer <paul.l.kehrer@gmail.com>
Paul Lietar <paul@lietar.net> Paul Lietar <paul@lietar.net>
Paul Mulders <justinkb@gmail.com> Paul Mulders <justinkb@gmail.com>
Paul Seyfert <pseyfert.mathphys@gmail.com>
Paul Weaver <pauweave@cisco.com> Paul Weaver <pauweave@cisco.com>
Pavel Pospisil <pospispa@gmail.com> Pavel Pospisil <pospispa@gmail.com>
Paweł Gronowski <pawel.gronowski@docker.com> Paweł Gronowski <pawel.gronowski@docker.com>
Paweł Pokrywka <pepawel@users.noreply.github.com> Paweł Pokrywka <pepawel@users.noreply.github.com>
Paweł Szczekutowicz <pszczekutowicz@gmail.com> Paweł Szczekutowicz <pszczekutowicz@gmail.com>
Peeyush Gupta <gpeeyush@linux.vnet.ibm.com> Peeyush Gupta <gpeeyush@linux.vnet.ibm.com>
Per Lundberg <per.lundberg@ecraft.com> Per Lundberg <perlun@gmail.com>
Peter Dave Hello <hsu@peterdavehello.org> Peter Dave Hello <hsu@peterdavehello.org>
Peter Edge <peter.edge@gmail.com> Peter Edge <peter.edge@gmail.com>
Peter Hsu <shhsu@microsoft.com> Peter Hsu <shhsu@microsoft.com>
@ -639,6 +671,7 @@ Preston Cowley <preston.cowley@sony.com>
Pure White <daniel48@126.com> Pure White <daniel48@126.com>
Qiang Huang <h.huangqiang@huawei.com> Qiang Huang <h.huangqiang@huawei.com>
Qinglan Peng <qinglanpeng@zju.edu.cn> Qinglan Peng <qinglanpeng@zju.edu.cn>
QQ喵 <gqqnb2005@gmail.com>
qudongfang <qudongfang@gmail.com> qudongfang <qudongfang@gmail.com>
Raghavendra K T <raghavendra.kt@linux.vnet.ibm.com> Raghavendra K T <raghavendra.kt@linux.vnet.ibm.com>
Rahul Kadyan <hi@znck.me> Rahul Kadyan <hi@znck.me>
@ -657,6 +690,7 @@ Rick Wieman <git@rickw.nl>
Ritesh H Shukla <sritesh@vmware.com> Ritesh H Shukla <sritesh@vmware.com>
Riyaz Faizullabhoy <riyaz.faizullabhoy@docker.com> Riyaz Faizullabhoy <riyaz.faizullabhoy@docker.com>
Rob Gulewich <rgulewich@netflix.com> Rob Gulewich <rgulewich@netflix.com>
Rob Murray <rob.murray@docker.com>
Robert Wallis <smilingrob@gmail.com> Robert Wallis <smilingrob@gmail.com>
Robin Naundorf <r.naundorf@fh-muenster.de> Robin Naundorf <r.naundorf@fh-muenster.de>
Robin Speekenbrink <robin@kingsquare.nl> Robin Speekenbrink <robin@kingsquare.nl>
@ -689,6 +723,7 @@ Sandro Jäckel <sandro.jaeckel@gmail.com>
Santhosh Manohar <santhosh@docker.com> Santhosh Manohar <santhosh@docker.com>
Sargun Dhillon <sargun@netflix.com> Sargun Dhillon <sargun@netflix.com>
Saswat Bhattacharya <sas.saswat@gmail.com> Saswat Bhattacharya <sas.saswat@gmail.com>
Saurabh Kumar <saurabhkumar0184@gmail.com>
Scott Brenner <scott@scottbrenner.me> Scott Brenner <scott@scottbrenner.me>
Scott Collier <emailscottcollier@gmail.com> Scott Collier <emailscottcollier@gmail.com>
Sean Christopherson <sean.j.christopherson@intel.com> Sean Christopherson <sean.j.christopherson@intel.com>
@ -788,6 +823,7 @@ uhayate <uhayate.gong@daocloud.io>
Ulrich Bareth <ulrich.bareth@gmail.com> Ulrich Bareth <ulrich.bareth@gmail.com>
Ulysses Souza <ulysses.souza@docker.com> Ulysses Souza <ulysses.souza@docker.com>
Umesh Yadav <umesh4257@gmail.com> Umesh Yadav <umesh4257@gmail.com>
Vaclav Struhar <struharv@gmail.com>
Valentin Lorentz <progval+git@progval.net> Valentin Lorentz <progval+git@progval.net>
Vardan Pogosian <vardan.pogosyan@gmail.com> Vardan Pogosian <vardan.pogosyan@gmail.com>
Venkateswara Reddy Bukkasamudram <bukkasamudram@outlook.com> Venkateswara Reddy Bukkasamudram <bukkasamudram@outlook.com>
@ -795,6 +831,7 @@ Veres Lajos <vlajos@gmail.com>
Victor Vieux <victor.vieux@docker.com> Victor Vieux <victor.vieux@docker.com>
Victoria Bialas <victoria.bialas@docker.com> Victoria Bialas <victoria.bialas@docker.com>
Viktor Stanchev <me@viktorstanchev.com> Viktor Stanchev <me@viktorstanchev.com>
Ville Skyttä <ville.skytta@iki.fi>
Vimal Raghubir <vraghubir0418@gmail.com> Vimal Raghubir <vraghubir0418@gmail.com>
Vincent Batts <vbatts@redhat.com> Vincent Batts <vbatts@redhat.com>
Vincent Bernat <Vincent.Bernat@exoscale.ch> Vincent Bernat <Vincent.Bernat@exoscale.ch>
@ -831,6 +868,7 @@ Yong Tang <yong.tang.github@outlook.com>
Yosef Fertel <yfertel@gmail.com> Yosef Fertel <yfertel@gmail.com>
Yu Peng <yu.peng36@zte.com.cn> Yu Peng <yu.peng36@zte.com.cn>
Yuan Sun <sunyuan3@huawei.com> Yuan Sun <sunyuan3@huawei.com>
Yucheng Wu <wyc123wyc@gmail.com>
Yue Zhang <zy675793960@yeah.net> Yue Zhang <zy675793960@yeah.net>
Yunxiang Huang <hyxqshk@vip.qq.com> Yunxiang Huang <hyxqshk@vip.qq.com>
Zachary Romero <zacromero3@gmail.com> Zachary Romero <zacromero3@gmail.com>

View File

@ -1,9 +1,5 @@
# Contributing to Docker # Contributing to Docker
Want to hack on Docker? Awesome! We have a contributor's guide that explains
[setting up a Docker development environment and the contribution
process](https://docs.docker.com/opensource/project/who-written-for/).
This page contains information about reporting issues as well as some tips and This page contains information about reporting issues as well as some tips and
guidelines useful to experienced open source contributors. Finally, make sure guidelines useful to experienced open source contributors. Finally, make sure
you read our [community guidelines](#docker-community-guidelines) before you you read our [community guidelines](#docker-community-guidelines) before you
@ -88,7 +84,7 @@ use for simple changes](https://docs.docker.com/opensource/workflow/make-a-contr
<tr> <tr>
<td>Community Slack</td> <td>Community Slack</td>
<td> <td>
The Docker Community has a dedicated Slack chat to discuss features and issues. You can sign-up <a href="https://dockr.ly/slack" target="_blank">with this link</a>. The Docker Community has a dedicated Slack chat to discuss features and issues. You can sign-up <a href="https://dockr.ly/comm-slack" target="_blank">with this link</a>.
</td> </td>
</tr> </tr>
<tr> <tr>
@ -192,7 +188,7 @@ For more details, see the [MAINTAINERS](MAINTAINERS) page.
The sign-off is a simple line at the end of the explanation for the patch. Your The sign-off is a simple line at the end of the explanation for the patch. Your
signature certifies that you wrote the patch or otherwise have the right to pass signature certifies that you wrote the patch or otherwise have the right to pass
it on as an open-source patch. The rules are pretty simple: if you can certify it on as an open-source patch. The rules are pretty simple: if you can certify
the below (from [developercertificate.org](http://developercertificate.org/)): the below (from [developercertificate.org](https://developercertificate.org):
``` ```
Developer Certificate of Origin Developer Certificate of Origin
@ -336,9 +332,8 @@ The rules:
1. All code should be formatted with `gofumpt` (preferred) or `gofmt -s`. 1. All code should be formatted with `gofumpt` (preferred) or `gofmt -s`.
2. All code should pass the default levels of 2. All code should pass the default levels of
[`golint`](https://github.com/golang/lint). [`golint`](https://github.com/golang/lint).
3. All code should follow the guidelines covered in [Effective 3. All code should follow the guidelines covered in [Effective Go](https://go.dev/doc/effective_go)
Go](http://golang.org/doc/effective_go.html) and [Go Code Review and [Go Code Review Comments](https://github.com/golang/go/wiki/CodeReviewComments).
Comments](https://github.com/golang/go/wiki/CodeReviewComments).
4. Comment the code. Tell us the why, the history and the context. 4. Comment the code. Tell us the why, the history and the context.
5. Document _all_ declarations and methods, even private ones. Declare 5. Document _all_ declarations and methods, even private ones. Declare
expectations, caveats and anything else that may be important. If a type expectations, caveats and anything else that may be important. If a type
@ -360,6 +355,6 @@ The rules:
guidelines. Since you've read all the rules, you now know that. guidelines. Since you've read all the rules, you now know that.
If you are having trouble getting into the mood of idiomatic Go, we recommend If you are having trouble getting into the mood of idiomatic Go, we recommend
reading through [Effective Go](https://golang.org/doc/effective_go.html). The reading through [Effective Go](https://go.dev/doc/effective_go). The
[Go Blog](https://blog.golang.org) is also a great resource. Drinking the [Go Blog](https://go.dev/blog/) is also a great resource. Drinking the
kool-aid is a lot easier than going thirsty. kool-aid is a lot easier than going thirsty.

View File

@ -1,17 +1,21 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
ARG BASE_VARIANT=alpine ARG BASE_VARIANT=alpine
ARG GO_VERSION=1.20.5 ARG ALPINE_VERSION=3.18
ARG ALPINE_VERSION=3.17 ARG BASE_DEBIAN_DISTRO=bookworm
ARG XX_VERSION=1.1.1
ARG GO_VERSION=1.21.9
ARG XX_VERSION=1.4.0
ARG GOVERSIONINFO_VERSION=v1.3.0 ARG GOVERSIONINFO_VERSION=v1.3.0
ARG GOTESTSUM_VERSION=v1.10.0 ARG GOTESTSUM_VERSION=v1.10.0
ARG BUILDX_VERSION=0.11.0 ARG BUILDX_VERSION=0.12.1
ARG COMPOSE_VERSION=v2.24.3
FROM --platform=$BUILDPLATFORM tonistiigi/xx:${XX_VERSION} AS xx FROM --platform=$BUILDPLATFORM tonistiigi/xx:${XX_VERSION} AS xx
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS build-base-alpine FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS build-base-alpine
COPY --from=xx / / ENV GOTOOLCHAIN=local
COPY --link --from=xx / /
RUN apk add --no-cache bash clang lld llvm file git RUN apk add --no-cache bash clang lld llvm file git
WORKDIR /go/src/github.com/docker/cli WORKDIR /go/src/github.com/docker/cli
@ -20,33 +24,27 @@ ARG TARGETPLATFORM
# gcc is installed for libgcc only # gcc is installed for libgcc only
RUN xx-apk add --no-cache musl-dev gcc RUN xx-apk add --no-cache musl-dev gcc
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-bullseye AS build-base-bullseye FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-${BASE_DEBIAN_DISTRO} AS build-base-debian
COPY --from=xx / / ENV GOTOOLCHAIN=local
COPY --link --from=xx / /
RUN apt-get update && apt-get install --no-install-recommends -y bash clang lld llvm file RUN apt-get update && apt-get install --no-install-recommends -y bash clang lld llvm file
WORKDIR /go/src/github.com/docker/cli WORKDIR /go/src/github.com/docker/cli
FROM build-base-bullseye AS build-bullseye FROM build-base-debian AS build-debian
ARG TARGETPLATFORM ARG TARGETPLATFORM
RUN xx-apt-get install --no-install-recommends -y libc6-dev libgcc-10-dev RUN xx-apt-get install --no-install-recommends -y libc6-dev libgcc-12-dev pkgconf
# workaround for issue with llvm 11 for darwin/amd64 platform:
# # github.com/docker/cli/cmd/docker
# /usr/local/go/pkg/tool/linux_amd64/link: /usr/local/go/pkg/tool/linux_amd64/link: running strip failed: exit status 1
# llvm-strip: error: unsupported load command (cmd=0x5)
# more info: https://github.com/docker/cli/pull/3717
# FIXME: remove once llvm 12 available on debian
RUN [ "$TARGETPLATFORM" != "darwin/amd64" ] || ln -sfnT /bin/true /usr/bin/llvm-strip
FROM build-base-${BASE_VARIANT} AS goversioninfo FROM build-base-${BASE_VARIANT} AS goversioninfo
ARG GOVERSIONINFO_VERSION ARG GOVERSIONINFO_VERSION
RUN --mount=type=cache,target=/root/.cache/go-build \ RUN --mount=type=cache,target=/root/.cache/go-build \
--mount=type=cache,target=/go/pkg/mod \ --mount=type=cache,target=/go/pkg/mod \
GOBIN=/out GO111MODULE=on go install "github.com/josephspurrier/goversioninfo/cmd/goversioninfo@${GOVERSIONINFO_VERSION}" GOBIN=/out GO111MODULE=on CGO_ENABLED=0 go install "github.com/josephspurrier/goversioninfo/cmd/goversioninfo@${GOVERSIONINFO_VERSION}"
FROM build-base-${BASE_VARIANT} AS gotestsum FROM build-base-${BASE_VARIANT} AS gotestsum
ARG GOTESTSUM_VERSION ARG GOTESTSUM_VERSION
RUN --mount=type=cache,target=/root/.cache/go-build \ RUN --mount=type=cache,target=/root/.cache/go-build \
--mount=type=cache,target=/go/pkg/mod \ --mount=type=cache,target=/go/pkg/mod \
GOBIN=/out GO111MODULE=on go install "gotest.tools/gotestsum@${GOTESTSUM_VERSION}" \ GOBIN=/out GO111MODULE=on CGO_ENABLED=0 go install "gotest.tools/gotestsum@${GOTESTSUM_VERSION}" \
&& /out/gotestsum --version && /out/gotestsum --version
FROM build-${BASE_VARIANT} AS build FROM build-${BASE_VARIANT} AS build
@ -62,9 +60,7 @@ ARG CGO_ENABLED
ARG VERSION ARG VERSION
# PACKAGER_NAME sets the company that produced the windows binary # PACKAGER_NAME sets the company that produced the windows binary
ARG PACKAGER_NAME ARG PACKAGER_NAME
COPY --from=goversioninfo /out/goversioninfo /usr/bin/goversioninfo COPY --link --from=goversioninfo /out/goversioninfo /usr/bin/goversioninfo
# in bullseye arm64 target does not link with lld so configure it to use ld instead
RUN [ ! -f /etc/alpine-release ] && xx-info is-cross && [ "$(xx-info arch)" = "arm64" ] && XX_CC_PREFER_LINKER=ld xx-clang --setup-target-triple || true
RUN --mount=type=bind,target=.,ro \ RUN --mount=type=bind,target=.,ro \
--mount=type=cache,target=/root/.cache \ --mount=type=cache,target=/root/.cache \
--mount=from=dockercore/golang-cross:xx-sdk-extras,target=/xx-sdk,src=/xx-sdk \ --mount=from=dockercore/golang-cross:xx-sdk-extras,target=/xx-sdk,src=/xx-sdk \
@ -76,7 +72,7 @@ RUN --mount=type=bind,target=.,ro \
xx-verify $([ "$GO_LINKMODE" = "static" ] && echo "--static") /out/docker xx-verify $([ "$GO_LINKMODE" = "static" ] && echo "--static") /out/docker
FROM build-${BASE_VARIANT} AS test FROM build-${BASE_VARIANT} AS test
COPY --from=gotestsum /out/gotestsum /usr/bin/gotestsum COPY --link --from=gotestsum /out/gotestsum /usr/bin/gotestsum
ENV GO111MODULE=auto ENV GO111MODULE=auto
RUN --mount=type=bind,target=.,rw \ RUN --mount=type=bind,target=.,rw \
--mount=type=cache,target=/root/.cache \ --mount=type=cache,target=/root/.cache \
@ -98,35 +94,43 @@ RUN --mount=ro --mount=type=cache,target=/root/.cache \
TARGET=/out ./scripts/build/plugins e2e/cli-plugins/plugins/* TARGET=/out ./scripts/build/plugins e2e/cli-plugins/plugins/*
FROM build-base-alpine AS e2e-base-alpine FROM build-base-alpine AS e2e-base-alpine
RUN apk add --no-cache build-base curl docker-compose openssl openssh-client RUN apk add --no-cache build-base curl openssl openssh-client
FROM build-base-bullseye AS e2e-base-bullseye FROM build-base-debian AS e2e-base-debian
RUN apt-get update && apt-get install -y build-essential curl openssl openssh-client RUN apt-get update && apt-get install -y build-essential curl openssl openssh-client
ARG COMPOSE_VERSION=1.29.2
RUN curl -fsSL https://github.com/docker/compose/releases/download/${COMPOSE_VERSION}/docker-compose-$(uname -s)-$(uname -m) -o /usr/local/bin/docker-compose && \
chmod +x /usr/local/bin/docker-compose
FROM docker/buildx-bin:${BUILDX_VERSION} AS buildx FROM docker/buildx-bin:${BUILDX_VERSION} AS buildx
FROM docker/compose-bin:${COMPOSE_VERSION} AS compose
FROM e2e-base-${BASE_VARIANT} AS e2e FROM e2e-base-${BASE_VARIANT} AS e2e
ARG NOTARY_VERSION=v0.6.1 ARG NOTARY_VERSION=v0.6.1
ADD --chmod=0755 https://github.com/theupdateframework/notary/releases/download/${NOTARY_VERSION}/notary-Linux-amd64 /usr/local/bin/notary ADD --chmod=0755 https://github.com/theupdateframework/notary/releases/download/${NOTARY_VERSION}/notary-Linux-amd64 /usr/local/bin/notary
COPY e2e/testdata/notary/root-ca.cert /usr/share/ca-certificates/notary.cert COPY --link e2e/testdata/notary/root-ca.cert /usr/share/ca-certificates/notary.cert
RUN echo 'notary.cert' >> /etc/ca-certificates.conf && update-ca-certificates RUN echo 'notary.cert' >> /etc/ca-certificates.conf && update-ca-certificates
COPY --from=gotestsum /out/gotestsum /usr/bin/gotestsum COPY --link --from=gotestsum /out/gotestsum /usr/bin/gotestsum
COPY --from=build /out ./build/ COPY --link --from=build /out ./build/
COPY --from=build-plugins /out ./build/ COPY --link --from=build-plugins /out ./build/
COPY --from=buildx /buildx /usr/libexec/docker/cli-plugins/docker-buildx COPY --link --from=buildx /buildx /usr/libexec/docker/cli-plugins/docker-buildx
COPY . . COPY --link --from=compose /docker-compose /usr/libexec/docker/cli-plugins/docker-compose
COPY --link . .
ENV DOCKER_BUILDKIT=1 ENV DOCKER_BUILDKIT=1
ENV PATH=/go/src/github.com/docker/cli/build:$PATH ENV PATH=/go/src/github.com/docker/cli/build:$PATH
CMD ./scripts/test/e2e/entry CMD ./scripts/test/e2e/entry
FROM build-base-${BASE_VARIANT} AS dev FROM build-base-${BASE_VARIANT} AS dev
COPY . . COPY --link . .
FROM scratch AS plugins FROM scratch AS plugins
COPY --from=build-plugins /out . COPY --from=build-plugins /out .
FROM scratch AS bin-image-linux
COPY --from=build /out/docker /docker
FROM scratch AS bin-image-darwin
COPY --from=build /out/docker /docker
FROM scratch AS bin-image-windows
COPY --from=build /out/docker /docker.exe
FROM bin-image-${TARGETOS} AS bin-image
FROM scratch AS binary FROM scratch AS binary
COPY --from=build /out . COPY --from=build /out .

View File

@ -24,7 +24,6 @@
people = [ people = [
"albers", "albers",
"cpuguy83", "cpuguy83",
"ndeloof",
"rumpl", "rumpl",
"silvin-lubecki", "silvin-lubecki",
"stevvooe", "stevvooe",
@ -98,11 +97,6 @@
Email = "dnephin@gmail.com" Email = "dnephin@gmail.com"
GitHub = "dnephin" GitHub = "dnephin"
[people.ndeloof]
Name = "Nicolas De Loof"
Email = "nicolas.deloof@gmail.com"
GitHub = "ndeloof"
[people.neersighted] [people.neersighted]
Name = "Bjorn Neergaard" Name = "Bjorn Neergaard"
Email = "bneergaard@mirantis.com" Email = "bneergaard@mirantis.com"

View File

@ -52,7 +52,7 @@ shellcheck: ## run shellcheck validation
.PHONY: fmt .PHONY: fmt
fmt: ## run gofumpt (if present) or gofmt fmt: ## run gofumpt (if present) or gofmt
@if command -v gofumpt > /dev/null; then \ @if command -v gofumpt > /dev/null; then \
gofumpt -w -d -lang=1.19 . ; \ gofumpt -w -d -lang=1.21 . ; \
else \ else \
go list -f {{.Dir}} ./... | xargs gofmt -w -s -d ; \ go list -f {{.Dir}} ./... | xargs gofmt -w -s -d ; \
fi fi

View File

@ -8,8 +8,7 @@
## About ## About
This repository is the home of the cli used in the Docker CE and This repository is the home of the Docker CLI.
Docker EE products.
## Development ## Development

View File

@ -45,7 +45,7 @@ func main() {
} }
var ( var (
who, context string who, optContext string
preRun, debug bool preRun, debug bool
) )
cmd := &cobra.Command{ cmd := &cobra.Command{
@ -65,7 +65,7 @@ func main() {
fmt.Fprintf(dockerCli.Err(), "Plugin debug mode enabled") fmt.Fprintf(dockerCli.Err(), "Plugin debug mode enabled")
} }
switch context { switch optContext {
case "Christmas": case "Christmas":
fmt.Fprintf(dockerCli.Out(), "Merry Christmas!\n") fmt.Fprintf(dockerCli.Out(), "Merry Christmas!\n")
return nil return nil
@ -92,7 +92,7 @@ func main() {
// These are intended to deliberately clash with the CLIs own top // These are intended to deliberately clash with the CLIs own top
// level arguments. // level arguments.
flags.BoolVarP(&debug, "debug", "D", false, "Enable debug") flags.BoolVarP(&debug, "debug", "D", false, "Enable debug")
flags.StringVarP(&context, "context", "c", "", "Is it Christmas?") flags.StringVarP(&optContext, "context", "c", "", "Is it Christmas?")
cmd.AddCommand(goodbye, apiversion, exitStatus2) cmd.AddCommand(goodbye, apiversion, exitStatus2)
return cmd return cmd

View File

@ -0,0 +1,18 @@
package hooks
import (
"fmt"
"io"
"github.com/morikuni/aec"
)
func PrintNextSteps(out io.Writer, messages []string) {
if len(messages) == 0 {
return
}
fmt.Fprintln(out, aec.Bold.Apply("\nWhat's next:"))
for _, n := range messages {
_, _ = fmt.Fprintf(out, " %s\n", n)
}
}

View File

@ -0,0 +1,38 @@
package hooks
import (
"bytes"
"testing"
"github.com/morikuni/aec"
"gotest.tools/v3/assert"
)
func TestPrintHookMessages(t *testing.T) {
testCases := []struct {
messages []string
expectedOutput string
}{
{
messages: []string{},
expectedOutput: "",
},
{
messages: []string{"Bork!"},
expectedOutput: aec.Bold.Apply("\nWhat's next:") + "\n" +
" Bork!\n",
},
{
messages: []string{"Foo", "bar"},
expectedOutput: aec.Bold.Apply("\nWhat's next:") + "\n" +
" Foo\n" +
" bar\n",
},
}
for _, tc := range testCases {
w := bytes.Buffer{}
PrintNextSteps(&w, tc.messages)
assert.Equal(t, w.String(), tc.expectedOutput)
}
}

View File

@ -0,0 +1,116 @@
package hooks
import (
"bytes"
"errors"
"fmt"
"strconv"
"strings"
"text/template"
"github.com/spf13/cobra"
)
type HookType int
const (
NextSteps = iota
)
// HookMessage represents a plugin hook response. Plugins
// declaring support for CLI hooks need to print a json
// representation of this type when their hook subcommand
// is invoked.
type HookMessage struct {
Type HookType
Template string
}
// TemplateReplaceSubcommandName returns a hook template string
// that will be replaced by the CLI subcommand being executed
//
// Example:
//
// "you ran the subcommand: " + TemplateReplaceSubcommandName()
//
// when being executed after the command:
// `docker run --name "my-container" alpine`
// will result in the message:
// `you ran the subcommand: run`
func TemplateReplaceSubcommandName() string {
return hookTemplateCommandName
}
// TemplateReplaceFlagValue returns a hook template string
// that will be replaced by the flags value.
//
// Example:
//
// "you ran a container named: " + TemplateReplaceFlagValue("name")
//
// when being executed after the command:
// `docker run --name "my-container" alpine`
// will result in the message:
// `you ran a container named: my-container`
func TemplateReplaceFlagValue(flag string) string {
return fmt.Sprintf(hookTemplateFlagValue, flag)
}
// TemplateReplaceArg takes an index i and returns a hook
// template string that the CLI will replace the template with
// the ith argument, after processing the passed flags.
//
// Example:
//
// "run this image with `docker run " + TemplateReplaceArg(0) + "`"
//
// when being executed after the command:
// `docker pull alpine`
// will result in the message:
// "Run this image with `docker run alpine`"
func TemplateReplaceArg(i int) string {
return fmt.Sprintf(hookTemplateArg, strconv.Itoa(i))
}
func ParseTemplate(hookTemplate string, cmd *cobra.Command) ([]string, error) {
tmpl := template.New("").Funcs(commandFunctions)
tmpl, err := tmpl.Parse(hookTemplate)
if err != nil {
return nil, err
}
b := bytes.Buffer{}
err = tmpl.Execute(&b, cmd)
if err != nil {
return nil, err
}
return strings.Split(b.String(), "\n"), nil
}
var ErrHookTemplateParse = errors.New("failed to parse hook template")
const (
hookTemplateCommandName = "{{.Name}}"
hookTemplateFlagValue = `{{flag . "%s"}}`
hookTemplateArg = "{{arg . %s}}"
)
var commandFunctions = template.FuncMap{
"flag": getFlagValue,
"arg": getArgValue,
}
func getFlagValue(cmd *cobra.Command, flag string) (string, error) {
cmdFlag := cmd.Flag(flag)
if cmdFlag == nil {
return "", ErrHookTemplateParse
}
return cmdFlag.Value.String(), nil
}
func getArgValue(cmd *cobra.Command, i int) (string, error) {
flags := cmd.Flags()
if flags == nil {
return "", ErrHookTemplateParse
}
return flags.Arg(i), nil
}

View File

@ -0,0 +1,86 @@
package hooks
import (
"testing"
"github.com/spf13/cobra"
"gotest.tools/v3/assert"
)
func TestParseTemplate(t *testing.T) {
type testFlag struct {
name string
value string
}
testCases := []struct {
template string
flags []testFlag
args []string
expectedOutput []string
}{
{
template: "",
expectedOutput: []string{""},
},
{
template: "a plain template message",
expectedOutput: []string{"a plain template message"},
},
{
template: TemplateReplaceFlagValue("tag"),
flags: []testFlag{
{
name: "tag",
value: "my-tag",
},
},
expectedOutput: []string{"my-tag"},
},
{
template: TemplateReplaceFlagValue("test-one") + " " + TemplateReplaceFlagValue("test2"),
flags: []testFlag{
{
name: "test-one",
value: "value",
},
{
name: "test2",
value: "value2",
},
},
expectedOutput: []string{"value value2"},
},
{
template: TemplateReplaceArg(0) + " " + TemplateReplaceArg(1),
args: []string{"zero", "one"},
expectedOutput: []string{"zero one"},
},
{
template: "You just pulled " + TemplateReplaceArg(0),
args: []string{"alpine"},
expectedOutput: []string{"You just pulled alpine"},
},
{
template: "one line\nanother line!",
expectedOutput: []string{"one line", "another line!"},
},
}
for _, tc := range testCases {
testCmd := &cobra.Command{
Use: "pull",
Args: cobra.ExactArgs(len(tc.args)),
}
for _, f := range tc.flags {
_ = testCmd.Flags().String(f.name, "", "")
err := testCmd.Flag(f.name).Value.Set(f.value)
assert.NilError(t, err)
}
err := testCmd.Flags().Parse(tc.args)
assert.NilError(t, err)
out, err := ParseTemplate(tc.template, testCmd)
assert.NilError(t, err)
assert.DeepEqual(t, out, tc.expectedOutput)
}
}

View File

@ -75,13 +75,14 @@ func TestValidateCandidate(t *testing.T) {
} { } {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
p, err := newPlugin(tc.c, fakeroot.Commands()) p, err := newPlugin(tc.c, fakeroot.Commands())
if tc.err != "" { switch {
case tc.err != "":
assert.ErrorContains(t, err, tc.err) assert.ErrorContains(t, err, tc.err)
} else if tc.invalid != "" { case tc.invalid != "":
assert.NilError(t, err) assert.NilError(t, err)
assert.Assert(t, cmp.ErrorType(p.Err, reflect.TypeOf(&pluginError{}))) assert.Assert(t, cmp.ErrorType(p.Err, reflect.TypeOf(&pluginError{})))
assert.ErrorContains(t, p.Err, tc.invalid) assert.ErrorContains(t, p.Err, tc.invalid)
} else { default:
assert.NilError(t, err) assert.NilError(t, err)
assert.Equal(t, NamePrefix+p.Name, goodPluginName) assert.Equal(t, NamePrefix+p.Name, goodPluginName)
assert.Equal(t, p.SchemaVersion, "0.1.0") assert.Equal(t, p.SchemaVersion, "0.1.0")

View File

@ -2,11 +2,14 @@ package manager
import ( import (
"fmt" "fmt"
"net/url"
"os" "os"
"strings"
"sync" "sync"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"go.opentelemetry.io/otel/attribute"
) )
const ( const (
@ -30,6 +33,10 @@ const (
// is, one which failed it's candidate test) and contains the // is, one which failed it's candidate test) and contains the
// reason for the failure. // reason for the failure.
CommandAnnotationPluginInvalid = "com.docker.cli.plugin-invalid" CommandAnnotationPluginInvalid = "com.docker.cli.plugin-invalid"
// CommandAnnotationPluginCommandPath is added to overwrite the
// command path for a plugin invocation.
CommandAnnotationPluginCommandPath = "com.docker.cli.plugin.command_path"
) )
var pluginCommandStubsOnce sync.Once var pluginCommandStubsOnce sync.Once
@ -98,3 +105,44 @@ func AddPluginCommandStubs(dockerCli command.Cli, rootCmd *cobra.Command) (err e
}) })
return err return err
} }
const (
dockerCliAttributePrefix = attribute.Key("docker.cli")
cobraCommandPath = attribute.Key("cobra.command_path")
)
func getPluginResourceAttributes(cmd *cobra.Command, plugin Plugin) attribute.Set {
commandPath := cmd.Annotations[CommandAnnotationPluginCommandPath]
if commandPath == "" {
commandPath = fmt.Sprintf("%s %s", cmd.CommandPath(), plugin.Name)
}
attrSet := attribute.NewSet(
cobraCommandPath.String(commandPath),
)
kvs := make([]attribute.KeyValue, 0, attrSet.Len())
for iter := attrSet.Iter(); iter.Next(); {
attr := iter.Attribute()
kvs = append(kvs, attribute.KeyValue{
Key: dockerCliAttributePrefix + "." + attr.Key,
Value: attr.Value,
})
}
return attribute.NewSet(kvs...)
}
func appendPluginResourceAttributesEnvvar(env []string, cmd *cobra.Command, plugin Plugin) []string {
if attrs := getPluginResourceAttributes(cmd, plugin); attrs.Len() > 0 {
// values in environment variables need to be in baggage format
// otel/baggage package can be used after update to v1.22, currently it encodes incorrectly
attrsSlice := make([]string, attrs.Len())
for iter := attrs.Iter(); iter.Next(); {
i, v := iter.IndexedAttribute()
attrsSlice[i] = string(v.Key) + "=" + url.PathEscape(v.Value.AsString())
}
env = append(env, ResourceAttributesEnvvar+"="+strings.Join(attrsSlice, ","))
}
return env
}

View File

@ -1,3 +1,6 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.19
package manager package manager
import ( import (
@ -38,11 +41,14 @@ func (e *pluginError) MarshalText() (text []byte, err error) {
// wrapAsPluginError wraps an error in a pluginError with an // wrapAsPluginError wraps an error in a pluginError with an
// additional message, analogous to errors.Wrapf. // additional message, analogous to errors.Wrapf.
func wrapAsPluginError(err error, msg string) error { func wrapAsPluginError(err error, msg string) error {
if err == nil {
return nil
}
return &pluginError{cause: errors.Wrap(err, msg)} return &pluginError{cause: errors.Wrap(err, msg)}
} }
// NewPluginError creates a new pluginError, analogous to // NewPluginError creates a new pluginError, analogous to
// errors.Errorf. // errors.Errorf.
func NewPluginError(msg string, args ...interface{}) error { func NewPluginError(msg string, args ...any) error {
return &pluginError{cause: errors.Errorf(msg, args...)} return &pluginError{cause: errors.Errorf(msg, args...)}
} }

View File

@ -0,0 +1,127 @@
package manager
import (
"encoding/json"
"strings"
"github.com/docker/cli/cli-plugins/hooks"
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
// HookPluginData is the type representing the information
// that plugins declaring support for hooks get passed when
// being invoked following a CLI command execution.
type HookPluginData struct {
RootCmd string
Flags map[string]string
}
// RunPluginHooks calls the hook subcommand for all present
// CLI plugins that declare support for hooks in their metadata
// and parses/prints their responses.
func RunPluginHooks(dockerCli command.Cli, rootCmd, subCommand *cobra.Command, plugin string, args []string) error {
subCmdName := subCommand.Name()
if plugin != "" {
subCmdName = plugin
}
var flags map[string]string
if plugin == "" {
flags = getCommandFlags(subCommand)
} else {
flags = getNaiveFlags(args)
}
nextSteps := invokeAndCollectHooks(dockerCli, rootCmd, subCommand, subCmdName, flags)
hooks.PrintNextSteps(dockerCli.Err(), nextSteps)
return nil
}
func invokeAndCollectHooks(dockerCli command.Cli, rootCmd, subCmd *cobra.Command, hookCmdName string, flags map[string]string) []string {
pluginsCfg := dockerCli.ConfigFile().Plugins
if pluginsCfg == nil {
return nil
}
nextSteps := make([]string, 0, len(pluginsCfg))
for pluginName, cfg := range pluginsCfg {
if !registersHook(cfg, hookCmdName) {
continue
}
p, err := GetPlugin(pluginName, dockerCli, rootCmd)
if err != nil {
continue
}
hookReturn, err := p.RunHook(hookCmdName, flags)
if err != nil {
// skip misbehaving plugins, but don't halt execution
continue
}
var hookMessageData hooks.HookMessage
err = json.Unmarshal(hookReturn, &hookMessageData)
if err != nil {
continue
}
// currently the only hook type
if hookMessageData.Type != hooks.NextSteps {
continue
}
processedHook, err := hooks.ParseTemplate(hookMessageData.Template, subCmd)
if err != nil {
continue
}
nextSteps = append(nextSteps, processedHook...)
}
return nextSteps
}
func registersHook(pluginCfg map[string]string, subCmdName string) bool {
hookCmdStr, ok := pluginCfg["hooks"]
if !ok {
return false
}
commands := strings.Split(hookCmdStr, ",")
for _, hookCmd := range commands {
if hookCmd == subCmdName {
return true
}
}
return false
}
func getCommandFlags(cmd *cobra.Command) map[string]string {
flags := make(map[string]string)
cmd.Flags().Visit(func(f *pflag.Flag) {
var fValue string
if f.Value.Type() == "bool" {
fValue = f.Value.String()
}
flags[f.Name] = fValue
})
return flags
}
// getNaiveFlags string-matches argv and parses them into a map.
// This is used when calling hooks after a plugin command, since
// in this case we can't rely on the cobra command tree to parse
// flags in this case. In this case, no values are ever passed,
// since we don't have enough information to process them.
func getNaiveFlags(args []string) map[string]string {
flags := make(map[string]string)
for _, arg := range args {
if strings.HasPrefix(arg, "--") {
flags[arg[2:]] = ""
continue
}
if strings.HasPrefix(arg, "-") {
flags[arg[1:]] = ""
}
}
return flags
}

View File

@ -0,0 +1,38 @@
package manager
import (
"testing"
"gotest.tools/v3/assert"
)
func TestGetNaiveFlags(t *testing.T) {
testCases := []struct {
args []string
expectedFlags map[string]string
}{
{
args: []string{"docker"},
expectedFlags: map[string]string{},
},
{
args: []string{"docker", "build", "-q", "--file", "test.Dockerfile", "."},
expectedFlags: map[string]string{
"q": "",
"file": "",
},
},
{
args: []string{"docker", "--context", "a-context", "pull", "-q", "--progress", "auto", "alpine"},
expectedFlags: map[string]string{
"context": "",
"q": "",
"progress": "",
},
},
}
for _, tc := range testCases {
assert.DeepEqual(t, getNaiveFlags(tc.args), tc.expectedFlags)
}
}

View File

@ -11,16 +11,23 @@ import (
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/config" "github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/config/configfile"
"github.com/fvbommel/sortorder" "github.com/fvbommel/sortorder"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
) )
const (
// ReexecEnvvar is the name of an ennvar which is set to the command // ReexecEnvvar is the name of an ennvar which is set to the command
// used to originally invoke the docker CLI when executing a // used to originally invoke the docker CLI when executing a
// plugin. Assuming $PATH and $CWD remain unchanged this should allow // plugin. Assuming $PATH and $CWD remain unchanged this should allow
// the plugin to re-execute the original CLI. // the plugin to re-execute the original CLI.
const ReexecEnvvar = "DOCKER_CLI_PLUGIN_ORIGINAL_CLI_COMMAND" ReexecEnvvar = "DOCKER_CLI_PLUGIN_ORIGINAL_CLI_COMMAND"
// ResourceAttributesEnvvar is the name of the envvar that includes additional
// resource attributes for OTEL.
ResourceAttributesEnvvar = "OTEL_RESOURCE_ATTRIBUTES"
)
// errPluginNotFound is the error returned when a plugin could not be found. // errPluginNotFound is the error returned when a plugin could not be found.
type errPluginNotFound string type errPluginNotFound string
@ -42,10 +49,10 @@ func IsNotFound(err error) bool {
return ok return ok
} }
func getPluginDirs(dockerCli command.Cli) ([]string, error) { func getPluginDirs(cfg *configfile.ConfigFile) ([]string, error) {
var pluginDirs []string var pluginDirs []string
if cfg := dockerCli.ConfigFile(); cfg != nil { if cfg != nil {
pluginDirs = append(pluginDirs, cfg.CLIPluginsExtraDirs...) pluginDirs = append(pluginDirs, cfg.CLIPluginsExtraDirs...)
} }
pluginDir, err := config.Path("cli-plugins") pluginDir, err := config.Path("cli-plugins")
@ -108,7 +115,7 @@ func listPluginCandidates(dirs []string) (map[string][]string, error) {
// GetPlugin returns a plugin on the system by its name // GetPlugin returns a plugin on the system by its name
func GetPlugin(name string, dockerCli command.Cli, rootcmd *cobra.Command) (*Plugin, error) { func GetPlugin(name string, dockerCli command.Cli, rootcmd *cobra.Command) (*Plugin, error) {
pluginDirs, err := getPluginDirs(dockerCli) pluginDirs, err := getPluginDirs(dockerCli.ConfigFile())
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -138,7 +145,7 @@ func GetPlugin(name string, dockerCli command.Cli, rootcmd *cobra.Command) (*Plu
// ListPlugins produces a list of the plugins available on the system // ListPlugins produces a list of the plugins available on the system
func ListPlugins(dockerCli command.Cli, rootcmd *cobra.Command) ([]Plugin, error) { func ListPlugins(dockerCli command.Cli, rootcmd *cobra.Command) ([]Plugin, error) {
pluginDirs, err := getPluginDirs(dockerCli) pluginDirs, err := getPluginDirs(dockerCli.ConfigFile())
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -198,7 +205,7 @@ func PluginRunCommand(dockerCli command.Cli, name string, rootcmd *cobra.Command
return nil, errPluginNotFound(name) return nil, errPluginNotFound(name)
} }
exename := addExeSuffix(NamePrefix + name) exename := addExeSuffix(NamePrefix + name)
pluginDirs, err := getPluginDirs(dockerCli) pluginDirs, err := getPluginDirs(dockerCli.ConfigFile())
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -235,6 +242,7 @@ func PluginRunCommand(dockerCli command.Cli, name string, rootcmd *cobra.Command
cmd.Env = os.Environ() cmd.Env = os.Environ()
cmd.Env = append(cmd.Env, ReexecEnvvar+"="+os.Args[0]) cmd.Env = append(cmd.Env, ReexecEnvvar+"="+os.Args[0])
cmd.Env = appendPluginResourceAttributesEnvvar(cmd.Env, rootcmd, plugin)
return cmd, nil return cmd, nil
} }

View File

@ -46,7 +46,7 @@ func TestListPluginCandidates(t *testing.T) {
) )
defer dir.Remove() defer dir.Remove()
var dirs []string dirs := make([]string, 0, 6)
for _, d := range []string{"plugins1", "nonexistent", "plugins2", "plugins3", "plugins4", "plugins5"} { for _, d := range []string{"plugins1", "nonexistent", "plugins2", "plugins3", "plugins4", "plugins5"} {
dirs = append(dirs, dir.Join(d)) dirs = append(dirs, dir.Join(d))
} }
@ -149,7 +149,7 @@ func TestGetPluginDirs(t *testing.T) {
expected := append([]string{pluginDir}, defaultSystemPluginDirs...) expected := append([]string{pluginDir}, defaultSystemPluginDirs...)
var pluginDirs []string var pluginDirs []string
pluginDirs, err = getPluginDirs(cli) pluginDirs, err = getPluginDirs(cli.ConfigFile())
assert.Equal(t, strings.Join(expected, ":"), strings.Join(pluginDirs, ":")) assert.Equal(t, strings.Join(expected, ":"), strings.Join(pluginDirs, ":"))
assert.NilError(t, err) assert.NilError(t, err)
@ -160,7 +160,7 @@ func TestGetPluginDirs(t *testing.T) {
cli.SetConfigFile(&configfile.ConfigFile{ cli.SetConfigFile(&configfile.ConfigFile{
CLIPluginsExtraDirs: extras, CLIPluginsExtraDirs: extras,
}) })
pluginDirs, err = getPluginDirs(cli) pluginDirs, err = getPluginDirs(cli.ConfigFile())
assert.DeepEqual(t, expected, pluginDirs) assert.DeepEqual(t, expected, pluginDirs)
assert.NilError(t, err) assert.NilError(t, err)
} }

View File

@ -8,6 +8,11 @@ const (
// which must be supported by every plugin and returns the // which must be supported by every plugin and returns the
// plugin metadata. // plugin metadata.
MetadataSubcommandName = "docker-cli-plugin-metadata" MetadataSubcommandName = "docker-cli-plugin-metadata"
// HookSubcommandName is the name of the plugin subcommand
// which must be implemented by plugins declaring support
// for hooks in their metadata.
HookSubcommandName = "docker-cli-plugin-hooks"
) )
// Metadata provided by the plugin. // Metadata provided by the plugin.

View File

@ -2,6 +2,8 @@ package manager
import ( import (
"encoding/json" "encoding/json"
"os"
"os/exec"
"path/filepath" "path/filepath"
"regexp" "regexp"
"strings" "strings"
@ -100,3 +102,25 @@ func newPlugin(c Candidate, cmds []*cobra.Command) (Plugin, error) {
} }
return p, nil return p, nil
} }
// RunHook executes the plugin's hooks command
// and returns its unprocessed output.
func (p *Plugin) RunHook(cmdName string, flags map[string]string) ([]byte, error) {
hDataBytes, err := json.Marshal(HookPluginData{
RootCmd: cmdName,
Flags: flags,
})
if err != nil {
return nil, wrapAsPluginError(err, "failed to marshall hook data")
}
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()
if err != nil {
return nil, wrapAsPluginError(err, "failed to execute plugin hook subcommand")
}
return hookCmdOutput, nil
}

View File

@ -1,6 +1,7 @@
package plugin package plugin
import ( import (
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"os" "os"
@ -8,17 +9,20 @@ import (
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
"github.com/docker/cli/cli-plugins/manager" "github.com/docker/cli/cli-plugins/manager"
"github.com/docker/cli/cli-plugins/socket"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/connhelper" "github.com/docker/cli/cli/connhelper"
"github.com/docker/cli/cli/debug"
"github.com/docker/docker/client" "github.com/docker/docker/client"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"go.opentelemetry.io/otel"
) )
// PersistentPreRunE must be called by any plugin command (or // PersistentPreRunE must be called by any plugin command (or
// subcommand) which uses the cobra `PersistentPreRun*` hook. Plugins // subcommand) which uses the cobra `PersistentPreRun*` hook. Plugins
// which do not make use of `PersistentPreRun*` do not need to call // which do not make use of `PersistentPreRun*` do not need to call
// this (although it remains safe to do so). Plugins are recommended // this (although it remains safe to do so). Plugins are recommended
// to use `PersistenPreRunE` to enable the error to be // to use `PersistentPreRunE` to enable the error to be
// returned. Should not be called outside of a command's // returned. Should not be called outside of a command's
// PersistentPreRunE hook and must not be run unless Run has been // PersistentPreRunE hook and must not be run unless Run has been
// called. // called.
@ -29,10 +33,21 @@ func RunPlugin(dockerCli *command.DockerCli, plugin *cobra.Command, meta manager
tcmd := newPluginCommand(dockerCli, plugin, meta) tcmd := newPluginCommand(dockerCli, plugin, meta)
var persistentPreRunOnce sync.Once var persistentPreRunOnce sync.Once
PersistentPreRunE = func(_ *cobra.Command, _ []string) error { PersistentPreRunE = func(cmd *cobra.Command, _ []string) error {
var err error var err error
persistentPreRunOnce.Do(func() { persistentPreRunOnce.Do(func() {
var opts []command.InitializeOpt 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)
var opts []command.CLIOption
if os.Getenv("DOCKER_CLI_PLUGIN_USE_DIAL_STDIO") != "" { if os.Getenv("DOCKER_CLI_PLUGIN_USE_DIAL_STDIO") != "" {
opts = append(opts, withPluginClientConn(plugin.Name())) opts = append(opts, withPluginClientConn(plugin.Name()))
} }
@ -53,6 +68,8 @@ func RunPlugin(dockerCli *command.DockerCli, plugin *cobra.Command, meta manager
// Run is the top-level entry point to the CLI plugin framework. It should be called from your plugin's `main()` function. // Run is the top-level entry point to the CLI plugin framework. It should be called from your plugin's `main()` function.
func Run(makeCmd func(command.Cli) *cobra.Command, meta manager.Metadata) { func Run(makeCmd func(command.Cli) *cobra.Command, meta manager.Metadata) {
otel.SetErrorHandler(debug.OTELErrorHandler)
dockerCli, err := command.NewDockerCli() dockerCli, err := command.NewDockerCli()
if err != nil { if err != nil {
fmt.Fprintln(os.Stderr, err) fmt.Fprintln(os.Stderr, err)
@ -78,7 +95,7 @@ func Run(makeCmd func(command.Cli) *cobra.Command, meta manager.Metadata) {
} }
} }
func withPluginClientConn(name string) command.InitializeOpt { func withPluginClientConn(name string) command.CLIOption {
return command.WithInitializeClient(func(dockerCli *command.DockerCli) (client.APIClient, error) { return command.WithInitializeClient(func(dockerCli *command.DockerCli) (client.APIClient, error) {
cmd := "docker" cmd := "docker"
if x := os.Getenv(manager.ReexecEnvvar); x != "" { if x := os.Getenv(manager.ReexecEnvvar); x != "" {

View File

@ -0,0 +1,158 @@
package socket
import (
"crypto/rand"
"encoding/hex"
"errors"
"io"
"net"
"os"
"runtime"
"sync"
)
// EnvKey represents the well-known environment variable used to pass the
// plugin being executed the socket name it should listen on to coordinate with
// the host CLI.
const EnvKey = "DOCKER_CLI_PLUGIN_SOCKET"
// NewPluginServer creates a plugin server that listens on a new Unix domain
// socket. h is called for each new connection to the socket in a goroutine.
func NewPluginServer(h func(net.Conn)) (*PluginServer, error) {
// Listen on a Unix socket, with the address being platform-dependent.
// When a non-abstract address is used, Go will unlink(2) the socket
// for us once the listener is closed, as documented in
// [net.UnixListener.SetUnlinkOnClose].
l, err := net.ListenUnix("unix", &net.UnixAddr{
Name: socketName("docker_cli_" + randomID()),
Net: "unix",
})
if err != nil {
return nil, err
}
if h == nil {
h = func(net.Conn) {}
}
pl := &PluginServer{
l: l,
h: h,
}
go func() {
defer pl.Close()
for {
err := pl.accept()
if err != nil {
return
}
}
}()
return pl, nil
}
type PluginServer struct {
mu sync.Mutex
conns []net.Conn
l *net.UnixListener
h func(net.Conn)
closed bool
}
func (pl *PluginServer) accept() error {
conn, err := pl.l.Accept()
if err != nil {
return err
}
pl.mu.Lock()
defer pl.mu.Unlock()
if pl.closed {
// Handle potential race between Close and accept.
conn.Close()
return errors.New("plugin server is closed")
}
pl.conns = append(pl.conns, conn)
go pl.h(conn)
return nil
}
// Addr returns the [net.Addr] of the underlying [net.Listener].
func (pl *PluginServer) Addr() net.Addr {
return pl.l.Addr()
}
// Close ensures that the server is no longer accepting new connections and
// closes all existing connections. Existing connections will receive [io.EOF].
//
// The error value is that of the underlying [net.Listner.Close] call.
func (pl *PluginServer) Close() error {
// Close connections first to ensure the connections get io.EOF instead
// of a connection reset.
pl.closeAllConns()
// Try to ensure that any active connections have a chance to receive
// io.EOF.
runtime.Gosched()
return pl.l.Close()
}
func (pl *PluginServer) closeAllConns() {
pl.mu.Lock()
defer pl.mu.Unlock()
// Prevent new connections from being accepted.
pl.closed = true
for _, conn := range pl.conns {
conn.Close()
}
pl.conns = nil
}
func randomID() string {
b := make([]byte, 16)
if _, err := rand.Read(b); err != nil {
panic(err) // This shouldn't happen
}
return hex.EncodeToString(b)
}
// ConnectAndWait connects to the socket passed via well-known env var,
// if present, and attempts to read from it until it receives an EOF, at which
// point cb is called.
func ConnectAndWait(cb func()) {
socketAddr, ok := os.LookupEnv(EnvKey)
if !ok {
// if a plugin compiled against a more recent version of docker/cli
// is executed by an older CLI binary, ignore missing environment
// variable and behave as usual
return
}
addr, err := net.ResolveUnixAddr("unix", socketAddr)
if err != nil {
return
}
conn, err := net.DialUnix("unix", nil, addr)
if err != nil {
return
}
go func() {
b := make([]byte, 1)
for {
_, err := conn.Read(b)
if errors.Is(err, io.EOF) {
cb()
return
}
}
}()
}

View File

@ -0,0 +1,9 @@
//go:build windows || linux
package socket
func socketName(basename string) string {
// Address of an abstract socket -- this socket can be opened by name,
// but is not present in the filesystem.
return "@" + basename
}

View File

@ -0,0 +1,14 @@
//go:build !windows && !linux
package socket
import (
"os"
"path/filepath"
)
func socketName(basename string) string {
// Because abstract sockets are unavailable, use a socket path in the
// system temporary directory.
return filepath.Join(os.TempDir(), basename)
}

View File

@ -0,0 +1,189 @@
package socket
import (
"errors"
"io"
"io/fs"
"net"
"os"
"runtime"
"strings"
"sync/atomic"
"testing"
"time"
"gotest.tools/v3/assert"
"gotest.tools/v3/poll"
)
func TestPluginServer(t *testing.T) {
t.Run("connection closes with EOF when server closes", func(t *testing.T) {
called := make(chan struct{})
srv, err := NewPluginServer(func(_ net.Conn) { close(called) })
assert.NilError(t, err)
assert.Assert(t, srv != nil, "returned nil server but no error")
addr, err := net.ResolveUnixAddr("unix", srv.Addr().String())
assert.NilError(t, err, "failed to resolve server address")
conn, err := net.DialUnix("unix", nil, addr)
assert.NilError(t, err, "failed to dial returned server")
defer conn.Close()
done := make(chan error, 1)
go func() {
_, err := conn.Read(make([]byte, 1))
done <- err
}()
select {
case <-called:
case <-time.After(10 * time.Millisecond):
t.Fatal("handler not called")
}
srv.Close()
select {
case err := <-done:
if !errors.Is(err, io.EOF) {
t.Fatalf("exepcted EOF error, got: %v", err)
}
case <-time.After(10 * time.Millisecond):
}
})
t.Run("allows reconnects", func(t *testing.T) {
var calls int32
h := func(_ net.Conn) {
atomic.AddInt32(&calls, 1)
}
srv, err := NewPluginServer(h)
assert.NilError(t, err)
defer srv.Close()
assert.Check(t, srv.Addr() != nil, "returned nil addr but no error")
addr, err := net.ResolveUnixAddr("unix", srv.Addr().String())
assert.NilError(t, err, "failed to resolve server address")
waitForCalls := func(n int) {
poll.WaitOn(t, func(t poll.LogT) poll.Result {
if atomic.LoadInt32(&calls) == int32(n) {
return poll.Success()
}
return poll.Continue("waiting for handler to be called")
})
}
otherConn, err := net.DialUnix("unix", nil, addr)
assert.NilError(t, err, "failed to dial returned server")
otherConn.Close()
waitForCalls(1)
conn, err := net.DialUnix("unix", nil, addr)
assert.NilError(t, err, "failed to redial server")
defer conn.Close()
waitForCalls(2)
// and again but don't close the existing connection
conn2, err := net.DialUnix("unix", nil, addr)
assert.NilError(t, err, "failed to redial server")
defer conn2.Close()
waitForCalls(3)
srv.Close()
// now make sure we get EOF on the existing connections
buf := make([]byte, 1)
_, err = conn.Read(buf)
assert.ErrorIs(t, err, io.EOF, "expected EOF error, got: %v", err)
_, err = conn2.Read(buf)
assert.ErrorIs(t, err, io.EOF, "expected EOF error, got: %v", err)
})
t.Run("does not leak sockets to local directory", func(t *testing.T) {
srv, err := NewPluginServer(nil)
assert.NilError(t, err)
assert.Check(t, srv != nil, "returned nil server but no error")
checkDirNoNewPluginServer(t)
addr, err := net.ResolveUnixAddr("unix", srv.Addr().String())
assert.NilError(t, err, "failed to resolve server address")
_, err = net.DialUnix("unix", nil, addr)
assert.NilError(t, err, "failed to dial returned server")
checkDirNoNewPluginServer(t)
})
}
func checkDirNoNewPluginServer(t *testing.T) {
t.Helper()
files, err := os.ReadDir(".")
assert.NilError(t, err, "failed to list files in dir to check for leaked sockets")
for _, f := range files {
info, err := f.Info()
assert.NilError(t, err, "failed to check file info")
// check for a socket with `docker_cli_` in the name (from `SetupConn()`)
if strings.Contains(f.Name(), "docker_cli_") && info.Mode().Type() == fs.ModeSocket {
t.Fatal("found socket in a local directory")
}
}
}
func TestConnectAndWait(t *testing.T) {
t.Run("calls cancel func on EOF", func(t *testing.T) {
srv, err := NewPluginServer(nil)
assert.NilError(t, err, "failed to setup server")
defer srv.Close()
done := make(chan struct{})
t.Setenv(EnvKey, srv.Addr().String())
cancelFunc := func() {
done <- struct{}{}
}
ConnectAndWait(cancelFunc)
select {
case <-done:
t.Fatal("unexpectedly done")
default:
}
srv.Close()
select {
case <-done:
case <-time.After(10 * time.Millisecond):
t.Fatal("cancel function not closed after 10ms")
}
})
// TODO: this test cannot be executed with `t.Parallel()`, due to
// relying on goroutine numbers to ensure correct behaviour
t.Run("connect goroutine exits after EOF", func(t *testing.T) {
srv, err := NewPluginServer(nil)
assert.NilError(t, err, "failed to setup server")
defer srv.Close()
t.Setenv(EnvKey, srv.Addr().String())
numGoroutines := runtime.NumGoroutine()
ConnectAndWait(func() {})
assert.Equal(t, runtime.NumGoroutine(), numGoroutines+1)
srv.Close()
poll.WaitOn(t, func(t poll.LogT) poll.Result {
if runtime.NumGoroutine() > numGoroutines+1 {
return poll.Continue("waiting for connect goroutine to exit")
}
return poll.Success()
}, poll.WithDelay(1*time.Millisecond), poll.WithTimeout(10*time.Millisecond))
})
}

View File

@ -176,7 +176,7 @@ func (tcmd *TopLevelCommand) HandleGlobalFlags() (*cobra.Command, []string, erro
} }
// Initialize finalises global option parsing and initializes the docker client. // Initialize finalises global option parsing and initializes the docker client.
func (tcmd *TopLevelCommand) Initialize(ops ...command.InitializeOpt) error { func (tcmd *TopLevelCommand) Initialize(ops ...command.CLIOption) error {
tcmd.opts.SetDefaultOptions(tcmd.flags) tcmd.opts.SetDefaultOptions(tcmd.flags)
return tcmd.dockerCli.Initialize(tcmd.opts, ops...) return tcmd.dockerCli.Initialize(tcmd.opts, ops...)
} }
@ -470,7 +470,7 @@ Common Commands:
Management Commands: Management Commands:
{{- range managementSubCommands . }} {{- range managementSubCommands . }}
{{rpad (decoratedName .) (add .NamePadding 1)}}{{.Short}}{{ if isPlugin .}} {{vendorAndVersion .}}{{ end}} {{rpad (decoratedName .) (add .NamePadding 1)}}{{.Short}}
{{- end}} {{- end}}
{{- end}} {{- end}}
@ -479,7 +479,7 @@ Management Commands:
Swarm Commands: Swarm Commands:
{{- range orchestratorSubCommands . }} {{- range orchestratorSubCommands . }}
{{rpad (decoratedName .) (add .NamePadding 1)}}{{.Short}}{{ if isPlugin .}} {{vendorAndVersion .}}{{ end}} {{rpad (decoratedName .) (add .NamePadding 1)}}{{.Short}}
{{- end}} {{- end}}
{{- end}} {{- end}}

View File

@ -0,0 +1,20 @@
package builder
import (
"context"
"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
)
type fakeClient struct {
client.Client
builderPruneFunc func(ctx context.Context, opts types.BuildCachePruneOptions) (*types.BuildCachePruneReport, error)
}
func (c *fakeClient) BuildCachePrune(ctx context.Context, opts types.BuildCachePruneOptions) (*types.BuildCachePruneReport, error) {
if c.builderPruneFunc != nil {
return c.builderPruneFunc(ctx, opts)
}
return nil, nil
}

View File

@ -2,6 +2,7 @@ package builder
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"strings" "strings"
@ -10,6 +11,7 @@ import (
"github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/command/completion"
"github.com/docker/cli/opts" "github.com/docker/cli/opts"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/errdefs"
units "github.com/docker/go-units" units "github.com/docker/go-units"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -30,7 +32,7 @@ func NewPruneCommand(dockerCli command.Cli) *cobra.Command {
Short: "Remove build cache", Short: "Remove build cache",
Args: cli.NoArgs, Args: cli.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
spaceReclaimed, output, err := runPrune(dockerCli, options) spaceReclaimed, output, err := runPrune(cmd.Context(), dockerCli, options)
if err != nil { if err != nil {
return err return err
} }
@ -58,7 +60,7 @@ const (
allCacheWarning = `WARNING! This will remove all build cache. Are you sure you want to continue?` allCacheWarning = `WARNING! This will remove all build cache. Are you sure you want to continue?`
) )
func runPrune(dockerCli command.Cli, options pruneOptions) (spaceReclaimed uint64, output string, err error) { func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions) (spaceReclaimed uint64, output string, err error) {
pruneFilters := options.filter.Value() pruneFilters := options.filter.Value()
pruneFilters = command.PruneFilters(dockerCli, pruneFilters) pruneFilters = command.PruneFilters(dockerCli, pruneFilters)
@ -66,11 +68,17 @@ func runPrune(dockerCli command.Cli, options pruneOptions) (spaceReclaimed uint6
if options.all { if options.all {
warning = allCacheWarning warning = allCacheWarning
} }
if !options.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), warning) { if !options.force {
return 0, "", nil r, err := command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), warning)
if err != nil {
return 0, "", err
}
if !r {
return 0, "", errdefs.Cancelled(errors.New("builder prune has been cancelled"))
}
} }
report, err := dockerCli.Client().BuildCachePrune(context.Background(), types.BuildCachePruneOptions{ report, err := dockerCli.Client().BuildCachePrune(ctx, types.BuildCachePruneOptions{
All: options.all, All: options.all,
KeepStorage: options.keepStorage.Value(), KeepStorage: options.keepStorage.Value(),
Filters: pruneFilters, Filters: pruneFilters,
@ -93,6 +101,6 @@ func runPrune(dockerCli command.Cli, options pruneOptions) (spaceReclaimed uint6
} }
// CachePrune executes a prune command for build cache // CachePrune executes a prune command for build cache
func CachePrune(dockerCli command.Cli, all bool, filter opts.FilterOpt) (uint64, string, error) { func CachePrune(ctx context.Context, dockerCli command.Cli, all bool, filter opts.FilterOpt) (uint64, string, error) {
return runPrune(dockerCli, pruneOptions{force: true, all: all, filter: filter}) return runPrune(ctx, dockerCli, pruneOptions{force: true, all: all, filter: filter})
} }

View File

@ -0,0 +1,23 @@
package builder
import (
"context"
"errors"
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/docker/api/types"
)
func TestBuilderPromptTermination(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
cli := test.NewFakeCli(&fakeClient{
builderPruneFunc: func(ctx context.Context, opts types.BuildCachePruneOptions) (*types.BuildCachePruneReport, error) {
return nil, errors.New("fakeClient builderPruneFunc should not be called")
},
})
cmd := NewPruneCommand(cli)
test.TerminatePrompt(ctx, t, cmd, cli)
}

View File

@ -3,34 +3,34 @@ package checkpoint
import ( import (
"context" "context"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types/checkpoint"
"github.com/docker/docker/client" "github.com/docker/docker/client"
) )
type fakeClient struct { type fakeClient struct {
client.Client client.Client
checkpointCreateFunc func(container string, options types.CheckpointCreateOptions) error checkpointCreateFunc func(container string, options checkpoint.CreateOptions) error
checkpointDeleteFunc func(container string, options types.CheckpointDeleteOptions) error checkpointDeleteFunc func(container string, options checkpoint.DeleteOptions) error
checkpointListFunc func(container string, options types.CheckpointListOptions) ([]types.Checkpoint, error) checkpointListFunc func(container string, options checkpoint.ListOptions) ([]checkpoint.Summary, error)
} }
func (cli *fakeClient) CheckpointCreate(_ context.Context, container string, options types.CheckpointCreateOptions) error { func (cli *fakeClient) CheckpointCreate(_ context.Context, container string, options checkpoint.CreateOptions) error {
if cli.checkpointCreateFunc != nil { if cli.checkpointCreateFunc != nil {
return cli.checkpointCreateFunc(container, options) return cli.checkpointCreateFunc(container, options)
} }
return nil return nil
} }
func (cli *fakeClient) CheckpointDelete(_ context.Context, container string, options types.CheckpointDeleteOptions) error { func (cli *fakeClient) CheckpointDelete(_ context.Context, container string, options checkpoint.DeleteOptions) error {
if cli.checkpointDeleteFunc != nil { if cli.checkpointDeleteFunc != nil {
return cli.checkpointDeleteFunc(container, options) return cli.checkpointDeleteFunc(container, options)
} }
return nil return nil
} }
func (cli *fakeClient) CheckpointList(_ context.Context, container string, options types.CheckpointListOptions) ([]types.Checkpoint, error) { func (cli *fakeClient) CheckpointList(_ context.Context, container string, options checkpoint.ListOptions) ([]checkpoint.Summary, error) {
if cli.checkpointListFunc != nil { if cli.checkpointListFunc != nil {
return cli.checkpointListFunc(container, options) return cli.checkpointListFunc(container, options)
} }
return []types.Checkpoint{}, nil return []checkpoint.Summary{}, nil
} }

View File

@ -7,7 +7,7 @@ import (
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/command/completion"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types/checkpoint"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -28,28 +28,24 @@ func newCreateCommand(dockerCli command.Cli) *cobra.Command {
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
opts.container = args[0] opts.container = args[0]
opts.checkpoint = args[1] opts.checkpoint = args[1]
return runCreate(dockerCli, opts) return runCreate(cmd.Context(), dockerCli, opts)
}, },
ValidArgsFunction: completion.NoComplete, ValidArgsFunction: completion.NoComplete,
} }
flags := cmd.Flags() flags := cmd.Flags()
flags.BoolVar(&opts.leaveRunning, "leave-running", false, "Leave the container running after checkpoint") flags.BoolVar(&opts.leaveRunning, "leave-running", false, "Leave the container running after checkpoint")
flags.StringVarP(&opts.checkpointDir, "checkpoint-dir", "", "", "Use a custom checkpoint storage directory") flags.StringVar(&opts.checkpointDir, "checkpoint-dir", "", "Use a custom checkpoint storage directory")
return cmd return cmd
} }
func runCreate(dockerCli command.Cli, opts createOptions) error { func runCreate(ctx context.Context, dockerCli command.Cli, opts createOptions) error {
client := dockerCli.Client() err := dockerCli.Client().CheckpointCreate(ctx, opts.container, checkpoint.CreateOptions{
checkpointOpts := types.CheckpointCreateOptions{
CheckpointID: opts.checkpoint, CheckpointID: opts.checkpoint,
CheckpointDir: opts.checkpointDir, CheckpointDir: opts.checkpointDir,
Exit: !opts.leaveRunning, Exit: !opts.leaveRunning,
} })
err := client.CheckpointCreate(context.Background(), opts.container, checkpointOpts)
if err != nil { if err != nil {
return err return err
} }

View File

@ -6,7 +6,7 @@ import (
"testing" "testing"
"github.com/docker/cli/internal/test" "github.com/docker/cli/internal/test"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types/checkpoint"
"github.com/pkg/errors" "github.com/pkg/errors"
"gotest.tools/v3/assert" "gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp" is "gotest.tools/v3/assert/cmp"
@ -15,7 +15,7 @@ import (
func TestCheckpointCreateErrors(t *testing.T) { func TestCheckpointCreateErrors(t *testing.T) {
testCases := []struct { testCases := []struct {
args []string args []string
checkpointCreateFunc func(container string, options types.CheckpointCreateOptions) error checkpointCreateFunc func(container string, options checkpoint.CreateOptions) error
expectedError string expectedError string
}{ }{
{ {
@ -28,7 +28,7 @@ func TestCheckpointCreateErrors(t *testing.T) {
}, },
{ {
args: []string{"foo", "bar"}, args: []string{"foo", "bar"},
checkpointCreateFunc: func(container string, options types.CheckpointCreateOptions) error { checkpointCreateFunc: func(container string, options checkpoint.CreateOptions) error {
return errors.Errorf("error creating checkpoint for container foo") return errors.Errorf("error creating checkpoint for container foo")
}, },
expectedError: "error creating checkpoint for container foo", expectedError: "error creating checkpoint for container foo",
@ -50,7 +50,7 @@ func TestCheckpointCreateWithOptions(t *testing.T) {
var containerID, checkpointID, checkpointDir string var containerID, checkpointID, checkpointDir string
var exit bool var exit bool
cli := test.NewFakeCli(&fakeClient{ cli := test.NewFakeCli(&fakeClient{
checkpointCreateFunc: func(container string, options types.CheckpointCreateOptions) error { checkpointCreateFunc: func(container string, options checkpoint.CreateOptions) error {
containerID = container containerID = container
checkpointID = options.CheckpointID checkpointID = options.CheckpointID
checkpointDir = options.CheckpointDir checkpointDir = options.CheckpointDir
@ -59,14 +59,14 @@ func TestCheckpointCreateWithOptions(t *testing.T) {
}, },
}) })
cmd := newCreateCommand(cli) cmd := newCreateCommand(cli)
checkpoint := "checkpoint-bar" cp := "checkpoint-bar"
cmd.SetArgs([]string{"container-foo", checkpoint}) cmd.SetArgs([]string{"container-foo", cp})
cmd.Flags().Set("leave-running", "true") cmd.Flags().Set("leave-running", "true")
cmd.Flags().Set("checkpoint-dir", "/dir/foo") cmd.Flags().Set("checkpoint-dir", "/dir/foo")
assert.NilError(t, cmd.Execute()) assert.NilError(t, cmd.Execute())
assert.Check(t, is.Equal("container-foo", containerID)) assert.Check(t, is.Equal("container-foo", containerID))
assert.Check(t, is.Equal(checkpoint, checkpointID)) assert.Check(t, is.Equal(cp, checkpointID))
assert.Check(t, is.Equal("/dir/foo", checkpointDir)) assert.Check(t, is.Equal("/dir/foo", checkpointDir))
assert.Check(t, is.Equal(false, exit)) assert.Check(t, is.Equal(false, exit))
assert.Check(t, is.Equal(checkpoint, strings.TrimSpace(cli.OutBuffer().String()))) assert.Check(t, is.Equal(cp, strings.TrimSpace(cli.OutBuffer().String())))
} }

View File

@ -2,29 +2,27 @@ package checkpoint
import ( import (
"github.com/docker/cli/cli/command/formatter" "github.com/docker/cli/cli/command/formatter"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types/checkpoint"
) )
const ( const (
defaultCheckpointFormat = "table {{.Name}}" defaultCheckpointFormat = "table {{.Name}}"
checkpointNameHeader = "CHECKPOINT NAME" checkpointNameHeader = "CHECKPOINT NAME"
) )
// NewFormat returns a format for use with a checkpoint Context // NewFormat returns a format for use with a checkpoint Context
func NewFormat(source string) formatter.Format { func NewFormat(source string) formatter.Format {
switch source { if source == formatter.TableFormatKey {
case formatter.TableFormatKey:
return defaultCheckpointFormat return defaultCheckpointFormat
} }
return formatter.Format(source) return formatter.Format(source)
} }
// FormatWrite writes formatted checkpoints using the Context // FormatWrite writes formatted checkpoints using the Context
func FormatWrite(ctx formatter.Context, checkpoints []types.Checkpoint) error { func FormatWrite(ctx formatter.Context, checkpoints []checkpoint.Summary) error {
render := func(format func(subContext formatter.SubContext) error) error { render := func(format func(subContext formatter.SubContext) error) error {
for _, checkpoint := range checkpoints { for _, cp := range checkpoints {
if err := format(&checkpointContext{c: checkpoint}); err != nil { if err := format(&checkpointContext{c: cp}); err != nil {
return err return err
} }
} }
@ -35,7 +33,7 @@ func FormatWrite(ctx formatter.Context, checkpoints []types.Checkpoint) error {
type checkpointContext struct { type checkpointContext struct {
formatter.HeaderContext formatter.HeaderContext
c types.Checkpoint c checkpoint.Summary
} }
func newCheckpointContext() *checkpointContext { func newCheckpointContext() *checkpointContext {

View File

@ -5,7 +5,7 @@ import (
"testing" "testing"
"github.com/docker/cli/cli/command/formatter" "github.com/docker/cli/cli/command/formatter"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types/checkpoint"
"gotest.tools/v3/assert" "gotest.tools/v3/assert"
) )
@ -38,15 +38,14 @@ checkpoint-3:
}, },
} }
checkpoints := []types.Checkpoint{
{Name: "checkpoint-1"},
{Name: "checkpoint-2"},
{Name: "checkpoint-3"},
}
for _, testcase := range cases { for _, testcase := range cases {
out := bytes.NewBufferString("") out := bytes.NewBufferString("")
testcase.context.Output = out testcase.context.Output = out
err := FormatWrite(testcase.context, checkpoints) err := FormatWrite(testcase.context, []checkpoint.Summary{
{Name: "checkpoint-1"},
{Name: "checkpoint-2"},
{Name: "checkpoint-3"},
})
assert.NilError(t, err) assert.NilError(t, err)
assert.Equal(t, out.String(), testcase.expected) assert.Equal(t, out.String(), testcase.expected)
} }

View File

@ -7,7 +7,7 @@ import (
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/command/completion"
"github.com/docker/cli/cli/command/formatter" "github.com/docker/cli/cli/command/formatter"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types/checkpoint"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -24,25 +24,21 @@ func newListCommand(dockerCli command.Cli) *cobra.Command {
Short: "List checkpoints for a container", Short: "List checkpoints for a container",
Args: cli.ExactArgs(1), Args: cli.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runList(dockerCli, args[0], opts) return runList(cmd.Context(), dockerCli, args[0], opts)
}, },
ValidArgsFunction: completion.ContainerNames(dockerCli, false), ValidArgsFunction: completion.ContainerNames(dockerCli, false),
} }
flags := cmd.Flags() flags := cmd.Flags()
flags.StringVarP(&opts.checkpointDir, "checkpoint-dir", "", "", "Use a custom checkpoint storage directory") flags.StringVar(&opts.checkpointDir, "checkpoint-dir", "", "Use a custom checkpoint storage directory")
return cmd return cmd
} }
func runList(dockerCli command.Cli, container string, opts listOptions) error { func runList(ctx context.Context, dockerCli command.Cli, container string, opts listOptions) error {
client := dockerCli.Client() checkpoints, err := dockerCli.Client().CheckpointList(ctx, container, checkpoint.ListOptions{
listOpts := types.CheckpointListOptions{
CheckpointDir: opts.checkpointDir, CheckpointDir: opts.checkpointDir,
} })
checkpoints, err := client.CheckpointList(context.Background(), container, listOpts)
if err != nil { if err != nil {
return err return err
} }

View File

@ -5,7 +5,7 @@ import (
"testing" "testing"
"github.com/docker/cli/internal/test" "github.com/docker/cli/internal/test"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types/checkpoint"
"github.com/pkg/errors" "github.com/pkg/errors"
"gotest.tools/v3/assert" "gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp" is "gotest.tools/v3/assert/cmp"
@ -15,7 +15,7 @@ import (
func TestCheckpointListErrors(t *testing.T) { func TestCheckpointListErrors(t *testing.T) {
testCases := []struct { testCases := []struct {
args []string args []string
checkpointListFunc func(container string, options types.CheckpointListOptions) ([]types.Checkpoint, error) checkpointListFunc func(container string, options checkpoint.ListOptions) ([]checkpoint.Summary, error)
expectedError string expectedError string
}{ }{
{ {
@ -28,8 +28,8 @@ func TestCheckpointListErrors(t *testing.T) {
}, },
{ {
args: []string{"foo"}, args: []string{"foo"},
checkpointListFunc: func(container string, options types.CheckpointListOptions) ([]types.Checkpoint, error) { checkpointListFunc: func(container string, options checkpoint.ListOptions) ([]checkpoint.Summary, error) {
return []types.Checkpoint{}, errors.Errorf("error getting checkpoints for container foo") return []checkpoint.Summary{}, errors.Errorf("error getting checkpoints for container foo")
}, },
expectedError: "error getting checkpoints for container foo", expectedError: "error getting checkpoints for container foo",
}, },
@ -49,10 +49,10 @@ func TestCheckpointListErrors(t *testing.T) {
func TestCheckpointListWithOptions(t *testing.T) { func TestCheckpointListWithOptions(t *testing.T) {
var containerID, checkpointDir string var containerID, checkpointDir string
cli := test.NewFakeCli(&fakeClient{ cli := test.NewFakeCli(&fakeClient{
checkpointListFunc: func(container string, options types.CheckpointListOptions) ([]types.Checkpoint, error) { checkpointListFunc: func(container string, options checkpoint.ListOptions) ([]checkpoint.Summary, error) {
containerID = container containerID = container
checkpointDir = options.CheckpointDir checkpointDir = options.CheckpointDir
return []types.Checkpoint{ return []checkpoint.Summary{
{Name: "checkpoint-foo"}, {Name: "checkpoint-foo"},
}, nil }, nil
}, },

View File

@ -5,7 +5,7 @@ import (
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types/checkpoint"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -22,23 +22,19 @@ func newRemoveCommand(dockerCli command.Cli) *cobra.Command {
Short: "Remove a checkpoint", Short: "Remove a checkpoint",
Args: cli.ExactArgs(2), Args: cli.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runRemove(dockerCli, args[0], args[1], opts) return runRemove(cmd.Context(), dockerCli, args[0], args[1], opts)
}, },
} }
flags := cmd.Flags() flags := cmd.Flags()
flags.StringVarP(&opts.checkpointDir, "checkpoint-dir", "", "", "Use a custom checkpoint storage directory") flags.StringVar(&opts.checkpointDir, "checkpoint-dir", "", "Use a custom checkpoint storage directory")
return cmd return cmd
} }
func runRemove(dockerCli command.Cli, container string, checkpoint string, opts removeOptions) error { func runRemove(ctx context.Context, dockerCli command.Cli, container string, checkpointID string, opts removeOptions) error {
client := dockerCli.Client() return dockerCli.Client().CheckpointDelete(ctx, container, checkpoint.DeleteOptions{
CheckpointID: checkpointID,
removeOpts := types.CheckpointDeleteOptions{
CheckpointID: checkpoint,
CheckpointDir: opts.checkpointDir, CheckpointDir: opts.checkpointDir,
} })
return client.CheckpointDelete(context.Background(), container, removeOpts)
} }

View File

@ -5,7 +5,7 @@ import (
"testing" "testing"
"github.com/docker/cli/internal/test" "github.com/docker/cli/internal/test"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types/checkpoint"
"github.com/pkg/errors" "github.com/pkg/errors"
"gotest.tools/v3/assert" "gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp" is "gotest.tools/v3/assert/cmp"
@ -14,7 +14,7 @@ import (
func TestCheckpointRemoveErrors(t *testing.T) { func TestCheckpointRemoveErrors(t *testing.T) {
testCases := []struct { testCases := []struct {
args []string args []string
checkpointDeleteFunc func(container string, options types.CheckpointDeleteOptions) error checkpointDeleteFunc func(container string, options checkpoint.DeleteOptions) error
expectedError string expectedError string
}{ }{
{ {
@ -27,7 +27,7 @@ func TestCheckpointRemoveErrors(t *testing.T) {
}, },
{ {
args: []string{"foo", "bar"}, args: []string{"foo", "bar"},
checkpointDeleteFunc: func(container string, options types.CheckpointDeleteOptions) error { checkpointDeleteFunc: func(container string, options checkpoint.DeleteOptions) error {
return errors.Errorf("error deleting checkpoint") return errors.Errorf("error deleting checkpoint")
}, },
expectedError: "error deleting checkpoint", expectedError: "error deleting checkpoint",
@ -48,7 +48,7 @@ func TestCheckpointRemoveErrors(t *testing.T) {
func TestCheckpointRemoveWithOptions(t *testing.T) { func TestCheckpointRemoveWithOptions(t *testing.T) {
var containerID, checkpointID, checkpointDir string var containerID, checkpointID, checkpointDir string
cli := test.NewFakeCli(&fakeClient{ cli := test.NewFakeCli(&fakeClient{
checkpointDeleteFunc: func(container string, options types.CheckpointDeleteOptions) error { checkpointDeleteFunc: func(container string, options checkpoint.DeleteOptions) error {
containerID = container containerID = container
checkpointID = options.CheckpointID checkpointID = options.CheckpointID
checkpointDir = options.CheckpointDir checkpointDir = options.CheckpointDir

View File

@ -1,3 +1,6 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.19
package command package command
import ( import (
@ -49,7 +52,7 @@ type Cli interface {
Client() client.APIClient Client() client.APIClient
Streams Streams
SetIn(in *streams.In) SetIn(in *streams.In)
Apply(ops ...DockerCliOption) error Apply(ops ...CLIOption) error
ConfigFile() *configfile.ConfigFile ConfigFile() *configfile.ConfigFile
ServerInfo() ServerInfo ServerInfo() ServerInfo
NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (notaryclient.Repository, error) NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (notaryclient.Repository, error)
@ -62,6 +65,7 @@ type Cli interface {
ContextStore() store.Store ContextStore() store.Store
CurrentContext() string CurrentContext() string
DockerEndpoint() docker.Endpoint DockerEndpoint() docker.Endpoint
TelemetryClient
} }
// DockerCli is an instance the docker command line client. // DockerCli is an instance the docker command line client.
@ -82,6 +86,12 @@ type DockerCli struct {
dockerEndpoint docker.Endpoint dockerEndpoint docker.Endpoint
contextStoreConfig store.Config contextStoreConfig store.Config
initTimeout time.Duration initTimeout time.Duration
res telemetryResource
// baseCtx is the base context used for internal operations. In the future
// this may be replaced by explicitly passing a context to functions that
// need it.
baseCtx context.Context
} }
// DefaultVersion returns api.defaultVersion. // DefaultVersion returns api.defaultVersion.
@ -179,6 +189,36 @@ func (cli *DockerCli) BuildKitEnabled() (bool, error) {
return cli.ServerInfo().OSType != "windows", nil return cli.ServerInfo().OSType != "windows", nil
} }
// HooksEnabled returns whether plugin hooks are enabled.
func (cli *DockerCli) HooksEnabled() bool {
// legacy support DOCKER_CLI_HINTS env var
if v := os.Getenv("DOCKER_CLI_HINTS"); v != "" {
enabled, err := strconv.ParseBool(v)
if err != nil {
return false
}
return enabled
}
// use DOCKER_CLI_HOOKS env var value if set and not empty
if v := os.Getenv("DOCKER_CLI_HOOKS"); v != "" {
enabled, err := strconv.ParseBool(v)
if err != nil {
return false
}
return enabled
}
featuresMap := cli.ConfigFile().Features
if v, ok := featuresMap["hooks"]; ok {
enabled, err := strconv.ParseBool(v)
if err != nil {
return false
}
return enabled
}
// default to false
return false
}
// ManifestStore returns a store for local manifests // ManifestStore returns a store for local manifests
func (cli *DockerCli) ManifestStore() manifeststore.Store { func (cli *DockerCli) ManifestStore() manifeststore.Store {
// TODO: support override default location from config file // TODO: support override default location from config file
@ -189,16 +229,13 @@ func (cli *DockerCli) ManifestStore() manifeststore.Store {
// registry // registry
func (cli *DockerCli) RegistryClient(allowInsecure bool) registryclient.RegistryClient { func (cli *DockerCli) RegistryClient(allowInsecure bool) registryclient.RegistryClient {
resolver := func(ctx context.Context, index *registry.IndexInfo) registry.AuthConfig { resolver := func(ctx context.Context, index *registry.IndexInfo) registry.AuthConfig {
return ResolveAuthConfig(ctx, cli, index) return ResolveAuthConfig(cli.ConfigFile(), index)
} }
return registryclient.NewRegistryClient(resolver, UserAgent(), allowInsecure) return registryclient.NewRegistryClient(resolver, UserAgent(), allowInsecure)
} }
// InitializeOpt is the type of the functional options passed to DockerCli.Initialize
type InitializeOpt func(dockerCli *DockerCli) error
// WithInitializeClient is passed to DockerCli.Initialize by callers who wish to set a particular API Client for use by the CLI. // WithInitializeClient is passed to DockerCli.Initialize by callers who wish to set a particular API Client for use by the CLI.
func WithInitializeClient(makeClient func(dockerCli *DockerCli) (client.APIClient, error)) InitializeOpt { func WithInitializeClient(makeClient func(dockerCli *DockerCli) (client.APIClient, error)) CLIOption {
return func(dockerCli *DockerCli) error { return func(dockerCli *DockerCli) error {
var err error var err error
dockerCli.client, err = makeClient(dockerCli) dockerCli.client, err = makeClient(dockerCli)
@ -208,7 +245,7 @@ func WithInitializeClient(makeClient func(dockerCli *DockerCli) (client.APIClien
// Initialize the dockerCli runs initialization that must happen after command // Initialize the dockerCli runs initialization that must happen after command
// line flags are parsed. // line flags are parsed.
func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...InitializeOpt) error { func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...CLIOption) error {
for _, o := range ops { for _, o := range ops {
if err := o(cli); err != nil { if err := o(cli); err != nil {
return err return err
@ -323,8 +360,7 @@ func (cli *DockerCli) getInitTimeout() time.Duration {
} }
func (cli *DockerCli) initializeFromClient() { func (cli *DockerCli) initializeFromClient() {
ctx := context.Background() ctx, cancel := context.WithTimeout(cli.baseCtx, cli.getInitTimeout())
ctx, cancel := context.WithTimeout(ctx, cli.getInitTimeout())
defer cancel() defer cancel()
ping, err := cli.client.Ping(ctx) ping, err := cli.client.Ping(ctx)
@ -394,7 +430,7 @@ func (cli *DockerCli) CurrentContext() string {
// occur when trying to use it. // occur when trying to use it.
// //
// Refer to [DockerCli.CurrentContext] above for further details. // Refer to [DockerCli.CurrentContext] above for further details.
func resolveContextName(opts *cliflags.ClientOptions, config *configfile.ConfigFile) string { func resolveContextName(opts *cliflags.ClientOptions, cfg *configfile.ConfigFile) string {
if opts != nil && opts.Context != "" { if opts != nil && opts.Context != "" {
return opts.Context return opts.Context
} }
@ -407,9 +443,9 @@ func resolveContextName(opts *cliflags.ClientOptions, config *configfile.ConfigF
if ctxName := os.Getenv(EnvOverrideContext); ctxName != "" { if ctxName := os.Getenv(EnvOverrideContext); ctxName != "" {
return ctxName return ctxName
} }
if config != nil && config.CurrentContext != "" { if cfg != nil && cfg.CurrentContext != "" {
// We don't validate if this context exists: errors may occur when trying to use it. // We don't validate if this context exists: errors may occur when trying to use it.
return config.CurrentContext return cfg.CurrentContext
} }
return DefaultContextName return DefaultContextName
} }
@ -444,13 +480,16 @@ func (cli *DockerCli) initialize() error {
return return
} }
} }
if cli.baseCtx == nil {
cli.baseCtx = context.Background()
}
cli.initializeFromClient() cli.initializeFromClient()
}) })
return cli.initErr return cli.initErr
} }
// Apply all the operation on the cli // Apply all the operation on the cli
func (cli *DockerCli) Apply(ops ...DockerCliOption) error { func (cli *DockerCli) Apply(ops ...CLIOption) error {
for _, op := range ops { for _, op := range ops {
if err := op(cli); err != nil { if err := op(cli); err != nil {
return err return err
@ -479,15 +518,15 @@ type ServerInfo struct {
// NewDockerCli returns a DockerCli instance with all operators applied on it. // NewDockerCli returns a DockerCli instance with all operators applied on it.
// It applies by default the standard streams, and the content trust from // It applies by default the standard streams, and the content trust from
// environment. // environment.
func NewDockerCli(ops ...DockerCliOption) (*DockerCli, error) { func NewDockerCli(ops ...CLIOption) (*DockerCli, error) {
defaultOps := []DockerCliOption{ defaultOps := []CLIOption{
WithContentTrustFromEnv(), WithContentTrustFromEnv(),
WithDefaultContextStoreConfig(), WithDefaultContextStoreConfig(),
WithStandardStreams(), WithStandardStreams(),
} }
ops = append(defaultOps, ops...) ops = append(defaultOps, ops...)
cli := &DockerCli{} cli := &DockerCli{baseCtx: context.Background()}
if err := cli.Apply(ops...); err != nil { if err := cli.Apply(ops...); err != nil {
return nil, err return nil, err
} }
@ -514,7 +553,7 @@ func UserAgent() string {
} }
var defaultStoreEndpoints = []store.NamedTypeGetter{ var defaultStoreEndpoints = []store.NamedTypeGetter{
store.EndpointTypeGetter(docker.DockerEndpoint, func() interface{} { return &docker.EndpointMeta{} }), store.EndpointTypeGetter(docker.DockerEndpoint, func() any { return &docker.EndpointMeta{} }),
} }
// RegisterDefaultStoreEndpoints registers a new named endpoint // RegisterDefaultStoreEndpoints registers a new named endpoint
@ -528,7 +567,7 @@ func RegisterDefaultStoreEndpoints(ep ...store.NamedTypeGetter) {
// DefaultContextStoreConfig returns a new store.Config with the default set of endpoints configured. // DefaultContextStoreConfig returns a new store.Config with the default set of endpoints configured.
func DefaultContextStoreConfig() store.Config { func DefaultContextStoreConfig() store.Config {
return store.NewConfig( return store.NewConfig(
func() interface{} { return &DockerContext{} }, func() any { return &DockerContext{} },
defaultStoreEndpoints..., defaultStoreEndpoints...,
) )
} }

View File

@ -1,6 +1,7 @@
package command package command
import ( import (
"context"
"io" "io"
"os" "os"
"strconv" "strconv"
@ -10,11 +11,13 @@ import (
"github.com/moby/term" "github.com/moby/term"
) )
// DockerCliOption applies a modification on a DockerCli. // CLIOption is a functional argument to apply options to a [DockerCli]. These
type DockerCliOption func(cli *DockerCli) error // options can be passed to [NewDockerCli] to initialize a new CLI, or
// applied with [DockerCli.Initialize] or [DockerCli.Apply].
type CLIOption func(cli *DockerCli) error
// WithStandardStreams sets a cli in, out and err streams with the standard streams. // WithStandardStreams sets a cli in, out and err streams with the standard streams.
func WithStandardStreams() DockerCliOption { func WithStandardStreams() CLIOption {
return func(cli *DockerCli) error { return func(cli *DockerCli) error {
// Set terminal emulation based on platform as required. // Set terminal emulation based on platform as required.
stdin, stdout, stderr := term.StdStreams() stdin, stdout, stderr := term.StdStreams()
@ -25,8 +28,17 @@ func WithStandardStreams() DockerCliOption {
} }
} }
// WithBaseContext sets the base context of a cli. It is used to propagate
// the context from the command line to the client.
func WithBaseContext(ctx context.Context) CLIOption {
return func(cli *DockerCli) error {
cli.baseCtx = ctx
return nil
}
}
// WithCombinedStreams uses the same stream for the output and error streams. // WithCombinedStreams uses the same stream for the output and error streams.
func WithCombinedStreams(combined io.Writer) DockerCliOption { func WithCombinedStreams(combined io.Writer) CLIOption {
return func(cli *DockerCli) error { return func(cli *DockerCli) error {
cli.out = streams.NewOut(combined) cli.out = streams.NewOut(combined)
cli.err = combined cli.err = combined
@ -35,7 +47,7 @@ func WithCombinedStreams(combined io.Writer) DockerCliOption {
} }
// WithInputStream sets a cli input stream. // WithInputStream sets a cli input stream.
func WithInputStream(in io.ReadCloser) DockerCliOption { func WithInputStream(in io.ReadCloser) CLIOption {
return func(cli *DockerCli) error { return func(cli *DockerCli) error {
cli.in = streams.NewIn(in) cli.in = streams.NewIn(in)
return nil return nil
@ -43,7 +55,7 @@ func WithInputStream(in io.ReadCloser) DockerCliOption {
} }
// WithOutputStream sets a cli output stream. // WithOutputStream sets a cli output stream.
func WithOutputStream(out io.Writer) DockerCliOption { func WithOutputStream(out io.Writer) CLIOption {
return func(cli *DockerCli) error { return func(cli *DockerCli) error {
cli.out = streams.NewOut(out) cli.out = streams.NewOut(out)
return nil return nil
@ -51,7 +63,7 @@ func WithOutputStream(out io.Writer) DockerCliOption {
} }
// WithErrorStream sets a cli error stream. // WithErrorStream sets a cli error stream.
func WithErrorStream(err io.Writer) DockerCliOption { func WithErrorStream(err io.Writer) CLIOption {
return func(cli *DockerCli) error { return func(cli *DockerCli) error {
cli.err = err cli.err = err
return nil return nil
@ -59,7 +71,7 @@ func WithErrorStream(err io.Writer) DockerCliOption {
} }
// WithContentTrustFromEnv enables content trust on a cli from environment variable DOCKER_CONTENT_TRUST value. // WithContentTrustFromEnv enables content trust on a cli from environment variable DOCKER_CONTENT_TRUST value.
func WithContentTrustFromEnv() DockerCliOption { func WithContentTrustFromEnv() CLIOption {
return func(cli *DockerCli) error { return func(cli *DockerCli) error {
cli.contentTrust = false cli.contentTrust = false
if e := os.Getenv("DOCKER_CONTENT_TRUST"); e != "" { if e := os.Getenv("DOCKER_CONTENT_TRUST"); e != "" {
@ -73,7 +85,7 @@ func WithContentTrustFromEnv() DockerCliOption {
} }
// WithContentTrust enables content trust on a cli. // WithContentTrust enables content trust on a cli.
func WithContentTrust(enabled bool) DockerCliOption { func WithContentTrust(enabled bool) CLIOption {
return func(cli *DockerCli) error { return func(cli *DockerCli) error {
cli.contentTrust = enabled cli.contentTrust = enabled
return nil return nil
@ -81,7 +93,7 @@ func WithContentTrust(enabled bool) DockerCliOption {
} }
// WithDefaultContextStoreConfig configures the cli to use the default context store configuration. // WithDefaultContextStoreConfig configures the cli to use the default context store configuration.
func WithDefaultContextStoreConfig() DockerCliOption { func WithDefaultContextStoreConfig() CLIOption {
return func(cli *DockerCli) error { return func(cli *DockerCli) error {
cli.contextStoreConfig = DefaultContextStoreConfig() cli.contextStoreConfig = DefaultContextStoreConfig()
return nil return nil
@ -89,7 +101,7 @@ func WithDefaultContextStoreConfig() DockerCliOption {
} }
// WithAPIClient configures the cli to use the given API client. // WithAPIClient configures the cli to use the given API client.
func WithAPIClient(c client.APIClient) DockerCliOption { func WithAPIClient(c client.APIClient) CLIOption {
return func(cli *DockerCli) error { return func(cli *DockerCli) error {
cli.client = c cli.client = c
return nil return nil

View File

@ -8,6 +8,7 @@ import (
) )
func contentTrustEnabled(t *testing.T) bool { func contentTrustEnabled(t *testing.T) bool {
t.Helper()
var cli DockerCli var cli DockerCli
assert.NilError(t, WithContentTrustFromEnv()(&cli)) assert.NilError(t, WithContentTrustFromEnv()(&cli))
return cli.contentTrust return cli.contentTrust

View File

@ -307,3 +307,56 @@ func TestInitializeShouldAlwaysCreateTheContextStore(t *testing.T) {
}))) })))
assert.Check(t, cli.ContextStore() != nil) assert.Check(t, cli.ContextStore() != nil)
} }
func TestHooksEnabled(t *testing.T) {
t.Run("disabled by default", func(t *testing.T) {
cli, err := NewDockerCli()
assert.NilError(t, err)
assert.Check(t, !cli.HooksEnabled())
})
t.Run("enabled in configFile", func(t *testing.T) {
configFile := `{
"features": {
"hooks": "true"
}}`
dir := fs.NewDir(t, "", fs.WithFile("config.json", configFile))
defer dir.Remove()
cli, err := NewDockerCli()
assert.NilError(t, err)
config.SetDir(dir.Path())
assert.Check(t, cli.HooksEnabled())
})
t.Run("env var overrides configFile", func(t *testing.T) {
configFile := `{
"features": {
"hooks": "true"
}}`
t.Setenv("DOCKER_CLI_HOOKS", "false")
dir := fs.NewDir(t, "", fs.WithFile("config.json", configFile))
defer dir.Remove()
cli, err := NewDockerCli()
assert.NilError(t, err)
config.SetDir(dir.Path())
assert.Check(t, !cli.HooksEnabled())
})
t.Run("legacy env var overrides configFile", func(t *testing.T) {
configFile := `{
"features": {
"hooks": "true"
}}`
t.Setenv("DOCKER_CLI_HINTS", "false")
dir := fs.NewDir(t, "", fs.WithFile("config.json", configFile))
defer dir.Remove()
cli, err := NewDockerCli()
assert.NilError(t, err)
config.SetDir(dir.Path())
assert.Check(t, !cli.HooksEnabled())
})
}

View File

@ -6,6 +6,8 @@ import (
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/formatter" "github.com/docker/cli/cli/command/formatter"
"github.com/docker/docker/api/types" "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/volume" "github.com/docker/docker/api/types/volume"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -16,7 +18,7 @@ type ValidArgsFn func(cmd *cobra.Command, args []string, toComplete string) ([]s
// ImageNames offers completion for images present within the local store // ImageNames offers completion for images present within the local store
func ImageNames(dockerCli command.Cli) ValidArgsFn { func ImageNames(dockerCli command.Cli) ValidArgsFn {
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
list, err := dockerCli.Client().ImageList(cmd.Context(), types.ImageListOptions{}) list, err := dockerCli.Client().ImageList(cmd.Context(), image.ListOptions{})
if err != nil { if err != nil {
return nil, cobra.ShellCompDirectiveError return nil, cobra.ShellCompDirectiveError
} }
@ -33,7 +35,7 @@ func ImageNames(dockerCli command.Cli) ValidArgsFn {
// Set DOCKER_COMPLETION_SHOW_CONTAINER_IDS=yes to also complete IDs. // Set DOCKER_COMPLETION_SHOW_CONTAINER_IDS=yes to also complete IDs.
func ContainerNames(dockerCli command.Cli, 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) { return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
list, err := dockerCli.Client().ContainerList(cmd.Context(), types.ContainerListOptions{ list, err := dockerCli.Client().ContainerList(cmd.Context(), container.ListOptions{
All: all, All: all,
}) })
if err != nil { if err != nil {

View File

@ -35,7 +35,7 @@ func newConfigCreateCommand(dockerCli command.Cli) *cobra.Command {
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
createOpts.Name = args[0] createOpts.Name = args[0]
createOpts.File = args[1] createOpts.File = args[1]
return RunConfigCreate(dockerCli, createOpts) return RunConfigCreate(cmd.Context(), dockerCli, createOpts)
}, },
ValidArgsFunction: completion.NoComplete, ValidArgsFunction: completion.NoComplete,
} }
@ -48,9 +48,8 @@ func newConfigCreateCommand(dockerCli command.Cli) *cobra.Command {
} }
// RunConfigCreate creates a config with the given options. // RunConfigCreate creates a config with the given options.
func RunConfigCreate(dockerCli command.Cli, options CreateOptions) error { func RunConfigCreate(ctx context.Context, dockerCli command.Cli, options CreateOptions) error {
client := dockerCli.Client() client := dockerCli.Client()
ctx := context.Background()
var in io.Reader = dockerCli.In() var in io.Reader = dockerCli.In()
if options.File != "-" { if options.File != "-" {

View File

@ -101,7 +101,7 @@ func (c *configContext) Labels() string {
if mapLabels == nil { if mapLabels == nil {
return "" return ""
} }
var joinLabels []string joinLabels := make([]string, 0, len(mapLabels))
for k, v := range mapLabels { for k, v := range mapLabels {
joinLabels = append(joinLabels, k+"="+v) joinLabels = append(joinLabels, k+"="+v)
} }

View File

@ -1,3 +1,6 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.19
package config package config
import ( import (
@ -27,7 +30,7 @@ func newConfigInspectCommand(dockerCli command.Cli) *cobra.Command {
Args: cli.RequiresMinArgs(1), Args: cli.RequiresMinArgs(1),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
opts.Names = args opts.Names = args
return RunConfigInspect(dockerCli, opts) return RunConfigInspect(cmd.Context(), dockerCli, opts)
}, },
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return completeNames(dockerCli)(cmd, args, toComplete) return completeNames(dockerCli)(cmd, args, toComplete)
@ -40,15 +43,14 @@ func newConfigInspectCommand(dockerCli command.Cli) *cobra.Command {
} }
// RunConfigInspect inspects the given Swarm config. // RunConfigInspect inspects the given Swarm config.
func RunConfigInspect(dockerCli command.Cli, opts InspectOptions) error { func RunConfigInspect(ctx context.Context, dockerCli command.Cli, opts InspectOptions) error {
client := dockerCli.Client() client := dockerCli.Client()
ctx := context.Background()
if opts.Pretty { if opts.Pretty {
opts.Format = "pretty" opts.Format = "pretty"
} }
getRef := func(id string) (interface{}, []byte, error) { getRef := func(id string) (any, []byte, error) {
return client.ConfigInspectWithRaw(ctx, id) return client.ConfigInspectWithRaw(ctx, id)
} }
f := opts.Format f := opts.Format

View File

@ -8,7 +8,7 @@ import (
"time" "time"
"github.com/docker/cli/internal/test" "github.com/docker/cli/internal/test"
. "github.com/docker/cli/internal/test/builders" // Import builders to get the builder function as package function "github.com/docker/cli/internal/test/builders"
"github.com/docker/docker/api/types/swarm" "github.com/docker/docker/api/types/swarm"
"github.com/pkg/errors" "github.com/pkg/errors"
"gotest.tools/v3/assert" "gotest.tools/v3/assert"
@ -43,7 +43,7 @@ func TestConfigInspectErrors(t *testing.T) {
args: []string{"foo", "bar"}, args: []string{"foo", "bar"},
configInspectFunc: func(_ context.Context, configID string) (swarm.Config, []byte, error) { configInspectFunc: func(_ context.Context, configID string) (swarm.Config, []byte, error) {
if configID == "foo" { if configID == "foo" {
return *Config(ConfigName("foo")), nil, nil return *builders.Config(builders.ConfigName("foo")), nil, nil
} }
return swarm.Config{}, nil, errors.Errorf("error while inspecting the config") return swarm.Config{}, nil, errors.Errorf("error while inspecting the config")
}, },
@ -58,7 +58,7 @@ func TestConfigInspectErrors(t *testing.T) {
) )
cmd.SetArgs(tc.args) cmd.SetArgs(tc.args)
for key, value := range tc.flags { for key, value := range tc.flags {
cmd.Flags().Set(key, value) assert.Check(t, cmd.Flags().Set(key, value))
} }
cmd.SetOut(io.Discard) cmd.SetOut(io.Discard)
assert.ErrorContains(t, cmd.Execute(), tc.expectedError) assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
@ -78,14 +78,14 @@ func TestConfigInspectWithoutFormat(t *testing.T) {
if name != "foo" { if name != "foo" {
return swarm.Config{}, nil, errors.Errorf("Invalid name, expected %s, got %s", "foo", name) return swarm.Config{}, nil, errors.Errorf("Invalid name, expected %s, got %s", "foo", name)
} }
return *Config(ConfigID("ID-foo"), ConfigName("foo")), nil, nil return *builders.Config(builders.ConfigID("ID-foo"), builders.ConfigName("foo")), nil, nil
}, },
}, },
{ {
name: "multiple-configs-with-labels", name: "multiple-configs-with-labels",
args: []string{"foo", "bar"}, args: []string{"foo", "bar"},
configInspectFunc: func(_ context.Context, name string) (swarm.Config, []byte, error) { configInspectFunc: func(_ context.Context, name string) (swarm.Config, []byte, error) {
return *Config(ConfigID("ID-"+name), ConfigName(name), ConfigLabels(map[string]string{ return *builders.Config(builders.ConfigID("ID-"+name), builders.ConfigName(name), builders.ConfigLabels(map[string]string{
"label1": "label-foo", "label1": "label-foo",
})), nil, nil })), nil, nil
}, },
@ -102,7 +102,7 @@ func TestConfigInspectWithoutFormat(t *testing.T) {
func TestConfigInspectWithFormat(t *testing.T) { func TestConfigInspectWithFormat(t *testing.T) {
configInspectFunc := func(_ context.Context, name string) (swarm.Config, []byte, error) { configInspectFunc := func(_ context.Context, name string) (swarm.Config, []byte, error) {
return *Config(ConfigName("foo"), ConfigLabels(map[string]string{ return *builders.Config(builders.ConfigName("foo"), builders.ConfigLabels(map[string]string{
"label1": "label-foo", "label1": "label-foo",
})), nil, nil })), nil, nil
} }
@ -131,7 +131,7 @@ func TestConfigInspectWithFormat(t *testing.T) {
}) })
cmd := newConfigInspectCommand(cli) cmd := newConfigInspectCommand(cli)
cmd.SetArgs(tc.args) cmd.SetArgs(tc.args)
cmd.Flags().Set("format", tc.format) assert.Check(t, cmd.Flags().Set("format", tc.format))
assert.NilError(t, cmd.Execute()) assert.NilError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("config-inspect-with-format.%s.golden", tc.name)) golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("config-inspect-with-format.%s.golden", tc.name))
} }
@ -145,15 +145,15 @@ func TestConfigInspectPretty(t *testing.T) {
{ {
name: "simple", name: "simple",
configInspectFunc: func(_ context.Context, id string) (swarm.Config, []byte, error) { configInspectFunc: func(_ context.Context, id string) (swarm.Config, []byte, error) {
return *Config( return *builders.Config(
ConfigLabels(map[string]string{ builders.ConfigLabels(map[string]string{
"lbl1": "value1", "lbl1": "value1",
}), }),
ConfigID("configID"), builders.ConfigID("configID"),
ConfigName("configName"), builders.ConfigName("configName"),
ConfigCreatedAt(time.Time{}), builders.ConfigCreatedAt(time.Time{}),
ConfigUpdatedAt(time.Time{}), builders.ConfigUpdatedAt(time.Time{}),
ConfigData([]byte("payload here")), builders.ConfigData([]byte("payload here")),
), []byte{}, nil ), []byte{}, nil
}, },
}, },
@ -165,7 +165,7 @@ func TestConfigInspectPretty(t *testing.T) {
cmd := newConfigInspectCommand(cli) cmd := newConfigInspectCommand(cli)
cmd.SetArgs([]string{"configID"}) cmd.SetArgs([]string{"configID"})
cmd.Flags().Set("pretty", "true") assert.Check(t, cmd.Flags().Set("pretty", "true"))
assert.NilError(t, cmd.Execute()) assert.NilError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("config-inspect-pretty.%s.golden", tc.name)) golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("config-inspect-pretty.%s.golden", tc.name))
} }

View File

@ -31,23 +31,22 @@ func newConfigListCommand(dockerCli command.Cli) *cobra.Command {
Short: "List configs", Short: "List configs",
Args: cli.NoArgs, Args: cli.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return RunConfigList(dockerCli, listOpts) return RunConfigList(cmd.Context(), dockerCli, listOpts)
}, },
ValidArgsFunction: completion.NoComplete, ValidArgsFunction: completion.NoComplete,
} }
flags := cmd.Flags() flags := cmd.Flags()
flags.BoolVarP(&listOpts.Quiet, "quiet", "q", false, "Only display IDs") flags.BoolVarP(&listOpts.Quiet, "quiet", "q", false, "Only display IDs")
flags.StringVarP(&listOpts.Format, "format", "", "", flagsHelper.FormatHelp) flags.StringVar(&listOpts.Format, "format", "", flagsHelper.FormatHelp)
flags.VarP(&listOpts.Filter, "filter", "f", "Filter output based on conditions provided") flags.VarP(&listOpts.Filter, "filter", "f", "Filter output based on conditions provided")
return cmd return cmd
} }
// RunConfigList lists Swarm configs. // RunConfigList lists Swarm configs.
func RunConfigList(dockerCli command.Cli, options ListOptions) error { func RunConfigList(ctx context.Context, dockerCli command.Cli, options ListOptions) error {
client := dockerCli.Client() client := dockerCli.Client()
ctx := context.Background()
configs, err := client.ConfigList(ctx, types.ConfigListOptions{Filters: options.Filter.Value()}) configs, err := client.ConfigList(ctx, types.ConfigListOptions{Filters: options.Filter.Value()})
if err != nil { if err != nil {

View File

@ -8,7 +8,7 @@ import (
"github.com/docker/cli/cli/config/configfile" "github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/internal/test" "github.com/docker/cli/internal/test"
. "github.com/docker/cli/internal/test/builders" // Import builders to get the builder function as package function "github.com/docker/cli/internal/test/builders"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/swarm" "github.com/docker/docker/api/types/swarm"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -50,23 +50,23 @@ func TestConfigList(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{ cli := test.NewFakeCli(&fakeClient{
configListFunc: func(_ context.Context, options types.ConfigListOptions) ([]swarm.Config, error) { configListFunc: func(_ context.Context, options types.ConfigListOptions) ([]swarm.Config, error) {
return []swarm.Config{ return []swarm.Config{
*Config(ConfigID("ID-1-foo"), *builders.Config(builders.ConfigID("ID-1-foo"),
ConfigName("1-foo"), builders.ConfigName("1-foo"),
ConfigVersion(swarm.Version{Index: 10}), builders.ConfigVersion(swarm.Version{Index: 10}),
ConfigCreatedAt(time.Now().Add(-2*time.Hour)), builders.ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
ConfigUpdatedAt(time.Now().Add(-1*time.Hour)), builders.ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
), ),
*Config(ConfigID("ID-10-foo"), *builders.Config(builders.ConfigID("ID-10-foo"),
ConfigName("10-foo"), builders.ConfigName("10-foo"),
ConfigVersion(swarm.Version{Index: 11}), builders.ConfigVersion(swarm.Version{Index: 11}),
ConfigCreatedAt(time.Now().Add(-2*time.Hour)), builders.ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
ConfigUpdatedAt(time.Now().Add(-1*time.Hour)), builders.ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
), ),
*Config(ConfigID("ID-2-foo"), *builders.Config(builders.ConfigID("ID-2-foo"),
ConfigName("2-foo"), builders.ConfigName("2-foo"),
ConfigVersion(swarm.Version{Index: 11}), builders.ConfigVersion(swarm.Version{Index: 11}),
ConfigCreatedAt(time.Now().Add(-2*time.Hour)), builders.ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
ConfigUpdatedAt(time.Now().Add(-1*time.Hour)), builders.ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
), ),
}, nil }, nil
}, },
@ -80,15 +80,15 @@ func TestConfigListWithQuietOption(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{ cli := test.NewFakeCli(&fakeClient{
configListFunc: func(_ context.Context, options types.ConfigListOptions) ([]swarm.Config, error) { configListFunc: func(_ context.Context, options types.ConfigListOptions) ([]swarm.Config, error) {
return []swarm.Config{ return []swarm.Config{
*Config(ConfigID("ID-foo"), ConfigName("foo")), *builders.Config(builders.ConfigID("ID-foo"), builders.ConfigName("foo")),
*Config(ConfigID("ID-bar"), ConfigName("bar"), ConfigLabels(map[string]string{ *builders.Config(builders.ConfigID("ID-bar"), builders.ConfigName("bar"), builders.ConfigLabels(map[string]string{
"label": "label-bar", "label": "label-bar",
})), })),
}, nil }, nil
}, },
}) })
cmd := newConfigListCommand(cli) cmd := newConfigListCommand(cli)
cmd.Flags().Set("quiet", "true") assert.Check(t, cmd.Flags().Set("quiet", "true"))
assert.NilError(t, cmd.Execute()) assert.NilError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "config-list-with-quiet-option.golden") golden.Assert(t, cli.OutBuffer().String(), "config-list-with-quiet-option.golden")
} }
@ -97,8 +97,8 @@ func TestConfigListWithConfigFormat(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{ cli := test.NewFakeCli(&fakeClient{
configListFunc: func(_ context.Context, options types.ConfigListOptions) ([]swarm.Config, error) { configListFunc: func(_ context.Context, options types.ConfigListOptions) ([]swarm.Config, error) {
return []swarm.Config{ return []swarm.Config{
*Config(ConfigID("ID-foo"), ConfigName("foo")), *builders.Config(builders.ConfigID("ID-foo"), builders.ConfigName("foo")),
*Config(ConfigID("ID-bar"), ConfigName("bar"), ConfigLabels(map[string]string{ *builders.Config(builders.ConfigID("ID-bar"), builders.ConfigName("bar"), builders.ConfigLabels(map[string]string{
"label": "label-bar", "label": "label-bar",
})), })),
}, nil }, nil
@ -116,15 +116,15 @@ func TestConfigListWithFormat(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{ cli := test.NewFakeCli(&fakeClient{
configListFunc: func(_ context.Context, options types.ConfigListOptions) ([]swarm.Config, error) { configListFunc: func(_ context.Context, options types.ConfigListOptions) ([]swarm.Config, error) {
return []swarm.Config{ return []swarm.Config{
*Config(ConfigID("ID-foo"), ConfigName("foo")), *builders.Config(builders.ConfigID("ID-foo"), builders.ConfigName("foo")),
*Config(ConfigID("ID-bar"), ConfigName("bar"), ConfigLabels(map[string]string{ *builders.Config(builders.ConfigID("ID-bar"), builders.ConfigName("bar"), builders.ConfigLabels(map[string]string{
"label": "label-bar", "label": "label-bar",
})), })),
}, nil }, nil
}, },
}) })
cmd := newConfigListCommand(cli) cmd := newConfigListCommand(cli)
cmd.Flags().Set("format", "{{ .Name }} {{ .Labels }}") assert.Check(t, cmd.Flags().Set("format", "{{ .Name }} {{ .Labels }}"))
assert.NilError(t, cmd.Execute()) assert.NilError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "config-list-with-format.golden") golden.Assert(t, cli.OutBuffer().String(), "config-list-with-format.golden")
} }
@ -135,24 +135,24 @@ func TestConfigListWithFilter(t *testing.T) {
assert.Check(t, is.Equal("foo", options.Filters.Get("name")[0])) assert.Check(t, is.Equal("foo", options.Filters.Get("name")[0]))
assert.Check(t, is.Equal("lbl1=Label-bar", options.Filters.Get("label")[0])) assert.Check(t, is.Equal("lbl1=Label-bar", options.Filters.Get("label")[0]))
return []swarm.Config{ return []swarm.Config{
*Config(ConfigID("ID-foo"), *builders.Config(builders.ConfigID("ID-foo"),
ConfigName("foo"), builders.ConfigName("foo"),
ConfigVersion(swarm.Version{Index: 10}), builders.ConfigVersion(swarm.Version{Index: 10}),
ConfigCreatedAt(time.Now().Add(-2*time.Hour)), builders.ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
ConfigUpdatedAt(time.Now().Add(-1*time.Hour)), builders.ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
), ),
*Config(ConfigID("ID-bar"), *builders.Config(builders.ConfigID("ID-bar"),
ConfigName("bar"), builders.ConfigName("bar"),
ConfigVersion(swarm.Version{Index: 11}), builders.ConfigVersion(swarm.Version{Index: 11}),
ConfigCreatedAt(time.Now().Add(-2*time.Hour)), builders.ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
ConfigUpdatedAt(time.Now().Add(-1*time.Hour)), builders.ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
), ),
}, nil }, nil
}, },
}) })
cmd := newConfigListCommand(cli) cmd := newConfigListCommand(cli)
cmd.Flags().Set("filter", "name=foo") assert.Check(t, cmd.Flags().Set("filter", "name=foo"))
cmd.Flags().Set("filter", "label=lbl1=Label-bar") assert.Check(t, cmd.Flags().Set("filter", "label=lbl1=Label-bar"))
assert.NilError(t, cmd.Execute()) assert.NilError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "config-list-with-filter.golden") golden.Assert(t, cli.OutBuffer().String(), "config-list-with-filter.golden")
} }

View File

@ -26,7 +26,7 @@ func newConfigRemoveCommand(dockerCli command.Cli) *cobra.Command {
opts := RemoveOptions{ opts := RemoveOptions{
Names: args, Names: args,
} }
return RunConfigRemove(dockerCli, opts) return RunConfigRemove(cmd.Context(), dockerCli, opts)
}, },
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return completeNames(dockerCli)(cmd, args, toComplete) return completeNames(dockerCli)(cmd, args, toComplete)
@ -35,9 +35,8 @@ func newConfigRemoveCommand(dockerCli command.Cli) *cobra.Command {
} }
// RunConfigRemove removes the given Swarm configs. // RunConfigRemove removes the given Swarm configs.
func RunConfigRemove(dockerCli command.Cli, opts RemoveOptions) error { func RunConfigRemove(ctx context.Context, dockerCli command.Cli, opts RemoveOptions) error {
client := dockerCli.Client() client := dockerCli.Client()
ctx := context.Background()
var errs []string var errs []string

View File

@ -17,16 +17,15 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
type attachOptions struct { // AttachOptions group options for `attach` command
noStdin bool type AttachOptions struct {
proxy bool NoStdin bool
detachKeys string Proxy bool
DetachKeys string
container string
} }
func inspectContainerAndCheckState(ctx context.Context, cli client.APIClient, args string) (*types.ContainerJSON, error) { func inspectContainerAndCheckState(ctx context.Context, apiClient client.APIClient, args string) (*types.ContainerJSON, error) {
c, err := cli.ContainerInspect(ctx, args) c, err := apiClient.ContainerInspect(ctx, args)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -44,56 +43,56 @@ func inspectContainerAndCheckState(ctx context.Context, cli client.APIClient, ar
} }
// NewAttachCommand creates a new cobra.Command for `docker attach` // NewAttachCommand creates a new cobra.Command for `docker attach`
func NewAttachCommand(dockerCli command.Cli) *cobra.Command { func NewAttachCommand(dockerCLI command.Cli) *cobra.Command {
var opts attachOptions var opts AttachOptions
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "attach [OPTIONS] CONTAINER", Use: "attach [OPTIONS] CONTAINER",
Short: "Attach local standard input, output, and error streams to a running container", Short: "Attach local standard input, output, and error streams to a running container",
Args: cli.ExactArgs(1), Args: cli.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
opts.container = args[0] containerID := args[0]
return runAttach(dockerCli, &opts) return RunAttach(cmd.Context(), dockerCLI, containerID, &opts)
}, },
Annotations: map[string]string{ Annotations: map[string]string{
"aliases": "docker container attach, docker attach", "aliases": "docker container attach, docker attach",
}, },
ValidArgsFunction: completion.ContainerNames(dockerCli, false, func(container types.Container) bool { ValidArgsFunction: completion.ContainerNames(dockerCLI, false, func(ctr types.Container) bool {
return container.State != "paused" return ctr.State != "paused"
}), }),
} }
flags := cmd.Flags() flags := cmd.Flags()
flags.BoolVar(&opts.noStdin, "no-stdin", false, "Do not attach STDIN") flags.BoolVar(&opts.NoStdin, "no-stdin", false, "Do not attach STDIN")
flags.BoolVar(&opts.proxy, "sig-proxy", true, "Proxy all received signals to the process") flags.BoolVar(&opts.Proxy, "sig-proxy", true, "Proxy all received signals to the process")
flags.StringVar(&opts.detachKeys, "detach-keys", "", "Override the key sequence for detaching a container") flags.StringVar(&opts.DetachKeys, "detach-keys", "", "Override the key sequence for detaching a container")
return cmd return cmd
} }
func runAttach(dockerCli command.Cli, opts *attachOptions) error { // RunAttach executes an `attach` command
ctx := context.Background() func RunAttach(ctx context.Context, dockerCLI command.Cli, containerID string, opts *AttachOptions) error {
apiClient := dockerCli.Client() apiClient := dockerCLI.Client()
// request channel to wait for client // request channel to wait for client
resultC, errC := apiClient.ContainerWait(ctx, opts.container, "") resultC, errC := apiClient.ContainerWait(ctx, containerID, "")
c, err := inspectContainerAndCheckState(ctx, apiClient, opts.container) c, err := inspectContainerAndCheckState(ctx, apiClient, containerID)
if err != nil { if err != nil {
return err return err
} }
if err := dockerCli.In().CheckTty(!opts.noStdin, c.Config.Tty); err != nil { if err := dockerCLI.In().CheckTty(!opts.NoStdin, c.Config.Tty); err != nil {
return err return err
} }
detachKeys := dockerCli.ConfigFile().DetachKeys detachKeys := dockerCLI.ConfigFile().DetachKeys
if opts.detachKeys != "" { if opts.DetachKeys != "" {
detachKeys = opts.detachKeys detachKeys = opts.DetachKeys
} }
options := types.ContainerAttachOptions{ options := container.AttachOptions{
Stream: true, Stream: true,
Stdin: !opts.noStdin && c.Config.OpenStdin, Stdin: !opts.NoStdin && c.Config.OpenStdin,
Stdout: true, Stdout: true,
Stderr: true, Stderr: true,
DetachKeys: detachKeys, DetachKeys: detachKeys,
@ -101,16 +100,16 @@ func runAttach(dockerCli command.Cli, opts *attachOptions) error {
var in io.ReadCloser var in io.ReadCloser
if options.Stdin { if options.Stdin {
in = dockerCli.In() in = dockerCLI.In()
} }
if opts.proxy && !c.Config.Tty { if opts.Proxy && !c.Config.Tty {
sigc := notifyAllSignals() sigc := notifyAllSignals()
go ForwardAllSignals(ctx, dockerCli, opts.container, sigc) go ForwardAllSignals(ctx, apiClient, containerID, sigc)
defer signal.StopCatch(sigc) defer signal.StopCatch(sigc)
} }
resp, errAttach := apiClient.ContainerAttach(ctx, opts.container, options) resp, errAttach := apiClient.ContainerAttach(ctx, containerID, options)
if errAttach != nil { if errAttach != nil {
return errAttach return errAttach
} }
@ -124,20 +123,20 @@ func runAttach(dockerCli command.Cli, opts *attachOptions) error {
// the container and not exit. // the container and not exit.
// //
// Recheck the container's state to avoid attach block. // Recheck the container's state to avoid attach block.
_, err = inspectContainerAndCheckState(ctx, apiClient, opts.container) _, err = inspectContainerAndCheckState(ctx, apiClient, containerID)
if err != nil { if err != nil {
return err return err
} }
if c.Config.Tty && dockerCli.Out().IsTerminal() { if c.Config.Tty && dockerCLI.Out().IsTerminal() {
resizeTTY(ctx, dockerCli, opts.container) resizeTTY(ctx, dockerCLI, containerID)
} }
streamer := hijackedIOStreamer{ streamer := hijackedIOStreamer{
streams: dockerCli, streams: dockerCLI,
inputStream: in, inputStream: in,
outputStream: dockerCli.Out(), outputStream: dockerCLI.Out(),
errorStream: dockerCli.Err(), errorStream: dockerCLI.Err(),
resp: resp, resp: resp,
tty: c.Config.Tty, tty: c.Config.Tty,
detachKeys: options.DetachKeys, detachKeys: options.DetachKeys,

View File

@ -6,7 +6,10 @@ import (
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container" "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/network"
"github.com/docker/docker/api/types/system"
"github.com/docker/docker/client" "github.com/docker/docker/client"
specs "github.com/opencontainers/image-spec/specs-go/v1" specs "github.com/opencontainers/image-spec/specs-go/v1"
) )
@ -15,28 +18,29 @@ type fakeClient struct {
client.Client client.Client
inspectFunc func(string) (types.ContainerJSON, error) inspectFunc func(string) (types.ContainerJSON, error)
execInspectFunc func(execID string) (types.ContainerExecInspect, error) execInspectFunc func(execID string) (types.ContainerExecInspect, error)
execCreateFunc func(container string, config types.ExecConfig) (types.IDResponse, error) execCreateFunc func(containerID string, config types.ExecConfig) (types.IDResponse, error)
createContainerFunc func(config *container.Config, createContainerFunc func(config *container.Config,
hostConfig *container.HostConfig, hostConfig *container.HostConfig,
networkingConfig *network.NetworkingConfig, networkingConfig *network.NetworkingConfig,
platform *specs.Platform, platform *specs.Platform,
containerName string) (container.CreateResponse, error) containerName string) (container.CreateResponse, error)
containerStartFunc func(container string, options types.ContainerStartOptions) error containerStartFunc func(containerID string, options container.StartOptions) error
imageCreateFunc func(parentReference string, options types.ImageCreateOptions) (io.ReadCloser, error) imageCreateFunc func(parentReference string, options image.CreateOptions) (io.ReadCloser, error)
infoFunc func() (types.Info, error) infoFunc func() (system.Info, error)
containerStatPathFunc func(container, path string) (types.ContainerPathStat, error) containerStatPathFunc func(containerID, path string) (types.ContainerPathStat, error)
containerCopyFromFunc func(container, srcPath string) (io.ReadCloser, types.ContainerPathStat, error) containerCopyFromFunc func(containerID, srcPath string) (io.ReadCloser, types.ContainerPathStat, error)
logFunc func(string, types.ContainerLogsOptions) (io.ReadCloser, error) logFunc func(string, container.LogsOptions) (io.ReadCloser, error)
waitFunc func(string) (<-chan container.WaitResponse, <-chan error) waitFunc func(string) (<-chan container.WaitResponse, <-chan error)
containerListFunc func(types.ContainerListOptions) ([]types.Container, error) containerListFunc func(container.ListOptions) ([]types.Container, error)
containerExportFunc func(string) (io.ReadCloser, error) containerExportFunc func(string) (io.ReadCloser, error)
containerExecResizeFunc func(id string, options types.ResizeOptions) error containerExecResizeFunc func(id string, options container.ResizeOptions) error
containerRemoveFunc func(ctx context.Context, container string, options types.ContainerRemoveOptions) error containerRemoveFunc func(ctx context.Context, containerID string, options container.RemoveOptions) error
containerKillFunc func(ctx context.Context, container, signal string) error containerKillFunc func(ctx context.Context, containerID, signal string) error
containerPruneFunc func(ctx context.Context, pruneFilters filters.Args) (types.ContainersPruneReport, error)
Version string Version string
} }
func (f *fakeClient) ContainerList(_ context.Context, options types.ContainerListOptions) ([]types.Container, error) { func (f *fakeClient) ContainerList(_ context.Context, options container.ListOptions) ([]types.Container, error) {
if f.containerListFunc != nil { if f.containerListFunc != nil {
return f.containerListFunc(options) return f.containerListFunc(options)
} }
@ -50,9 +54,9 @@ func (f *fakeClient) ContainerInspect(_ context.Context, containerID string) (ty
return types.ContainerJSON{}, nil return types.ContainerJSON{}, nil
} }
func (f *fakeClient) ContainerExecCreate(_ context.Context, container string, config types.ExecConfig) (types.IDResponse, error) { func (f *fakeClient) ContainerExecCreate(_ context.Context, containerID string, config types.ExecConfig) (types.IDResponse, error) {
if f.execCreateFunc != nil { if f.execCreateFunc != nil {
return f.execCreateFunc(container, config) return f.execCreateFunc(containerID, config)
} }
return types.IDResponse{}, nil return types.IDResponse{}, nil
} }
@ -82,44 +86,44 @@ func (f *fakeClient) ContainerCreate(
return container.CreateResponse{}, nil return container.CreateResponse{}, nil
} }
func (f *fakeClient) ContainerRemove(ctx context.Context, container string, options types.ContainerRemoveOptions) error { func (f *fakeClient) ContainerRemove(ctx context.Context, containerID string, options container.RemoveOptions) error {
if f.containerRemoveFunc != nil { if f.containerRemoveFunc != nil {
return f.containerRemoveFunc(ctx, container, options) return f.containerRemoveFunc(ctx, containerID, options)
} }
return nil return nil
} }
func (f *fakeClient) ImageCreate(_ context.Context, parentReference string, options types.ImageCreateOptions) (io.ReadCloser, error) { func (f *fakeClient) ImageCreate(_ context.Context, parentReference string, options image.CreateOptions) (io.ReadCloser, error) {
if f.imageCreateFunc != nil { if f.imageCreateFunc != nil {
return f.imageCreateFunc(parentReference, options) return f.imageCreateFunc(parentReference, options)
} }
return nil, nil return nil, nil
} }
func (f *fakeClient) Info(_ context.Context) (types.Info, error) { func (f *fakeClient) Info(_ context.Context) (system.Info, error) {
if f.infoFunc != nil { if f.infoFunc != nil {
return f.infoFunc() return f.infoFunc()
} }
return types.Info{}, nil return system.Info{}, nil
} }
func (f *fakeClient) ContainerStatPath(_ context.Context, container, path string) (types.ContainerPathStat, error) { func (f *fakeClient) ContainerStatPath(_ context.Context, containerID, path string) (types.ContainerPathStat, error) {
if f.containerStatPathFunc != nil { if f.containerStatPathFunc != nil {
return f.containerStatPathFunc(container, path) return f.containerStatPathFunc(containerID, path)
} }
return types.ContainerPathStat{}, nil return types.ContainerPathStat{}, nil
} }
func (f *fakeClient) CopyFromContainer(_ context.Context, container, srcPath string) (io.ReadCloser, types.ContainerPathStat, error) { func (f *fakeClient) CopyFromContainer(_ context.Context, containerID, srcPath string) (io.ReadCloser, types.ContainerPathStat, error) {
if f.containerCopyFromFunc != nil { if f.containerCopyFromFunc != nil {
return f.containerCopyFromFunc(container, srcPath) return f.containerCopyFromFunc(containerID, srcPath)
} }
return nil, types.ContainerPathStat{}, nil return nil, types.ContainerPathStat{}, nil
} }
func (f *fakeClient) ContainerLogs(_ context.Context, container string, options types.ContainerLogsOptions) (io.ReadCloser, error) { func (f *fakeClient) ContainerLogs(_ context.Context, containerID string, options container.LogsOptions) (io.ReadCloser, error) {
if f.logFunc != nil { if f.logFunc != nil {
return f.logFunc(container, options) return f.logFunc(containerID, options)
} }
return nil, nil return nil, nil
} }
@ -128,37 +132,44 @@ func (f *fakeClient) ClientVersion() string {
return f.Version return f.Version
} }
func (f *fakeClient) ContainerWait(_ context.Context, container string, _ container.WaitCondition) (<-chan container.WaitResponse, <-chan error) { func (f *fakeClient) ContainerWait(_ context.Context, containerID string, _ container.WaitCondition) (<-chan container.WaitResponse, <-chan error) {
if f.waitFunc != nil { if f.waitFunc != nil {
return f.waitFunc(container) return f.waitFunc(containerID)
} }
return nil, nil return nil, nil
} }
func (f *fakeClient) ContainerStart(_ context.Context, container string, options types.ContainerStartOptions) error { func (f *fakeClient) ContainerStart(_ context.Context, containerID string, options container.StartOptions) error {
if f.containerStartFunc != nil { if f.containerStartFunc != nil {
return f.containerStartFunc(container, options) return f.containerStartFunc(containerID, options)
} }
return nil return nil
} }
func (f *fakeClient) ContainerExport(_ context.Context, container string) (io.ReadCloser, error) { func (f *fakeClient) ContainerExport(_ context.Context, containerID string) (io.ReadCloser, error) {
if f.containerExportFunc != nil { if f.containerExportFunc != nil {
return f.containerExportFunc(container) return f.containerExportFunc(containerID)
} }
return nil, nil return nil, nil
} }
func (f *fakeClient) ContainerExecResize(_ context.Context, id string, options types.ResizeOptions) error { func (f *fakeClient) ContainerExecResize(_ context.Context, id string, options container.ResizeOptions) error {
if f.containerExecResizeFunc != nil { if f.containerExecResizeFunc != nil {
return f.containerExecResizeFunc(id, options) return f.containerExecResizeFunc(id, options)
} }
return nil return nil
} }
func (f *fakeClient) ContainerKill(ctx context.Context, container, signal string) error { func (f *fakeClient) ContainerKill(ctx context.Context, containerID, signal string) error {
if f.containerKillFunc != nil { if f.containerKillFunc != nil {
return f.containerKillFunc(ctx, container, signal) return f.containerKillFunc(ctx, containerID, signal)
} }
return nil return nil
} }
func (f *fakeClient) ContainersPrune(ctx context.Context, pruneFilters filters.Args) (types.ContainersPruneReport, error) {
if f.containerPruneFunc != nil {
return f.containerPruneFunc(ctx, pruneFilters)
}
return types.ContainersPruneReport{}, nil
}

View File

@ -8,7 +8,7 @@ import (
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/command/completion"
"github.com/docker/cli/opts" "github.com/docker/cli/opts"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -35,7 +35,7 @@ func NewCommitCommand(dockerCli command.Cli) *cobra.Command {
if len(args) > 1 { if len(args) > 1 {
options.reference = args[1] options.reference = args[1]
} }
return runCommit(dockerCli, &options) return runCommit(cmd.Context(), dockerCli, &options)
}, },
Annotations: map[string]string{ Annotations: map[string]string{
"aliases": "docker container commit, docker commit", "aliases": "docker container commit, docker commit",
@ -56,21 +56,14 @@ func NewCommitCommand(dockerCli command.Cli) *cobra.Command {
return cmd return cmd
} }
func runCommit(dockerCli command.Cli, options *commitOptions) error { func runCommit(ctx context.Context, dockerCli command.Cli, options *commitOptions) error {
ctx := context.Background() response, err := dockerCli.Client().ContainerCommit(ctx, options.container, container.CommitOptions{
Reference: options.reference,
name := options.container
reference := options.reference
commitOptions := types.ContainerCommitOptions{
Reference: reference,
Comment: options.comment, Comment: options.comment,
Author: options.author, Author: options.author,
Changes: options.changes.GetAll(), Changes: options.changes.GetAll(),
Pause: options.pause, Pause: options.pause,
} })
response, err := dockerCli.Client().ContainerCommit(ctx, name, commitOptions)
if err != nil { if err != nil {
return err return err
} }

View File

@ -151,7 +151,7 @@ func NewCopyCommand(dockerCli command.Cli) *cobra.Command {
// User did not specify "quiet" flag; suppress output if no terminal is attached // User did not specify "quiet" flag; suppress output if no terminal is attached
opts.quiet = !dockerCli.Out().IsTerminal() opts.quiet = !dockerCli.Out().IsTerminal()
} }
return runCopy(dockerCli, opts) return runCopy(cmd.Context(), dockerCli, opts)
}, },
Annotations: map[string]string{ Annotations: map[string]string{
"aliases": "docker container cp, docker cp", "aliases": "docker container cp, docker cp",
@ -169,7 +169,7 @@ func progressHumanSize(n int64) string {
return units.HumanSizeWithPrecision(float64(n), 3) return units.HumanSizeWithPrecision(float64(n), 3)
} }
func runCopy(dockerCli command.Cli, opts copyOptions) error { func runCopy(ctx context.Context, dockerCli command.Cli, opts copyOptions) error {
srcContainer, srcPath := splitCpArg(opts.source) srcContainer, srcPath := splitCpArg(opts.source)
destContainer, destPath := splitCpArg(opts.destination) destContainer, destPath := splitCpArg(opts.destination)
@ -191,8 +191,6 @@ func runCopy(dockerCli command.Cli, opts copyOptions) error {
copyConfig.container = destContainer copyConfig.container = destContainer
} }
ctx := context.Background()
switch direction { switch direction {
case fromContainer: case fromContainer:
return copyFromContainer(ctx, dockerCli, copyConfig) return copyFromContainer(ctx, dockerCli, copyConfig)
@ -246,7 +244,6 @@ func copyFromContainer(ctx context.Context, dockerCli command.Cli, copyConfig cp
linkTarget, rebaseName = archive.GetRebaseName(srcPath, linkTarget) linkTarget, rebaseName = archive.GetRebaseName(srcPath, linkTarget)
srcPath = linkTarget srcPath = linkTarget
} }
} }
ctx, cancel := signal.NotifyContext(ctx, os.Interrupt) ctx, cancel := signal.NotifyContext(ctx, os.Interrupt)

View File

@ -1,6 +1,7 @@
package container package container
import ( import (
"context"
"io" "io"
"os" "os"
"runtime" "runtime"
@ -41,7 +42,7 @@ func TestRunCopyWithInvalidArguments(t *testing.T) {
} }
for _, testcase := range testcases { for _, testcase := range testcases {
t.Run(testcase.doc, func(t *testing.T) { t.Run(testcase.doc, func(t *testing.T) {
err := runCopy(test.NewFakeCli(nil), testcase.options) err := runCopy(context.TODO(), test.NewFakeCli(nil), testcase.options)
assert.Error(t, err, testcase.expectedErr) assert.Error(t, err, testcase.expectedErr)
}) })
} }
@ -58,7 +59,7 @@ func TestRunCopyFromContainerToStdout(t *testing.T) {
} }
options := copyOptions{source: "container:/path", destination: "-"} options := copyOptions{source: "container:/path", destination: "-"}
cli := test.NewFakeCli(fakeClient) cli := test.NewFakeCli(fakeClient)
err := runCopy(cli, options) err := runCopy(context.TODO(), cli, options)
assert.NilError(t, err) assert.NilError(t, err)
assert.Check(t, is.Equal(tarContent, cli.OutBuffer().String())) assert.Check(t, is.Equal(tarContent, cli.OutBuffer().String()))
assert.Check(t, is.Equal("", cli.ErrBuffer().String())) assert.Check(t, is.Equal("", cli.ErrBuffer().String()))
@ -78,7 +79,7 @@ func TestRunCopyFromContainerToFilesystem(t *testing.T) {
} }
options := copyOptions{source: "container:/path", destination: destDir.Path(), quiet: true} options := copyOptions{source: "container:/path", destination: destDir.Path(), quiet: true}
cli := test.NewFakeCli(fakeClient) cli := test.NewFakeCli(fakeClient)
err := runCopy(cli, options) err := runCopy(context.TODO(), cli, options)
assert.NilError(t, err) assert.NilError(t, err)
assert.Check(t, is.Equal("", cli.OutBuffer().String())) assert.Check(t, is.Equal("", cli.OutBuffer().String()))
assert.Check(t, is.Equal("", cli.ErrBuffer().String())) assert.Check(t, is.Equal("", cli.ErrBuffer().String()))
@ -106,7 +107,7 @@ func TestRunCopyFromContainerToFilesystemMissingDestinationDirectory(t *testing.
destination: destDir.Join("missing", "foo"), destination: destDir.Join("missing", "foo"),
} }
cli := test.NewFakeCli(fakeClient) cli := test.NewFakeCli(fakeClient)
err := runCopy(cli, options) err := runCopy(context.TODO(), cli, options)
assert.ErrorContains(t, err, destDir.Join("missing")) assert.ErrorContains(t, err, destDir.Join("missing"))
} }
@ -119,7 +120,7 @@ func TestRunCopyToContainerFromFileWithTrailingSlash(t *testing.T) {
destination: "container:/path", destination: "container:/path",
} }
cli := test.NewFakeCli(&fakeClient{}) cli := test.NewFakeCli(&fakeClient{})
err := runCopy(cli, options) err := runCopy(context.TODO(), cli, options)
expectedError := "not a directory" expectedError := "not a directory"
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
@ -134,7 +135,7 @@ func TestRunCopyToContainerSourceDoesNotExist(t *testing.T) {
destination: "container:/path", destination: "container:/path",
} }
cli := test.NewFakeCli(&fakeClient{}) cli := test.NewFakeCli(&fakeClient{})
err := runCopy(cli, options) err := runCopy(context.TODO(), cli, options)
expected := "no such file or directory" expected := "no such file or directory"
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
expected = "cannot find the file specified" expected = "cannot find the file specified"
@ -193,7 +194,7 @@ func TestSplitCpArg(t *testing.T) {
func TestRunCopyFromContainerToFilesystemIrregularDestination(t *testing.T) { func TestRunCopyFromContainerToFilesystemIrregularDestination(t *testing.T) {
options := copyOptions{source: "container:/dev/null", destination: "/dev/random"} options := copyOptions{source: "container:/dev/null", destination: "/dev/random"}
cli := test.NewFakeCli(nil) cli := test.NewFakeCli(nil)
err := runCopy(cli, options) err := runCopy(context.TODO(), cli, options)
assert.Assert(t, err != nil) assert.Assert(t, err != nil)
expected := `"/dev/random" must be a directory or a regular file` expected := `"/dev/random" must be a directory or a regular file`
assert.ErrorContains(t, err, expected) assert.ErrorContains(t, err, expected)

View File

@ -8,15 +8,15 @@ import (
"regexp" "regexp"
"github.com/containerd/containerd/platforms" "github.com/containerd/containerd/platforms"
"github.com/distribution/reference"
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/command/completion"
"github.com/docker/cli/cli/command/image" "github.com/docker/cli/cli/command/image"
"github.com/docker/cli/cli/streams" "github.com/docker/cli/cli/streams"
"github.com/docker/cli/opts" "github.com/docker/cli/opts"
"github.com/docker/distribution/reference"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
imagetypes "github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/versions" "github.com/docker/docker/api/types/versions"
"github.com/docker/docker/errdefs" "github.com/docker/docker/errdefs"
"github.com/docker/docker/pkg/jsonmessage" "github.com/docker/docker/pkg/jsonmessage"
@ -55,7 +55,7 @@ func NewCreateCommand(dockerCli command.Cli) *cobra.Command {
if len(args) > 1 { if len(args) > 1 {
copts.Args = args[1:] copts.Args = args[1:]
} }
return runCreate(dockerCli, cmd.Flags(), &options, copts) return runCreate(cmd.Context(), dockerCli, cmd.Flags(), &options, copts)
}, },
Annotations: map[string]string{ Annotations: map[string]string{
"aliases": "docker container create, docker create", "aliases": "docker container create, docker create",
@ -80,7 +80,7 @@ func NewCreateCommand(dockerCli command.Cli) *cobra.Command {
return cmd return cmd
} }
func runCreate(dockerCli command.Cli, flags *pflag.FlagSet, options *createOptions, copts *containerOptions) error { func runCreate(ctx context.Context, dockerCli command.Cli, flags *pflag.FlagSet, options *createOptions, copts *containerOptions) error {
if err := validatePullOpt(options.pull); err != nil { if err := validatePullOpt(options.pull); err != nil {
reportError(dockerCli.Err(), "create", err.Error(), true) reportError(dockerCli.Err(), "create", err.Error(), true)
return cli.StatusError{StatusCode: 125} return cli.StatusError{StatusCode: 125}
@ -104,7 +104,7 @@ func runCreate(dockerCli command.Cli, flags *pflag.FlagSet, options *createOptio
reportError(dockerCli.Err(), "create", err.Error(), true) reportError(dockerCli.Err(), "create", err.Error(), true)
return cli.StatusError{StatusCode: 125} return cli.StatusError{StatusCode: 125}
} }
id, err := createContainer(context.Background(), dockerCli, containerCfg, options) id, err := createContainer(ctx, dockerCli, containerCfg, options)
if err != nil { if err != nil {
return err return err
} }
@ -113,15 +113,15 @@ func runCreate(dockerCli command.Cli, flags *pflag.FlagSet, options *createOptio
} }
// FIXME(thaJeztah): this is the only code-path that uses APIClient.ImageCreate. Rewrite this to use the regular "pull" code (or vice-versa). // FIXME(thaJeztah): this is the only code-path that uses APIClient.ImageCreate. Rewrite this to use the regular "pull" code (or vice-versa).
func pullImage(ctx context.Context, dockerCli command.Cli, image string, opts *createOptions) error { func pullImage(ctx context.Context, dockerCli command.Cli, img string, options *createOptions) error {
encodedAuth, err := command.RetrieveAuthTokenFromImage(ctx, dockerCli, image) encodedAuth, err := command.RetrieveAuthTokenFromImage(dockerCli.ConfigFile(), img)
if err != nil { if err != nil {
return err return err
} }
responseBody, err := dockerCli.Client().ImageCreate(ctx, image, types.ImageCreateOptions{ responseBody, err := dockerCli.Client().ImageCreate(ctx, img, imagetypes.CreateOptions{
RegistryAuth: encodedAuth, RegistryAuth: encodedAuth,
Platform: opts.platform, Platform: options.platform,
}) })
if err != nil { if err != nil {
return err return err
@ -129,7 +129,7 @@ func pullImage(ctx context.Context, dockerCli command.Cli, image string, opts *c
defer responseBody.Close() defer responseBody.Close()
out := dockerCli.Err() out := dockerCli.Err()
if opts.quiet { if options.quiet {
out = io.Discard out = io.Discard
} }
return jsonmessage.DisplayJSONMessagesToStream(responseBody, streams.NewOut(out), nil) return jsonmessage.DisplayJSONMessagesToStream(responseBody, streams.NewOut(out), nil)
@ -185,7 +185,7 @@ func newCIDFile(path string) (*cidFile, error) {
} }
//nolint:gocyclo //nolint:gocyclo
func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *containerConfig, opts *createOptions) (containerID string, err error) { func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *containerConfig, options *createOptions) (containerID string, err error) {
config := containerCfg.Config config := containerCfg.Config
hostConfig := containerCfg.HostConfig hostConfig := containerCfg.HostConfig
networkingConfig := containerCfg.NetworkingConfig networkingConfig := containerCfg.NetworkingConfig
@ -211,7 +211,7 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *c
if named, ok := ref.(reference.Named); ok { if named, ok := ref.(reference.Named); ok {
namedRef = reference.TagNameOnly(named) namedRef = reference.TagNameOnly(named)
if taggedRef, ok := namedRef.(reference.NamedTagged); ok && !opts.untrusted { if taggedRef, ok := namedRef.(reference.NamedTagged); ok && !options.untrusted {
var err error var err error
trustedRef, err = image.TrustedReference(ctx, dockerCli, taggedRef) trustedRef, err = image.TrustedReference(ctx, dockerCli, taggedRef)
if err != nil { if err != nil {
@ -222,7 +222,7 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *c
} }
pullAndTagImage := func() error { pullAndTagImage := func() error {
if err := pullImage(ctx, dockerCli, config.Image, opts); err != nil { if err := pullImage(ctx, dockerCli, config.Image, options); err != nil {
return err return err
} }
if taggedRef, ok := namedRef.(reference.NamedTagged); ok && trustedRef != nil { if taggedRef, ok := namedRef.(reference.NamedTagged); ok && trustedRef != nil {
@ -236,15 +236,15 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *c
// create. It will produce an error if you try to set a platform on older API // create. It will produce an error if you try to set a platform on older API
// versions, so check the API version here to maintain backwards // versions, so check the API version here to maintain backwards
// compatibility for CLI users. // compatibility for CLI users.
if opts.platform != "" && versions.GreaterThanOrEqualTo(dockerCli.Client().ClientVersion(), "1.41") { if options.platform != "" && versions.GreaterThanOrEqualTo(dockerCli.Client().ClientVersion(), "1.41") {
p, err := platforms.Parse(opts.platform) p, err := platforms.Parse(options.platform)
if err != nil { if err != nil {
return "", errors.Wrap(err, "error parsing specified platform") return "", errors.Wrap(err, "error parsing specified platform")
} }
platform = &p platform = &p
} }
if opts.pull == PullImageAlways { if options.pull == PullImageAlways {
if err := pullAndTagImage(); err != nil { if err := pullAndTagImage(); err != nil {
return "", err return "", err
} }
@ -252,11 +252,11 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *c
hostConfig.ConsoleSize[0], hostConfig.ConsoleSize[1] = dockerCli.Out().GetTtySize() hostConfig.ConsoleSize[0], hostConfig.ConsoleSize[1] = dockerCli.Out().GetTtySize()
response, err := dockerCli.Client().ContainerCreate(ctx, config, hostConfig, networkingConfig, platform, opts.name) response, err := dockerCli.Client().ContainerCreate(ctx, config, hostConfig, networkingConfig, platform, options.name)
if err != nil { if err != nil {
// Pull image if it does not exist locally and we have the PullImageMissing option. Default behavior. // Pull image if it does not exist locally and we have the PullImageMissing option. Default behavior.
if errdefs.IsNotFound(err) && namedRef != nil && opts.pull == PullImageMissing { if errdefs.IsNotFound(err) && namedRef != nil && options.pull == PullImageMissing {
if !opts.quiet { if !options.quiet {
// we don't want to write to stdout anything apart from container.ID // we don't want to write to stdout anything apart from container.ID
fmt.Fprintf(dockerCli.Err(), "Unable to find image '%s' locally\n", reference.FamiliarString(namedRef)) fmt.Fprintf(dockerCli.Err(), "Unable to find image '%s' locally\n", reference.FamiliarString(namedRef))
} }
@ -266,7 +266,7 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *c
} }
var retryErr error var retryErr error
response, retryErr = dockerCli.Client().ContainerCreate(ctx, config, hostConfig, networkingConfig, platform, opts.name) response, retryErr = dockerCli.Client().ContainerCreate(ctx, config, hostConfig, networkingConfig, platform, options.name)
if retryErr != nil { if retryErr != nil {
return "", retryErr return "", retryErr
} }

View File

@ -15,9 +15,10 @@ import (
"github.com/docker/cli/cli/config/configfile" "github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/internal/test" "github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/notary" "github.com/docker/cli/internal/test/notary"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container" "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/network"
"github.com/docker/docker/api/types/system"
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
specs "github.com/opencontainers/image-spec/specs-go/v1" specs "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/spf13/pflag" "github.com/spf13/pflag"
@ -133,12 +134,12 @@ func TestCreateContainerImagePullPolicy(t *testing.T) {
return container.CreateResponse{ID: containerID}, nil return container.CreateResponse{ID: containerID}, nil
} }
}, },
imageCreateFunc: func(parentReference string, options types.ImageCreateOptions) (io.ReadCloser, error) { imageCreateFunc: func(parentReference string, options image.CreateOptions) (io.ReadCloser, error) {
defer func() { pullCounter++ }() defer func() { pullCounter++ }()
return io.NopCloser(strings.NewReader("")), nil return io.NopCloser(strings.NewReader("")), nil
}, },
infoFunc: func() (types.Info, error) { infoFunc: func() (system.Info, error) {
return types.Info{IndexServerAddress: "https://indexserver.example.com"}, nil return system.Info{IndexServerAddress: "https://indexserver.example.com"}, nil
}, },
} }
fakeCLI := test.NewFakeCli(client) fakeCLI := test.NewFakeCli(client)
@ -180,6 +181,7 @@ func TestCreateContainerImagePullPolicyInvalid(t *testing.T) {
t.Run(tc.PullPolicy, func(t *testing.T) { t.Run(tc.PullPolicy, func(t *testing.T) {
dockerCli := test.NewFakeCli(&fakeClient{}) dockerCli := test.NewFakeCli(&fakeClient{})
err := runCreate( err := runCreate(
context.TODO(),
dockerCli, dockerCli,
&pflag.FlagSet{}, &pflag.FlagSet{},
&createOptions{pull: tc.PullPolicy}, &createOptions{pull: tc.PullPolicy},
@ -222,7 +224,7 @@ func TestNewCreateCommandWithContentTrustErrors(t *testing.T) {
} }
for _, tc := range testCases { for _, tc := range testCases {
tc := tc tc := tc
cli := test.NewFakeCli(&fakeClient{ fakeCLI := test.NewFakeCli(&fakeClient{
createContainerFunc: func(config *container.Config, createContainerFunc: func(config *container.Config,
hostConfig *container.HostConfig, hostConfig *container.HostConfig,
networkingConfig *network.NetworkingConfig, networkingConfig *network.NetworkingConfig,
@ -232,8 +234,8 @@ func TestNewCreateCommandWithContentTrustErrors(t *testing.T) {
return container.CreateResponse{}, fmt.Errorf("shouldn't try to pull image") return container.CreateResponse{}, fmt.Errorf("shouldn't try to pull image")
}, },
}, test.EnableContentTrust) }, test.EnableContentTrust)
cli.SetNotaryClient(tc.notaryFunc) fakeCLI.SetNotaryClient(tc.notaryFunc)
cmd := NewCreateCommand(cli) cmd := NewCreateCommand(fakeCLI)
cmd.SetOut(io.Discard) cmd.SetOut(io.Discard)
cmd.SetArgs(tc.args) cmd.SetArgs(tc.args)
err := cmd.Execute() err := cmd.Execute()
@ -322,7 +324,7 @@ func TestCreateContainerWithProxyConfig(t *testing.T) {
} }
sort.Strings(expected) sort.Strings(expected)
cli := test.NewFakeCli(&fakeClient{ fakeCLI := test.NewFakeCli(&fakeClient{
createContainerFunc: func(config *container.Config, createContainerFunc: func(config *container.Config,
hostConfig *container.HostConfig, hostConfig *container.HostConfig,
networkingConfig *network.NetworkingConfig, networkingConfig *network.NetworkingConfig,
@ -334,7 +336,7 @@ func TestCreateContainerWithProxyConfig(t *testing.T) {
return container.CreateResponse{}, nil return container.CreateResponse{}, nil
}, },
}) })
cli.SetConfigFile(&configfile.ConfigFile{ fakeCLI.SetConfigFile(&configfile.ConfigFile{
Proxies: map[string]configfile.ProxyConfig{ Proxies: map[string]configfile.ProxyConfig{
"default": { "default": {
HTTPProxy: "httpProxy", HTTPProxy: "httpProxy",
@ -345,7 +347,7 @@ func TestCreateContainerWithProxyConfig(t *testing.T) {
}, },
}, },
}) })
cmd := NewCreateCommand(cli) cmd := NewCreateCommand(fakeCLI)
cmd.SetOut(io.Discard) cmd.SetOut(io.Discard)
cmd.SetArgs([]string{"image:tag"}) cmd.SetArgs([]string{"image:tag"})
err := cmd.Execute() err := cmd.Execute()

View File

@ -25,7 +25,7 @@ func NewDiffCommand(dockerCli command.Cli) *cobra.Command {
Args: cli.ExactArgs(1), Args: cli.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
opts.container = args[0] opts.container = args[0]
return runDiff(dockerCli, &opts) return runDiff(cmd.Context(), dockerCli, &opts)
}, },
Annotations: map[string]string{ Annotations: map[string]string{
"aliases": "docker container diff, docker diff", "aliases": "docker container diff, docker diff",
@ -34,12 +34,10 @@ func NewDiffCommand(dockerCli command.Cli) *cobra.Command {
} }
} }
func runDiff(dockerCli command.Cli, opts *diffOptions) error { func runDiff(ctx context.Context, dockerCli command.Cli, opts *diffOptions) error {
if opts.container == "" { if opts.container == "" {
return errors.New("Container name cannot be empty") return errors.New("Container name cannot be empty")
} }
ctx := context.Background()
changes, err := dockerCli.Client().ContainerDiff(ctx, opts.container) changes, err := dockerCli.Client().ContainerDiff(ctx, opts.container)
if err != nil { if err != nil {
return err return err

View File

@ -28,7 +28,6 @@ type ExecOptions struct {
Privileged bool Privileged bool
Env opts.ListOpts Env opts.ListOpts
Workdir string Workdir string
Container string
Command []string Command []string
EnvFile opts.ListOpts EnvFile opts.ListOpts
} }
@ -44,15 +43,16 @@ func NewExecOptions() ExecOptions {
// NewExecCommand creates a new cobra.Command for `docker exec` // NewExecCommand creates a new cobra.Command for `docker exec`
func NewExecCommand(dockerCli command.Cli) *cobra.Command { func NewExecCommand(dockerCli command.Cli) *cobra.Command {
options := NewExecOptions() options := NewExecOptions()
var container string
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "exec [OPTIONS] CONTAINER COMMAND [ARG...]", Use: "exec [OPTIONS] CONTAINER COMMAND [ARG...]",
Short: "Execute a command in a running container", Short: "Execute a command in a running container",
Args: cli.RequiresMinArgs(2), Args: cli.RequiresMinArgs(2),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
options.Container = args[0] container = args[0]
options.Command = args[1:] options.Command = args[1:]
return RunExec(dockerCli, options) return RunExec(cmd.Context(), dockerCli, container, options)
}, },
ValidArgsFunction: completion.ContainerNames(dockerCli, false, func(container types.Container) bool { ValidArgsFunction: completion.ContainerNames(dockerCli, false, func(container types.Container) bool {
return container.State != "paused" return container.State != "paused"
@ -66,12 +66,12 @@ func NewExecCommand(dockerCli command.Cli) *cobra.Command {
flags := cmd.Flags() flags := cmd.Flags()
flags.SetInterspersed(false) flags.SetInterspersed(false)
flags.StringVarP(&options.DetachKeys, "detach-keys", "", "", "Override the key sequence for detaching a container") flags.StringVar(&options.DetachKeys, "detach-keys", "", "Override the key sequence for detaching a container")
flags.BoolVarP(&options.Interactive, "interactive", "i", false, "Keep STDIN open even if not attached") flags.BoolVarP(&options.Interactive, "interactive", "i", false, "Keep STDIN open even if not attached")
flags.BoolVarP(&options.TTY, "tty", "t", false, "Allocate a pseudo-TTY") flags.BoolVarP(&options.TTY, "tty", "t", false, "Allocate a pseudo-TTY")
flags.BoolVarP(&options.Detach, "detach", "d", false, "Detached mode: run command in the background") flags.BoolVarP(&options.Detach, "detach", "d", false, "Detached mode: run command in the background")
flags.StringVarP(&options.User, "user", "u", "", `Username or UID (format: "<name|uid>[:<group|gid>]")`) flags.StringVarP(&options.User, "user", "u", "", `Username or UID (format: "<name|uid>[:<group|gid>]")`)
flags.BoolVarP(&options.Privileged, "privileged", "", false, "Give extended privileges to the command") flags.BoolVar(&options.Privileged, "privileged", false, "Give extended privileges to the command")
flags.VarP(&options.Env, "env", "e", "Set environment variables") flags.VarP(&options.Env, "env", "e", "Set environment variables")
flags.SetAnnotation("env", "version", []string{"1.25"}) flags.SetAnnotation("env", "version", []string{"1.25"})
flags.Var(&options.EnvFile, "env-file", "Read in a file of environment variables") flags.Var(&options.EnvFile, "env-file", "Read in a file of environment variables")
@ -96,20 +96,19 @@ func NewExecCommand(dockerCli command.Cli) *cobra.Command {
} }
// RunExec executes an `exec` command // RunExec executes an `exec` command
func RunExec(dockerCli command.Cli, options ExecOptions) error { func RunExec(ctx context.Context, dockerCli command.Cli, container string, options ExecOptions) error {
execConfig, err := parseExec(options, dockerCli.ConfigFile()) execConfig, err := parseExec(options, dockerCli.ConfigFile())
if err != nil { if err != nil {
return err return err
} }
ctx := context.Background()
client := dockerCli.Client() client := dockerCli.Client()
// We need to check the tty _before_ we do the ContainerExecCreate, because // We need to check the tty _before_ we do the ContainerExecCreate, because
// otherwise if we error out we will leak execIDs on the server (and // otherwise if we error out we will leak execIDs on the server (and
// there's no easy way to clean those up). But also in order to make "not // 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. // exist" errors take precedence we do a dummy inspect first.
if _, err := client.ContainerInspect(ctx, options.Container); err != nil { if _, err := client.ContainerInspect(ctx, container); err != nil {
return err return err
} }
if !execConfig.Detach { if !execConfig.Detach {
@ -120,7 +119,7 @@ func RunExec(dockerCli command.Cli, options ExecOptions) error {
fillConsoleSize(execConfig, dockerCli) fillConsoleSize(execConfig, dockerCli)
response, err := client.ContainerExecCreate(ctx, options.Container, *execConfig) response, err := client.ContainerExecCreate(ctx, container, *execConfig)
if err != nil { if err != nil {
return err return err
} }

View File

@ -169,7 +169,6 @@ func TestRunExec(t *testing.T) {
{ {
doc: "successful detach", doc: "successful detach",
options: withDefaultOpts(ExecOptions{ options: withDefaultOpts(ExecOptions{
Container: "thecontainer",
Detach: true, Detach: true,
}), }),
client: fakeClient{execCreateFunc: execCreateWithID}, client: fakeClient{execCreateFunc: execCreateWithID},
@ -193,18 +192,16 @@ func TestRunExec(t *testing.T) {
for _, testcase := range testcases { for _, testcase := range testcases {
t.Run(testcase.doc, func(t *testing.T) { t.Run(testcase.doc, func(t *testing.T) {
cli := test.NewFakeCli(&testcase.client) fakeCLI := test.NewFakeCli(&testcase.client)
err := RunExec(cli, testcase.options) err := RunExec(context.TODO(), fakeCLI, "thecontainer", testcase.options)
if testcase.expectedError != "" { if testcase.expectedError != "" {
assert.ErrorContains(t, err, testcase.expectedError) assert.ErrorContains(t, err, testcase.expectedError)
} else { } else if !assert.Check(t, err) {
if !assert.Check(t, err) {
return return
} }
} assert.Check(t, is.Equal(testcase.expectedOut, fakeCLI.OutBuffer().String()))
assert.Check(t, is.Equal(testcase.expectedOut, cli.OutBuffer().String())) assert.Check(t, is.Equal(testcase.expectedErr, fakeCLI.ErrBuffer().String()))
assert.Check(t, is.Equal(testcase.expectedErr, cli.ErrBuffer().String()))
}) })
} }
} }
@ -265,8 +262,8 @@ func TestNewExecCommandErrors(t *testing.T) {
}, },
} }
for _, tc := range testCases { for _, tc := range testCases {
cli := test.NewFakeCli(&fakeClient{inspectFunc: tc.containerInspectFunc}) fakeCLI := test.NewFakeCli(&fakeClient{inspectFunc: tc.containerInspectFunc})
cmd := NewExecCommand(cli) cmd := NewExecCommand(fakeCLI)
cmd.SetOut(io.Discard) cmd.SetOut(io.Discard)
cmd.SetArgs(tc.args) cmd.SetArgs(tc.args)
assert.ErrorContains(t, cmd.Execute(), tc.expectedError) assert.ErrorContains(t, cmd.Execute(), tc.expectedError)

View File

@ -26,7 +26,7 @@ func NewExportCommand(dockerCli command.Cli) *cobra.Command {
Args: cli.ExactArgs(1), Args: cli.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
opts.container = args[0] opts.container = args[0]
return runExport(dockerCli, opts) return runExport(cmd.Context(), dockerCli, opts)
}, },
Annotations: map[string]string{ Annotations: map[string]string{
"aliases": "docker container export, docker export", "aliases": "docker container export, docker export",
@ -41,7 +41,7 @@ func NewExportCommand(dockerCli command.Cli) *cobra.Command {
return cmd return cmd
} }
func runExport(dockerCli command.Cli, opts exportOptions) error { func runExport(ctx context.Context, dockerCli command.Cli, opts exportOptions) error {
if opts.output == "" && dockerCli.Out().IsTerminal() { if opts.output == "" && dockerCli.Out().IsTerminal() {
return errors.New("cowardly refusing to save to a terminal. Use the -o flag or redirect") return errors.New("cowardly refusing to save to a terminal. Use the -o flag or redirect")
} }
@ -52,7 +52,7 @@ func runExport(dockerCli command.Cli, opts exportOptions) error {
clnt := dockerCli.Client() clnt := dockerCli.Client()
responseBody, err := clnt.ContainerExport(context.Background(), opts.container) responseBody, err := clnt.ContainerExport(ctx, opts.container)
if err != nil { if err != nil {
return err return err
} }

View File

@ -14,8 +14,7 @@ const (
// NewDiffFormat returns a format for use with a diff Context // NewDiffFormat returns a format for use with a diff Context
func NewDiffFormat(source string) formatter.Format { func NewDiffFormat(source string) formatter.Format {
switch source { if source == formatter.TableFormatKey {
case formatter.TableFormatKey:
return defaultDiffTableFormat return defaultDiffTableFormat
} }
return formatter.Format(source) return formatter.Format(source)

View File

@ -24,7 +24,7 @@ const (
pidsHeader = "PIDS" // Used only on Linux pidsHeader = "PIDS" // Used only on Linux
) )
// StatsEntry represents represents the statistics data collected from a container // StatsEntry represents the statistics data collected from a container
type StatsEntry struct { type StatsEntry struct {
Container string Container string
Name string Name string
@ -116,9 +116,9 @@ func NewStats(container string) *Stats {
} }
// statsFormatWrite renders the context for a list of containers statistics // statsFormatWrite renders the context for a list of containers statistics
func statsFormatWrite(ctx formatter.Context, Stats []StatsEntry, osType string, trunc bool) error { func statsFormatWrite(ctx formatter.Context, stats []StatsEntry, osType string, trunc bool) error {
render := func(format func(subContext formatter.SubContext) error) error { render := func(format func(subContext formatter.SubContext) error) error {
for _, cstats := range Stats { for _, cstats := range stats {
statsCtx := &statsContext{ statsCtx := &statsContext{
s: cstats, s: cstats,
os: osType, os: osType,

View File

@ -1,3 +1,6 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.19
package container package container
import ( import (
@ -27,7 +30,7 @@ func newInspectCommand(dockerCli command.Cli) *cobra.Command {
Args: cli.RequiresMinArgs(1), Args: cli.RequiresMinArgs(1),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
opts.refs = args opts.refs = args
return runInspect(dockerCli, opts) return runInspect(cmd.Context(), dockerCli, opts)
}, },
ValidArgsFunction: completion.ContainerNames(dockerCli, true), ValidArgsFunction: completion.ContainerNames(dockerCli, true),
} }
@ -39,11 +42,10 @@ func newInspectCommand(dockerCli command.Cli) *cobra.Command {
return cmd return cmd
} }
func runInspect(dockerCli command.Cli, opts inspectOptions) error { func runInspect(ctx context.Context, dockerCli command.Cli, opts inspectOptions) error {
client := dockerCli.Client() client := dockerCli.Client()
ctx := context.Background()
getRefFunc := func(ref string) (interface{}, []byte, error) { getRefFunc := func(ref string) (any, []byte, error) {
return client.ContainerInspectWithRaw(ctx, ref, opts.size) return client.ContainerInspectWithRaw(ctx, ref, opts.size)
} }
return inspect.Inspect(dockerCli.Out(), opts.refs, opts.format, getRefFunc) return inspect.Inspect(dockerCli.Out(), opts.refs, opts.format, getRefFunc)

View File

@ -28,7 +28,7 @@ func NewKillCommand(dockerCli command.Cli) *cobra.Command {
Args: cli.RequiresMinArgs(1), Args: cli.RequiresMinArgs(1),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
opts.containers = args opts.containers = args
return runKill(dockerCli, &opts) return runKill(cmd.Context(), dockerCli, &opts)
}, },
Annotations: map[string]string{ Annotations: map[string]string{
"aliases": "docker container kill, docker kill", "aliases": "docker container kill, docker kill",
@ -41,9 +41,8 @@ func NewKillCommand(dockerCli command.Cli) *cobra.Command {
return cmd return cmd
} }
func runKill(dockerCli command.Cli, opts *killOptions) error { func runKill(ctx context.Context, dockerCli command.Cli, opts *killOptions) error {
var errs []string var errs []string
ctx := context.Background()
errChan := parallelOperation(ctx, opts.containers, func(ctx context.Context, container string) error { errChan := parallelOperation(ctx, opts.containers, func(ctx context.Context, container string) error {
return dockerCli.Client().ContainerKill(ctx, container, opts.signal) return dockerCli.Client().ContainerKill(ctx, container, opts.signal)
}) })

View File

@ -11,7 +11,7 @@ import (
flagsHelper "github.com/docker/cli/cli/flags" flagsHelper "github.com/docker/cli/cli/flags"
"github.com/docker/cli/opts" "github.com/docker/cli/opts"
"github.com/docker/cli/templates" "github.com/docker/cli/templates"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -29,7 +29,7 @@ type psOptions struct {
} }
// NewPsCommand creates a new cobra.Command for `docker ps` // NewPsCommand creates a new cobra.Command for `docker ps`
func NewPsCommand(dockerCli command.Cli) *cobra.Command { func NewPsCommand(dockerCLI command.Cli) *cobra.Command {
options := psOptions{filter: opts.NewFilterOpt()} options := psOptions{filter: opts.NewFilterOpt()}
cmd := &cobra.Command{ cmd := &cobra.Command{
@ -38,7 +38,7 @@ func NewPsCommand(dockerCli command.Cli) *cobra.Command {
Args: cli.NoArgs, Args: cli.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
options.sizeChanged = cmd.Flags().Changed("size") options.sizeChanged = cmd.Flags().Changed("size")
return runPs(dockerCli, &options) return runPs(cmd.Context(), dockerCLI, &options)
}, },
Annotations: map[string]string{ Annotations: map[string]string{
"category-top": "3", "category-top": "3",
@ -55,34 +55,34 @@ func NewPsCommand(dockerCli command.Cli) *cobra.Command {
flags.BoolVar(&options.noTrunc, "no-trunc", false, "Don't truncate output") flags.BoolVar(&options.noTrunc, "no-trunc", false, "Don't truncate output")
flags.BoolVarP(&options.nLatest, "latest", "l", false, "Show the latest created container (includes all states)") flags.BoolVarP(&options.nLatest, "latest", "l", false, "Show the latest created container (includes all states)")
flags.IntVarP(&options.last, "last", "n", -1, "Show n last created containers (includes all states)") flags.IntVarP(&options.last, "last", "n", -1, "Show n last created containers (includes all states)")
flags.StringVarP(&options.format, "format", "", "", flagsHelper.FormatHelp) flags.StringVar(&options.format, "format", "", flagsHelper.FormatHelp)
flags.VarP(&options.filter, "filter", "f", "Filter output based on conditions provided") flags.VarP(&options.filter, "filter", "f", "Filter output based on conditions provided")
return cmd return cmd
} }
func newListCommand(dockerCli command.Cli) *cobra.Command { func newListCommand(dockerCLI command.Cli) *cobra.Command {
cmd := *NewPsCommand(dockerCli) cmd := *NewPsCommand(dockerCLI)
cmd.Aliases = []string{"ps", "list"} cmd.Aliases = []string{"ps", "list"}
cmd.Use = "ls [OPTIONS]" cmd.Use = "ls [OPTIONS]"
return &cmd return &cmd
} }
func buildContainerListOptions(opts *psOptions) (*types.ContainerListOptions, error) { func buildContainerListOptions(options *psOptions) (*container.ListOptions, error) {
options := &types.ContainerListOptions{ listOptions := &container.ListOptions{
All: opts.all, All: options.all,
Limit: opts.last, Limit: options.last,
Size: opts.size, Size: options.size,
Filters: opts.filter.Value(), Filters: options.filter.Value(),
} }
if opts.nLatest && opts.last == -1 { if options.nLatest && options.last == -1 {
options.Limit = 1 listOptions.Limit = 1
} }
// always validate template when `--format` is used, for consistency // always validate template when `--format` is used, for consistency
if len(opts.format) > 0 { if len(options.format) > 0 {
tmpl, err := templates.NewParse("", opts.format) tmpl, err := templates.NewParse("", options.format)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "failed to parse template") return nil, errors.Wrap(err, "failed to parse template")
} }
@ -97,7 +97,7 @@ func buildContainerListOptions(opts *psOptions) (*types.ContainerListOptions, er
// if `size` was not explicitly set to false (with `--size=false`) // if `size` was not explicitly set to false (with `--size=false`)
// and `--quiet` is not set, request size if the template requires it // and `--quiet` is not set, request size if the template requires it
if !opts.quiet && !options.Size && !opts.sizeChanged { if !options.quiet && !listOptions.Size && !options.sizeChanged {
// The --size option isn't set, but .Size may be used in the template. // The --size option isn't set, but .Size may be used in the template.
// Parse and execute the given template to detect if the .Size field is // Parse and execute the given template to detect if the .Size field is
// used. If it is, then automatically enable the --size option. See #24696 // used. If it is, then automatically enable the --size option. See #24696
@ -106,22 +106,20 @@ func buildContainerListOptions(opts *psOptions) (*types.ContainerListOptions, er
// because calculating the size is a costly operation. // because calculating the size is a costly operation.
if _, ok := optionsProcessor.FieldsUsed["Size"]; ok { if _, ok := optionsProcessor.FieldsUsed["Size"]; ok {
options.Size = true listOptions.Size = true
} }
} }
} }
return options, nil return listOptions, nil
} }
func runPs(dockerCli command.Cli, options *psOptions) error { func runPs(ctx context.Context, dockerCLI command.Cli, options *psOptions) error {
ctx := context.Background()
if len(options.format) == 0 { if len(options.format) == 0 {
// load custom psFormat from CLI config (if any) // load custom psFormat from CLI config (if any)
options.format = dockerCli.ConfigFile().PsFormat options.format = dockerCLI.ConfigFile().PsFormat
} else if options.quiet { } else if options.quiet {
_, _ = dockerCli.Err().Write([]byte("WARNING: Ignoring custom format, because both --format and --quiet are set.\n")) _, _ = dockerCLI.Err().Write([]byte("WARNING: Ignoring custom format, because both --format and --quiet are set.\n"))
} }
listOptions, err := buildContainerListOptions(options) listOptions, err := buildContainerListOptions(options)
@ -129,13 +127,13 @@ func runPs(dockerCli command.Cli, options *psOptions) error {
return err return err
} }
containers, err := dockerCli.Client().ContainerList(ctx, *listOptions) containers, err := dockerCLI.Client().ContainerList(ctx, *listOptions)
if err != nil { if err != nil {
return err return err
} }
containerCtx := formatter.Context{ containerCtx := formatter.Context{
Output: dockerCli.Out(), Output: dockerCLI.Out(),
Format: formatter.NewContainerFormat(options.format, options.quiet, listOptions.Size), Format: formatter.NewContainerFormat(options.format, options.quiet, listOptions.Size),
Trunc: !options.noTrunc, Trunc: !options.noTrunc,
} }

View File

@ -7,9 +7,10 @@ import (
"github.com/docker/cli/cli/config/configfile" "github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/internal/test" "github.com/docker/cli/internal/test"
. "github.com/docker/cli/internal/test/builders" // Import builders to get the builder function as package function "github.com/docker/cli/internal/test/builders"
"github.com/docker/cli/opts" "github.com/docker/cli/opts"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"gotest.tools/v3/assert" "gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp" is "gotest.tools/v3/assert/cmp"
"gotest.tools/v3/golden" "gotest.tools/v3/golden"
@ -129,7 +130,7 @@ func TestContainerListErrors(t *testing.T) {
testCases := []struct { testCases := []struct {
args []string args []string
flags map[string]string flags map[string]string
containerListFunc func(types.ContainerListOptions) ([]types.Container, error) containerListFunc func(container.ListOptions) ([]types.Container, error)
expectedError string expectedError string
}{ }{
{ {
@ -145,7 +146,7 @@ func TestContainerListErrors(t *testing.T) {
expectedError: `wrong number of args for join`, expectedError: `wrong number of args for join`,
}, },
{ {
containerListFunc: func(_ types.ContainerListOptions) ([]types.Container, error) { containerListFunc: func(_ container.ListOptions) ([]types.Container, error) {
return nil, fmt.Errorf("error listing containers") return nil, fmt.Errorf("error listing containers")
}, },
expectedError: "error listing containers", expectedError: "error listing containers",
@ -159,7 +160,7 @@ func TestContainerListErrors(t *testing.T) {
) )
cmd.SetArgs(tc.args) cmd.SetArgs(tc.args)
for key, value := range tc.flags { for key, value := range tc.flags {
cmd.Flags().Set(key, value) assert.Check(t, cmd.Flags().Set(key, value))
} }
cmd.SetOut(io.Discard) cmd.SetOut(io.Discard)
assert.ErrorContains(t, cmd.Execute(), tc.expectedError) assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
@ -168,13 +169,13 @@ func TestContainerListErrors(t *testing.T) {
func TestContainerListWithoutFormat(t *testing.T) { func TestContainerListWithoutFormat(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{ cli := test.NewFakeCli(&fakeClient{
containerListFunc: func(_ types.ContainerListOptions) ([]types.Container, error) { containerListFunc: func(_ container.ListOptions) ([]types.Container, error) {
return []types.Container{ return []types.Container{
*Container("c1"), *builders.Container("c1"),
*Container("c2", WithName("foo")), *builders.Container("c2", builders.WithName("foo")),
*Container("c3", WithPort(80, 80, TCP), WithPort(81, 81, TCP), WithPort(82, 82, TCP)), *builders.Container("c3", builders.WithPort(80, 80, builders.TCP), builders.WithPort(81, 81, builders.TCP), builders.WithPort(82, 82, builders.TCP)),
*Container("c4", WithPort(81, 81, UDP)), *builders.Container("c4", builders.WithPort(81, 81, builders.UDP)),
*Container("c5", WithPort(82, 82, IP("8.8.8.8"), TCP)), *builders.Container("c5", builders.WithPort(82, 82, builders.IP("8.8.8.8"), builders.TCP)),
}, nil }, nil
}, },
}) })
@ -185,15 +186,15 @@ func TestContainerListWithoutFormat(t *testing.T) {
func TestContainerListNoTrunc(t *testing.T) { func TestContainerListNoTrunc(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{ cli := test.NewFakeCli(&fakeClient{
containerListFunc: func(_ types.ContainerListOptions) ([]types.Container, error) { containerListFunc: func(_ container.ListOptions) ([]types.Container, error) {
return []types.Container{ return []types.Container{
*Container("c1"), *builders.Container("c1"),
*Container("c2", WithName("foo/bar")), *builders.Container("c2", builders.WithName("foo/bar")),
}, nil }, nil
}, },
}) })
cmd := newListCommand(cli) cmd := newListCommand(cli)
cmd.Flags().Set("no-trunc", "true") assert.Check(t, cmd.Flags().Set("no-trunc", "true"))
assert.NilError(t, cmd.Execute()) assert.NilError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "container-list-without-format-no-trunc.golden") golden.Assert(t, cli.OutBuffer().String(), "container-list-without-format-no-trunc.golden")
} }
@ -201,15 +202,15 @@ func TestContainerListNoTrunc(t *testing.T) {
// Test for GitHub issue docker/docker#21772 // Test for GitHub issue docker/docker#21772
func TestContainerListNamesMultipleTime(t *testing.T) { func TestContainerListNamesMultipleTime(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{ cli := test.NewFakeCli(&fakeClient{
containerListFunc: func(_ types.ContainerListOptions) ([]types.Container, error) { containerListFunc: func(_ container.ListOptions) ([]types.Container, error) {
return []types.Container{ return []types.Container{
*Container("c1"), *builders.Container("c1"),
*Container("c2", WithName("foo/bar")), *builders.Container("c2", builders.WithName("foo/bar")),
}, nil }, nil
}, },
}) })
cmd := newListCommand(cli) cmd := newListCommand(cli)
cmd.Flags().Set("format", "{{.Names}} {{.Names}}") assert.Check(t, cmd.Flags().Set("format", "{{.Names}} {{.Names}}"))
assert.NilError(t, cmd.Execute()) assert.NilError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "container-list-format-name-name.golden") golden.Assert(t, cli.OutBuffer().String(), "container-list-format-name-name.golden")
} }
@ -217,15 +218,15 @@ func TestContainerListNamesMultipleTime(t *testing.T) {
// Test for GitHub issue docker/docker#30291 // Test for GitHub issue docker/docker#30291
func TestContainerListFormatTemplateWithArg(t *testing.T) { func TestContainerListFormatTemplateWithArg(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{ cli := test.NewFakeCli(&fakeClient{
containerListFunc: func(_ types.ContainerListOptions) ([]types.Container, error) { containerListFunc: func(_ container.ListOptions) ([]types.Container, error) {
return []types.Container{ return []types.Container{
*Container("c1", WithLabel("some.label", "value")), *builders.Container("c1", builders.WithLabel("some.label", "value")),
*Container("c2", WithName("foo/bar"), WithLabel("foo", "bar")), *builders.Container("c2", builders.WithName("foo/bar"), builders.WithLabel("foo", "bar")),
}, nil }, nil
}, },
}) })
cmd := newListCommand(cli) cmd := newListCommand(cli)
cmd.Flags().Set("format", `{{.Names}} {{.Label "some.label"}}`) assert.Check(t, cmd.Flags().Set("format", `{{.Names}} {{.Label "some.label"}}`))
assert.NilError(t, cmd.Execute()) assert.NilError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "container-list-format-with-arg.golden") golden.Assert(t, cli.OutBuffer().String(), "container-list-format-with-arg.golden")
} }
@ -268,15 +269,15 @@ func TestContainerListFormatSizeSetsOption(t *testing.T) {
tc := tc tc := tc
t.Run(tc.doc, func(t *testing.T) { t.Run(tc.doc, func(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{ cli := test.NewFakeCli(&fakeClient{
containerListFunc: func(options types.ContainerListOptions) ([]types.Container, error) { containerListFunc: func(options container.ListOptions) ([]types.Container, error) {
assert.Check(t, is.Equal(options.Size, tc.sizeExpected)) assert.Check(t, is.Equal(options.Size, tc.sizeExpected))
return []types.Container{}, nil return []types.Container{}, nil
}, },
}) })
cmd := newListCommand(cli) cmd := newListCommand(cli)
cmd.Flags().Set("format", tc.format) assert.Check(t, cmd.Flags().Set("format", tc.format))
if tc.sizeFlag != "" { if tc.sizeFlag != "" {
cmd.Flags().Set("size", tc.sizeFlag) assert.Check(t, cmd.Flags().Set("size", tc.sizeFlag))
} }
assert.NilError(t, cmd.Execute()) assert.NilError(t, cmd.Execute())
}) })
@ -285,10 +286,10 @@ func TestContainerListFormatSizeSetsOption(t *testing.T) {
func TestContainerListWithConfigFormat(t *testing.T) { func TestContainerListWithConfigFormat(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{ cli := test.NewFakeCli(&fakeClient{
containerListFunc: func(_ types.ContainerListOptions) ([]types.Container, error) { containerListFunc: func(_ container.ListOptions) ([]types.Container, error) {
return []types.Container{ return []types.Container{
*Container("c1", WithLabel("some.label", "value"), WithSize(10700000)), *builders.Container("c1", builders.WithLabel("some.label", "value"), builders.WithSize(10700000)),
*Container("c2", WithName("foo/bar"), WithLabel("foo", "bar"), WithSize(3200000)), *builders.Container("c2", builders.WithName("foo/bar"), builders.WithLabel("foo", "bar"), builders.WithSize(3200000)),
}, nil }, nil
}, },
}) })
@ -302,10 +303,10 @@ func TestContainerListWithConfigFormat(t *testing.T) {
func TestContainerListWithFormat(t *testing.T) { func TestContainerListWithFormat(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{ cli := test.NewFakeCli(&fakeClient{
containerListFunc: func(_ types.ContainerListOptions) ([]types.Container, error) { containerListFunc: func(_ container.ListOptions) ([]types.Container, error) {
return []types.Container{ return []types.Container{
*Container("c1", WithLabel("some.label", "value")), *builders.Container("c1", builders.WithLabel("some.label", "value")),
*Container("c2", WithName("foo/bar"), WithLabel("foo", "bar")), *builders.Container("c2", builders.WithName("foo/bar"), builders.WithLabel("foo", "bar")),
}, nil }, nil
}, },
}) })

View File

@ -7,7 +7,7 @@ import (
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/command/completion"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container"
"github.com/docker/docker/pkg/stdcopy" "github.com/docker/docker/pkg/stdcopy"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -33,7 +33,7 @@ func NewLogsCommand(dockerCli command.Cli) *cobra.Command {
Args: cli.ExactArgs(1), Args: cli.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
opts.container = args[0] opts.container = args[0]
return runLogs(dockerCli, &opts) return runLogs(cmd.Context(), dockerCli, &opts)
}, },
Annotations: map[string]string{ Annotations: map[string]string{
"aliases": "docker container logs, docker logs", "aliases": "docker container logs, docker logs",
@ -52,15 +52,13 @@ func NewLogsCommand(dockerCli command.Cli) *cobra.Command {
return cmd return cmd
} }
func runLogs(dockerCli command.Cli, opts *logsOptions) error { func runLogs(ctx context.Context, dockerCli command.Cli, opts *logsOptions) error {
ctx := context.Background()
c, err := dockerCli.Client().ContainerInspect(ctx, opts.container) c, err := dockerCli.Client().ContainerInspect(ctx, opts.container)
if err != nil { if err != nil {
return err return err
} }
options := types.ContainerLogsOptions{ responseBody, err := dockerCli.Client().ContainerLogs(ctx, c.ID, container.LogsOptions{
ShowStdout: true, ShowStdout: true,
ShowStderr: true, ShowStderr: true,
Since: opts.since, Since: opts.since,
@ -69,8 +67,7 @@ func runLogs(dockerCli command.Cli, opts *logsOptions) error {
Follow: opts.follow, Follow: opts.follow,
Tail: opts.tail, Tail: opts.tail,
Details: opts.details, Details: opts.details,
} })
responseBody, err := dockerCli.Client().ContainerLogs(ctx, c.ID, options)
if err != nil { if err != nil {
return err return err
} }

View File

@ -1,6 +1,7 @@
package container package container
import ( import (
"context"
"io" "io"
"strings" "strings"
"testing" "testing"
@ -12,8 +13,8 @@ import (
is "gotest.tools/v3/assert/cmp" is "gotest.tools/v3/assert/cmp"
) )
var logFn = func(expectedOut string) func(string, types.ContainerLogsOptions) (io.ReadCloser, error) { var logFn = func(expectedOut string) func(string, container.LogsOptions) (io.ReadCloser, error) {
return func(container string, opts types.ContainerLogsOptions) (io.ReadCloser, error) { return func(container string, opts container.LogsOptions) (io.ReadCloser, error) {
return io.NopCloser(strings.NewReader(expectedOut)), nil return io.NopCloser(strings.NewReader(expectedOut)), nil
} }
} }
@ -46,14 +47,12 @@ func TestRunLogs(t *testing.T) {
t.Run(testcase.doc, func(t *testing.T) { t.Run(testcase.doc, func(t *testing.T) {
cli := test.NewFakeCli(&testcase.client) cli := test.NewFakeCli(&testcase.client)
err := runLogs(cli, testcase.options) err := runLogs(context.TODO(), cli, testcase.options)
if testcase.expectedError != "" { if testcase.expectedError != "" {
assert.ErrorContains(t, err, testcase.expectedError) assert.ErrorContains(t, err, testcase.expectedError)
} else { } else if !assert.Check(t, err) {
if !assert.Check(t, err) {
return return
} }
}
assert.Check(t, is.Equal(testcase.expectedOut, cli.OutBuffer().String())) assert.Check(t, is.Equal(testcase.expectedOut, cli.OutBuffer().String()))
assert.Check(t, is.Equal(testcase.expectedErr, cli.ErrBuffer().String())) assert.Check(t, is.Equal(testcase.expectedErr, cli.ErrBuffer().String()))
}) })

View File

@ -13,18 +13,33 @@ import (
"strings" "strings"
"time" "time"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/compose/loader" "github.com/docker/cli/cli/compose/loader"
"github.com/docker/cli/opts" "github.com/docker/cli/opts"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
mounttypes "github.com/docker/docker/api/types/mount" mounttypes "github.com/docker/docker/api/types/mount"
networktypes "github.com/docker/docker/api/types/network" networktypes "github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/strslice" "github.com/docker/docker/api/types/strslice"
"github.com/docker/docker/api/types/versions"
"github.com/docker/docker/errdefs" "github.com/docker/docker/errdefs"
"github.com/docker/go-connections/nat" "github.com/docker/go-connections/nat"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/spf13/pflag" "github.com/spf13/pflag"
cdi "tags.cncf.io/container-device-interface/pkg/parser"
)
const (
// TODO(thaJeztah): define these in the API-types, or query available defaults
// from the daemon, or require "local" profiles to be an absolute path or
// relative paths starting with "./". The daemon-config has consts for this
// but we don't want to import that package:
// https://github.com/moby/moby/blob/v23.0.0/daemon/config/config.go#L63-L67
// seccompProfileDefault is the built-in default seccomp profile.
seccompProfileDefault = "builtin"
// seccompProfileUnconfined is a special profile name for seccomp to use an
// "unconfined" seccomp profile.
seccompProfileUnconfined = "unconfined"
) )
var deviceCgroupRuleRegexp = regexp.MustCompile(`^[acb] ([0-9]+|\*):([0-9]+|\*) [rwm]{1,3}$`) var deviceCgroupRuleRegexp = regexp.MustCompile(`^[acb] ([0-9]+|\*):([0-9]+|\*) [rwm]{1,3}$`)
@ -119,6 +134,7 @@ type containerOptions struct {
healthInterval time.Duration healthInterval time.Duration
healthTimeout time.Duration healthTimeout time.Duration
healthStartPeriod time.Duration healthStartPeriod time.Duration
healthStartInterval time.Duration
healthRetries int healthRetries int
runtime string runtime string
autoRemove bool autoRemove bool
@ -183,7 +199,7 @@ func addFlags(flags *pflag.FlagSet) *containerOptions {
flags.VarP(&copts.labels, "label", "l", "Set meta data on a container") flags.VarP(&copts.labels, "label", "l", "Set meta data on a container")
flags.Var(&copts.labelsFile, "label-file", "Read in a line delimited file of labels") flags.Var(&copts.labelsFile, "label-file", "Read in a line delimited file of labels")
flags.BoolVar(&copts.readonlyRootfs, "read-only", false, "Mount the container's root filesystem as read only") flags.BoolVar(&copts.readonlyRootfs, "read-only", false, "Mount the container's root filesystem as read only")
flags.StringVar(&copts.restartPolicy, "restart", "no", "Restart policy to apply when a container exits") flags.StringVar(&copts.restartPolicy, "restart", string(container.RestartPolicyDisabled), "Restart policy to apply when a container exits")
flags.StringVar(&copts.stopSignal, "stop-signal", "", "Signal to stop the container") flags.StringVar(&copts.stopSignal, "stop-signal", "", "Signal to stop the container")
flags.IntVar(&copts.stopTimeout, "stop-timeout", 0, "Timeout (in seconds) to stop a container") flags.IntVar(&copts.stopTimeout, "stop-timeout", 0, "Timeout (in seconds) to stop a container")
flags.SetAnnotation("stop-timeout", "version", []string{"1.25"}) flags.SetAnnotation("stop-timeout", "version", []string{"1.25"})
@ -250,6 +266,8 @@ func addFlags(flags *pflag.FlagSet) *containerOptions {
flags.DurationVar(&copts.healthTimeout, "health-timeout", 0, "Maximum time to allow one check to run (ms|s|m|h) (default 0s)") flags.DurationVar(&copts.healthTimeout, "health-timeout", 0, "Maximum time to allow one check to run (ms|s|m|h) (default 0s)")
flags.DurationVar(&copts.healthStartPeriod, "health-start-period", 0, "Start period for the container to initialize before starting health-retries countdown (ms|s|m|h) (default 0s)") flags.DurationVar(&copts.healthStartPeriod, "health-start-period", 0, "Start period for the container to initialize before starting health-retries countdown (ms|s|m|h) (default 0s)")
flags.SetAnnotation("health-start-period", "version", []string{"1.29"}) flags.SetAnnotation("health-start-period", "version", []string{"1.29"})
flags.DurationVar(&copts.healthStartInterval, "health-start-interval", 0, "Time between running the check during the start period (ms|s|m|h) (default 0s)")
flags.SetAnnotation("health-start-interval", "version", []string{"1.44"})
flags.BoolVar(&copts.noHealthcheck, "no-healthcheck", false, "Disable any container-specified HEALTHCHECK") flags.BoolVar(&copts.noHealthcheck, "no-healthcheck", false, "Disable any container-specified HEALTHCHECK")
// Resource management // Resource management
@ -354,7 +372,10 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
volumes := copts.volumes.GetMap() volumes := copts.volumes.GetMap()
// add any bind targets to the list of container volumes // add any bind targets to the list of container volumes
for bind := range copts.volumes.GetMap() { for bind := range copts.volumes.GetMap() {
parsed, _ := loader.ParseVolume(bind) parsed, err := loader.ParseVolume(bind)
if err != nil {
return nil, err
}
if parsed.Source != "" { if parsed.Source != "" {
toBind := bind toBind := bind
@ -449,12 +470,17 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
// parsing flags, we haven't yet sent a _ping to the daemon to determine // parsing flags, we haven't yet sent a _ping to the daemon to determine
// what operating system it is. // what operating system it is.
deviceMappings := []container.DeviceMapping{} deviceMappings := []container.DeviceMapping{}
var cdiDeviceNames []string
for _, device := range copts.devices.GetAll() { for _, device := range copts.devices.GetAll() {
var ( var (
validated string validated string
deviceMapping container.DeviceMapping deviceMapping container.DeviceMapping
err error err error
) )
if cdi.IsQualifiedName(device) {
cdiDeviceNames = append(cdiDeviceNames, device)
continue
}
validated, err = validateDevice(device, serverOS) validated, err = validateDevice(device, serverOS)
if err != nil { if err != nil {
return nil, err return nil, err
@ -526,7 +552,8 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
copts.healthInterval != 0 || copts.healthInterval != 0 ||
copts.healthTimeout != 0 || copts.healthTimeout != 0 ||
copts.healthStartPeriod != 0 || copts.healthStartPeriod != 0 ||
copts.healthRetries != 0 copts.healthRetries != 0 ||
copts.healthStartInterval != 0
if copts.noHealthcheck { if copts.noHealthcheck {
if haveHealthSettings { if haveHealthSettings {
return nil, errors.Errorf("--no-healthcheck conflicts with --health-* options") return nil, errors.Errorf("--no-healthcheck conflicts with --health-* options")
@ -549,16 +576,29 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
if copts.healthStartPeriod < 0 { if copts.healthStartPeriod < 0 {
return nil, fmt.Errorf("--health-start-period cannot be negative") return nil, fmt.Errorf("--health-start-period cannot be negative")
} }
if copts.healthStartInterval < 0 {
return nil, fmt.Errorf("--health-start-interval cannot be negative")
}
healthConfig = &container.HealthConfig{ healthConfig = &container.HealthConfig{
Test: probe, Test: probe,
Interval: copts.healthInterval, Interval: copts.healthInterval,
Timeout: copts.healthTimeout, Timeout: copts.healthTimeout,
StartPeriod: copts.healthStartPeriod, StartPeriod: copts.healthStartPeriod,
StartInterval: copts.healthStartInterval,
Retries: copts.healthRetries, Retries: copts.healthRetries,
} }
} }
deviceRequests := copts.gpus.Value()
if len(cdiDeviceNames) > 0 {
cdiDeviceRequest := container.DeviceRequest{
Driver: "cdi",
DeviceIDs: cdiDeviceNames,
}
deviceRequests = append(deviceRequests, cdiDeviceRequest)
}
resources := container.Resources{ resources := container.Resources{
CgroupParent: copts.cgroupParent, CgroupParent: copts.cgroupParent,
Memory: copts.memory.Value(), Memory: copts.memory.Value(),
@ -589,7 +629,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
Ulimits: copts.ulimits.GetList(), Ulimits: copts.ulimits.GetList(),
DeviceCgroupRules: copts.deviceCgroupRules.GetAll(), DeviceCgroupRules: copts.deviceCgroupRules.GetAll(),
Devices: deviceMappings, Devices: deviceMappings,
DeviceRequests: copts.gpus.Value(), DeviceRequests: deviceRequests,
} }
config := &container.Config{ config := &container.Config{
@ -686,6 +726,12 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
return nil, err return nil, err
} }
// Put the endpoint-specific MacAddress of the "main" network attachment into the container Config for backward
// compatibility with older daemons.
if nw, ok := networkingConfig.EndpointsConfig[hostConfig.NetworkMode.NetworkName()]; ok {
config.MacAddress = nw.MacAddress //nolint:staticcheck // ignore SA1019: field is deprecated, but still used on API < v1.44.
}
return &containerConfig{ return &containerConfig{
Config: config, Config: config,
HostConfig: hostConfig, HostConfig: hostConfig,
@ -706,6 +752,20 @@ func parseNetworkOpts(copts *containerOptions) (map[string]*networktypes.Endpoin
hasUserDefined, hasNonUserDefined bool hasUserDefined, hasNonUserDefined bool
) )
if len(copts.netMode.Value()) == 0 {
n := opts.NetworkAttachmentOpts{
Target: "default",
}
if err := applyContainerOptions(&n, copts); err != nil {
return nil, err
}
ep, err := parseNetworkAttachmentOpt(n)
if err != nil {
return nil, err
}
endpoints["default"] = ep
}
for i, n := range copts.netMode.Value() { for i, n := range copts.netMode.Value() {
n := n n := n
if container.NetworkMode(n.Target).IsUserDefined() { if container.NetworkMode(n.Target).IsUserDefined() {
@ -747,8 +807,7 @@ func parseNetworkOpts(copts *containerOptions) (map[string]*networktypes.Endpoin
return endpoints, nil return endpoints, nil
} }
func applyContainerOptions(n *opts.NetworkAttachmentOpts, copts *containerOptions) error { func applyContainerOptions(n *opts.NetworkAttachmentOpts, copts *containerOptions) error { //nolint:gocyclo
// TODO should copts.MacAddress actually be set on the first network? (currently it's not)
// TODO should we error if _any_ advanced option is used? (i.e. forbid to combine advanced notation with the "old" flags (`--network-alias`, `--link`, `--ip`, `--ip6`)? // TODO should we error if _any_ advanced option is used? (i.e. forbid to combine advanced notation with the "old" flags (`--network-alias`, `--link`, `--ip`, `--ip6`)?
if len(n.Aliases) > 0 && copts.aliases.Len() > 0 { if len(n.Aliases) > 0 && copts.aliases.Len() > 0 {
return errdefs.InvalidParameter(errors.New("conflicting options: cannot specify both --network-alias and per-network alias")) return errdefs.InvalidParameter(errors.New("conflicting options: cannot specify both --network-alias and per-network alias"))
@ -762,11 +821,17 @@ func applyContainerOptions(n *opts.NetworkAttachmentOpts, copts *containerOption
if n.IPv6Address != "" && copts.ipv6Address != "" { if n.IPv6Address != "" && copts.ipv6Address != "" {
return errdefs.InvalidParameter(errors.New("conflicting options: cannot specify both --ip6 and per-network IPv6 address")) return errdefs.InvalidParameter(errors.New("conflicting options: cannot specify both --ip6 and per-network IPv6 address"))
} }
if n.MacAddress != "" && copts.macAddress != "" {
return errdefs.InvalidParameter(errors.New("conflicting options: cannot specify both --mac-address and per-network MAC address"))
}
if len(n.LinkLocalIPs) > 0 && copts.linkLocalIPs.Len() > 0 {
return errdefs.InvalidParameter(errors.New("conflicting options: cannot specify both --link-local-ip and per-network link-local IP addresses"))
}
if copts.aliases.Len() > 0 { if copts.aliases.Len() > 0 {
n.Aliases = make([]string, copts.aliases.Len()) n.Aliases = make([]string, copts.aliases.Len())
copy(n.Aliases, copts.aliases.GetAll()) copy(n.Aliases, copts.aliases.GetAll())
} }
if copts.links.Len() > 0 { if n.Target != "default" && copts.links.Len() > 0 {
n.Links = make([]string, copts.links.Len()) n.Links = make([]string, copts.links.Len())
copy(n.Links, copts.links.GetAll()) copy(n.Links, copts.links.GetAll())
} }
@ -776,8 +841,9 @@ func applyContainerOptions(n *opts.NetworkAttachmentOpts, copts *containerOption
if copts.ipv6Address != "" { if copts.ipv6Address != "" {
n.IPv6Address = copts.ipv6Address n.IPv6Address = copts.ipv6Address
} }
if copts.macAddress != "" {
// TODO should linkLocalIPs be added to the _first_ network only, or to _all_ networks? (should this be a per-network option as well?) n.MacAddress = copts.macAddress
}
if copts.linkLocalIPs.Len() > 0 { if copts.linkLocalIPs.Len() > 0 {
n.LinkLocalIPs = make([]string, copts.linkLocalIPs.Len()) n.LinkLocalIPs = make([]string, copts.linkLocalIPs.Len())
copy(n.LinkLocalIPs, copts.linkLocalIPs.GetAll()) copy(n.LinkLocalIPs, copts.linkLocalIPs.GetAll())
@ -814,6 +880,12 @@ func parseNetworkAttachmentOpt(ep opts.NetworkAttachmentOpts) (*networktypes.End
LinkLocalIPs: ep.LinkLocalIPs, LinkLocalIPs: ep.LinkLocalIPs,
} }
} }
if ep.MacAddress != "" {
if _, err := opts.ValidateMACAddress(ep.MacAddress); err != nil {
return nil, errors.Errorf("%s is not a valid mac address", ep.MacAddress)
}
epConfig.MacAddress = ep.MacAddress
}
return epConfig, nil return epConfig, nil
} }
@ -856,7 +928,13 @@ func parseSecurityOpts(securityOpts []string) ([]string, error) {
// "no-new-privileges" is the only option that does not require a value. // "no-new-privileges" is the only option that does not require a value.
return securityOpts, errors.Errorf("Invalid --security-opt: %q", opt) return securityOpts, errors.Errorf("Invalid --security-opt: %q", opt)
} }
if k == "seccomp" && v != "unconfined" { if k == "seccomp" {
switch v {
case seccompProfileDefault, seccompProfileUnconfined:
// known special names for built-in profiles, nothing to do.
default:
// value may be a filename, in which case we send the profile's
// content if it's valid JSON.
f, err := os.ReadFile(v) f, err := os.ReadFile(v)
if err != nil { if err != nil {
return securityOpts, errors.Errorf("opening seccomp profile (%s) failed: %v", v, err) return securityOpts, errors.Errorf("opening seccomp profile (%s) failed: %v", v, err)
@ -868,6 +946,7 @@ func parseSecurityOpts(securityOpts []string) ([]string, error) {
securityOpts[key] = fmt.Sprintf("seccomp=%s", b.Bytes()) securityOpts[key] = fmt.Sprintf("seccomp=%s", b.Bytes())
} }
} }
}
return securityOpts, nil return securityOpts, nil
} }
@ -1061,8 +1140,8 @@ func validateAttach(val string) (string, error) {
func validateAPIVersion(c *containerConfig, serverAPIVersion string) error { func validateAPIVersion(c *containerConfig, serverAPIVersion string) error {
for _, m := range c.HostConfig.Mounts { for _, m := range c.HostConfig.Mounts {
if m.BindOptions != nil && m.BindOptions.NonRecursive && versions.LessThan(serverAPIVersion, "1.40") { if err := command.ValidateMountWithAPIVersion(m, serverAPIVersion); err != nil {
return errors.Errorf("bind-nonrecursive requires API v1.40 or later") return err
} }
} }
return nil return nil

View File

@ -64,21 +64,21 @@ func setupRunFlags() (*pflag.FlagSet, *containerOptions) {
return flags, copts return flags, copts
} }
func mustParse(t *testing.T, args string) (*container.Config, *container.HostConfig) { func mustParse(t *testing.T, args string) (*container.Config, *container.HostConfig, *networktypes.NetworkingConfig) {
t.Helper() t.Helper()
config, hostConfig, _, err := parseRun(append(strings.Split(args, " "), "ubuntu", "bash")) config, hostConfig, nwConfig, err := parseRun(append(strings.Split(args, " "), "ubuntu", "bash"))
assert.NilError(t, err) assert.NilError(t, err)
return config, hostConfig return config, hostConfig, nwConfig
} }
func TestParseRunLinks(t *testing.T) { func TestParseRunLinks(t *testing.T) {
if _, hostConfig := mustParse(t, "--link a:b"); len(hostConfig.Links) == 0 || hostConfig.Links[0] != "a:b" { if _, hostConfig, _ := mustParse(t, "--link a:b"); len(hostConfig.Links) == 0 || hostConfig.Links[0] != "a:b" {
t.Fatalf("Error parsing links. Expected []string{\"a:b\"}, received: %v", hostConfig.Links) t.Fatalf("Error parsing links. Expected []string{\"a:b\"}, received: %v", hostConfig.Links)
} }
if _, hostConfig := mustParse(t, "--link a:b --link c:d"); len(hostConfig.Links) < 2 || hostConfig.Links[0] != "a:b" || hostConfig.Links[1] != "c:d" { if _, hostConfig, _ := mustParse(t, "--link a:b --link c:d"); len(hostConfig.Links) < 2 || hostConfig.Links[0] != "a:b" || hostConfig.Links[1] != "c:d" {
t.Fatalf("Error parsing links. Expected []string{\"a:b\", \"c:d\"}, received: %v", hostConfig.Links) t.Fatalf("Error parsing links. Expected []string{\"a:b\", \"c:d\"}, received: %v", hostConfig.Links)
} }
if _, hostConfig := mustParse(t, ""); len(hostConfig.Links) != 0 { if _, hostConfig, _ := mustParse(t, ""); len(hostConfig.Links) != 0 {
t.Fatalf("Error parsing links. No link expected, received: %v", hostConfig.Links) t.Fatalf("Error parsing links. No link expected, received: %v", hostConfig.Links)
} }
} }
@ -128,7 +128,7 @@ func TestParseRunAttach(t *testing.T) {
for _, tc := range tests { for _, tc := range tests {
tc := tc tc := tc
t.Run(tc.input, func(t *testing.T) { t.Run(tc.input, func(t *testing.T) {
config, _ := mustParse(t, tc.input) config, _, _ := mustParse(t, tc.input)
assert.Equal(t, config.AttachStdin, tc.expected.AttachStdin) assert.Equal(t, config.AttachStdin, tc.expected.AttachStdin)
assert.Equal(t, config.AttachStdout, tc.expected.AttachStdout) assert.Equal(t, config.AttachStdout, tc.expected.AttachStdout)
assert.Equal(t, config.AttachStderr, tc.expected.AttachStderr) assert.Equal(t, config.AttachStderr, tc.expected.AttachStderr)
@ -186,7 +186,7 @@ func TestParseRunWithInvalidArgs(t *testing.T) {
func TestParseWithVolumes(t *testing.T) { func TestParseWithVolumes(t *testing.T) {
// A single volume // A single volume
arr, tryit := setupPlatformVolume([]string{`/tmp`}, []string{`c:\tmp`}) arr, tryit := setupPlatformVolume([]string{`/tmp`}, []string{`c:\tmp`})
if config, hostConfig := mustParse(t, tryit); hostConfig.Binds != nil { if config, hostConfig, _ := mustParse(t, tryit); hostConfig.Binds != nil {
t.Fatalf("Error parsing volume flags, %q should not mount-bind anything. Received %v", tryit, hostConfig.Binds) t.Fatalf("Error parsing volume flags, %q should not mount-bind anything. Received %v", tryit, hostConfig.Binds)
} else if _, exists := config.Volumes[arr[0]]; !exists { } else if _, exists := config.Volumes[arr[0]]; !exists {
t.Fatalf("Error parsing volume flags, %q is missing from volumes. Received %v", tryit, config.Volumes) t.Fatalf("Error parsing volume flags, %q is missing from volumes. Received %v", tryit, config.Volumes)
@ -194,23 +194,23 @@ func TestParseWithVolumes(t *testing.T) {
// Two volumes // Two volumes
arr, tryit = setupPlatformVolume([]string{`/tmp`, `/var`}, []string{`c:\tmp`, `c:\var`}) arr, tryit = setupPlatformVolume([]string{`/tmp`, `/var`}, []string{`c:\tmp`, `c:\var`})
if config, hostConfig := mustParse(t, tryit); hostConfig.Binds != nil { if config, hostConfig, _ := mustParse(t, tryit); hostConfig.Binds != nil {
t.Fatalf("Error parsing volume flags, %q should not mount-bind anything. Received %v", tryit, hostConfig.Binds) t.Fatalf("Error parsing volume flags, %q should not mount-bind anything. Received %v", tryit, hostConfig.Binds)
} else if _, exists := config.Volumes[arr[0]]; !exists { } else if _, exists := config.Volumes[arr[0]]; !exists {
t.Fatalf("Error parsing volume flags, %s is missing from volumes. Received %v", arr[0], config.Volumes) t.Fatalf("Error parsing volume flags, %s is missing from volumes. Received %v", arr[0], config.Volumes)
} else if _, exists := config.Volumes[arr[1]]; !exists { } else if _, exists := config.Volumes[arr[1]]; !exists { //nolint:govet // ignore shadow-check
t.Fatalf("Error parsing volume flags, %s is missing from volumes. Received %v", arr[1], config.Volumes) t.Fatalf("Error parsing volume flags, %s is missing from volumes. Received %v", arr[1], config.Volumes)
} }
// A single bind mount // A single bind mount
arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp`}, []string{os.Getenv("TEMP") + `:c:\containerTmp`}) arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp`}, []string{os.Getenv("TEMP") + `:c:\containerTmp`})
if config, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || hostConfig.Binds[0] != arr[0] { if config, hostConfig, _ := mustParse(t, tryit); hostConfig.Binds == nil || hostConfig.Binds[0] != arr[0] {
t.Fatalf("Error parsing volume flags, %q should mount-bind the path before the colon into the path after the colon. Received %v %v", arr[0], hostConfig.Binds, config.Volumes) t.Fatalf("Error parsing volume flags, %q should mount-bind the path before the colon into the path after the colon. Received %v %v", arr[0], hostConfig.Binds, config.Volumes)
} }
// Two bind mounts. // Two bind mounts.
arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp`, `/hostVar:/containerVar`}, []string{os.Getenv("ProgramData") + `:c:\ContainerPD`, os.Getenv("TEMP") + `:c:\containerTmp`}) arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp`, `/hostVar:/containerVar`}, []string{os.Getenv("ProgramData") + `:c:\ContainerPD`, os.Getenv("TEMP") + `:c:\containerTmp`})
if _, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], arr[0], arr[1]) != nil { if _, hostConfig, _ := mustParse(t, tryit); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], arr[0], arr[1]) != nil {
t.Fatalf("Error parsing volume flags, `%s and %s` did not mount-bind correctly. Received %v", arr[0], arr[1], hostConfig.Binds) t.Fatalf("Error parsing volume flags, `%s and %s` did not mount-bind correctly. Received %v", arr[0], arr[1], hostConfig.Binds)
} }
@ -219,26 +219,26 @@ func TestParseWithVolumes(t *testing.T) {
arr, tryit = setupPlatformVolume( arr, tryit = setupPlatformVolume(
[]string{`/hostTmp:/containerTmp:ro`, `/hostVar:/containerVar:rw`}, []string{`/hostTmp:/containerTmp:ro`, `/hostVar:/containerVar:rw`},
[]string{os.Getenv("TEMP") + `:c:\containerTmp:rw`, os.Getenv("ProgramData") + `:c:\ContainerPD:rw`}) []string{os.Getenv("TEMP") + `:c:\containerTmp:rw`, os.Getenv("ProgramData") + `:c:\ContainerPD:rw`})
if _, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], arr[0], arr[1]) != nil { if _, hostConfig, _ := mustParse(t, tryit); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], arr[0], arr[1]) != nil {
t.Fatalf("Error parsing volume flags, `%s and %s` did not mount-bind correctly. Received %v", arr[0], arr[1], hostConfig.Binds) t.Fatalf("Error parsing volume flags, `%s and %s` did not mount-bind correctly. Received %v", arr[0], arr[1], hostConfig.Binds)
} }
// Similar to previous test but with alternate modes which are only supported by Linux // Similar to previous test but with alternate modes which are only supported by Linux
if runtime.GOOS != "windows" { if runtime.GOOS != "windows" {
arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp:ro,Z`, `/hostVar:/containerVar:rw,Z`}, []string{}) arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp:ro,Z`, `/hostVar:/containerVar:rw,Z`}, []string{})
if _, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], arr[0], arr[1]) != nil { if _, hostConfig, _ := mustParse(t, tryit); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], arr[0], arr[1]) != nil {
t.Fatalf("Error parsing volume flags, `%s and %s` did not mount-bind correctly. Received %v", arr[0], arr[1], hostConfig.Binds) t.Fatalf("Error parsing volume flags, `%s and %s` did not mount-bind correctly. Received %v", arr[0], arr[1], hostConfig.Binds)
} }
arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp:Z`, `/hostVar:/containerVar:z`}, []string{}) arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp:Z`, `/hostVar:/containerVar:z`}, []string{})
if _, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], arr[0], arr[1]) != nil { if _, hostConfig, _ := mustParse(t, tryit); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], arr[0], arr[1]) != nil {
t.Fatalf("Error parsing volume flags, `%s and %s` did not mount-bind correctly. Received %v", arr[0], arr[1], hostConfig.Binds) t.Fatalf("Error parsing volume flags, `%s and %s` did not mount-bind correctly. Received %v", arr[0], arr[1], hostConfig.Binds)
} }
} }
// One bind mount and one volume // One bind mount and one volume
arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp`, `/containerVar`}, []string{os.Getenv("TEMP") + `:c:\containerTmp`, `c:\containerTmp`}) arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp`, `/containerVar`}, []string{os.Getenv("TEMP") + `:c:\containerTmp`, `c:\containerTmp`})
if config, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || len(hostConfig.Binds) > 1 || hostConfig.Binds[0] != arr[0] { if config, hostConfig, _ := mustParse(t, tryit); hostConfig.Binds == nil || len(hostConfig.Binds) > 1 || hostConfig.Binds[0] != arr[0] {
t.Fatalf("Error parsing volume flags, %s and %s should only one and only one bind mount %s. Received %s", arr[0], arr[1], arr[0], hostConfig.Binds) t.Fatalf("Error parsing volume flags, %s and %s should only one and only one bind mount %s. Received %s", arr[0], arr[1], arr[0], hostConfig.Binds)
} else if _, exists := config.Volumes[arr[1]]; !exists { } else if _, exists := config.Volumes[arr[1]]; !exists {
t.Fatalf("Error parsing volume flags %s and %s. %s is missing from volumes. Received %v", arr[0], arr[1], arr[1], config.Volumes) t.Fatalf("Error parsing volume flags %s and %s. %s is missing from volumes. Received %v", arr[0], arr[1], arr[1], config.Volumes)
@ -247,7 +247,7 @@ func TestParseWithVolumes(t *testing.T) {
// Root to non-c: drive letter (Windows specific) // Root to non-c: drive letter (Windows specific)
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
arr, tryit = setupPlatformVolume([]string{}, []string{os.Getenv("SystemDrive") + `\:d:`}) arr, tryit = setupPlatformVolume([]string{}, []string{os.Getenv("SystemDrive") + `\:d:`})
if config, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || len(hostConfig.Binds) > 1 || hostConfig.Binds[0] != arr[0] || len(config.Volumes) != 0 { if config, hostConfig, _ := mustParse(t, tryit); hostConfig.Binds == nil || len(hostConfig.Binds) > 1 || hostConfig.Binds[0] != arr[0] || len(config.Volumes) != 0 {
t.Fatalf("Error parsing %s. Should have a single bind mount and no volumes", arr[0]) t.Fatalf("Error parsing %s. Should have a single bind mount and no volumes", arr[0])
} }
} }
@ -290,8 +290,14 @@ func TestParseWithMacAddress(t *testing.T) {
if _, _, _, err := parseRun([]string{invalidMacAddress, "img", "cmd"}); err != nil && err.Error() != "invalidMacAddress is not a valid mac address" { if _, _, _, err := parseRun([]string{invalidMacAddress, "img", "cmd"}); err != nil && err.Error() != "invalidMacAddress is not a valid mac address" {
t.Fatalf("Expected an error with %v mac-address, got %v", invalidMacAddress, err) t.Fatalf("Expected an error with %v mac-address, got %v", invalidMacAddress, err)
} }
if config, _ := mustParse(t, validMacAddress); config.MacAddress != "92:d0:c6:0a:29:33" { config, hostConfig, nwConfig := mustParse(t, validMacAddress)
t.Fatalf("Expected the config to have '92:d0:c6:0a:29:33' as MacAddress, got '%v'", config.MacAddress) if config.MacAddress != "92:d0:c6:0a:29:33" { //nolint:staticcheck // ignore SA1019: field is deprecated, but still used on API < v1.44.
t.Fatalf("Expected the config to have '92:d0:c6:0a:29:33' as container-wide MacAddress, got '%v'",
config.MacAddress) //nolint:staticcheck // ignore SA1019: field is deprecated, but still used on API < v1.44.
}
defaultNw := hostConfig.NetworkMode.NetworkName()
if nwConfig.EndpointsConfig[defaultNw].MacAddress != "92:d0:c6:0a:29:33" {
t.Fatalf("Expected the default endpoint to have the MacAddress '92:d0:c6:0a:29:33' set, got '%v'", nwConfig.EndpointsConfig[defaultNw].MacAddress)
} }
} }
@ -301,7 +307,7 @@ func TestRunFlagsParseWithMemory(t *testing.T) {
err := flags.Parse(args) err := flags.Parse(args)
assert.ErrorContains(t, err, `invalid argument "invalid" for "-m, --memory" flag`) assert.ErrorContains(t, err, `invalid argument "invalid" for "-m, --memory" flag`)
_, hostconfig := mustParse(t, "--memory=1G") _, hostconfig, _ := mustParse(t, "--memory=1G")
assert.Check(t, is.Equal(int64(1073741824), hostconfig.Memory)) assert.Check(t, is.Equal(int64(1073741824), hostconfig.Memory))
} }
@ -311,10 +317,10 @@ func TestParseWithMemorySwap(t *testing.T) {
err := flags.Parse(args) err := flags.Parse(args)
assert.ErrorContains(t, err, `invalid argument "invalid" for "--memory-swap" flag`) assert.ErrorContains(t, err, `invalid argument "invalid" for "--memory-swap" flag`)
_, hostconfig := mustParse(t, "--memory-swap=1G") _, hostconfig, _ := mustParse(t, "--memory-swap=1G")
assert.Check(t, is.Equal(int64(1073741824), hostconfig.MemorySwap)) assert.Check(t, is.Equal(int64(1073741824), hostconfig.MemorySwap))
_, hostconfig = mustParse(t, "--memory-swap=-1") _, hostconfig, _ = mustParse(t, "--memory-swap=-1")
assert.Check(t, is.Equal(int64(-1), hostconfig.MemorySwap)) assert.Check(t, is.Equal(int64(-1), hostconfig.MemorySwap))
} }
@ -329,14 +335,14 @@ func TestParseHostname(t *testing.T) {
hostnameWithDomain := "--hostname=hostname.domainname" hostnameWithDomain := "--hostname=hostname.domainname"
hostnameWithDomainTld := "--hostname=hostname.domainname.tld" hostnameWithDomainTld := "--hostname=hostname.domainname.tld"
for hostname, expectedHostname := range validHostnames { for hostname, expectedHostname := range validHostnames {
if config, _ := mustParse(t, fmt.Sprintf("--hostname=%s", 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) t.Fatalf("Expected the config to have 'hostname' as %q, got %q", expectedHostname, config.Hostname)
} }
} }
if config, _ := mustParse(t, hostnameWithDomain); config.Hostname != "hostname.domainname" || config.Domainname != "" { if config, _, _ := mustParse(t, hostnameWithDomain); config.Hostname != "hostname.domainname" || config.Domainname != "" {
t.Fatalf("Expected the config to have 'hostname' as hostname.domainname, got %q", config.Hostname) t.Fatalf("Expected the config to have 'hostname' as hostname.domainname, got %q", config.Hostname)
} }
if config, _ := mustParse(t, hostnameWithDomainTld); config.Hostname != "hostname.domainname.tld" || config.Domainname != "" { if config, _, _ := mustParse(t, hostnameWithDomainTld); config.Hostname != "hostname.domainname.tld" || config.Domainname != "" {
t.Fatalf("Expected the config to have 'hostname' as hostname.domainname.tld, got %q", config.Hostname) t.Fatalf("Expected the config to have 'hostname' as hostname.domainname.tld, got %q", config.Hostname)
} }
} }
@ -350,14 +356,14 @@ func TestParseHostnameDomainname(t *testing.T) {
"domainname-63-bytes-long-should-be-valid-and-without-any-errors": "domainname-63-bytes-long-should-be-valid-and-without-any-errors", "domainname-63-bytes-long-should-be-valid-and-without-any-errors": "domainname-63-bytes-long-should-be-valid-and-without-any-errors",
} }
for domainname, expectedDomainname := range validDomainnames { for domainname, expectedDomainname := range validDomainnames {
if config, _ := mustParse(t, "--domainname="+domainname); config.Domainname != expectedDomainname { if config, _, _ := mustParse(t, "--domainname="+domainname); config.Domainname != expectedDomainname {
t.Fatalf("Expected the config to have 'domainname' as %q, got %q", expectedDomainname, config.Domainname) t.Fatalf("Expected the config to have 'domainname' as %q, got %q", expectedDomainname, config.Domainname)
} }
} }
if config, _ := mustParse(t, "--hostname=some.prefix --domainname=domainname"); config.Hostname != "some.prefix" || config.Domainname != "domainname" { if config, _, _ := mustParse(t, "--hostname=some.prefix --domainname=domainname"); config.Hostname != "some.prefix" || config.Domainname != "domainname" {
t.Fatalf("Expected the config to have 'hostname' as 'some.prefix' and 'domainname' as 'domainname', got %q and %q", config.Hostname, config.Domainname) t.Fatalf("Expected the config to have 'hostname' as 'some.prefix' and 'domainname' as 'domainname', got %q and %q", config.Hostname, config.Domainname)
} }
if config, _ := mustParse(t, "--hostname=another-prefix --domainname=domainname.tld"); config.Hostname != "another-prefix" || config.Domainname != "domainname.tld" { if config, _, _ := mustParse(t, "--hostname=another-prefix --domainname=domainname.tld"); config.Hostname != "another-prefix" || config.Domainname != "domainname.tld" {
t.Fatalf("Expected the config to have 'hostname' as 'another-prefix' and 'domainname' as 'domainname.tld', got %q and %q", config.Hostname, config.Domainname) t.Fatalf("Expected the config to have 'hostname' as 'another-prefix' and 'domainname' as 'domainname.tld', got %q and %q", config.Hostname, config.Domainname)
} }
} }
@ -366,8 +372,8 @@ func TestParseWithExpose(t *testing.T) {
invalids := map[string]string{ invalids := map[string]string{
":": "invalid port format for --expose: :", ":": "invalid port format for --expose: :",
"8080:9090": "invalid port format for --expose: 8080:9090", "8080:9090": "invalid port format for --expose: 8080:9090",
"/tcp": "invalid range format for --expose: /tcp, error: Empty string specified for ports.", "/tcp": "invalid range format for --expose: /tcp, error: empty string specified for ports",
"/udp": "invalid range format for --expose: /udp, error: Empty string specified for ports.", "/udp": "invalid range format for --expose: /udp, error: empty string specified for ports",
"NaN/tcp": `invalid range format for --expose: NaN/tcp, error: strconv.ParseUint: parsing "NaN": invalid syntax`, "NaN/tcp": `invalid range format for --expose: NaN/tcp, error: strconv.ParseUint: parsing "NaN": invalid syntax`,
"NaN-NaN/tcp": `invalid range format for --expose: NaN-NaN/tcp, error: strconv.ParseUint: parsing "NaN": invalid syntax`, "NaN-NaN/tcp": `invalid range format for --expose: NaN-NaN/tcp, error: strconv.ParseUint: parsing "NaN": invalid syntax`,
"8080-NaN/tcp": `invalid range format for --expose: 8080-NaN/tcp, error: strconv.ParseUint: parsing "NaN": invalid syntax`, "8080-NaN/tcp": `invalid range format for --expose: 8080-NaN/tcp, error: strconv.ParseUint: parsing "NaN": invalid syntax`,
@ -417,39 +423,91 @@ func TestParseWithExpose(t *testing.T) {
func TestParseDevice(t *testing.T) { func TestParseDevice(t *testing.T) {
skip.If(t, runtime.GOOS != "linux") // Windows and macOS validate server-side skip.If(t, runtime.GOOS != "linux") // Windows and macOS validate server-side
valids := map[string]container.DeviceMapping{ testCases := []struct {
"/dev/snd": { devices []string
deviceMapping *container.DeviceMapping
deviceRequests []container.DeviceRequest
}{
{
devices: []string{"/dev/snd"},
deviceMapping: &container.DeviceMapping{
PathOnHost: "/dev/snd", PathOnHost: "/dev/snd",
PathInContainer: "/dev/snd", PathInContainer: "/dev/snd",
CgroupPermissions: "rwm", CgroupPermissions: "rwm",
}, },
"/dev/snd:rw": { },
{
devices: []string{"/dev/snd:rw"},
deviceMapping: &container.DeviceMapping{
PathOnHost: "/dev/snd", PathOnHost: "/dev/snd",
PathInContainer: "/dev/snd", PathInContainer: "/dev/snd",
CgroupPermissions: "rw", CgroupPermissions: "rw",
}, },
"/dev/snd:/something": { },
{
devices: []string{"/dev/snd:/something"},
deviceMapping: &container.DeviceMapping{
PathOnHost: "/dev/snd", PathOnHost: "/dev/snd",
PathInContainer: "/something", PathInContainer: "/something",
CgroupPermissions: "rwm", CgroupPermissions: "rwm",
}, },
"/dev/snd:/something:rw": { },
{
devices: []string{"/dev/snd:/something:rw"},
deviceMapping: &container.DeviceMapping{
PathOnHost: "/dev/snd", PathOnHost: "/dev/snd",
PathInContainer: "/something", PathInContainer: "/something",
CgroupPermissions: "rw", CgroupPermissions: "rw",
}, },
},
{
devices: []string{"vendor.com/class=name"},
deviceMapping: nil,
deviceRequests: []container.DeviceRequest{
{
Driver: "cdi",
DeviceIDs: []string{"vendor.com/class=name"},
},
},
},
{
devices: []string{"vendor.com/class=name", "/dev/snd:/something:rw"},
deviceMapping: &container.DeviceMapping{
PathOnHost: "/dev/snd",
PathInContainer: "/something",
CgroupPermissions: "rw",
},
deviceRequests: []container.DeviceRequest{
{
Driver: "cdi",
DeviceIDs: []string{"vendor.com/class=name"},
},
},
},
} }
for device, deviceMapping := range valids {
_, hostconfig, _, err := parseRun([]string{fmt.Sprintf("--device=%v", device), "img", "cmd"}) for _, tc := range testCases {
if err != nil { t.Run(fmt.Sprintf("%s", tc.devices), func(t *testing.T) {
t.Fatal(err) var args []string
for _, d := range tc.devices {
args = append(args, fmt.Sprintf("--device=%v", d))
} }
if len(hostconfig.Devices) != 1 { args = append(args, "img", "cmd")
t.Fatalf("Expected 1 devices, got %v", hostconfig.Devices)
_, hostconfig, _, err := parseRun(args)
assert.NilError(t, err)
if tc.deviceMapping != nil {
if assert.Check(t, is.Len(hostconfig.Devices, 1)) {
assert.Check(t, is.DeepEqual(*tc.deviceMapping, hostconfig.Devices[0]))
} }
if hostconfig.Devices[0] != deviceMapping { } else {
t.Fatalf("Expected %v, got %v", deviceMapping, hostconfig.Devices) assert.Check(t, is.Len(hostconfig.Devices, 0))
} }
assert.Check(t, is.DeepEqual(tc.deviceRequests, hostconfig.DeviceRequests))
})
} }
} }
@ -458,20 +516,21 @@ func TestParseNetworkConfig(t *testing.T) {
name string name string
flags []string flags []string
expected map[string]*networktypes.EndpointSettings expected map[string]*networktypes.EndpointSettings
expectedCfg container.HostConfig expectedCfg container.Config
expectedHostCfg container.HostConfig
expectedErr string expectedErr string
}{ }{
{ {
name: "single-network-legacy", name: "single-network-legacy",
flags: []string{"--network", "net1"}, flags: []string{"--network", "net1"},
expected: map[string]*networktypes.EndpointSettings{}, expected: map[string]*networktypes.EndpointSettings{},
expectedCfg: container.HostConfig{NetworkMode: "net1"}, expectedHostCfg: container.HostConfig{NetworkMode: "net1"},
}, },
{ {
name: "single-network-advanced", name: "single-network-advanced",
flags: []string{"--network", "name=net1"}, flags: []string{"--network", "name=net1"},
expected: map[string]*networktypes.EndpointSettings{}, expected: map[string]*networktypes.EndpointSettings{},
expectedCfg: container.HostConfig{NetworkMode: "net1"}, expectedHostCfg: container.HostConfig{NetworkMode: "net1"},
}, },
{ {
name: "single-network-legacy-with-options", name: "single-network-legacy-with-options",
@ -497,7 +556,7 @@ func TestParseNetworkConfig(t *testing.T) {
Aliases: []string{"web1", "web2"}, Aliases: []string{"web1", "web2"},
}, },
}, },
expectedCfg: container.HostConfig{NetworkMode: "net1"}, expectedHostCfg: container.HostConfig{NetworkMode: "net1"},
}, },
{ {
name: "multiple-network-advanced-mixed", name: "multiple-network-advanced-mixed",
@ -513,6 +572,7 @@ func TestParseNetworkConfig(t *testing.T) {
"--network-alias", "web2", "--network-alias", "web2",
"--network", "net2", "--network", "net2",
"--network", "name=net3,alias=web3,driver-opt=field3=value3,ip=172.20.88.22,ip6=2001:db8::8822", "--network", "name=net3,alias=web3,driver-opt=field3=value3,ip=172.20.88.22,ip6=2001:db8::8822",
"--network", "name=net4,mac-address=02:32:1c:23:00:04,link-local-ip=169.254.169.254",
}, },
expected: map[string]*networktypes.EndpointSettings{ expected: map[string]*networktypes.EndpointSettings{
"net1": { "net1": {
@ -534,12 +594,18 @@ func TestParseNetworkConfig(t *testing.T) {
}, },
Aliases: []string{"web3"}, Aliases: []string{"web3"},
}, },
"net4": {
MacAddress: "02:32:1c:23:00:04",
IPAMConfig: &networktypes.EndpointIPAMConfig{
LinkLocalIPs: []string{"169.254.169.254"},
}, },
expectedCfg: container.HostConfig{NetworkMode: "net1"}, },
},
expectedHostCfg: container.HostConfig{NetworkMode: "net1"},
}, },
{ {
name: "single-network-advanced-with-options", name: "single-network-advanced-with-options",
flags: []string{"--network", "name=net1,alias=web1,alias=web2,driver-opt=field1=value1,driver-opt=field2=value2,ip=172.20.88.22,ip6=2001:db8::8822"}, flags: []string{"--network", "name=net1,alias=web1,alias=web2,driver-opt=field1=value1,driver-opt=field2=value2,ip=172.20.88.22,ip6=2001:db8::8822,mac-address=02:32:1c:23:00:04"},
expected: map[string]*networktypes.EndpointSettings{ expected: map[string]*networktypes.EndpointSettings{
"net1": { "net1": {
DriverOpts: map[string]string{ DriverOpts: map[string]string{
@ -551,15 +617,29 @@ func TestParseNetworkConfig(t *testing.T) {
IPv6Address: "2001:db8::8822", IPv6Address: "2001:db8::8822",
}, },
Aliases: []string{"web1", "web2"}, Aliases: []string{"web1", "web2"},
MacAddress: "02:32:1c:23:00:04",
}, },
}, },
expectedCfg: container.HostConfig{NetworkMode: "net1"}, expectedCfg: container.Config{MacAddress: "02:32:1c:23:00:04"},
expectedHostCfg: container.HostConfig{NetworkMode: "net1"},
}, },
{ {
name: "multiple-networks", name: "multiple-networks",
flags: []string{"--network", "net1", "--network", "name=net2"}, flags: []string{"--network", "net1", "--network", "name=net2"},
expected: map[string]*networktypes.EndpointSettings{"net1": {}, "net2": {}}, expected: map[string]*networktypes.EndpointSettings{"net1": {}, "net2": {}},
expectedCfg: container.HostConfig{NetworkMode: "net1"}, expectedHostCfg: container.HostConfig{NetworkMode: "net1"},
},
{
name: "advanced-options-with-standalone-mac-address-flag",
flags: []string{"--network=name=net1,alias=foobar", "--mac-address", "52:0f:f3:dc:50:10"},
expected: map[string]*networktypes.EndpointSettings{
"net1": {
Aliases: []string{"foobar"},
MacAddress: "52:0f:f3:dc:50:10",
},
},
expectedCfg: container.Config{MacAddress: "52:0f:f3:dc:50:10"},
expectedHostCfg: container.HostConfig{NetworkMode: "net1"},
}, },
{ {
name: "conflict-network", name: "conflict-network",
@ -586,11 +666,26 @@ func TestParseNetworkConfig(t *testing.T) {
flags: []string{"--network", "name=host", "--network", "net1"}, flags: []string{"--network", "name=host", "--network", "net1"},
expectedErr: `conflicting options: cannot attach both user-defined and non-user-defined network-modes`, expectedErr: `conflicting options: cannot attach both user-defined and non-user-defined network-modes`,
}, },
{
name: "conflict-options-link-local-ip",
flags: []string{"--network", "name=net1,link-local-ip=169.254.169.254", "--link-local-ip", "169.254.10.8"},
expectedErr: `conflicting options: cannot specify both --link-local-ip and per-network link-local IP addresses`,
},
{
name: "conflict-options-mac-address",
flags: []string{"--network", "name=net1,mac-address=02:32:1c:23:00:04", "--mac-address", "02:32:1c:23:00:04"},
expectedErr: `conflicting options: cannot specify both --mac-address and per-network MAC address`,
},
{
name: "invalid-mac-address",
flags: []string{"--network", "name=net1,mac-address=foobar"},
expectedErr: "foobar is not a valid mac address",
},
} }
for _, tc := range tests { for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
_, hConfig, nwConfig, err := parseRun(tc.flags) config, hConfig, nwConfig, err := parseRun(tc.flags)
if tc.expectedErr != "" { if tc.expectedErr != "" {
assert.Error(t, err, tc.expectedErr) assert.Error(t, err, tc.expectedErr)
@ -598,7 +693,8 @@ func TestParseNetworkConfig(t *testing.T) {
} }
assert.NilError(t, err) assert.NilError(t, err)
assert.DeepEqual(t, hConfig.NetworkMode, tc.expectedCfg.NetworkMode) assert.DeepEqual(t, config.MacAddress, tc.expectedCfg.MacAddress) //nolint:staticcheck // ignore SA1019: field is deprecated, but still used on API < v1.44.
assert.DeepEqual(t, hConfig.NetworkMode, tc.expectedHostCfg.NetworkMode)
assert.DeepEqual(t, nwConfig.EndpointsConfig, tc.expected) assert.DeepEqual(t, nwConfig.EndpointsConfig, tc.expected)
}) })
} }
@ -648,34 +744,75 @@ func TestRunFlagsParseShmSize(t *testing.T) {
} }
func TestParseRestartPolicy(t *testing.T) { func TestParseRestartPolicy(t *testing.T) {
invalids := map[string]string{ tests := []struct {
"always:2:3": "invalid restart policy format: maximum retry count must be an integer", input string
"on-failure:invalid": "invalid restart policy format: maximum retry count must be an integer", expected container.RestartPolicy
} expectedErr string
valids := map[string]container.RestartPolicy{ }{
"": {}, {
"always": { input: "",
Name: "always",
MaximumRetryCount: 0,
}, },
"on-failure:1": { {
Name: "on-failure", input: "no",
expected: container.RestartPolicy{
Name: container.RestartPolicyDisabled,
},
},
{
input: ":1",
expectedErr: "invalid restart policy format: no policy provided before colon",
},
{
input: "always",
expected: container.RestartPolicy{
Name: container.RestartPolicyAlways,
},
},
{
input: "always:1",
expected: container.RestartPolicy{
Name: container.RestartPolicyAlways,
MaximumRetryCount: 1, MaximumRetryCount: 1,
}, },
},
{
input: "always:2:3",
expectedErr: "invalid restart policy format: maximum retry count must be an integer",
},
{
input: "on-failure:1",
expected: container.RestartPolicy{
Name: container.RestartPolicyOnFailure,
MaximumRetryCount: 1,
},
},
{
input: "on-failure:invalid",
expectedErr: "invalid restart policy format: maximum retry count must be an integer",
},
{
input: "unless-stopped",
expected: container.RestartPolicy{
Name: container.RestartPolicyUnlessStopped,
},
},
{
input: "unless-stopped:invalid",
expectedErr: "invalid restart policy format: maximum retry count must be an integer",
},
} }
for restart, expectedError := range invalids { for _, tc := range tests {
if _, _, _, err := parseRun([]string{fmt.Sprintf("--restart=%s", restart), "img", "cmd"}); err == nil || err.Error() != expectedError { tc := tc
t.Fatalf("Expected an error with message '%v' for %v, got %v", expectedError, restart, err) t.Run(tc.input, func(t *testing.T) {
} _, hostConfig, _, err := parseRun([]string{"--restart=" + tc.input, "img", "cmd"})
} if tc.expectedErr != "" {
for restart, expected := range valids { assert.Check(t, is.Error(err, tc.expectedErr))
_, hostconfig, _, err := parseRun([]string{fmt.Sprintf("--restart=%v", restart), "img", "cmd"}) assert.Check(t, is.Nil(hostConfig))
if err != nil { } else {
t.Fatal(err) assert.NilError(t, err)
} assert.Check(t, is.DeepEqual(hostConfig.RestartPolicy, tc.expected))
if hostconfig.RestartPolicy != expected {
t.Fatalf("Expected %v, got %v", expected, hostconfig.RestartPolicy)
} }
})
} }
} }
@ -720,8 +857,8 @@ func TestParseHealth(t *testing.T) {
checkError("--no-healthcheck conflicts with --health-* options", checkError("--no-healthcheck conflicts with --health-* options",
"--no-healthcheck", "--health-cmd=/check.sh -q", "img", "cmd") "--no-healthcheck", "--health-cmd=/check.sh -q", "img", "cmd")
health = checkOk("--health-timeout=2s", "--health-retries=3", "--health-interval=4.5s", "--health-start-period=5s", "img", "cmd") health = checkOk("--health-timeout=2s", "--health-retries=3", "--health-interval=4.5s", "--health-start-period=5s", "--health-start-interval=1s", "img", "cmd")
if health.Timeout != 2*time.Second || health.Retries != 3 || health.Interval != 4500*time.Millisecond || health.StartPeriod != 5*time.Second { if health.Timeout != 2*time.Second || health.Retries != 3 || health.Interval != 4500*time.Millisecond || health.StartPeriod != 5*time.Second || health.StartInterval != 1*time.Second {
t.Fatalf("--health-*: got %#v", health) t.Fatalf("--health-*: got %#v", health)
} }
} }
@ -874,13 +1011,11 @@ func TestValidateDevice(t *testing.T) {
for path, expectedError := range invalid { for path, expectedError := range invalid {
if _, err := validateDevice(path, runtime.GOOS); err == nil { if _, err := validateDevice(path, runtime.GOOS); err == nil {
t.Fatalf("ValidateDevice(`%q`) should have failed validation", path) t.Fatalf("ValidateDevice(`%q`) should have failed validation", path)
} else { } else if err.Error() != expectedError {
if err.Error() != expectedError {
t.Fatalf("ValidateDevice(`%q`) error should contain %q, got %q", path, expectedError, err.Error()) t.Fatalf("ValidateDevice(`%q`) error should contain %q, got %q", path, expectedError, err.Error())
} }
} }
} }
}
func TestParseSystemPaths(t *testing.T) { func TestParseSystemPaths(t *testing.T) {
tests := []struct { tests := []struct {

View File

@ -27,7 +27,7 @@ func NewPauseCommand(dockerCli command.Cli) *cobra.Command {
Args: cli.RequiresMinArgs(1), Args: cli.RequiresMinArgs(1),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
opts.containers = args opts.containers = args
return runPause(dockerCli, &opts) return runPause(cmd.Context(), dockerCli, &opts)
}, },
Annotations: map[string]string{ Annotations: map[string]string{
"aliases": "docker container pause, docker pause", "aliases": "docker container pause, docker pause",
@ -38,9 +38,7 @@ func NewPauseCommand(dockerCli command.Cli) *cobra.Command {
} }
} }
func runPause(dockerCli command.Cli, opts *pauseOptions) error { func runPause(ctx context.Context, dockerCli command.Cli, opts *pauseOptions) error {
ctx := context.Background()
var errs []string var errs []string
errChan := parallelOperation(ctx, opts.containers, dockerCli.Client().ContainerPause) errChan := parallelOperation(ctx, opts.containers, dockerCli.Client().ContainerPause)
for _, container := range opts.containers { for _, container := range opts.containers {

View File

@ -36,7 +36,7 @@ func NewPortCommand(dockerCli command.Cli) *cobra.Command {
if len(args) > 1 { if len(args) > 1 {
opts.port = args[1] opts.port = args[1]
} }
return runPort(dockerCli, &opts) return runPort(cmd.Context(), dockerCli, &opts)
}, },
Annotations: map[string]string{ Annotations: map[string]string{
"aliases": "docker container port, docker port", "aliases": "docker container port, docker port",
@ -52,9 +52,7 @@ func NewPortCommand(dockerCli command.Cli) *cobra.Command {
// TODO(thaJeztah): currently this defaults to show the TCP port if no // TODO(thaJeztah): currently this defaults to show the TCP port if no
// proto is specified. We should consider changing this to "any" protocol // proto is specified. We should consider changing this to "any" protocol
// for the given private port. // for the given private port.
func runPort(dockerCli command.Cli, opts *portOptions) error { func runPort(ctx context.Context, dockerCli command.Cli, opts *portOptions) error {
ctx := context.Background()
c, err := dockerCli.Client().ContainerInspect(ctx, opts.container) c, err := dockerCli.Client().ContainerInspect(ctx, opts.container)
if err != nil { if err != nil {
return err return err

View File

@ -8,7 +8,9 @@ import (
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/command/completion"
"github.com/docker/cli/opts" "github.com/docker/cli/opts"
"github.com/docker/docker/errdefs"
units "github.com/docker/go-units" units "github.com/docker/go-units"
"github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -26,7 +28,7 @@ func NewPruneCommand(dockerCli command.Cli) *cobra.Command {
Short: "Remove all stopped containers", Short: "Remove all stopped containers",
Args: cli.NoArgs, Args: cli.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
spaceReclaimed, output, err := runPrune(dockerCli, options) spaceReclaimed, output, err := runPrune(cmd.Context(), dockerCli, options)
if err != nil { if err != nil {
return err return err
} }
@ -50,14 +52,20 @@ func NewPruneCommand(dockerCli command.Cli) *cobra.Command {
const warning = `WARNING! This will remove all stopped containers. const warning = `WARNING! This will remove all stopped containers.
Are you sure you want to continue?` Are you sure you want to continue?`
func runPrune(dockerCli command.Cli, options pruneOptions) (spaceReclaimed uint64, output string, err error) { func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions) (spaceReclaimed uint64, output string, err error) {
pruneFilters := command.PruneFilters(dockerCli, options.filter.Value()) pruneFilters := command.PruneFilters(dockerCli, options.filter.Value())
if !options.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), warning) { if !options.force {
return 0, "", nil r, err := command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), warning)
if err != nil {
return 0, "", err
}
if !r {
return 0, "", errdefs.Cancelled(errors.New("container prune has been cancelled"))
}
} }
report, err := dockerCli.Client().ContainersPrune(context.Background(), pruneFilters) report, err := dockerCli.Client().ContainersPrune(ctx, pruneFilters)
if err != nil { if err != nil {
return 0, "", err return 0, "", err
} }
@ -75,6 +83,6 @@ func runPrune(dockerCli command.Cli, options pruneOptions) (spaceReclaimed uint6
// RunPrune calls the Container Prune API // RunPrune calls the Container Prune API
// This returns the amount of space reclaimed and a detailed output string // This returns the amount of space reclaimed and a detailed output string
func RunPrune(dockerCli command.Cli, _ bool, filter opts.FilterOpt) (uint64, string, error) { func RunPrune(ctx context.Context, dockerCli command.Cli, _ bool, filter opts.FilterOpt) (uint64, string, error) {
return runPrune(dockerCli, pruneOptions{force: true, filter: filter}) return runPrune(ctx, dockerCli, pruneOptions{force: true, filter: filter})
} }

View File

@ -0,0 +1,24 @@
package container
import (
"context"
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/pkg/errors"
)
func TestContainerPrunePromptTermination(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
cli := test.NewFakeCli(&fakeClient{
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)
test.TerminatePrompt(ctx, t, cmd, cli)
}

View File

@ -28,7 +28,7 @@ func NewRenameCommand(dockerCli command.Cli) *cobra.Command {
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
opts.oldName = args[0] opts.oldName = args[0]
opts.newName = args[1] opts.newName = args[1]
return runRename(dockerCli, &opts) return runRename(cmd.Context(), dockerCli, &opts)
}, },
Annotations: map[string]string{ Annotations: map[string]string{
"aliases": "docker container rename, docker rename", "aliases": "docker container rename, docker rename",
@ -38,9 +38,7 @@ func NewRenameCommand(dockerCli command.Cli) *cobra.Command {
return cmd return cmd
} }
func runRename(dockerCli command.Cli, opts *renameOptions) error { func runRename(ctx context.Context, dockerCli command.Cli, opts *renameOptions) error {
ctx := context.Background()
oldName := strings.TrimSpace(opts.oldName) oldName := strings.TrimSpace(opts.oldName)
newName := strings.TrimSpace(opts.newName) newName := strings.TrimSpace(opts.newName)

View File

@ -32,7 +32,7 @@ func NewRestartCommand(dockerCli command.Cli) *cobra.Command {
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
opts.containers = args opts.containers = args
opts.timeoutChanged = cmd.Flags().Changed("time") opts.timeoutChanged = cmd.Flags().Changed("time")
return runRestart(dockerCli, &opts) return runRestart(cmd.Context(), dockerCli, &opts)
}, },
Annotations: map[string]string{ Annotations: map[string]string{
"aliases": "docker container restart, docker restart", "aliases": "docker container restart, docker restart",
@ -46,8 +46,7 @@ func NewRestartCommand(dockerCli command.Cli) *cobra.Command {
return cmd return cmd
} }
func runRestart(dockerCli command.Cli, opts *restartOptions) error { func runRestart(ctx context.Context, dockerCli command.Cli, opts *restartOptions) error {
ctx := context.Background()
var errs []string var errs []string
var timeout *int var timeout *int
if opts.timeoutChanged { if opts.timeoutChanged {

View File

@ -8,7 +8,7 @@ import (
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/command/completion"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container"
"github.com/docker/docker/errdefs" "github.com/docker/docker/errdefs"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -33,7 +33,7 @@ func NewRmCommand(dockerCli command.Cli) *cobra.Command {
Args: cli.RequiresMinArgs(1), Args: cli.RequiresMinArgs(1),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
opts.containers = args opts.containers = args
return runRm(dockerCli, &opts) return runRm(cmd.Context(), dockerCli, &opts)
}, },
Annotations: map[string]string{ Annotations: map[string]string{
"aliases": "docker container rm, docker container remove, docker rm", "aliases": "docker container rm, docker container remove, docker rm",
@ -48,22 +48,18 @@ func NewRmCommand(dockerCli command.Cli) *cobra.Command {
return cmd return cmd
} }
func runRm(dockerCli command.Cli, opts *rmOptions) error { func runRm(ctx context.Context, dockerCli command.Cli, opts *rmOptions) error {
ctx := context.Background()
var errs []string var errs []string
options := types.ContainerRemoveOptions{ errChan := parallelOperation(ctx, opts.containers, func(ctx context.Context, ctrID string) error {
ctrID = strings.Trim(ctrID, "/")
if ctrID == "" {
return errors.New("Container name cannot be empty")
}
return dockerCli.Client().ContainerRemove(ctx, ctrID, container.RemoveOptions{
RemoveVolumes: opts.rmVolumes, RemoveVolumes: opts.rmVolumes,
RemoveLinks: opts.rmLink, RemoveLinks: opts.rmLink,
Force: opts.force, Force: opts.force,
} })
errChan := parallelOperation(ctx, opts.containers, func(ctx context.Context, container string) error {
container = strings.Trim(container, "/")
if container == "" {
return errors.New("Container name cannot be empty")
}
return dockerCli.Client().ContainerRemove(ctx, container, options)
}) })
for _, name := range opts.containers { for _, name := range opts.containers {

View File

@ -9,7 +9,7 @@ import (
"testing" "testing"
"github.com/docker/cli/internal/test" "github.com/docker/cli/internal/test"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container"
"github.com/docker/docker/errdefs" "github.com/docker/docker/errdefs"
"gotest.tools/v3/assert" "gotest.tools/v3/assert"
) )
@ -29,7 +29,7 @@ func TestRemoveForce(t *testing.T) {
mutex := new(sync.Mutex) mutex := new(sync.Mutex)
cli := test.NewFakeCli(&fakeClient{ cli := test.NewFakeCli(&fakeClient{
containerRemoveFunc: func(ctx context.Context, container string, options types.ContainerRemoveOptions) error { containerRemoveFunc: func(ctx context.Context, container string, options container.RemoveOptions) error {
// containerRemoveFunc is called in parallel for each container // containerRemoveFunc is called in parallel for each container
// by the remove command so append must be synchronized. // by the remove command so append must be synchronized.
mutex.Lock() mutex.Lock()

View File

@ -12,7 +12,6 @@ import (
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/command/completion"
"github.com/docker/cli/opts" "github.com/docker/cli/opts"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
"github.com/moby/sys/signal" "github.com/moby/sys/signal"
"github.com/moby/term" "github.com/moby/term"
@ -43,7 +42,7 @@ func NewRunCommand(dockerCli command.Cli) *cobra.Command {
if len(args) > 1 { if len(args) > 1 {
copts.Args = args[1:] copts.Args = args[1:]
} }
return runRun(dockerCli, cmd.Flags(), &options, copts) return runRun(cmd.Context(), dockerCli, cmd.Flags(), &options, copts)
}, },
ValidArgsFunction: completion.ImageNames(dockerCli), ValidArgsFunction: completion.ImageNames(dockerCli),
Annotations: map[string]string{ Annotations: map[string]string{
@ -90,7 +89,7 @@ func NewRunCommand(dockerCli command.Cli) *cobra.Command {
return cmd return cmd
} }
func runRun(dockerCli command.Cli, flags *pflag.FlagSet, ropts *runOptions, copts *containerOptions) error { func runRun(ctx context.Context, dockerCli command.Cli, flags *pflag.FlagSet, ropts *runOptions, copts *containerOptions) error {
if err := validatePullOpt(ropts.pull); err != nil { if err := validatePullOpt(ropts.pull); err != nil {
reportError(dockerCli.Err(), "run", err.Error(), true) reportError(dockerCli.Err(), "run", err.Error(), true)
return cli.StatusError{StatusCode: 125} return cli.StatusError{StatusCode: 125}
@ -115,18 +114,18 @@ func runRun(dockerCli command.Cli, flags *pflag.FlagSet, ropts *runOptions, copt
reportError(dockerCli.Err(), "run", err.Error(), true) reportError(dockerCli.Err(), "run", err.Error(), true)
return cli.StatusError{StatusCode: 125} return cli.StatusError{StatusCode: 125}
} }
return runContainer(dockerCli, ropts, copts, containerCfg) return runContainer(ctx, dockerCli, ropts, copts, containerCfg)
} }
//nolint:gocyclo //nolint:gocyclo
func runContainer(dockerCli command.Cli, opts *runOptions, copts *containerOptions, containerCfg *containerConfig) error { func runContainer(ctx context.Context, dockerCli command.Cli, runOpts *runOptions, copts *containerOptions, containerCfg *containerConfig) error {
config := containerCfg.Config config := containerCfg.Config
stdout, stderr := dockerCli.Out(), dockerCli.Err() stdout, stderr := dockerCli.Out(), dockerCli.Err()
client := dockerCli.Client() apiClient := dockerCli.Client()
config.ArgsEscaped = false config.ArgsEscaped = false
if !opts.detach { if !runOpts.detach {
if err := dockerCli.In().CheckTty(config.AttachStdin, config.Tty); err != nil { if err := dockerCli.In().CheckTty(config.AttachStdin, config.Tty); err != nil {
return err return err
} }
@ -141,17 +140,17 @@ func runContainer(dockerCli command.Cli, opts *runOptions, copts *containerOptio
config.StdinOnce = false config.StdinOnce = false
} }
ctx, cancelFun := context.WithCancel(context.Background()) ctx, cancelFun := context.WithCancel(ctx)
defer cancelFun() defer cancelFun()
containerID, err := createContainer(ctx, dockerCli, containerCfg, &opts.createOptions) containerID, err := createContainer(ctx, dockerCli, containerCfg, &runOpts.createOptions)
if err != nil { if err != nil {
reportError(stderr, "run", err.Error(), true) reportError(stderr, "run", err.Error(), true)
return runStartContainerErr(err) return runStartContainerErr(err)
} }
if opts.sigProxy { if runOpts.sigProxy {
sigc := notifyAllSignals() sigc := notifyAllSignals()
go ForwardAllSignals(ctx, dockerCli, containerID, sigc) go ForwardAllSignals(ctx, apiClient, containerID, sigc)
defer signal.StopCatch(sigc) defer signal.StopCatch(sigc)
} }
@ -170,11 +169,11 @@ func runContainer(dockerCli command.Cli, opts *runOptions, copts *containerOptio
attach := config.AttachStdin || config.AttachStdout || config.AttachStderr attach := config.AttachStdin || config.AttachStdout || config.AttachStderr
if attach { if attach {
detachKeys := dockerCli.ConfigFile().DetachKeys detachKeys := dockerCli.ConfigFile().DetachKeys
if opts.detachKeys != "" { if runOpts.detachKeys != "" {
detachKeys = opts.detachKeys detachKeys = runOpts.detachKeys
} }
closeFn, err := attachContainer(ctx, dockerCli, containerID, &errCh, config, types.ContainerAttachOptions{ closeFn, err := attachContainer(ctx, dockerCli, containerID, &errCh, config, container.AttachOptions{
Stream: true, Stream: true,
Stdin: config.AttachStdin, Stdin: config.AttachStdin,
Stdout: config.AttachStdout, Stdout: config.AttachStdout,
@ -187,10 +186,10 @@ func runContainer(dockerCli command.Cli, opts *runOptions, copts *containerOptio
defer closeFn() defer closeFn()
} }
statusChan := waitExitOrRemoved(ctx, dockerCli, containerID, copts.autoRemove) statusChan := waitExitOrRemoved(ctx, apiClient, containerID, copts.autoRemove)
// start the container // start the container
if err := client.ContainerStart(ctx, containerID, types.ContainerStartOptions{}); err != nil { if err := apiClient.ContainerStart(ctx, containerID, container.StartOptions{}); err != nil {
// If we have hijackedIOStreamer, we should notify // If we have hijackedIOStreamer, we should notify
// hijackedIOStreamer we are going to exit and wait // hijackedIOStreamer we are going to exit and wait
// to avoid the terminal are not restored. // to avoid the terminal are not restored.
@ -239,7 +238,7 @@ func runContainer(dockerCli command.Cli, opts *runOptions, copts *containerOptio
return nil return nil
} }
func attachContainer(ctx context.Context, dockerCli command.Cli, containerID string, errCh *chan error, config *container.Config, options types.ContainerAttachOptions) (func(), error) { func attachContainer(ctx context.Context, dockerCli command.Cli, containerID string, errCh *chan error, config *container.Config, options container.AttachOptions) (func(), error) {
resp, errAttach := dockerCli.Client().ContainerAttach(ctx, containerID, options) resp, errAttach := dockerCli.Client().ContainerAttach(ctx, containerID, options)
if errAttach != nil { if errAttach != nil {
return nil, errAttach return nil, errAttach

View File

@ -1,6 +1,7 @@
package container package container
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
"io" "io"
@ -18,7 +19,7 @@ import (
) )
func TestRunLabel(t *testing.T) { func TestRunLabel(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{ fakeCLI := test.NewFakeCli(&fakeClient{
createContainerFunc: func(_ *container.Config, _ *container.HostConfig, _ *network.NetworkingConfig, _ *specs.Platform, _ string) (container.CreateResponse, error) { createContainerFunc: func(_ *container.Config, _ *container.HostConfig, _ *network.NetworkingConfig, _ *specs.Platform, _ string) (container.CreateResponse, error) {
return container.CreateResponse{ return container.CreateResponse{
ID: "id", ID: "id",
@ -26,7 +27,7 @@ func TestRunLabel(t *testing.T) {
}, },
Version: "1.36", Version: "1.36",
}) })
cmd := NewRunCommand(cli) cmd := NewRunCommand(fakeCLI)
cmd.SetArgs([]string{"--detach=true", "--label", "foo", "busybox"}) cmd.SetArgs([]string{"--detach=true", "--label", "foo", "busybox"})
assert.NilError(t, cmd.Execute()) assert.NilError(t, cmd.Execute())
} }
@ -58,7 +59,7 @@ func TestRunCommandWithContentTrustErrors(t *testing.T) {
}, },
} }
for _, tc := range testCases { for _, tc := range testCases {
cli := test.NewFakeCli(&fakeClient{ fakeCLI := test.NewFakeCli(&fakeClient{
createContainerFunc: func(config *container.Config, createContainerFunc: func(config *container.Config,
hostConfig *container.HostConfig, hostConfig *container.HostConfig,
networkingConfig *network.NetworkingConfig, networkingConfig *network.NetworkingConfig,
@ -68,13 +69,13 @@ func TestRunCommandWithContentTrustErrors(t *testing.T) {
return container.CreateResponse{}, fmt.Errorf("shouldn't try to pull image") return container.CreateResponse{}, fmt.Errorf("shouldn't try to pull image")
}, },
}, test.EnableContentTrust) }, test.EnableContentTrust)
cli.SetNotaryClient(tc.notaryFunc) fakeCLI.SetNotaryClient(tc.notaryFunc)
cmd := NewRunCommand(cli) cmd := NewRunCommand(fakeCLI)
cmd.SetArgs(tc.args) cmd.SetArgs(tc.args)
cmd.SetOut(io.Discard) cmd.SetOut(io.Discard)
err := cmd.Execute() err := cmd.Execute()
assert.Assert(t, err != nil) assert.Assert(t, err != nil)
assert.Assert(t, is.Contains(cli.ErrBuffer().String(), tc.expectedError)) assert.Assert(t, is.Contains(fakeCLI.ErrBuffer().String(), tc.expectedError))
} }
} }
@ -97,6 +98,7 @@ func TestRunContainerImagePullPolicyInvalid(t *testing.T) {
t.Run(tc.PullPolicy, func(t *testing.T) { t.Run(tc.PullPolicy, func(t *testing.T) {
dockerCli := test.NewFakeCli(&fakeClient{}) dockerCli := test.NewFakeCli(&fakeClient{})
err := runRun( err := runRun(
context.TODO(),
dockerCli, dockerCli,
&pflag.FlagSet{}, &pflag.FlagSet{},
&runOptions{createOptions: createOptions{pull: tc.PullPolicy}}, &runOptions{createOptions: createOptions{pull: tc.PullPolicy}},

View File

@ -5,7 +5,7 @@ import (
"os" "os"
gosignal "os/signal" gosignal "os/signal"
"github.com/docker/cli/cli/command" "github.com/docker/docker/client"
"github.com/moby/sys/signal" "github.com/moby/sys/signal"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
@ -13,7 +13,7 @@ import (
// ForwardAllSignals forwards signals to the container // ForwardAllSignals forwards signals to the container
// //
// The channel you pass in must already be setup to receive any signals you want to forward. // The channel you pass in must already be setup to receive any signals you want to forward.
func ForwardAllSignals(ctx context.Context, cli command.Cli, cid string, sigc <-chan os.Signal) { func ForwardAllSignals(ctx context.Context, apiClient client.ContainerAPIClient, cid string, sigc <-chan os.Signal) {
var ( var (
s os.Signal s os.Signal
ok bool ok bool
@ -48,7 +48,7 @@ func ForwardAllSignals(ctx context.Context, cli command.Cli, cid string, sigc <-
continue continue
} }
if err := cli.Client().ContainerKill(ctx, cid, sig); err != nil { if err := apiClient.ContainerKill(ctx, cid, sig); err != nil {
logrus.Debugf("Error sending signal: %s", err) logrus.Debugf("Error sending signal: %s", err)
} }
} }

View File

@ -6,7 +6,6 @@ import (
"testing" "testing"
"time" "time"
"github.com/docker/cli/internal/test"
"github.com/moby/sys/signal" "github.com/moby/sys/signal"
) )
@ -15,16 +14,15 @@ func TestForwardSignals(t *testing.T) {
defer cancel() defer cancel()
called := make(chan struct{}) called := make(chan struct{})
client := &fakeClient{containerKillFunc: func(ctx context.Context, container, signal string) error { apiClient := &fakeClient{containerKillFunc: func(ctx context.Context, container, signal string) error {
close(called) close(called)
return nil return nil
}} }}
cli := test.NewFakeCli(client)
sigc := make(chan os.Signal) sigc := make(chan os.Signal)
defer close(sigc) defer close(sigc)
go ForwardAllSignals(ctx, cli, t.Name(), sigc) go ForwardAllSignals(ctx, apiClient, t.Name(), sigc)
timer := time.NewTimer(30 * time.Second) timer := time.NewTimer(30 * time.Second)
defer timer.Stop() defer timer.Stop()

View File

@ -9,7 +9,6 @@ import (
"testing" "testing"
"time" "time"
"github.com/docker/cli/internal/test"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
"gotest.tools/v3/assert" "gotest.tools/v3/assert"
) )
@ -23,18 +22,17 @@ func TestIgnoredSignals(t *testing.T) {
defer cancel() defer cancel()
var called bool var called bool
client := &fakeClient{containerKillFunc: func(ctx context.Context, container, signal string) error { apiClient := &fakeClient{containerKillFunc: func(ctx context.Context, container, signal string) error {
called = true called = true
return nil return nil
}} }}
cli := test.NewFakeCli(client)
sigc := make(chan os.Signal) sigc := make(chan os.Signal)
defer close(sigc) defer close(sigc)
done := make(chan struct{}) done := make(chan struct{})
go func() { go func() {
ForwardAllSignals(ctx, cli, t.Name(), sigc) ForwardAllSignals(ctx, apiClient, t.Name(), sigc)
close(done) close(done)
}() }()

View File

@ -10,6 +10,7 @@ import (
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/command/completion"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/moby/sys/signal" "github.com/moby/sys/signal"
"github.com/moby/term" "github.com/moby/term"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -27,11 +28,6 @@ type StartOptions struct {
Containers []string Containers []string
} }
// NewStartOptions creates a new StartOptions
func NewStartOptions() StartOptions {
return StartOptions{}
}
// NewStartCommand creates a new cobra.Command for `docker start` // NewStartCommand creates a new cobra.Command for `docker start`
func NewStartCommand(dockerCli command.Cli) *cobra.Command { func NewStartCommand(dockerCli command.Cli) *cobra.Command {
var opts StartOptions var opts StartOptions
@ -42,7 +38,7 @@ func NewStartCommand(dockerCli command.Cli) *cobra.Command {
Args: cli.RequiresMinArgs(1), Args: cli.RequiresMinArgs(1),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
opts.Containers = args opts.Containers = args
return RunStart(dockerCli, &opts) return RunStart(cmd.Context(), dockerCli, &opts)
}, },
Annotations: map[string]string{ Annotations: map[string]string{
"aliases": "docker container start, docker start", "aliases": "docker container start, docker start",
@ -69,11 +65,12 @@ func NewStartCommand(dockerCli command.Cli) *cobra.Command {
// RunStart executes a `start` command // RunStart executes a `start` command
// //
//nolint:gocyclo //nolint:gocyclo
func RunStart(dockerCli command.Cli, opts *StartOptions) error { func RunStart(ctx context.Context, dockerCli command.Cli, opts *StartOptions) error {
ctx, cancelFun := context.WithCancel(context.Background()) ctx, cancelFun := context.WithCancel(ctx)
defer cancelFun() defer cancelFun()
if opts.Attach || opts.OpenStdin { switch {
case opts.Attach || opts.OpenStdin:
// We're going to attach to a container. // We're going to attach to a container.
// 1. Ensure we only have one container. // 1. Ensure we only have one container.
if len(opts.Containers) > 1 { if len(opts.Containers) > 1 {
@ -81,8 +78,8 @@ func RunStart(dockerCli command.Cli, opts *StartOptions) error {
} }
// 2. Attach to the container. // 2. Attach to the container.
container := opts.Containers[0] ctr := opts.Containers[0]
c, err := dockerCli.Client().ContainerInspect(ctx, container) c, err := dockerCli.Client().ContainerInspect(ctx, ctr)
if err != nil { if err != nil {
return err return err
} }
@ -90,7 +87,7 @@ func RunStart(dockerCli command.Cli, opts *StartOptions) error {
// We always use c.ID instead of container to maintain consistency during `docker start` // We always use c.ID instead of container to maintain consistency during `docker start`
if !c.Config.Tty { if !c.Config.Tty {
sigc := notifyAllSignals() sigc := notifyAllSignals()
go ForwardAllSignals(ctx, dockerCli, c.ID, sigc) go ForwardAllSignals(ctx, dockerCli.Client(), c.ID, sigc)
defer signal.StopCatch(sigc) defer signal.StopCatch(sigc)
} }
@ -99,7 +96,7 @@ func RunStart(dockerCli command.Cli, opts *StartOptions) error {
detachKeys = opts.DetachKeys detachKeys = opts.DetachKeys
} }
options := types.ContainerAttachOptions{ options := container.AttachOptions{
Stream: true, Stream: true,
Stdin: opts.OpenStdin && c.Config.OpenStdin, Stdin: opts.OpenStdin && c.Config.OpenStdin,
Stdout: true, Stdout: true,
@ -143,14 +140,14 @@ func RunStart(dockerCli command.Cli, opts *StartOptions) error {
// 3. We should open a channel for receiving status code of the container // 3. We should open a channel for receiving status code of the container
// no matter it's detached, removed on daemon side(--rm) or exit normally. // no matter it's detached, removed on daemon side(--rm) or exit normally.
statusChan := waitExitOrRemoved(ctx, dockerCli, c.ID, c.HostConfig.AutoRemove) statusChan := waitExitOrRemoved(ctx, dockerCli.Client(), c.ID, c.HostConfig.AutoRemove)
startOptions := types.ContainerStartOptions{
CheckpointID: opts.Checkpoint,
CheckpointDir: opts.CheckpointDir,
}
// 4. Start the container. // 4. Start the container.
if err := dockerCli.Client().ContainerStart(ctx, c.ID, startOptions); err != nil { err = dockerCli.Client().ContainerStart(ctx, c.ID, container.StartOptions{
CheckpointID: opts.Checkpoint,
CheckpointDir: opts.CheckpointDir,
})
if err != nil {
cancelFun() cancelFun()
<-cErr <-cErr
if c.HostConfig.AutoRemove { if c.HostConfig.AutoRemove {
@ -177,35 +174,32 @@ func RunStart(dockerCli command.Cli, opts *StartOptions) error {
if status := <-statusChan; status != 0 { if status := <-statusChan; status != 0 {
return cli.StatusError{StatusCode: status} return cli.StatusError{StatusCode: status}
} }
} else if opts.Checkpoint != "" { return nil
case opts.Checkpoint != "":
if len(opts.Containers) > 1 { if len(opts.Containers) > 1 {
return errors.New("you cannot restore multiple containers at once") return errors.New("you cannot restore multiple containers at once")
} }
container := opts.Containers[0] ctr := opts.Containers[0]
startOptions := types.ContainerStartOptions{ return dockerCli.Client().ContainerStart(ctx, ctr, container.StartOptions{
CheckpointID: opts.Checkpoint, CheckpointID: opts.Checkpoint,
CheckpointDir: opts.CheckpointDir, CheckpointDir: opts.CheckpointDir,
} })
return dockerCli.Client().ContainerStart(ctx, container, startOptions) default:
} else {
// We're not going to attach to anything. // We're not going to attach to anything.
// Start as many containers as we want. // Start as many containers as we want.
return startContainersWithoutAttachments(ctx, dockerCli, opts.Containers) return startContainersWithoutAttachments(ctx, dockerCli, opts.Containers)
} }
return nil
} }
func startContainersWithoutAttachments(ctx context.Context, dockerCli command.Cli, containers []string) error { func startContainersWithoutAttachments(ctx context.Context, dockerCli command.Cli, containers []string) error {
var failedContainers []string var failedContainers []string
for _, container := range containers { for _, ctr := range containers {
if err := dockerCli.Client().ContainerStart(ctx, container, types.ContainerStartOptions{}); err != nil { if err := dockerCli.Client().ContainerStart(ctx, ctr, container.StartOptions{}); err != nil {
fmt.Fprintln(dockerCli.Err(), err) fmt.Fprintln(dockerCli.Err(), err)
failedContainers = append(failedContainers, container) failedContainers = append(failedContainers, ctr)
continue continue
} }
fmt.Fprintln(dockerCli.Out(), container) fmt.Fprintln(dockerCli.Out(), ctr)
} }
if len(failedContainers) > 0 { if len(failedContainers) > 0 {

View File

@ -14,68 +14,161 @@ import (
"github.com/docker/cli/cli/command/formatter" "github.com/docker/cli/cli/command/formatter"
flagsHelper "github.com/docker/cli/cli/flags" flagsHelper "github.com/docker/cli/cli/flags"
"github.com/docker/docker/api/types" "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/events"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
type statsOptions struct { // StatsOptions defines options for [RunStats].
all bool type StatsOptions struct {
noStream bool // All allows including both running and stopped containers. The default
noTrunc bool // is to only include running containers.
format string All bool
containers []string
// NoStream disables streaming stats. If enabled, stats are collected once,
// and the result is printed.
NoStream bool
// NoTrunc disables truncating the output. The default is to truncate
// output such as container-IDs.
NoTrunc bool
// Format is a custom template to use for presenting the stats.
// Refer to [flagsHelper.FormatHelp] for accepted formats.
Format string
// Containers is the list of container names or IDs to include in the stats.
// If empty, all containers are included. It is mutually exclusive with the
// Filters option, and an error is produced if both are set.
Containers []string
// Filters provides optional filters to filter the list of containers and their
// associated container-events to include in the stats if no list of containers
// is set. If no filter is provided, all containers are included. Filters and
// Containers are currently mutually exclusive, and setting both options
// produces an error.
//
// These filters are used both to collect the initial list of containers and
// to refresh the list of containers based on container-events, accepted
// filters are limited to the intersection of filters accepted by "events"
// and "container list".
//
// Currently only "label" / "label=value" filters are accepted. Additional
// filter options may be added in future (within the constraints described
// above), but may require daemon-side validation as the list of accepted
// filters can differ between daemon- and API versions.
Filters *filters.Args
} }
// NewStatsCommand creates a new cobra.Command for `docker stats` // NewStatsCommand creates a new [cobra.Command] for "docker stats".
func NewStatsCommand(dockerCli command.Cli) *cobra.Command { func NewStatsCommand(dockerCLI command.Cli) *cobra.Command {
var opts statsOptions options := StatsOptions{}
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "stats [OPTIONS] [CONTAINER...]", Use: "stats [OPTIONS] [CONTAINER...]",
Short: "Display a live stream of container(s) resource usage statistics", Short: "Display a live stream of container(s) resource usage statistics",
Args: cli.RequiresMinArgs(0), Args: cli.RequiresMinArgs(0),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
opts.containers = args options.Containers = args
return runStats(dockerCli, &opts) return RunStats(cmd.Context(), dockerCLI, &options)
}, },
Annotations: map[string]string{ Annotations: map[string]string{
"aliases": "docker container stats, docker stats", "aliases": "docker container stats, docker stats",
}, },
ValidArgsFunction: completion.ContainerNames(dockerCli, false), ValidArgsFunction: completion.ContainerNames(dockerCLI, false),
} }
flags := cmd.Flags() flags := cmd.Flags()
flags.BoolVarP(&opts.all, "all", "a", false, "Show all containers (default shows just running)") flags.BoolVarP(&options.All, "all", "a", false, "Show all containers (default shows just running)")
flags.BoolVar(&opts.noStream, "no-stream", false, "Disable streaming stats and only pull the first result") flags.BoolVar(&options.NoStream, "no-stream", false, "Disable streaming stats and only pull the first result")
flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Do not truncate output") flags.BoolVar(&options.NoTrunc, "no-trunc", false, "Do not truncate output")
flags.StringVar(&opts.format, "format", "", flagsHelper.FormatHelp) flags.StringVar(&options.Format, "format", "", flagsHelper.FormatHelp)
return cmd return cmd
} }
// runStats displays a live stream of resource usage statistics for one or more containers. // acceptedStatsFilters is the list of filters accepted by [RunStats] (through
// the [StatsOptions.Filters] option).
//
// TODO(thaJeztah): don't hard-code the list of accept filters, and expand
// to the intersection of filters accepted by both "container list" and
// "system events". Validating filters may require an initial API call
// to both endpoints ("container list" and "system events").
var acceptedStatsFilters = map[string]bool{
"label": true,
}
// RunStats displays a live stream of resource usage statistics for one or more containers.
// This shows real-time information on CPU usage, memory usage, and network I/O. // This shows real-time information on CPU usage, memory usage, and network I/O.
// //
//nolint:gocyclo //nolint:gocyclo
func runStats(dockerCli command.Cli, opts *statsOptions) error { func RunStats(ctx context.Context, dockerCLI command.Cli, options *StatsOptions) error {
showAll := len(opts.containers) == 0 apiClient := dockerCLI.Client()
closeChan := make(chan error)
ctx := context.Background() // waitFirst is a WaitGroup to wait first stat data's reach for each container
waitFirst := &sync.WaitGroup{}
// closeChan is a non-buffered channel used to collect errors from goroutines.
closeChan := make(chan error)
cStats := stats{}
showAll := len(options.Containers) == 0
if showAll {
// If no names were specified, start a long-running goroutine which
// monitors container events. We make sure we're subscribed before
// retrieving the list of running containers to avoid a race where we
// would "miss" a creation.
started := make(chan struct{})
if options.Filters == nil {
f := filters.NewArgs()
options.Filters = &f
}
if err := options.Filters.Validate(acceptedStatsFilters); err != nil {
return err
}
eh := newEventHandler()
if options.All {
eh.setHandler(events.ActionCreate, func(e events.Message) {
s := NewStats(e.Actor.ID[:12])
if cStats.add(s) {
waitFirst.Add(1)
go collect(ctx, s, apiClient, !options.NoStream, waitFirst)
}
})
}
eh.setHandler(events.ActionStart, func(e events.Message) {
s := NewStats(e.Actor.ID[:12])
if cStats.add(s) {
waitFirst.Add(1)
go collect(ctx, s, apiClient, !options.NoStream, waitFirst)
}
})
if !options.All {
eh.setHandler(events.ActionDie, func(e events.Message) {
cStats.remove(e.Actor.ID[:12])
})
}
// monitorContainerEvents watches for container creation and removal (only // monitorContainerEvents watches for container creation and removal (only
// used when calling `docker stats` without arguments). // used when calling `docker stats` without arguments).
monitorContainerEvents := func(started chan<- struct{}, c chan events.Message, stopped <-chan struct{}) { monitorContainerEvents := func(started chan<- struct{}, c chan events.Message, stopped <-chan struct{}) {
f := filters.NewArgs() // Create a copy of the custom filters so that we don't mutate
f.Add("type", "container") // the original set of filters. Custom filters are used both
options := types.EventsOptions{ // to list containers and to filter events, but the "type" filter
// is not valid for filtering containers.
f := options.Filters.Clone()
f.Add("type", string(events.ContainerEventType))
eventChan, errChan := apiClient.Events(ctx, types.EventsOptions{
Filters: f, Filters: f,
} })
eventq, errq := dockerCli.Client().Events(ctx, options) // Whether we successfully subscribed to eventChan or not, we can now
// Whether we successfully subscribed to eventq or not, we can now
// unblock the main goroutine. // unblock the main goroutine.
close(started) close(started)
defer close(c) defer close(c)
@ -84,100 +177,58 @@ func runStats(dockerCli command.Cli, opts *statsOptions) error {
select { select {
case <-stopped: case <-stopped:
return return
case event := <-eventq: case event := <-eventChan:
c <- event c <- event
case err := <-errq: case err := <-errChan:
closeChan <- err closeChan <- err
return return
} }
} }
} }
// Get the daemonOSType if not set already
if daemonOSType == "" {
svctx := context.Background()
sv, err := dockerCli.Client().ServerVersion(svctx)
if err != nil {
return err
}
daemonOSType = sv.Os
}
// waitFirst is a WaitGroup to wait first stat data's reach for each container
waitFirst := &sync.WaitGroup{}
cStats := stats{}
// getContainerList simulates creation event for all previously existing
// containers (only used when calling `docker stats` without arguments).
getContainerList := func() {
options := types.ContainerListOptions{
All: opts.all,
}
cs, err := dockerCli.Client().ContainerList(ctx, options)
if err != nil {
closeChan <- err
}
for _, container := range cs {
s := NewStats(container.ID[:12])
if cStats.add(s) {
waitFirst.Add(1)
go collect(ctx, s, dockerCli.Client(), !opts.noStream, waitFirst)
}
}
}
if showAll {
// If no names were specified, start a long running goroutine which
// monitors container events. We make sure we're subscribed before
// retrieving the list of running containers to avoid a race where we
// would "miss" a creation.
started := make(chan struct{})
eh := command.InitEventHandler()
eh.Handle("create", func(e events.Message) {
if opts.all {
s := NewStats(e.ID[:12])
if cStats.add(s) {
waitFirst.Add(1)
go collect(ctx, s, dockerCli.Client(), !opts.noStream, waitFirst)
}
}
})
eh.Handle("start", func(e events.Message) {
s := NewStats(e.ID[:12])
if cStats.add(s) {
waitFirst.Add(1)
go collect(ctx, s, dockerCli.Client(), !opts.noStream, waitFirst)
}
})
eh.Handle("die", func(e events.Message) {
if !opts.all {
cStats.remove(e.ID[:12])
}
})
eventChan := make(chan events.Message) eventChan := make(chan events.Message)
go eh.Watch(eventChan) go eh.watch(eventChan)
stopped := make(chan struct{}) stopped := make(chan struct{})
go monitorContainerEvents(started, eventChan, stopped) go monitorContainerEvents(started, eventChan, stopped)
defer close(stopped) defer close(stopped)
<-started <-started
// Start a short-lived goroutine to retrieve the initial list of // Fetch the initial list of containers and collect stats for them.
// containers. // After the initial list was collected, we start listening for events
getContainerList() // to refresh the list of containers.
cs, err := apiClient.ContainerList(ctx, container.ListOptions{
All: options.All,
Filters: *options.Filters,
})
if err != nil {
return err
}
for _, ctr := range cs {
s := NewStats(ctr.ID[:12])
if cStats.add(s) {
waitFirst.Add(1)
go collect(ctx, s, apiClient, !options.NoStream, waitFirst)
}
}
// make sure each container get at least one valid stat data // make sure each container get at least one valid stat data
waitFirst.Wait() waitFirst.Wait()
} else { } else {
// Artificially send creation events for the containers we were asked to // TODO(thaJeztah): re-implement options.Containers as a filter so that
// monitor (same code path than we use when monitoring all containers). // only a single code-path is needed, and custom filters can be combined
for _, name := range opts.containers { // with a list of container names/IDs.
s := NewStats(name)
if options.Filters != nil && options.Filters.Len() > 0 {
return fmt.Errorf("filtering is not supported when specifying a list of containers")
}
// Create the list of containers, and start collecting stats for all
// containers passed.
for _, ctr := range options.Containers {
s := NewStats(ctr)
if cStats.add(s) { if cStats.add(s) {
waitFirst.Add(1) waitFirst.Add(1)
go collect(ctx, s, dockerCli.Client(), !opts.noStream, waitFirst) go collect(ctx, s, apiClient, !options.NoStream, waitFirst)
} }
} }
@ -200,22 +251,28 @@ func runStats(dockerCli command.Cli, opts *statsOptions) error {
} }
} }
format := opts.format format := options.Format
if len(format) == 0 { if len(format) == 0 {
if len(dockerCli.ConfigFile().StatsFormat) > 0 { if len(dockerCLI.ConfigFile().StatsFormat) > 0 {
format = dockerCli.ConfigFile().StatsFormat format = dockerCLI.ConfigFile().StatsFormat
} else { } else {
format = formatter.TableFormatKey format = formatter.TableFormatKey
} }
} }
if daemonOSType == "" {
// Get the daemonOSType if not set already. The daemonOSType variable
// should already be set when collecting stats as part of "collect()",
// so we unlikely hit this code in practice.
daemonOSType = dockerCLI.ServerInfo().OSType
}
statsCtx := formatter.Context{ statsCtx := formatter.Context{
Output: dockerCli.Out(), Output: dockerCLI.Out(),
Format: NewStatsFormat(format, daemonOSType), Format: NewStatsFormat(format, daemonOSType),
} }
cleanScreen := func() { cleanScreen := func() {
if !opts.noStream { if !options.NoStream {
fmt.Fprint(dockerCli.Out(), "\033[2J") _, _ = fmt.Fprint(dockerCLI.Out(), "\033[2J")
fmt.Fprint(dockerCli.Out(), "\033[H") _, _ = fmt.Fprint(dockerCLI.Out(), "\033[H")
} }
} }
@ -224,28 +281,28 @@ func runStats(dockerCli command.Cli, opts *statsOptions) error {
defer ticker.Stop() defer ticker.Stop()
for range ticker.C { for range ticker.C {
cleanScreen() cleanScreen()
ccstats := []StatsEntry{} var ccStats []StatsEntry
cStats.mu.RLock() cStats.mu.RLock()
for _, c := range cStats.cs { for _, c := range cStats.cs {
ccstats = append(ccstats, c.GetStatistics()) ccStats = append(ccStats, c.GetStatistics())
} }
cStats.mu.RUnlock() cStats.mu.RUnlock()
if err = statsFormatWrite(statsCtx, ccstats, daemonOSType, !opts.noTrunc); err != nil { if err = statsFormatWrite(statsCtx, ccStats, daemonOSType, !options.NoTrunc); err != nil {
break break
} }
if len(cStats.cs) == 0 && !showAll { if len(cStats.cs) == 0 && !showAll {
break break
} }
if opts.noStream { if options.NoStream {
break break
} }
select { select {
case err, ok := <-closeChan: case err, ok := <-closeChan:
if ok { if ok {
if err != nil { if err != nil {
// this is suppressing "unexpected EOF" in the cli when the // Suppress "unexpected EOF" errors in the CLI so that
// daemon restarts so it shutdowns cleanly // it shuts down cleanly when the daemon restarts.
if err == io.ErrUnexpectedEOF { if errors.Is(err, io.ErrUnexpectedEOF) {
return nil return nil
} }
return err return err
@ -257,3 +314,31 @@ func runStats(dockerCli command.Cli, opts *statsOptions) error {
} }
return err return err
} }
// newEventHandler initializes and returns an eventHandler
func newEventHandler() *eventHandler {
return &eventHandler{handlers: make(map[events.Action]func(events.Message))}
}
// eventHandler allows for registering specific events to setHandler.
type eventHandler struct {
handlers map[events.Action]func(events.Message)
}
func (eh *eventHandler) setHandler(action events.Action, handler func(events.Message)) {
eh.handlers[action] = handler
}
// watch ranges over the passed in event chan and processes the events based on the
// handlers created for a given action.
// To stop watching, close the event chan.
func (eh *eventHandler) watch(c <-chan events.Message) {
for e := range c {
h, exists := eh.handlers[e.Action]
if !exists {
continue
}
logrus.Debugf("event handler: received event: %v", e)
go h(e)
}
}

View File

@ -206,9 +206,9 @@ func calculateBlockIO(blkio types.BlkioStats) (uint64, uint64) {
} }
switch bioEntry.Op[0] { switch bioEntry.Op[0] {
case 'r', 'R': case 'r', 'R':
blkRead = blkRead + bioEntry.Value blkRead += bioEntry.Value
case 'w', 'W': case 'w', 'W':
blkWrite = blkWrite + bioEntry.Value blkWrite += bioEntry.Value
} }
} }
return blkRead, blkWrite return blkRead, blkWrite

View File

@ -32,7 +32,7 @@ func NewStopCommand(dockerCli command.Cli) *cobra.Command {
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
opts.containers = args opts.containers = args
opts.timeoutChanged = cmd.Flags().Changed("time") opts.timeoutChanged = cmd.Flags().Changed("time")
return runStop(dockerCli, &opts) return runStop(cmd.Context(), dockerCli, &opts)
}, },
Annotations: map[string]string{ Annotations: map[string]string{
"aliases": "docker container stop, docker stop", "aliases": "docker container stop, docker stop",
@ -46,13 +46,13 @@ func NewStopCommand(dockerCli command.Cli) *cobra.Command {
return cmd return cmd
} }
func runStop(dockerCli command.Cli, opts *stopOptions) error { func runStop(ctx context.Context, dockerCli command.Cli, opts *stopOptions) error {
var timeout *int var timeout *int
if opts.timeoutChanged { if opts.timeoutChanged {
timeout = &opts.timeout timeout = &opts.timeout
} }
errChan := parallelOperation(context.Background(), opts.containers, func(ctx context.Context, id string) error { errChan := parallelOperation(ctx, opts.containers, func(ctx context.Context, id string) error {
return dockerCli.Client().ContainerStop(ctx, id, container.StopOptions{ return dockerCli.Client().ContainerStop(ctx, id, container.StopOptions{
Signal: opts.signal, Signal: opts.signal,
Timeout: timeout, Timeout: timeout,

View File

@ -29,7 +29,7 @@ func NewTopCommand(dockerCli command.Cli) *cobra.Command {
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
opts.container = args[0] opts.container = args[0]
opts.args = args[1:] opts.args = args[1:]
return runTop(dockerCli, &opts) return runTop(cmd.Context(), dockerCli, &opts)
}, },
Annotations: map[string]string{ Annotations: map[string]string{
"aliases": "docker container top, docker top", "aliases": "docker container top, docker top",
@ -43,9 +43,7 @@ func NewTopCommand(dockerCli command.Cli) *cobra.Command {
return cmd return cmd
} }
func runTop(dockerCli command.Cli, opts *topOptions) error { func runTop(ctx context.Context, dockerCli command.Cli, opts *topOptions) error {
ctx := context.Background()
procList, err := dockerCli.Client().ContainerTop(ctx, opts.container, opts.args) procList, err := dockerCli.Client().ContainerTop(ctx, opts.container, opts.args)
if err != nil { if err != nil {
return err return err

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