mirror of https://github.com/docker/cli.git
Merge branch 'docker:master' into fix-swarm-init-help
Signed-off-by: Hernan Garcia <hernandanielg@gmail.com>
This commit is contained in:
commit
b0cd429db7
|
@ -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
|
|
|
@ -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)**
|
||||||
|
|
||||||
|
|
|
@ -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: |
|
||||||
|
|
|
@ -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
|
|
|
@ -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"
|
|
@ -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 }}
|
||||||
|
|
|
@ -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 }}
|
||||||
|
|
|
@ -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
|
|
@ -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}"'
|
||||||
|
|
|
@ -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
|
||||||
|
|
26
.mailmap
26
.mailmap
|
@ -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
46
AUTHORS
|
@ -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>
|
||||||
|
|
|
@ -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.
|
||||||
|
|
72
Dockerfile
72
Dockerfile
|
@ -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 .
|
||||||
|
|
|
@ -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"
|
||||||
|
|
2
Makefile
2
Makefile
|
@ -52,7 +52,7 @@ shellcheck: ## run shellcheck validation
|
||||||
.PHONY: fmt
|
.PHONY: fmt
|
||||||
fmt: ## run gofumpt (if present) or gofmt
|
fmt: ## run gofumpt (if present) or gofmt
|
||||||
@if command -v gofumpt > /dev/null; then \
|
@if command -v gofumpt > /dev/null; then \
|
||||||
gofumpt -w -d -lang=1.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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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")
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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...)}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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 != "" {
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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))
|
||||||
|
})
|
||||||
|
}
|
|
@ -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}}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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())))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
},
|
},
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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...,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 != "-" {
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
})
|
})
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()))
|
||||||
})
|
})
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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}},
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
Loading…
Reference in New Issue