Import Upstream version 20.10.16
This commit is contained in:
commit
46a93db995
|
@ -0,0 +1,153 @@
|
|||
version: 2
|
||||
|
||||
jobs:
|
||||
|
||||
lint:
|
||||
working_directory: /work
|
||||
docker: [{image: 'docker:20.10-git'}]
|
||||
environment:
|
||||
DOCKER_BUILDKIT: 1
|
||||
steps:
|
||||
- checkout
|
||||
- setup_remote_docker:
|
||||
version: 20.10.6
|
||||
reusable: true
|
||||
exclusive: false
|
||||
- run:
|
||||
name: "Docker version"
|
||||
command: docker version
|
||||
- run:
|
||||
name: "Docker info"
|
||||
command: docker info
|
||||
- run:
|
||||
name: "Shellcheck - build image"
|
||||
command: |
|
||||
docker build --progress=plain -f dockerfiles/Dockerfile.shellcheck --tag cli-validator:$CIRCLE_BUILD_NUM .
|
||||
- run:
|
||||
name: "Shellcheck"
|
||||
command: |
|
||||
docker run --rm cli-validator:$CIRCLE_BUILD_NUM \
|
||||
make shellcheck
|
||||
- run:
|
||||
name: "Lint - build image"
|
||||
command: |
|
||||
docker build --progress=plain -f dockerfiles/Dockerfile.lint --tag cli-linter:$CIRCLE_BUILD_NUM .
|
||||
- run:
|
||||
name: "Lint"
|
||||
command: |
|
||||
docker run --rm cli-linter:$CIRCLE_BUILD_NUM
|
||||
|
||||
cross:
|
||||
working_directory: /work
|
||||
docker: [{image: 'docker:20.10-git'}]
|
||||
environment:
|
||||
DOCKER_BUILDKIT: 1
|
||||
BUILDX_VERSION: "v0.8.2"
|
||||
parallelism: 3
|
||||
steps:
|
||||
- checkout
|
||||
- setup_remote_docker:
|
||||
version: 20.10.6
|
||||
reusable: true
|
||||
exclusive: false
|
||||
- run:
|
||||
name: "Docker version"
|
||||
command: docker version
|
||||
- run:
|
||||
name: "Docker info"
|
||||
command: docker info
|
||||
- run: apk add make curl
|
||||
- run: mkdir -vp ~/.docker/cli-plugins/
|
||||
- run: curl -fsSL --output ~/.docker/cli-plugins/docker-buildx https://github.com/docker/buildx/releases/download/${BUILDX_VERSION}/buildx-${BUILDX_VERSION}.linux-amd64
|
||||
- run: chmod a+x ~/.docker/cli-plugins/docker-buildx
|
||||
- run: docker buildx version
|
||||
- run: docker context create buildctx
|
||||
- run: docker buildx create --use buildctx && docker buildx inspect --bootstrap
|
||||
- run: GROUP_INDEX=$CIRCLE_NODE_INDEX GROUP_TOTAL=$CIRCLE_NODE_TOTAL docker buildx bake cross --progress=plain
|
||||
- store_artifacts:
|
||||
path: /work/build
|
||||
|
||||
test:
|
||||
working_directory: /work
|
||||
docker: [{image: 'docker:20.10-git'}]
|
||||
environment:
|
||||
DOCKER_BUILDKIT: 1
|
||||
steps:
|
||||
- checkout
|
||||
- setup_remote_docker:
|
||||
version: 20.10.6
|
||||
reusable: true
|
||||
exclusive: false
|
||||
- run:
|
||||
name: "Docker version"
|
||||
command: docker version
|
||||
- run:
|
||||
name: "Docker info"
|
||||
command: docker info
|
||||
- run:
|
||||
name: "Unit Test with Coverage - build image"
|
||||
command: |
|
||||
mkdir -p test-results/unit-tests
|
||||
docker build --progress=plain -f dockerfiles/Dockerfile.dev --tag cli-builder:$CIRCLE_BUILD_NUM .
|
||||
- run:
|
||||
name: "Unit Test with Coverage"
|
||||
command: |
|
||||
docker run \
|
||||
-e GOTESTSUM_JUNITFILE=/tmp/junit.xml \
|
||||
--name \
|
||||
test-$CIRCLE_BUILD_NUM cli-builder:$CIRCLE_BUILD_NUM \
|
||||
make test-coverage
|
||||
docker cp \
|
||||
test-$CIRCLE_BUILD_NUM:/tmp/junit.xml \
|
||||
./test-results/unit-tests/junit.xml
|
||||
- run:
|
||||
name: "Upload to Codecov"
|
||||
command: |
|
||||
docker cp \
|
||||
test-$CIRCLE_BUILD_NUM:/go/src/github.com/docker/cli/coverage.txt \
|
||||
coverage.txt
|
||||
apk add -U bash curl
|
||||
curl -s https://codecov.io/bash | bash || \
|
||||
echo 'Codecov failed to upload'
|
||||
- store_test_results:
|
||||
path: test-results
|
||||
- store_artifacts:
|
||||
path: test-results
|
||||
|
||||
validate:
|
||||
working_directory: /work
|
||||
docker: [{image: 'docker:20.10-git'}]
|
||||
environment:
|
||||
DOCKER_BUILDKIT: 1
|
||||
steps:
|
||||
- checkout
|
||||
- setup_remote_docker:
|
||||
version: 20.10.6
|
||||
reusable: true
|
||||
exclusive: false
|
||||
- run:
|
||||
name: "Docker version"
|
||||
command: docker version
|
||||
- run:
|
||||
name: "Docker info"
|
||||
command: docker info
|
||||
- run:
|
||||
name: "Validate - build image"
|
||||
command: |
|
||||
rm -f .dockerignore # include .git
|
||||
docker build --progress=plain -f dockerfiles/Dockerfile.dev --tag cli-builder-with-git:$CIRCLE_BUILD_NUM .
|
||||
- run:
|
||||
name: "Validate Vendor, Docs, and Code Generation"
|
||||
command: |
|
||||
docker run --rm cli-builder-with-git:$CIRCLE_BUILD_NUM \
|
||||
make ci-validate
|
||||
no_output_timeout: 15m
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
ci:
|
||||
jobs:
|
||||
- lint
|
||||
- cross
|
||||
- test
|
||||
- validate
|
|
@ -0,0 +1,7 @@
|
|||
.circleci
|
||||
.dockerignore
|
||||
.github
|
||||
.gitignore
|
||||
appveyor.yml
|
||||
build
|
||||
/vndr.log
|
|
@ -0,0 +1,7 @@
|
|||
# GitHub code owners
|
||||
# See https://github.com/blog/2392-introducing-code-owners
|
||||
|
||||
cli/command/stack/** @silvin-lubecki
|
||||
contrib/completion/bash/** @albers
|
||||
contrib/completion/zsh/** @sdurrheimer
|
||||
docs/** @thaJeztah
|
|
@ -0,0 +1,64 @@
|
|||
<!--
|
||||
If you are reporting a new issue, make sure that we do not have any duplicates
|
||||
already open. You can ensure this by searching the issue list for this
|
||||
repository. If there is a duplicate, please close your issue and add a comment
|
||||
to the existing issue instead.
|
||||
|
||||
If you suspect your issue is a bug, please edit your issue description to
|
||||
include the BUG REPORT INFORMATION shown below. If you fail to provide this
|
||||
information within 7 days, we cannot debug your issue and will close it. We
|
||||
will, however, reopen it if you later provide the information.
|
||||
|
||||
For more information about reporting issues, see
|
||||
https://github.com/docker/cli/blob/master/CONTRIBUTING.md#reporting-other-issues
|
||||
|
||||
---------------------------------------------------
|
||||
GENERAL SUPPORT INFORMATION
|
||||
---------------------------------------------------
|
||||
|
||||
The GitHub issue tracker is for bug reports and feature requests.
|
||||
General support can be found at the following locations:
|
||||
|
||||
- Docker Support Forums - https://forums.docker.com
|
||||
- Docker Community Slack - https://dockr.ly/community
|
||||
- Post a question on StackOverflow, using the Docker tag
|
||||
|
||||
---------------------------------------------------
|
||||
BUG REPORT INFORMATION
|
||||
---------------------------------------------------
|
||||
Use the commands below to provide key information from your environment:
|
||||
You do NOT have to include this information if this is a FEATURE REQUEST
|
||||
-->
|
||||
|
||||
**Description**
|
||||
|
||||
<!--
|
||||
Briefly describe the problem you are having in a few paragraphs.
|
||||
-->
|
||||
|
||||
**Steps to reproduce the issue:**
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
|
||||
**Describe the results you received:**
|
||||
|
||||
|
||||
**Describe the results you expected:**
|
||||
|
||||
|
||||
**Additional information you deem important (e.g. issue happens only occasionally):**
|
||||
|
||||
**Output of `docker version`:**
|
||||
|
||||
```
|
||||
(paste your output here)
|
||||
```
|
||||
|
||||
**Output of `docker info`:**
|
||||
|
||||
```
|
||||
(paste your output here)
|
||||
```
|
||||
|
||||
**Additional environment details (AWS, VirtualBox, physical, etc.):**
|
|
@ -0,0 +1,30 @@
|
|||
<!--
|
||||
Please make sure you've read and understood our contributing guidelines;
|
||||
https://github.com/docker/cli/blob/master/CONTRIBUTING.md
|
||||
|
||||
** Make sure all your commits include a signature generated with `git commit -s` **
|
||||
|
||||
For additional information on our contributing process, read our contributing
|
||||
guide https://docs.docker.com/opensource/code/
|
||||
|
||||
If this is a bug fix, make sure your description includes "fixes #xxxx", or
|
||||
"closes #xxxx"
|
||||
|
||||
Please provide the following information:
|
||||
-->
|
||||
|
||||
**- What I did**
|
||||
|
||||
**- How I did it**
|
||||
|
||||
**- How to verify it**
|
||||
|
||||
**- Description for the changelog**
|
||||
<!--
|
||||
Write a short (one line) summary that describes the changes in this
|
||||
pull request for inclusion in the changelog:
|
||||
-->
|
||||
|
||||
|
||||
**- A picture of a cute animal (not mandatory but encouraged)**
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
# push:
|
||||
# branches: [master]
|
||||
# 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:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# Override automatic language detection by changing the below list
|
||||
# Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
|
||||
language: ['go']
|
||||
# Learn more...
|
||||
# https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
# We must fetch at least the immediate parents so that if this is
|
||||
# a pull request then we can checkout the head.
|
||||
fetch-depth: 2
|
||||
|
||||
# If this run was triggered by a pull request event, then checkout
|
||||
# the head of the pull request instead of the merge commit.
|
||||
- run: git checkout HEAD^2
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
|
||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||
# and modify them (or add more) to build your code if your project
|
||||
# uses a compiled language
|
||||
|
||||
#- run: |
|
||||
# make bootstrap
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
|
@ -0,0 +1,18 @@
|
|||
# if you want to ignore files created by your editor/tools,
|
||||
# please consider a global .gitignore https://help.github.com/articles/ignoring-files
|
||||
*.exe
|
||||
*.exe~
|
||||
*.orig
|
||||
.*.swp
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
.editorconfig
|
||||
/build/
|
||||
cli/winresources/rsrc_*.syso
|
||||
/man/man1/
|
||||
/man/man5/
|
||||
/man/man8/
|
||||
/docs/yaml/gen/
|
||||
coverage.txt
|
||||
profile.out
|
||||
/vndr.log
|
|
@ -0,0 +1,83 @@
|
|||
linters:
|
||||
enable:
|
||||
- bodyclose
|
||||
- deadcode
|
||||
- dogsled
|
||||
- gocyclo
|
||||
- goimports
|
||||
- golint
|
||||
- gosec
|
||||
- gosimple
|
||||
- govet
|
||||
- ineffassign
|
||||
- interfacer
|
||||
- lll
|
||||
- megacheck
|
||||
- misspell
|
||||
- nakedret
|
||||
- staticcheck
|
||||
- structcheck
|
||||
- typecheck
|
||||
- unconvert
|
||||
- unparam
|
||||
- unused
|
||||
- varcheck
|
||||
|
||||
disable:
|
||||
- errcheck
|
||||
|
||||
run:
|
||||
timeout: 5m
|
||||
skip-dirs:
|
||||
- cli/command/stack/kubernetes/api/openapi
|
||||
- cli/command/stack/kubernetes/api/client
|
||||
skip-files:
|
||||
- cli/compose/schema/bindata.go
|
||||
- .*generated.*
|
||||
|
||||
linters-settings:
|
||||
gocyclo:
|
||||
min-complexity: 16
|
||||
govet:
|
||||
check-shadowing: false
|
||||
lll:
|
||||
line-length: 200
|
||||
nakedret:
|
||||
command: nakedret
|
||||
pattern: ^(?P<path>.*?\\.go):(?P<line>\\d+)\\s*(?P<message>.*)$
|
||||
|
||||
issues:
|
||||
# The default exclusion rules are a bit too permissive, so copying the relevant ones below
|
||||
exclude-use-default: false
|
||||
|
||||
exclude:
|
||||
- parameter .* always receives
|
||||
|
||||
exclude-rules:
|
||||
# These are copied from the default exclude rules, except for "ineffective break statement"
|
||||
# and GoDoc checks.
|
||||
# https://github.com/golangci/golangci-lint/blob/0cc87df732aaf1d5ad9ce9ca538d38d916918b36/pkg/config/config.go#L36
|
||||
- text: "Error return value of .((os\\.)?std(out|err)\\..*|.*Close|.*Flush|os\\.Remove(All)?|.*printf?|os\\.(Un)?Setenv). is not checked"
|
||||
linters:
|
||||
- errcheck
|
||||
- text: "func name will be used as test\\.Test.* by other packages, and that stutters; consider calling this"
|
||||
linters:
|
||||
- golint
|
||||
- text: "G103: Use of unsafe calls should be audited"
|
||||
linters:
|
||||
- gosec
|
||||
- text: "G104: Errors unhandled"
|
||||
linters:
|
||||
- gosec
|
||||
- text: "G204: Subprocess launch(ed with (variable|function call)|ing should be audited)"
|
||||
linters:
|
||||
- gosec
|
||||
- text: "(G301|G302): (Expect directory permissions to be 0750 or less|Expect file permissions to be 0600 or less)"
|
||||
linters:
|
||||
- gosec
|
||||
- text: "G304: Potential file inclusion via variable"
|
||||
linters:
|
||||
- gosec
|
||||
- text: "(G201|G202): SQL string (formatting|concatenation)"
|
||||
linters:
|
||||
- gosec
|
|
@ -0,0 +1,530 @@
|
|||
# Generate AUTHORS: scripts/docs/generate-authors.sh
|
||||
|
||||
# Tip for finding duplicates (besides scanning the output of AUTHORS for name
|
||||
# duplicates that aren't also email duplicates): scan the output of:
|
||||
# git log --format='%aE - %aN' | sort -uf
|
||||
#
|
||||
# For explanation on this file format: man git-shortlog
|
||||
|
||||
Aaron L. Xu <liker.xu@foxmail.com>
|
||||
Abhinandan Prativadi <abhi@docker.com>
|
||||
Ace Tang <aceapril@126.com>
|
||||
Adrien Gallouët <adrien@gallouet.fr> <angt@users.noreply.github.com>
|
||||
Ahmed Kamal <email.ahmedkamal@googlemail.com>
|
||||
Ahmet Alp Balkan <ahmetb@microsoft.com> <ahmetalpbalkan@gmail.com>
|
||||
AJ Bowen <aj@soulshake.net>
|
||||
AJ Bowen <aj@soulshake.net> <aj@gandi.net>
|
||||
AJ Bowen <aj@soulshake.net> <amy@gandi.net>
|
||||
Akihiro Matsushima <amatsusbit@gmail.com> <amatsus@users.noreply.github.com>
|
||||
Akihiro Suda <akihiro.suda.cz@hco.ntt.co.jp>
|
||||
Akihiro Suda <akihiro.suda.cz@hco.ntt.co.jp> <suda.akihiro@lab.ntt.co.jp>
|
||||
Akihiro Suda <akihiro.suda.cz@hco.ntt.co.jp> <suda.kyoto@gmail.com>
|
||||
Aleksa Sarai <asarai@suse.de>
|
||||
Aleksa Sarai <asarai@suse.de> <asarai@suse.com>
|
||||
Aleksa Sarai <asarai@suse.de> <cyphar@cyphar.com>
|
||||
Aleksandrs Fadins <aleks@s-ko.net>
|
||||
Alessandro Boch <aboch@tetrationanalytics.com> <aboch@docker.com>
|
||||
Alex Chen <alexchenunix@gmail.com> <root@localhost.localdomain>
|
||||
Alex Ellis <alexellis2@gmail.com>
|
||||
Alexander Larsson <alexl@redhat.com> <alexander.larsson@gmail.com>
|
||||
Alexander Morozov <lk4d4@docker.com>
|
||||
Alexander Morozov <lk4d4@docker.com> <lk4d4math@gmail.com>
|
||||
Alexandre Beslic <alexandre.beslic@gmail.com> <abronan@docker.com>
|
||||
Alicia Lauerman <alicia@eta.im> <allydevour@me.com>
|
||||
Allen Sun <allensun.shl@alibaba-inc.com> <allen.sun@daocloud.io>
|
||||
Allen Sun <allensun.shl@alibaba-inc.com> <shlallen1990@gmail.com>
|
||||
Andrew Weiss <andrew.weiss@docker.com> <andrew.weiss@microsoft.com>
|
||||
Andrew Weiss <andrew.weiss@docker.com> <andrew.weiss@outlook.com>
|
||||
André Martins <aanm90@gmail.com> <martins@noironetworks.com>
|
||||
Andy Rothfusz <github@developersupport.net> <github@metaliveblog.com>
|
||||
Andy Smith <github@anarkystic.com>
|
||||
Ankush Agarwal <ankushagarwal11@gmail.com> <ankushagarwal@users.noreply.github.com>
|
||||
Antonio Murdaca <antonio.murdaca@gmail.com> <amurdaca@redhat.com>
|
||||
Antonio Murdaca <antonio.murdaca@gmail.com> <me@runcom.ninja>
|
||||
Antonio Murdaca <antonio.murdaca@gmail.com> <runcom@linux.com>
|
||||
Antonio Murdaca <antonio.murdaca@gmail.com> <runcom@redhat.com>
|
||||
Antonio Murdaca <antonio.murdaca@gmail.com> <runcom@users.noreply.github.com>
|
||||
Anuj Bahuguna <anujbahuguna.dev@gmail.com>
|
||||
Anuj Bahuguna <anujbahuguna.dev@gmail.com> <abahuguna@fiberlink.com>
|
||||
Anusha Ragunathan <anusha.ragunathan@docker.com> <anusha@docker.com>
|
||||
Ao Li <la9249@163.com>
|
||||
Arnaud Porterie <arnaud.porterie@docker.com>
|
||||
Arnaud Porterie <arnaud.porterie@docker.com> <icecrime@gmail.com>
|
||||
Arthur Gautier <baloo@gandi.net> <superbaloo+registrations.github@superbaloo.net>
|
||||
Arthur Peka <arthur.peka@outlook.com> <arthrp@users.noreply.github.com>
|
||||
Avi Miller <avi.miller@oracle.com> <avi.miller@gmail.com>
|
||||
Ben Bonnefoy <frenchben@docker.com>
|
||||
Ben Golub <ben.golub@dotcloud.com>
|
||||
Ben Toews <mastahyeti@gmail.com> <mastahyeti@users.noreply.github.com>
|
||||
Benoit Chesneau <bchesneau@gmail.com>
|
||||
Bhiraj Butala <abhiraj.butala@gmail.com>
|
||||
Bhumika Bayani <bhumikabayani@gmail.com>
|
||||
Bilal Amarni <bilal.amarni@gmail.com> <bamarni@users.noreply.github.com>
|
||||
Bill Wang <ozbillwang@gmail.com> <SydOps@users.noreply.github.com>
|
||||
Bin Liu <liubin0329@gmail.com>
|
||||
Bin Liu <liubin0329@gmail.com> <liubin0329@users.noreply.github.com>
|
||||
Bingshen Wang <bingshen.wbs@alibaba-inc.com>
|
||||
Boaz Shuster <ripcurld.github@gmail.com>
|
||||
Brandon Philips <brandon.philips@coreos.com> <brandon@ifup.co>
|
||||
Brandon Philips <brandon.philips@coreos.com> <brandon@ifup.org>
|
||||
Brent Salisbury <brent.salisbury@docker.com> <brent@docker.com>
|
||||
Brian Goff <cpuguy83@gmail.com>
|
||||
Brian Goff <cpuguy83@gmail.com> <bgoff@cpuguy83-mbp.home>
|
||||
Brian Goff <cpuguy83@gmail.com> <bgoff@cpuguy83-mbp.local>
|
||||
Carlos de Paula <me@carlosedp.com>
|
||||
Chad Faragher <wyckster@hotmail.com>
|
||||
Chander Govindarajan <chandergovind@gmail.com>
|
||||
Chao Wang <wangchao.fnst@cn.fujitsu.com> <chaowang@localhost.localdomain>
|
||||
Charles Hooper <charles.hooper@dotcloud.com> <chooper@plumata.com>
|
||||
Chen Chao <cc272309126@gmail.com>
|
||||
Chen Chuanliang <chen.chuanliang@zte.com.cn>
|
||||
Chen Mingjie <chenmingjie0828@163.com>
|
||||
Chen Qiu <cheney-90@hotmail.com>
|
||||
Chen Qiu <cheney-90@hotmail.com> <21321229@zju.edu.cn>
|
||||
Chris Dias <cdias@microsoft.com>
|
||||
Chris McKinnel <chris.mckinnel@tangentlabs.co.uk>
|
||||
Christopher Biscardi <biscarch@sketcht.com>
|
||||
Christopher Latham <sudosurootdev@gmail.com>
|
||||
Christy Norman <christy@linux.vnet.ibm.com>
|
||||
Chun Chen <ramichen@tencent.com> <chenchun.feed@gmail.com>
|
||||
Comical Derskeal <27731088+derskeal@users.noreply.github.com>
|
||||
Corbin Coleman <corbin.coleman@docker.com>
|
||||
Cristian Staretu <cristian.staretu@gmail.com>
|
||||
Cristian Staretu <cristian.staretu@gmail.com> <unclejack@users.noreply.github.com>
|
||||
Cristian Staretu <cristian.staretu@gmail.com> <unclejacksons@gmail.com>
|
||||
CUI Wei <ghostplant@qq.com> cuiwei13 <cuiwei13@pku.edu.cn>
|
||||
Daehyeok Mun <daehyeok@gmail.com>
|
||||
Daehyeok Mun <daehyeok@gmail.com> <daehyeok@daehyeok-ui-MacBook-Air.local>
|
||||
Daehyeok Mun <daehyeok@gmail.com> <daehyeok@daehyeokui-MacBook-Air.local>
|
||||
Daisuke Ito <itodaisuke00@gmail.com>
|
||||
Dan Feldman <danf@jfrog.com>
|
||||
Daniel Dao <dqminh@cloudflare.com>
|
||||
Daniel Dao <dqminh@cloudflare.com> <dqminh89@gmail.com>
|
||||
Daniel Garcia <daniel@danielgarcia.info>
|
||||
Daniel Gasienica <daniel@gasienica.ch> <dgasienica@zynga.com>
|
||||
Daniel Goosen <daniel.goosen@surveysampling.com> <djgoosen@users.noreply.github.com>
|
||||
Daniel Grunwell <mwgrunny@gmail.com>
|
||||
Daniel J Walsh <dwalsh@redhat.com>
|
||||
Daniel Mizyrycki <daniel.mizyrycki@dotcloud.com> <daniel@dotcloud.com>
|
||||
Daniel Mizyrycki <daniel.mizyrycki@dotcloud.com> <mzdaniel@glidelink.net>
|
||||
Daniel Mizyrycki <daniel.mizyrycki@dotcloud.com> <root@vagrant-ubuntu-12.10.vagrantup.com>
|
||||
Daniel Nephin <dnephin@docker.com> <dnephin@gmail.com>
|
||||
Daniel Norberg <dano@spotify.com> <daniel.norberg@gmail.com>
|
||||
Daniel Watkins <daniel@daniel-watkins.co.uk>
|
||||
Danny Yates <danny@codeaholics.org> <Danny.Yates@mailonline.co.uk>
|
||||
Darren Shepherd <darren.s.shepherd@gmail.com> <darren@rancher.com>
|
||||
Dattatraya Kumbhar <dattatraya.kumbhar@gslab.com>
|
||||
Dave Goodchild <buddhamagnet@gmail.com>
|
||||
Dave Henderson <dhenderson@gmail.com> <Dave.Henderson@ca.ibm.com>
|
||||
Dave Tucker <dt@docker.com> <dave@dtucker.co.uk>
|
||||
David M. Karr <davidmichaelkarr@gmail.com>
|
||||
David Sheets <dsheets@docker.com> <sheets@alum.mit.edu>
|
||||
David Sissitka <me@dsissitka.com>
|
||||
David Williamson <david.williamson@docker.com> <davidwilliamson@users.noreply.github.com>
|
||||
Deshi Xiao <dxiao@redhat.com> <dsxiao@dataman-inc.com>
|
||||
Deshi Xiao <dxiao@redhat.com> <xiaods@gmail.com>
|
||||
Diego Siqueira <dieg0@live.com>
|
||||
Diogo Monica <diogo@docker.com> <diogo.monica@gmail.com>
|
||||
Dominik Honnef <dominik@honnef.co> <dominikh@fork-bomb.org>
|
||||
Doug Davis <dug@us.ibm.com> <duglin@users.noreply.github.com>
|
||||
Doug Tangren <d.tangren@gmail.com>
|
||||
Drew Erny <derny@mirantis.com>
|
||||
Drew Erny <derny@mirantis.com> <drew.erny@docker.com>
|
||||
Elan Ruusamäe <glen@pld-linux.org>
|
||||
Elan Ruusamäe <glen@pld-linux.org> <glen@delfi.ee>
|
||||
Elango Sivanandam <elango.siva@docker.com>
|
||||
Eric G. Noriega <enoriega@vizuri.com> <egnoriega@users.noreply.github.com>
|
||||
Eric Hanchrow <ehanchrow@ine.com> <eric.hanchrow@gmail.com>
|
||||
Eric Rosenberg <ehaydenr@gmail.com> <ehaydenr@users.noreply.github.com>
|
||||
Erica Windisch <erica@windisch.us> <eric@windisch.us>
|
||||
Erica Windisch <erica@windisch.us> <ewindisch@docker.com>
|
||||
Erik Hollensbe <github@hollensbe.org> <erik+github@hollensbe.org>
|
||||
Erwin van der Koogh <info@erronis.nl>
|
||||
Euan Kemp <euan.kemp@coreos.com> <euank@amazon.com>
|
||||
Eugen Krizo <eugen.krizo@gmail.com>
|
||||
Evan Hazlett <ejhazlett@gmail.com> <ehazlett@users.noreply.github.com>
|
||||
Evelyn Xu <evelynhsu21@gmail.com>
|
||||
Evgeny Shmarnev <shmarnev@gmail.com>
|
||||
Faiz Khan <faizkhan00@gmail.com>
|
||||
Felix Hupfeld <felix@quobyte.com> <quofelix@users.noreply.github.com>
|
||||
Felix Ruess <felix.ruess@gmail.com> <felix.ruess@roboception.de>
|
||||
Feng Yan <fy2462@gmail.com>
|
||||
Fengtu Wang <wangfengtu@huawei.com> <wangfengtu@huawei.com>
|
||||
Francisco Carriedo <fcarriedo@gmail.com>
|
||||
Frank Rosquin <frank.rosquin+github@gmail.com> <frank.rosquin@gmail.com>
|
||||
Frederick F. Kautz IV <fkautz@redhat.com> <fkautz@alumni.cmu.edu>
|
||||
Gabriel Nicolas Avellaneda <avellaneda.gabriel@gmail.com>
|
||||
Gaetan de Villele <gdevillele@gmail.com>
|
||||
Gang Qiao <qiaohai8866@gmail.com> <1373319223@qq.com>
|
||||
George Kontridze <george@bugsnag.com>
|
||||
Gerwim Feiken <g.feiken@tfe.nl> <gerwim@gmail.com>
|
||||
Giampaolo Mancini <giampaolo@trampolineup.com>
|
||||
Gopikannan Venugopalsamy <gopikannan.venugopalsamy@gmail.com>
|
||||
Gou Rao <gou@portworx.com> <gourao@users.noreply.github.com>
|
||||
Greg Stephens <greg@udon.org>
|
||||
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.net>
|
||||
Guillaume J. Charmes <guillaume.charmes@docker.com> <guillaume@docker.com>
|
||||
Guillaume J. Charmes <guillaume.charmes@docker.com> <guillaume@dotcloud.com>
|
||||
Guillaume Le Floch <glfloch@gmail.com>
|
||||
Gurjeet Singh <gurjeet@singh.im> <singh.gurjeet@gmail.com>
|
||||
Gustav Sinder <gustav.sinder@gmail.com>
|
||||
Günther Jungbluth <gunther@gameslabs.net>
|
||||
Hakan Özler <hakan.ozler@kodcu.com>
|
||||
Hao Shu Wei <haosw@cn.ibm.com>
|
||||
Hao Shu Wei <haosw@cn.ibm.com> <haoshuwei1989@163.com>
|
||||
Harald Albers <github@albersweb.de> <albers@users.noreply.github.com>
|
||||
Harold Cooper <hrldcpr@gmail.com>
|
||||
Harry Zhang <harryz@hyper.sh> <harryzhang@zju.edu.cn>
|
||||
Harry Zhang <harryz@hyper.sh> <resouer@163.com>
|
||||
Harry Zhang <harryz@hyper.sh> <resouer@gmail.com>
|
||||
Harry Zhang <resouer@163.com>
|
||||
Harshal Patil <harshal.patil@in.ibm.com> <harche@users.noreply.github.com>
|
||||
Helen Xie <chenjg@harmonycloud.cn>
|
||||
Hollie Teal <hollie@docker.com>
|
||||
Hollie Teal <hollie@docker.com> <hollie.teal@docker.com>
|
||||
Hollie Teal <hollie@docker.com> <hollietealok@users.noreply.github.com>
|
||||
Hu Keping <hukeping@huawei.com>
|
||||
Hugo Gabriel Eyherabide <hugogabriel.eyherabide@gmail.com>
|
||||
Huu Nguyen <huu@prismskylabs.com> <whoshuu@gmail.com>
|
||||
Hyzhou Zhy <hyzhou.zhy@alibaba-inc.com>
|
||||
Hyzhou Zhy <hyzhou.zhy@alibaba-inc.com> <1187766782@qq.com>
|
||||
Ian Campbell <ian.campbell@docker.com>
|
||||
Ian Campbell <ian.campbell@docker.com> <ijc@docker.com>
|
||||
Ilya Khlopotov <ilya.khlopotov@gmail.com>
|
||||
Jack Laxson <jackjrabbit@gmail.com>
|
||||
Jacob Atzen <jacob@jacobatzen.dk> <jatzen@gmail.com>
|
||||
Jacob Tomlinson <jacob@tom.linson.uk> <jacobtomlinson@users.noreply.github.com>
|
||||
Jaivish Kothari <janonymous.codevulture@gmail.com>
|
||||
Jake Lambert <jake.lambert@volusion.com>
|
||||
Jake Lambert <jake.lambert@volusion.com> <32850427+jake-lambert-volusion@users.noreply.github.com>
|
||||
Jamie Hannaford <jamie@limetree.org> <jamie.hannaford@rackspace.com>
|
||||
Jean-Baptiste Barth <jeanbaptiste.barth@gmail.com>
|
||||
Jean-Baptiste Dalido <jeanbaptiste@appgratis.com>
|
||||
Jean-Pierre Huynh <jean-pierre.huynh@ounet.fr> <jp@moogsoft.com>
|
||||
Jean-Tiare Le Bigot <jt@yadutaf.fr> <admin@jtlebi.fr>
|
||||
Jeff Anderson <jeff@docker.com> <jefferya@programmerq.net>
|
||||
Jeff Nickoloff <jeff.nickoloff@gmail.com> <jeff@allingeek.com>
|
||||
Jeroen Franse <jeroenfranse@gmail.com>
|
||||
Jessica Frazelle <jess@oxide.computer>
|
||||
Jessica Frazelle <jess@oxide.computer> <acidburn@docker.com>
|
||||
Jessica Frazelle <jess@oxide.computer> <acidburn@google.com>
|
||||
Jessica Frazelle <jess@oxide.computer> <acidburn@microsoft.com>
|
||||
Jessica Frazelle <jess@oxide.computer> <jess@docker.com>
|
||||
Jessica Frazelle <jess@oxide.computer> <jess@mesosphere.com>
|
||||
Jessica Frazelle <jess@oxide.computer> <jessfraz@google.com>
|
||||
Jessica Frazelle <jess@oxide.computer> <jfrazelle@users.noreply.github.com>
|
||||
Jessica Frazelle <jess@oxide.computer> <me@jessfraz.com>
|
||||
Jessica Frazelle <jess@oxide.computer> <princess@docker.com>
|
||||
Jim Galasyn <jim.galasyn@docker.com>
|
||||
Jiuyue Ma <majiuyue@huawei.com>
|
||||
Joey Geiger <jgeiger@gmail.com>
|
||||
Joffrey F <joffrey@docker.com>
|
||||
Joffrey F <joffrey@docker.com> <f.joffrey@gmail.com>
|
||||
Joffrey F <joffrey@docker.com> <joffrey@dotcloud.com>
|
||||
Johan Euphrosine <proppy@google.com> <proppy@aminche.com>
|
||||
John Harris <john@johnharris.io>
|
||||
John Howard <github@lowenna.com>
|
||||
John Howard <github@lowenna.com> <John.Howard@microsoft.com>
|
||||
John Howard <github@lowenna.com> <jhoward@microsoft.com>
|
||||
John Howard <github@lowenna.com> <jhoward@ntdev.microsoft.com>
|
||||
John Howard <github@lowenna.com> <jhowardmsft@users.noreply.github.com>
|
||||
John Howard <github@lowenna.com> <john.howard@microsoft.com>
|
||||
John Stephens <johnstep@docker.com> <johnstep@users.noreply.github.com>
|
||||
Jordan Arentsen <blissdev@gmail.com>
|
||||
Jordan Jennings <jjn2009@gmail.com> <jjn2009@users.noreply.github.com>
|
||||
Jorit Kleine-Möllhoff <joppich@bricknet.de> <joppich@users.noreply.github.com>
|
||||
Jose Diaz-Gonzalez <email@josediazgonzalez.com>
|
||||
Jose Diaz-Gonzalez <email@josediazgonzalez.com> <jose@seatgeek.com>
|
||||
Jose Diaz-Gonzalez <email@josediazgonzalez.com> <josegonzalez@users.noreply.github.com>
|
||||
Josh Eveleth <joshe@opendns.com> <jeveleth@users.noreply.github.com>
|
||||
Josh Hawn <josh.hawn@docker.com> <jlhawn@berkeley.edu>
|
||||
Josh Horwitz <horwitz@addthis.com> <horwitzja@gmail.com>
|
||||
Josh Soref <jsoref@gmail.com> <jsoref@users.noreply.github.com>
|
||||
Josh Wilson <josh.wilson@fivestars.com> <jcwilson@users.noreply.github.com>
|
||||
Joyce Jang <mail@joycejang.com>
|
||||
Julien Bordellier <julienbordellier@gmail.com> <git@julienbordellier.com>
|
||||
Julien Bordellier <julienbordellier@gmail.com> <me@julienbordellier.com>
|
||||
Justin Cormack <justin.cormack@docker.com>
|
||||
Justin Cormack <justin.cormack@docker.com> <justin.cormack@unikernel.com>
|
||||
Justin Cormack <justin.cormack@docker.com> <justin@specialbusservice.com>
|
||||
Justin Simonelis <justin.p.simonelis@gmail.com> <justin.simonelis@PTS-JSIMON2.toronto.exclamation.com>
|
||||
Jérôme Petazzoni <jerome.petazzoni@docker.com> <jerome.petazzoni@dotcloud.com>
|
||||
Jérôme Petazzoni <jerome.petazzoni@docker.com> <jerome.petazzoni@gmail.com>
|
||||
Jérôme Petazzoni <jerome.petazzoni@docker.com> <jp@enix.org>
|
||||
K. Heller <pestophagous@gmail.com> <pestophagous@users.noreply.github.com>
|
||||
Kai Qiang Wu (Kennan) <wkq5325@gmail.com>
|
||||
Kai Qiang Wu (Kennan) <wkq5325@gmail.com> <wkqwu@cn.ibm.com>
|
||||
Kamil Domański <kamil@domanski.co>
|
||||
Kamjar Gerami <kami.gerami@gmail.com>
|
||||
Kat Samperi <kat.samperi@gmail.com> <kizzie@users.noreply.github.com>
|
||||
Kathryn Spiers <kathryn@spiers.me>
|
||||
Kathryn Spiers <kathryn@spiers.me> <kyle@Spiers.me>
|
||||
Kathryn Spiers <kathryn@spiers.me> <Kyle@Spiers.me>
|
||||
Ken Cochrane <kencochrane@gmail.com> <KenCochrane@gmail.com>
|
||||
Ken Herner <kherner@progress.com> <chosenken@gmail.com>
|
||||
Kenfe-Mickaël Laventure <mickael.laventure@gmail.com>
|
||||
Kevin Feyrer <kevin.feyrer@btinternet.com> <kevinfeyrer@users.noreply.github.com>
|
||||
Kevin Kern <kaiwentan@harmonycloud.cn>
|
||||
Kevin Meredith <kevin.m.meredith@gmail.com>
|
||||
Kevin Woblick <mail@kovah.de>
|
||||
Kir Kolyshkin <kolyshkin@gmail.com>
|
||||
Kir Kolyshkin <kolyshkin@gmail.com> <kir@openvz.org>
|
||||
Kir Kolyshkin <kolyshkin@gmail.com> <kolyshkin@users.noreply.github.com>
|
||||
Konrad Kleine <konrad.wilhelm.kleine@gmail.com> <kwk@users.noreply.github.com>
|
||||
Konstantin Gribov <grossws@gmail.com>
|
||||
Konstantin Pelykh <kpelykh@zettaset.com>
|
||||
Kotaro Yoshimatsu <kotaro.yoshimatsu@gmail.com>
|
||||
Kunal Kushwaha <kushwaha_kunal_v7@lab.ntt.co.jp> <kunal.kushwaha@gmail.com>
|
||||
Lajos Papp <lajos.papp@sequenceiq.com> <lalyos@yahoo.com>
|
||||
Lei Jitang <leijitang@huawei.com>
|
||||
Lei Jitang <leijitang@huawei.com> <leijitang@gmail.com>
|
||||
Liang Mingqiang <mqliang.zju@gmail.com>
|
||||
Liang-Chi Hsieh <viirya@gmail.com>
|
||||
Liao Qingwei <liaoqingwei@huawei.com>
|
||||
Linus Heckemann <lheckemann@twig-world.com>
|
||||
Linus Heckemann <lheckemann@twig-world.com> <anonymouse2048@gmail.com>
|
||||
Lokesh Mandvekar <lsm5@fedoraproject.org> <lsm5@redhat.com>
|
||||
Lorenzo Fontana <lo@linux.com> <fontanalorenzo@me.com>
|
||||
Louis Opter <kalessin@kalessin.fr>
|
||||
Louis Opter <kalessin@kalessin.fr> <louis@dotcloud.com>
|
||||
Luca Favatella <luca.favatella@erlang-solutions.com> <lucafavatella@users.noreply.github.com>
|
||||
Luke Marsden <me@lukemarsden.net> <luke@digital-crocus.com>
|
||||
Lyn <energylyn@zju.edu.cn>
|
||||
Lynda O'Leary <lyndaoleary29@gmail.com>
|
||||
Lynda O'Leary <lyndaoleary29@gmail.com> <lyndaoleary@hotmail.com>
|
||||
Ma Müller <mueller-ma@users.noreply.github.com>
|
||||
Maciej Kalisz <maciej.d.kalisz@gmail.com>
|
||||
Maciej Kalisz <maciej.d.kalisz@gmail.com> <mdkalish@users.noreply.github.com>
|
||||
Madhan Raj Mookkandy <MadhanRaj.Mookkandy@microsoft.com> <madhanm@microsoft.com>
|
||||
Madhu Venugopal <madhu@socketplane.io> <madhu@docker.com>
|
||||
Mageee <fangpuyi@foxmail.com> <21521230.zju.edu.cn>
|
||||
Mansi Nahar <mmn4185@rit.edu> <mansi.nahar@macbookpro-mansinahar.local>
|
||||
Mansi Nahar <mmn4185@rit.edu> <mansinahar@users.noreply.github.com>
|
||||
Marc Abramowitz <marc@marc-abramowitz.com> <msabramo@gmail.com>
|
||||
Marcelo Horacio Fortino <info@fortinux.com> <fortinux@users.noreply.github.com>
|
||||
Marcus Linke <marcus.linke@gmx.de>
|
||||
Marianna Tessel <mtesselh@gmail.com>
|
||||
Marius Ileana <marius.ileana@gmail.com>
|
||||
Mark Oates <fl0yd@me.com>
|
||||
Markan Patel <mpatel678@gmail.com>
|
||||
Markus Kortlang <hyp3rdino@googlemail.com> <markus.kortlang@lhsystems.com>
|
||||
Marsh Macy <marsma@microsoft.com>
|
||||
Martin Redmond <redmond.martin@gmail.com> <martin@tinychat.com>
|
||||
Martin Redmond <redmond.martin@gmail.com> <xgithub@redmond5.com>
|
||||
Mary Anthony <mary.anthony@docker.com> <mary@docker.com>
|
||||
Mary Anthony <mary.anthony@docker.com> <moxieandmore@gmail.com>
|
||||
Mary Anthony <mary.anthony@docker.com> moxiegirl <mary@docker.com>
|
||||
Mateusz Major <apkd@users.noreply.github.com>
|
||||
Matt Bentley <matt.bentley@docker.com> <mbentley@mbentley.net>
|
||||
Matt Schurenko <matt.schurenko@gmail.com>
|
||||
Matt Williams <mattyw@me.com>
|
||||
Matt Williams <mattyw@me.com> <gh@mattyw.net>
|
||||
Matthew Heon <mheon@redhat.com> <mheon@mheonlaptop.redhat.com>
|
||||
Matthew Mosesohn <raytrac3r@gmail.com>
|
||||
Matthew Mueller <mattmuelle@gmail.com>
|
||||
Matthias Kühnle <git.nivoc@neverbox.com> <kuehnle@online.de>
|
||||
Mauricio Garavaglia <mauricio@medallia.com> <mauriciogaravaglia@gmail.com>
|
||||
Michael Crosby <michael@docker.com> <crosby.michael@gmail.com>
|
||||
Michael Crosby <michael@docker.com> <crosbymichael@gmail.com>
|
||||
Michael Crosby <michael@docker.com> <michael@crosbymichael.com>
|
||||
Michael Hudson-Doyle <michael.hudson@canonical.com> <michael.hudson@linaro.org>
|
||||
Michael Huettermann <michael@huettermann.net>
|
||||
Michael Käufl <docker@c.michael-kaeufl.de> <michael-k@users.noreply.github.com>
|
||||
Michael Spetsiotis <michael_spets@hotmail.com>
|
||||
Michal Minář <miminar@redhat.com>
|
||||
Miguel Angel Alvarez Cabrerizo <doncicuto@gmail.com> <30386061+doncicuto@users.noreply.github.com>
|
||||
Miguel Angel Fernández <elmendalerenda@gmail.com>
|
||||
Mihai Borobocea <MihaiBorob@gmail.com> <MihaiBorobocea@gmail.com>
|
||||
Mike Casas <mkcsas0@gmail.com> <mikecasas@users.noreply.github.com>
|
||||
Mike Goelzer <mike.goelzer@docker.com> <mgoelzer@docker.com>
|
||||
Milind Chawre <milindchawre@gmail.com>
|
||||
Misty Stanley-Jones <misty@docker.com> <misty@apache.org>
|
||||
Mohit Soni <mosoni@ebay.com> <mohitsoni1989@gmail.com>
|
||||
Moorthy RS <rsmoorthy@gmail.com> <rsmoorthy@users.noreply.github.com>
|
||||
Morten Hekkvang <morten.hekkvang@sbab.se>
|
||||
Morten Hekkvang <morten.hekkvang@sbab.se> <morten.hekkvang@tele2.com>
|
||||
Moysés Borges <moysesb@gmail.com>
|
||||
Moysés Borges <moysesb@gmail.com> <moyses.furtado@wplex.com.br>
|
||||
Nace Oroz <orkica@gmail.com>
|
||||
Nathan LeClaire <nathan.leclaire@docker.com> <nathan.leclaire@gmail.com>
|
||||
Nathan LeClaire <nathan.leclaire@docker.com> <nathanleclaire@gmail.com>
|
||||
Neil Horman <nhorman@tuxdriver.com> <nhorman@hmswarspite.think-freely.org>
|
||||
Nick Russo <nicholasjamesrusso@gmail.com> <nicholasrusso@icloud.com>
|
||||
Nicolas Borboën <ponsfrilus@gmail.com> <ponsfrilus@users.noreply.github.com>
|
||||
Nigel Poulton <nigelpoulton@hotmail.com>
|
||||
Nik Nyby <nikolas@gnu.org> <nnyby@columbia.edu>
|
||||
Nolan Darilek <nolan@thewordnerd.info>
|
||||
O.S. Tezer <ostezer@gmail.com>
|
||||
O.S. Tezer <ostezer@gmail.com> <ostezer@users.noreply.github.com>
|
||||
Oh Jinkyun <tintypemolly@gmail.com> <tintypemolly@Ohui-MacBook-Pro.local>
|
||||
Ouyang Liduo <oyld0210@163.com>
|
||||
Patrick Stapleton <github@gdi2290.com>
|
||||
Paul Liljenberg <liljenberg.paul@gmail.com> <letters@paulnotcom.se>
|
||||
Pavel Tikhomirov <ptikhomirov@virtuozzo.com> <ptikhomirov@parallels.com>
|
||||
Pawel Konczalski <mail@konczalski.de>
|
||||
Peter Choi <phkchoi89@gmail.com> <reikani@Peters-MacBook-Pro.local>
|
||||
Peter Dave Hello <hsu@peterdavehello.org> <PeterDaveHello@users.noreply.github.com>
|
||||
Peter Hsu <shhsu@microsoft.com>
|
||||
Peter Jaffe <pjaffe@nevo.com>
|
||||
Peter Nagy <xificurC@gmail.com> <pnagy@gratex.com>
|
||||
Peter Waller <p@pwaller.net> <peter@scraperwiki.com>
|
||||
Phil Estes <estesp@linux.vnet.ibm.com> <estesp@gmail.com>
|
||||
Philip Alexander Etling <paetling@gmail.com>
|
||||
Philipp Gillé <philipp.gille@gmail.com> <philippgille@users.noreply.github.com>
|
||||
Qiang Huang <h.huangqiang@huawei.com>
|
||||
Qiang Huang <h.huangqiang@huawei.com> <qhuang@10.0.2.15>
|
||||
Ray Tsang <rayt@google.com> <saturnism@users.noreply.github.com>
|
||||
Renaud Gaubert <rgaubert@nvidia.com> <renaud.gaubert@gmail.com>
|
||||
Robert Terhaar <rterhaar@atlanticdynamic.com> <robbyt@users.noreply.github.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>
|
||||
Roman Dudin <katrmr@gmail.com> <decadent@users.noreply.github.com>
|
||||
Ross Boucher <rboucher@gmail.com>
|
||||
Runshen Zhu <runshen.zhu@gmail.com>
|
||||
Ryan Stelly <ryan.stelly@live.com>
|
||||
Sakeven Jiang <jc5930@sina.cn>
|
||||
Samarth Shah <samashah@microsoft.com>
|
||||
Sandeep Bansal <sabansal@microsoft.com>
|
||||
Sandeep Bansal <sabansal@microsoft.com> <msabansal@microsoft.com>
|
||||
Sargun Dhillon <sargun@netflix.com> <sargun@sargun.me>
|
||||
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> <thaJeztah@users.noreply.github.com>
|
||||
Shaun Kaasten <shaunk@gmail.com>
|
||||
Shawn Landden <shawn@churchofgit.com> <shawnlandden@gmail.com>
|
||||
Shengbo Song <thomassong@tencent.com>
|
||||
Shengbo Song <thomassong@tencent.com> <mymneo@163.com>
|
||||
Shih-Yuan Lee <fourdollars@gmail.com>
|
||||
Shishir Mahajan <shishir.mahajan@redhat.com> <smahajan@redhat.com>
|
||||
Shukui Yang <yangshukui@huawei.com>
|
||||
Shuwei Hao <haosw@cn.ibm.com>
|
||||
Shuwei Hao <haosw@cn.ibm.com> <haoshuwei24@gmail.com>
|
||||
Sidhartha Mani <sidharthamn@gmail.com>
|
||||
Silvin Lubecki <silvin.lubecki@docker.com>
|
||||
Silvin Lubecki <silvin.lubecki@docker.com> <31478878+silvin-lubecki@users.noreply.github.com>
|
||||
Sjoerd Langkemper <sjoerd-github@linuxonly.nl> <sjoerd@byte.nl>
|
||||
Solomon Hykes <solomon@docker.com> <s@docker.com>
|
||||
Solomon Hykes <solomon@docker.com> <solomon.hykes@dotcloud.com>
|
||||
Solomon Hykes <solomon@docker.com> <solomon@dotcloud.com>
|
||||
Soshi Katsuta <soshi.katsuta@gmail.com>
|
||||
Soshi Katsuta <soshi.katsuta@gmail.com> <katsuta_soshi@cyberagent.co.jp>
|
||||
Sridhar Ratnakumar <sridharr@activestate.com>
|
||||
Sridhar Ratnakumar <sridharr@activestate.com> <github@srid.name>
|
||||
Srini Brahmaroutu <srbrahma@us.ibm.com> <sbrahma@us.ibm.com>
|
||||
Srinivasan Srivatsan <srinivasan.srivatsan@hpe.com> <srinsriv@users.noreply.github.com>
|
||||
Stefan Berger <stefanb@linux.vnet.ibm.com>
|
||||
Stefan Berger <stefanb@linux.vnet.ibm.com> <stefanb@us.ibm.com>
|
||||
Stefan J. Wernli <swernli@microsoft.com> <swernli@ntdev.microsoft.com>
|
||||
Stefan S. <tronicum@user.github.com>
|
||||
Stefan Scherer <stefan.scherer@docker.com>
|
||||
Stefan Scherer <stefan.scherer@docker.com> <scherer_stefan@icloud.com>
|
||||
Stephen Day <stevvooe@gmail.com>
|
||||
Stephen Day <stevvooe@gmail.com> <stephen.day@docker.com>
|
||||
Stephen Day <stevvooe@gmail.com> <stevvooe@users.noreply.github.com>
|
||||
Steve Desmond <steve@vtsv.ca> <stevedesmond-ca@users.noreply.github.com>
|
||||
Steve Richards <steve.richards@docker.com> stevejr <>
|
||||
Sun Gengze <690388648@qq.com>
|
||||
Sun Jianbo <wonderflow.sun@gmail.com>
|
||||
Sun Jianbo <wonderflow.sun@gmail.com> <wonderflow@zju.edu.cn>
|
||||
Sunny Gogoi <indiasuny000@gmail.com>
|
||||
Sunny Gogoi <indiasuny000@gmail.com> <me@darkowlzz.space>
|
||||
Sven Dowideit <SvenDowideit@home.org.au>
|
||||
Sven Dowideit <SvenDowideit@home.org.au> <sven@t440s.home.gateway>
|
||||
Sven Dowideit <SvenDowideit@home.org.au> <SvenDowideit@docker.com>
|
||||
Sven Dowideit <SvenDowideit@home.org.au> <SvenDowideit@fosiki.com>
|
||||
Sven Dowideit <SvenDowideit@home.org.au> <SvenDowideit@home.org.au>
|
||||
Sven Dowideit <SvenDowideit@home.org.au> <SvenDowideit@users.noreply.github.com>
|
||||
Sven Dowideit <SvenDowideit@home.org.au> <¨SvenDowideit@home.org.au¨>
|
||||
Sylvain Bellemare <sylvain@ascribe.io>
|
||||
Sylvain Bellemare <sylvain@ascribe.io> <sylvain.bellemare@ezeep.com>
|
||||
Tangi Colin <tangicolin@gmail.com>
|
||||
Tejesh Mehta <tejesh.mehta@gmail.com> <tj@init.me>
|
||||
Teppei Fukuda <knqyf263@gmail.com>
|
||||
Thatcher Peskens <thatcher@docker.com>
|
||||
Thatcher Peskens <thatcher@docker.com> <thatcher@dotcloud.com>
|
||||
Thatcher Peskens <thatcher@docker.com> <thatcher@gmx.net>
|
||||
Thomas Gazagnaire <thomas@gazagnaire.org> <thomas@gazagnaire.com>
|
||||
Thomas Krzero <thomas.kovatchitch@gmail.com>
|
||||
Thomas Léveil <thomasleveil@gmail.com>
|
||||
Thomas Léveil <thomasleveil@gmail.com> <thomasleveil@users.noreply.github.com>
|
||||
Thomas Riccardi <thomas@deepomatic.com>
|
||||
Thomas Riccardi <thomas@deepomatic.com> <riccardi@systran.fr>
|
||||
Tibor Vass <teabee89@gmail.com> <tibor@docker.com>
|
||||
Tibor Vass <teabee89@gmail.com> <tiborvass@users.noreply.github.com>
|
||||
Tim Bart <tim@fewagainstmany.com>
|
||||
Tim Bosse <taim@bosboot.org> <maztaim@users.noreply.github.com>
|
||||
Tim Ruffles <oi@truffles.me.uk> <timruffles@googlemail.com>
|
||||
Tim Terhorst <mynamewastaken+git@gmail.com>
|
||||
Tim Zju <21651152@zju.edu.cn>
|
||||
Timothy Hobbs <timothyhobbs@seznam.cz>
|
||||
Toli Kuznets <toli@docker.com>
|
||||
Tom Barlow <tomwbarlow@gmail.com>
|
||||
Tom Milligan <code@tommilligan.net>
|
||||
Tom Milligan <code@tommilligan.net> <tommilligan@users.noreply.github.com>
|
||||
Tom Sweeney <tsweeney@redhat.com>
|
||||
Tõnis Tiigi <tonistiigi@gmail.com>
|
||||
Trishna Guha <trishnaguha17@gmail.com>
|
||||
Tristan Carel <tristan@cogniteev.com>
|
||||
Tristan Carel <tristan@cogniteev.com> <tristan.carel@gmail.com>
|
||||
Ulrich Bareth <ulrich.bareth@gmail.com>
|
||||
Ulrich Bareth <ulrich.bareth@gmail.com> <usb79@users.noreply.github.com>
|
||||
Umesh Yadav <umesh4257@gmail.com>
|
||||
Umesh Yadav <umesh4257@gmail.com> <dungeonmaster18@users.noreply.github.com>
|
||||
Victor Lyuboslavsky <victor@victoreda.com>
|
||||
Victor Vieux <victor.vieux@docker.com> <dev@vvieux.com>
|
||||
Victor Vieux <victor.vieux@docker.com> <victor.vieux@dotcloud.com>
|
||||
Victor Vieux <victor.vieux@docker.com> <victor@docker.com>
|
||||
Victor Vieux <victor.vieux@docker.com> <victor@dotcloud.com>
|
||||
Victor Vieux <victor.vieux@docker.com> <victorvieux@gmail.com>
|
||||
Victor Vieux <victor.vieux@docker.com> <vieux@docker.com>
|
||||
Viktor Vojnovski <viktor.vojnovski@amadeus.com> <vojnovski@gmail.com>
|
||||
Vincent Batts <vbatts@redhat.com> <vbatts@hashbangbash.com>
|
||||
Vincent Bernat <Vincent.Bernat@exoscale.ch> <bernat@luffy.cx>
|
||||
Vincent Bernat <Vincent.Bernat@exoscale.ch> <vincent@bernat.im>
|
||||
Vincent Demeester <vincent.demeester@docker.com> <vincent+github@demeester.fr>
|
||||
Vincent Demeester <vincent.demeester@docker.com> <vincent@demeester.fr>
|
||||
Vincent Demeester <vincent.demeester@docker.com> <vincent@sbr.pm>
|
||||
Vishnu Kannan <vishnuk@google.com>
|
||||
Vladimir Rutsky <altsysrq@gmail.com> <iamironbob@gmail.com>
|
||||
Walter Stanish <walter@pratyeka.org>
|
||||
Wang Guoliang <liangcszzu@163.com>
|
||||
Wang Jie <wangjie5@chinaskycloud.com>
|
||||
Wang Lei <wanglei@tenxcloud.com>
|
||||
Wang Ping <present.wp@icloud.com>
|
||||
Wang Xing <hzwangxing@corp.netease.com> <root@localhost>
|
||||
Wang Yuexiao <wang.yuexiao@zte.com.cn>
|
||||
Wayne Chang <wayne@neverfear.org>
|
||||
Wayne Song <wsong@docker.com> <wsong@users.noreply.github.com>
|
||||
Wei Wu <wuwei4455@gmail.com> cizixs <cizixs@163.com>
|
||||
Wenjun Tang <tangwj2@lenovo.com> <dodia@163.com>
|
||||
Wewang Xiaorenfine <wang.xiaoren@zte.com.cn>
|
||||
Will Weaver <monkey@buildingbananas.com>
|
||||
Xianglin Gao <xlgao@zju.edu.cn>
|
||||
Xianlu Bird <xianlubird@gmail.com>
|
||||
Xiaoyu Zhang <zhang.xiaoyu33@zte.com.cn>
|
||||
Xuecong Liao <satorulogic@gmail.com>
|
||||
Yamasaki Masahide <masahide.y@gmail.com>
|
||||
Yao Zaiyong <yaozaiyong@hotmail.com>
|
||||
Yassine Tijani <yasstij11@gmail.com>
|
||||
Yazhong Liu <yorkiefixer@gmail.com>
|
||||
Yestin Sun <sunyi0804@gmail.com> <yestin.sun@polyera.com>
|
||||
Yi EungJun <eungjun.yi@navercorp.com> <semtlenori@gmail.com>
|
||||
Ying Li <ying.li@docker.com>
|
||||
Ying Li <ying.li@docker.com> <cyli@twistedmatrix.com>
|
||||
Yong Tang <yong.tang.github@outlook.com> <yongtang@users.noreply.github.com>
|
||||
Yosef Fertel <yfertel@gmail.com> <frosforever@users.noreply.github.com>
|
||||
Yu Changchun <yuchangchun1@huawei.com>
|
||||
Yu Chengxia <yuchengxia@huawei.com>
|
||||
Yu Peng <yu.peng36@zte.com.cn>
|
||||
Yu Peng <yu.peng36@zte.com.cn> <yupeng36@zte.com.cn>
|
||||
Yue Zhang <zy675793960@yeah.net>
|
||||
Zachary Jaffee <zjaffee@us.ibm.com> <zij@case.edu>
|
||||
Zachary Jaffee <zjaffee@us.ibm.com> <zjaffee@apache.org>
|
||||
ZhangHang <stevezhang2014@gmail.com>
|
||||
Zhenkun Bi <bi.zhenkun@zte.com.cn>
|
||||
Zhou Hao <zhouhao@cn.fujitsu.com>
|
||||
Zhoulin Xie <zhoulin.xie@daocloud.io>
|
||||
Zhu Kunjia <zhu.kunjia@zte.com.cn>
|
||||
Zou Yu <zouyu7@huawei.com>
|
|
@ -0,0 +1,771 @@
|
|||
# This file lists all individuals having contributed content to the repository.
|
||||
# For how it is generated, see `scripts/docs/generate-authors.sh`.
|
||||
|
||||
Aanand Prasad <aanand.prasad@gmail.com>
|
||||
Aaron L. Xu <liker.xu@foxmail.com>
|
||||
Aaron Lehmann <aaron.lehmann@docker.com>
|
||||
Aaron.L.Xu <likexu@harmonycloud.cn>
|
||||
Abdur Rehman <abdur_rehman@mentor.com>
|
||||
Abhinandan Prativadi <abhi@docker.com>
|
||||
Abin Shahab <ashahab@altiscale.com>
|
||||
Abreto FU <public@abreto.email>
|
||||
Ace Tang <aceapril@126.com>
|
||||
Addam Hardy <addam.hardy@gmail.com>
|
||||
Adolfo Ochagavía <aochagavia92@gmail.com>
|
||||
Adrian Plata <adrian.plata@docker.com>
|
||||
Adrien Duermael <adrien@duermael.com>
|
||||
Adrien Folie <folie.adrien@gmail.com>
|
||||
Ahmet Alp Balkan <ahmetb@microsoft.com>
|
||||
Aidan Feldman <aidan.feldman@gmail.com>
|
||||
Aidan Hobson Sayers <aidanhs@cantab.net>
|
||||
AJ Bowen <aj@soulshake.net>
|
||||
Akhil Mohan <akhil.mohan@mayadata.io>
|
||||
Akihiro Suda <akihiro.suda.cz@hco.ntt.co.jp>
|
||||
Akim Demaille <akim.demaille@docker.com>
|
||||
Alan Thompson <cloojure@gmail.com>
|
||||
Albert Callarisa <shark234@gmail.com>
|
||||
Albin Kerouanton <albin@akerouanton.name>
|
||||
Aleksa Sarai <asarai@suse.de>
|
||||
Aleksander Piotrowski <apiotrowski312@gmail.com>
|
||||
Alessandro Boch <aboch@tetrationanalytics.com>
|
||||
Alex Mavrogiannis <alex.mavrogiannis@docker.com>
|
||||
Alex Mayer <amayer5125@gmail.com>
|
||||
Alexander Boyd <alex@opengroove.org>
|
||||
Alexander Larsson <alexl@redhat.com>
|
||||
Alexander Morozov <lk4d4@docker.com>
|
||||
Alexander Ryabov <i@sepa.spb.ru>
|
||||
Alexandre González <agonzalezro@gmail.com>
|
||||
Alfred Landrum <alfred.landrum@docker.com>
|
||||
Alicia Lauerman <alicia@eta.im>
|
||||
Allen Sun <allensun.shl@alibaba-inc.com>
|
||||
Alvin Deng <alvin.q.deng@utexas.edu>
|
||||
Amen Belayneh <amenbelayneh@gmail.com>
|
||||
Amir Goldstein <amir73il@aquasec.com>
|
||||
Amit Krishnan <amit.krishnan@oracle.com>
|
||||
Amit Shukla <amit.shukla@docker.com>
|
||||
Amy Lindburg <amy.lindburg@docker.com>
|
||||
Anca Iordache <anca.iordache@docker.com>
|
||||
Anda Xu <anda.xu@docker.com>
|
||||
Andrea Luzzardi <aluzzardi@gmail.com>
|
||||
Andreas Köhler <andi5.py@gmx.net>
|
||||
Andrew France <andrew@avito.co.uk>
|
||||
Andrew Hsu <andrewhsu@docker.com>
|
||||
Andrew Macpherson <hopscotch23@gmail.com>
|
||||
Andrew McDonnell <bugs@andrewmcdonnell.net>
|
||||
Andrew Po <absourd.noise@gmail.com>
|
||||
Andrey Petrov <andrey.petrov@shazow.net>
|
||||
Andrii Berehuliak <berkusandrew@gmail.com>
|
||||
André Martins <aanm90@gmail.com>
|
||||
Andy Goldstein <agoldste@redhat.com>
|
||||
Andy Rothfusz <github@developersupport.net>
|
||||
Anil Madhavapeddy <anil@recoil.org>
|
||||
Ankush Agarwal <ankushagarwal11@gmail.com>
|
||||
Anne Henmi <anne.henmi@docker.com>
|
||||
Anton Polonskiy <anton.polonskiy@gmail.com>
|
||||
Antonio Murdaca <antonio.murdaca@gmail.com>
|
||||
Antonis Kalipetis <akalipetis@gmail.com>
|
||||
Anusha Ragunathan <anusha.ragunathan@docker.com>
|
||||
Ao Li <la9249@163.com>
|
||||
Arash Deshmeh <adeshmeh@ca.ibm.com>
|
||||
Arko Dasgupta <arko.dasgupta@docker.com>
|
||||
Arnaud Porterie <arnaud.porterie@docker.com>
|
||||
Arthur Peka <arthur.peka@outlook.com>
|
||||
Ashwini Oruganti <ashwini.oruganti@gmail.com>
|
||||
Azat Khuyiyakhmetov <shadow_uz@mail.ru>
|
||||
Bardia Keyoumarsi <bkeyouma@ucsc.edu>
|
||||
Barnaby Gray <barnaby@pickle.me.uk>
|
||||
Bastiaan Bakker <bbakker@xebia.com>
|
||||
BastianHofmann <bastianhofmann@me.com>
|
||||
Ben Bonnefoy <frenchben@docker.com>
|
||||
Ben Creasy <ben@bencreasy.com>
|
||||
Ben Firshman <ben@firshman.co.uk>
|
||||
Benjamin Boudreau <boudreau.benjamin@gmail.com>
|
||||
Benoit Sigoure <tsunanet@gmail.com>
|
||||
Bhumika Bayani <bhumikabayani@gmail.com>
|
||||
Bill Wang <ozbillwang@gmail.com>
|
||||
Bin Liu <liubin0329@gmail.com>
|
||||
Bingshen Wang <bingshen.wbs@alibaba-inc.com>
|
||||
Boaz Shuster <ripcurld.github@gmail.com>
|
||||
Bogdan Anton <contact@bogdananton.ro>
|
||||
Boris Pruessmann <boris@pruessmann.org>
|
||||
Bradley Cicenas <bradley.cicenas@gmail.com>
|
||||
Brandon Mitchell <git@bmitch.net>
|
||||
Brandon Philips <brandon.philips@coreos.com>
|
||||
Brent Salisbury <brent.salisbury@docker.com>
|
||||
Bret Fisher <bret@bretfisher.com>
|
||||
Brian (bex) Exelbierd <bexelbie@redhat.com>
|
||||
Brian Goff <cpuguy83@gmail.com>
|
||||
Brian Wieder <brian@4wieders.com>
|
||||
Bryan Bess <squarejaw@bsbess.com>
|
||||
Bryan Boreham <bjboreham@gmail.com>
|
||||
Bryan Murphy <bmurphy1976@gmail.com>
|
||||
bryfry <bryon.fryer@gmail.com>
|
||||
Cameron Spear <cameronspear@gmail.com>
|
||||
Cao Weiwei <cao.weiwei30@zte.com.cn>
|
||||
Carlo Mion <mion00@gmail.com>
|
||||
Carlos Alexandro Becker <caarlos0@gmail.com>
|
||||
Carlos de Paula <me@carlosedp.com>
|
||||
Ce Gao <ce.gao@outlook.com>
|
||||
Cedric Davies <cedricda@microsoft.com>
|
||||
Cezar Sa Espinola <cezarsa@gmail.com>
|
||||
Chad Faragher <wyckster@hotmail.com>
|
||||
Chao Wang <wangchao.fnst@cn.fujitsu.com>
|
||||
Charles Chan <charleswhchan@users.noreply.github.com>
|
||||
Charles Law <claw@conduce.com>
|
||||
Charles Smith <charles.smith@docker.com>
|
||||
Charlie Drage <charlie@charliedrage.com>
|
||||
ChaYoung You <yousbe@gmail.com>
|
||||
Chen Chuanliang <chen.chuanliang@zte.com.cn>
|
||||
Chen Hanxiao <chenhanxiao@cn.fujitsu.com>
|
||||
Chen Mingjie <chenmingjie0828@163.com>
|
||||
Chen Qiu <cheney-90@hotmail.com>
|
||||
Chris Gavin <chris@chrisgavin.me>
|
||||
Chris Gibson <chris@chrisg.io>
|
||||
Chris McKinnel <chrismckinnel@gmail.com>
|
||||
Chris Snow <chsnow123@gmail.com>
|
||||
Chris Weyl <cweyl@alumni.drew.edu>
|
||||
Christian Persson <saser@live.se>
|
||||
Christian Stefanescu <st.chris@gmail.com>
|
||||
Christophe Robin <crobin@nekoo.com>
|
||||
Christophe Vidal <kriss@krizalys.com>
|
||||
Christopher Biscardi <biscarch@sketcht.com>
|
||||
Christopher Crone <christopher.crone@docker.com>
|
||||
Christopher Jones <tophj@linux.vnet.ibm.com>
|
||||
Christy Norman <christy@linux.vnet.ibm.com>
|
||||
Chun Chen <ramichen@tencent.com>
|
||||
Clinton Kitson <clintonskitson@gmail.com>
|
||||
Coenraad Loubser <coenraad@wish.org.za>
|
||||
Colin Hebert <hebert.colin@gmail.com>
|
||||
Collin Guarino <collin.guarino@gmail.com>
|
||||
Colm Hally <colmhally@gmail.com>
|
||||
Comical Derskeal <27731088+derskeal@users.noreply.github.com>
|
||||
Corey Farrell <git@cfware.com>
|
||||
Corey Quon <corey.quon@docker.com>
|
||||
Craig Wilhite <crwilhit@microsoft.com>
|
||||
Cristian Staretu <cristian.staretu@gmail.com>
|
||||
Daehyeok Mun <daehyeok@gmail.com>
|
||||
Dafydd Crosby <dtcrsby@gmail.com>
|
||||
Daisuke Ito <itodaisuke00@gmail.com>
|
||||
dalanlan <dalanlan925@gmail.com>
|
||||
Damien Nadé <github@livna.org>
|
||||
Dan Cotora <dan@bluevision.ro>
|
||||
Daniel Artine <daniel.artine@ufrj.br>
|
||||
Daniel Cassidy <mail@danielcassidy.me.uk>
|
||||
Daniel Dao <dqminh@cloudflare.com>
|
||||
Daniel Farrell <dfarrell@redhat.com>
|
||||
Daniel Gasienica <daniel@gasienica.ch>
|
||||
Daniel Goosen <daniel.goosen@surveysampling.com>
|
||||
Daniel Helfand <dhelfand@redhat.com>
|
||||
Daniel Hiltgen <daniel.hiltgen@docker.com>
|
||||
Daniel J Walsh <dwalsh@redhat.com>
|
||||
Daniel Nephin <dnephin@docker.com>
|
||||
Daniel Norberg <dano@spotify.com>
|
||||
Daniel Watkins <daniel@daniel-watkins.co.uk>
|
||||
Daniel Zhang <jmzwcn@gmail.com>
|
||||
Daniil Nikolenko <qoo2p5@gmail.com>
|
||||
Danny Berger <dpb587@gmail.com>
|
||||
Darren Shepherd <darren.s.shepherd@gmail.com>
|
||||
Darren Stahl <darst@microsoft.com>
|
||||
Dattatraya Kumbhar <dattatraya.kumbhar@gslab.com>
|
||||
Dave Goodchild <buddhamagnet@gmail.com>
|
||||
Dave Henderson <dhenderson@gmail.com>
|
||||
Dave Tucker <dt@docker.com>
|
||||
David Beitey <david@davidjb.com>
|
||||
David Calavera <david.calavera@gmail.com>
|
||||
David Cramer <davcrame@cisco.com>
|
||||
David Dooling <dooling@gmail.com>
|
||||
David Gageot <david@gageot.net>
|
||||
David Lechner <david@lechnology.com>
|
||||
David Scott <dave@recoil.org>
|
||||
David Sheets <dsheets@docker.com>
|
||||
David Williamson <david.williamson@docker.com>
|
||||
David Xia <dxia@spotify.com>
|
||||
David Young <yangboh@cn.ibm.com>
|
||||
Deng Guangxing <dengguangxing@huawei.com>
|
||||
Denis Defreyne <denis@soundcloud.com>
|
||||
Denis Gladkikh <denis@gladkikh.email>
|
||||
Denis Ollier <larchunix@users.noreply.github.com>
|
||||
Dennis Docter <dennis@d23.nl>
|
||||
Derek McGowan <derek@mcgstyle.net>
|
||||
Deshi Xiao <dxiao@redhat.com>
|
||||
Dharmit Shah <shahdharmit@gmail.com>
|
||||
Dhawal Yogesh Bhanushali <dbhanushali@vmware.com>
|
||||
Dieter Reuter <dieter.reuter@me.com>
|
||||
Dima Stopel <dima@twistlock.com>
|
||||
Dimitry Andric <d.andric@activevideo.com>
|
||||
Ding Fei <dingfei@stars.org.cn>
|
||||
Diogo Monica <diogo@docker.com>
|
||||
Djordje Lukic <djordje.lukic@docker.com>
|
||||
Dmitry Gusev <dmitry.gusev@gmail.com>
|
||||
Dmitry Smirnov <onlyjob@member.fsf.org>
|
||||
Dmitry V. Krivenok <krivenok.dmitry@gmail.com>
|
||||
Dominik Braun <dominik.braun@nbsp.de>
|
||||
Don Kjer <don.kjer@gmail.com>
|
||||
Dong Chen <dongluo.chen@docker.com>
|
||||
Doug Davis <dug@us.ibm.com>
|
||||
Drew Erny <derny@mirantis.com>
|
||||
Ed Costello <epc@epcostello.com>
|
||||
Elango Sivanandam <elango.siva@docker.com>
|
||||
Eli Uriegas <eli.uriegas@docker.com>
|
||||
Eli Uriegas <seemethere101@gmail.com>
|
||||
Elias Faxö <elias.faxo@tre.se>
|
||||
Elliot Luo <956941328@qq.com>
|
||||
Eric Curtin <ericcurtin17@gmail.com>
|
||||
Eric G. Noriega <enoriega@vizuri.com>
|
||||
Eric Rosenberg <ehaydenr@gmail.com>
|
||||
Eric Sage <eric.david.sage@gmail.com>
|
||||
Eric-Olivier Lamey <eo@lamey.me>
|
||||
Erica Windisch <erica@windisch.us>
|
||||
Erik Hollensbe <github@hollensbe.org>
|
||||
Erik St. Martin <alakriti@gmail.com>
|
||||
Essam A. Hassan <es.hassan187@gmail.com>
|
||||
Ethan Haynes <ethanhaynes@alumni.harvard.edu>
|
||||
Euan Kemp <euank@euank.com>
|
||||
Eugene Yakubovich <eugene.yakubovich@coreos.com>
|
||||
Evan Allrich <evan@unguku.com>
|
||||
Evan Hazlett <ejhazlett@gmail.com>
|
||||
Evan Krall <krall@yelp.com>
|
||||
Evelyn Xu <evelynhsu21@gmail.com>
|
||||
Everett Toews <everett.toews@rackspace.com>
|
||||
Fabio Falci <fabiofalci@gmail.com>
|
||||
Fabrizio Soppelsa <fsoppelsa@mirantis.com>
|
||||
Felix Hupfeld <felix@quobyte.com>
|
||||
Felix Rabe <felix@rabe.io>
|
||||
Filip Jareš <filipjares@gmail.com>
|
||||
Flavio Crisciani <flavio.crisciani@docker.com>
|
||||
Florian Klein <florian.klein@free.fr>
|
||||
Forest Johnson <fjohnson@peoplenetonline.com>
|
||||
Foysal Iqbal <foysal.iqbal.fb@gmail.com>
|
||||
François Scala <francois.scala@swiss-as.com>
|
||||
Fred Lifton <fred.lifton@docker.com>
|
||||
Frederic Hemberger <mail@frederic-hemberger.de>
|
||||
Frederick F. Kautz IV <fkautz@redhat.com>
|
||||
Frederik Nordahl Jul Sabroe <frederikns@gmail.com>
|
||||
Frieder Bluemle <frieder.bluemle@gmail.com>
|
||||
Gabriel Nicolas Avellaneda <avellaneda.gabriel@gmail.com>
|
||||
Gaetan de Villele <gdevillele@gmail.com>
|
||||
Gang Qiao <qiaohai8866@gmail.com>
|
||||
Gary Schaetz <gary@schaetzkc.com>
|
||||
Genki Takiuchi <genki@s21g.com>
|
||||
George MacRorie <gmacr31@gmail.com>
|
||||
George Xie <georgexsh@gmail.com>
|
||||
Gianluca Borello <g.borello@gmail.com>
|
||||
Gildas Cuisinier <gildas.cuisinier@gcuisinier.net>
|
||||
Goksu Toprak <goksu.toprak@docker.com>
|
||||
Gou Rao <gou@portworx.com>
|
||||
Grant Reaber <grant.reaber@gmail.com>
|
||||
Greg Pflaum <gpflaum@users.noreply.github.com>
|
||||
Guilhem Lettron <guilhem+github@lettron.fr>
|
||||
Guillaume J. Charmes <guillaume.charmes@docker.com>
|
||||
Guillaume Le Floch <glfloch@gmail.com>
|
||||
gwx296173 <gaojing3@huawei.com>
|
||||
Günther Jungbluth <gunther@gameslabs.net>
|
||||
Hakan Özler <hakan.ozler@kodcu.com>
|
||||
Hao Zhang <21521210@zju.edu.cn>
|
||||
Harald Albers <github@albersweb.de>
|
||||
Harold Cooper <hrldcpr@gmail.com>
|
||||
Harry Zhang <harryz@hyper.sh>
|
||||
He Simei <hesimei@zju.edu.cn>
|
||||
Hector S <hfsam88@gmail.com>
|
||||
Helen Xie <chenjg@harmonycloud.cn>
|
||||
Henning Sprang <henning.sprang@gmail.com>
|
||||
Henry N <henrynmail-github@yahoo.de>
|
||||
Hernan Garcia <hernandanielg@gmail.com>
|
||||
Hongbin Lu <hongbin034@gmail.com>
|
||||
Hu Keping <hukeping@huawei.com>
|
||||
Huayi Zhang <irachex@gmail.com>
|
||||
Hugo Gabriel Eyherabide <hugogabriel.eyherabide@gmail.com>
|
||||
huqun <huqun@zju.edu.cn>
|
||||
Huu Nguyen <huu@prismskylabs.com>
|
||||
Hyzhou Zhy <hyzhou.zhy@alibaba-inc.com>
|
||||
Ian Campbell <ian.campbell@docker.com>
|
||||
Ian Philpot <ian.philpot@microsoft.com>
|
||||
Ignacio Capurro <icapurrofagian@gmail.com>
|
||||
Ilya Dmitrichenko <errordeveloper@gmail.com>
|
||||
Ilya Khlopotov <ilya.khlopotov@gmail.com>
|
||||
Ilya Sotkov <ilya@sotkov.com>
|
||||
Ioan Eugen Stan <eu@ieugen.ro>
|
||||
Isabel Jimenez <contact.isabeljimenez@gmail.com>
|
||||
Ivan Grcic <igrcic@gmail.com>
|
||||
Ivan Markin <sw@nogoegst.net>
|
||||
Jacob Atzen <jacob@jacobatzen.dk>
|
||||
Jacob Tomlinson <jacob@tom.linson.uk>
|
||||
Jaivish Kothari <janonymous.codevulture@gmail.com>
|
||||
Jake Lambert <jake.lambert@volusion.com>
|
||||
Jake Sanders <jsand@google.com>
|
||||
James Nesbitt <james.nesbitt@wunderkraut.com>
|
||||
James Turnbull <james@lovedthanlost.net>
|
||||
Jamie Hannaford <jamie@limetree.org>
|
||||
Jan Koprowski <jan.koprowski@gmail.com>
|
||||
Jan Pazdziora <jpazdziora@redhat.com>
|
||||
Jan-Jaap Driessen <janjaapdriessen@gmail.com>
|
||||
Jana Radhakrishnan <mrjana@docker.com>
|
||||
Jared Hocutt <jaredh@netapp.com>
|
||||
Jasmine Hegman <jasmine@jhegman.com>
|
||||
Jason Heiss <jheiss@aput.net>
|
||||
Jason Plum <jplum@devonit.com>
|
||||
Jay Kamat <github@jgkamat.33mail.com>
|
||||
Jean Rouge <rougej+github@gmail.com>
|
||||
Jean-Christophe Sirot <jean-christophe.sirot@docker.com>
|
||||
Jean-Pierre Huynh <jean-pierre.huynh@ounet.fr>
|
||||
Jeff Lindsay <progrium@gmail.com>
|
||||
Jeff Nickoloff <jeff.nickoloff@gmail.com>
|
||||
Jeff Silberman <jsilberm@gmail.com>
|
||||
Jeremy Chambers <jeremy@thehipbot.com>
|
||||
Jeremy Unruh <jeremybunruh@gmail.com>
|
||||
Jeremy Yallop <yallop@docker.com>
|
||||
Jeroen Franse <jeroenfranse@gmail.com>
|
||||
Jesse Adametz <jesseadametz@gmail.com>
|
||||
Jessica Frazelle <jess@oxide.computer>
|
||||
Jezeniel Zapanta <jpzapanta22@gmail.com>
|
||||
Jian Zhang <zhangjian.fnst@cn.fujitsu.com>
|
||||
Jie Luo <luo612@zju.edu.cn>
|
||||
Jilles Oldenbeuving <ojilles@gmail.com>
|
||||
Jim Galasyn <jim.galasyn@docker.com>
|
||||
Jimmy Leger <jimmy.leger@gmail.com>
|
||||
Jimmy Song <rootsongjc@gmail.com>
|
||||
jimmyxian <jimmyxian2004@yahoo.com.cn>
|
||||
Jintao Zhang <zhangjintao9020@gmail.com>
|
||||
Joao Fernandes <joao.fernandes@docker.com>
|
||||
Joe Abbey <joe.abbey@gmail.com>
|
||||
Joe Doliner <jdoliner@pachyderm.io>
|
||||
Joe Gordon <joe.gordon0@gmail.com>
|
||||
Joel Handwell <joelhandwell@gmail.com>
|
||||
Joey Geiger <jgeiger@gmail.com>
|
||||
Joffrey F <joffrey@docker.com>
|
||||
Johan Euphrosine <proppy@google.com>
|
||||
Johannes 'fish' Ziemke <github@freigeist.org>
|
||||
John Feminella <jxf@jxf.me>
|
||||
John Harris <john@johnharris.io>
|
||||
John Howard <github@lowenna.com>
|
||||
John Laswell <john.n.laswell@gmail.com>
|
||||
John Maguire <jmaguire@duosecurity.com>
|
||||
John Mulhausen <john@docker.com>
|
||||
John Starks <jostarks@microsoft.com>
|
||||
John Stephens <johnstep@docker.com>
|
||||
John Tims <john.k.tims@gmail.com>
|
||||
John V. Martinez <jvmatl@gmail.com>
|
||||
John Willis <john.willis@docker.com>
|
||||
Jon Johnson <jonjohnson@google.com>
|
||||
Jonatas Baldin <jonatas.baldin@gmail.com>
|
||||
Jonathan Boulle <jonathanboulle@gmail.com>
|
||||
Jonathan Lee <jonjohn1232009@gmail.com>
|
||||
Jonathan Lomas <jonathan@floatinglomas.ca>
|
||||
Jonathan McCrohan <jmccrohan@gmail.com>
|
||||
Jonh Wendell <jonh.wendell@redhat.com>
|
||||
Jordan Jennings <jjn2009@gmail.com>
|
||||
Jose J. Escobar <53836904+jescobar-docker@users.noreply.github.com>
|
||||
Joseph Kern <jkern@semafour.net>
|
||||
Josh Bodah <jb3689@yahoo.com>
|
||||
Josh Chorlton <jchorlton@gmail.com>
|
||||
Josh Hawn <josh.hawn@docker.com>
|
||||
Josh Horwitz <horwitz@addthis.com>
|
||||
Josh Soref <jsoref@gmail.com>
|
||||
Julien Barbier <write0@gmail.com>
|
||||
Julien Kassar <github@kassisol.com>
|
||||
Julien Maitrehenry <julien.maitrehenry@me.com>
|
||||
Justas Brazauskas <brazauskasjustas@gmail.com>
|
||||
Justin Cormack <justin.cormack@docker.com>
|
||||
Justin Simonelis <justin.p.simonelis@gmail.com>
|
||||
Justyn Temme <justyntemme@gmail.com>
|
||||
Jyrki Puttonen <jyrkiput@gmail.com>
|
||||
Jérémie Drouet <jeremie.drouet@gmail.com>
|
||||
Jérôme Petazzoni <jerome.petazzoni@docker.com>
|
||||
Jörg Thalheim <joerg@higgsboson.tk>
|
||||
Kai Blin <kai@samba.org>
|
||||
Kai Qiang Wu (Kennan) <wkq5325@gmail.com>
|
||||
Kara Alexandra <kalexandra@us.ibm.com>
|
||||
Kareem Khazem <karkhaz@karkhaz.com>
|
||||
Karthik Nayak <Karthik.188@gmail.com>
|
||||
Kat Samperi <kat.samperi@gmail.com>
|
||||
Kathryn Spiers <kathryn@spiers.me>
|
||||
Katie McLaughlin <katie@glasnt.com>
|
||||
Ke Xu <leonhartx.k@gmail.com>
|
||||
Kei Ohmura <ohmura.kei@gmail.com>
|
||||
Keith Hudgins <greenman@greenman.org>
|
||||
Ken Cochrane <kencochrane@gmail.com>
|
||||
Ken ICHIKAWA <ichikawa.ken@jp.fujitsu.com>
|
||||
Kenfe-Mickaël Laventure <mickael.laventure@gmail.com>
|
||||
Kevin Burke <kev@inburke.com>
|
||||
Kevin Feyrer <kevin.feyrer@btinternet.com>
|
||||
Kevin Kern <kaiwentan@harmonycloud.cn>
|
||||
Kevin Kirsche <Kev.Kirsche+GitHub@gmail.com>
|
||||
Kevin Meredith <kevin.m.meredith@gmail.com>
|
||||
Kevin Richardson <kevin@kevinrichardson.co>
|
||||
Kevin Woblick <mail@kovah.de>
|
||||
khaled souf <khaled.souf@gmail.com>
|
||||
Kim Eik <kim@heldig.org>
|
||||
Kir Kolyshkin <kolyshkin@gmail.com>
|
||||
Kotaro Yoshimatsu <kotaro.yoshimatsu@gmail.com>
|
||||
Krasi Georgiev <krasi@vip-consult.solutions>
|
||||
Kris-Mikael Krister <krismikael@protonmail.com>
|
||||
Kun Zhang <zkazure@gmail.com>
|
||||
Kunal Kushwaha <kushwaha_kunal_v7@lab.ntt.co.jp>
|
||||
Lachlan Cooper <lachlancooper@gmail.com>
|
||||
Lai Jiangshan <jiangshanlai@gmail.com>
|
||||
Lars Kellogg-Stedman <lars@redhat.com>
|
||||
Laura Frank <ljfrank@gmail.com>
|
||||
Laurent Erignoux <lerignoux@gmail.com>
|
||||
Lee Gaines <eightlimbed@gmail.com>
|
||||
Lei Jitang <leijitang@huawei.com>
|
||||
Lennie <github@consolejunkie.net>
|
||||
Leo Gallucci <elgalu3@gmail.com>
|
||||
Lewis Daly <lewisdaly@me.com>
|
||||
Li Yi <denverdino@gmail.com>
|
||||
Li Yi <weiyuan.yl@alibaba-inc.com>
|
||||
Liang-Chi Hsieh <viirya@gmail.com>
|
||||
Lifubang <lifubang@acmcoder.com>
|
||||
Lihua Tang <lhtang@alauda.io>
|
||||
Lily Guo <lily.guo@docker.com>
|
||||
Lin Lu <doraalin@163.com>
|
||||
Linus Heckemann <lheckemann@twig-world.com>
|
||||
Liping Xue <lipingxue@gmail.com>
|
||||
Liron Levin <liron@twistlock.com>
|
||||
liwenqi <vikilwq@zju.edu.cn>
|
||||
lixiaobing10051267 <li.xiaobing1@zte.com.cn>
|
||||
Lloyd Dewolf <foolswisdom@gmail.com>
|
||||
Lorenzo Fontana <lo@linux.com>
|
||||
Louis Opter <kalessin@kalessin.fr>
|
||||
Luca Favatella <luca.favatella@erlang-solutions.com>
|
||||
Luca Marturana <lucamarturana@gmail.com>
|
||||
Lucas Chan <lucas-github@lucaschan.com>
|
||||
Luka Hartwig <mail@lukahartwig.de>
|
||||
Lukas Heeren <lukas-heeren@hotmail.com>
|
||||
Lukasz Zajaczkowski <Lukasz.Zajaczkowski@ts.fujitsu.com>
|
||||
Lydell Manganti <LydellManganti@users.noreply.github.com>
|
||||
Lénaïc Huard <lhuard@amadeus.com>
|
||||
Ma Shimiao <mashimiao.fnst@cn.fujitsu.com>
|
||||
Mabin <bin.ma@huawei.com>
|
||||
Maciej Kalisz <maciej.d.kalisz@gmail.com>
|
||||
Madhav Puri <madhav.puri@gmail.com>
|
||||
Madhu Venugopal <madhu@socketplane.io>
|
||||
Madhur Batra <madhurbatra097@gmail.com>
|
||||
Malte Janduda <mail@janduda.net>
|
||||
Manjunath A Kumatagi <mkumatag@in.ibm.com>
|
||||
Mansi Nahar <mmn4185@rit.edu>
|
||||
mapk0y <mapk0y@gmail.com>
|
||||
Marc Bihlmaier <marc.bihlmaier@reddoxx.com>
|
||||
Marco Mariani <marco.mariani@alterway.fr>
|
||||
Marco Vedovati <mvedovati@suse.com>
|
||||
Marcus Martins <marcus@docker.com>
|
||||
Marianna Tessel <mtesselh@gmail.com>
|
||||
Marius Ileana <marius.ileana@gmail.com>
|
||||
Marius Sturm <marius@graylog.com>
|
||||
Mark Oates <fl0yd@me.com>
|
||||
Marsh Macy <marsma@microsoft.com>
|
||||
Martin Mosegaard Amdisen <martin.amdisen@praqma.com>
|
||||
Mary Anthony <mary.anthony@docker.com>
|
||||
Mason Fish <mason.fish@docker.com>
|
||||
Mason Malone <mason.malone@gmail.com>
|
||||
Mateusz Major <apkd@users.noreply.github.com>
|
||||
Mathieu Champlon <mathieu.champlon@docker.com>
|
||||
Matt Gucci <matt9ucci@gmail.com>
|
||||
Matt Robenolt <matt@ydekproductions.com>
|
||||
Matteo Orefice <matteo.orefice@bites4bits.software>
|
||||
Matthew Heon <mheon@redhat.com>
|
||||
Matthieu Hauglustaine <matt.hauglustaine@gmail.com>
|
||||
Mauro Porras P <mauroporrasp@gmail.com>
|
||||
Max Shytikov <mshytikov@gmail.com>
|
||||
Maxime Petazzoni <max@signalfuse.com>
|
||||
Mei ChunTao <mei.chuntao@zte.com.cn>
|
||||
Micah Zoltu <micah@newrelic.com>
|
||||
Michael A. Smith <michael@smith-li.com>
|
||||
Michael Bridgen <mikeb@squaremobius.net>
|
||||
Michael Crosby <michael@docker.com>
|
||||
Michael Friis <friism@gmail.com>
|
||||
Michael Irwin <mikesir87@gmail.com>
|
||||
Michael Käufl <docker@c.michael-kaeufl.de>
|
||||
Michael Prokop <github@michael-prokop.at>
|
||||
Michael Scharf <github@scharf.gr>
|
||||
Michael Spetsiotis <michael_spets@hotmail.com>
|
||||
Michael Steinert <mike.steinert@gmail.com>
|
||||
Michael West <mwest@mdsol.com>
|
||||
Michal Minář <miminar@redhat.com>
|
||||
Michał Czeraszkiewicz <czerasz@gmail.com>
|
||||
Miguel Angel Alvarez Cabrerizo <doncicuto@gmail.com>
|
||||
Mihai Borobocea <MihaiBorob@gmail.com>
|
||||
Mihuleacc Sergiu <mihuleac.sergiu@gmail.com>
|
||||
Mike Brown <brownwm@us.ibm.com>
|
||||
Mike Casas <mkcsas0@gmail.com>
|
||||
Mike Danese <mikedanese@google.com>
|
||||
Mike Dillon <mike@embody.org>
|
||||
Mike Goelzer <mike.goelzer@docker.com>
|
||||
Mike MacCana <mike.maccana@gmail.com>
|
||||
mikelinjie <294893458@qq.com>
|
||||
Mikhail Vasin <vasin@cloud-tv.ru>
|
||||
Milind Chawre <milindchawre@gmail.com>
|
||||
Mindaugas Rukas <momomg@gmail.com>
|
||||
Miroslav Gula <miroslav.gula@naytrolabs.com>
|
||||
Misty Stanley-Jones <misty@docker.com>
|
||||
Mohammad Banikazemi <mb@us.ibm.com>
|
||||
Mohammed Aaqib Ansari <maaquib@gmail.com>
|
||||
Mohini Anne Dsouza <mohini3917@gmail.com>
|
||||
Moorthy RS <rsmoorthy@gmail.com>
|
||||
Morgan Bauer <mbauer@us.ibm.com>
|
||||
Morten Hekkvang <morten.hekkvang@sbab.se>
|
||||
Moysés Borges <moysesb@gmail.com>
|
||||
Mrunal Patel <mrunalp@gmail.com>
|
||||
muicoder <muicoder@gmail.com>
|
||||
Muthukumar R <muthur@gmail.com>
|
||||
Máximo Cuadros <mcuadros@gmail.com>
|
||||
Mårten Cassel <marten.cassel@gmail.com>
|
||||
Nace Oroz <orkica@gmail.com>
|
||||
Nahum Shalman <nshalman@omniti.com>
|
||||
Nalin Dahyabhai <nalin@redhat.com>
|
||||
Nao YONASHIRO <owan.orisano@gmail.com>
|
||||
Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>
|
||||
Natalie Parker <nparker@omnifone.com>
|
||||
Nate Brennand <nate.brennand@clever.com>
|
||||
Nathan Hsieh <hsieh.nathan@gmail.com>
|
||||
Nathan LeClaire <nathan.leclaire@docker.com>
|
||||
Nathan McCauley <nathan.mccauley@docker.com>
|
||||
Neil Peterson <neilpeterson@outlook.com>
|
||||
Nick Adcock <nick.adcock@docker.com>
|
||||
Nico Stapelbroek <nstapelbroek@gmail.com>
|
||||
Nicola Kabar <nicolaka@gmail.com>
|
||||
Nicolas Borboën <ponsfrilus@gmail.com>
|
||||
Nicolas De Loof <nicolas.deloof@gmail.com>
|
||||
Nikhil Chawla <chawlanikhil24@gmail.com>
|
||||
Nikolas Garofil <nikolas.garofil@uantwerpen.be>
|
||||
Nikolay Milovanov <nmil@itransformers.net>
|
||||
Nir Soffer <nsoffer@redhat.com>
|
||||
Nishant Totla <nishanttotla@gmail.com>
|
||||
NIWA Hideyuki <niwa.niwa@nifty.ne.jp>
|
||||
Noah Treuhaft <noah.treuhaft@docker.com>
|
||||
O.S. Tezer <ostezer@gmail.com>
|
||||
Odin Ugedal <odin@ugedal.com>
|
||||
ohmystack <jun.jiang02@ele.me>
|
||||
Olle Jonsson <olle.jonsson@gmail.com>
|
||||
Olli Janatuinen <olli.janatuinen@gmail.com>
|
||||
Oscar Wieman <oscrx@icloud.com>
|
||||
Otto Kekäläinen <otto@seravo.fi>
|
||||
Ovidio Mallo <ovidio.mallo@gmail.com>
|
||||
Pascal Borreli <pascal@borreli.com>
|
||||
Patrick Böänziger <patrick.baenziger@bsi-software.com>
|
||||
Patrick Hemmer <patrick.hemmer@gmail.com>
|
||||
Patrick Lang <plang@microsoft.com>
|
||||
Paul <paul9869@gmail.com>
|
||||
Paul Kehrer <paul.l.kehrer@gmail.com>
|
||||
Paul Lietar <paul@lietar.net>
|
||||
Paul Mulders <justinkb@gmail.com>
|
||||
Paul Weaver <pauweave@cisco.com>
|
||||
Pavel Pospisil <pospispa@gmail.com>
|
||||
Paweł Szczekutowicz <pszczekutowicz@gmail.com>
|
||||
Peeyush Gupta <gpeeyush@linux.vnet.ibm.com>
|
||||
Per Lundberg <per.lundberg@ecraft.com>
|
||||
Peter Edge <peter.edge@gmail.com>
|
||||
Peter Hsu <shhsu@microsoft.com>
|
||||
Peter Jaffe <pjaffe@nevo.com>
|
||||
Peter Kehl <peter.kehl@gmail.com>
|
||||
Peter Nagy <xificurC@gmail.com>
|
||||
Peter Salvatore <peter@psftw.com>
|
||||
Peter Waller <p@pwaller.net>
|
||||
Phil Estes <estesp@linux.vnet.ibm.com>
|
||||
Philip Alexander Etling <paetling@gmail.com>
|
||||
Philipp Gillé <philipp.gille@gmail.com>
|
||||
Philipp Schmied <pschmied@schutzwerk.com>
|
||||
pidster <pid@pidster.com>
|
||||
pixelistik <pixelistik@users.noreply.github.com>
|
||||
Pratik Karki <prertik@outlook.com>
|
||||
Prayag Verma <prayag.verma@gmail.com>
|
||||
Preston Cowley <preston.cowley@sony.com>
|
||||
Pure White <daniel48@126.com>
|
||||
Qiang Huang <h.huangqiang@huawei.com>
|
||||
Qinglan Peng <qinglanpeng@zju.edu.cn>
|
||||
qudongfang <qudongfang@gmail.com>
|
||||
Raghavendra K T <raghavendra.kt@linux.vnet.ibm.com>
|
||||
Rahul Zoldyck <rahulzoldyck@gmail.com>
|
||||
Ravi Shekhar Jethani <rsjethani@gmail.com>
|
||||
Ray Tsang <rayt@google.com>
|
||||
Reficul <xuzhenglun@gmail.com>
|
||||
Remy Suen <remy.suen@gmail.com>
|
||||
Renaud Gaubert <rgaubert@nvidia.com>
|
||||
Ricardo N Feliciano <FelicianoTech@gmail.com>
|
||||
Rich Moyse <rich@moyse.us>
|
||||
Richard Mathie <richard.mathie@amey.co.uk>
|
||||
Richard Scothern <richard.scothern@gmail.com>
|
||||
Rick Wieman <git@rickw.nl>
|
||||
Ritesh H Shukla <sritesh@vmware.com>
|
||||
Riyaz Faizullabhoy <riyaz.faizullabhoy@docker.com>
|
||||
Rob Gulewich <rgulewich@netflix.com>
|
||||
Robert Wallis <smilingrob@gmail.com>
|
||||
Robin Naundorf <r.naundorf@fh-muenster.de>
|
||||
Robin Speekenbrink <robin@kingsquare.nl>
|
||||
Rodolfo Ortiz <rodolfo.ortiz@definityfirst.com>
|
||||
Rogelio Canedo <rcanedo@mappy.priv>
|
||||
Rohan Verma <hello@rohanverma.net>
|
||||
Roland Kammerer <roland.kammerer@linbit.com>
|
||||
Roman Dudin <katrmr@gmail.com>
|
||||
Rory Hunter <roryhunter2@gmail.com>
|
||||
Ross Boucher <rboucher@gmail.com>
|
||||
Rubens Figueiredo <r.figueiredo.52@gmail.com>
|
||||
Rui Cao <ruicao@alauda.io>
|
||||
Ryan Belgrave <rmb1993@gmail.com>
|
||||
Ryan Detzel <ryan.detzel@gmail.com>
|
||||
Ryan Stelly <ryan.stelly@live.com>
|
||||
Ryan Wilson-Perkin <ryanwilsonperkin@gmail.com>
|
||||
Ryan Zhang <ryan.zhang@docker.com>
|
||||
Sainath Grandhi <sainath.grandhi@intel.com>
|
||||
Sakeven Jiang <jc5930@sina.cn>
|
||||
Sally O'Malley <somalley@redhat.com>
|
||||
Sam Neirinck <sam@samneirinck.com>
|
||||
Samarth Shah <samashah@microsoft.com>
|
||||
Sambuddha Basu <sambuddhabasu1@gmail.com>
|
||||
Sami Tabet <salph.tabet@gmail.com>
|
||||
Samuel Cochran <sj26@sj26.com>
|
||||
Samuel Karp <skarp@amazon.com>
|
||||
Santhosh Manohar <santhosh@docker.com>
|
||||
Sargun Dhillon <sargun@netflix.com>
|
||||
Saswat Bhattacharya <sas.saswat@gmail.com>
|
||||
Scott Brenner <scott@scottbrenner.me>
|
||||
Scott Collier <emailscottcollier@gmail.com>
|
||||
Sean Christopherson <sean.j.christopherson@intel.com>
|
||||
Sean Rodman <srodman7689@gmail.com>
|
||||
Sebastiaan van Stijn <github@gone.nl>
|
||||
Sergey Tryuber <Sergeant007@users.noreply.github.com>
|
||||
Serhat Gülçiçek <serhat25@gmail.com>
|
||||
Sevki Hasirci <s@sevki.org>
|
||||
Shaun Kaasten <shaunk@gmail.com>
|
||||
Sheng Yang <sheng@yasker.org>
|
||||
Shijiang Wei <mountkin@gmail.com>
|
||||
Shishir Mahajan <shishir.mahajan@redhat.com>
|
||||
Shoubhik Bose <sbose78@gmail.com>
|
||||
Shukui Yang <yangshukui@huawei.com>
|
||||
Sian Lerk Lau <kiawin@gmail.com>
|
||||
Sidhartha Mani <sidharthamn@gmail.com>
|
||||
sidharthamani <sid@rancher.com>
|
||||
Silvin Lubecki <silvin.lubecki@docker.com>
|
||||
Simei He <hesimei@zju.edu.cn>
|
||||
Simon Ferquel <simon.ferquel@docker.com>
|
||||
Simon Heimberg <simon.heimberg@heimberg-ea.ch>
|
||||
Sindhu S <sindhus@live.in>
|
||||
Slava Semushin <semushin@redhat.com>
|
||||
Solomon Hykes <solomon@docker.com>
|
||||
Song Gao <song@gao.io>
|
||||
Spencer Brown <spencer@spencerbrown.org>
|
||||
squeegels <1674195+squeegels@users.noreply.github.com>
|
||||
Srini Brahmaroutu <srbrahma@us.ibm.com>
|
||||
Stefan S. <tronicum@user.github.com>
|
||||
Stefan Scherer <stefan.scherer@docker.com>
|
||||
Stefan Weil <sw@weilnetz.de>
|
||||
Stephane Jeandeaux <stephane.jeandeaux@gmail.com>
|
||||
Stephen Day <stevvooe@gmail.com>
|
||||
Stephen Rust <srust@blockbridge.com>
|
||||
Steve Durrheimer <s.durrheimer@gmail.com>
|
||||
Steve Richards <steve.richards@docker.com>
|
||||
Steven Burgess <steven.a.burgess@hotmail.com>
|
||||
Subhajit Ghosh <isubuz.g@gmail.com>
|
||||
Sun Jianbo <wonderflow.sun@gmail.com>
|
||||
Sune Keller <absukl@almbrand.dk>
|
||||
Sungwon Han <sungwon.han@navercorp.com>
|
||||
Sunny Gogoi <indiasuny000@gmail.com>
|
||||
Sven Dowideit <SvenDowideit@home.org.au>
|
||||
Sylvain Baubeau <sbaubeau@redhat.com>
|
||||
Sébastien HOUZÉ <cto@verylastroom.com>
|
||||
T K Sourabh <sourabhtk37@gmail.com>
|
||||
TAGOMORI Satoshi <tagomoris@gmail.com>
|
||||
taiji-tech <csuhqg@foxmail.com>
|
||||
Taylor Jones <monitorjbl@gmail.com>
|
||||
Tejaswini Duggaraju <naduggar@microsoft.com>
|
||||
Tengfei Wang <tfwang@alauda.io>
|
||||
Teppei Fukuda <knqyf263@gmail.com>
|
||||
Thatcher Peskens <thatcher@docker.com>
|
||||
Thibault Coupin <thibault.coupin@gmail.com>
|
||||
Thomas Gazagnaire <thomas@gazagnaire.org>
|
||||
Thomas Krzero <thomas.kovatchitch@gmail.com>
|
||||
Thomas Leonard <thomas.leonard@docker.com>
|
||||
Thomas Léveil <thomasleveil@gmail.com>
|
||||
Thomas Riccardi <thomas@deepomatic.com>
|
||||
Thomas Swift <tgs242@gmail.com>
|
||||
Tianon Gravi <admwiggin@gmail.com>
|
||||
Tianyi Wang <capkurmagati@gmail.com>
|
||||
Tibor Vass <teabee89@gmail.com>
|
||||
Tim Dettrick <t.dettrick@uq.edu.au>
|
||||
Tim Hockin <thockin@google.com>
|
||||
Tim Sampson <tim@sampson.fi>
|
||||
Tim Smith <timbot@google.com>
|
||||
Tim Waugh <twaugh@redhat.com>
|
||||
Tim Wraight <tim.wraight@tangentlabs.co.uk>
|
||||
timfeirg <kkcocogogo@gmail.com>
|
||||
Timothy Hobbs <timothyhobbs@seznam.cz>
|
||||
Tobias Bradtke <webwurst@gmail.com>
|
||||
Tobias Gesellchen <tobias@gesellix.de>
|
||||
Todd Whiteman <todd.whiteman@joyent.com>
|
||||
Tom Denham <tom@tomdee.co.uk>
|
||||
Tom Fotherby <tom+github@peopleperhour.com>
|
||||
Tom Klingenberg <tklingenberg@lastflood.net>
|
||||
Tom Milligan <code@tommilligan.net>
|
||||
Tom X. Tobin <tomxtobin@tomxtobin.com>
|
||||
Tomas Tomecek <ttomecek@redhat.com>
|
||||
Tomasz Kopczynski <tomek@kopczynski.net.pl>
|
||||
Tomáš Hrčka <thrcka@redhat.com>
|
||||
Tony Abboud <tdabboud@hotmail.com>
|
||||
Tõnis Tiigi <tonistiigi@gmail.com>
|
||||
Trapier Marshall <trapier.marshall@docker.com>
|
||||
Travis Cline <travis.cline@gmail.com>
|
||||
Tristan Carel <tristan@cogniteev.com>
|
||||
Tycho Andersen <tycho@docker.com>
|
||||
Tycho Andersen <tycho@tycho.ws>
|
||||
uhayate <uhayate.gong@daocloud.io>
|
||||
Ulrich Bareth <ulrich.bareth@gmail.com>
|
||||
Ulysses Souza <ulysses.souza@docker.com>
|
||||
Umesh Yadav <umesh4257@gmail.com>
|
||||
Valentin Lorentz <progval+git@progval.net>
|
||||
Venkateswara Reddy Bukkasamudram <bukkasamudram@outlook.com>
|
||||
Veres Lajos <vlajos@gmail.com>
|
||||
Victor Vieux <victor.vieux@docker.com>
|
||||
Victoria Bialas <victoria.bialas@docker.com>
|
||||
Viktor Stanchev <me@viktorstanchev.com>
|
||||
Vimal Raghubir <vraghubir0418@gmail.com>
|
||||
Vincent Batts <vbatts@redhat.com>
|
||||
Vincent Bernat <Vincent.Bernat@exoscale.ch>
|
||||
Vincent Demeester <vincent.demeester@docker.com>
|
||||
Vincent Woo <me@vincentwoo.com>
|
||||
Vishnu Kannan <vishnuk@google.com>
|
||||
Vivek Goyal <vgoyal@redhat.com>
|
||||
Wang Jie <wangjie5@chinaskycloud.com>
|
||||
Wang Lei <wanglei@tenxcloud.com>
|
||||
Wang Long <long.wanglong@huawei.com>
|
||||
Wang Ping <present.wp@icloud.com>
|
||||
Wang Xing <hzwangxing@corp.netease.com>
|
||||
Wang Yuexiao <wang.yuexiao@zte.com.cn>
|
||||
Wang Yumu <37442693@qq.com>
|
||||
Wataru Ishida <ishida.wataru@lab.ntt.co.jp>
|
||||
Wayne Song <wsong@docker.com>
|
||||
Wen Cheng Ma <wenchma@cn.ibm.com>
|
||||
Wenzhi Liang <wenzhi.liang@gmail.com>
|
||||
Wes Morgan <cap10morgan@gmail.com>
|
||||
Wewang Xiaorenfine <wang.xiaoren@zte.com.cn>
|
||||
William Henry <whenry@redhat.com>
|
||||
Xianglin Gao <xlgao@zju.edu.cn>
|
||||
Xiaodong Liu <liuxiaodong@loongson.cn>
|
||||
Xiaodong Zhang <a4012017@sina.com>
|
||||
Xiaoxi He <xxhe@alauda.io>
|
||||
Xinbo Weng <xihuanbo_0521@zju.edu.cn>
|
||||
Xuecong Liao <satorulogic@gmail.com>
|
||||
Yan Feng <yanfeng2@huawei.com>
|
||||
Yanqiang Miao <miao.yanqiang@zte.com.cn>
|
||||
Yassine Tijani <yasstij11@gmail.com>
|
||||
Yi EungJun <eungjun.yi@navercorp.com>
|
||||
Ying Li <ying.li@docker.com>
|
||||
Yong Tang <yong.tang.github@outlook.com>
|
||||
Yosef Fertel <yfertel@gmail.com>
|
||||
Yu Peng <yu.peng36@zte.com.cn>
|
||||
Yuan Sun <sunyuan3@huawei.com>
|
||||
Yue Zhang <zy675793960@yeah.net>
|
||||
Yunxiang Huang <hyxqshk@vip.qq.com>
|
||||
Zachary Romero <zacromero3@gmail.com>
|
||||
Zander Mackie <zmackie@gmail.com>
|
||||
zebrilee <zebrilee@gmail.com>
|
||||
Zhang Kun <zkazure@gmail.com>
|
||||
Zhang Wei <zhangwei555@huawei.com>
|
||||
Zhang Wentao <zhangwentao234@huawei.com>
|
||||
ZhangHang <stevezhang2014@gmail.com>
|
||||
zhenghenghuo <zhenghenghuo@zju.edu.cn>
|
||||
Zhou Hao <zhouhao@cn.fujitsu.com>
|
||||
Zhoulin Xie <zhoulin.xie@daocloud.io>
|
||||
Zhu Guihua <zhugh.fnst@cn.fujitsu.com>
|
||||
Álex González <agonzalezro@gmail.com>
|
||||
Álvaro Lázaro <alvaro.lazaro.g@gmail.com>
|
||||
Átila Camurça Alves <camurca.home@gmail.com>
|
||||
徐俊杰 <paco.xu@daocloud.io>
|
|
@ -0,0 +1,365 @@
|
|||
# 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
|
||||
guidelines useful to experienced open source contributors. Finally, make sure
|
||||
you read our [community guidelines](#docker-community-guidelines) before you
|
||||
start participating.
|
||||
|
||||
## Topics
|
||||
|
||||
* [Reporting Security Issues](#reporting-security-issues)
|
||||
* [Design and Cleanup Proposals](#design-and-cleanup-proposals)
|
||||
* [Reporting Issues](#reporting-other-issues)
|
||||
* [Quick Contribution Tips and Guidelines](#quick-contribution-tips-and-guidelines)
|
||||
* [Community Guidelines](#docker-community-guidelines)
|
||||
|
||||
## Reporting security issues
|
||||
|
||||
The Docker maintainers take security seriously. If you discover a security
|
||||
issue, please bring it to their attention right away!
|
||||
|
||||
Please **DO NOT** file a public issue, instead send your report privately to
|
||||
[security@docker.com](mailto:security@docker.com).
|
||||
|
||||
Security reports are greatly appreciated and we will publicly thank you for it.
|
||||
We also like to send gifts—if you're into Docker schwag, make sure to let
|
||||
us know. We currently do not offer a paid security bounty program, but are not
|
||||
ruling it out in the future.
|
||||
|
||||
|
||||
## Reporting other issues
|
||||
|
||||
A great way to contribute to the project is to send a detailed report when you
|
||||
encounter an issue. We always appreciate a well-written, thorough bug report,
|
||||
and will thank you for it!
|
||||
|
||||
Check that [our issue database](https://github.com/docker/cli/issues)
|
||||
doesn't already include that problem or suggestion before submitting an issue.
|
||||
If you find a match, you can use the "subscribe" button to get notified on
|
||||
updates. Do *not* leave random "+1" or "I have this too" comments, as they
|
||||
only clutter the discussion, and don't help resolving it. However, if you
|
||||
have ways to reproduce the issue or have additional information that may help
|
||||
resolving the issue, please leave a comment.
|
||||
|
||||
When reporting issues, always include:
|
||||
|
||||
* The output of `docker version`.
|
||||
* The output of `docker info`.
|
||||
|
||||
Also include the steps required to reproduce the problem if possible and
|
||||
applicable. This information will help us review and fix your issue faster.
|
||||
When sending lengthy log-files, consider posting them as a gist (https://gist.github.com).
|
||||
Don't forget to remove sensitive data from your logfiles before posting (you can
|
||||
replace those parts with "REDACTED").
|
||||
|
||||
## Quick contribution tips and guidelines
|
||||
|
||||
This section gives the experienced contributor some tips and guidelines.
|
||||
|
||||
### Pull requests are always welcome
|
||||
|
||||
Not sure if that typo is worth a pull request? Found a bug and know how to fix
|
||||
it? Do it! We will appreciate it. Any significant improvement should be
|
||||
documented as [a GitHub issue](https://github.com/docker/cli/issues) before
|
||||
anybody starts working on it.
|
||||
|
||||
We are always thrilled to receive pull requests. We do our best to process them
|
||||
quickly. If your pull request is not accepted on the first try,
|
||||
don't get discouraged! Our contributor's guide explains [the review process we
|
||||
use for simple changes](https://docs.docker.com/opensource/workflow/make-a-contribution/).
|
||||
|
||||
### Talking to other Docker users and contributors
|
||||
|
||||
<table class="tg">
|
||||
<col width="45%">
|
||||
<col width="65%">
|
||||
<tr>
|
||||
<td>Forums</td>
|
||||
<td>
|
||||
A public forum for users to discuss questions and explore current design patterns and
|
||||
best practices about Docker and related projects in the Docker Ecosystem. To participate,
|
||||
just log in with your Docker Hub account on <a href="https://forums.docker.com" target="_blank">https://forums.docker.com</a>.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Community Slack</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>.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Twitter</td>
|
||||
<td>
|
||||
You can follow <a href="https://twitter.com/docker/" target="_blank">Docker's Twitter feed</a>
|
||||
to get updates on our products. You can also tweet us questions or just
|
||||
share blogs or stories.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Stack Overflow</td>
|
||||
<td>
|
||||
Stack Overflow has over 17000 Docker questions listed. We regularly
|
||||
monitor <a href="https://stackoverflow.com/search?tab=newest&q=docker" target="_blank">Docker questions</a>
|
||||
and so do many other knowledgeable Docker users.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
### Conventions
|
||||
|
||||
Fork the repository and make changes on your fork in a feature branch:
|
||||
|
||||
- If it's a bug fix branch, name it XXXX-something where XXXX is the number of
|
||||
the issue.
|
||||
- If it's a feature branch, create an enhancement issue to announce
|
||||
your intentions, and name it XXXX-something where XXXX is the number of the
|
||||
issue.
|
||||
|
||||
Submit unit tests for your changes. Go has a great test framework built in; use
|
||||
it! Take a look at existing tests for inspiration. [Run the full test
|
||||
suite](README.md) on your branch before
|
||||
submitting a pull request.
|
||||
|
||||
Update the documentation when creating or modifying features. Test your
|
||||
documentation changes for clarity, concision, and correctness, as well as a
|
||||
clean documentation build. See our contributors guide for [our style
|
||||
guide](https://docs.docker.com/opensource/doc-style) and instructions on [building
|
||||
the documentation](https://docs.docker.com/opensource/project/test-and-docs/#build-and-test-the-documentation).
|
||||
|
||||
Write clean code. Universally formatted code promotes ease of writing, reading,
|
||||
and maintenance. Always run `gofmt -s -w file.go` on each changed file before
|
||||
committing your changes. Most editors have plug-ins that do this automatically.
|
||||
|
||||
Pull request descriptions should be as clear as possible and include a reference
|
||||
to all the issues that they address.
|
||||
|
||||
Commit messages must start with a capitalized and short summary (max. 50 chars)
|
||||
written in the imperative, followed by an optional, more detailed explanatory
|
||||
text which is separated from the summary by an empty line.
|
||||
|
||||
Code review comments may be added to your pull request. Discuss, then make the
|
||||
suggested modifications and push additional commits to your feature branch. Post
|
||||
a comment after pushing. New commits show up in the pull request automatically,
|
||||
but the reviewers are notified only when you comment.
|
||||
|
||||
Pull requests must be cleanly rebased on top of master without multiple branches
|
||||
mixed into the PR.
|
||||
|
||||
**Git tip**: If your PR no longer merges cleanly, use `rebase master` in your
|
||||
feature branch to update your pull request rather than `merge master`.
|
||||
|
||||
Before you make a pull request, squash your commits into logical units of work
|
||||
using `git rebase -i` and `git push -f`. A logical unit of work is a consistent
|
||||
set of patches that should be reviewed together: for example, upgrading the
|
||||
version of a vendored dependency and taking advantage of its now available new
|
||||
feature constitute two separate units of work. Implementing a new function and
|
||||
calling it in another file constitute a single logical unit of work. The very
|
||||
high majority of submissions should have a single commit, so if in doubt: squash
|
||||
down to one.
|
||||
|
||||
After every commit, make sure the test suite passes. Include documentation
|
||||
changes in the same pull request so that a revert would remove all traces of
|
||||
the feature or fix.
|
||||
|
||||
Include an issue reference like `Closes #XXXX` or `Fixes #XXXX` in the pull request
|
||||
description that close an issue. Including references automatically closes the issue
|
||||
on a merge.
|
||||
|
||||
Please do not add yourself to the `AUTHORS` file, as it is regenerated regularly
|
||||
from the Git history.
|
||||
|
||||
Please see the [Coding Style](#coding-style) for further guidelines.
|
||||
|
||||
### Merge approval
|
||||
|
||||
Docker maintainers use LGTM (Looks Good To Me) in comments on the code review to
|
||||
indicate acceptance.
|
||||
|
||||
A change requires LGTMs from an absolute majority of the maintainers of each
|
||||
component affected. For example, if a change affects `docs/` and `registry/`, it
|
||||
needs an absolute majority from the maintainers of `docs/` AND, separately, an
|
||||
absolute majority of the maintainers of `registry/`.
|
||||
|
||||
For more details, see the [MAINTAINERS](MAINTAINERS) page.
|
||||
|
||||
### Sign your work
|
||||
|
||||
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
|
||||
it on as an open-source patch. The rules are pretty simple: if you can certify
|
||||
the below (from [developercertificate.org](http://developercertificate.org/)):
|
||||
|
||||
```
|
||||
Developer Certificate of Origin
|
||||
Version 1.1
|
||||
|
||||
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
|
||||
660 York Street, Suite 102,
|
||||
San Francisco, CA 94110 USA
|
||||
|
||||
Everyone is permitted to copy and distribute verbatim copies of this
|
||||
license document, but changing it is not allowed.
|
||||
|
||||
Developer's Certificate of Origin 1.1
|
||||
|
||||
By making a contribution to this project, I certify that:
|
||||
|
||||
(a) The contribution was created in whole or in part by me and I
|
||||
have the right to submit it under the open source license
|
||||
indicated in the file; or
|
||||
|
||||
(b) The contribution is based upon previous work that, to the best
|
||||
of my knowledge, is covered under an appropriate open source
|
||||
license and I have the right under that license to submit that
|
||||
work with modifications, whether created in whole or in part
|
||||
by me, under the same open source license (unless I am
|
||||
permitted to submit under a different license), as indicated
|
||||
in the file; or
|
||||
|
||||
(c) The contribution was provided directly to me by some other
|
||||
person who certified (a), (b) or (c) and I have not modified
|
||||
it.
|
||||
|
||||
(d) I understand and agree that this project and the contribution
|
||||
are public and that a record of the contribution (including all
|
||||
personal information I submit with it, including my sign-off) is
|
||||
maintained indefinitely and may be redistributed consistent with
|
||||
this project or the open source license(s) involved.
|
||||
```
|
||||
|
||||
Then you just add a line to every git commit message:
|
||||
|
||||
Signed-off-by: Joe Smith <joe.smith@email.com>
|
||||
|
||||
Use your real name (sorry, no pseudonyms or anonymous contributions.)
|
||||
|
||||
If you set your `user.name` and `user.email` git configs, you can sign your
|
||||
commit automatically with `git commit -s`.
|
||||
|
||||
### How can I become a maintainer?
|
||||
|
||||
The procedures for adding new maintainers are explained in the
|
||||
global [MAINTAINERS](https://github.com/docker/opensource/blob/master/MAINTAINERS)
|
||||
file in the [https://github.com/docker/opensource/](https://github.com/docker/opensource/)
|
||||
repository.
|
||||
|
||||
Don't forget: being a maintainer is a time investment. Make sure you
|
||||
will have time to make yourself available. You don't have to be a
|
||||
maintainer to make a difference on the project!
|
||||
|
||||
## Docker community guidelines
|
||||
|
||||
We want to keep the Docker community awesome, growing and collaborative. We need
|
||||
your help to keep it that way. To help with this we've come up with some general
|
||||
guidelines for the community as a whole:
|
||||
|
||||
* Be nice: Be courteous, respectful and polite to fellow community members:
|
||||
no regional, racial, gender, or other abuse will be tolerated. We like
|
||||
nice people way better than mean ones!
|
||||
|
||||
* Encourage diversity and participation: Make everyone in our community feel
|
||||
welcome, regardless of their background and the extent of their
|
||||
contributions, and do everything possible to encourage participation in
|
||||
our community.
|
||||
|
||||
* Keep it legal: Basically, don't get us in trouble. Share only content that
|
||||
you own, do not share private or sensitive information, and don't break
|
||||
the law.
|
||||
|
||||
* Stay on topic: Make sure that you are posting to the correct channel and
|
||||
avoid off-topic discussions. Remember when you update an issue or respond
|
||||
to an email you are potentially sending to a large number of people. Please
|
||||
consider this before you update. Also remember that nobody likes spam.
|
||||
|
||||
* Don't send email to the maintainers: There's no need to send email to the
|
||||
maintainers to ask them to investigate an issue or to take a look at a
|
||||
pull request. Instead of sending an email, GitHub mentions should be
|
||||
used to ping maintainers to review a pull request, a proposal or an
|
||||
issue.
|
||||
|
||||
### Guideline violations — 3 strikes method
|
||||
|
||||
The point of this section is not to find opportunities to punish people, but we
|
||||
do need a fair way to deal with people who are making our community suck.
|
||||
|
||||
1. First occurrence: We'll give you a friendly, but public reminder that the
|
||||
behavior is inappropriate according to our guidelines.
|
||||
|
||||
2. Second occurrence: We will send you a private message with a warning that
|
||||
any additional violations will result in removal from the community.
|
||||
|
||||
3. Third occurrence: Depending on the violation, we may need to delete or ban
|
||||
your account.
|
||||
|
||||
**Notes:**
|
||||
|
||||
* Obvious spammers are banned on first occurrence. If we don't do this, we'll
|
||||
have spam all over the place.
|
||||
|
||||
* Violations are forgiven after 6 months of good behavior, and we won't hold a
|
||||
grudge.
|
||||
|
||||
* People who commit minor infractions will get some education, rather than
|
||||
hammering them in the 3 strikes process.
|
||||
|
||||
* The rules apply equally to everyone in the community, no matter how much
|
||||
you've contributed.
|
||||
|
||||
* Extreme violations of a threatening, abusive, destructive or illegal nature
|
||||
will be addressed immediately and are not subject to 3 strikes or forgiveness.
|
||||
|
||||
* Contact abuse@docker.com to report abuse or appeal violations. In the case of
|
||||
appeals, we know that mistakes happen, and we'll work with you to come up with a
|
||||
fair solution if there has been a misunderstanding.
|
||||
|
||||
## Coding Style
|
||||
|
||||
Unless explicitly stated, we follow all coding guidelines from the Go
|
||||
community. While some of these standards may seem arbitrary, they somehow seem
|
||||
to result in a solid, consistent codebase.
|
||||
|
||||
It is possible that the code base does not currently comply with these
|
||||
guidelines. We are not looking for a massive PR that fixes this, since that
|
||||
goes against the spirit of the guidelines. All new contributions should make a
|
||||
best effort to clean up and make the code base better than they left it.
|
||||
Obviously, apply your best judgement. Remember, the goal here is to make the
|
||||
code base easier for humans to navigate and understand. Always keep that in
|
||||
mind when nudging others to comply.
|
||||
|
||||
The rules:
|
||||
|
||||
1. All code should be formatted with `gofmt -s`.
|
||||
2. All code should pass the default levels of
|
||||
[`golint`](https://github.com/golang/lint).
|
||||
3. All code should follow the guidelines covered in [Effective
|
||||
Go](http://golang.org/doc/effective_go.html) and [Go Code Review
|
||||
Comments](https://github.com/golang/go/wiki/CodeReviewComments).
|
||||
4. Comment the code. Tell us the why, the history and the context.
|
||||
5. Document _all_ declarations and methods, even private ones. Declare
|
||||
expectations, caveats and anything else that may be important. If a type
|
||||
gets exported, having the comments already there will ensure it's ready.
|
||||
6. Variable name length should be proportional to its context and no longer.
|
||||
`noCommaALongVariableNameLikeThisIsNotMoreClearWhenASimpleCommentWouldDo`.
|
||||
In practice, short methods will have short variable names and globals will
|
||||
have longer names.
|
||||
7. No underscores in package names. If you need a compound name, step back,
|
||||
and re-examine why you need a compound name. If you still think you need a
|
||||
compound name, lose the underscore.
|
||||
8. No utils or helpers packages. If a function is not general enough to
|
||||
warrant its own package, it has not been written generally enough to be a
|
||||
part of a util package. Just leave it unexported and well-documented.
|
||||
9. All tests should run with `go test` and outside tooling should not be
|
||||
required. No, we don't need another unit testing framework. Assertion
|
||||
packages are acceptable if they provide _real_ incremental value.
|
||||
10. Even though we call these "rules" above, they are actually just
|
||||
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
|
||||
reading through [Effective Go](https://golang.org/doc/effective_go.html). The
|
||||
[Go Blog](https://blog.golang.org) is also a great resource. Drinking the
|
||||
kool-aid is a lot easier than going thirsty.
|
|
@ -0,0 +1,51 @@
|
|||
# syntax=docker/dockerfile:1.3
|
||||
|
||||
ARG BASE_VARIANT=alpine
|
||||
ARG GO_VERSION=1.17.10
|
||||
ARG XX_VERSION=1.1.0
|
||||
|
||||
FROM --platform=$BUILDPLATFORM tonistiigi/xx:${XX_VERSION} AS xx
|
||||
|
||||
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-${BASE_VARIANT} AS build-base-alpine
|
||||
COPY --from=xx / /
|
||||
RUN apk add --no-cache clang lld llvm file git
|
||||
WORKDIR /go/src/github.com/docker/cli
|
||||
|
||||
FROM build-base-alpine AS build-alpine
|
||||
ARG TARGETPLATFORM
|
||||
# gcc is installed for libgcc only
|
||||
RUN xx-apk add --no-cache musl-dev gcc
|
||||
|
||||
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-buster AS build-base-buster
|
||||
COPY --from=xx / /
|
||||
RUN apt-get update && apt-get install --no-install-recommends -y clang lld file
|
||||
WORKDIR /go/src/github.com/docker/cli
|
||||
|
||||
FROM build-base-buster AS build-buster
|
||||
ARG TARGETPLATFORM
|
||||
RUN xx-apt install --no-install-recommends -y libc6-dev libgcc-8-dev
|
||||
|
||||
FROM build-${BASE_VARIANT} AS build
|
||||
# GO_LINKMODE defines if static or dynamic binary should be produced
|
||||
ARG GO_LINKMODE=static
|
||||
# GO_BUILDTAGS defines additional build tags
|
||||
ARG GO_BUILDTAGS
|
||||
# GO_STRIP strips debugging symbols if set
|
||||
ARG GO_STRIP
|
||||
# CGO_ENABLED manually sets if cgo is used
|
||||
ARG CGO_ENABLED
|
||||
# VERSION sets the version for the produced binary
|
||||
ARG VERSION
|
||||
RUN --mount=ro --mount=type=cache,target=/root/.cache \
|
||||
--mount=from=dockercore/golang-cross:xx-sdk-extras,target=/xx-sdk,src=/xx-sdk \
|
||||
--mount=type=tmpfs,target=cli/winresources \
|
||||
xx-go --wrap && \
|
||||
# export GOCACHE=$(go env GOCACHE)/$(xx-info)$([ -f /etc/alpine-release ] && echo "alpine") && \
|
||||
TARGET=/out ./scripts/build/binary && \
|
||||
xx-verify $([ "$GO_LINKMODE" = "static" ] && echo "--static") /out/docker
|
||||
|
||||
FROM build-base-${BASE_VARIANT} AS dev
|
||||
COPY . .
|
||||
|
||||
FROM scratch AS binary
|
||||
COPY --from=build /out .
|
|
@ -0,0 +1,47 @@
|
|||
pipeline {
|
||||
agent {
|
||||
label "amd64 && ubuntu-1804 && overlay2"
|
||||
}
|
||||
|
||||
options {
|
||||
timeout(time: 60, unit: 'MINUTES')
|
||||
}
|
||||
|
||||
stages {
|
||||
stage("Docker info") {
|
||||
steps {
|
||||
sh "docker version"
|
||||
sh "docker info"
|
||||
}
|
||||
}
|
||||
stage("e2e (non-experimental) - stable engine") {
|
||||
steps {
|
||||
sh "E2E_UNIQUE_ID=clie2e${BUILD_NUMBER} \
|
||||
IMAGE_TAG=clie2e${BUILD_NUMBER} \
|
||||
make -f docker.Makefile test-e2e-non-experimental"
|
||||
}
|
||||
}
|
||||
stage("e2e (non-experimental) - 19.03 engine") {
|
||||
steps {
|
||||
sh "E2E_ENGINE_VERSION=19.03-dind \
|
||||
E2E_UNIQUE_ID=clie2e${BUILD_NUMBER} \
|
||||
IMAGE_TAG=clie2e${BUILD_NUMBER} \
|
||||
make -f docker.Makefile test-e2e-non-experimental"
|
||||
}
|
||||
}
|
||||
stage("e2e (experimental)") {
|
||||
steps {
|
||||
sh "E2E_UNIQUE_ID=clie2e${BUILD_NUMBER} \
|
||||
IMAGE_TAG=clie2e${BUILD_NUMBER} \
|
||||
make -f docker.Makefile test-e2e-experimental"
|
||||
}
|
||||
}
|
||||
stage("e2e (ssh connhelper)") {
|
||||
steps {
|
||||
sh "E2E_UNIQUE_ID=clie2e${BUILD_NUMBER} \
|
||||
IMAGE_TAG=clie2e${BUILD_NUMBER} \
|
||||
make -f docker.Makefile test-e2e-connhelper-ssh"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,191 @@
|
|||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
https://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
Copyright 2013-2017 Docker, Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
|
@ -0,0 +1,130 @@
|
|||
# Docker maintainers file
|
||||
#
|
||||
# This file describes who runs the docker/cli project and how.
|
||||
# This is a living document - if you see something out of date or missing, speak up!
|
||||
#
|
||||
# It is structured to be consumable by both humans and programs.
|
||||
# To extract its contents programmatically, use any TOML-compliant
|
||||
# parser.
|
||||
#
|
||||
# This file is compiled into the MAINTAINERS file in docker/opensource.
|
||||
#
|
||||
[Org]
|
||||
|
||||
[Org."Core maintainers"]
|
||||
|
||||
# The Core maintainers are the ghostbusters of the project: when there's a problem others
|
||||
# can't solve, they show up and fix it with bizarre devices and weaponry.
|
||||
# They have final say on technical implementation and coding style.
|
||||
# They are ultimately responsible for quality in all its forms: usability polish,
|
||||
# bugfixes, performance, stability, etc. When ownership can cleanly be passed to
|
||||
# a subsystem, they are responsible for doing so and holding the
|
||||
# subsystem maintainers accountable. If ownership is unclear, they are the de facto owners.
|
||||
|
||||
people = [
|
||||
"albers",
|
||||
"cpuguy83",
|
||||
"silvin-lubecki",
|
||||
"stevvooe",
|
||||
"thajeztah",
|
||||
"tibor",
|
||||
"tonistiigi",
|
||||
"vdemeester",
|
||||
"vieux",
|
||||
]
|
||||
|
||||
[Org.Curators]
|
||||
|
||||
# The curators help ensure that incoming issues and pull requests are properly triaged and
|
||||
# that our various contribution and reviewing processes are respected. With their knowledge of
|
||||
# the repository activity, they can also guide contributors to relevant material or
|
||||
# discussions.
|
||||
#
|
||||
# They are neither code nor docs reviewers, so they are never expected to merge. They can
|
||||
# however:
|
||||
# - close an issue or pull request when it's an exact duplicate
|
||||
# - close an issue or pull request when it's inappropriate or off-topic
|
||||
|
||||
people = [
|
||||
"programmerq",
|
||||
"thajeztah"
|
||||
]
|
||||
|
||||
[Org.Alumni]
|
||||
|
||||
# This list contains maintainers that are no longer active on the project.
|
||||
# It is thanks to these people that the project has become what it is today.
|
||||
# Thank you!
|
||||
|
||||
people = [
|
||||
# Before becoming a maintainer, Daniel Nephin was a core contributor
|
||||
# to "Fig" (now known as Docker Compose). As a maintainer for both the
|
||||
# Engine and Docker CLI, Daniel contributed many features, among which
|
||||
# the `docker stack` commands, allowing users to deploy their Docker
|
||||
# Compose projects as a Swarm service.
|
||||
"dnephin",
|
||||
]
|
||||
|
||||
[people]
|
||||
|
||||
# A reference list of all people associated with the project.
|
||||
# All other sections should refer to people by their canonical key
|
||||
# in the people section.
|
||||
|
||||
# ADD YOURSELF HERE IN ALPHABETICAL ORDER
|
||||
|
||||
[people.albers]
|
||||
Name = "Harald Albers"
|
||||
Email = "github@albersweb.de"
|
||||
GitHub = "albers"
|
||||
|
||||
[people.cpuguy83]
|
||||
Name = "Brian Goff"
|
||||
Email = "cpuguy83@gmail.com"
|
||||
GitHub = "cpuguy83"
|
||||
|
||||
[people.dnephin]
|
||||
Name = "Daniel Nephin"
|
||||
Email = "dnephin@gmail.com"
|
||||
GitHub = "dnephin"
|
||||
|
||||
[people.programmerq]
|
||||
Name = "Jeff Anderson"
|
||||
Email = "jeff@docker.com"
|
||||
GitHub = "programmerq"
|
||||
|
||||
[people.silvin-lubecki]
|
||||
Name = "Silvin Lubecki"
|
||||
Email = "silvin.lubecki@docker.com"
|
||||
GitHub = "silvin-lubecki"
|
||||
|
||||
[people.stevvooe]
|
||||
Name = "Stephen Day"
|
||||
Email = "stevvooe@gmail.com"
|
||||
GitHub = "stevvooe"
|
||||
|
||||
[people.thajeztah]
|
||||
Name = "Sebastiaan van Stijn"
|
||||
Email = "github@gone.nl"
|
||||
GitHub = "thaJeztah"
|
||||
|
||||
[people.tibor]
|
||||
Name = "Tibor Vass"
|
||||
Email = "tibor@docker.com"
|
||||
GitHub = "tiborvass"
|
||||
|
||||
[people.tonistiigi]
|
||||
Name = "Tõnis Tiigi"
|
||||
Email = "tonis@docker.com"
|
||||
GitHub = "tonistiigi"
|
||||
|
||||
[people.vdemeester]
|
||||
Name = "Vincent Demeester"
|
||||
Email = "vincent@sbr.pm"
|
||||
GitHub = "vdemeester"
|
||||
|
||||
[people.vieux]
|
||||
Name = "Victor Vieux"
|
||||
Email = "vieux@docker.com"
|
||||
GitHub = "vieux"
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
#
|
||||
# github.com/docker/cli
|
||||
#
|
||||
all: binary
|
||||
|
||||
|
||||
_:=$(shell ./scripts/warn-outside-container $(MAKECMDGOALS))
|
||||
|
||||
.PHONY: clean
|
||||
clean: ## remove build artifacts
|
||||
rm -rf ./build/* cli/winresources/rsrc_* ./man/man[1-9] docs/yaml/gen
|
||||
|
||||
.PHONY: test-unit
|
||||
test-unit: ## run unit tests, to change the output format use: GOTESTSUM_FORMAT=(dots|short|standard-quiet|short-verbose|standard-verbose) make test-unit
|
||||
gotestsum $(TESTFLAGS) -- $${TESTDIRS:-$(shell go list ./... | grep -vE '/vendor/|/e2e/')}
|
||||
|
||||
.PHONY: test
|
||||
test: test-unit ## run tests
|
||||
|
||||
.PHONY: test-coverage
|
||||
test-coverage: ## run test coverage
|
||||
gotestsum -- -coverprofile=coverage.txt $(shell go list ./... | grep -vE '/vendor/|/e2e/')
|
||||
|
||||
.PHONY: fmt
|
||||
fmt:
|
||||
go list -f {{.Dir}} ./... | xargs gofmt -w -s -d
|
||||
|
||||
.PHONY: lint
|
||||
lint: ## run all the lint tools
|
||||
gometalinter --config gometalinter.json ./...
|
||||
|
||||
.PHONY: binary
|
||||
binary:
|
||||
./scripts/build/binary
|
||||
|
||||
.PHONY: plugins
|
||||
plugins: ## build example CLI plugins
|
||||
./scripts/build/plugins
|
||||
|
||||
.PHONY: cross
|
||||
cross:
|
||||
./scripts/build/binary
|
||||
|
||||
.PHONY: plugins-windows
|
||||
plugins-windows: ## build example CLI plugins for Windows
|
||||
./scripts/build/plugins-windows
|
||||
|
||||
.PHONY: plugins-osx
|
||||
plugins-osx: ## build example CLI plugins for macOS
|
||||
./scripts/build/plugins-osx
|
||||
|
||||
.PHONY: dynbinary
|
||||
dynbinary: ## build dynamically linked binary
|
||||
GO_LINKMODE=dynamic ./scripts/build/binary
|
||||
|
||||
vendor: vendor.conf ## check that vendor matches vendor.conf
|
||||
rm -rf vendor
|
||||
bash -c 'vndr |& grep -v -i clone | tee ./vndr.log'
|
||||
scripts/validate/check-git-diff vendor
|
||||
scripts/validate/check-all-packages-vendored
|
||||
|
||||
.PHONY: authors
|
||||
authors: ## generate AUTHORS file from git history
|
||||
scripts/docs/generate-authors.sh
|
||||
|
||||
.PHONY: manpages
|
||||
manpages: ## generate man pages from go source and markdown
|
||||
scripts/docs/generate-man.sh
|
||||
|
||||
.PHONY: yamldocs
|
||||
yamldocs: ## generate documentation YAML files consumed by docs repo
|
||||
scripts/docs/generate-yaml.sh
|
||||
|
||||
.PHONY: shellcheck
|
||||
shellcheck: ## run shellcheck validation
|
||||
scripts/validate/shellcheck
|
||||
|
||||
.PHONY: help
|
||||
help: ## print this help
|
||||
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z0-9_-]+:.*?## / {gsub("\\\\n",sprintf("\n%22c",""), $$2);printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
|
||||
|
||||
|
||||
cli/compose/schema/bindata.go: cli/compose/schema/data/*.json
|
||||
go generate github.com/docker/cli/cli/compose/schema
|
||||
|
||||
compose-jsonschema: cli/compose/schema/bindata.go ## generate compose-file schemas
|
||||
scripts/validate/check-git-diff cli/compose/schema/bindata.go
|
||||
|
||||
.PHONY: ci-validate
|
||||
ci-validate:
|
||||
time make -B vendor
|
||||
time make -B compose-jsonschema
|
||||
time make manpages
|
||||
time make yamldocs
|
|
@ -0,0 +1,19 @@
|
|||
Docker
|
||||
Copyright 2012-2017 Docker, Inc.
|
||||
|
||||
This product includes software developed at Docker, Inc. (https://www.docker.com).
|
||||
|
||||
This product contains software (https://github.com/creack/pty) developed
|
||||
by Keith Rarick, licensed under the MIT License.
|
||||
|
||||
The following is courtesy of our legal counsel:
|
||||
|
||||
|
||||
Use and transfer of Docker may be subject to certain restrictions by the
|
||||
United States and other governments.
|
||||
It is your responsibility to ensure that your use and/or transfer does not
|
||||
violate applicable laws.
|
||||
|
||||
For more information, please see https://www.bis.doc.gov
|
||||
|
||||
See also https://www.apache.org/dev/crypto.html and/or seek legal counsel.
|
|
@ -0,0 +1,77 @@
|
|||
[![build status](https://circleci.com/gh/docker/cli.svg?style=shield)](https://circleci.com/gh/docker/cli/tree/master)
|
||||
[![Build Status](https://ci.docker.com/public/job/cli/job/master/badge/icon)](https://ci.docker.com/public/job/cli/job/master)
|
||||
|
||||
docker/cli
|
||||
==========
|
||||
|
||||
This repository is the home of the cli used in the Docker CE and
|
||||
Docker EE products.
|
||||
|
||||
Development
|
||||
===========
|
||||
|
||||
`docker/cli` is developed using Docker.
|
||||
|
||||
Build CLI from source:
|
||||
|
||||
```
|
||||
$ docker buildx bake
|
||||
```
|
||||
|
||||
Build binaries for all supported platforms:
|
||||
|
||||
```
|
||||
$ docker buildx bake cross
|
||||
```
|
||||
|
||||
Build for a specific platform:
|
||||
|
||||
```
|
||||
$ docker buildx bake --set binary.platform=linux/arm64
|
||||
```
|
||||
|
||||
Build dynamic binary for glibc or musl:
|
||||
|
||||
```
|
||||
$ USE_GLIBC=1 docker buildx bake dynbinary
|
||||
```
|
||||
|
||||
|
||||
Run all linting:
|
||||
|
||||
```
|
||||
$ make -f docker.Makefile lint
|
||||
```
|
||||
|
||||
List all the available targets:
|
||||
|
||||
```
|
||||
$ make help
|
||||
```
|
||||
|
||||
### In-container development environment
|
||||
|
||||
Start an interactive development environment:
|
||||
|
||||
```
|
||||
$ make -f docker.Makefile shell
|
||||
```
|
||||
|
||||
Legal
|
||||
=====
|
||||
*Brought to you courtesy of our legal counsel. For more context,
|
||||
please see the [NOTICE](https://github.com/docker/cli/blob/master/NOTICE) document in this repo.*
|
||||
|
||||
Use and transfer of Docker may be subject to certain restrictions by the
|
||||
United States and other governments.
|
||||
|
||||
It is your responsibility to ensure that your use and/or transfer does not
|
||||
violate applicable laws.
|
||||
|
||||
For more information, please see https://www.bis.doc.gov
|
||||
|
||||
Licensing
|
||||
=========
|
||||
docker/cli is licensed under the Apache License, Version 2.0. See
|
||||
[LICENSE](https://github.com/docker/docker/blob/master/LICENSE) for the full
|
||||
license text.
|
|
@ -0,0 +1,85 @@
|
|||
# Testing
|
||||
|
||||
The following guidelines summarize the testing policy for docker/cli.
|
||||
|
||||
## Unit Test Suite
|
||||
|
||||
All code changes should have unit test coverage.
|
||||
|
||||
Error cases should be tested with unit tests.
|
||||
|
||||
Bug fixes should be covered by new unit tests or additional assertions in
|
||||
existing unit tests.
|
||||
|
||||
### Details
|
||||
|
||||
The unit test suite follows the standard Go testing convention. Tests are
|
||||
located in the package directory in `_test.go` files.
|
||||
|
||||
Unit tests should be named using the convention:
|
||||
|
||||
```
|
||||
Test<Function Name><Test Case Name>
|
||||
```
|
||||
|
||||
[Table tests](https://github.com/golang/go/wiki/TableDrivenTests) should be used
|
||||
where appropriate, but may not be appropriate in all cases.
|
||||
|
||||
Assertions should be made using
|
||||
[gotest.tools/assert](https://godoc.org/gotest.tools/assert).
|
||||
|
||||
Fakes, and testing utilities can be found in
|
||||
[internal/test](https://godoc.org/github.com/docker/cli/internal/test) and
|
||||
[gotest.tools](https://godoc.org/gotest.tools).
|
||||
|
||||
## End-to-End Test Suite
|
||||
|
||||
The end-to-end test suite tests a cli binary against a real API backend.
|
||||
|
||||
### Guidelines
|
||||
|
||||
Each feature (subcommand) should have a single end-to-end test for
|
||||
the success case. The test should include all (or most) flags/options supported
|
||||
by that feature.
|
||||
|
||||
In some rare cases a couple additional end-to-end tests may be written for a
|
||||
sufficiently complex and critical feature (ex: `container run`, `service
|
||||
create`, `service update`, and `docker build` may have ~3-5 cases each).
|
||||
|
||||
In some rare cases a sufficiently critical error paths may have a single
|
||||
end-to-end test case.
|
||||
|
||||
In all other cases the behaviour should be covered by unit tests.
|
||||
|
||||
If a code change adds a new flag, that flag should be added to the existing
|
||||
"success case" end-to-end test.
|
||||
|
||||
If a code change fixes a bug, that bug fix should be covered either by adding
|
||||
assertions to the existing end-to-end test, or with one or more unit test.
|
||||
|
||||
### Details
|
||||
|
||||
The end-to-end test suite is located in
|
||||
[./e2e](https://github.com/docker/cli/tree/master/e2e). Each directory in `e2e`
|
||||
corresponds to a directory in `cli/command` and contains the tests for that
|
||||
subcommand. Files in each directory should be named `<command>_test.go` where
|
||||
command is the basename of the command (ex: the test for `docker stack deploy`
|
||||
is found in `e2e/stack/deploy_test.go`).
|
||||
|
||||
Tests should be named using the convention:
|
||||
|
||||
```
|
||||
Test<Command Basename>[<Test Case Name>]
|
||||
```
|
||||
|
||||
where the test case name is only required when there are multiple test cases for
|
||||
a single command.
|
||||
|
||||
End-to-end test should run the `docker` binary using
|
||||
[gotestyourself/icmd](https://godoc.org/github.com/gotestyourself/gotestyourself/icmd)
|
||||
and make assertions about the exit code, stdout, stderr, and local file system.
|
||||
|
||||
Any Docker image or registry operations should use `registry:5000/<image name>`
|
||||
to communicate with the local instance of the Docker registry. To load
|
||||
additional fixture images to the registry see
|
||||
[scripts/test/e2e/run](https://github.com/docker/cli/blob/master/scripts/test/e2e/run).
|
|
@ -0,0 +1 @@
|
|||
20.10.0-dev
|
|
@ -0,0 +1,23 @@
|
|||
version: "{build}"
|
||||
|
||||
clone_folder: c:\gopath\src\github.com\docker\cli
|
||||
|
||||
environment:
|
||||
GOPATH: c:\gopath
|
||||
GOVERSION: 1.17.10
|
||||
DEPVERSION: v0.4.1
|
||||
|
||||
install:
|
||||
- rmdir c:\go /s /q
|
||||
- appveyor DownloadFile https://storage.googleapis.com/golang/go%GOVERSION%.windows-amd64.msi
|
||||
- msiexec /i go%GOVERSION%.windows-amd64.msi /q
|
||||
- go version
|
||||
- go env
|
||||
|
||||
deploy: false
|
||||
|
||||
build_script:
|
||||
- ps: .\scripts\make.ps1 -Binary
|
||||
|
||||
test_script:
|
||||
- ps: .\scripts\make.ps1 -TestUnit
|
|
@ -0,0 +1,105 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/docker/cli/cli-plugins/manager"
|
||||
"github.com/docker/cli/cli-plugins/plugin"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func main() {
|
||||
plugin.Run(func(dockerCli command.Cli) *cobra.Command {
|
||||
goodbye := &cobra.Command{
|
||||
Use: "goodbye",
|
||||
Short: "Say Goodbye instead of Hello",
|
||||
Run: func(cmd *cobra.Command, _ []string) {
|
||||
fmt.Fprintln(dockerCli.Out(), "Goodbye World!")
|
||||
},
|
||||
}
|
||||
apiversion := &cobra.Command{
|
||||
Use: "apiversion",
|
||||
Short: "Print the API version of the server",
|
||||
RunE: func(_ *cobra.Command, _ []string) error {
|
||||
cli := dockerCli.Client()
|
||||
ping, err := cli.Ping(context.Background())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(ping.APIVersion)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
exitStatus2 := &cobra.Command{
|
||||
Use: "exitstatus2",
|
||||
Short: "Exit with status 2",
|
||||
RunE: func(_ *cobra.Command, _ []string) error {
|
||||
fmt.Fprintln(dockerCli.Err(), "Exiting with error status 2")
|
||||
os.Exit(2)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var (
|
||||
who, context string
|
||||
preRun, debug bool
|
||||
)
|
||||
cmd := &cobra.Command{
|
||||
Use: "helloworld",
|
||||
Short: "A basic Hello World plugin for tests",
|
||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
if err := plugin.PersistentPreRunE(cmd, args); err != nil {
|
||||
return err
|
||||
}
|
||||
if preRun {
|
||||
fmt.Fprintf(dockerCli.Err(), "Plugin PersistentPreRunE called")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if debug {
|
||||
fmt.Fprintf(dockerCli.Err(), "Plugin debug mode enabled")
|
||||
}
|
||||
|
||||
switch context {
|
||||
case "Christmas":
|
||||
fmt.Fprintf(dockerCli.Out(), "Merry Christmas!\n")
|
||||
return nil
|
||||
case "":
|
||||
// nothing
|
||||
}
|
||||
|
||||
if who == "" {
|
||||
who, _ = dockerCli.ConfigFile().PluginConfig("helloworld", "who")
|
||||
}
|
||||
if who == "" {
|
||||
who = "World"
|
||||
}
|
||||
|
||||
fmt.Fprintf(dockerCli.Out(), "Hello %s!\n", who)
|
||||
dockerCli.ConfigFile().SetPluginConfig("helloworld", "lastwho", who)
|
||||
return dockerCli.ConfigFile().Save()
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.StringVar(&who, "who", "", "Who are we addressing?")
|
||||
flags.BoolVar(&preRun, "pre-run", false, "Log from prerun hook")
|
||||
// These are intended to deliberately clash with the CLIs own top
|
||||
// level arguments.
|
||||
flags.BoolVarP(&debug, "debug", "D", false, "Enable debug")
|
||||
flags.StringVarP(&context, "context", "c", "", "Is it Christmas?")
|
||||
|
||||
cmd.AddCommand(goodbye, apiversion, exitStatus2)
|
||||
return cmd
|
||||
},
|
||||
manager.Metadata{
|
||||
SchemaVersion: "0.1.0",
|
||||
Vendor: "Docker Inc.",
|
||||
Version: "testing",
|
||||
})
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package manager
|
||||
|
||||
import (
|
||||
exec "golang.org/x/sys/execabs"
|
||||
)
|
||||
|
||||
// Candidate represents a possible plugin candidate, for mocking purposes
|
||||
type Candidate interface {
|
||||
Path() string
|
||||
Metadata() ([]byte, error)
|
||||
}
|
||||
|
||||
type candidate struct {
|
||||
path string
|
||||
}
|
||||
|
||||
func (c *candidate) Path() string {
|
||||
return c.path
|
||||
}
|
||||
|
||||
func (c *candidate) Metadata() ([]byte, error) {
|
||||
return exec.Command(c.path, MetadataSubcommandName).Output()
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
package manager
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"gotest.tools/v3/assert"
|
||||
"gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
|
||||
type fakeCandidate struct {
|
||||
path string
|
||||
exec bool
|
||||
meta string
|
||||
}
|
||||
|
||||
func (c *fakeCandidate) Path() string {
|
||||
return c.path
|
||||
}
|
||||
|
||||
func (c *fakeCandidate) Metadata() ([]byte, error) {
|
||||
if !c.exec {
|
||||
return nil, fmt.Errorf("faked a failure to exec %q", c.path)
|
||||
}
|
||||
return []byte(c.meta), nil
|
||||
}
|
||||
|
||||
func TestValidateCandidate(t *testing.T) {
|
||||
const (
|
||||
goodPluginName = NamePrefix + "goodplugin"
|
||||
|
||||
builtinName = NamePrefix + "builtin"
|
||||
builtinAlias = NamePrefix + "alias"
|
||||
|
||||
badPrefixPath = "/usr/local/libexec/cli-plugins/wobble"
|
||||
badNamePath = "/usr/local/libexec/cli-plugins/docker-123456"
|
||||
goodPluginPath = "/usr/local/libexec/cli-plugins/" + goodPluginName
|
||||
metaExperimental = `{"SchemaVersion": "0.1.0", "Vendor": "e2e-testing", "Experimental": true}`
|
||||
)
|
||||
|
||||
fakeroot := &cobra.Command{Use: "docker"}
|
||||
fakeroot.AddCommand(&cobra.Command{
|
||||
Use: strings.TrimPrefix(builtinName, NamePrefix),
|
||||
Aliases: []string{
|
||||
strings.TrimPrefix(builtinAlias, NamePrefix),
|
||||
},
|
||||
})
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
c *fakeCandidate
|
||||
|
||||
// Either err or invalid may be non-empty, but not both (both can be empty for a good plugin).
|
||||
err string
|
||||
invalid string
|
||||
}{
|
||||
/* Each failing one of the tests */
|
||||
{name: "empty path", c: &fakeCandidate{path: ""}, err: "plugin candidate path cannot be empty"},
|
||||
{name: "bad prefix", c: &fakeCandidate{path: badPrefixPath}, err: fmt.Sprintf("does not have %q prefix", NamePrefix)},
|
||||
{name: "bad path", c: &fakeCandidate{path: badNamePath}, invalid: "did not match"},
|
||||
{name: "builtin command", c: &fakeCandidate{path: builtinName}, invalid: `plugin "builtin" duplicates builtin command`},
|
||||
{name: "builtin alias", c: &fakeCandidate{path: builtinAlias}, invalid: `plugin "alias" duplicates an alias of builtin command "builtin"`},
|
||||
{name: "fetch failure", c: &fakeCandidate{path: goodPluginPath, exec: false}, invalid: fmt.Sprintf("failed to fetch metadata: faked a failure to exec %q", goodPluginPath)},
|
||||
{name: "metadata not json", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `xyzzy`}, invalid: "invalid character"},
|
||||
{name: "empty schemaversion", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{}`}, invalid: `plugin SchemaVersion "" is not valid`},
|
||||
{name: "invalid schemaversion", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "xyzzy"}`}, invalid: `plugin SchemaVersion "xyzzy" is not valid`},
|
||||
{name: "no vendor", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0"}`}, invalid: "plugin metadata does not define a vendor"},
|
||||
{name: "empty vendor", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0", "Vendor": ""}`}, invalid: "plugin metadata does not define a vendor"},
|
||||
// This one should work
|
||||
{name: "valid", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0", "Vendor": "e2e-testing"}`}},
|
||||
{name: "experimental + allowing experimental", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: metaExperimental}},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
p, err := newPlugin(tc.c, fakeroot)
|
||||
if tc.err != "" {
|
||||
assert.ErrorContains(t, err, tc.err)
|
||||
} else if tc.invalid != "" {
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, cmp.ErrorType(p.Err, reflect.TypeOf(&pluginError{})))
|
||||
assert.ErrorContains(t, p.Err, tc.invalid)
|
||||
} else {
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, NamePrefix+p.Name, goodPluginName)
|
||||
assert.Equal(t, p.SchemaVersion, "0.1.0")
|
||||
assert.Equal(t, p.Vendor, "e2e-testing")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCandidatePath(t *testing.T) {
|
||||
exp := "/some/path"
|
||||
cand := &candidate{path: exp}
|
||||
assert.Equal(t, exp, cand.Path())
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
package manager
|
||||
|
||||
import (
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const (
|
||||
// CommandAnnotationPlugin is added to every stub command added by
|
||||
// AddPluginCommandStubs with the value "true" and so can be
|
||||
// used to distinguish plugin stubs from regular commands.
|
||||
CommandAnnotationPlugin = "com.docker.cli.plugin"
|
||||
|
||||
// CommandAnnotationPluginVendor is added to every stub command
|
||||
// added by AddPluginCommandStubs and contains the vendor of
|
||||
// that plugin.
|
||||
CommandAnnotationPluginVendor = "com.docker.cli.plugin.vendor"
|
||||
|
||||
// CommandAnnotationPluginVersion is added to every stub command
|
||||
// added by AddPluginCommandStubs and contains the version of
|
||||
// that plugin.
|
||||
CommandAnnotationPluginVersion = "com.docker.cli.plugin.version"
|
||||
|
||||
// CommandAnnotationPluginInvalid is added to any stub command
|
||||
// added by AddPluginCommandStubs for an invalid command (that
|
||||
// is, one which failed it's candidate test) and contains the
|
||||
// reason for the failure.
|
||||
CommandAnnotationPluginInvalid = "com.docker.cli.plugin-invalid"
|
||||
)
|
||||
|
||||
// AddPluginCommandStubs adds a stub cobra.Commands for each valid and invalid
|
||||
// plugin. The command stubs will have several annotations added, see
|
||||
// `CommandAnnotationPlugin*`.
|
||||
func AddPluginCommandStubs(dockerCli command.Cli, cmd *cobra.Command) error {
|
||||
plugins, err := ListPlugins(dockerCli, cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, p := range plugins {
|
||||
vendor := p.Vendor
|
||||
if vendor == "" {
|
||||
vendor = "unknown"
|
||||
}
|
||||
annotations := map[string]string{
|
||||
CommandAnnotationPlugin: "true",
|
||||
CommandAnnotationPluginVendor: vendor,
|
||||
CommandAnnotationPluginVersion: p.Version,
|
||||
}
|
||||
if p.Err != nil {
|
||||
annotations[CommandAnnotationPluginInvalid] = p.Err.Error()
|
||||
}
|
||||
cmd.AddCommand(&cobra.Command{
|
||||
Use: p.Name,
|
||||
Short: p.ShortDescription,
|
||||
Run: func(_ *cobra.Command, _ []string) {},
|
||||
Annotations: annotations,
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
package manager
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// pluginError is set as Plugin.Err by NewPlugin if the plugin
|
||||
// candidate fails one of the candidate tests. This exists primarily
|
||||
// to implement encoding.TextMarshaller such that rendering a plugin as JSON
|
||||
// (e.g. for `docker info -f '{{json .CLIPlugins}}'`) renders the Err
|
||||
// field as a useful string and not just `{}`. See
|
||||
// https://github.com/golang/go/issues/10748 for some discussion
|
||||
// around why the builtin error type doesn't implement this.
|
||||
type pluginError struct {
|
||||
cause error
|
||||
}
|
||||
|
||||
// Error satisfies the core error interface for pluginError.
|
||||
func (e *pluginError) Error() string {
|
||||
return e.cause.Error()
|
||||
}
|
||||
|
||||
// Cause satisfies the errors.causer interface for pluginError.
|
||||
func (e *pluginError) Cause() error {
|
||||
return e.cause
|
||||
}
|
||||
|
||||
// Unwrap provides compatibility for Go 1.13 error chains.
|
||||
func (e *pluginError) Unwrap() error {
|
||||
return e.cause
|
||||
}
|
||||
|
||||
// MarshalText marshalls the pluginError into a textual form.
|
||||
func (e *pluginError) MarshalText() (text []byte, err error) {
|
||||
return []byte(e.cause.Error()), nil
|
||||
}
|
||||
|
||||
// wrapAsPluginError wraps an error in a pluginError with an
|
||||
// additional message, analogous to errors.Wrapf.
|
||||
func wrapAsPluginError(err error, msg string) error {
|
||||
return &pluginError{cause: errors.Wrap(err, msg)}
|
||||
}
|
||||
|
||||
// NewPluginError creates a new pluginError, analogous to
|
||||
// errors.Errorf.
|
||||
func NewPluginError(msg string, args ...interface{}) error {
|
||||
return &pluginError{cause: errors.Errorf(msg, args...)}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package manager
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"gopkg.in/yaml.v2"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestPluginError(t *testing.T) {
|
||||
err := NewPluginError("new error")
|
||||
assert.Error(t, err, "new error")
|
||||
|
||||
inner := fmt.Errorf("testing")
|
||||
err = wrapAsPluginError(inner, "wrapping")
|
||||
assert.Error(t, err, "wrapping: testing")
|
||||
assert.Assert(t, errors.Is(err, inner))
|
||||
|
||||
actual, err := yaml.Marshal(err)
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, "'wrapping: testing'\n", string(actual))
|
||||
}
|
|
@ -0,0 +1,197 @@
|
|||
package manager
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/config"
|
||||
"github.com/fvbommel/sortorder"
|
||||
"github.com/spf13/cobra"
|
||||
exec "golang.org/x/sys/execabs"
|
||||
)
|
||||
|
||||
// ReexecEnvvar is the name of an ennvar which is set to the command
|
||||
// used to originally invoke the docker CLI when executing a
|
||||
// plugin. Assuming $PATH and $CWD remain unchanged this should allow
|
||||
// the plugin to re-execute the original CLI.
|
||||
const ReexecEnvvar = "DOCKER_CLI_PLUGIN_ORIGINAL_CLI_COMMAND"
|
||||
|
||||
// errPluginNotFound is the error returned when a plugin could not be found.
|
||||
type errPluginNotFound string
|
||||
|
||||
func (e errPluginNotFound) NotFound() {}
|
||||
|
||||
func (e errPluginNotFound) Error() string {
|
||||
return "Error: No such CLI plugin: " + string(e)
|
||||
}
|
||||
|
||||
type notFound interface{ NotFound() }
|
||||
|
||||
// IsNotFound is true if the given error is due to a plugin not being found.
|
||||
func IsNotFound(err error) bool {
|
||||
if e, ok := err.(*pluginError); ok {
|
||||
err = e.Cause()
|
||||
}
|
||||
_, ok := err.(notFound)
|
||||
return ok
|
||||
}
|
||||
|
||||
func getPluginDirs(dockerCli command.Cli) ([]string, error) {
|
||||
var pluginDirs []string
|
||||
|
||||
if cfg := dockerCli.ConfigFile(); cfg != nil {
|
||||
pluginDirs = append(pluginDirs, cfg.CLIPluginsExtraDirs...)
|
||||
}
|
||||
pluginDir, err := config.Path("cli-plugins")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pluginDirs = append(pluginDirs, pluginDir)
|
||||
pluginDirs = append(pluginDirs, defaultSystemPluginDirs...)
|
||||
return pluginDirs, nil
|
||||
}
|
||||
|
||||
func addPluginCandidatesFromDir(res map[string][]string, d string) error {
|
||||
dentries, err := ioutil.ReadDir(d)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, dentry := range dentries {
|
||||
switch dentry.Mode() & os.ModeType {
|
||||
case 0, os.ModeSymlink:
|
||||
// Regular file or symlink, keep going
|
||||
default:
|
||||
// Something else, ignore.
|
||||
continue
|
||||
}
|
||||
name := dentry.Name()
|
||||
if !strings.HasPrefix(name, NamePrefix) {
|
||||
continue
|
||||
}
|
||||
name = strings.TrimPrefix(name, NamePrefix)
|
||||
var err error
|
||||
if name, err = trimExeSuffix(name); err != nil {
|
||||
continue
|
||||
}
|
||||
res[name] = append(res[name], filepath.Join(d, dentry.Name()))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// listPluginCandidates returns a map from plugin name to the list of (unvalidated) Candidates. The list is in descending order of priority.
|
||||
func listPluginCandidates(dirs []string) (map[string][]string, error) {
|
||||
result := make(map[string][]string)
|
||||
for _, d := range dirs {
|
||||
// Silently ignore any directories which we cannot
|
||||
// Stat (e.g. due to permissions or anything else) or
|
||||
// which is not a directory.
|
||||
if fi, err := os.Stat(d); err != nil || !fi.IsDir() {
|
||||
continue
|
||||
}
|
||||
if err := addPluginCandidatesFromDir(result, d); err != nil {
|
||||
// Silently ignore paths which don't exist.
|
||||
if os.IsNotExist(err) {
|
||||
continue
|
||||
}
|
||||
return nil, err // Or return partial result?
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ListPlugins produces a list of the plugins available on the system
|
||||
func ListPlugins(dockerCli command.Cli, rootcmd *cobra.Command) ([]Plugin, error) {
|
||||
pluginDirs, err := getPluginDirs(dockerCli)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
candidates, err := listPluginCandidates(pluginDirs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var plugins []Plugin
|
||||
for _, paths := range candidates {
|
||||
if len(paths) == 0 {
|
||||
continue
|
||||
}
|
||||
c := &candidate{paths[0]}
|
||||
p, err := newPlugin(c, rootcmd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !IsNotFound(p.Err) {
|
||||
p.ShadowedPaths = paths[1:]
|
||||
plugins = append(plugins, p)
|
||||
}
|
||||
}
|
||||
|
||||
sort.Slice(plugins, func(i, j int) bool {
|
||||
return sortorder.NaturalLess(plugins[i].Name, plugins[j].Name)
|
||||
})
|
||||
|
||||
return plugins, nil
|
||||
}
|
||||
|
||||
// PluginRunCommand returns an "os/exec".Cmd which when .Run() will execute the named plugin.
|
||||
// The rootcmd argument is referenced to determine the set of builtin commands in order to detect conficts.
|
||||
// The error returned satisfies the IsNotFound() predicate if no plugin was found or if the first candidate plugin was invalid somehow.
|
||||
func PluginRunCommand(dockerCli command.Cli, name string, rootcmd *cobra.Command) (*exec.Cmd, error) {
|
||||
// This uses the full original args, not the args which may
|
||||
// have been provided by cobra to our caller. This is because
|
||||
// they lack e.g. global options which we must propagate here.
|
||||
args := os.Args[1:]
|
||||
if !pluginNameRe.MatchString(name) {
|
||||
// We treat this as "not found" so that callers will
|
||||
// fallback to their "invalid" command path.
|
||||
return nil, errPluginNotFound(name)
|
||||
}
|
||||
exename := addExeSuffix(NamePrefix + name)
|
||||
pluginDirs, err := getPluginDirs(dockerCli)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, d := range pluginDirs {
|
||||
path := filepath.Join(d, exename)
|
||||
|
||||
// We stat here rather than letting the exec tell us
|
||||
// ENOENT because the latter does not distinguish a
|
||||
// file not existing from its dynamic loader or one of
|
||||
// its libraries not existing.
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
continue
|
||||
}
|
||||
|
||||
c := &candidate{path: path}
|
||||
plugin, err := newPlugin(c, rootcmd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if plugin.Err != nil {
|
||||
// TODO: why are we not returning plugin.Err?
|
||||
return nil, errPluginNotFound(name)
|
||||
}
|
||||
cmd := exec.Command(plugin.Path, args...)
|
||||
// Using dockerCli.{In,Out,Err}() here results in a hang until something is input.
|
||||
// See: - https://github.com/golang/go/issues/10338
|
||||
// - https://github.com/golang/go/commit/d000e8742a173aa0659584aa01b7ba2834ba28ab
|
||||
// os.Stdin is a *os.File which avoids this behaviour. We don't need the functionality
|
||||
// of the wrappers here anyway.
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
cmd.Env = os.Environ()
|
||||
cmd.Env = append(cmd.Env, ReexecEnvvar+"="+os.Args[0])
|
||||
|
||||
return cmd, nil
|
||||
}
|
||||
return nil, errPluginNotFound(name)
|
||||
}
|
|
@ -0,0 +1,143 @@
|
|||
package manager
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/config"
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/spf13/cobra"
|
||||
"gotest.tools/v3/assert"
|
||||
"gotest.tools/v3/fs"
|
||||
)
|
||||
|
||||
func TestListPluginCandidates(t *testing.T) {
|
||||
// Populate a selection of directories with various shadowed and bogus/obscure plugin candidates.
|
||||
// For the purposes of this test no contents is required and permissions are irrelevant.
|
||||
dir := fs.NewDir(t, t.Name(),
|
||||
fs.WithDir(
|
||||
"plugins1",
|
||||
fs.WithFile("docker-plugin1", ""), // This appears in each directory
|
||||
fs.WithFile("not-a-plugin", ""), // Should be ignored
|
||||
fs.WithFile("docker-symlinked1", ""), // This and ...
|
||||
fs.WithSymlink("docker-symlinked2", "docker-symlinked1"), // ... this should both appear
|
||||
fs.WithDir("ignored1"), // A directory should be ignored
|
||||
),
|
||||
fs.WithDir(
|
||||
"plugins2",
|
||||
fs.WithFile("docker-plugin1", ""),
|
||||
fs.WithFile("also-not-a-plugin", ""),
|
||||
fs.WithFile("docker-hardlink1", ""), // This and ...
|
||||
fs.WithHardlink("docker-hardlink2", "docker-hardlink1"), // ... this should both appear
|
||||
fs.WithDir("ignored2"),
|
||||
),
|
||||
fs.WithDir(
|
||||
"plugins3-target", // Will be referenced as a symlink from below
|
||||
fs.WithFile("docker-plugin1", ""),
|
||||
fs.WithDir("ignored3"),
|
||||
fs.WithSymlink("docker-brokensymlink", "broken"), // A broken symlink is still a candidate (but would fail tests later)
|
||||
fs.WithFile("non-plugin-symlinked", ""), // This shouldn't appear, but ...
|
||||
fs.WithSymlink("docker-symlinked", "non-plugin-symlinked"), // ... this link to it should.
|
||||
),
|
||||
fs.WithSymlink("plugins3", "plugins3-target"),
|
||||
fs.WithFile("/plugins4", ""),
|
||||
fs.WithSymlink("plugins5", "plugins5-nonexistent-target"),
|
||||
)
|
||||
defer dir.Remove()
|
||||
|
||||
var dirs []string
|
||||
for _, d := range []string{"plugins1", "nonexistent", "plugins2", "plugins3", "plugins4", "plugins5"} {
|
||||
dirs = append(dirs, dir.Join(d))
|
||||
}
|
||||
|
||||
candidates, err := listPluginCandidates(dirs)
|
||||
assert.NilError(t, err)
|
||||
exp := map[string][]string{
|
||||
"plugin1": {
|
||||
dir.Join("plugins1", "docker-plugin1"),
|
||||
dir.Join("plugins2", "docker-plugin1"),
|
||||
dir.Join("plugins3", "docker-plugin1"),
|
||||
},
|
||||
"symlinked1": {
|
||||
dir.Join("plugins1", "docker-symlinked1"),
|
||||
},
|
||||
"symlinked2": {
|
||||
dir.Join("plugins1", "docker-symlinked2"),
|
||||
},
|
||||
"hardlink1": {
|
||||
dir.Join("plugins2", "docker-hardlink1"),
|
||||
},
|
||||
"hardlink2": {
|
||||
dir.Join("plugins2", "docker-hardlink2"),
|
||||
},
|
||||
"brokensymlink": {
|
||||
dir.Join("plugins3", "docker-brokensymlink"),
|
||||
},
|
||||
"symlinked": {
|
||||
dir.Join("plugins3", "docker-symlinked"),
|
||||
},
|
||||
}
|
||||
|
||||
assert.DeepEqual(t, candidates, exp)
|
||||
}
|
||||
|
||||
func TestListPluginsIsSorted(t *testing.T) {
|
||||
dir := fs.NewDir(t, t.Name(),
|
||||
fs.WithFile("docker-bbb", `
|
||||
#!/bin/sh
|
||||
echo '{"SchemaVersion":"0.1.0"}'`, fs.WithMode(0777)),
|
||||
fs.WithFile("docker-aaa", `
|
||||
#!/bin/sh
|
||||
echo '{"SchemaVersion":"0.1.0"}'`, fs.WithMode(0777)),
|
||||
)
|
||||
defer dir.Remove()
|
||||
|
||||
cli := test.NewFakeCli(nil)
|
||||
cli.SetConfigFile(&configfile.ConfigFile{CLIPluginsExtraDirs: []string{dir.Path()}})
|
||||
|
||||
plugins, err := ListPlugins(cli, &cobra.Command{})
|
||||
assert.NilError(t, err)
|
||||
|
||||
// We're only interested in the plugins we created for testing this, and only
|
||||
// if they appear in the expected order
|
||||
var names []string
|
||||
for _, p := range plugins {
|
||||
if p.Name == "aaa" || p.Name == "bbb" {
|
||||
names = append(names, p.Name)
|
||||
}
|
||||
}
|
||||
assert.DeepEqual(t, names, []string{"aaa", "bbb"})
|
||||
}
|
||||
|
||||
func TestErrPluginNotFound(t *testing.T) {
|
||||
var err error = errPluginNotFound("test")
|
||||
err.(errPluginNotFound).NotFound()
|
||||
assert.Error(t, err, "Error: No such CLI plugin: test")
|
||||
assert.Assert(t, IsNotFound(err))
|
||||
assert.Assert(t, !IsNotFound(nil))
|
||||
}
|
||||
|
||||
func TestGetPluginDirs(t *testing.T) {
|
||||
cli := test.NewFakeCli(nil)
|
||||
|
||||
pluginDir, err := config.Path("cli-plugins")
|
||||
assert.NilError(t, err)
|
||||
expected := append([]string{pluginDir}, defaultSystemPluginDirs...)
|
||||
|
||||
var pluginDirs []string
|
||||
pluginDirs, err = getPluginDirs(cli)
|
||||
assert.Equal(t, strings.Join(expected, ":"), strings.Join(pluginDirs, ":"))
|
||||
assert.NilError(t, err)
|
||||
|
||||
extras := []string{
|
||||
"foo", "bar", "baz",
|
||||
}
|
||||
expected = append(extras, expected...)
|
||||
cli.SetConfigFile(&configfile.ConfigFile{
|
||||
CLIPluginsExtraDirs: extras,
|
||||
})
|
||||
pluginDirs, err = getPluginDirs(cli)
|
||||
assert.DeepEqual(t, expected, pluginDirs)
|
||||
assert.NilError(t, err)
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package manager
|
||||
|
||||
var defaultSystemPluginDirs = []string{
|
||||
"/usr/local/lib/docker/cli-plugins", "/usr/local/libexec/docker/cli-plugins",
|
||||
"/usr/lib/docker/cli-plugins", "/usr/libexec/docker/cli-plugins",
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package manager
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
var defaultSystemPluginDirs = []string{
|
||||
filepath.Join(os.Getenv("ProgramData"), "Docker", "cli-plugins"),
|
||||
filepath.Join(os.Getenv("ProgramFiles"), "Docker", "cli-plugins"),
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package manager
|
||||
|
||||
const (
|
||||
// NamePrefix is the prefix required on all plugin binary names
|
||||
NamePrefix = "docker-"
|
||||
|
||||
// MetadataSubcommandName is the name of the plugin subcommand
|
||||
// which must be supported by every plugin and returns the
|
||||
// plugin metadata.
|
||||
MetadataSubcommandName = "docker-cli-plugin-metadata"
|
||||
)
|
||||
|
||||
// Metadata provided by the plugin.
|
||||
type Metadata struct {
|
||||
// SchemaVersion describes the version of this struct. Mandatory, must be "0.1.0"
|
||||
SchemaVersion string `json:",omitempty"`
|
||||
// Vendor is the name of the plugin vendor. Mandatory
|
||||
Vendor string `json:",omitempty"`
|
||||
// Version is the optional version of this plugin.
|
||||
Version string `json:",omitempty"`
|
||||
// ShortDescription should be suitable for a single line help message.
|
||||
ShortDescription string `json:",omitempty"`
|
||||
// URL is a pointer to the plugin's homepage.
|
||||
URL string `json:",omitempty"`
|
||||
// Experimental specifies whether the plugin is experimental.
|
||||
// Deprecated: experimental features are now always enabled in the CLI
|
||||
Experimental bool `json:",omitempty"`
|
||||
}
|
|
@ -0,0 +1,108 @@
|
|||
package manager
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
pluginNameRe = regexp.MustCompile("^[a-z][a-z0-9]*$")
|
||||
)
|
||||
|
||||
// Plugin represents a potential plugin with all it's metadata.
|
||||
type Plugin struct {
|
||||
Metadata
|
||||
|
||||
Name string `json:",omitempty"`
|
||||
Path string `json:",omitempty"`
|
||||
|
||||
// Err is non-nil if the plugin failed one of the candidate tests.
|
||||
Err error `json:",omitempty"`
|
||||
|
||||
// ShadowedPaths contains the paths of any other plugins which this plugin takes precedence over.
|
||||
ShadowedPaths []string `json:",omitempty"`
|
||||
}
|
||||
|
||||
// newPlugin determines if the given candidate is valid and returns a
|
||||
// Plugin. If the candidate fails one of the tests then `Plugin.Err`
|
||||
// is set, and is always a `pluginError`, but the `Plugin` is still
|
||||
// returned with no error. An error is only returned due to a
|
||||
// non-recoverable error.
|
||||
//
|
||||
// nolint: gocyclo
|
||||
func newPlugin(c Candidate, rootcmd *cobra.Command) (Plugin, error) {
|
||||
path := c.Path()
|
||||
if path == "" {
|
||||
return Plugin{}, errors.New("plugin candidate path cannot be empty")
|
||||
}
|
||||
|
||||
// The candidate listing process should have skipped anything
|
||||
// which would fail here, so there are all real errors.
|
||||
fullname := filepath.Base(path)
|
||||
if fullname == "." {
|
||||
return Plugin{}, errors.Errorf("unable to determine basename of plugin candidate %q", path)
|
||||
}
|
||||
var err error
|
||||
if fullname, err = trimExeSuffix(fullname); err != nil {
|
||||
return Plugin{}, errors.Wrapf(err, "plugin candidate %q", path)
|
||||
}
|
||||
if !strings.HasPrefix(fullname, NamePrefix) {
|
||||
return Plugin{}, errors.Errorf("plugin candidate %q: does not have %q prefix", path, NamePrefix)
|
||||
}
|
||||
|
||||
p := Plugin{
|
||||
Name: strings.TrimPrefix(fullname, NamePrefix),
|
||||
Path: path,
|
||||
}
|
||||
|
||||
// Now apply the candidate tests, so these update p.Err.
|
||||
if !pluginNameRe.MatchString(p.Name) {
|
||||
p.Err = NewPluginError("plugin candidate %q did not match %q", p.Name, pluginNameRe.String())
|
||||
return p, nil
|
||||
}
|
||||
|
||||
if rootcmd != nil {
|
||||
for _, cmd := range rootcmd.Commands() {
|
||||
// Ignore conflicts with commands which are
|
||||
// just plugin stubs (i.e. from a previous
|
||||
// call to AddPluginCommandStubs).
|
||||
if p := cmd.Annotations[CommandAnnotationPlugin]; p == "true" {
|
||||
continue
|
||||
}
|
||||
if cmd.Name() == p.Name {
|
||||
p.Err = NewPluginError("plugin %q duplicates builtin command", p.Name)
|
||||
return p, nil
|
||||
}
|
||||
if cmd.HasAlias(p.Name) {
|
||||
p.Err = NewPluginError("plugin %q duplicates an alias of builtin command %q", p.Name, cmd.Name())
|
||||
return p, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We are supposed to check for relevant execute permissions here. Instead we rely on an attempt to execute.
|
||||
meta, err := c.Metadata()
|
||||
if err != nil {
|
||||
p.Err = wrapAsPluginError(err, "failed to fetch metadata")
|
||||
return p, nil
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(meta, &p.Metadata); err != nil {
|
||||
p.Err = wrapAsPluginError(err, "invalid metadata")
|
||||
return p, nil
|
||||
}
|
||||
if p.Metadata.SchemaVersion != "0.1.0" {
|
||||
p.Err = NewPluginError("plugin SchemaVersion %q is not valid, must be 0.1.0", p.Metadata.SchemaVersion)
|
||||
return p, nil
|
||||
}
|
||||
if p.Metadata.Vendor == "" {
|
||||
p.Err = NewPluginError("plugin metadata does not define a vendor")
|
||||
return p, nil
|
||||
}
|
||||
return p, nil
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package manager
|
||||
|
||||
func trimExeSuffix(s string) (string, error) {
|
||||
return s, nil
|
||||
}
|
||||
func addExeSuffix(s string) string {
|
||||
return s
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
package manager
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// This is made slightly more complex due to needing to be case insensitive.
|
||||
func trimExeSuffix(s string) (string, error) {
|
||||
ext := filepath.Ext(s)
|
||||
if ext == "" {
|
||||
return "", errors.Errorf("path %q lacks required file extension", s)
|
||||
}
|
||||
|
||||
exe := ".exe"
|
||||
if !strings.EqualFold(ext, exe) {
|
||||
return "", errors.Errorf("path %q lacks required %q suffix", s, exe)
|
||||
}
|
||||
return strings.TrimSuffix(s, ext), nil
|
||||
}
|
||||
|
||||
func addExeSuffix(s string) string {
|
||||
return s + ".exe"
|
||||
}
|
|
@ -0,0 +1,162 @@
|
|||
package plugin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli-plugins/manager"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/connhelper"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// PersistentPreRunE must be called by any plugin command (or
|
||||
// subcommand) which uses the cobra `PersistentPreRun*` hook. Plugins
|
||||
// which do not make use of `PersistentPreRun*` do not need to call
|
||||
// this (although it remains safe to do so). Plugins are recommended
|
||||
// to use `PersistenPreRunE` to enable the error to be
|
||||
// returned. Should not be called outside of a command's
|
||||
// PersistentPreRunE hook and must not be run unless Run has been
|
||||
// called.
|
||||
var PersistentPreRunE func(*cobra.Command, []string) error
|
||||
|
||||
// RunPlugin executes the specified plugin command
|
||||
func RunPlugin(dockerCli *command.DockerCli, plugin *cobra.Command, meta manager.Metadata) error {
|
||||
tcmd := newPluginCommand(dockerCli, plugin, meta)
|
||||
|
||||
var persistentPreRunOnce sync.Once
|
||||
PersistentPreRunE = func(_ *cobra.Command, _ []string) error {
|
||||
var err error
|
||||
persistentPreRunOnce.Do(func() {
|
||||
var opts []command.InitializeOpt
|
||||
if os.Getenv("DOCKER_CLI_PLUGIN_USE_DIAL_STDIO") != "" {
|
||||
opts = append(opts, withPluginClientConn(plugin.Name()))
|
||||
}
|
||||
err = tcmd.Initialize(opts...)
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
cmd, args, err := tcmd.HandleGlobalFlags()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// We've parsed global args already, so reset args to those
|
||||
// which remain.
|
||||
cmd.SetArgs(args)
|
||||
return cmd.Execute()
|
||||
}
|
||||
|
||||
// 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) {
|
||||
dockerCli, err := command.NewDockerCli()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
plugin := makeCmd(dockerCli)
|
||||
|
||||
if err := RunPlugin(dockerCli, plugin, meta); err != nil {
|
||||
if sterr, ok := err.(cli.StatusError); ok {
|
||||
if sterr.Status != "" {
|
||||
fmt.Fprintln(dockerCli.Err(), sterr.Status)
|
||||
}
|
||||
// StatusError should only be used for errors, and all errors should
|
||||
// have a non-zero exit status, so never exit with 0
|
||||
if sterr.StatusCode == 0 {
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(sterr.StatusCode)
|
||||
}
|
||||
fmt.Fprintln(dockerCli.Err(), err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func withPluginClientConn(name string) command.InitializeOpt {
|
||||
return command.WithInitializeClient(func(dockerCli *command.DockerCli) (client.APIClient, error) {
|
||||
cmd := "docker"
|
||||
if x := os.Getenv(manager.ReexecEnvvar); x != "" {
|
||||
cmd = x
|
||||
}
|
||||
var flags []string
|
||||
|
||||
// Accumulate all the global arguments, that is those
|
||||
// up to (but not including) the plugin's name. This
|
||||
// ensures that `docker system dial-stdio` is
|
||||
// evaluating the same set of `--config`, `--tls*` etc
|
||||
// global options as the plugin was called with, which
|
||||
// in turn is the same as what the original docker
|
||||
// invocation was passed.
|
||||
for _, a := range os.Args[1:] {
|
||||
if a == name {
|
||||
break
|
||||
}
|
||||
flags = append(flags, a)
|
||||
}
|
||||
flags = append(flags, "system", "dial-stdio")
|
||||
|
||||
helper, err := connhelper.GetCommandConnectionHelper(cmd, flags...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return client.NewClientWithOpts(client.WithDialContext(helper.Dialer))
|
||||
})
|
||||
}
|
||||
|
||||
func newPluginCommand(dockerCli *command.DockerCli, plugin *cobra.Command, meta manager.Metadata) *cli.TopLevelCommand {
|
||||
name := plugin.Name()
|
||||
fullname := manager.NamePrefix + name
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: fmt.Sprintf("docker [OPTIONS] %s [ARG...]", name),
|
||||
Short: fullname + " is a Docker CLI plugin",
|
||||
SilenceUsage: true,
|
||||
SilenceErrors: true,
|
||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
// We can't use this as the hook directly since it is initialised later (in runPlugin)
|
||||
return PersistentPreRunE(cmd, args)
|
||||
},
|
||||
TraverseChildren: true,
|
||||
DisableFlagsInUseLine: true,
|
||||
}
|
||||
opts, flags := cli.SetupPluginRootCommand(cmd)
|
||||
|
||||
cmd.SetOut(dockerCli.Out())
|
||||
|
||||
cmd.AddCommand(
|
||||
plugin,
|
||||
newMetadataSubcommand(plugin, meta),
|
||||
)
|
||||
|
||||
cli.DisableFlagsInUseLine(cmd)
|
||||
|
||||
return cli.NewTopLevelCommand(cmd, dockerCli, opts, flags)
|
||||
}
|
||||
|
||||
func newMetadataSubcommand(plugin *cobra.Command, meta manager.Metadata) *cobra.Command {
|
||||
if meta.ShortDescription == "" {
|
||||
meta.ShortDescription = plugin.Short
|
||||
}
|
||||
cmd := &cobra.Command{
|
||||
Use: manager.MetadataSubcommandName,
|
||||
Hidden: true,
|
||||
// Suppress the global/parent PersistentPreRunE, which
|
||||
// needlessly initializes the client and tries to
|
||||
// connect to the daemon.
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
enc.SetEscapeHTML(false)
|
||||
enc.SetIndent("", " ")
|
||||
return enc.Encode(meta)
|
||||
},
|
||||
}
|
||||
return cmd
|
||||
}
|
|
@ -0,0 +1,386 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
pluginmanager "github.com/docker/cli/cli-plugins/manager"
|
||||
"github.com/docker/cli/cli/command"
|
||||
cliconfig "github.com/docker/cli/cli/config"
|
||||
cliflags "github.com/docker/cli/cli/flags"
|
||||
"github.com/moby/term"
|
||||
"github.com/morikuni/aec"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
// setupCommonRootCommand contains the setup common to
|
||||
// SetupRootCommand and SetupPluginRootCommand.
|
||||
func setupCommonRootCommand(rootCmd *cobra.Command) (*cliflags.ClientOptions, *pflag.FlagSet, *cobra.Command) {
|
||||
opts := cliflags.NewClientOptions()
|
||||
flags := rootCmd.Flags()
|
||||
|
||||
flags.StringVar(&opts.ConfigDir, "config", cliconfig.Dir(), "Location of client config files")
|
||||
opts.Common.InstallFlags(flags)
|
||||
|
||||
cobra.AddTemplateFunc("add", func(a, b int) int { return a + b })
|
||||
cobra.AddTemplateFunc("hasSubCommands", hasSubCommands)
|
||||
cobra.AddTemplateFunc("hasManagementSubCommands", hasManagementSubCommands)
|
||||
cobra.AddTemplateFunc("hasInvalidPlugins", hasInvalidPlugins)
|
||||
cobra.AddTemplateFunc("operationSubCommands", operationSubCommands)
|
||||
cobra.AddTemplateFunc("managementSubCommands", managementSubCommands)
|
||||
cobra.AddTemplateFunc("invalidPlugins", invalidPlugins)
|
||||
cobra.AddTemplateFunc("wrappedFlagUsages", wrappedFlagUsages)
|
||||
cobra.AddTemplateFunc("vendorAndVersion", vendorAndVersion)
|
||||
cobra.AddTemplateFunc("invalidPluginReason", invalidPluginReason)
|
||||
cobra.AddTemplateFunc("isPlugin", isPlugin)
|
||||
cobra.AddTemplateFunc("isExperimental", isExperimental)
|
||||
cobra.AddTemplateFunc("hasAdditionalHelp", hasAdditionalHelp)
|
||||
cobra.AddTemplateFunc("additionalHelp", additionalHelp)
|
||||
cobra.AddTemplateFunc("decoratedName", decoratedName)
|
||||
|
||||
rootCmd.SetUsageTemplate(usageTemplate)
|
||||
rootCmd.SetHelpTemplate(helpTemplate)
|
||||
rootCmd.SetFlagErrorFunc(FlagErrorFunc)
|
||||
rootCmd.SetHelpCommand(helpCommand)
|
||||
|
||||
rootCmd.PersistentFlags().BoolP("help", "h", false, "Print usage")
|
||||
rootCmd.PersistentFlags().MarkShorthandDeprecated("help", "please use --help")
|
||||
rootCmd.PersistentFlags().Lookup("help").Hidden = true
|
||||
|
||||
rootCmd.Annotations = map[string]string{"additionalHelp": "To get more help with docker, check out our guides at https://docs.docker.com/go/guides/"}
|
||||
|
||||
return opts, flags, helpCommand
|
||||
}
|
||||
|
||||
// SetupRootCommand sets default usage, help, and error handling for the
|
||||
// root command.
|
||||
func SetupRootCommand(rootCmd *cobra.Command) (*cliflags.ClientOptions, *pflag.FlagSet, *cobra.Command) {
|
||||
opts, flags, helpCmd := setupCommonRootCommand(rootCmd)
|
||||
|
||||
rootCmd.SetVersionTemplate("Docker version {{.Version}}\n")
|
||||
|
||||
return opts, flags, helpCmd
|
||||
}
|
||||
|
||||
// SetupPluginRootCommand sets default usage, help and error handling for a plugin root command.
|
||||
func SetupPluginRootCommand(rootCmd *cobra.Command) (*cliflags.ClientOptions, *pflag.FlagSet) {
|
||||
opts, flags, _ := setupCommonRootCommand(rootCmd)
|
||||
return opts, flags
|
||||
}
|
||||
|
||||
// FlagErrorFunc prints an error message which matches the format of the
|
||||
// docker/cli/cli error messages
|
||||
func FlagErrorFunc(cmd *cobra.Command, err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
usage := ""
|
||||
if cmd.HasSubCommands() {
|
||||
usage = "\n\n" + cmd.UsageString()
|
||||
}
|
||||
return StatusError{
|
||||
Status: fmt.Sprintf("%s\nSee '%s --help'.%s", err, cmd.CommandPath(), usage),
|
||||
StatusCode: 125,
|
||||
}
|
||||
}
|
||||
|
||||
// TopLevelCommand encapsulates a top-level cobra command (either
|
||||
// docker CLI or a plugin) and global flag handling logic necessary
|
||||
// for plugins.
|
||||
type TopLevelCommand struct {
|
||||
cmd *cobra.Command
|
||||
dockerCli *command.DockerCli
|
||||
opts *cliflags.ClientOptions
|
||||
flags *pflag.FlagSet
|
||||
args []string
|
||||
}
|
||||
|
||||
// NewTopLevelCommand returns a new TopLevelCommand object
|
||||
func NewTopLevelCommand(cmd *cobra.Command, dockerCli *command.DockerCli, opts *cliflags.ClientOptions, flags *pflag.FlagSet) *TopLevelCommand {
|
||||
return &TopLevelCommand{cmd, dockerCli, opts, flags, os.Args[1:]}
|
||||
}
|
||||
|
||||
// SetArgs sets the args (default os.Args[:1] used to invoke the command
|
||||
func (tcmd *TopLevelCommand) SetArgs(args []string) {
|
||||
tcmd.args = args
|
||||
tcmd.cmd.SetArgs(args)
|
||||
}
|
||||
|
||||
// SetFlag sets a flag in the local flag set of the top-level command
|
||||
func (tcmd *TopLevelCommand) SetFlag(name, value string) {
|
||||
tcmd.cmd.Flags().Set(name, value)
|
||||
}
|
||||
|
||||
// HandleGlobalFlags takes care of parsing global flags defined on the
|
||||
// command, it returns the underlying cobra command and the args it
|
||||
// will be called with (or an error).
|
||||
//
|
||||
// On success the caller is responsible for calling Initialize()
|
||||
// before calling `Execute` on the returned command.
|
||||
func (tcmd *TopLevelCommand) HandleGlobalFlags() (*cobra.Command, []string, error) {
|
||||
cmd := tcmd.cmd
|
||||
|
||||
// We manually parse the global arguments and find the
|
||||
// subcommand in order to properly deal with plugins. We rely
|
||||
// on the root command never having any non-flag arguments. We
|
||||
// create our own FlagSet so that we can configure it
|
||||
// (e.g. `SetInterspersed` below) in an idempotent way.
|
||||
flags := pflag.NewFlagSet(cmd.Name(), pflag.ContinueOnError)
|
||||
|
||||
// We need !interspersed to ensure we stop at the first
|
||||
// potential command instead of accumulating it into
|
||||
// flags.Args() and then continuing on and finding other
|
||||
// arguments which we try and treat as globals (when they are
|
||||
// actually arguments to the subcommand).
|
||||
flags.SetInterspersed(false)
|
||||
|
||||
// We need the single parse to see both sets of flags.
|
||||
flags.AddFlagSet(cmd.Flags())
|
||||
flags.AddFlagSet(cmd.PersistentFlags())
|
||||
// Now parse the global flags, up to (but not including) the
|
||||
// first command. The result will be that all the remaining
|
||||
// arguments are in `flags.Args()`.
|
||||
if err := flags.Parse(tcmd.args); err != nil {
|
||||
// Our FlagErrorFunc uses the cli, make sure it is initialized
|
||||
if err := tcmd.Initialize(); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return nil, nil, cmd.FlagErrorFunc()(cmd, err)
|
||||
}
|
||||
|
||||
return cmd, flags.Args(), nil
|
||||
}
|
||||
|
||||
// Initialize finalises global option parsing and initializes the docker client.
|
||||
func (tcmd *TopLevelCommand) Initialize(ops ...command.InitializeOpt) error {
|
||||
tcmd.opts.Common.SetDefaultOptions(tcmd.flags)
|
||||
return tcmd.dockerCli.Initialize(tcmd.opts, ops...)
|
||||
}
|
||||
|
||||
// VisitAll will traverse all commands from the root.
|
||||
// This is different from the VisitAll of cobra.Command where only parents
|
||||
// are checked.
|
||||
func VisitAll(root *cobra.Command, fn func(*cobra.Command)) {
|
||||
for _, cmd := range root.Commands() {
|
||||
VisitAll(cmd, fn)
|
||||
}
|
||||
fn(root)
|
||||
}
|
||||
|
||||
// DisableFlagsInUseLine sets the DisableFlagsInUseLine flag on all
|
||||
// commands within the tree rooted at cmd.
|
||||
func DisableFlagsInUseLine(cmd *cobra.Command) {
|
||||
VisitAll(cmd, func(ccmd *cobra.Command) {
|
||||
// do not add a `[flags]` to the end of the usage line.
|
||||
ccmd.DisableFlagsInUseLine = true
|
||||
})
|
||||
}
|
||||
|
||||
var helpCommand = &cobra.Command{
|
||||
Use: "help [command]",
|
||||
Short: "Help about the command",
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {},
|
||||
PersistentPostRun: func(cmd *cobra.Command, args []string) {},
|
||||
RunE: func(c *cobra.Command, args []string) error {
|
||||
cmd, args, e := c.Root().Find(args)
|
||||
if cmd == nil || e != nil || len(args) > 0 {
|
||||
return errors.Errorf("unknown help topic: %v", strings.Join(args, " "))
|
||||
}
|
||||
helpFunc := cmd.HelpFunc()
|
||||
helpFunc(cmd, args)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func isExperimental(cmd *cobra.Command) bool {
|
||||
if _, ok := cmd.Annotations["experimentalCLI"]; ok {
|
||||
return true
|
||||
}
|
||||
var experimental bool
|
||||
cmd.VisitParents(func(cmd *cobra.Command) {
|
||||
if _, ok := cmd.Annotations["experimentalCLI"]; ok {
|
||||
experimental = true
|
||||
}
|
||||
})
|
||||
return experimental
|
||||
}
|
||||
|
||||
func additionalHelp(cmd *cobra.Command) string {
|
||||
if additionalHelp, ok := cmd.Annotations["additionalHelp"]; ok {
|
||||
style := aec.EmptyBuilder.Bold().ANSI
|
||||
return style.Apply(additionalHelp)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func hasAdditionalHelp(cmd *cobra.Command) bool {
|
||||
return additionalHelp(cmd) != ""
|
||||
}
|
||||
|
||||
func isPlugin(cmd *cobra.Command) bool {
|
||||
return cmd.Annotations[pluginmanager.CommandAnnotationPlugin] == "true"
|
||||
}
|
||||
|
||||
func hasSubCommands(cmd *cobra.Command) bool {
|
||||
return len(operationSubCommands(cmd)) > 0
|
||||
}
|
||||
|
||||
func hasManagementSubCommands(cmd *cobra.Command) bool {
|
||||
return len(managementSubCommands(cmd)) > 0
|
||||
}
|
||||
|
||||
func hasInvalidPlugins(cmd *cobra.Command) bool {
|
||||
return len(invalidPlugins(cmd)) > 0
|
||||
}
|
||||
|
||||
func operationSubCommands(cmd *cobra.Command) []*cobra.Command {
|
||||
cmds := []*cobra.Command{}
|
||||
for _, sub := range cmd.Commands() {
|
||||
if isPlugin(sub) {
|
||||
continue
|
||||
}
|
||||
if sub.IsAvailableCommand() && !sub.HasSubCommands() {
|
||||
cmds = append(cmds, sub)
|
||||
}
|
||||
}
|
||||
return cmds
|
||||
}
|
||||
|
||||
func wrappedFlagUsages(cmd *cobra.Command) string {
|
||||
width := 80
|
||||
if ws, err := term.GetWinsize(0); err == nil {
|
||||
width = int(ws.Width)
|
||||
}
|
||||
return cmd.Flags().FlagUsagesWrapped(width - 1)
|
||||
}
|
||||
|
||||
func decoratedName(cmd *cobra.Command) string {
|
||||
decoration := " "
|
||||
if isPlugin(cmd) {
|
||||
decoration = "*"
|
||||
}
|
||||
return cmd.Name() + decoration
|
||||
}
|
||||
|
||||
func vendorAndVersion(cmd *cobra.Command) string {
|
||||
if vendor, ok := cmd.Annotations[pluginmanager.CommandAnnotationPluginVendor]; ok && isPlugin(cmd) {
|
||||
version := ""
|
||||
if v, ok := cmd.Annotations[pluginmanager.CommandAnnotationPluginVersion]; ok && v != "" {
|
||||
version = ", " + v
|
||||
}
|
||||
return fmt.Sprintf("(%s%s)", vendor, version)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func managementSubCommands(cmd *cobra.Command) []*cobra.Command {
|
||||
cmds := []*cobra.Command{}
|
||||
for _, sub := range cmd.Commands() {
|
||||
if isPlugin(sub) {
|
||||
if invalidPluginReason(sub) == "" {
|
||||
cmds = append(cmds, sub)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if sub.IsAvailableCommand() && sub.HasSubCommands() {
|
||||
cmds = append(cmds, sub)
|
||||
}
|
||||
}
|
||||
return cmds
|
||||
}
|
||||
|
||||
func invalidPlugins(cmd *cobra.Command) []*cobra.Command {
|
||||
cmds := []*cobra.Command{}
|
||||
for _, sub := range cmd.Commands() {
|
||||
if !isPlugin(sub) {
|
||||
continue
|
||||
}
|
||||
if invalidPluginReason(sub) != "" {
|
||||
cmds = append(cmds, sub)
|
||||
}
|
||||
}
|
||||
return cmds
|
||||
}
|
||||
|
||||
func invalidPluginReason(cmd *cobra.Command) string {
|
||||
return cmd.Annotations[pluginmanager.CommandAnnotationPluginInvalid]
|
||||
}
|
||||
|
||||
var usageTemplate = `Usage:
|
||||
|
||||
{{- if not .HasSubCommands}} {{.UseLine}}{{end}}
|
||||
{{- if .HasSubCommands}} {{ .CommandPath}}{{- if .HasAvailableFlags}} [OPTIONS]{{end}} COMMAND{{end}}
|
||||
|
||||
{{if ne .Long ""}}{{ .Long | trim }}{{ else }}{{ .Short | trim }}{{end}}
|
||||
{{- if isExperimental .}}
|
||||
|
||||
EXPERIMENTAL:
|
||||
{{.CommandPath}} is an experimental feature.
|
||||
Experimental features provide early access to product functionality. These
|
||||
features may change between releases without warning, or can be removed from a
|
||||
future release. Learn more about experimental features in our documentation:
|
||||
https://docs.docker.com/go/experimental/
|
||||
|
||||
{{- end}}
|
||||
{{- if gt .Aliases 0}}
|
||||
|
||||
Aliases:
|
||||
{{.NameAndAliases}}
|
||||
|
||||
{{- end}}
|
||||
{{- if .HasExample}}
|
||||
|
||||
Examples:
|
||||
{{ .Example }}
|
||||
|
||||
{{- end}}
|
||||
{{- if .HasAvailableFlags}}
|
||||
|
||||
Options:
|
||||
{{ wrappedFlagUsages . | trimRightSpace}}
|
||||
|
||||
{{- end}}
|
||||
{{- if hasManagementSubCommands . }}
|
||||
|
||||
Management Commands:
|
||||
|
||||
{{- range managementSubCommands . }}
|
||||
{{rpad (decoratedName .) (add .NamePadding 1)}}{{.Short}}{{ if isPlugin .}} {{vendorAndVersion .}}{{ end}}
|
||||
{{- end}}
|
||||
|
||||
{{- end}}
|
||||
{{- if hasSubCommands .}}
|
||||
|
||||
Commands:
|
||||
|
||||
{{- range operationSubCommands . }}
|
||||
{{rpad .Name .NamePadding }} {{.Short}}
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
|
||||
{{- if hasInvalidPlugins . }}
|
||||
|
||||
Invalid Plugins:
|
||||
|
||||
{{- range invalidPlugins . }}
|
||||
{{rpad .Name .NamePadding }} {{invalidPluginReason .}}
|
||||
{{- end}}
|
||||
|
||||
{{- end}}
|
||||
|
||||
{{- if .HasSubCommands }}
|
||||
|
||||
Run '{{.CommandPath}} COMMAND --help' for more information on a command.
|
||||
{{- end}}
|
||||
{{- if hasAdditionalHelp .}}
|
||||
|
||||
{{ additionalHelp . }}
|
||||
{{- end}}
|
||||
`
|
||||
|
||||
var helpTemplate = `
|
||||
{{if or .Runnable .HasSubCommands}}{{.UsageString}}{{end}}`
|
|
@ -0,0 +1,88 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
pluginmanager "github.com/docker/cli/cli-plugins/manager"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"github.com/spf13/cobra"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
|
||||
func TestVisitAll(t *testing.T) {
|
||||
root := &cobra.Command{Use: "root"}
|
||||
sub1 := &cobra.Command{Use: "sub1"}
|
||||
sub1sub1 := &cobra.Command{Use: "sub1sub1"}
|
||||
sub1sub2 := &cobra.Command{Use: "sub1sub2"}
|
||||
sub2 := &cobra.Command{Use: "sub2"}
|
||||
|
||||
root.AddCommand(sub1, sub2)
|
||||
sub1.AddCommand(sub1sub1, sub1sub2)
|
||||
|
||||
// Take the opportunity to test DisableFlagsInUseLine too
|
||||
DisableFlagsInUseLine(root)
|
||||
|
||||
var visited []string
|
||||
VisitAll(root, func(ccmd *cobra.Command) {
|
||||
visited = append(visited, ccmd.Name())
|
||||
assert.Assert(t, ccmd.DisableFlagsInUseLine, "DisableFlagsInUseLine not set on %q", ccmd.Name())
|
||||
})
|
||||
expected := []string{"sub1sub1", "sub1sub2", "sub1", "sub2", "root"}
|
||||
assert.DeepEqual(t, expected, visited)
|
||||
}
|
||||
|
||||
func TestVendorAndVersion(t *testing.T) {
|
||||
// Non plugin.
|
||||
assert.Equal(t, vendorAndVersion(&cobra.Command{Use: "test"}), "")
|
||||
|
||||
// Plugins with various lengths of vendor.
|
||||
for _, tc := range []struct {
|
||||
vendor string
|
||||
version string
|
||||
expected string
|
||||
}{
|
||||
{vendor: "vendor", expected: "(vendor)"},
|
||||
{vendor: "vendor", version: "testing", expected: "(vendor, testing)"},
|
||||
} {
|
||||
t.Run(tc.vendor, func(t *testing.T) {
|
||||
cmd := &cobra.Command{
|
||||
Use: "test",
|
||||
Annotations: map[string]string{
|
||||
pluginmanager.CommandAnnotationPlugin: "true",
|
||||
pluginmanager.CommandAnnotationPluginVendor: tc.vendor,
|
||||
pluginmanager.CommandAnnotationPluginVersion: tc.version,
|
||||
},
|
||||
}
|
||||
assert.Equal(t, vendorAndVersion(cmd), tc.expected)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidPlugin(t *testing.T) {
|
||||
root := &cobra.Command{Use: "root"}
|
||||
sub1 := &cobra.Command{Use: "sub1"}
|
||||
sub1sub1 := &cobra.Command{Use: "sub1sub1"}
|
||||
sub1sub2 := &cobra.Command{Use: "sub1sub2"}
|
||||
sub2 := &cobra.Command{Use: "sub2"}
|
||||
|
||||
assert.Assert(t, is.Len(invalidPlugins(root), 0))
|
||||
|
||||
sub1.Annotations = map[string]string{
|
||||
pluginmanager.CommandAnnotationPlugin: "true",
|
||||
pluginmanager.CommandAnnotationPluginInvalid: "foo",
|
||||
}
|
||||
root.AddCommand(sub1, sub2)
|
||||
sub1.AddCommand(sub1sub1, sub1sub2)
|
||||
|
||||
assert.DeepEqual(t, invalidPlugins(root), []*cobra.Command{sub1}, cmpopts.IgnoreUnexported(cobra.Command{}))
|
||||
}
|
||||
|
||||
func TestDecoratedName(t *testing.T) {
|
||||
root := &cobra.Command{Use: "root"}
|
||||
topLevelCommand := &cobra.Command{Use: "pluginTopLevelCommand"}
|
||||
root.AddCommand(topLevelCommand)
|
||||
assert.Equal(t, decoratedName(topLevelCommand), "pluginTopLevelCommand ")
|
||||
topLevelCommand.Annotations = map[string]string{pluginmanager.CommandAnnotationPlugin: "true"}
|
||||
assert.Equal(t, decoratedName(topLevelCommand), "pluginTopLevelCommand*")
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package builder
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/image"
|
||||
)
|
||||
|
||||
// NewBuilderCommand returns a cobra command for `builder` subcommands
|
||||
func NewBuilderCommand(dockerCli command.Cli) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "builder",
|
||||
Short: "Manage builds",
|
||||
Args: cli.NoArgs,
|
||||
RunE: command.ShowHelp(dockerCli.Err()),
|
||||
Annotations: map[string]string{"version": "1.31"},
|
||||
}
|
||||
cmd.AddCommand(
|
||||
NewPruneCommand(dockerCli),
|
||||
image.NewBuildCommand(dockerCli),
|
||||
)
|
||||
return cmd
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
package builder
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api/types"
|
||||
units "github.com/docker/go-units"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type pruneOptions struct {
|
||||
force bool
|
||||
all bool
|
||||
filter opts.FilterOpt
|
||||
keepStorage opts.MemBytes
|
||||
}
|
||||
|
||||
// NewPruneCommand returns a new cobra prune command for images
|
||||
func NewPruneCommand(dockerCli command.Cli) *cobra.Command {
|
||||
options := pruneOptions{filter: opts.NewFilterOpt()}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "prune",
|
||||
Short: "Remove build cache",
|
||||
Args: cli.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
spaceReclaimed, output, err := runPrune(dockerCli, options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if output != "" {
|
||||
fmt.Fprintln(dockerCli.Out(), output)
|
||||
}
|
||||
fmt.Fprintln(dockerCli.Out(), "Total reclaimed space:", units.HumanSize(float64(spaceReclaimed)))
|
||||
return nil
|
||||
},
|
||||
Annotations: map[string]string{"version": "1.39"},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVarP(&options.force, "force", "f", false, "Do not prompt for confirmation")
|
||||
flags.BoolVarP(&options.all, "all", "a", false, "Remove all unused build cache, not just dangling ones")
|
||||
flags.Var(&options.filter, "filter", "Provide filter values (e.g. 'until=24h')")
|
||||
flags.Var(&options.keepStorage, "keep-storage", "Amount of disk space to keep for cache")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
const (
|
||||
normalWarning = `WARNING! This will remove all dangling 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) {
|
||||
pruneFilters := options.filter.Value()
|
||||
pruneFilters = command.PruneFilters(dockerCli, pruneFilters)
|
||||
|
||||
warning := normalWarning
|
||||
if options.all {
|
||||
warning = allCacheWarning
|
||||
}
|
||||
if !options.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), warning) {
|
||||
return 0, "", nil
|
||||
}
|
||||
|
||||
report, err := dockerCli.Client().BuildCachePrune(context.Background(), types.BuildCachePruneOptions{
|
||||
All: options.all,
|
||||
KeepStorage: options.keepStorage.Value(),
|
||||
Filters: pruneFilters,
|
||||
})
|
||||
if err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
|
||||
if len(report.CachesDeleted) > 0 {
|
||||
var sb strings.Builder
|
||||
sb.WriteString("Deleted build cache objects:\n")
|
||||
for _, id := range report.CachesDeleted {
|
||||
sb.WriteString(id)
|
||||
sb.WriteByte('\n')
|
||||
}
|
||||
output = sb.String()
|
||||
}
|
||||
|
||||
return report.SpaceReclaimed, output, nil
|
||||
}
|
||||
|
||||
// CachePrune executes a prune command for build cache
|
||||
func CachePrune(dockerCli command.Cli, all bool, filter opts.FilterOpt) (uint64, string, error) {
|
||||
return runPrune(dockerCli, pruneOptions{force: true, all: all, filter: filter})
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
package checkpoint
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/client"
|
||||
)
|
||||
|
||||
type fakeClient struct {
|
||||
client.Client
|
||||
checkpointCreateFunc func(container string, options types.CheckpointCreateOptions) error
|
||||
checkpointDeleteFunc func(container string, options types.CheckpointDeleteOptions) error
|
||||
checkpointListFunc func(container string, options types.CheckpointListOptions) ([]types.Checkpoint, error)
|
||||
}
|
||||
|
||||
func (cli *fakeClient) CheckpointCreate(ctx context.Context, container string, options types.CheckpointCreateOptions) error {
|
||||
if cli.checkpointCreateFunc != nil {
|
||||
return cli.checkpointCreateFunc(container, options)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cli *fakeClient) CheckpointDelete(ctx context.Context, container string, options types.CheckpointDeleteOptions) error {
|
||||
if cli.checkpointDeleteFunc != nil {
|
||||
return cli.checkpointDeleteFunc(container, options)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cli *fakeClient) CheckpointList(ctx context.Context, container string, options types.CheckpointListOptions) ([]types.Checkpoint, error) {
|
||||
if cli.checkpointListFunc != nil {
|
||||
return cli.checkpointListFunc(container, options)
|
||||
}
|
||||
return []types.Checkpoint{}, nil
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package checkpoint
|
||||
|
||||
import (
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// NewCheckpointCommand returns the `checkpoint` subcommand (only in experimental)
|
||||
func NewCheckpointCommand(dockerCli command.Cli) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "checkpoint",
|
||||
Short: "Manage checkpoints",
|
||||
Args: cli.NoArgs,
|
||||
RunE: command.ShowHelp(dockerCli.Err()),
|
||||
Annotations: map[string]string{
|
||||
"experimental": "",
|
||||
"ostype": "linux",
|
||||
"version": "1.25",
|
||||
},
|
||||
}
|
||||
cmd.AddCommand(
|
||||
newCreateCommand(dockerCli),
|
||||
newListCommand(dockerCli),
|
||||
newRemoveCommand(dockerCli),
|
||||
)
|
||||
return cmd
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
package checkpoint
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type createOptions struct {
|
||||
container string
|
||||
checkpoint string
|
||||
checkpointDir string
|
||||
leaveRunning bool
|
||||
}
|
||||
|
||||
func newCreateCommand(dockerCli command.Cli) *cobra.Command {
|
||||
var opts createOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "create [OPTIONS] CONTAINER CHECKPOINT",
|
||||
Short: "Create a checkpoint from a running container",
|
||||
Args: cli.ExactArgs(2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.container = args[0]
|
||||
opts.checkpoint = args[1]
|
||||
return runCreate(dockerCli, opts)
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
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")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runCreate(dockerCli command.Cli, opts createOptions) error {
|
||||
client := dockerCli.Client()
|
||||
|
||||
checkpointOpts := types.CheckpointCreateOptions{
|
||||
CheckpointID: opts.checkpoint,
|
||||
CheckpointDir: opts.checkpointDir,
|
||||
Exit: !opts.leaveRunning,
|
||||
}
|
||||
|
||||
err := client.CheckpointCreate(context.Background(), opts.container, checkpointOpts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintf(dockerCli.Out(), "%s\n", opts.checkpoint)
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
package checkpoint
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/pkg/errors"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
|
||||
func TestCheckpointCreateErrors(t *testing.T) {
|
||||
testCases := []struct {
|
||||
args []string
|
||||
checkpointCreateFunc func(container string, options types.CheckpointCreateOptions) error
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
args: []string{"too-few-arguments"},
|
||||
expectedError: "requires exactly 2 arguments",
|
||||
},
|
||||
{
|
||||
args: []string{"too", "many", "arguments"},
|
||||
expectedError: "requires exactly 2 arguments",
|
||||
},
|
||||
{
|
||||
args: []string{"foo", "bar"},
|
||||
checkpointCreateFunc: func(container string, options types.CheckpointCreateOptions) error {
|
||||
return errors.Errorf("error creating checkpoint for container foo")
|
||||
},
|
||||
expectedError: "error creating checkpoint for container foo",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
checkpointCreateFunc: tc.checkpointCreateFunc,
|
||||
})
|
||||
cmd := newCreateCommand(cli)
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.SetOut(ioutil.Discard)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckpointCreateWithOptions(t *testing.T) {
|
||||
var containerID, checkpointID, checkpointDir string
|
||||
var exit bool
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
checkpointCreateFunc: func(container string, options types.CheckpointCreateOptions) error {
|
||||
containerID = container
|
||||
checkpointID = options.CheckpointID
|
||||
checkpointDir = options.CheckpointDir
|
||||
exit = options.Exit
|
||||
return nil
|
||||
},
|
||||
})
|
||||
cmd := newCreateCommand(cli)
|
||||
checkpoint := "checkpoint-bar"
|
||||
cmd.SetArgs([]string{"container-foo", checkpoint})
|
||||
cmd.Flags().Set("leave-running", "true")
|
||||
cmd.Flags().Set("checkpoint-dir", "/dir/foo")
|
||||
assert.NilError(t, cmd.Execute())
|
||||
assert.Check(t, is.Equal("container-foo", containerID))
|
||||
assert.Check(t, is.Equal(checkpoint, checkpointID))
|
||||
assert.Check(t, is.Equal("/dir/foo", checkpointDir))
|
||||
assert.Check(t, is.Equal(false, exit))
|
||||
assert.Check(t, is.Equal(checkpoint, strings.TrimSpace(cli.OutBuffer().String())))
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
package checkpoint
|
||||
|
||||
import (
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/docker/docker/api/types"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultCheckpointFormat = "table {{.Name}}"
|
||||
|
||||
checkpointNameHeader = "CHECKPOINT NAME"
|
||||
)
|
||||
|
||||
// NewFormat returns a format for use with a checkpoint Context
|
||||
func NewFormat(source string) formatter.Format {
|
||||
switch source {
|
||||
case formatter.TableFormatKey:
|
||||
return defaultCheckpointFormat
|
||||
}
|
||||
return formatter.Format(source)
|
||||
}
|
||||
|
||||
// FormatWrite writes formatted checkpoints using the Context
|
||||
func FormatWrite(ctx formatter.Context, checkpoints []types.Checkpoint) error {
|
||||
render := func(format func(subContext formatter.SubContext) error) error {
|
||||
for _, checkpoint := range checkpoints {
|
||||
if err := format(&checkpointContext{c: checkpoint}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return ctx.Write(newCheckpointContext(), render)
|
||||
}
|
||||
|
||||
type checkpointContext struct {
|
||||
formatter.HeaderContext
|
||||
c types.Checkpoint
|
||||
}
|
||||
|
||||
func newCheckpointContext() *checkpointContext {
|
||||
cpCtx := checkpointContext{}
|
||||
cpCtx.Header = formatter.SubHeaderContext{
|
||||
"Name": checkpointNameHeader,
|
||||
}
|
||||
return &cpCtx
|
||||
}
|
||||
|
||||
func (c *checkpointContext) MarshalJSON() ([]byte, error) {
|
||||
return formatter.MarshalJSON(c)
|
||||
}
|
||||
|
||||
func (c *checkpointContext) Name() string {
|
||||
return c.c.Name
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
package checkpoint
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/docker/docker/api/types"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestCheckpointContextFormatWrite(t *testing.T) {
|
||||
cases := []struct {
|
||||
context formatter.Context
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
formatter.Context{Format: NewFormat(defaultCheckpointFormat)},
|
||||
`CHECKPOINT NAME
|
||||
checkpoint-1
|
||||
checkpoint-2
|
||||
checkpoint-3
|
||||
`,
|
||||
},
|
||||
{
|
||||
formatter.Context{Format: NewFormat("{{.Name}}")},
|
||||
`checkpoint-1
|
||||
checkpoint-2
|
||||
checkpoint-3
|
||||
`,
|
||||
},
|
||||
{
|
||||
formatter.Context{Format: NewFormat("{{.Name}}:")},
|
||||
`checkpoint-1:
|
||||
checkpoint-2:
|
||||
checkpoint-3:
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
checkpoints := []types.Checkpoint{
|
||||
{Name: "checkpoint-1"},
|
||||
{Name: "checkpoint-2"},
|
||||
{Name: "checkpoint-3"},
|
||||
}
|
||||
for _, testcase := range cases {
|
||||
out := bytes.NewBufferString("")
|
||||
testcase.context.Output = out
|
||||
err := FormatWrite(testcase.context, checkpoints)
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, out.String(), testcase.expected)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
package checkpoint
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type listOptions struct {
|
||||
checkpointDir string
|
||||
}
|
||||
|
||||
func newListCommand(dockerCli command.Cli) *cobra.Command {
|
||||
var opts listOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "ls [OPTIONS] CONTAINER",
|
||||
Aliases: []string{"list"},
|
||||
Short: "List checkpoints for a container",
|
||||
Args: cli.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runList(dockerCli, args[0], opts)
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.StringVarP(&opts.checkpointDir, "checkpoint-dir", "", "", "Use a custom checkpoint storage directory")
|
||||
|
||||
return cmd
|
||||
|
||||
}
|
||||
|
||||
func runList(dockerCli command.Cli, container string, opts listOptions) error {
|
||||
client := dockerCli.Client()
|
||||
|
||||
listOpts := types.CheckpointListOptions{
|
||||
CheckpointDir: opts.checkpointDir,
|
||||
}
|
||||
|
||||
checkpoints, err := client.CheckpointList(context.Background(), container, listOpts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cpCtx := formatter.Context{
|
||||
Output: dockerCli.Out(),
|
||||
Format: NewFormat(formatter.TableFormatKey),
|
||||
}
|
||||
return FormatWrite(cpCtx, checkpoints)
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
package checkpoint
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/pkg/errors"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
"gotest.tools/v3/golden"
|
||||
)
|
||||
|
||||
func TestCheckpointListErrors(t *testing.T) {
|
||||
testCases := []struct {
|
||||
args []string
|
||||
checkpointListFunc func(container string, options types.CheckpointListOptions) ([]types.Checkpoint, error)
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
args: []string{},
|
||||
expectedError: "requires exactly 1 argument",
|
||||
},
|
||||
{
|
||||
args: []string{"too", "many", "arguments"},
|
||||
expectedError: "requires exactly 1 argument",
|
||||
},
|
||||
{
|
||||
args: []string{"foo"},
|
||||
checkpointListFunc: func(container string, options types.CheckpointListOptions) ([]types.Checkpoint, error) {
|
||||
return []types.Checkpoint{}, errors.Errorf("error getting checkpoints for container foo")
|
||||
},
|
||||
expectedError: "error getting checkpoints for container foo",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
checkpointListFunc: tc.checkpointListFunc,
|
||||
})
|
||||
cmd := newListCommand(cli)
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.SetOut(ioutil.Discard)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckpointListWithOptions(t *testing.T) {
|
||||
var containerID, checkpointDir string
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
checkpointListFunc: func(container string, options types.CheckpointListOptions) ([]types.Checkpoint, error) {
|
||||
containerID = container
|
||||
checkpointDir = options.CheckpointDir
|
||||
return []types.Checkpoint{
|
||||
{Name: "checkpoint-foo"},
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
cmd := newListCommand(cli)
|
||||
cmd.SetArgs([]string{"container-foo"})
|
||||
cmd.Flags().Set("checkpoint-dir", "/dir/foo")
|
||||
assert.NilError(t, cmd.Execute())
|
||||
assert.Check(t, is.Equal("container-foo", containerID))
|
||||
assert.Check(t, is.Equal("/dir/foo", checkpointDir))
|
||||
golden.Assert(t, cli.OutBuffer().String(), "checkpoint-list-with-options.golden")
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
package checkpoint
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type removeOptions struct {
|
||||
checkpointDir string
|
||||
}
|
||||
|
||||
func newRemoveCommand(dockerCli command.Cli) *cobra.Command {
|
||||
var opts removeOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "rm [OPTIONS] CONTAINER CHECKPOINT",
|
||||
Aliases: []string{"remove"},
|
||||
Short: "Remove a checkpoint",
|
||||
Args: cli.ExactArgs(2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runRemove(dockerCli, args[0], args[1], opts)
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.StringVarP(&opts.checkpointDir, "checkpoint-dir", "", "", "Use a custom checkpoint storage directory")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runRemove(dockerCli command.Cli, container string, checkpoint string, opts removeOptions) error {
|
||||
client := dockerCli.Client()
|
||||
|
||||
removeOpts := types.CheckpointDeleteOptions{
|
||||
CheckpointID: checkpoint,
|
||||
CheckpointDir: opts.checkpointDir,
|
||||
}
|
||||
|
||||
return client.CheckpointDelete(context.Background(), container, removeOpts)
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
package checkpoint
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/pkg/errors"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
|
||||
func TestCheckpointRemoveErrors(t *testing.T) {
|
||||
testCases := []struct {
|
||||
args []string
|
||||
checkpointDeleteFunc func(container string, options types.CheckpointDeleteOptions) error
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
args: []string{"too-few-arguments"},
|
||||
expectedError: "requires exactly 2 arguments",
|
||||
},
|
||||
{
|
||||
args: []string{"too", "many", "arguments"},
|
||||
expectedError: "requires exactly 2 arguments",
|
||||
},
|
||||
{
|
||||
args: []string{"foo", "bar"},
|
||||
checkpointDeleteFunc: func(container string, options types.CheckpointDeleteOptions) error {
|
||||
return errors.Errorf("error deleting checkpoint")
|
||||
},
|
||||
expectedError: "error deleting checkpoint",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
checkpointDeleteFunc: tc.checkpointDeleteFunc,
|
||||
})
|
||||
cmd := newRemoveCommand(cli)
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.SetOut(ioutil.Discard)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckpointRemoveWithOptions(t *testing.T) {
|
||||
var containerID, checkpointID, checkpointDir string
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
checkpointDeleteFunc: func(container string, options types.CheckpointDeleteOptions) error {
|
||||
containerID = container
|
||||
checkpointID = options.CheckpointID
|
||||
checkpointDir = options.CheckpointDir
|
||||
return nil
|
||||
},
|
||||
})
|
||||
cmd := newRemoveCommand(cli)
|
||||
cmd.SetArgs([]string{"container-foo", "checkpoint-bar"})
|
||||
cmd.Flags().Set("checkpoint-dir", "/dir/foo")
|
||||
assert.NilError(t, cmd.Execute())
|
||||
assert.Check(t, is.Equal("container-foo", containerID))
|
||||
assert.Check(t, is.Equal("checkpoint-bar", checkpointID))
|
||||
assert.Check(t, is.Equal("/dir/foo", checkpointDir))
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
CHECKPOINT NAME
|
||||
checkpoint-foo
|
|
@ -0,0 +1,558 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/docker/cli/cli/config"
|
||||
cliconfig "github.com/docker/cli/cli/config"
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
dcontext "github.com/docker/cli/cli/context"
|
||||
"github.com/docker/cli/cli/context/docker"
|
||||
"github.com/docker/cli/cli/context/store"
|
||||
"github.com/docker/cli/cli/debug"
|
||||
cliflags "github.com/docker/cli/cli/flags"
|
||||
manifeststore "github.com/docker/cli/cli/manifest/store"
|
||||
registryclient "github.com/docker/cli/cli/registry/client"
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/docker/cli/cli/trust"
|
||||
"github.com/docker/cli/cli/version"
|
||||
dopts "github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api"
|
||||
"github.com/docker/docker/api/types"
|
||||
registrytypes "github.com/docker/docker/api/types/registry"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/docker/go-connections/tlsconfig"
|
||||
"github.com/moby/term"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/theupdateframework/notary"
|
||||
notaryclient "github.com/theupdateframework/notary/client"
|
||||
"github.com/theupdateframework/notary/passphrase"
|
||||
)
|
||||
|
||||
// Streams is an interface which exposes the standard input and output streams
|
||||
type Streams interface {
|
||||
In() *streams.In
|
||||
Out() *streams.Out
|
||||
Err() io.Writer
|
||||
}
|
||||
|
||||
// Cli represents the docker command line client.
|
||||
type Cli interface {
|
||||
Client() client.APIClient
|
||||
Out() *streams.Out
|
||||
Err() io.Writer
|
||||
In() *streams.In
|
||||
SetIn(in *streams.In)
|
||||
Apply(ops ...DockerCliOption) error
|
||||
ConfigFile() *configfile.ConfigFile
|
||||
ServerInfo() ServerInfo
|
||||
ClientInfo() ClientInfo
|
||||
NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (notaryclient.Repository, error)
|
||||
DefaultVersion() string
|
||||
ManifestStore() manifeststore.Store
|
||||
RegistryClient(bool) registryclient.RegistryClient
|
||||
ContentTrustEnabled() bool
|
||||
ContextStore() store.Store
|
||||
CurrentContext() string
|
||||
StackOrchestrator(flagValue string) (Orchestrator, error)
|
||||
DockerEndpoint() docker.Endpoint
|
||||
}
|
||||
|
||||
// DockerCli is an instance the docker command line client.
|
||||
// Instances of the client can be returned from NewDockerCli.
|
||||
type DockerCli struct {
|
||||
configFile *configfile.ConfigFile
|
||||
in *streams.In
|
||||
out *streams.Out
|
||||
err io.Writer
|
||||
client client.APIClient
|
||||
serverInfo ServerInfo
|
||||
clientInfo *ClientInfo
|
||||
contentTrust bool
|
||||
contextStore store.Store
|
||||
currentContext string
|
||||
dockerEndpoint docker.Endpoint
|
||||
contextStoreConfig store.Config
|
||||
}
|
||||
|
||||
// DefaultVersion returns api.defaultVersion or DOCKER_API_VERSION if specified.
|
||||
func (cli *DockerCli) DefaultVersion() string {
|
||||
return cli.ClientInfo().DefaultVersion
|
||||
}
|
||||
|
||||
// Client returns the APIClient
|
||||
func (cli *DockerCli) Client() client.APIClient {
|
||||
return cli.client
|
||||
}
|
||||
|
||||
// Out returns the writer used for stdout
|
||||
func (cli *DockerCli) Out() *streams.Out {
|
||||
return cli.out
|
||||
}
|
||||
|
||||
// Err returns the writer used for stderr
|
||||
func (cli *DockerCli) Err() io.Writer {
|
||||
return cli.err
|
||||
}
|
||||
|
||||
// SetIn sets the reader used for stdin
|
||||
func (cli *DockerCli) SetIn(in *streams.In) {
|
||||
cli.in = in
|
||||
}
|
||||
|
||||
// In returns the reader used for stdin
|
||||
func (cli *DockerCli) In() *streams.In {
|
||||
return cli.in
|
||||
}
|
||||
|
||||
// ShowHelp shows the command help.
|
||||
func ShowHelp(err io.Writer) func(*cobra.Command, []string) error {
|
||||
return func(cmd *cobra.Command, args []string) error {
|
||||
cmd.SetOut(err)
|
||||
cmd.HelpFunc()(cmd, args)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// ConfigFile returns the ConfigFile
|
||||
func (cli *DockerCli) ConfigFile() *configfile.ConfigFile {
|
||||
if cli.configFile == nil {
|
||||
cli.loadConfigFile()
|
||||
}
|
||||
return cli.configFile
|
||||
}
|
||||
|
||||
func (cli *DockerCli) loadConfigFile() {
|
||||
cli.configFile = cliconfig.LoadDefaultConfigFile(cli.err)
|
||||
}
|
||||
|
||||
// ServerInfo returns the server version details for the host this client is
|
||||
// connected to
|
||||
func (cli *DockerCli) ServerInfo() ServerInfo {
|
||||
return cli.serverInfo
|
||||
}
|
||||
|
||||
// ClientInfo returns the client details for the cli
|
||||
func (cli *DockerCli) ClientInfo() ClientInfo {
|
||||
if cli.clientInfo == nil {
|
||||
if err := cli.loadClientInfo(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
return *cli.clientInfo
|
||||
}
|
||||
|
||||
func (cli *DockerCli) loadClientInfo() error {
|
||||
var v string
|
||||
if cli.client != nil {
|
||||
v = cli.client.ClientVersion()
|
||||
} else {
|
||||
v = api.DefaultVersion
|
||||
}
|
||||
cli.clientInfo = &ClientInfo{
|
||||
DefaultVersion: v,
|
||||
HasExperimental: true,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ContentTrustEnabled returns whether content trust has been enabled by an
|
||||
// environment variable.
|
||||
func (cli *DockerCli) ContentTrustEnabled() bool {
|
||||
return cli.contentTrust
|
||||
}
|
||||
|
||||
// BuildKitEnabled returns whether buildkit is enabled either through a daemon setting
|
||||
// or otherwise the client-side DOCKER_BUILDKIT environment variable
|
||||
func BuildKitEnabled(si ServerInfo) (bool, error) {
|
||||
buildkitEnabled := si.BuildkitVersion == types.BuilderBuildKit
|
||||
if buildkitEnv := os.Getenv("DOCKER_BUILDKIT"); buildkitEnv != "" {
|
||||
var err error
|
||||
buildkitEnabled, err = strconv.ParseBool(buildkitEnv)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "DOCKER_BUILDKIT environment variable expects boolean value")
|
||||
}
|
||||
}
|
||||
return buildkitEnabled, nil
|
||||
}
|
||||
|
||||
// ManifestStore returns a store for local manifests
|
||||
func (cli *DockerCli) ManifestStore() manifeststore.Store {
|
||||
// TODO: support override default location from config file
|
||||
return manifeststore.NewStore(filepath.Join(config.Dir(), "manifests"))
|
||||
}
|
||||
|
||||
// RegistryClient returns a client for communicating with a Docker distribution
|
||||
// registry
|
||||
func (cli *DockerCli) RegistryClient(allowInsecure bool) registryclient.RegistryClient {
|
||||
resolver := func(ctx context.Context, index *registrytypes.IndexInfo) types.AuthConfig {
|
||||
return ResolveAuthConfig(ctx, cli, index)
|
||||
}
|
||||
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.
|
||||
func WithInitializeClient(makeClient func(dockerCli *DockerCli) (client.APIClient, error)) InitializeOpt {
|
||||
return func(dockerCli *DockerCli) error {
|
||||
var err error
|
||||
dockerCli.client, err = makeClient(dockerCli)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the dockerCli runs initialization that must happen after command
|
||||
// line flags are parsed.
|
||||
func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...InitializeOpt) error {
|
||||
var err error
|
||||
|
||||
for _, o := range ops {
|
||||
if err := o(cli); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
cliflags.SetLogLevel(opts.Common.LogLevel)
|
||||
|
||||
if opts.ConfigDir != "" {
|
||||
cliconfig.SetDir(opts.ConfigDir)
|
||||
}
|
||||
|
||||
if opts.Common.Debug {
|
||||
debug.Enable()
|
||||
}
|
||||
|
||||
cli.loadConfigFile()
|
||||
|
||||
baseContextStore := store.New(cliconfig.ContextStoreDir(), cli.contextStoreConfig)
|
||||
cli.contextStore = &ContextStoreWithDefault{
|
||||
Store: baseContextStore,
|
||||
Resolver: func() (*DefaultContext, error) {
|
||||
return ResolveDefaultContext(opts.Common, cli.ConfigFile(), cli.contextStoreConfig, cli.Err())
|
||||
},
|
||||
}
|
||||
cli.currentContext, err = resolveContextName(opts.Common, cli.configFile, cli.contextStore)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cli.dockerEndpoint, err = resolveDockerEndpoint(cli.contextStore, cli.currentContext)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to resolve docker endpoint")
|
||||
}
|
||||
|
||||
if cli.client == nil {
|
||||
cli.client, err = newAPIClientFromEndpoint(cli.dockerEndpoint, cli.configFile)
|
||||
if tlsconfig.IsErrEncryptedKey(err) {
|
||||
passRetriever := passphrase.PromptRetrieverWithInOut(cli.In(), cli.Out(), nil)
|
||||
newClient := func(password string) (client.APIClient, error) {
|
||||
cli.dockerEndpoint.TLSPassword = password //nolint: staticcheck // SA1019: cli.dockerEndpoint.TLSPassword is deprecated
|
||||
return newAPIClientFromEndpoint(cli.dockerEndpoint, cli.configFile)
|
||||
}
|
||||
cli.client, err = getClientWithPassword(passRetriever, newClient)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
cli.initializeFromClient()
|
||||
|
||||
if err := cli.loadClientInfo(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewAPIClientFromFlags creates a new APIClient from command line flags
|
||||
func NewAPIClientFromFlags(opts *cliflags.CommonOptions, configFile *configfile.ConfigFile) (client.APIClient, error) {
|
||||
storeConfig := DefaultContextStoreConfig()
|
||||
store := &ContextStoreWithDefault{
|
||||
Store: store.New(cliconfig.ContextStoreDir(), storeConfig),
|
||||
Resolver: func() (*DefaultContext, error) {
|
||||
return ResolveDefaultContext(opts, configFile, storeConfig, ioutil.Discard)
|
||||
},
|
||||
}
|
||||
contextName, err := resolveContextName(opts, configFile, store)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
endpoint, err := resolveDockerEndpoint(store, contextName)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "unable to resolve docker endpoint")
|
||||
}
|
||||
return newAPIClientFromEndpoint(endpoint, configFile)
|
||||
}
|
||||
|
||||
func newAPIClientFromEndpoint(ep docker.Endpoint, configFile *configfile.ConfigFile) (client.APIClient, error) {
|
||||
clientOpts, err := ep.ClientOpts()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
customHeaders := make(map[string]string, len(configFile.HTTPHeaders))
|
||||
for k, v := range configFile.HTTPHeaders {
|
||||
customHeaders[k] = v
|
||||
}
|
||||
customHeaders["User-Agent"] = UserAgent()
|
||||
clientOpts = append(clientOpts, client.WithHTTPHeaders(customHeaders))
|
||||
return client.NewClientWithOpts(clientOpts...)
|
||||
}
|
||||
|
||||
func resolveDockerEndpoint(s store.Reader, contextName string) (docker.Endpoint, error) {
|
||||
ctxMeta, err := s.GetMetadata(contextName)
|
||||
if err != nil {
|
||||
return docker.Endpoint{}, err
|
||||
}
|
||||
epMeta, err := docker.EndpointFromContext(ctxMeta)
|
||||
if err != nil {
|
||||
return docker.Endpoint{}, err
|
||||
}
|
||||
return docker.WithTLSData(s, contextName, epMeta)
|
||||
}
|
||||
|
||||
// Resolve the Docker endpoint for the default context (based on config, env vars and CLI flags)
|
||||
func resolveDefaultDockerEndpoint(opts *cliflags.CommonOptions) (docker.Endpoint, error) {
|
||||
host, err := getServerHost(opts.Hosts, opts.TLSOptions)
|
||||
if err != nil {
|
||||
return docker.Endpoint{}, err
|
||||
}
|
||||
|
||||
var (
|
||||
skipTLSVerify bool
|
||||
tlsData *dcontext.TLSData
|
||||
)
|
||||
|
||||
if opts.TLSOptions != nil {
|
||||
skipTLSVerify = opts.TLSOptions.InsecureSkipVerify
|
||||
tlsData, err = dcontext.TLSDataFromFiles(opts.TLSOptions.CAFile, opts.TLSOptions.CertFile, opts.TLSOptions.KeyFile)
|
||||
if err != nil {
|
||||
return docker.Endpoint{}, err
|
||||
}
|
||||
}
|
||||
|
||||
return docker.Endpoint{
|
||||
EndpointMeta: docker.EndpointMeta{
|
||||
Host: host,
|
||||
SkipTLSVerify: skipTLSVerify,
|
||||
},
|
||||
TLSData: tlsData,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (cli *DockerCli) initializeFromClient() {
|
||||
ctx := context.Background()
|
||||
if strings.HasPrefix(cli.DockerEndpoint().Host, "tcp://") {
|
||||
// @FIXME context.WithTimeout doesn't work with connhelper / ssh connections
|
||||
// time="2020-04-10T10:16:26Z" level=warning msg="commandConn.CloseWrite: commandconn: failed to wait: signal: killed"
|
||||
var cancel func()
|
||||
ctx, cancel = context.WithTimeout(ctx, 2*time.Second)
|
||||
defer cancel()
|
||||
}
|
||||
|
||||
ping, err := cli.client.Ping(ctx)
|
||||
if err != nil {
|
||||
// Default to true if we fail to connect to daemon
|
||||
cli.serverInfo = ServerInfo{HasExperimental: true}
|
||||
|
||||
if ping.APIVersion != "" {
|
||||
cli.client.NegotiateAPIVersionPing(ping)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
cli.serverInfo = ServerInfo{
|
||||
HasExperimental: ping.Experimental,
|
||||
OSType: ping.OSType,
|
||||
BuildkitVersion: ping.BuilderVersion,
|
||||
}
|
||||
cli.client.NegotiateAPIVersionPing(ping)
|
||||
}
|
||||
|
||||
func getClientWithPassword(passRetriever notary.PassRetriever, newClient func(password string) (client.APIClient, error)) (client.APIClient, error) {
|
||||
for attempts := 0; ; attempts++ {
|
||||
passwd, giveup, err := passRetriever("private", "encrypted TLS private", false, attempts)
|
||||
if giveup || err != nil {
|
||||
return nil, errors.Wrap(err, "private key is encrypted, but could not get passphrase")
|
||||
}
|
||||
|
||||
apiclient, err := newClient(passwd)
|
||||
if !tlsconfig.IsErrEncryptedKey(err) {
|
||||
return apiclient, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NotaryClient provides a Notary Repository to interact with signed metadata for an image
|
||||
func (cli *DockerCli) NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (notaryclient.Repository, error) {
|
||||
return trust.GetNotaryRepository(cli.In(), cli.Out(), UserAgent(), imgRefAndAuth.RepoInfo(), imgRefAndAuth.AuthConfig(), actions...)
|
||||
}
|
||||
|
||||
// ContextStore returns the ContextStore
|
||||
func (cli *DockerCli) ContextStore() store.Store {
|
||||
return cli.contextStore
|
||||
}
|
||||
|
||||
// CurrentContext returns the current context name
|
||||
func (cli *DockerCli) CurrentContext() string {
|
||||
return cli.currentContext
|
||||
}
|
||||
|
||||
// StackOrchestrator resolves which stack orchestrator is in use
|
||||
func (cli *DockerCli) StackOrchestrator(flagValue string) (Orchestrator, error) {
|
||||
currentContext := cli.CurrentContext()
|
||||
ctxRaw, err := cli.ContextStore().GetMetadata(currentContext)
|
||||
if store.IsErrContextDoesNotExist(err) {
|
||||
// case where the currentContext has been removed (CLI behavior is to fallback to using DOCKER_HOST based resolution)
|
||||
return GetStackOrchestrator(flagValue, "", cli.ConfigFile().StackOrchestrator, cli.Err())
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
ctxMeta, err := GetDockerContext(ctxRaw)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
ctxOrchestrator := string(ctxMeta.StackOrchestrator)
|
||||
return GetStackOrchestrator(flagValue, ctxOrchestrator, cli.ConfigFile().StackOrchestrator, cli.Err())
|
||||
}
|
||||
|
||||
// DockerEndpoint returns the current docker endpoint
|
||||
func (cli *DockerCli) DockerEndpoint() docker.Endpoint {
|
||||
return cli.dockerEndpoint
|
||||
}
|
||||
|
||||
// Apply all the operation on the cli
|
||||
func (cli *DockerCli) Apply(ops ...DockerCliOption) error {
|
||||
for _, op := range ops {
|
||||
if err := op(cli); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ServerInfo stores details about the supported features and platform of the
|
||||
// server
|
||||
type ServerInfo struct {
|
||||
HasExperimental bool
|
||||
OSType string
|
||||
BuildkitVersion types.BuilderVersion
|
||||
}
|
||||
|
||||
// ClientInfo stores details about the supported features of the client
|
||||
type ClientInfo struct {
|
||||
// Deprecated: experimental CLI features always enabled. This field is kept
|
||||
// for backward-compatibility, and is always "true".
|
||||
HasExperimental bool
|
||||
DefaultVersion string
|
||||
}
|
||||
|
||||
// NewDockerCli returns a DockerCli instance with all operators applied on it.
|
||||
// It applies by default the standard streams, and the content trust from
|
||||
// environment.
|
||||
func NewDockerCli(ops ...DockerCliOption) (*DockerCli, error) {
|
||||
cli := &DockerCli{}
|
||||
defaultOps := []DockerCliOption{
|
||||
WithContentTrustFromEnv(),
|
||||
}
|
||||
cli.contextStoreConfig = DefaultContextStoreConfig()
|
||||
ops = append(defaultOps, ops...)
|
||||
if err := cli.Apply(ops...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if cli.out == nil || cli.in == nil || cli.err == nil {
|
||||
stdin, stdout, stderr := term.StdStreams()
|
||||
if cli.in == nil {
|
||||
cli.in = streams.NewIn(stdin)
|
||||
}
|
||||
if cli.out == nil {
|
||||
cli.out = streams.NewOut(stdout)
|
||||
}
|
||||
if cli.err == nil {
|
||||
cli.err = stderr
|
||||
}
|
||||
}
|
||||
return cli, nil
|
||||
}
|
||||
|
||||
func getServerHost(hosts []string, tlsOptions *tlsconfig.Options) (string, error) {
|
||||
var host string
|
||||
switch len(hosts) {
|
||||
case 0:
|
||||
host = os.Getenv("DOCKER_HOST")
|
||||
case 1:
|
||||
host = hosts[0]
|
||||
default:
|
||||
return "", errors.New("Please specify only one -H")
|
||||
}
|
||||
|
||||
return dopts.ParseHost(tlsOptions != nil, host)
|
||||
}
|
||||
|
||||
// UserAgent returns the user agent string used for making API requests
|
||||
func UserAgent() string {
|
||||
return "Docker-Client/" + version.Version + " (" + runtime.GOOS + ")"
|
||||
}
|
||||
|
||||
// resolveContextName resolves the current context name with the following rules:
|
||||
// - setting both --context and --host flags is ambiguous
|
||||
// - if --context is set, use this value
|
||||
// - if --host flag or DOCKER_HOST is set, fallbacks to use the same logic as before context-store was added
|
||||
// for backward compatibility with existing scripts
|
||||
// - if DOCKER_CONTEXT is set, use this value
|
||||
// - if Config file has a globally set "CurrentContext", use this value
|
||||
// - fallbacks to default HOST, uses TLS config from flags/env vars
|
||||
func resolveContextName(opts *cliflags.CommonOptions, config *configfile.ConfigFile, contextstore store.Reader) (string, error) {
|
||||
if opts.Context != "" && len(opts.Hosts) > 0 {
|
||||
return "", errors.New("Conflicting options: either specify --host or --context, not both")
|
||||
}
|
||||
if opts.Context != "" {
|
||||
return opts.Context, nil
|
||||
}
|
||||
if len(opts.Hosts) > 0 {
|
||||
return DefaultContextName, nil
|
||||
}
|
||||
if _, present := os.LookupEnv("DOCKER_HOST"); present {
|
||||
return DefaultContextName, nil
|
||||
}
|
||||
if ctxName, ok := os.LookupEnv("DOCKER_CONTEXT"); ok {
|
||||
return ctxName, nil
|
||||
}
|
||||
if config != nil && config.CurrentContext != "" {
|
||||
_, err := contextstore.GetMetadata(config.CurrentContext)
|
||||
if store.IsErrContextDoesNotExist(err) {
|
||||
return "", errors.Errorf("Current context %q is not found on the file system, please check your config file at %s", config.CurrentContext, config.Filename)
|
||||
}
|
||||
return config.CurrentContext, err
|
||||
}
|
||||
return DefaultContextName, nil
|
||||
}
|
||||
|
||||
var defaultStoreEndpoints = []store.NamedTypeGetter{
|
||||
store.EndpointTypeGetter(docker.DockerEndpoint, func() interface{} { return &docker.EndpointMeta{} }),
|
||||
}
|
||||
|
||||
// RegisterDefaultStoreEndpoints registers a new named endpoint
|
||||
// metadata type with the default context store config, so that
|
||||
// endpoint will be supported by stores using the config returned by
|
||||
// DefaultContextStoreConfig.
|
||||
func RegisterDefaultStoreEndpoints(ep ...store.NamedTypeGetter) {
|
||||
defaultStoreEndpoints = append(defaultStoreEndpoints, ep...)
|
||||
}
|
||||
|
||||
// DefaultContextStoreConfig returns a new store.Config with the default set of endpoints configured.
|
||||
func DefaultContextStoreConfig() store.Config {
|
||||
return store.NewConfig(
|
||||
func() interface{} { return &DockerContext{} },
|
||||
defaultStoreEndpoints...,
|
||||
)
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/docker/cli/cli/context/docker"
|
||||
"github.com/docker/cli/cli/context/store"
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/moby/term"
|
||||
)
|
||||
|
||||
// DockerCliOption applies a modification on a DockerCli.
|
||||
type DockerCliOption func(cli *DockerCli) error
|
||||
|
||||
// WithStandardStreams sets a cli in, out and err streams with the standard streams.
|
||||
func WithStandardStreams() DockerCliOption {
|
||||
return func(cli *DockerCli) error {
|
||||
// Set terminal emulation based on platform as required.
|
||||
stdin, stdout, stderr := term.StdStreams()
|
||||
cli.in = streams.NewIn(stdin)
|
||||
cli.out = streams.NewOut(stdout)
|
||||
cli.err = stderr
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithCombinedStreams uses the same stream for the output and error streams.
|
||||
func WithCombinedStreams(combined io.Writer) DockerCliOption {
|
||||
return func(cli *DockerCli) error {
|
||||
cli.out = streams.NewOut(combined)
|
||||
cli.err = combined
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithInputStream sets a cli input stream.
|
||||
func WithInputStream(in io.ReadCloser) DockerCliOption {
|
||||
return func(cli *DockerCli) error {
|
||||
cli.in = streams.NewIn(in)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithOutputStream sets a cli output stream.
|
||||
func WithOutputStream(out io.Writer) DockerCliOption {
|
||||
return func(cli *DockerCli) error {
|
||||
cli.out = streams.NewOut(out)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithErrorStream sets a cli error stream.
|
||||
func WithErrorStream(err io.Writer) DockerCliOption {
|
||||
return func(cli *DockerCli) error {
|
||||
cli.err = err
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithContentTrustFromEnv enables content trust on a cli from environment variable DOCKER_CONTENT_TRUST value.
|
||||
func WithContentTrustFromEnv() DockerCliOption {
|
||||
return func(cli *DockerCli) error {
|
||||
cli.contentTrust = false
|
||||
if e := os.Getenv("DOCKER_CONTENT_TRUST"); e != "" {
|
||||
if t, err := strconv.ParseBool(e); t || err != nil {
|
||||
// treat any other value as true
|
||||
cli.contentTrust = true
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithContentTrust enables content trust on a cli.
|
||||
func WithContentTrust(enabled bool) DockerCliOption {
|
||||
return func(cli *DockerCli) error {
|
||||
cli.contentTrust = enabled
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithContextEndpointType add support for an additional typed endpoint in the context store
|
||||
// Plugins should use this to store additional endpoints configuration in the context store
|
||||
func WithContextEndpointType(endpointName string, endpointType store.TypeGetter) DockerCliOption {
|
||||
return func(cli *DockerCli) error {
|
||||
switch endpointName {
|
||||
case docker.DockerEndpoint:
|
||||
return fmt.Errorf("cannot change %q endpoint type", endpointName)
|
||||
}
|
||||
cli.contextStoreConfig.SetEndpoint(endpointName, endpointType)
|
||||
return nil
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func contentTrustEnabled(t *testing.T) bool {
|
||||
var cli DockerCli
|
||||
assert.NilError(t, WithContentTrustFromEnv()(&cli))
|
||||
return cli.contentTrust
|
||||
}
|
||||
|
||||
// NB: Do not t.Parallel() this test -- it messes with the process environment.
|
||||
func TestWithContentTrustFromEnv(t *testing.T) {
|
||||
envvar := "DOCKER_CONTENT_TRUST"
|
||||
if orig, ok := os.LookupEnv(envvar); ok {
|
||||
defer func() {
|
||||
os.Setenv(envvar, orig)
|
||||
}()
|
||||
} else {
|
||||
defer func() {
|
||||
os.Unsetenv(envvar)
|
||||
}()
|
||||
}
|
||||
|
||||
os.Setenv(envvar, "true")
|
||||
assert.Assert(t, contentTrustEnabled(t))
|
||||
os.Setenv(envvar, "false")
|
||||
assert.Assert(t, !contentTrustEnabled(t))
|
||||
os.Setenv(envvar, "invalid")
|
||||
assert.Assert(t, contentTrustEnabled(t))
|
||||
os.Unsetenv(envvar)
|
||||
assert.Assert(t, !contentTrustEnabled(t))
|
||||
}
|
|
@ -0,0 +1,306 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
cliconfig "github.com/docker/cli/cli/config"
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
"github.com/docker/cli/cli/flags"
|
||||
"github.com/docker/docker/api"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/pkg/errors"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
"gotest.tools/v3/env"
|
||||
"gotest.tools/v3/fs"
|
||||
)
|
||||
|
||||
func TestNewAPIClientFromFlags(t *testing.T) {
|
||||
host := "unix://path"
|
||||
if runtime.GOOS == "windows" {
|
||||
host = "npipe://./"
|
||||
}
|
||||
opts := &flags.CommonOptions{Hosts: []string{host}}
|
||||
configFile := &configfile.ConfigFile{
|
||||
HTTPHeaders: map[string]string{
|
||||
"My-Header": "Custom-Value",
|
||||
},
|
||||
}
|
||||
apiclient, err := NewAPIClientFromFlags(opts, configFile)
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, is.Equal(host, apiclient.DaemonHost()))
|
||||
|
||||
expectedHeaders := map[string]string{
|
||||
"My-Header": "Custom-Value",
|
||||
"User-Agent": UserAgent(),
|
||||
}
|
||||
assert.Check(t, is.DeepEqual(expectedHeaders, apiclient.(*client.Client).CustomHTTPHeaders()))
|
||||
assert.Check(t, is.Equal(api.DefaultVersion, apiclient.ClientVersion()))
|
||||
assert.DeepEqual(t, configFile.HTTPHeaders, map[string]string{"My-Header": "Custom-Value"})
|
||||
}
|
||||
|
||||
func TestNewAPIClientFromFlagsForDefaultSchema(t *testing.T) {
|
||||
host := ":2375"
|
||||
opts := &flags.CommonOptions{Hosts: []string{host}}
|
||||
configFile := &configfile.ConfigFile{
|
||||
HTTPHeaders: map[string]string{
|
||||
"My-Header": "Custom-Value",
|
||||
},
|
||||
}
|
||||
apiclient, err := NewAPIClientFromFlags(opts, configFile)
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, is.Equal("tcp://localhost"+host, apiclient.DaemonHost()))
|
||||
|
||||
expectedHeaders := map[string]string{
|
||||
"My-Header": "Custom-Value",
|
||||
"User-Agent": UserAgent(),
|
||||
}
|
||||
assert.Check(t, is.DeepEqual(expectedHeaders, apiclient.(*client.Client).CustomHTTPHeaders()))
|
||||
assert.Check(t, is.Equal(api.DefaultVersion, apiclient.ClientVersion()))
|
||||
}
|
||||
|
||||
func TestNewAPIClientFromFlagsWithAPIVersionFromEnv(t *testing.T) {
|
||||
customVersion := "v3.3.3"
|
||||
defer env.Patch(t, "DOCKER_API_VERSION", customVersion)()
|
||||
defer env.Patch(t, "DOCKER_HOST", ":2375")()
|
||||
|
||||
opts := &flags.CommonOptions{}
|
||||
configFile := &configfile.ConfigFile{}
|
||||
apiclient, err := NewAPIClientFromFlags(opts, configFile)
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, is.Equal(customVersion, apiclient.ClientVersion()))
|
||||
}
|
||||
|
||||
type fakeClient struct {
|
||||
client.Client
|
||||
pingFunc func() (types.Ping, error)
|
||||
version string
|
||||
negotiated bool
|
||||
}
|
||||
|
||||
func (c *fakeClient) Ping(_ context.Context) (types.Ping, error) {
|
||||
return c.pingFunc()
|
||||
}
|
||||
|
||||
func (c *fakeClient) ClientVersion() string {
|
||||
return c.version
|
||||
}
|
||||
|
||||
func (c *fakeClient) NegotiateAPIVersionPing(types.Ping) {
|
||||
c.negotiated = true
|
||||
}
|
||||
|
||||
func TestInitializeFromClient(t *testing.T) {
|
||||
defaultVersion := "v1.55"
|
||||
|
||||
var testcases = []struct {
|
||||
doc string
|
||||
pingFunc func() (types.Ping, error)
|
||||
expectedServer ServerInfo
|
||||
negotiated bool
|
||||
}{
|
||||
{
|
||||
doc: "successful ping",
|
||||
pingFunc: func() (types.Ping, error) {
|
||||
return types.Ping{Experimental: true, OSType: "linux", APIVersion: "v1.30"}, nil
|
||||
},
|
||||
expectedServer: ServerInfo{HasExperimental: true, OSType: "linux"},
|
||||
negotiated: true,
|
||||
},
|
||||
{
|
||||
doc: "failed ping, no API version",
|
||||
pingFunc: func() (types.Ping, error) {
|
||||
return types.Ping{}, errors.New("failed")
|
||||
},
|
||||
expectedServer: ServerInfo{HasExperimental: true},
|
||||
},
|
||||
{
|
||||
doc: "failed ping, with API version",
|
||||
pingFunc: func() (types.Ping, error) {
|
||||
return types.Ping{APIVersion: "v1.33"}, errors.New("failed")
|
||||
},
|
||||
expectedServer: ServerInfo{HasExperimental: true},
|
||||
negotiated: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testcase := range testcases {
|
||||
testcase := testcase
|
||||
t.Run(testcase.doc, func(t *testing.T) {
|
||||
apiclient := &fakeClient{
|
||||
pingFunc: testcase.pingFunc,
|
||||
version: defaultVersion,
|
||||
}
|
||||
|
||||
cli := &DockerCli{client: apiclient}
|
||||
cli.initializeFromClient()
|
||||
assert.Check(t, is.DeepEqual(testcase.expectedServer, cli.serverInfo))
|
||||
assert.Check(t, is.Equal(testcase.negotiated, apiclient.negotiated))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// The CLI no longer disables/hides experimental CLI features, however, we need
|
||||
// to verify that existing configuration files do not break
|
||||
func TestExperimentalCLI(t *testing.T) {
|
||||
defaultVersion := "v1.55"
|
||||
|
||||
var testcases = []struct {
|
||||
doc string
|
||||
configfile string
|
||||
}{
|
||||
{
|
||||
doc: "default",
|
||||
configfile: `{}`,
|
||||
},
|
||||
{
|
||||
doc: "experimental",
|
||||
configfile: `{
|
||||
"experimental": "enabled"
|
||||
}`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testcase := range testcases {
|
||||
testcase := testcase
|
||||
t.Run(testcase.doc, func(t *testing.T) {
|
||||
dir := fs.NewDir(t, testcase.doc, fs.WithFile("config.json", testcase.configfile))
|
||||
defer dir.Remove()
|
||||
apiclient := &fakeClient{
|
||||
version: defaultVersion,
|
||||
pingFunc: func() (types.Ping, error) {
|
||||
return types.Ping{Experimental: true, OSType: "linux", APIVersion: defaultVersion}, nil
|
||||
},
|
||||
}
|
||||
|
||||
cli := &DockerCli{client: apiclient, err: os.Stderr}
|
||||
cliconfig.SetDir(dir.Path())
|
||||
err := cli.Initialize(flags.NewClientOptions())
|
||||
assert.NilError(t, err)
|
||||
// For backward-compatibility, HasExperimental will always be "true"
|
||||
assert.Check(t, is.Equal(true, cli.ClientInfo().HasExperimental))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetClientWithPassword(t *testing.T) {
|
||||
expected := "password"
|
||||
|
||||
var testcases = []struct {
|
||||
doc string
|
||||
password string
|
||||
retrieverErr error
|
||||
retrieverGiveup bool
|
||||
newClientErr error
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
doc: "successful connect",
|
||||
password: expected,
|
||||
},
|
||||
{
|
||||
doc: "password retriever exhausted",
|
||||
retrieverGiveup: true,
|
||||
retrieverErr: errors.New("failed"),
|
||||
expectedErr: "private key is encrypted, but could not get passphrase",
|
||||
},
|
||||
{
|
||||
doc: "password retriever error",
|
||||
retrieverErr: errors.New("failed"),
|
||||
expectedErr: "failed",
|
||||
},
|
||||
{
|
||||
doc: "newClient error",
|
||||
newClientErr: errors.New("failed to connect"),
|
||||
expectedErr: "failed to connect",
|
||||
},
|
||||
}
|
||||
|
||||
for _, testcase := range testcases {
|
||||
testcase := testcase
|
||||
t.Run(testcase.doc, func(t *testing.T) {
|
||||
passRetriever := func(_, _ string, _ bool, attempts int) (passphrase string, giveup bool, err error) {
|
||||
// Always return an invalid pass first to test iteration
|
||||
switch attempts {
|
||||
case 0:
|
||||
return "something else", false, nil
|
||||
default:
|
||||
return testcase.password, testcase.retrieverGiveup, testcase.retrieverErr
|
||||
}
|
||||
}
|
||||
|
||||
newClient := func(currentPassword string) (client.APIClient, error) {
|
||||
if testcase.newClientErr != nil {
|
||||
return nil, testcase.newClientErr
|
||||
}
|
||||
if currentPassword == expected {
|
||||
return &client.Client{}, nil
|
||||
}
|
||||
return &client.Client{}, x509.IncorrectPasswordError
|
||||
}
|
||||
|
||||
_, err := getClientWithPassword(passRetriever, newClient)
|
||||
if testcase.expectedErr != "" {
|
||||
assert.ErrorContains(t, err, testcase.expectedErr)
|
||||
return
|
||||
}
|
||||
|
||||
assert.NilError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewDockerCliAndOperators(t *testing.T) {
|
||||
// Test default operations and also overriding default ones
|
||||
cli, err := NewDockerCli(
|
||||
WithContentTrust(true),
|
||||
)
|
||||
assert.NilError(t, err)
|
||||
// Check streams are initialized
|
||||
assert.Check(t, cli.In() != nil)
|
||||
assert.Check(t, cli.Out() != nil)
|
||||
assert.Check(t, cli.Err() != nil)
|
||||
assert.Equal(t, cli.ContentTrustEnabled(), true)
|
||||
|
||||
// Apply can modify a dockerCli after construction
|
||||
inbuf := bytes.NewBuffer([]byte("input"))
|
||||
outbuf := bytes.NewBuffer(nil)
|
||||
errbuf := bytes.NewBuffer(nil)
|
||||
err = cli.Apply(
|
||||
WithInputStream(ioutil.NopCloser(inbuf)),
|
||||
WithOutputStream(outbuf),
|
||||
WithErrorStream(errbuf),
|
||||
)
|
||||
assert.NilError(t, err)
|
||||
// Check input stream
|
||||
inputStream, err := ioutil.ReadAll(cli.In())
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, string(inputStream), "input")
|
||||
// Check output stream
|
||||
fmt.Fprintf(cli.Out(), "output")
|
||||
outputStream, err := ioutil.ReadAll(outbuf)
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, string(outputStream), "output")
|
||||
// Check error stream
|
||||
fmt.Fprintf(cli.Err(), "error")
|
||||
errStream, err := ioutil.ReadAll(errbuf)
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, string(errStream), "error")
|
||||
}
|
||||
|
||||
func TestInitializeShouldAlwaysCreateTheContextStore(t *testing.T) {
|
||||
cli, err := NewDockerCli()
|
||||
assert.NilError(t, err)
|
||||
assert.NilError(t, cli.Initialize(flags.NewClientOptions(), WithInitializeClient(func(cli *DockerCli) (client.APIClient, error) {
|
||||
return client.NewClientWithOpts()
|
||||
})))
|
||||
assert.Check(t, cli.ContextStore() != nil)
|
||||
}
|
|
@ -0,0 +1,139 @@
|
|||
package commands
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/builder"
|
||||
"github.com/docker/cli/cli/command/checkpoint"
|
||||
"github.com/docker/cli/cli/command/config"
|
||||
"github.com/docker/cli/cli/command/container"
|
||||
"github.com/docker/cli/cli/command/context"
|
||||
"github.com/docker/cli/cli/command/image"
|
||||
"github.com/docker/cli/cli/command/manifest"
|
||||
"github.com/docker/cli/cli/command/network"
|
||||
"github.com/docker/cli/cli/command/node"
|
||||
"github.com/docker/cli/cli/command/plugin"
|
||||
"github.com/docker/cli/cli/command/registry"
|
||||
"github.com/docker/cli/cli/command/secret"
|
||||
"github.com/docker/cli/cli/command/service"
|
||||
"github.com/docker/cli/cli/command/stack"
|
||||
"github.com/docker/cli/cli/command/swarm"
|
||||
"github.com/docker/cli/cli/command/system"
|
||||
"github.com/docker/cli/cli/command/trust"
|
||||
"github.com/docker/cli/cli/command/volume"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// AddCommands adds all the commands from cli/command to the root command
|
||||
func AddCommands(cmd *cobra.Command, dockerCli command.Cli) {
|
||||
cmd.AddCommand(
|
||||
// checkpoint
|
||||
checkpoint.NewCheckpointCommand(dockerCli),
|
||||
|
||||
// config
|
||||
config.NewConfigCommand(dockerCli),
|
||||
|
||||
// container
|
||||
container.NewContainerCommand(dockerCli),
|
||||
container.NewRunCommand(dockerCli),
|
||||
|
||||
// image
|
||||
image.NewImageCommand(dockerCli),
|
||||
image.NewBuildCommand(dockerCli),
|
||||
|
||||
// builder
|
||||
builder.NewBuilderCommand(dockerCli),
|
||||
|
||||
// manifest
|
||||
manifest.NewManifestCommand(dockerCli),
|
||||
|
||||
// network
|
||||
network.NewNetworkCommand(dockerCli),
|
||||
|
||||
// node
|
||||
node.NewNodeCommand(dockerCli),
|
||||
|
||||
// plugin
|
||||
plugin.NewPluginCommand(dockerCli),
|
||||
|
||||
// registry
|
||||
registry.NewLoginCommand(dockerCli),
|
||||
registry.NewLogoutCommand(dockerCli),
|
||||
registry.NewSearchCommand(dockerCli),
|
||||
|
||||
// secret
|
||||
secret.NewSecretCommand(dockerCli),
|
||||
|
||||
// service
|
||||
service.NewServiceCommand(dockerCli),
|
||||
|
||||
// system
|
||||
system.NewSystemCommand(dockerCli),
|
||||
system.NewVersionCommand(dockerCli),
|
||||
|
||||
// stack
|
||||
stack.NewStackCommand(dockerCli),
|
||||
|
||||
// swarm
|
||||
swarm.NewSwarmCommand(dockerCli),
|
||||
|
||||
// trust
|
||||
trust.NewTrustCommand(dockerCli),
|
||||
|
||||
// volume
|
||||
volume.NewVolumeCommand(dockerCli),
|
||||
|
||||
// context
|
||||
context.NewContextCommand(dockerCli),
|
||||
|
||||
// legacy commands may be hidden
|
||||
hide(system.NewEventsCommand(dockerCli)),
|
||||
hide(system.NewInfoCommand(dockerCli)),
|
||||
hide(system.NewInspectCommand(dockerCli)),
|
||||
hide(container.NewAttachCommand(dockerCli)),
|
||||
hide(container.NewCommitCommand(dockerCli)),
|
||||
hide(container.NewCopyCommand(dockerCli)),
|
||||
hide(container.NewCreateCommand(dockerCli)),
|
||||
hide(container.NewDiffCommand(dockerCli)),
|
||||
hide(container.NewExecCommand(dockerCli)),
|
||||
hide(container.NewExportCommand(dockerCli)),
|
||||
hide(container.NewKillCommand(dockerCli)),
|
||||
hide(container.NewLogsCommand(dockerCli)),
|
||||
hide(container.NewPauseCommand(dockerCli)),
|
||||
hide(container.NewPortCommand(dockerCli)),
|
||||
hide(container.NewPsCommand(dockerCli)),
|
||||
hide(container.NewRenameCommand(dockerCli)),
|
||||
hide(container.NewRestartCommand(dockerCli)),
|
||||
hide(container.NewRmCommand(dockerCli)),
|
||||
hide(container.NewStartCommand(dockerCli)),
|
||||
hide(container.NewStatsCommand(dockerCli)),
|
||||
hide(container.NewStopCommand(dockerCli)),
|
||||
hide(container.NewTopCommand(dockerCli)),
|
||||
hide(container.NewUnpauseCommand(dockerCli)),
|
||||
hide(container.NewUpdateCommand(dockerCli)),
|
||||
hide(container.NewWaitCommand(dockerCli)),
|
||||
hide(image.NewHistoryCommand(dockerCli)),
|
||||
hide(image.NewImagesCommand(dockerCli)),
|
||||
hide(image.NewImportCommand(dockerCli)),
|
||||
hide(image.NewLoadCommand(dockerCli)),
|
||||
hide(image.NewPullCommand(dockerCli)),
|
||||
hide(image.NewPushCommand(dockerCli)),
|
||||
hide(image.NewRemoveCommand(dockerCli)),
|
||||
hide(image.NewSaveCommand(dockerCli)),
|
||||
hide(image.NewTagCommand(dockerCli)),
|
||||
)
|
||||
}
|
||||
|
||||
func hide(cmd *cobra.Command) *cobra.Command {
|
||||
// If the environment variable with name "DOCKER_HIDE_LEGACY_COMMANDS" is not empty,
|
||||
// these legacy commands (such as `docker ps`, `docker exec`, etc)
|
||||
// will not be shown in output console.
|
||||
if os.Getenv("DOCKER_HIDE_LEGACY_COMMANDS") == "" {
|
||||
return cmd
|
||||
}
|
||||
cmdCopy := *cmd
|
||||
cmdCopy.Hidden = true
|
||||
cmdCopy.Aliases = []string{}
|
||||
return &cmdCopy
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/client"
|
||||
)
|
||||
|
||||
type fakeClient struct {
|
||||
client.Client
|
||||
configCreateFunc func(swarm.ConfigSpec) (types.ConfigCreateResponse, error)
|
||||
configInspectFunc func(string) (swarm.Config, []byte, error)
|
||||
configListFunc func(types.ConfigListOptions) ([]swarm.Config, error)
|
||||
configRemoveFunc func(string) error
|
||||
}
|
||||
|
||||
func (c *fakeClient) ConfigCreate(ctx context.Context, spec swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
|
||||
if c.configCreateFunc != nil {
|
||||
return c.configCreateFunc(spec)
|
||||
}
|
||||
return types.ConfigCreateResponse{}, nil
|
||||
}
|
||||
|
||||
func (c *fakeClient) ConfigInspectWithRaw(ctx context.Context, id string) (swarm.Config, []byte, error) {
|
||||
if c.configInspectFunc != nil {
|
||||
return c.configInspectFunc(id)
|
||||
}
|
||||
return swarm.Config{}, nil, nil
|
||||
}
|
||||
|
||||
func (c *fakeClient) ConfigList(ctx context.Context, options types.ConfigListOptions) ([]swarm.Config, error) {
|
||||
if c.configListFunc != nil {
|
||||
return c.configListFunc(options)
|
||||
}
|
||||
return []swarm.Config{}, nil
|
||||
}
|
||||
|
||||
func (c *fakeClient) ConfigRemove(ctx context.Context, name string) error {
|
||||
if c.configRemoveFunc != nil {
|
||||
return c.configRemoveFunc(name)
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
)
|
||||
|
||||
// NewConfigCommand returns a cobra command for `config` subcommands
|
||||
func NewConfigCommand(dockerCli command.Cli) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "config",
|
||||
Short: "Manage Docker configs",
|
||||
Args: cli.NoArgs,
|
||||
RunE: command.ShowHelp(dockerCli.Err()),
|
||||
Annotations: map[string]string{
|
||||
"version": "1.30",
|
||||
"swarm": "",
|
||||
},
|
||||
}
|
||||
cmd.AddCommand(
|
||||
newConfigListCommand(dockerCli),
|
||||
newConfigCreateCommand(dockerCli),
|
||||
newConfigInspectCommand(dockerCli),
|
||||
newConfigRemoveCommand(dockerCli),
|
||||
)
|
||||
return cmd
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/pkg/system"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// CreateOptions specifies some options that are used when creating a config.
|
||||
type CreateOptions struct {
|
||||
Name string
|
||||
TemplateDriver string
|
||||
File string
|
||||
Labels opts.ListOpts
|
||||
}
|
||||
|
||||
func newConfigCreateCommand(dockerCli command.Cli) *cobra.Command {
|
||||
createOpts := CreateOptions{
|
||||
Labels: opts.NewListOpts(opts.ValidateLabel),
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "create [OPTIONS] CONFIG file|-",
|
||||
Short: "Create a config from a file or STDIN",
|
||||
Args: cli.ExactArgs(2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
createOpts.Name = args[0]
|
||||
createOpts.File = args[1]
|
||||
return RunConfigCreate(dockerCli, createOpts)
|
||||
},
|
||||
}
|
||||
flags := cmd.Flags()
|
||||
flags.VarP(&createOpts.Labels, "label", "l", "Config labels")
|
||||
flags.StringVar(&createOpts.TemplateDriver, "template-driver", "", "Template driver")
|
||||
flags.SetAnnotation("template-driver", "version", []string{"1.37"})
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// RunConfigCreate creates a config with the given options.
|
||||
func RunConfigCreate(dockerCli command.Cli, options CreateOptions) error {
|
||||
client := dockerCli.Client()
|
||||
ctx := context.Background()
|
||||
|
||||
var in io.Reader = dockerCli.In()
|
||||
if options.File != "-" {
|
||||
file, err := system.OpenSequential(options.File)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
in = file
|
||||
defer file.Close()
|
||||
}
|
||||
|
||||
configData, err := ioutil.ReadAll(in)
|
||||
if err != nil {
|
||||
return errors.Errorf("Error reading content from %q: %v", options.File, err)
|
||||
}
|
||||
|
||||
spec := swarm.ConfigSpec{
|
||||
Annotations: swarm.Annotations{
|
||||
Name: options.Name,
|
||||
Labels: opts.ConvertKVStringsToMap(options.Labels.GetAll()),
|
||||
},
|
||||
Data: configData,
|
||||
}
|
||||
if options.TemplateDriver != "" {
|
||||
spec.Templating = &swarm.Driver{
|
||||
Name: options.TemplateDriver,
|
||||
}
|
||||
}
|
||||
r, err := client.ConfigCreate(ctx, spec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintln(dockerCli.Out(), r.ID)
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,143 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/pkg/errors"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
"gotest.tools/v3/golden"
|
||||
)
|
||||
|
||||
const configDataFile = "config-create-with-name.golden"
|
||||
|
||||
func TestConfigCreateErrors(t *testing.T) {
|
||||
testCases := []struct {
|
||||
args []string
|
||||
configCreateFunc func(swarm.ConfigSpec) (types.ConfigCreateResponse, error)
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
args: []string{"too_few"},
|
||||
expectedError: "requires exactly 2 arguments",
|
||||
},
|
||||
{args: []string{"too", "many", "arguments"},
|
||||
expectedError: "requires exactly 2 arguments",
|
||||
},
|
||||
{
|
||||
args: []string{"name", filepath.Join("testdata", configDataFile)},
|
||||
configCreateFunc: func(configSpec swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
|
||||
return types.ConfigCreateResponse{}, errors.Errorf("error creating config")
|
||||
},
|
||||
expectedError: "error creating config",
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
cmd := newConfigCreateCommand(
|
||||
test.NewFakeCli(&fakeClient{
|
||||
configCreateFunc: tc.configCreateFunc,
|
||||
}),
|
||||
)
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.SetOut(ioutil.Discard)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigCreateWithName(t *testing.T) {
|
||||
name := "foo"
|
||||
var actual []byte
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
configCreateFunc: func(spec swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
|
||||
if spec.Name != name {
|
||||
return types.ConfigCreateResponse{}, errors.Errorf("expected name %q, got %q", name, spec.Name)
|
||||
}
|
||||
|
||||
actual = spec.Data
|
||||
|
||||
return types.ConfigCreateResponse{
|
||||
ID: "ID-" + spec.Name,
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
|
||||
cmd := newConfigCreateCommand(cli)
|
||||
cmd.SetArgs([]string{name, filepath.Join("testdata", configDataFile)})
|
||||
assert.NilError(t, cmd.Execute())
|
||||
golden.Assert(t, string(actual), configDataFile)
|
||||
assert.Check(t, is.Equal("ID-"+name, strings.TrimSpace(cli.OutBuffer().String())))
|
||||
}
|
||||
|
||||
func TestConfigCreateWithLabels(t *testing.T) {
|
||||
expectedLabels := map[string]string{
|
||||
"lbl1": "Label-foo",
|
||||
"lbl2": "Label-bar",
|
||||
}
|
||||
name := "foo"
|
||||
|
||||
data, err := ioutil.ReadFile(filepath.Join("testdata", configDataFile))
|
||||
assert.NilError(t, err)
|
||||
|
||||
expected := swarm.ConfigSpec{
|
||||
Annotations: swarm.Annotations{
|
||||
Name: name,
|
||||
Labels: expectedLabels,
|
||||
},
|
||||
Data: data,
|
||||
}
|
||||
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
configCreateFunc: func(spec swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
|
||||
if !reflect.DeepEqual(spec, expected) {
|
||||
return types.ConfigCreateResponse{}, errors.Errorf("expected %+v, got %+v", expected, spec)
|
||||
}
|
||||
|
||||
return types.ConfigCreateResponse{
|
||||
ID: "ID-" + spec.Name,
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
|
||||
cmd := newConfigCreateCommand(cli)
|
||||
cmd.SetArgs([]string{name, filepath.Join("testdata", configDataFile)})
|
||||
cmd.Flags().Set("label", "lbl1=Label-foo")
|
||||
cmd.Flags().Set("label", "lbl2=Label-bar")
|
||||
assert.NilError(t, cmd.Execute())
|
||||
assert.Check(t, is.Equal("ID-"+name, strings.TrimSpace(cli.OutBuffer().String())))
|
||||
}
|
||||
|
||||
func TestConfigCreateWithTemplatingDriver(t *testing.T) {
|
||||
expectedDriver := &swarm.Driver{
|
||||
Name: "template-driver",
|
||||
}
|
||||
name := "foo"
|
||||
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
configCreateFunc: func(spec swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
|
||||
if spec.Name != name {
|
||||
return types.ConfigCreateResponse{}, errors.Errorf("expected name %q, got %q", name, spec.Name)
|
||||
}
|
||||
|
||||
if spec.Templating.Name != expectedDriver.Name {
|
||||
return types.ConfigCreateResponse{}, errors.Errorf("expected driver %v, got %v", expectedDriver, spec.Labels)
|
||||
}
|
||||
|
||||
return types.ConfigCreateResponse{
|
||||
ID: "ID-" + spec.Name,
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
|
||||
cmd := newConfigCreateCommand(cli)
|
||||
cmd.SetArgs([]string{name, filepath.Join("testdata", configDataFile)})
|
||||
cmd.Flags().Set("template-driver", expectedDriver.Name)
|
||||
assert.NilError(t, cmd.Execute())
|
||||
assert.Check(t, is.Equal("ID-"+name, strings.TrimSpace(cli.OutBuffer().String())))
|
||||
}
|
|
@ -0,0 +1,172 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/docker/cli/cli/command/inspect"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
units "github.com/docker/go-units"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultConfigTableFormat = "table {{.ID}}\t{{.Name}}\t{{.CreatedAt}}\t{{.UpdatedAt}}"
|
||||
configIDHeader = "ID"
|
||||
configCreatedHeader = "CREATED"
|
||||
configUpdatedHeader = "UPDATED"
|
||||
configInspectPrettyTemplate formatter.Format = `ID: {{.ID}}
|
||||
Name: {{.Name}}
|
||||
{{- if .Labels }}
|
||||
Labels:
|
||||
{{- range $k, $v := .Labels }}
|
||||
- {{ $k }}{{if $v }}={{ $v }}{{ end }}
|
||||
{{- end }}{{ end }}
|
||||
Created at: {{.CreatedAt}}
|
||||
Updated at: {{.UpdatedAt}}
|
||||
Data:
|
||||
{{.Data}}`
|
||||
)
|
||||
|
||||
// NewFormat returns a Format for rendering using a config Context
|
||||
func NewFormat(source string, quiet bool) formatter.Format {
|
||||
switch source {
|
||||
case formatter.PrettyFormatKey:
|
||||
return configInspectPrettyTemplate
|
||||
case formatter.TableFormatKey:
|
||||
if quiet {
|
||||
return formatter.DefaultQuietFormat
|
||||
}
|
||||
return defaultConfigTableFormat
|
||||
}
|
||||
return formatter.Format(source)
|
||||
}
|
||||
|
||||
// FormatWrite writes the context
|
||||
func FormatWrite(ctx formatter.Context, configs []swarm.Config) error {
|
||||
render := func(format func(subContext formatter.SubContext) error) error {
|
||||
for _, config := range configs {
|
||||
configCtx := &configContext{c: config}
|
||||
if err := format(configCtx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return ctx.Write(newConfigContext(), render)
|
||||
}
|
||||
|
||||
func newConfigContext() *configContext {
|
||||
cCtx := &configContext{}
|
||||
|
||||
cCtx.Header = formatter.SubHeaderContext{
|
||||
"ID": configIDHeader,
|
||||
"Name": formatter.NameHeader,
|
||||
"CreatedAt": configCreatedHeader,
|
||||
"UpdatedAt": configUpdatedHeader,
|
||||
"Labels": formatter.LabelsHeader,
|
||||
}
|
||||
return cCtx
|
||||
}
|
||||
|
||||
type configContext struct {
|
||||
formatter.HeaderContext
|
||||
c swarm.Config
|
||||
}
|
||||
|
||||
func (c *configContext) MarshalJSON() ([]byte, error) {
|
||||
return formatter.MarshalJSON(c)
|
||||
}
|
||||
|
||||
func (c *configContext) ID() string {
|
||||
return c.c.ID
|
||||
}
|
||||
|
||||
func (c *configContext) Name() string {
|
||||
return c.c.Spec.Annotations.Name
|
||||
}
|
||||
|
||||
func (c *configContext) CreatedAt() string {
|
||||
return units.HumanDuration(time.Now().UTC().Sub(c.c.Meta.CreatedAt)) + " ago"
|
||||
}
|
||||
|
||||
func (c *configContext) UpdatedAt() string {
|
||||
return units.HumanDuration(time.Now().UTC().Sub(c.c.Meta.UpdatedAt)) + " ago"
|
||||
}
|
||||
|
||||
func (c *configContext) Labels() string {
|
||||
mapLabels := c.c.Spec.Annotations.Labels
|
||||
if mapLabels == nil {
|
||||
return ""
|
||||
}
|
||||
var joinLabels []string
|
||||
for k, v := range mapLabels {
|
||||
joinLabels = append(joinLabels, fmt.Sprintf("%s=%s", k, v))
|
||||
}
|
||||
return strings.Join(joinLabels, ",")
|
||||
}
|
||||
|
||||
func (c *configContext) Label(name string) string {
|
||||
if c.c.Spec.Annotations.Labels == nil {
|
||||
return ""
|
||||
}
|
||||
return c.c.Spec.Annotations.Labels[name]
|
||||
}
|
||||
|
||||
// InspectFormatWrite renders the context for a list of configs
|
||||
func InspectFormatWrite(ctx formatter.Context, refs []string, getRef inspect.GetRefFunc) error {
|
||||
if ctx.Format != configInspectPrettyTemplate {
|
||||
return inspect.Inspect(ctx.Output, refs, string(ctx.Format), getRef)
|
||||
}
|
||||
render := func(format func(subContext formatter.SubContext) error) error {
|
||||
for _, ref := range refs {
|
||||
configI, _, err := getRef(ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
config, ok := configI.(swarm.Config)
|
||||
if !ok {
|
||||
return fmt.Errorf("got wrong object to inspect :%v", ok)
|
||||
}
|
||||
if err := format(&configInspectContext{Config: config}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return ctx.Write(&configInspectContext{}, render)
|
||||
}
|
||||
|
||||
type configInspectContext struct {
|
||||
swarm.Config
|
||||
formatter.SubContext
|
||||
}
|
||||
|
||||
func (ctx *configInspectContext) ID() string {
|
||||
return ctx.Config.ID
|
||||
}
|
||||
|
||||
func (ctx *configInspectContext) Name() string {
|
||||
return ctx.Config.Spec.Name
|
||||
}
|
||||
|
||||
func (ctx *configInspectContext) Labels() map[string]string {
|
||||
return ctx.Config.Spec.Labels
|
||||
}
|
||||
|
||||
func (ctx *configInspectContext) CreatedAt() string {
|
||||
return command.PrettyPrint(ctx.Config.CreatedAt)
|
||||
}
|
||||
|
||||
func (ctx *configInspectContext) UpdatedAt() string {
|
||||
return command.PrettyPrint(ctx.Config.UpdatedAt)
|
||||
}
|
||||
|
||||
func (ctx *configInspectContext) Data() string {
|
||||
if ctx.Config.Spec.Data == nil {
|
||||
return ""
|
||||
}
|
||||
return string(ctx.Config.Spec.Data)
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestConfigContextFormatWrite(t *testing.T) {
|
||||
// Check default output format (verbose and non-verbose mode) for table headers
|
||||
cases := []struct {
|
||||
context formatter.Context
|
||||
expected string
|
||||
}{
|
||||
// Errors
|
||||
{
|
||||
formatter.Context{Format: "{{InvalidFunction}}"},
|
||||
`Template parsing error: template: :1: function "InvalidFunction" not defined
|
||||
`,
|
||||
},
|
||||
{
|
||||
formatter.Context{Format: "{{nil}}"},
|
||||
`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command
|
||||
`,
|
||||
},
|
||||
// Table format
|
||||
{formatter.Context{Format: NewFormat("table", false)},
|
||||
`ID NAME CREATED UPDATED
|
||||
1 passwords Less than a second ago Less than a second ago
|
||||
2 id_rsa Less than a second ago Less than a second ago
|
||||
`},
|
||||
{formatter.Context{Format: NewFormat("table {{.Name}}", true)},
|
||||
`NAME
|
||||
passwords
|
||||
id_rsa
|
||||
`},
|
||||
{formatter.Context{Format: NewFormat("{{.ID}}-{{.Name}}", false)},
|
||||
`1-passwords
|
||||
2-id_rsa
|
||||
`},
|
||||
}
|
||||
|
||||
configs := []swarm.Config{
|
||||
{ID: "1",
|
||||
Meta: swarm.Meta{CreatedAt: time.Now(), UpdatedAt: time.Now()},
|
||||
Spec: swarm.ConfigSpec{Annotations: swarm.Annotations{Name: "passwords"}}},
|
||||
{ID: "2",
|
||||
Meta: swarm.Meta{CreatedAt: time.Now(), UpdatedAt: time.Now()},
|
||||
Spec: swarm.ConfigSpec{Annotations: swarm.Annotations{Name: "id_rsa"}}},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(string(tc.context.Format), func(t *testing.T) {
|
||||
var out bytes.Buffer
|
||||
tc.context.Output = &out
|
||||
if err := FormatWrite(tc.context, configs); err != nil {
|
||||
assert.ErrorContains(t, err, tc.expected)
|
||||
} else {
|
||||
assert.Equal(t, out.String(), tc.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// InspectOptions contains options for the docker config inspect command.
|
||||
type InspectOptions struct {
|
||||
Names []string
|
||||
Format string
|
||||
Pretty bool
|
||||
}
|
||||
|
||||
func newConfigInspectCommand(dockerCli command.Cli) *cobra.Command {
|
||||
opts := InspectOptions{}
|
||||
cmd := &cobra.Command{
|
||||
Use: "inspect [OPTIONS] CONFIG [CONFIG...]",
|
||||
Short: "Display detailed information on one or more configs",
|
||||
Args: cli.RequiresMinArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.Names = args
|
||||
return RunConfigInspect(dockerCli, opts)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(&opts.Format, "format", "f", "", "Format the output using the given Go template")
|
||||
cmd.Flags().BoolVar(&opts.Pretty, "pretty", false, "Print the information in a human friendly format")
|
||||
return cmd
|
||||
}
|
||||
|
||||
// RunConfigInspect inspects the given Swarm config.
|
||||
func RunConfigInspect(dockerCli command.Cli, opts InspectOptions) error {
|
||||
client := dockerCli.Client()
|
||||
ctx := context.Background()
|
||||
|
||||
if opts.Pretty {
|
||||
opts.Format = "pretty"
|
||||
}
|
||||
|
||||
getRef := func(id string) (interface{}, []byte, error) {
|
||||
return client.ConfigInspectWithRaw(ctx, id)
|
||||
}
|
||||
f := opts.Format
|
||||
|
||||
// check if the user is trying to apply a template to the pretty format, which
|
||||
// is not supported
|
||||
if strings.HasPrefix(f, "pretty") && f != "pretty" {
|
||||
return fmt.Errorf("Cannot supply extra formatting options to the pretty template")
|
||||
}
|
||||
|
||||
configCtx := formatter.Context{
|
||||
Output: dockerCli.Out(),
|
||||
Format: NewFormat(f, false),
|
||||
}
|
||||
|
||||
if err := InspectFormatWrite(configCtx, opts.Names, getRef); err != nil {
|
||||
return cli.StatusError{StatusCode: 1, Status: err.Error()}
|
||||
}
|
||||
return nil
|
||||
|
||||
}
|
|
@ -0,0 +1,171 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"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/docker/api/types/swarm"
|
||||
"github.com/pkg/errors"
|
||||
"gotest.tools/v3/assert"
|
||||
"gotest.tools/v3/golden"
|
||||
)
|
||||
|
||||
func TestConfigInspectErrors(t *testing.T) {
|
||||
testCases := []struct {
|
||||
args []string
|
||||
flags map[string]string
|
||||
configInspectFunc func(configID string) (swarm.Config, []byte, error)
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
expectedError: "requires at least 1 argument",
|
||||
},
|
||||
{
|
||||
args: []string{"foo"},
|
||||
configInspectFunc: func(configID string) (swarm.Config, []byte, error) {
|
||||
return swarm.Config{}, nil, errors.Errorf("error while inspecting the config")
|
||||
},
|
||||
expectedError: "error while inspecting the config",
|
||||
},
|
||||
{
|
||||
args: []string{"foo"},
|
||||
flags: map[string]string{
|
||||
"format": "{{invalid format}}",
|
||||
},
|
||||
expectedError: "Template parsing error",
|
||||
},
|
||||
{
|
||||
args: []string{"foo", "bar"},
|
||||
configInspectFunc: func(configID string) (swarm.Config, []byte, error) {
|
||||
if configID == "foo" {
|
||||
return *Config(ConfigName("foo")), nil, nil
|
||||
}
|
||||
return swarm.Config{}, nil, errors.Errorf("error while inspecting the config")
|
||||
},
|
||||
expectedError: "error while inspecting the config",
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
cmd := newConfigInspectCommand(
|
||||
test.NewFakeCli(&fakeClient{
|
||||
configInspectFunc: tc.configInspectFunc,
|
||||
}),
|
||||
)
|
||||
cmd.SetArgs(tc.args)
|
||||
for key, value := range tc.flags {
|
||||
cmd.Flags().Set(key, value)
|
||||
}
|
||||
cmd.SetOut(ioutil.Discard)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigInspectWithoutFormat(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
args []string
|
||||
configInspectFunc func(configID string) (swarm.Config, []byte, error)
|
||||
}{
|
||||
{
|
||||
name: "single-config",
|
||||
args: []string{"foo"},
|
||||
configInspectFunc: func(name string) (swarm.Config, []byte, error) {
|
||||
if name != "foo" {
|
||||
return swarm.Config{}, nil, errors.Errorf("Invalid name, expected %s, got %s", "foo", name)
|
||||
}
|
||||
return *Config(ConfigID("ID-foo"), ConfigName("foo")), nil, nil
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple-configs-with-labels",
|
||||
args: []string{"foo", "bar"},
|
||||
configInspectFunc: func(name string) (swarm.Config, []byte, error) {
|
||||
return *Config(ConfigID("ID-"+name), ConfigName(name), ConfigLabels(map[string]string{
|
||||
"label1": "label-foo",
|
||||
})), nil, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
cli := test.NewFakeCli(&fakeClient{configInspectFunc: tc.configInspectFunc})
|
||||
cmd := newConfigInspectCommand(cli)
|
||||
cmd.SetArgs(tc.args)
|
||||
assert.NilError(t, cmd.Execute())
|
||||
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("config-inspect-without-format.%s.golden", tc.name))
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigInspectWithFormat(t *testing.T) {
|
||||
configInspectFunc := func(name string) (swarm.Config, []byte, error) {
|
||||
return *Config(ConfigName("foo"), ConfigLabels(map[string]string{
|
||||
"label1": "label-foo",
|
||||
})), nil, nil
|
||||
}
|
||||
testCases := []struct {
|
||||
name string
|
||||
format string
|
||||
args []string
|
||||
configInspectFunc func(name string) (swarm.Config, []byte, error)
|
||||
}{
|
||||
{
|
||||
name: "simple-template",
|
||||
format: "{{.Spec.Name}}",
|
||||
args: []string{"foo"},
|
||||
configInspectFunc: configInspectFunc,
|
||||
},
|
||||
{
|
||||
name: "json-template",
|
||||
format: "{{json .Spec.Labels}}",
|
||||
args: []string{"foo"},
|
||||
configInspectFunc: configInspectFunc,
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
configInspectFunc: tc.configInspectFunc,
|
||||
})
|
||||
cmd := newConfigInspectCommand(cli)
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.Flags().Set("format", tc.format)
|
||||
assert.NilError(t, cmd.Execute())
|
||||
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("config-inspect-with-format.%s.golden", tc.name))
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigInspectPretty(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
configInspectFunc func(string) (swarm.Config, []byte, error)
|
||||
}{
|
||||
{
|
||||
name: "simple",
|
||||
configInspectFunc: func(id string) (swarm.Config, []byte, error) {
|
||||
return *Config(
|
||||
ConfigLabels(map[string]string{
|
||||
"lbl1": "value1",
|
||||
}),
|
||||
ConfigID("configID"),
|
||||
ConfigName("configName"),
|
||||
ConfigCreatedAt(time.Time{}),
|
||||
ConfigUpdatedAt(time.Time{}),
|
||||
ConfigData([]byte("payload here")),
|
||||
), []byte{}, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
configInspectFunc: tc.configInspectFunc,
|
||||
})
|
||||
cmd := newConfigInspectCommand(cli)
|
||||
|
||||
cmd.SetArgs([]string{"configID"})
|
||||
cmd.Flags().Set("pretty", "true")
|
||||
assert.NilError(t, cmd.Execute())
|
||||
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("config-inspect-pretty.%s.golden", tc.name))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/fvbommel/sortorder"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// ListOptions contains options for the docker config ls command.
|
||||
type ListOptions struct {
|
||||
Quiet bool
|
||||
Format string
|
||||
Filter opts.FilterOpt
|
||||
}
|
||||
|
||||
func newConfigListCommand(dockerCli command.Cli) *cobra.Command {
|
||||
listOpts := ListOptions{Filter: opts.NewFilterOpt()}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "ls [OPTIONS]",
|
||||
Aliases: []string{"list"},
|
||||
Short: "List configs",
|
||||
Args: cli.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return RunConfigList(dockerCli, listOpts)
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVarP(&listOpts.Quiet, "quiet", "q", false, "Only display IDs")
|
||||
flags.StringVarP(&listOpts.Format, "format", "", "", "Pretty-print configs using a Go template")
|
||||
flags.VarP(&listOpts.Filter, "filter", "f", "Filter output based on conditions provided")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// RunConfigList lists Swarm configs.
|
||||
func RunConfigList(dockerCli command.Cli, options ListOptions) error {
|
||||
client := dockerCli.Client()
|
||||
ctx := context.Background()
|
||||
|
||||
configs, err := client.ConfigList(ctx, types.ConfigListOptions{Filters: options.Filter.Value()})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
format := options.Format
|
||||
if len(format) == 0 {
|
||||
if len(dockerCli.ConfigFile().ConfigFormat) > 0 && !options.Quiet {
|
||||
format = dockerCli.ConfigFile().ConfigFormat
|
||||
} else {
|
||||
format = formatter.TableFormatKey
|
||||
}
|
||||
}
|
||||
|
||||
sort.Slice(configs, func(i, j int) bool {
|
||||
return sortorder.NaturalLess(configs[i].Spec.Name, configs[j].Spec.Name)
|
||||
})
|
||||
|
||||
configCtx := formatter.Context{
|
||||
Output: dockerCli.Out(),
|
||||
Format: NewFormat(format, options.Quiet),
|
||||
}
|
||||
return FormatWrite(configCtx, configs)
|
||||
}
|
|
@ -0,0 +1,157 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
"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/docker/api/types"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/pkg/errors"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
"gotest.tools/v3/golden"
|
||||
)
|
||||
|
||||
func TestConfigListErrors(t *testing.T) {
|
||||
testCases := []struct {
|
||||
args []string
|
||||
configListFunc func(types.ConfigListOptions) ([]swarm.Config, error)
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
args: []string{"foo"},
|
||||
expectedError: "accepts no argument",
|
||||
},
|
||||
{
|
||||
configListFunc: func(options types.ConfigListOptions) ([]swarm.Config, error) {
|
||||
return []swarm.Config{}, errors.Errorf("error listing configs")
|
||||
},
|
||||
expectedError: "error listing configs",
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
cmd := newConfigListCommand(
|
||||
test.NewFakeCli(&fakeClient{
|
||||
configListFunc: tc.configListFunc,
|
||||
}),
|
||||
)
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.SetOut(ioutil.Discard)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigList(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
configListFunc: func(options types.ConfigListOptions) ([]swarm.Config, error) {
|
||||
return []swarm.Config{
|
||||
*Config(ConfigID("ID-1-foo"),
|
||||
ConfigName("1-foo"),
|
||||
ConfigVersion(swarm.Version{Index: 10}),
|
||||
ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
|
||||
ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
|
||||
),
|
||||
*Config(ConfigID("ID-10-foo"),
|
||||
ConfigName("10-foo"),
|
||||
ConfigVersion(swarm.Version{Index: 11}),
|
||||
ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
|
||||
ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
|
||||
),
|
||||
*Config(ConfigID("ID-2-foo"),
|
||||
ConfigName("2-foo"),
|
||||
ConfigVersion(swarm.Version{Index: 11}),
|
||||
ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
|
||||
ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
|
||||
),
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
cmd := newConfigListCommand(cli)
|
||||
assert.NilError(t, cmd.Execute())
|
||||
golden.Assert(t, cli.OutBuffer().String(), "config-list-sort.golden")
|
||||
}
|
||||
|
||||
func TestConfigListWithQuietOption(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
configListFunc: func(options types.ConfigListOptions) ([]swarm.Config, error) {
|
||||
return []swarm.Config{
|
||||
*Config(ConfigID("ID-foo"), ConfigName("foo")),
|
||||
*Config(ConfigID("ID-bar"), ConfigName("bar"), ConfigLabels(map[string]string{
|
||||
"label": "label-bar",
|
||||
})),
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
cmd := newConfigListCommand(cli)
|
||||
cmd.Flags().Set("quiet", "true")
|
||||
assert.NilError(t, cmd.Execute())
|
||||
golden.Assert(t, cli.OutBuffer().String(), "config-list-with-quiet-option.golden")
|
||||
}
|
||||
|
||||
func TestConfigListWithConfigFormat(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
configListFunc: func(options types.ConfigListOptions) ([]swarm.Config, error) {
|
||||
return []swarm.Config{
|
||||
*Config(ConfigID("ID-foo"), ConfigName("foo")),
|
||||
*Config(ConfigID("ID-bar"), ConfigName("bar"), ConfigLabels(map[string]string{
|
||||
"label": "label-bar",
|
||||
})),
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
cli.SetConfigFile(&configfile.ConfigFile{
|
||||
ConfigFormat: "{{ .Name }} {{ .Labels }}",
|
||||
})
|
||||
cmd := newConfigListCommand(cli)
|
||||
assert.NilError(t, cmd.Execute())
|
||||
golden.Assert(t, cli.OutBuffer().String(), "config-list-with-config-format.golden")
|
||||
}
|
||||
|
||||
func TestConfigListWithFormat(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
configListFunc: func(options types.ConfigListOptions) ([]swarm.Config, error) {
|
||||
return []swarm.Config{
|
||||
*Config(ConfigID("ID-foo"), ConfigName("foo")),
|
||||
*Config(ConfigID("ID-bar"), ConfigName("bar"), ConfigLabels(map[string]string{
|
||||
"label": "label-bar",
|
||||
})),
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
cmd := newConfigListCommand(cli)
|
||||
cmd.Flags().Set("format", "{{ .Name }} {{ .Labels }}")
|
||||
assert.NilError(t, cmd.Execute())
|
||||
golden.Assert(t, cli.OutBuffer().String(), "config-list-with-format.golden")
|
||||
}
|
||||
|
||||
func TestConfigListWithFilter(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
configListFunc: func(options types.ConfigListOptions) ([]swarm.Config, error) {
|
||||
assert.Check(t, is.Equal("foo", options.Filters.Get("name")[0]))
|
||||
assert.Check(t, is.Equal("lbl1=Label-bar", options.Filters.Get("label")[0]))
|
||||
return []swarm.Config{
|
||||
*Config(ConfigID("ID-foo"),
|
||||
ConfigName("foo"),
|
||||
ConfigVersion(swarm.Version{Index: 10}),
|
||||
ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
|
||||
ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
|
||||
),
|
||||
*Config(ConfigID("ID-bar"),
|
||||
ConfigName("bar"),
|
||||
ConfigVersion(swarm.Version{Index: 11}),
|
||||
ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
|
||||
ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
|
||||
),
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
cmd := newConfigListCommand(cli)
|
||||
cmd.Flags().Set("filter", "name=foo")
|
||||
cmd.Flags().Set("filter", "label=lbl1=Label-bar")
|
||||
assert.NilError(t, cmd.Execute())
|
||||
golden.Assert(t, cli.OutBuffer().String(), "config-list-with-filter.golden")
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// RemoveOptions contains options for the docker config rm command.
|
||||
type RemoveOptions struct {
|
||||
Names []string
|
||||
}
|
||||
|
||||
func newConfigRemoveCommand(dockerCli command.Cli) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "rm CONFIG [CONFIG...]",
|
||||
Aliases: []string{"remove"},
|
||||
Short: "Remove one or more configs",
|
||||
Args: cli.RequiresMinArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts := RemoveOptions{
|
||||
Names: args,
|
||||
}
|
||||
return RunConfigRemove(dockerCli, opts)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// RunConfigRemove removes the given Swarm configs.
|
||||
func RunConfigRemove(dockerCli command.Cli, opts RemoveOptions) error {
|
||||
client := dockerCli.Client()
|
||||
ctx := context.Background()
|
||||
|
||||
var errs []string
|
||||
|
||||
for _, name := range opts.Names {
|
||||
if err := client.ConfigRemove(ctx, name); err != nil {
|
||||
errs = append(errs, err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.Fprintln(dockerCli.Out(), name)
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return errors.Errorf("%s", strings.Join(errs, "\n"))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/pkg/errors"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
|
||||
func TestConfigRemoveErrors(t *testing.T) {
|
||||
testCases := []struct {
|
||||
args []string
|
||||
configRemoveFunc func(string) error
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
args: []string{},
|
||||
expectedError: "requires at least 1 argument.",
|
||||
},
|
||||
{
|
||||
args: []string{"foo"},
|
||||
configRemoveFunc: func(name string) error {
|
||||
return errors.Errorf("error removing config")
|
||||
},
|
||||
expectedError: "error removing config",
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
cmd := newConfigRemoveCommand(
|
||||
test.NewFakeCli(&fakeClient{
|
||||
configRemoveFunc: tc.configRemoveFunc,
|
||||
}),
|
||||
)
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.SetOut(ioutil.Discard)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigRemoveWithName(t *testing.T) {
|
||||
names := []string{"foo", "bar"}
|
||||
var removedConfigs []string
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
configRemoveFunc: func(name string) error {
|
||||
removedConfigs = append(removedConfigs, name)
|
||||
return nil
|
||||
},
|
||||
})
|
||||
cmd := newConfigRemoveCommand(cli)
|
||||
cmd.SetArgs(names)
|
||||
assert.NilError(t, cmd.Execute())
|
||||
assert.Check(t, is.DeepEqual(names, strings.Split(strings.TrimSpace(cli.OutBuffer().String()), "\n")))
|
||||
assert.Check(t, is.DeepEqual(names, removedConfigs))
|
||||
}
|
||||
|
||||
func TestConfigRemoveContinueAfterError(t *testing.T) {
|
||||
names := []string{"foo", "bar"}
|
||||
var removedConfigs []string
|
||||
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
configRemoveFunc: func(name string) error {
|
||||
removedConfigs = append(removedConfigs, name)
|
||||
if name == "foo" {
|
||||
return errors.Errorf("error removing config: %s", name)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
})
|
||||
|
||||
cmd := newConfigRemoveCommand(cli)
|
||||
cmd.SetArgs(names)
|
||||
cmd.SetOut(ioutil.Discard)
|
||||
assert.Error(t, cmd.Execute(), "error removing config: foo")
|
||||
assert.Check(t, is.DeepEqual(names, removedConfigs))
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
config_foo_bar
|
|
@ -0,0 +1,8 @@
|
|||
ID: configID
|
||||
Name: configName
|
||||
Labels:
|
||||
- lbl1=value1
|
||||
Created at: 0001-01-01 00:00:00 +0000 utc
|
||||
Updated at: 0001-01-01 00:00:00 +0000 utc
|
||||
Data:
|
||||
payload here
|
1
cli/cli/command/config/testdata/config-inspect-with-format.json-template.golden
vendored
Normal file
1
cli/cli/command/config/testdata/config-inspect-with-format.json-template.golden
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
{"label1":"label-foo"}
|
1
cli/cli/command/config/testdata/config-inspect-with-format.simple-template.golden
vendored
Normal file
1
cli/cli/command/config/testdata/config-inspect-with-format.simple-template.golden
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
foo
|
|
@ -0,0 +1,26 @@
|
|||
[
|
||||
{
|
||||
"ID": "ID-foo",
|
||||
"Version": {},
|
||||
"CreatedAt": "0001-01-01T00:00:00Z",
|
||||
"UpdatedAt": "0001-01-01T00:00:00Z",
|
||||
"Spec": {
|
||||
"Name": "foo",
|
||||
"Labels": {
|
||||
"label1": "label-foo"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "ID-bar",
|
||||
"Version": {},
|
||||
"CreatedAt": "0001-01-01T00:00:00Z",
|
||||
"UpdatedAt": "0001-01-01T00:00:00Z",
|
||||
"Spec": {
|
||||
"Name": "bar",
|
||||
"Labels": {
|
||||
"label1": "label-foo"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
12
cli/cli/command/config/testdata/config-inspect-without-format.single-config.golden
vendored
Normal file
12
cli/cli/command/config/testdata/config-inspect-without-format.single-config.golden
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
[
|
||||
{
|
||||
"ID": "ID-foo",
|
||||
"Version": {},
|
||||
"CreatedAt": "0001-01-01T00:00:00Z",
|
||||
"UpdatedAt": "0001-01-01T00:00:00Z",
|
||||
"Spec": {
|
||||
"Name": "foo",
|
||||
"Labels": null
|
||||
}
|
||||
}
|
||||
]
|
|
@ -0,0 +1,4 @@
|
|||
ID NAME CREATED UPDATED
|
||||
ID-1-foo 1-foo 2 hours ago About an hour ago
|
||||
ID-2-foo 2-foo 2 hours ago About an hour ago
|
||||
ID-10-foo 10-foo 2 hours ago About an hour ago
|
|
@ -0,0 +1,2 @@
|
|||
bar label=label-bar
|
||||
foo
|
|
@ -0,0 +1,3 @@
|
|||
ID NAME CREATED UPDATED
|
||||
ID-bar bar 2 hours ago About an hour ago
|
||||
ID-foo foo 2 hours ago About an hour ago
|
|
@ -0,0 +1,2 @@
|
|||
bar label=label-bar
|
||||
foo
|
|
@ -0,0 +1,2 @@
|
|||
ID-bar
|
||||
ID-foo
|
|
@ -0,0 +1,174 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/docker/docker/pkg/signal"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type attachOptions struct {
|
||||
noStdin bool
|
||||
proxy bool
|
||||
detachKeys string
|
||||
|
||||
container string
|
||||
}
|
||||
|
||||
func inspectContainerAndCheckState(ctx context.Context, cli client.APIClient, args string) (*types.ContainerJSON, error) {
|
||||
c, err := cli.ContainerInspect(ctx, args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !c.State.Running {
|
||||
return nil, errors.New("You cannot attach to a stopped container, start it first")
|
||||
}
|
||||
if c.State.Paused {
|
||||
return nil, errors.New("You cannot attach to a paused container, unpause it first")
|
||||
}
|
||||
if c.State.Restarting {
|
||||
return nil, errors.New("You cannot attach to a restarting container, wait until it is running")
|
||||
}
|
||||
|
||||
return &c, nil
|
||||
}
|
||||
|
||||
// NewAttachCommand creates a new cobra.Command for `docker attach`
|
||||
func NewAttachCommand(dockerCli command.Cli) *cobra.Command {
|
||||
var opts attachOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "attach [OPTIONS] CONTAINER",
|
||||
Short: "Attach local standard input, output, and error streams to a running container",
|
||||
Args: cli.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.container = args[0]
|
||||
return runAttach(dockerCli, &opts)
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
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.StringVar(&opts.detachKeys, "detach-keys", "", "Override the key sequence for detaching a container")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runAttach(dockerCli command.Cli, opts *attachOptions) error {
|
||||
ctx := context.Background()
|
||||
client := dockerCli.Client()
|
||||
|
||||
// request channel to wait for client
|
||||
resultC, errC := client.ContainerWait(ctx, opts.container, "")
|
||||
|
||||
c, err := inspectContainerAndCheckState(ctx, client, opts.container)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := dockerCli.In().CheckTty(!opts.noStdin, c.Config.Tty); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if opts.detachKeys != "" {
|
||||
dockerCli.ConfigFile().DetachKeys = opts.detachKeys
|
||||
}
|
||||
|
||||
options := types.ContainerAttachOptions{
|
||||
Stream: true,
|
||||
Stdin: !opts.noStdin && c.Config.OpenStdin,
|
||||
Stdout: true,
|
||||
Stderr: true,
|
||||
DetachKeys: dockerCli.ConfigFile().DetachKeys,
|
||||
}
|
||||
|
||||
var in io.ReadCloser
|
||||
if options.Stdin {
|
||||
in = dockerCli.In()
|
||||
}
|
||||
|
||||
if opts.proxy && !c.Config.Tty {
|
||||
sigc := notfiyAllSignals()
|
||||
go ForwardAllSignals(ctx, dockerCli, opts.container, sigc)
|
||||
defer signal.StopCatch(sigc)
|
||||
}
|
||||
|
||||
resp, errAttach := client.ContainerAttach(ctx, opts.container, options)
|
||||
if errAttach != nil {
|
||||
return errAttach
|
||||
}
|
||||
defer resp.Close()
|
||||
|
||||
// If use docker attach command to attach to a stop container, it will return
|
||||
// "You cannot attach to a stopped container" error, it's ok, but when
|
||||
// attach to a running container, it(docker attach) use inspect to check
|
||||
// the container's state, if it pass the state check on the client side,
|
||||
// and then the container is stopped, docker attach command still attach to
|
||||
// the container and not exit.
|
||||
//
|
||||
// Recheck the container's state to avoid attach block.
|
||||
_, err = inspectContainerAndCheckState(ctx, client, opts.container)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.Config.Tty && dockerCli.Out().IsTerminal() {
|
||||
resizeTTY(ctx, dockerCli, opts.container)
|
||||
}
|
||||
|
||||
streamer := hijackedIOStreamer{
|
||||
streams: dockerCli,
|
||||
inputStream: in,
|
||||
outputStream: dockerCli.Out(),
|
||||
errorStream: dockerCli.Err(),
|
||||
resp: resp,
|
||||
tty: c.Config.Tty,
|
||||
detachKeys: options.DetachKeys,
|
||||
}
|
||||
|
||||
if err := streamer.stream(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return getExitStatus(errC, resultC)
|
||||
}
|
||||
|
||||
func getExitStatus(errC <-chan error, resultC <-chan container.ContainerWaitOKBody) error {
|
||||
select {
|
||||
case result := <-resultC:
|
||||
if result.Error != nil {
|
||||
return fmt.Errorf(result.Error.Message)
|
||||
}
|
||||
if result.StatusCode != 0 {
|
||||
return cli.StatusError{StatusCode: int(result.StatusCode)}
|
||||
}
|
||||
case err := <-errC:
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func resizeTTY(ctx context.Context, dockerCli command.Cli, containerID string) {
|
||||
height, width := dockerCli.Out().GetTtySize()
|
||||
// To handle the case where a user repeatedly attaches/detaches without resizing their
|
||||
// terminal, the only way to get the shell prompt to display for attaches 2+ is to artificially
|
||||
// resize it, then go back to normal. Without this, every attach after the first will
|
||||
// require the user to manually resize or hit enter.
|
||||
resizeTtyTo(ctx, dockerCli.Client(), containerID, height+1, width+1, false)
|
||||
|
||||
// After the above resizing occurs, the call to MonitorTtySize below will handle resetting back
|
||||
// to the actual size.
|
||||
if err := MonitorTtySize(ctx, dockerCli, containerID, false); err != nil {
|
||||
logrus.Debugf("Error monitoring TTY size: %s", err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,129 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/pkg/errors"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestNewAttachCommandErrors(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
args []string
|
||||
expectedError string
|
||||
containerInspectFunc func(img string) (types.ContainerJSON, error)
|
||||
}{
|
||||
{
|
||||
name: "client-error",
|
||||
args: []string{"5cb5bb5e4a3b"},
|
||||
expectedError: "something went wrong",
|
||||
containerInspectFunc: func(containerID string) (types.ContainerJSON, error) {
|
||||
return types.ContainerJSON{}, errors.Errorf("something went wrong")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "client-stopped",
|
||||
args: []string{"5cb5bb5e4a3b"},
|
||||
expectedError: "You cannot attach to a stopped container",
|
||||
containerInspectFunc: func(containerID string) (types.ContainerJSON, error) {
|
||||
c := types.ContainerJSON{}
|
||||
c.ContainerJSONBase = &types.ContainerJSONBase{}
|
||||
c.ContainerJSONBase.State = &types.ContainerState{Running: false}
|
||||
return c, nil
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "client-paused",
|
||||
args: []string{"5cb5bb5e4a3b"},
|
||||
expectedError: "You cannot attach to a paused container",
|
||||
containerInspectFunc: func(containerID string) (types.ContainerJSON, error) {
|
||||
c := types.ContainerJSON{}
|
||||
c.ContainerJSONBase = &types.ContainerJSONBase{}
|
||||
c.ContainerJSONBase.State = &types.ContainerState{
|
||||
Running: true,
|
||||
Paused: true,
|
||||
}
|
||||
return c, nil
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "client-restarting",
|
||||
args: []string{"5cb5bb5e4a3b"},
|
||||
expectedError: "You cannot attach to a restarting container",
|
||||
containerInspectFunc: func(containerID string) (types.ContainerJSON, error) {
|
||||
c := types.ContainerJSON{}
|
||||
c.ContainerJSONBase = &types.ContainerJSONBase{}
|
||||
c.ContainerJSONBase.State = &types.ContainerState{
|
||||
Running: true,
|
||||
Paused: false,
|
||||
Restarting: true,
|
||||
}
|
||||
return c, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
cmd := NewAttachCommand(test.NewFakeCli(&fakeClient{inspectFunc: tc.containerInspectFunc}))
|
||||
cmd.SetOut(ioutil.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetExitStatus(t *testing.T) {
|
||||
var (
|
||||
expectedErr = fmt.Errorf("unexpected error")
|
||||
errC = make(chan error, 1)
|
||||
resultC = make(chan container.ContainerWaitOKBody, 1)
|
||||
)
|
||||
|
||||
testcases := []struct {
|
||||
result *container.ContainerWaitOKBody
|
||||
err error
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
result: &container.ContainerWaitOKBody{
|
||||
StatusCode: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
err: expectedErr,
|
||||
expectedError: expectedErr,
|
||||
},
|
||||
{
|
||||
result: &container.ContainerWaitOKBody{
|
||||
Error: &container.ContainerWaitOKBodyError{Message: expectedErr.Error()},
|
||||
},
|
||||
expectedError: expectedErr,
|
||||
},
|
||||
{
|
||||
result: &container.ContainerWaitOKBody{
|
||||
StatusCode: 15,
|
||||
},
|
||||
expectedError: cli.StatusError{StatusCode: 15},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testcase := range testcases {
|
||||
if testcase.err != nil {
|
||||
errC <- testcase.err
|
||||
}
|
||||
if testcase.result != nil {
|
||||
resultC <- *testcase.result
|
||||
}
|
||||
err := getExitStatus(errC, resultC)
|
||||
if testcase.expectedError == nil {
|
||||
assert.NilError(t, err)
|
||||
} else {
|
||||
assert.Error(t, err, testcase.expectedError.Error())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,164 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/docker/client"
|
||||
specs "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
)
|
||||
|
||||
type fakeClient struct {
|
||||
client.Client
|
||||
inspectFunc func(string) (types.ContainerJSON, error)
|
||||
execInspectFunc func(execID string) (types.ContainerExecInspect, error)
|
||||
execCreateFunc func(container string, config types.ExecConfig) (types.IDResponse, error)
|
||||
createContainerFunc func(config *container.Config,
|
||||
hostConfig *container.HostConfig,
|
||||
networkingConfig *network.NetworkingConfig,
|
||||
platform *specs.Platform,
|
||||
containerName string) (container.ContainerCreateCreatedBody, error)
|
||||
containerStartFunc func(container string, options types.ContainerStartOptions) error
|
||||
imageCreateFunc func(parentReference string, options types.ImageCreateOptions) (io.ReadCloser, error)
|
||||
infoFunc func() (types.Info, error)
|
||||
containerStatPathFunc func(container, path string) (types.ContainerPathStat, error)
|
||||
containerCopyFromFunc func(container, srcPath string) (io.ReadCloser, types.ContainerPathStat, error)
|
||||
logFunc func(string, types.ContainerLogsOptions) (io.ReadCloser, error)
|
||||
waitFunc func(string) (<-chan container.ContainerWaitOKBody, <-chan error)
|
||||
containerListFunc func(types.ContainerListOptions) ([]types.Container, error)
|
||||
containerExportFunc func(string) (io.ReadCloser, error)
|
||||
containerExecResizeFunc func(id string, options types.ResizeOptions) error
|
||||
containerRemoveFunc func(ctx context.Context, container string, options types.ContainerRemoveOptions) error
|
||||
containerKillFunc func(ctx context.Context, container, signal string) error
|
||||
Version string
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerList(_ context.Context, options types.ContainerListOptions) ([]types.Container, error) {
|
||||
if f.containerListFunc != nil {
|
||||
return f.containerListFunc(options)
|
||||
}
|
||||
return []types.Container{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerInspect(_ context.Context, containerID string) (types.ContainerJSON, error) {
|
||||
if f.inspectFunc != nil {
|
||||
return f.inspectFunc(containerID)
|
||||
}
|
||||
return types.ContainerJSON{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerExecCreate(_ context.Context, container string, config types.ExecConfig) (types.IDResponse, error) {
|
||||
if f.execCreateFunc != nil {
|
||||
return f.execCreateFunc(container, config)
|
||||
}
|
||||
return types.IDResponse{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerExecInspect(_ context.Context, execID string) (types.ContainerExecInspect, error) {
|
||||
if f.execInspectFunc != nil {
|
||||
return f.execInspectFunc(execID)
|
||||
}
|
||||
return types.ContainerExecInspect{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerExecStart(ctx context.Context, execID string, config types.ExecStartCheck) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerCreate(
|
||||
_ context.Context,
|
||||
config *container.Config,
|
||||
hostConfig *container.HostConfig,
|
||||
networkingConfig *network.NetworkingConfig,
|
||||
platform *specs.Platform,
|
||||
containerName string,
|
||||
) (container.ContainerCreateCreatedBody, error) {
|
||||
if f.createContainerFunc != nil {
|
||||
return f.createContainerFunc(config, hostConfig, networkingConfig, platform, containerName)
|
||||
}
|
||||
return container.ContainerCreateCreatedBody{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerRemove(ctx context.Context, container string, options types.ContainerRemoveOptions) error {
|
||||
if f.containerRemoveFunc != nil {
|
||||
return f.containerRemoveFunc(ctx, container, options)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ImageCreate(ctx context.Context, parentReference string, options types.ImageCreateOptions) (io.ReadCloser, error) {
|
||||
if f.imageCreateFunc != nil {
|
||||
return f.imageCreateFunc(parentReference, options)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) Info(_ context.Context) (types.Info, error) {
|
||||
if f.infoFunc != nil {
|
||||
return f.infoFunc()
|
||||
}
|
||||
return types.Info{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerStatPath(_ context.Context, container, path string) (types.ContainerPathStat, error) {
|
||||
if f.containerStatPathFunc != nil {
|
||||
return f.containerStatPathFunc(container, path)
|
||||
}
|
||||
return types.ContainerPathStat{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) CopyFromContainer(_ context.Context, container, srcPath string) (io.ReadCloser, types.ContainerPathStat, error) {
|
||||
if f.containerCopyFromFunc != nil {
|
||||
return f.containerCopyFromFunc(container, srcPath)
|
||||
}
|
||||
return nil, types.ContainerPathStat{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerLogs(_ context.Context, container string, options types.ContainerLogsOptions) (io.ReadCloser, error) {
|
||||
if f.logFunc != nil {
|
||||
return f.logFunc(container, options)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ClientVersion() string {
|
||||
return f.Version
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerWait(_ context.Context, container string, _ container.WaitCondition) (<-chan container.ContainerWaitOKBody, <-chan error) {
|
||||
if f.waitFunc != nil {
|
||||
return f.waitFunc(container)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerStart(_ context.Context, container string, options types.ContainerStartOptions) error {
|
||||
if f.containerStartFunc != nil {
|
||||
return f.containerStartFunc(container, options)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerExport(_ context.Context, container string) (io.ReadCloser, error) {
|
||||
if f.containerExportFunc != nil {
|
||||
return f.containerExportFunc(container)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerExecResize(_ context.Context, id string, options types.ResizeOptions) error {
|
||||
if f.containerExecResizeFunc != nil {
|
||||
return f.containerExecResizeFunc(id, options)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerKill(ctx context.Context, container, signal string) error {
|
||||
if f.containerKillFunc != nil {
|
||||
return f.containerKillFunc(ctx, container, signal)
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// NewContainerCommand returns a cobra command for `container` subcommands
|
||||
func NewContainerCommand(dockerCli command.Cli) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "container",
|
||||
Short: "Manage containers",
|
||||
Args: cli.NoArgs,
|
||||
RunE: command.ShowHelp(dockerCli.Err()),
|
||||
}
|
||||
cmd.AddCommand(
|
||||
NewAttachCommand(dockerCli),
|
||||
NewCommitCommand(dockerCli),
|
||||
NewCopyCommand(dockerCli),
|
||||
NewCreateCommand(dockerCli),
|
||||
NewDiffCommand(dockerCli),
|
||||
NewExecCommand(dockerCli),
|
||||
NewExportCommand(dockerCli),
|
||||
NewKillCommand(dockerCli),
|
||||
NewLogsCommand(dockerCli),
|
||||
NewPauseCommand(dockerCli),
|
||||
NewPortCommand(dockerCli),
|
||||
NewRenameCommand(dockerCli),
|
||||
NewRestartCommand(dockerCli),
|
||||
NewRmCommand(dockerCli),
|
||||
NewRunCommand(dockerCli),
|
||||
NewStartCommand(dockerCli),
|
||||
NewStatsCommand(dockerCli),
|
||||
NewStopCommand(dockerCli),
|
||||
NewTopCommand(dockerCli),
|
||||
NewUnpauseCommand(dockerCli),
|
||||
NewUpdateCommand(dockerCli),
|
||||
NewWaitCommand(dockerCli),
|
||||
newListCommand(dockerCli),
|
||||
newInspectCommand(dockerCli),
|
||||
NewPruneCommand(dockerCli),
|
||||
)
|
||||
return cmd
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type commitOptions struct {
|
||||
container string
|
||||
reference string
|
||||
|
||||
pause bool
|
||||
comment string
|
||||
author string
|
||||
changes opts.ListOpts
|
||||
}
|
||||
|
||||
// NewCommitCommand creates a new cobra.Command for `docker commit`
|
||||
func NewCommitCommand(dockerCli command.Cli) *cobra.Command {
|
||||
var options commitOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "commit [OPTIONS] CONTAINER [REPOSITORY[:TAG]]",
|
||||
Short: "Create a new image from a container's changes",
|
||||
Args: cli.RequiresRangeArgs(1, 2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
options.container = args[0]
|
||||
if len(args) > 1 {
|
||||
options.reference = args[1]
|
||||
}
|
||||
return runCommit(dockerCli, &options)
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.SetInterspersed(false)
|
||||
|
||||
flags.BoolVarP(&options.pause, "pause", "p", true, "Pause container during commit")
|
||||
flags.StringVarP(&options.comment, "message", "m", "", "Commit message")
|
||||
flags.StringVarP(&options.author, "author", "a", "", "Author (e.g., \"John Hannibal Smith <hannibal@a-team.com>\")")
|
||||
|
||||
options.changes = opts.NewListOpts(nil)
|
||||
flags.VarP(&options.changes, "change", "c", "Apply Dockerfile instruction to the created image")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runCommit(dockerCli command.Cli, options *commitOptions) error {
|
||||
ctx := context.Background()
|
||||
|
||||
name := options.container
|
||||
reference := options.reference
|
||||
|
||||
commitOptions := types.ContainerCommitOptions{
|
||||
Reference: reference,
|
||||
Comment: options.comment,
|
||||
Author: options.author,
|
||||
Changes: options.changes.GetAll(),
|
||||
Pause: options.pause,
|
||||
}
|
||||
|
||||
response, err := dockerCli.Client().ContainerCommit(ctx, name, commitOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintln(dockerCli.Out(), response.ID)
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,313 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/pkg/archive"
|
||||
"github.com/docker/docker/pkg/system"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type copyOptions struct {
|
||||
source string
|
||||
destination string
|
||||
followLink bool
|
||||
copyUIDGID bool
|
||||
}
|
||||
|
||||
type copyDirection int
|
||||
|
||||
const (
|
||||
fromContainer copyDirection = 1 << iota
|
||||
toContainer
|
||||
acrossContainers = fromContainer | toContainer
|
||||
)
|
||||
|
||||
type cpConfig struct {
|
||||
followLink bool
|
||||
copyUIDGID bool
|
||||
sourcePath string
|
||||
destPath string
|
||||
container string
|
||||
}
|
||||
|
||||
// NewCopyCommand creates a new `docker cp` command
|
||||
func NewCopyCommand(dockerCli command.Cli) *cobra.Command {
|
||||
var opts copyOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: `cp [OPTIONS] CONTAINER:SRC_PATH DEST_PATH|-
|
||||
docker cp [OPTIONS] SRC_PATH|- CONTAINER:DEST_PATH`,
|
||||
Short: "Copy files/folders between a container and the local filesystem",
|
||||
Long: strings.Join([]string{
|
||||
"Copy files/folders between a container and the local filesystem\n",
|
||||
"\nUse '-' as the source to read a tar archive from stdin\n",
|
||||
"and extract it to a directory destination in a container.\n",
|
||||
"Use '-' as the destination to stream a tar archive of a\n",
|
||||
"container source to stdout.",
|
||||
}, ""),
|
||||
Args: cli.ExactArgs(2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if args[0] == "" {
|
||||
return errors.New("source can not be empty")
|
||||
}
|
||||
if args[1] == "" {
|
||||
return errors.New("destination can not be empty")
|
||||
}
|
||||
opts.source = args[0]
|
||||
opts.destination = args[1]
|
||||
return runCopy(dockerCli, opts)
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVarP(&opts.followLink, "follow-link", "L", false, "Always follow symbol link in SRC_PATH")
|
||||
flags.BoolVarP(&opts.copyUIDGID, "archive", "a", false, "Archive mode (copy all uid/gid information)")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runCopy(dockerCli command.Cli, opts copyOptions) error {
|
||||
srcContainer, srcPath := splitCpArg(opts.source)
|
||||
destContainer, destPath := splitCpArg(opts.destination)
|
||||
|
||||
copyConfig := cpConfig{
|
||||
followLink: opts.followLink,
|
||||
copyUIDGID: opts.copyUIDGID,
|
||||
sourcePath: srcPath,
|
||||
destPath: destPath,
|
||||
}
|
||||
|
||||
var direction copyDirection
|
||||
if srcContainer != "" {
|
||||
direction |= fromContainer
|
||||
copyConfig.container = srcContainer
|
||||
}
|
||||
if destContainer != "" {
|
||||
direction |= toContainer
|
||||
copyConfig.container = destContainer
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
switch direction {
|
||||
case fromContainer:
|
||||
return copyFromContainer(ctx, dockerCli, copyConfig)
|
||||
case toContainer:
|
||||
return copyToContainer(ctx, dockerCli, copyConfig)
|
||||
case acrossContainers:
|
||||
return errors.New("copying between containers is not supported")
|
||||
default:
|
||||
return errors.New("must specify at least one container source")
|
||||
}
|
||||
}
|
||||
|
||||
func resolveLocalPath(localPath string) (absPath string, err error) {
|
||||
if absPath, err = filepath.Abs(localPath); err != nil {
|
||||
return
|
||||
}
|
||||
return archive.PreserveTrailingDotOrSeparator(absPath, localPath, filepath.Separator), nil
|
||||
}
|
||||
|
||||
func copyFromContainer(ctx context.Context, dockerCli command.Cli, copyConfig cpConfig) (err error) {
|
||||
dstPath := copyConfig.destPath
|
||||
srcPath := copyConfig.sourcePath
|
||||
|
||||
if dstPath != "-" {
|
||||
// Get an absolute destination path.
|
||||
dstPath, err = resolveLocalPath(dstPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := command.ValidateOutputPath(dstPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client := dockerCli.Client()
|
||||
// if client requests to follow symbol link, then must decide target file to be copied
|
||||
var rebaseName string
|
||||
if copyConfig.followLink {
|
||||
srcStat, err := client.ContainerStatPath(ctx, copyConfig.container, srcPath)
|
||||
|
||||
// If the destination is a symbolic link, we should follow it.
|
||||
if err == nil && srcStat.Mode&os.ModeSymlink != 0 {
|
||||
linkTarget := srcStat.LinkTarget
|
||||
if !system.IsAbs(linkTarget) {
|
||||
// Join with the parent directory.
|
||||
srcParent, _ := archive.SplitPathDirEntry(srcPath)
|
||||
linkTarget = filepath.Join(srcParent, linkTarget)
|
||||
}
|
||||
|
||||
linkTarget, rebaseName = archive.GetRebaseName(srcPath, linkTarget)
|
||||
srcPath = linkTarget
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
content, stat, err := client.CopyFromContainer(ctx, copyConfig.container, srcPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer content.Close()
|
||||
|
||||
if dstPath == "-" {
|
||||
_, err = io.Copy(dockerCli.Out(), content)
|
||||
return err
|
||||
}
|
||||
|
||||
srcInfo := archive.CopyInfo{
|
||||
Path: srcPath,
|
||||
Exists: true,
|
||||
IsDir: stat.Mode.IsDir(),
|
||||
RebaseName: rebaseName,
|
||||
}
|
||||
|
||||
preArchive := content
|
||||
if len(srcInfo.RebaseName) != 0 {
|
||||
_, srcBase := archive.SplitPathDirEntry(srcInfo.Path)
|
||||
preArchive = archive.RebaseArchiveEntries(content, srcBase, srcInfo.RebaseName)
|
||||
}
|
||||
return archive.CopyTo(preArchive, srcInfo, dstPath)
|
||||
}
|
||||
|
||||
// In order to get the copy behavior right, we need to know information
|
||||
// about both the source and destination. The API is a simple tar
|
||||
// archive/extract API but we can use the stat info header about the
|
||||
// destination to be more informed about exactly what the destination is.
|
||||
func copyToContainer(ctx context.Context, dockerCli command.Cli, copyConfig cpConfig) (err error) {
|
||||
srcPath := copyConfig.sourcePath
|
||||
dstPath := copyConfig.destPath
|
||||
|
||||
if srcPath != "-" {
|
||||
// Get an absolute source path.
|
||||
srcPath, err = resolveLocalPath(srcPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
client := dockerCli.Client()
|
||||
// Prepare destination copy info by stat-ing the container path.
|
||||
dstInfo := archive.CopyInfo{Path: dstPath}
|
||||
dstStat, err := client.ContainerStatPath(ctx, copyConfig.container, dstPath)
|
||||
|
||||
// If the destination is a symbolic link, we should evaluate it.
|
||||
if err == nil && dstStat.Mode&os.ModeSymlink != 0 {
|
||||
linkTarget := dstStat.LinkTarget
|
||||
if !system.IsAbs(linkTarget) {
|
||||
// Join with the parent directory.
|
||||
dstParent, _ := archive.SplitPathDirEntry(dstPath)
|
||||
linkTarget = filepath.Join(dstParent, linkTarget)
|
||||
}
|
||||
|
||||
dstInfo.Path = linkTarget
|
||||
dstStat, err = client.ContainerStatPath(ctx, copyConfig.container, linkTarget)
|
||||
}
|
||||
|
||||
// Validate the destination path
|
||||
if err := command.ValidateOutputPathFileMode(dstStat.Mode); err != nil {
|
||||
return errors.Wrapf(err, `destination "%s:%s" must be a directory or a regular file`, copyConfig.container, dstPath)
|
||||
}
|
||||
|
||||
// Ignore any error and assume that the parent directory of the destination
|
||||
// path exists, in which case the copy may still succeed. If there is any
|
||||
// type of conflict (e.g., non-directory overwriting an existing directory
|
||||
// or vice versa) the extraction will fail. If the destination simply did
|
||||
// not exist, but the parent directory does, the extraction will still
|
||||
// succeed.
|
||||
if err == nil {
|
||||
dstInfo.Exists, dstInfo.IsDir = true, dstStat.Mode.IsDir()
|
||||
}
|
||||
|
||||
var (
|
||||
content io.Reader
|
||||
resolvedDstPath string
|
||||
)
|
||||
|
||||
if srcPath == "-" {
|
||||
content = os.Stdin
|
||||
resolvedDstPath = dstInfo.Path
|
||||
if !dstInfo.IsDir {
|
||||
return errors.Errorf("destination \"%s:%s\" must be a directory", copyConfig.container, dstPath)
|
||||
}
|
||||
} else {
|
||||
// Prepare source copy info.
|
||||
srcInfo, err := archive.CopyInfoSourcePath(srcPath, copyConfig.followLink)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
srcArchive, err := archive.TarResource(srcInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer srcArchive.Close()
|
||||
|
||||
// With the stat info about the local source as well as the
|
||||
// destination, we have enough information to know whether we need to
|
||||
// alter the archive that we upload so that when the server extracts
|
||||
// it to the specified directory in the container we get the desired
|
||||
// copy behavior.
|
||||
|
||||
// See comments in the implementation of `archive.PrepareArchiveCopy`
|
||||
// for exactly what goes into deciding how and whether the source
|
||||
// archive needs to be altered for the correct copy behavior when it is
|
||||
// extracted. This function also infers from the source and destination
|
||||
// info which directory to extract to, which may be the parent of the
|
||||
// destination that the user specified.
|
||||
dstDir, preparedArchive, err := archive.PrepareArchiveCopy(srcArchive, srcInfo, dstInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer preparedArchive.Close()
|
||||
|
||||
resolvedDstPath = dstDir
|
||||
content = preparedArchive
|
||||
}
|
||||
|
||||
options := types.CopyToContainerOptions{
|
||||
AllowOverwriteDirWithFile: false,
|
||||
CopyUIDGID: copyConfig.copyUIDGID,
|
||||
}
|
||||
return client.CopyToContainer(ctx, copyConfig.container, resolvedDstPath, content, options)
|
||||
}
|
||||
|
||||
// We use `:` as a delimiter between CONTAINER and PATH, but `:` could also be
|
||||
// in a valid LOCALPATH, like `file:name.txt`. We can resolve this ambiguity by
|
||||
// requiring a LOCALPATH with a `:` to be made explicit with a relative or
|
||||
// absolute path:
|
||||
// `/path/to/file:name.txt` or `./file:name.txt`
|
||||
//
|
||||
// This is apparently how `scp` handles this as well:
|
||||
// http://www.cyberciti.biz/faq/rsync-scp-file-name-with-colon-punctuation-in-it/
|
||||
//
|
||||
// We can't simply check for a filepath separator because container names may
|
||||
// have a separator, e.g., "host0/cname1" if container is in a Docker cluster,
|
||||
// so we have to check for a `/` or `.` prefix. Also, in the case of a Windows
|
||||
// client, a `:` could be part of an absolute Windows path, in which case it
|
||||
// is immediately proceeded by a backslash.
|
||||
func splitCpArg(arg string) (container, path string) {
|
||||
if system.IsAbs(arg) {
|
||||
// Explicit local absolute path, e.g., `C:\foo` or `/foo`.
|
||||
return "", arg
|
||||
}
|
||||
|
||||
parts := strings.SplitN(arg, ":", 2)
|
||||
|
||||
if len(parts) == 1 || strings.HasPrefix(parts[0], ".") {
|
||||
// Either there's no `:` in the arg
|
||||
// OR it's an explicit local relative path like `./file:name.txt`.
|
||||
return "", arg
|
||||
}
|
||||
|
||||
return parts[0], parts[1]
|
||||
}
|
|
@ -0,0 +1,201 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/pkg/archive"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
"gotest.tools/v3/fs"
|
||||
"gotest.tools/v3/skip"
|
||||
)
|
||||
|
||||
func TestRunCopyWithInvalidArguments(t *testing.T) {
|
||||
var testcases = []struct {
|
||||
doc string
|
||||
options copyOptions
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
doc: "copy between container",
|
||||
options: copyOptions{
|
||||
source: "first:/path",
|
||||
destination: "second:/path",
|
||||
},
|
||||
expectedErr: "copying between containers is not supported",
|
||||
},
|
||||
{
|
||||
doc: "copy without a container",
|
||||
options: copyOptions{
|
||||
source: "./source",
|
||||
destination: "./dest",
|
||||
},
|
||||
expectedErr: "must specify at least one container source",
|
||||
},
|
||||
}
|
||||
for _, testcase := range testcases {
|
||||
t.Run(testcase.doc, func(t *testing.T) {
|
||||
err := runCopy(test.NewFakeCli(nil), testcase.options)
|
||||
assert.Error(t, err, testcase.expectedErr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunCopyFromContainerToStdout(t *testing.T) {
|
||||
tarContent := "the tar content"
|
||||
|
||||
fakeClient := &fakeClient{
|
||||
containerCopyFromFunc: func(container, srcPath string) (io.ReadCloser, types.ContainerPathStat, error) {
|
||||
assert.Check(t, is.Equal("container", container))
|
||||
return ioutil.NopCloser(strings.NewReader(tarContent)), types.ContainerPathStat{}, nil
|
||||
},
|
||||
}
|
||||
options := copyOptions{source: "container:/path", destination: "-"}
|
||||
cli := test.NewFakeCli(fakeClient)
|
||||
err := runCopy(cli, options)
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, is.Equal(tarContent, cli.OutBuffer().String()))
|
||||
assert.Check(t, is.Equal("", cli.ErrBuffer().String()))
|
||||
}
|
||||
|
||||
func TestRunCopyFromContainerToFilesystem(t *testing.T) {
|
||||
destDir := fs.NewDir(t, "cp-test",
|
||||
fs.WithFile("file1", "content\n"))
|
||||
defer destDir.Remove()
|
||||
|
||||
fakeClient := &fakeClient{
|
||||
containerCopyFromFunc: func(container, srcPath string) (io.ReadCloser, types.ContainerPathStat, error) {
|
||||
assert.Check(t, is.Equal("container", container))
|
||||
readCloser, err := archive.TarWithOptions(destDir.Path(), &archive.TarOptions{})
|
||||
return readCloser, types.ContainerPathStat{}, err
|
||||
},
|
||||
}
|
||||
options := copyOptions{source: "container:/path", destination: destDir.Path()}
|
||||
cli := test.NewFakeCli(fakeClient)
|
||||
err := runCopy(cli, options)
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, is.Equal("", cli.OutBuffer().String()))
|
||||
assert.Check(t, is.Equal("", cli.ErrBuffer().String()))
|
||||
|
||||
content, err := ioutil.ReadFile(destDir.Join("file1"))
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, is.Equal("content\n", string(content)))
|
||||
}
|
||||
|
||||
func TestRunCopyFromContainerToFilesystemMissingDestinationDirectory(t *testing.T) {
|
||||
destDir := fs.NewDir(t, "cp-test",
|
||||
fs.WithFile("file1", "content\n"))
|
||||
defer destDir.Remove()
|
||||
|
||||
fakeClient := &fakeClient{
|
||||
containerCopyFromFunc: func(container, srcPath string) (io.ReadCloser, types.ContainerPathStat, error) {
|
||||
assert.Check(t, is.Equal("container", container))
|
||||
readCloser, err := archive.TarWithOptions(destDir.Path(), &archive.TarOptions{})
|
||||
return readCloser, types.ContainerPathStat{}, err
|
||||
},
|
||||
}
|
||||
|
||||
options := copyOptions{
|
||||
source: "container:/path",
|
||||
destination: destDir.Join("missing", "foo"),
|
||||
}
|
||||
cli := test.NewFakeCli(fakeClient)
|
||||
err := runCopy(cli, options)
|
||||
assert.ErrorContains(t, err, destDir.Join("missing"))
|
||||
}
|
||||
|
||||
func TestRunCopyToContainerFromFileWithTrailingSlash(t *testing.T) {
|
||||
srcFile := fs.NewFile(t, t.Name())
|
||||
defer srcFile.Remove()
|
||||
|
||||
options := copyOptions{
|
||||
source: srcFile.Path() + string(os.PathSeparator),
|
||||
destination: "container:/path",
|
||||
}
|
||||
cli := test.NewFakeCli(&fakeClient{})
|
||||
err := runCopy(cli, options)
|
||||
|
||||
expectedError := "not a directory"
|
||||
if runtime.GOOS == "windows" {
|
||||
expectedError = "The filename, directory name, or volume label syntax is incorrect"
|
||||
}
|
||||
assert.ErrorContains(t, err, expectedError)
|
||||
}
|
||||
|
||||
func TestRunCopyToContainerSourceDoesNotExist(t *testing.T) {
|
||||
options := copyOptions{
|
||||
source: "/does/not/exist",
|
||||
destination: "container:/path",
|
||||
}
|
||||
cli := test.NewFakeCli(&fakeClient{})
|
||||
err := runCopy(cli, options)
|
||||
expected := "no such file or directory"
|
||||
if runtime.GOOS == "windows" {
|
||||
expected = "cannot find the file specified"
|
||||
}
|
||||
assert.ErrorContains(t, err, expected)
|
||||
}
|
||||
|
||||
func TestSplitCpArg(t *testing.T) {
|
||||
var testcases = []struct {
|
||||
doc string
|
||||
path string
|
||||
os string
|
||||
expectedContainer string
|
||||
expectedPath string
|
||||
}{
|
||||
{
|
||||
doc: "absolute path with colon",
|
||||
os: "linux",
|
||||
path: "/abs/path:withcolon",
|
||||
expectedPath: "/abs/path:withcolon",
|
||||
},
|
||||
{
|
||||
doc: "relative path with colon",
|
||||
path: "./relative:path",
|
||||
expectedPath: "./relative:path",
|
||||
},
|
||||
{
|
||||
doc: "absolute path with drive",
|
||||
os: "windows",
|
||||
path: `d:\abs\path`,
|
||||
expectedPath: `d:\abs\path`,
|
||||
},
|
||||
{
|
||||
doc: "no separator",
|
||||
path: "relative/path",
|
||||
expectedPath: "relative/path",
|
||||
},
|
||||
{
|
||||
doc: "with separator",
|
||||
path: "container:/opt/foo",
|
||||
expectedPath: "/opt/foo",
|
||||
expectedContainer: "container",
|
||||
},
|
||||
}
|
||||
for _, testcase := range testcases {
|
||||
t.Run(testcase.doc, func(t *testing.T) {
|
||||
skip.If(t, testcase.os != "" && testcase.os != runtime.GOOS)
|
||||
|
||||
container, path := splitCpArg(testcase.path)
|
||||
assert.Check(t, is.Equal(testcase.expectedContainer, container))
|
||||
assert.Check(t, is.Equal(testcase.expectedPath, path))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunCopyFromContainerToFilesystemIrregularDestination(t *testing.T) {
|
||||
options := copyOptions{source: "container:/dev/null", destination: "/dev/random"}
|
||||
cli := test.NewFakeCli(nil)
|
||||
err := runCopy(cli, options)
|
||||
assert.Assert(t, err != nil)
|
||||
expected := `"/dev/random" must be a directory or a regular file`
|
||||
assert.ErrorContains(t, err, expected)
|
||||
}
|
|
@ -0,0 +1,312 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"regexp"
|
||||
|
||||
"github.com/containerd/containerd/platforms"
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/image"
|
||||
"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/versions"
|
||||
apiclient "github.com/docker/docker/client"
|
||||
"github.com/docker/docker/pkg/jsonmessage"
|
||||
"github.com/docker/docker/registry"
|
||||
specs "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
// Pull constants
|
||||
const (
|
||||
PullImageAlways = "always"
|
||||
PullImageMissing = "missing" // Default (matches previous behavior)
|
||||
PullImageNever = "never"
|
||||
)
|
||||
|
||||
type createOptions struct {
|
||||
name string
|
||||
platform string
|
||||
untrusted bool
|
||||
pull string // alway, missing, never
|
||||
}
|
||||
|
||||
// NewCreateCommand creates a new cobra.Command for `docker create`
|
||||
func NewCreateCommand(dockerCli command.Cli) *cobra.Command {
|
||||
var opts createOptions
|
||||
var copts *containerOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "create [OPTIONS] IMAGE [COMMAND] [ARG...]",
|
||||
Short: "Create a new container",
|
||||
Args: cli.RequiresMinArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
copts.Image = args[0]
|
||||
if len(args) > 1 {
|
||||
copts.Args = args[1:]
|
||||
}
|
||||
return runCreate(dockerCli, cmd.Flags(), &opts, copts)
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.SetInterspersed(false)
|
||||
|
||||
flags.StringVar(&opts.name, "name", "", "Assign a name to the container")
|
||||
flags.StringVar(&opts.pull, "pull", PullImageMissing,
|
||||
`Pull image before creating ("`+PullImageAlways+`"|"`+PullImageMissing+`"|"`+PullImageNever+`")`)
|
||||
|
||||
// Add an explicit help that doesn't have a `-h` to prevent the conflict
|
||||
// with hostname
|
||||
flags.Bool("help", false, "Print usage")
|
||||
|
||||
command.AddPlatformFlag(flags, &opts.platform)
|
||||
command.AddTrustVerificationFlags(flags, &opts.untrusted, dockerCli.ContentTrustEnabled())
|
||||
copts = addFlags(flags)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runCreate(dockerCli command.Cli, flags *pflag.FlagSet, options *createOptions, copts *containerOptions) error {
|
||||
proxyConfig := dockerCli.ConfigFile().ParseProxyConfig(dockerCli.Client().DaemonHost(), opts.ConvertKVStringsToMapWithNil(copts.env.GetAll()))
|
||||
newEnv := []string{}
|
||||
for k, v := range proxyConfig {
|
||||
if v == nil {
|
||||
newEnv = append(newEnv, k)
|
||||
} else {
|
||||
newEnv = append(newEnv, fmt.Sprintf("%s=%s", k, *v))
|
||||
}
|
||||
}
|
||||
copts.env = *opts.NewListOptsRef(&newEnv, nil)
|
||||
containerConfig, err := parse(flags, copts, dockerCli.ServerInfo().OSType)
|
||||
if err != nil {
|
||||
reportError(dockerCli.Err(), "create", err.Error(), true)
|
||||
return cli.StatusError{StatusCode: 125}
|
||||
}
|
||||
if err = validateAPIVersion(containerConfig, dockerCli.Client().ClientVersion()); err != nil {
|
||||
reportError(dockerCli.Err(), "create", err.Error(), true)
|
||||
return cli.StatusError{StatusCode: 125}
|
||||
}
|
||||
response, err := createContainer(context.Background(), dockerCli, containerConfig, options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintln(dockerCli.Out(), response.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func pullImage(ctx context.Context, dockerCli command.Cli, image string, platform string, out io.Writer) error {
|
||||
ref, err := reference.ParseNormalizedNamed(image)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Resolve the Repository name from fqn to RepositoryInfo
|
||||
repoInfo, err := registry.ParseRepositoryInfo(ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
authConfig := command.ResolveAuthConfig(ctx, dockerCli, repoInfo.Index)
|
||||
encodedAuth, err := command.EncodeAuthToBase64(authConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
options := types.ImageCreateOptions{
|
||||
RegistryAuth: encodedAuth,
|
||||
Platform: platform,
|
||||
}
|
||||
|
||||
responseBody, err := dockerCli.Client().ImageCreate(ctx, image, options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer responseBody.Close()
|
||||
|
||||
return jsonmessage.DisplayJSONMessagesStream(
|
||||
responseBody,
|
||||
out,
|
||||
dockerCli.Out().FD(),
|
||||
dockerCli.Out().IsTerminal(),
|
||||
nil)
|
||||
}
|
||||
|
||||
type cidFile struct {
|
||||
path string
|
||||
file *os.File
|
||||
written bool
|
||||
}
|
||||
|
||||
func (cid *cidFile) Close() error {
|
||||
if cid.file == nil {
|
||||
return nil
|
||||
}
|
||||
cid.file.Close()
|
||||
|
||||
if cid.written {
|
||||
return nil
|
||||
}
|
||||
if err := os.Remove(cid.path); err != nil {
|
||||
return errors.Errorf("failed to remove the CID file '%s': %s \n", cid.path, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cid *cidFile) Write(id string) error {
|
||||
if cid.file == nil {
|
||||
return nil
|
||||
}
|
||||
if _, err := cid.file.Write([]byte(id)); err != nil {
|
||||
return errors.Errorf("Failed to write the container ID to the file: %s", err)
|
||||
}
|
||||
cid.written = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func newCIDFile(path string) (*cidFile, error) {
|
||||
if path == "" {
|
||||
return &cidFile{}, nil
|
||||
}
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
return nil, errors.Errorf("Container ID file found, make sure the other container isn't running or delete %s", path)
|
||||
}
|
||||
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
return nil, errors.Errorf("Failed to create the container ID file: %s", err)
|
||||
}
|
||||
|
||||
return &cidFile{path: path, file: f}, nil
|
||||
}
|
||||
|
||||
// nolint: gocyclo
|
||||
func createContainer(ctx context.Context, dockerCli command.Cli, containerConfig *containerConfig, opts *createOptions) (*container.ContainerCreateCreatedBody, error) {
|
||||
config := containerConfig.Config
|
||||
hostConfig := containerConfig.HostConfig
|
||||
networkingConfig := containerConfig.NetworkingConfig
|
||||
stderr := dockerCli.Err()
|
||||
|
||||
warnOnOomKillDisable(*hostConfig, stderr)
|
||||
warnOnLocalhostDNS(*hostConfig, stderr)
|
||||
|
||||
var (
|
||||
trustedRef reference.Canonical
|
||||
namedRef reference.Named
|
||||
)
|
||||
|
||||
containerIDFile, err := newCIDFile(hostConfig.ContainerIDFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer containerIDFile.Close()
|
||||
|
||||
ref, err := reference.ParseAnyReference(config.Image)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if named, ok := ref.(reference.Named); ok {
|
||||
namedRef = reference.TagNameOnly(named)
|
||||
|
||||
if taggedRef, ok := namedRef.(reference.NamedTagged); ok && !opts.untrusted {
|
||||
var err error
|
||||
trustedRef, err = image.TrustedReference(ctx, dockerCli, taggedRef, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config.Image = reference.FamiliarString(trustedRef)
|
||||
}
|
||||
}
|
||||
|
||||
pullAndTagImage := func() error {
|
||||
if err := pullImage(ctx, dockerCli, config.Image, opts.platform, stderr); err != nil {
|
||||
return err
|
||||
}
|
||||
if taggedRef, ok := namedRef.(reference.NamedTagged); ok && trustedRef != nil {
|
||||
return image.TagTrusted(ctx, dockerCli, trustedRef, taggedRef)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var platform *specs.Platform
|
||||
// Engine API version 1.41 first introduced the option to specify platform on
|
||||
// create. It will produce an error if you try to set a platform on older API
|
||||
// versions, so check the API version here to maintain backwards
|
||||
// compatibility for CLI users.
|
||||
if opts.platform != "" && versions.GreaterThanOrEqualTo(dockerCli.Client().ClientVersion(), "1.41") {
|
||||
p, err := platforms.Parse(opts.platform)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error parsing specified platform")
|
||||
}
|
||||
platform = &p
|
||||
}
|
||||
|
||||
if opts.pull == PullImageAlways {
|
||||
if err := pullAndTagImage(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
response, err := dockerCli.Client().ContainerCreate(ctx, config, hostConfig, networkingConfig, platform, opts.name)
|
||||
if err != nil {
|
||||
// Pull image if it does not exist locally and we have the PullImageMissing option. Default behavior.
|
||||
if apiclient.IsErrNotFound(err) && namedRef != nil && opts.pull == PullImageMissing {
|
||||
// we don't want to write to stdout anything apart from container.ID
|
||||
fmt.Fprintf(stderr, "Unable to find image '%s' locally\n", reference.FamiliarString(namedRef))
|
||||
if err := pullAndTagImage(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var retryErr error
|
||||
response, retryErr = dockerCli.Client().ContainerCreate(ctx, config, hostConfig, networkingConfig, platform, opts.name)
|
||||
if retryErr != nil {
|
||||
return nil, retryErr
|
||||
}
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
for _, warning := range response.Warnings {
|
||||
fmt.Fprintf(stderr, "WARNING: %s\n", warning)
|
||||
}
|
||||
err = containerIDFile.Write(response.ID)
|
||||
return &response, err
|
||||
}
|
||||
|
||||
func warnOnOomKillDisable(hostConfig container.HostConfig, stderr io.Writer) {
|
||||
if hostConfig.OomKillDisable != nil && *hostConfig.OomKillDisable && hostConfig.Memory == 0 {
|
||||
fmt.Fprintln(stderr, "WARNING: Disabling the OOM killer on containers without setting a '-m/--memory' limit may be dangerous.")
|
||||
}
|
||||
}
|
||||
|
||||
// check the DNS settings passed via --dns against localhost regexp to warn if
|
||||
// they are trying to set a DNS to a localhost address
|
||||
func warnOnLocalhostDNS(hostConfig container.HostConfig, stderr io.Writer) {
|
||||
for _, dnsIP := range hostConfig.DNS {
|
||||
if isLocalhost(dnsIP) {
|
||||
fmt.Fprintf(stderr, "WARNING: Localhost DNS setting (--dns=%s) may fail in containers.\n", dnsIP)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// IPLocalhost is a regex pattern for IPv4 or IPv6 loopback range.
|
||||
const ipLocalhost = `((127\.([0-9]{1,3}\.){2}[0-9]{1,3})|(::1)$)`
|
||||
|
||||
var localhostIPRegexp = regexp.MustCompile(ipLocalhost)
|
||||
|
||||
// IsLocalhost returns true if ip matches the localhost IP regular expression.
|
||||
// Used for determining if nameserver settings are being passed which are
|
||||
// localhost addresses
|
||||
func isLocalhost(ip string) bool {
|
||||
return localhostIPRegexp.MatchString(ip)
|
||||
}
|
|
@ -0,0 +1,315 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/cli/internal/test/notary"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
specs "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
"gotest.tools/v3/fs"
|
||||
"gotest.tools/v3/golden"
|
||||
)
|
||||
|
||||
func TestCIDFileNoOPWithNoFilename(t *testing.T) {
|
||||
file, err := newCIDFile("")
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, &cidFile{}, file, cmp.AllowUnexported(cidFile{}))
|
||||
|
||||
assert.NilError(t, file.Write("id"))
|
||||
assert.NilError(t, file.Close())
|
||||
}
|
||||
|
||||
func TestNewCIDFileWhenFileAlreadyExists(t *testing.T) {
|
||||
tempfile := fs.NewFile(t, "test-cid-file")
|
||||
defer tempfile.Remove()
|
||||
|
||||
_, err := newCIDFile(tempfile.Path())
|
||||
assert.ErrorContains(t, err, "Container ID file found")
|
||||
}
|
||||
|
||||
func TestCIDFileCloseWithNoWrite(t *testing.T) {
|
||||
tempdir := fs.NewDir(t, "test-cid-file")
|
||||
defer tempdir.Remove()
|
||||
|
||||
path := tempdir.Join("cidfile")
|
||||
file, err := newCIDFile(path)
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, is.Equal(file.path, path))
|
||||
|
||||
assert.NilError(t, file.Close())
|
||||
_, err = os.Stat(path)
|
||||
assert.Check(t, os.IsNotExist(err))
|
||||
}
|
||||
|
||||
func TestCIDFileCloseWithWrite(t *testing.T) {
|
||||
tempdir := fs.NewDir(t, "test-cid-file")
|
||||
defer tempdir.Remove()
|
||||
|
||||
path := tempdir.Join("cidfile")
|
||||
file, err := newCIDFile(path)
|
||||
assert.NilError(t, err)
|
||||
|
||||
content := "id"
|
||||
assert.NilError(t, file.Write(content))
|
||||
|
||||
actual, err := ioutil.ReadFile(path)
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, is.Equal(content, string(actual)))
|
||||
|
||||
assert.NilError(t, file.Close())
|
||||
_, err = os.Stat(path)
|
||||
assert.NilError(t, err)
|
||||
}
|
||||
|
||||
func TestCreateContainerImagePullPolicy(t *testing.T) {
|
||||
imageName := "does-not-exist-locally"
|
||||
containerID := "abcdef"
|
||||
config := &containerConfig{
|
||||
Config: &container.Config{
|
||||
Image: imageName,
|
||||
},
|
||||
HostConfig: &container.HostConfig{},
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
PullPolicy string
|
||||
ExpectedPulls int
|
||||
ExpectedBody container.ContainerCreateCreatedBody
|
||||
ExpectedErrMsg string
|
||||
ResponseCounter int
|
||||
}{
|
||||
{
|
||||
PullPolicy: PullImageMissing,
|
||||
ExpectedPulls: 1,
|
||||
ExpectedBody: container.ContainerCreateCreatedBody{ID: containerID},
|
||||
}, {
|
||||
PullPolicy: PullImageAlways,
|
||||
ExpectedPulls: 1,
|
||||
ExpectedBody: container.ContainerCreateCreatedBody{ID: containerID},
|
||||
ResponseCounter: 1, // This lets us return a container on the first pull
|
||||
}, {
|
||||
PullPolicy: PullImageNever,
|
||||
ExpectedPulls: 0,
|
||||
ExpectedErrMsg: "error fake not found",
|
||||
},
|
||||
}
|
||||
for _, c := range cases {
|
||||
c := c
|
||||
pullCounter := 0
|
||||
|
||||
client := &fakeClient{
|
||||
createContainerFunc: func(
|
||||
config *container.Config,
|
||||
hostConfig *container.HostConfig,
|
||||
networkingConfig *network.NetworkingConfig,
|
||||
platform *specs.Platform,
|
||||
containerName string,
|
||||
) (container.ContainerCreateCreatedBody, error) {
|
||||
defer func() { c.ResponseCounter++ }()
|
||||
switch c.ResponseCounter {
|
||||
case 0:
|
||||
return container.ContainerCreateCreatedBody{}, fakeNotFound{}
|
||||
default:
|
||||
return container.ContainerCreateCreatedBody{ID: containerID}, nil
|
||||
}
|
||||
},
|
||||
imageCreateFunc: func(parentReference string, options types.ImageCreateOptions) (io.ReadCloser, error) {
|
||||
defer func() { pullCounter++ }()
|
||||
return ioutil.NopCloser(strings.NewReader("")), nil
|
||||
},
|
||||
infoFunc: func() (types.Info, error) {
|
||||
return types.Info{IndexServerAddress: "https://indexserver.example.com"}, nil
|
||||
},
|
||||
}
|
||||
cli := test.NewFakeCli(client)
|
||||
body, err := createContainer(context.Background(), cli, config, &createOptions{
|
||||
name: "name",
|
||||
platform: runtime.GOOS,
|
||||
untrusted: true,
|
||||
pull: c.PullPolicy,
|
||||
})
|
||||
|
||||
if c.ExpectedErrMsg != "" {
|
||||
assert.ErrorContains(t, err, c.ExpectedErrMsg)
|
||||
} else {
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, is.DeepEqual(c.ExpectedBody, *body))
|
||||
}
|
||||
|
||||
assert.Check(t, is.Equal(c.ExpectedPulls, pullCounter))
|
||||
}
|
||||
}
|
||||
func TestNewCreateCommandWithContentTrustErrors(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
args []string
|
||||
expectedError string
|
||||
notaryFunc test.NotaryClientFuncType
|
||||
}{
|
||||
{
|
||||
name: "offline-notary-server",
|
||||
notaryFunc: notary.GetOfflineNotaryRepository,
|
||||
expectedError: "client is offline",
|
||||
args: []string{"image:tag"},
|
||||
},
|
||||
{
|
||||
name: "uninitialized-notary-server",
|
||||
notaryFunc: notary.GetUninitializedNotaryRepository,
|
||||
expectedError: "remote trust data does not exist",
|
||||
args: []string{"image:tag"},
|
||||
},
|
||||
{
|
||||
name: "empty-notary-server",
|
||||
notaryFunc: notary.GetEmptyTargetsNotaryRepository,
|
||||
expectedError: "No valid trust data for tag",
|
||||
args: []string{"image:tag"},
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
createContainerFunc: func(config *container.Config,
|
||||
hostConfig *container.HostConfig,
|
||||
networkingConfig *network.NetworkingConfig,
|
||||
platform *specs.Platform,
|
||||
containerName string,
|
||||
) (container.ContainerCreateCreatedBody, error) {
|
||||
return container.ContainerCreateCreatedBody{}, fmt.Errorf("shouldn't try to pull image")
|
||||
},
|
||||
}, test.EnableContentTrust)
|
||||
cli.SetNotaryClient(tc.notaryFunc)
|
||||
cmd := NewCreateCommand(cli)
|
||||
cmd.SetOut(ioutil.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
err := cmd.Execute()
|
||||
assert.ErrorContains(t, err, tc.expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewCreateCommandWithWarnings(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
args []string
|
||||
warning bool
|
||||
}{
|
||||
{
|
||||
name: "container-create-without-oom-kill-disable",
|
||||
args: []string{"image:tag"},
|
||||
},
|
||||
{
|
||||
name: "container-create-oom-kill-disable-false",
|
||||
args: []string{"--oom-kill-disable=false", "image:tag"},
|
||||
},
|
||||
{
|
||||
name: "container-create-oom-kill-without-memory-limit",
|
||||
args: []string{"--oom-kill-disable", "image:tag"},
|
||||
warning: true,
|
||||
},
|
||||
{
|
||||
name: "container-create-oom-kill-true-without-memory-limit",
|
||||
args: []string{"--oom-kill-disable=true", "image:tag"},
|
||||
warning: true,
|
||||
},
|
||||
{
|
||||
name: "container-create-oom-kill-true-with-memory-limit",
|
||||
args: []string{"--oom-kill-disable=true", "--memory=100M", "image:tag"},
|
||||
},
|
||||
{
|
||||
name: "container-create-localhost-dns",
|
||||
args: []string{"--dns=127.0.0.11", "image:tag"},
|
||||
warning: true,
|
||||
},
|
||||
{
|
||||
name: "container-create-localhost-dns-ipv6",
|
||||
args: []string{"--dns=::1", "image:tag"},
|
||||
warning: true,
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
createContainerFunc: func(config *container.Config,
|
||||
hostConfig *container.HostConfig,
|
||||
networkingConfig *network.NetworkingConfig,
|
||||
platform *specs.Platform,
|
||||
containerName string,
|
||||
) (container.ContainerCreateCreatedBody, error) {
|
||||
return container.ContainerCreateCreatedBody{}, nil
|
||||
},
|
||||
})
|
||||
cmd := NewCreateCommand(cli)
|
||||
cmd.SetOut(ioutil.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
err := cmd.Execute()
|
||||
assert.NilError(t, err)
|
||||
if tc.warning {
|
||||
golden.Assert(t, cli.ErrBuffer().String(), tc.name+".golden")
|
||||
} else {
|
||||
assert.Equal(t, cli.ErrBuffer().String(), "")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateContainerWithProxyConfig(t *testing.T) {
|
||||
expected := []string{
|
||||
"HTTP_PROXY=httpProxy",
|
||||
"http_proxy=httpProxy",
|
||||
"HTTPS_PROXY=httpsProxy",
|
||||
"https_proxy=httpsProxy",
|
||||
"NO_PROXY=noProxy",
|
||||
"no_proxy=noProxy",
|
||||
"FTP_PROXY=ftpProxy",
|
||||
"ftp_proxy=ftpProxy",
|
||||
}
|
||||
sort.Strings(expected)
|
||||
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
createContainerFunc: func(config *container.Config,
|
||||
hostConfig *container.HostConfig,
|
||||
networkingConfig *network.NetworkingConfig,
|
||||
platform *specs.Platform,
|
||||
containerName string,
|
||||
) (container.ContainerCreateCreatedBody, error) {
|
||||
sort.Strings(config.Env)
|
||||
assert.DeepEqual(t, config.Env, expected)
|
||||
return container.ContainerCreateCreatedBody{}, nil
|
||||
},
|
||||
})
|
||||
cli.SetConfigFile(&configfile.ConfigFile{
|
||||
Proxies: map[string]configfile.ProxyConfig{
|
||||
"default": {
|
||||
HTTPProxy: "httpProxy",
|
||||
HTTPSProxy: "httpsProxy",
|
||||
NoProxy: "noProxy",
|
||||
FTPProxy: "ftpProxy",
|
||||
},
|
||||
},
|
||||
})
|
||||
cmd := NewCreateCommand(cli)
|
||||
cmd.SetOut(ioutil.Discard)
|
||||
cmd.SetArgs([]string{"image:tag"})
|
||||
err := cmd.Execute()
|
||||
assert.NilError(t, err)
|
||||
}
|
||||
|
||||
type fakeNotFound struct{}
|
||||
|
||||
func (f fakeNotFound) NotFound() bool { return true }
|
||||
func (f fakeNotFound) Error() string { return "error fake not found" }
|
|
@ -0,0 +1,47 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type diffOptions struct {
|
||||
container string
|
||||
}
|
||||
|
||||
// NewDiffCommand creates a new cobra.Command for `docker diff`
|
||||
func NewDiffCommand(dockerCli command.Cli) *cobra.Command {
|
||||
var opts diffOptions
|
||||
|
||||
return &cobra.Command{
|
||||
Use: "diff CONTAINER",
|
||||
Short: "Inspect changes to files or directories on a container's filesystem",
|
||||
Args: cli.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.container = args[0]
|
||||
return runDiff(dockerCli, &opts)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func runDiff(dockerCli command.Cli, opts *diffOptions) error {
|
||||
if opts.container == "" {
|
||||
return errors.New("Container name cannot be empty")
|
||||
}
|
||||
ctx := context.Background()
|
||||
|
||||
changes, err := dockerCli.Client().ContainerDiff(ctx, opts.container)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
diffCtx := formatter.Context{
|
||||
Output: dockerCli.Out(),
|
||||
Format: NewDiffFormat("{{.Type}} {{.Path}}"),
|
||||
}
|
||||
return DiffFormatWrite(diffCtx, changes)
|
||||
}
|
|
@ -0,0 +1,229 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api/types"
|
||||
apiclient "github.com/docker/docker/client"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type execOptions struct {
|
||||
detachKeys string
|
||||
interactive bool
|
||||
tty bool
|
||||
detach bool
|
||||
user string
|
||||
privileged bool
|
||||
env opts.ListOpts
|
||||
workdir string
|
||||
container string
|
||||
command []string
|
||||
envFile opts.ListOpts
|
||||
}
|
||||
|
||||
func newExecOptions() execOptions {
|
||||
return execOptions{
|
||||
env: opts.NewListOpts(opts.ValidateEnv),
|
||||
envFile: opts.NewListOpts(nil),
|
||||
}
|
||||
}
|
||||
|
||||
// NewExecCommand creates a new cobra.Command for `docker exec`
|
||||
func NewExecCommand(dockerCli command.Cli) *cobra.Command {
|
||||
options := newExecOptions()
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "exec [OPTIONS] CONTAINER COMMAND [ARG...]",
|
||||
Short: "Run a command in a running container",
|
||||
Args: cli.RequiresMinArgs(2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
options.container = args[0]
|
||||
options.command = args[1:]
|
||||
return runExec(dockerCli, options)
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.SetInterspersed(false)
|
||||
|
||||
flags.StringVarP(&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.tty, "tty", "t", false, "Allocate a pseudo-TTY")
|
||||
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.BoolVarP(&options.privileged, "privileged", "", false, "Give extended privileges to the command")
|
||||
flags.VarP(&options.env, "env", "e", "Set environment variables")
|
||||
flags.SetAnnotation("env", "version", []string{"1.25"})
|
||||
flags.Var(&options.envFile, "env-file", "Read in a file of environment variables")
|
||||
flags.SetAnnotation("env-file", "version", []string{"1.25"})
|
||||
flags.StringVarP(&options.workdir, "workdir", "w", "", "Working directory inside the container")
|
||||
flags.SetAnnotation("workdir", "version", []string{"1.35"})
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runExec(dockerCli command.Cli, options execOptions) error {
|
||||
execConfig, err := parseExec(options, dockerCli.ConfigFile())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
client := dockerCli.Client()
|
||||
|
||||
// We need to check the tty _before_ we do the ContainerExecCreate, because
|
||||
// otherwise if we error out we will leak execIDs on the server (and
|
||||
// there's no easy way to clean those up). But also in order to make "not
|
||||
// exist" errors take precedence we do a dummy inspect first.
|
||||
if _, err := client.ContainerInspect(ctx, options.container); err != nil {
|
||||
return err
|
||||
}
|
||||
if !execConfig.Detach {
|
||||
if err := dockerCli.In().CheckTty(execConfig.AttachStdin, execConfig.Tty); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
response, err := client.ContainerExecCreate(ctx, options.container, *execConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
execID := response.ID
|
||||
if execID == "" {
|
||||
return errors.New("exec ID empty")
|
||||
}
|
||||
|
||||
if execConfig.Detach {
|
||||
execStartCheck := types.ExecStartCheck{
|
||||
Detach: execConfig.Detach,
|
||||
Tty: execConfig.Tty,
|
||||
}
|
||||
return client.ContainerExecStart(ctx, execID, execStartCheck)
|
||||
}
|
||||
return interactiveExec(ctx, dockerCli, execConfig, execID)
|
||||
}
|
||||
|
||||
func interactiveExec(ctx context.Context, dockerCli command.Cli, execConfig *types.ExecConfig, execID string) error {
|
||||
// Interactive exec requested.
|
||||
var (
|
||||
out, stderr io.Writer
|
||||
in io.ReadCloser
|
||||
)
|
||||
|
||||
if execConfig.AttachStdin {
|
||||
in = dockerCli.In()
|
||||
}
|
||||
if execConfig.AttachStdout {
|
||||
out = dockerCli.Out()
|
||||
}
|
||||
if execConfig.AttachStderr {
|
||||
if execConfig.Tty {
|
||||
stderr = dockerCli.Out()
|
||||
} else {
|
||||
stderr = dockerCli.Err()
|
||||
}
|
||||
}
|
||||
|
||||
client := dockerCli.Client()
|
||||
execStartCheck := types.ExecStartCheck{
|
||||
Tty: execConfig.Tty,
|
||||
}
|
||||
resp, err := client.ContainerExecAttach(ctx, execID, execStartCheck)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Close()
|
||||
|
||||
errCh := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
defer close(errCh)
|
||||
errCh <- func() error {
|
||||
streamer := hijackedIOStreamer{
|
||||
streams: dockerCli,
|
||||
inputStream: in,
|
||||
outputStream: out,
|
||||
errorStream: stderr,
|
||||
resp: resp,
|
||||
tty: execConfig.Tty,
|
||||
detachKeys: execConfig.DetachKeys,
|
||||
}
|
||||
|
||||
return streamer.stream(ctx)
|
||||
}()
|
||||
}()
|
||||
|
||||
if execConfig.Tty && dockerCli.In().IsTerminal() {
|
||||
if err := MonitorTtySize(ctx, dockerCli, execID, true); err != nil {
|
||||
fmt.Fprintln(dockerCli.Err(), "Error monitoring TTY size:", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := <-errCh; err != nil {
|
||||
logrus.Debugf("Error hijack: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return getExecExitStatus(ctx, client, execID)
|
||||
}
|
||||
|
||||
func getExecExitStatus(ctx context.Context, client apiclient.ContainerAPIClient, execID string) error {
|
||||
resp, err := client.ContainerExecInspect(ctx, execID)
|
||||
if err != nil {
|
||||
// If we can't connect, then the daemon probably died.
|
||||
if !apiclient.IsErrConnectionFailed(err) {
|
||||
return err
|
||||
}
|
||||
return cli.StatusError{StatusCode: -1}
|
||||
}
|
||||
status := resp.ExitCode
|
||||
if status != 0 {
|
||||
return cli.StatusError{StatusCode: status}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseExec parses the specified args for the specified command and generates
|
||||
// an ExecConfig from it.
|
||||
func parseExec(execOpts execOptions, configFile *configfile.ConfigFile) (*types.ExecConfig, error) {
|
||||
execConfig := &types.ExecConfig{
|
||||
User: execOpts.user,
|
||||
Privileged: execOpts.privileged,
|
||||
Tty: execOpts.tty,
|
||||
Cmd: execOpts.command,
|
||||
Detach: execOpts.detach,
|
||||
WorkingDir: execOpts.workdir,
|
||||
}
|
||||
|
||||
// collect all the environment variables for the container
|
||||
var err error
|
||||
if execConfig.Env, err = opts.ReadKVEnvStrings(execOpts.envFile.GetAll(), execOpts.env.GetAll()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If -d is not set, attach to everything by default
|
||||
if !execOpts.detach {
|
||||
execConfig.AttachStdout = true
|
||||
execConfig.AttachStderr = true
|
||||
if execOpts.interactive {
|
||||
execConfig.AttachStdin = true
|
||||
}
|
||||
}
|
||||
|
||||
if execOpts.detachKeys != "" {
|
||||
execConfig.DetachKeys = execOpts.detachKeys
|
||||
} else {
|
||||
execConfig.DetachKeys = configFile.DetachKeys
|
||||
}
|
||||
return execConfig, nil
|
||||
}
|
|
@ -0,0 +1,274 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/pkg/errors"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
"gotest.tools/v3/fs"
|
||||
)
|
||||
|
||||
func withDefaultOpts(options execOptions) execOptions {
|
||||
options.env = opts.NewListOpts(opts.ValidateEnv)
|
||||
options.envFile = opts.NewListOpts(nil)
|
||||
if len(options.command) == 0 {
|
||||
options.command = []string{"command"}
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
func TestParseExec(t *testing.T) {
|
||||
content := `ONE=1
|
||||
TWO=2
|
||||
`
|
||||
|
||||
tmpFile := fs.NewFile(t, t.Name(), fs.WithContent(content))
|
||||
defer tmpFile.Remove()
|
||||
|
||||
testcases := []struct {
|
||||
options execOptions
|
||||
configFile configfile.ConfigFile
|
||||
expected types.ExecConfig
|
||||
}{
|
||||
{
|
||||
expected: types.ExecConfig{
|
||||
Cmd: []string{"command"},
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
},
|
||||
options: withDefaultOpts(execOptions{}),
|
||||
},
|
||||
{
|
||||
expected: types.ExecConfig{
|
||||
Cmd: []string{"command1", "command2"},
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
},
|
||||
options: withDefaultOpts(execOptions{
|
||||
command: []string{"command1", "command2"},
|
||||
}),
|
||||
},
|
||||
{
|
||||
options: withDefaultOpts(execOptions{
|
||||
interactive: true,
|
||||
tty: true,
|
||||
user: "uid",
|
||||
}),
|
||||
expected: types.ExecConfig{
|
||||
User: "uid",
|
||||
AttachStdin: true,
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
Tty: true,
|
||||
Cmd: []string{"command"},
|
||||
},
|
||||
},
|
||||
{
|
||||
options: withDefaultOpts(execOptions{detach: true}),
|
||||
expected: types.ExecConfig{
|
||||
Detach: true,
|
||||
Cmd: []string{"command"},
|
||||
},
|
||||
},
|
||||
{
|
||||
options: withDefaultOpts(execOptions{
|
||||
tty: true,
|
||||
interactive: true,
|
||||
detach: true,
|
||||
}),
|
||||
expected: types.ExecConfig{
|
||||
Detach: true,
|
||||
Tty: true,
|
||||
Cmd: []string{"command"},
|
||||
},
|
||||
},
|
||||
{
|
||||
options: withDefaultOpts(execOptions{detach: true}),
|
||||
configFile: configfile.ConfigFile{DetachKeys: "de"},
|
||||
expected: types.ExecConfig{
|
||||
Cmd: []string{"command"},
|
||||
DetachKeys: "de",
|
||||
Detach: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
options: withDefaultOpts(execOptions{
|
||||
detach: true,
|
||||
detachKeys: "ab",
|
||||
}),
|
||||
configFile: configfile.ConfigFile{DetachKeys: "de"},
|
||||
expected: types.ExecConfig{
|
||||
Cmd: []string{"command"},
|
||||
DetachKeys: "ab",
|
||||
Detach: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
expected: types.ExecConfig{
|
||||
Cmd: []string{"command"},
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
Env: []string{"ONE=1", "TWO=2"},
|
||||
},
|
||||
options: func() execOptions {
|
||||
o := withDefaultOpts(execOptions{})
|
||||
o.envFile.Set(tmpFile.Path())
|
||||
return o
|
||||
}(),
|
||||
},
|
||||
{
|
||||
expected: types.ExecConfig{
|
||||
Cmd: []string{"command"},
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
Env: []string{"ONE=1", "TWO=2", "ONE=override"},
|
||||
},
|
||||
options: func() execOptions {
|
||||
o := withDefaultOpts(execOptions{})
|
||||
o.envFile.Set(tmpFile.Path())
|
||||
o.env.Set("ONE=override")
|
||||
return o
|
||||
}(),
|
||||
},
|
||||
}
|
||||
|
||||
for _, testcase := range testcases {
|
||||
execConfig, err := parseExec(testcase.options, &testcase.configFile)
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, is.DeepEqual(testcase.expected, *execConfig))
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseExecNoSuchFile(t *testing.T) {
|
||||
execOpts := withDefaultOpts(execOptions{})
|
||||
execOpts.envFile.Set("no-such-env-file")
|
||||
execConfig, err := parseExec(execOpts, &configfile.ConfigFile{})
|
||||
assert.ErrorContains(t, err, "no-such-env-file")
|
||||
assert.Check(t, os.IsNotExist(err))
|
||||
assert.Check(t, execConfig == nil)
|
||||
}
|
||||
|
||||
func TestRunExec(t *testing.T) {
|
||||
var testcases = []struct {
|
||||
doc string
|
||||
options execOptions
|
||||
client fakeClient
|
||||
expectedError string
|
||||
expectedOut string
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
doc: "successful detach",
|
||||
options: withDefaultOpts(execOptions{
|
||||
container: "thecontainer",
|
||||
detach: true,
|
||||
}),
|
||||
client: fakeClient{execCreateFunc: execCreateWithID},
|
||||
},
|
||||
{
|
||||
doc: "inspect error",
|
||||
options: newExecOptions(),
|
||||
client: fakeClient{
|
||||
inspectFunc: func(string) (types.ContainerJSON, error) {
|
||||
return types.ContainerJSON{}, errors.New("failed inspect")
|
||||
},
|
||||
},
|
||||
expectedError: "failed inspect",
|
||||
},
|
||||
{
|
||||
doc: "missing exec ID",
|
||||
options: newExecOptions(),
|
||||
expectedError: "exec ID empty",
|
||||
},
|
||||
}
|
||||
|
||||
for _, testcase := range testcases {
|
||||
t.Run(testcase.doc, func(t *testing.T) {
|
||||
cli := test.NewFakeCli(&testcase.client)
|
||||
|
||||
err := runExec(cli, testcase.options)
|
||||
if testcase.expectedError != "" {
|
||||
assert.ErrorContains(t, err, testcase.expectedError)
|
||||
} else {
|
||||
if !assert.Check(t, err) {
|
||||
return
|
||||
}
|
||||
}
|
||||
assert.Check(t, is.Equal(testcase.expectedOut, cli.OutBuffer().String()))
|
||||
assert.Check(t, is.Equal(testcase.expectedErr, cli.ErrBuffer().String()))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func execCreateWithID(_ string, _ types.ExecConfig) (types.IDResponse, error) {
|
||||
return types.IDResponse{ID: "execid"}, nil
|
||||
}
|
||||
|
||||
func TestGetExecExitStatus(t *testing.T) {
|
||||
execID := "the exec id"
|
||||
expecatedErr := errors.New("unexpected error")
|
||||
|
||||
testcases := []struct {
|
||||
inspectError error
|
||||
exitCode int
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
inspectError: nil,
|
||||
exitCode: 0,
|
||||
},
|
||||
{
|
||||
inspectError: expecatedErr,
|
||||
expectedError: expecatedErr,
|
||||
},
|
||||
{
|
||||
exitCode: 15,
|
||||
expectedError: cli.StatusError{StatusCode: 15},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testcase := range testcases {
|
||||
client := &fakeClient{
|
||||
execInspectFunc: func(id string) (types.ContainerExecInspect, error) {
|
||||
assert.Check(t, is.Equal(execID, id))
|
||||
return types.ContainerExecInspect{ExitCode: testcase.exitCode}, testcase.inspectError
|
||||
},
|
||||
}
|
||||
err := getExecExitStatus(context.Background(), client, execID)
|
||||
assert.Check(t, is.Equal(testcase.expectedError, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewExecCommandErrors(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
args []string
|
||||
expectedError string
|
||||
containerInspectFunc func(img string) (types.ContainerJSON, error)
|
||||
}{
|
||||
{
|
||||
name: "client-error",
|
||||
args: []string{"5cb5bb5e4a3b", "-t", "-i", "bash"},
|
||||
expectedError: "something went wrong",
|
||||
containerInspectFunc: func(containerID string) (types.ContainerJSON, error) {
|
||||
return types.ContainerJSON{}, errors.Errorf("something went wrong")
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
cli := test.NewFakeCli(&fakeClient{inspectFunc: tc.containerInspectFunc})
|
||||
cmd := NewExecCommand(cli)
|
||||
cmd.SetOut(ioutil.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type exportOptions struct {
|
||||
container string
|
||||
output string
|
||||
}
|
||||
|
||||
// NewExportCommand creates a new `docker export` command
|
||||
func NewExportCommand(dockerCli command.Cli) *cobra.Command {
|
||||
var opts exportOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "export [OPTIONS] CONTAINER",
|
||||
Short: "Export a container's filesystem as a tar archive",
|
||||
Args: cli.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.container = args[0]
|
||||
return runExport(dockerCli, opts)
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
|
||||
flags.StringVarP(&opts.output, "output", "o", "", "Write to a file, instead of STDOUT")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runExport(dockerCli command.Cli, opts exportOptions) error {
|
||||
if opts.output == "" && dockerCli.Out().IsTerminal() {
|
||||
return errors.New("cowardly refusing to save to a terminal. Use the -o flag or redirect")
|
||||
}
|
||||
|
||||
if err := command.ValidateOutputPath(opts.output); err != nil {
|
||||
return errors.Wrap(err, "failed to export container")
|
||||
}
|
||||
|
||||
clnt := dockerCli.Client()
|
||||
|
||||
responseBody, err := clnt.ContainerExport(context.Background(), opts.container)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer responseBody.Close()
|
||||
|
||||
if opts.output == "" {
|
||||
_, err := io.Copy(dockerCli.Out(), responseBody)
|
||||
return err
|
||||
}
|
||||
|
||||
return command.CopyToFile(opts.output, responseBody)
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"gotest.tools/v3/assert"
|
||||
"gotest.tools/v3/fs"
|
||||
)
|
||||
|
||||
func TestContainerExportOutputToFile(t *testing.T) {
|
||||
dir := fs.NewDir(t, "export-test")
|
||||
defer dir.Remove()
|
||||
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
containerExportFunc: func(container string) (io.ReadCloser, error) {
|
||||
return ioutil.NopCloser(strings.NewReader("bar")), nil
|
||||
},
|
||||
})
|
||||
cmd := NewExportCommand(cli)
|
||||
cmd.SetOut(ioutil.Discard)
|
||||
cmd.SetArgs([]string{"-o", dir.Join("foo"), "container"})
|
||||
assert.NilError(t, cmd.Execute())
|
||||
|
||||
expected := fs.Expected(t,
|
||||
fs.WithFile("foo", "bar", fs.MatchAnyFileMode),
|
||||
)
|
||||
|
||||
assert.Assert(t, fs.Equal(dir.Path(), expected))
|
||||
}
|
||||
|
||||
func TestContainerExportOutputToIrregularFile(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
containerExportFunc: func(container string) (io.ReadCloser, error) {
|
||||
return ioutil.NopCloser(strings.NewReader("foo")), nil
|
||||
},
|
||||
})
|
||||
cmd := NewExportCommand(cli)
|
||||
cmd.SetOut(ioutil.Discard)
|
||||
cmd.SetArgs([]string{"-o", "/dev/random", "container"})
|
||||
|
||||
err := cmd.Execute()
|
||||
assert.Assert(t, err != nil)
|
||||
expected := `"/dev/random" must be a directory or a regular file`
|
||||
assert.ErrorContains(t, err, expected)
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/pkg/archive"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultDiffTableFormat = "table {{.Type}}\t{{.Path}}"
|
||||
|
||||
changeTypeHeader = "CHANGE TYPE"
|
||||
pathHeader = "PATH"
|
||||
)
|
||||
|
||||
// NewDiffFormat returns a format for use with a diff Context
|
||||
func NewDiffFormat(source string) formatter.Format {
|
||||
switch source {
|
||||
case formatter.TableFormatKey:
|
||||
return defaultDiffTableFormat
|
||||
}
|
||||
return formatter.Format(source)
|
||||
}
|
||||
|
||||
// DiffFormatWrite writes formatted diff using the Context
|
||||
func DiffFormatWrite(ctx formatter.Context, changes []container.ContainerChangeResponseItem) error {
|
||||
|
||||
render := func(format func(subContext formatter.SubContext) error) error {
|
||||
for _, change := range changes {
|
||||
if err := format(&diffContext{c: change}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return ctx.Write(newDiffContext(), render)
|
||||
}
|
||||
|
||||
type diffContext struct {
|
||||
formatter.HeaderContext
|
||||
c container.ContainerChangeResponseItem
|
||||
}
|
||||
|
||||
func newDiffContext() *diffContext {
|
||||
diffCtx := diffContext{}
|
||||
diffCtx.Header = formatter.SubHeaderContext{
|
||||
"Type": changeTypeHeader,
|
||||
"Path": pathHeader,
|
||||
}
|
||||
return &diffCtx
|
||||
}
|
||||
|
||||
func (d *diffContext) MarshalJSON() ([]byte, error) {
|
||||
return formatter.MarshalJSON(d)
|
||||
}
|
||||
|
||||
func (d *diffContext) Type() string {
|
||||
var kind string
|
||||
switch d.c.Kind {
|
||||
case archive.ChangeModify:
|
||||
kind = "C"
|
||||
case archive.ChangeAdd:
|
||||
kind = "A"
|
||||
case archive.ChangeDelete:
|
||||
kind = "D"
|
||||
}
|
||||
return kind
|
||||
|
||||
}
|
||||
|
||||
func (d *diffContext) Path() string {
|
||||
return d.c.Path
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/pkg/archive"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestDiffContextFormatWrite(t *testing.T) {
|
||||
// Check default output format (verbose and non-verbose mode) for table headers
|
||||
cases := []struct {
|
||||
context formatter.Context
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
formatter.Context{Format: NewDiffFormat("table")},
|
||||
`CHANGE TYPE PATH
|
||||
C /var/log/app.log
|
||||
A /usr/app/app.js
|
||||
D /usr/app/old_app.js
|
||||
`,
|
||||
},
|
||||
{
|
||||
formatter.Context{Format: NewDiffFormat("table {{.Path}}")},
|
||||
`PATH
|
||||
/var/log/app.log
|
||||
/usr/app/app.js
|
||||
/usr/app/old_app.js
|
||||
`,
|
||||
},
|
||||
{
|
||||
formatter.Context{Format: NewDiffFormat("{{.Type}}: {{.Path}}")},
|
||||
`C: /var/log/app.log
|
||||
A: /usr/app/app.js
|
||||
D: /usr/app/old_app.js
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
diffs := []container.ContainerChangeResponseItem{
|
||||
{Kind: archive.ChangeModify, Path: "/var/log/app.log"},
|
||||
{Kind: archive.ChangeAdd, Path: "/usr/app/app.js"},
|
||||
{Kind: archive.ChangeDelete, Path: "/usr/app/old_app.js"},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(string(tc.context.Format), func(t *testing.T) {
|
||||
out := bytes.NewBufferString("")
|
||||
tc.context.Output = out
|
||||
err := DiffFormatWrite(tc.context, diffs)
|
||||
if err != nil {
|
||||
assert.Error(t, err, tc.expected)
|
||||
} else {
|
||||
assert.Equal(t, out.String(), tc.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,225 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
units "github.com/docker/go-units"
|
||||
)
|
||||
|
||||
const (
|
||||
winOSType = "windows"
|
||||
defaultStatsTableFormat = "table {{.ID}}\t{{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}\t{{.NetIO}}\t{{.BlockIO}}\t{{.PIDs}}"
|
||||
winDefaultStatsTableFormat = "table {{.ID}}\t{{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.NetIO}}\t{{.BlockIO}}"
|
||||
|
||||
containerHeader = "CONTAINER"
|
||||
cpuPercHeader = "CPU %"
|
||||
netIOHeader = "NET I/O"
|
||||
blockIOHeader = "BLOCK I/O"
|
||||
memPercHeader = "MEM %" // Used only on Linux
|
||||
winMemUseHeader = "PRIV WORKING SET" // Used only on Windows
|
||||
memUseHeader = "MEM USAGE / LIMIT" // Used only on Linux
|
||||
pidsHeader = "PIDS" // Used only on Linux
|
||||
)
|
||||
|
||||
// StatsEntry represents represents the statistics data collected from a container
|
||||
type StatsEntry struct {
|
||||
Container string
|
||||
Name string
|
||||
ID string
|
||||
CPUPercentage float64
|
||||
Memory float64 // On Windows this is the private working set
|
||||
MemoryLimit float64 // Not used on Windows
|
||||
MemoryPercentage float64 // Not used on Windows
|
||||
NetworkRx float64
|
||||
NetworkTx float64
|
||||
BlockRead float64
|
||||
BlockWrite float64
|
||||
PidsCurrent uint64 // Not used on Windows
|
||||
IsInvalid bool
|
||||
}
|
||||
|
||||
// Stats represents an entity to store containers statistics synchronously
|
||||
type Stats struct {
|
||||
mutex sync.Mutex
|
||||
StatsEntry
|
||||
err error
|
||||
}
|
||||
|
||||
// GetError returns the container statistics error.
|
||||
// This is used to determine whether the statistics are valid or not
|
||||
func (cs *Stats) GetError() error {
|
||||
cs.mutex.Lock()
|
||||
defer cs.mutex.Unlock()
|
||||
return cs.err
|
||||
}
|
||||
|
||||
// SetErrorAndReset zeroes all the container statistics and store the error.
|
||||
// It is used when receiving time out error during statistics collecting to reduce lock overhead
|
||||
func (cs *Stats) SetErrorAndReset(err error) {
|
||||
cs.mutex.Lock()
|
||||
defer cs.mutex.Unlock()
|
||||
cs.CPUPercentage = 0
|
||||
cs.Memory = 0
|
||||
cs.MemoryPercentage = 0
|
||||
cs.MemoryLimit = 0
|
||||
cs.NetworkRx = 0
|
||||
cs.NetworkTx = 0
|
||||
cs.BlockRead = 0
|
||||
cs.BlockWrite = 0
|
||||
cs.PidsCurrent = 0
|
||||
cs.err = err
|
||||
cs.IsInvalid = true
|
||||
}
|
||||
|
||||
// SetError sets container statistics error
|
||||
func (cs *Stats) SetError(err error) {
|
||||
cs.mutex.Lock()
|
||||
defer cs.mutex.Unlock()
|
||||
cs.err = err
|
||||
if err != nil {
|
||||
cs.IsInvalid = true
|
||||
}
|
||||
}
|
||||
|
||||
// SetStatistics set the container statistics
|
||||
func (cs *Stats) SetStatistics(s StatsEntry) {
|
||||
cs.mutex.Lock()
|
||||
defer cs.mutex.Unlock()
|
||||
s.Container = cs.Container
|
||||
cs.StatsEntry = s
|
||||
}
|
||||
|
||||
// GetStatistics returns container statistics with other meta data such as the container name
|
||||
func (cs *Stats) GetStatistics() StatsEntry {
|
||||
cs.mutex.Lock()
|
||||
defer cs.mutex.Unlock()
|
||||
return cs.StatsEntry
|
||||
}
|
||||
|
||||
// NewStatsFormat returns a format for rendering an CStatsContext
|
||||
func NewStatsFormat(source, osType string) formatter.Format {
|
||||
if source == formatter.TableFormatKey {
|
||||
if osType == winOSType {
|
||||
return winDefaultStatsTableFormat
|
||||
}
|
||||
return defaultStatsTableFormat
|
||||
}
|
||||
return formatter.Format(source)
|
||||
}
|
||||
|
||||
// NewStats returns a new Stats entity and sets in it the given name
|
||||
func NewStats(container string) *Stats {
|
||||
return &Stats{StatsEntry: StatsEntry{Container: container}}
|
||||
}
|
||||
|
||||
// statsFormatWrite renders the context for a list of containers statistics
|
||||
func statsFormatWrite(ctx formatter.Context, Stats []StatsEntry, osType string, trunc bool) error {
|
||||
render := func(format func(subContext formatter.SubContext) error) error {
|
||||
for _, cstats := range Stats {
|
||||
statsCtx := &statsContext{
|
||||
s: cstats,
|
||||
os: osType,
|
||||
trunc: trunc,
|
||||
}
|
||||
if err := format(statsCtx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
memUsage := memUseHeader
|
||||
if osType == winOSType {
|
||||
memUsage = winMemUseHeader
|
||||
}
|
||||
statsCtx := statsContext{}
|
||||
statsCtx.Header = formatter.SubHeaderContext{
|
||||
"Container": containerHeader,
|
||||
"Name": formatter.NameHeader,
|
||||
"ID": formatter.ContainerIDHeader,
|
||||
"CPUPerc": cpuPercHeader,
|
||||
"MemUsage": memUsage,
|
||||
"MemPerc": memPercHeader,
|
||||
"NetIO": netIOHeader,
|
||||
"BlockIO": blockIOHeader,
|
||||
"PIDs": pidsHeader,
|
||||
}
|
||||
statsCtx.os = osType
|
||||
return ctx.Write(&statsCtx, render)
|
||||
}
|
||||
|
||||
type statsContext struct {
|
||||
formatter.HeaderContext
|
||||
s StatsEntry
|
||||
os string
|
||||
trunc bool
|
||||
}
|
||||
|
||||
func (c *statsContext) MarshalJSON() ([]byte, error) {
|
||||
return formatter.MarshalJSON(c)
|
||||
}
|
||||
|
||||
func (c *statsContext) Container() string {
|
||||
return c.s.Container
|
||||
}
|
||||
|
||||
func (c *statsContext) Name() string {
|
||||
if len(c.s.Name) > 1 {
|
||||
return c.s.Name[1:]
|
||||
}
|
||||
return "--"
|
||||
}
|
||||
|
||||
func (c *statsContext) ID() string {
|
||||
if c.trunc {
|
||||
return stringid.TruncateID(c.s.ID)
|
||||
}
|
||||
return c.s.ID
|
||||
}
|
||||
|
||||
func (c *statsContext) CPUPerc() string {
|
||||
if c.s.IsInvalid {
|
||||
return fmt.Sprintf("--")
|
||||
}
|
||||
return fmt.Sprintf("%.2f%%", c.s.CPUPercentage)
|
||||
}
|
||||
|
||||
func (c *statsContext) MemUsage() string {
|
||||
if c.s.IsInvalid {
|
||||
return fmt.Sprintf("-- / --")
|
||||
}
|
||||
if c.os == winOSType {
|
||||
return units.BytesSize(c.s.Memory)
|
||||
}
|
||||
return fmt.Sprintf("%s / %s", units.BytesSize(c.s.Memory), units.BytesSize(c.s.MemoryLimit))
|
||||
}
|
||||
|
||||
func (c *statsContext) MemPerc() string {
|
||||
if c.s.IsInvalid || c.os == winOSType {
|
||||
return fmt.Sprintf("--")
|
||||
}
|
||||
return fmt.Sprintf("%.2f%%", c.s.MemoryPercentage)
|
||||
}
|
||||
|
||||
func (c *statsContext) NetIO() string {
|
||||
if c.s.IsInvalid {
|
||||
return fmt.Sprintf("--")
|
||||
}
|
||||
return fmt.Sprintf("%s / %s", units.HumanSizeWithPrecision(c.s.NetworkRx, 3), units.HumanSizeWithPrecision(c.s.NetworkTx, 3))
|
||||
}
|
||||
|
||||
func (c *statsContext) BlockIO() string {
|
||||
if c.s.IsInvalid {
|
||||
return fmt.Sprintf("--")
|
||||
}
|
||||
return fmt.Sprintf("%s / %s", units.HumanSizeWithPrecision(c.s.BlockRead, 3), units.HumanSizeWithPrecision(c.s.BlockWrite, 3))
|
||||
}
|
||||
|
||||
func (c *statsContext) PIDs() string {
|
||||
if c.s.IsInvalid || c.os == winOSType {
|
||||
return fmt.Sprintf("--")
|
||||
}
|
||||
return fmt.Sprintf("%d", c.s.PidsCurrent)
|
||||
}
|
|
@ -0,0 +1,312 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
|
||||
func TestContainerStatsContext(t *testing.T) {
|
||||
containerID := stringid.GenerateRandomID()
|
||||
|
||||
var ctx statsContext
|
||||
tt := []struct {
|
||||
stats StatsEntry
|
||||
osType string
|
||||
expValue string
|
||||
expHeader string
|
||||
call func() string
|
||||
}{
|
||||
{StatsEntry{Container: containerID}, "", containerID, containerHeader, ctx.Container},
|
||||
{StatsEntry{CPUPercentage: 5.5}, "", "5.50%", cpuPercHeader, ctx.CPUPerc},
|
||||
{StatsEntry{CPUPercentage: 5.5, IsInvalid: true}, "", "--", cpuPercHeader, ctx.CPUPerc},
|
||||
{StatsEntry{NetworkRx: 0.31, NetworkTx: 12.3}, "", "0.31B / 12.3B", netIOHeader, ctx.NetIO},
|
||||
{StatsEntry{NetworkRx: 0.31, NetworkTx: 12.3, IsInvalid: true}, "", "--", netIOHeader, ctx.NetIO},
|
||||
{StatsEntry{BlockRead: 0.1, BlockWrite: 2.3}, "", "0.1B / 2.3B", blockIOHeader, ctx.BlockIO},
|
||||
{StatsEntry{BlockRead: 0.1, BlockWrite: 2.3, IsInvalid: true}, "", "--", blockIOHeader, ctx.BlockIO},
|
||||
{StatsEntry{MemoryPercentage: 10.2}, "", "10.20%", memPercHeader, ctx.MemPerc},
|
||||
{StatsEntry{MemoryPercentage: 10.2, IsInvalid: true}, "", "--", memPercHeader, ctx.MemPerc},
|
||||
{StatsEntry{MemoryPercentage: 10.2}, "windows", "--", memPercHeader, ctx.MemPerc},
|
||||
{StatsEntry{Memory: 24, MemoryLimit: 30}, "", "24B / 30B", memUseHeader, ctx.MemUsage},
|
||||
{StatsEntry{Memory: 24, MemoryLimit: 30, IsInvalid: true}, "", "-- / --", memUseHeader, ctx.MemUsage},
|
||||
{StatsEntry{Memory: 24, MemoryLimit: 30}, "windows", "24B", winMemUseHeader, ctx.MemUsage},
|
||||
{StatsEntry{PidsCurrent: 10}, "", "10", pidsHeader, ctx.PIDs},
|
||||
{StatsEntry{PidsCurrent: 10, IsInvalid: true}, "", "--", pidsHeader, ctx.PIDs},
|
||||
{StatsEntry{PidsCurrent: 10}, "windows", "--", pidsHeader, ctx.PIDs},
|
||||
}
|
||||
|
||||
for _, te := range tt {
|
||||
ctx = statsContext{s: te.stats, os: te.osType}
|
||||
if v := te.call(); v != te.expValue {
|
||||
t.Fatalf("Expected %q, got %q", te.expValue, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestContainerStatsContextWrite(t *testing.T) {
|
||||
tt := []struct {
|
||||
context formatter.Context
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
formatter.Context{Format: "{{InvalidFunction}}"},
|
||||
`Template parsing error: template: :1: function "InvalidFunction" not defined
|
||||
`,
|
||||
},
|
||||
{
|
||||
formatter.Context{Format: "{{nil}}"},
|
||||
`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command
|
||||
`,
|
||||
},
|
||||
{
|
||||
formatter.Context{Format: "table {{.MemUsage}}"},
|
||||
`MEM USAGE / LIMIT
|
||||
20B / 20B
|
||||
-- / --
|
||||
`,
|
||||
},
|
||||
{
|
||||
formatter.Context{Format: "{{.Container}} {{.ID}} {{.Name}}"},
|
||||
`container1 abcdef foo
|
||||
container2 --
|
||||
`,
|
||||
},
|
||||
{
|
||||
formatter.Context{Format: "{{.Container}} {{.CPUPerc}}"},
|
||||
`container1 20.00%
|
||||
container2 --
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, te := range tt {
|
||||
stats := []StatsEntry{
|
||||
{
|
||||
Container: "container1",
|
||||
ID: "abcdef",
|
||||
Name: "/foo",
|
||||
CPUPercentage: 20,
|
||||
Memory: 20,
|
||||
MemoryLimit: 20,
|
||||
MemoryPercentage: 20,
|
||||
NetworkRx: 20,
|
||||
NetworkTx: 20,
|
||||
BlockRead: 20,
|
||||
BlockWrite: 20,
|
||||
PidsCurrent: 2,
|
||||
IsInvalid: false,
|
||||
},
|
||||
{
|
||||
Container: "container2",
|
||||
CPUPercentage: 30,
|
||||
Memory: 30,
|
||||
MemoryLimit: 30,
|
||||
MemoryPercentage: 30,
|
||||
NetworkRx: 30,
|
||||
NetworkTx: 30,
|
||||
BlockRead: 30,
|
||||
BlockWrite: 30,
|
||||
PidsCurrent: 3,
|
||||
IsInvalid: true,
|
||||
},
|
||||
}
|
||||
var out bytes.Buffer
|
||||
te.context.Output = &out
|
||||
err := statsFormatWrite(te.context, stats, "linux", false)
|
||||
if err != nil {
|
||||
assert.Error(t, err, te.expected)
|
||||
} else {
|
||||
assert.Check(t, is.Equal(te.expected, out.String()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestContainerStatsContextWriteWindows(t *testing.T) {
|
||||
cases := []struct {
|
||||
context formatter.Context
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
formatter.Context{Format: "table {{.MemUsage}}"},
|
||||
`PRIV WORKING SET
|
||||
20B
|
||||
-- / --
|
||||
`,
|
||||
},
|
||||
{
|
||||
formatter.Context{Format: "{{.Container}} {{.CPUPerc}}"},
|
||||
`container1 20.00%
|
||||
container2 --
|
||||
`,
|
||||
},
|
||||
{
|
||||
formatter.Context{Format: "{{.Container}} {{.MemPerc}} {{.PIDs}}"},
|
||||
`container1 -- --
|
||||
container2 -- --
|
||||
`,
|
||||
},
|
||||
}
|
||||
stats := []StatsEntry{
|
||||
{
|
||||
Container: "container1",
|
||||
CPUPercentage: 20,
|
||||
Memory: 20,
|
||||
MemoryLimit: 20,
|
||||
MemoryPercentage: 20,
|
||||
NetworkRx: 20,
|
||||
NetworkTx: 20,
|
||||
BlockRead: 20,
|
||||
BlockWrite: 20,
|
||||
PidsCurrent: 2,
|
||||
IsInvalid: false,
|
||||
},
|
||||
{
|
||||
Container: "container2",
|
||||
CPUPercentage: 30,
|
||||
Memory: 30,
|
||||
MemoryLimit: 30,
|
||||
MemoryPercentage: 30,
|
||||
NetworkRx: 30,
|
||||
NetworkTx: 30,
|
||||
BlockRead: 30,
|
||||
BlockWrite: 30,
|
||||
PidsCurrent: 3,
|
||||
IsInvalid: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(string(tc.context.Format), func(t *testing.T) {
|
||||
var out bytes.Buffer
|
||||
tc.context.Output = &out
|
||||
err := statsFormatWrite(tc.context, stats, "windows", false)
|
||||
if err != nil {
|
||||
assert.Error(t, err, tc.expected)
|
||||
} else {
|
||||
assert.Equal(t, out.String(), tc.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestContainerStatsContextWriteWithNoStats(t *testing.T) {
|
||||
var out bytes.Buffer
|
||||
|
||||
cases := []struct {
|
||||
context formatter.Context
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
formatter.Context{
|
||||
Format: "{{.Container}}",
|
||||
Output: &out,
|
||||
},
|
||||
"",
|
||||
},
|
||||
{
|
||||
formatter.Context{
|
||||
Format: "table {{.Container}}",
|
||||
Output: &out,
|
||||
},
|
||||
"CONTAINER\n",
|
||||
},
|
||||
{
|
||||
formatter.Context{
|
||||
Format: "table {{.Container}}\t{{.CPUPerc}}",
|
||||
Output: &out,
|
||||
},
|
||||
"CONTAINER CPU %\n",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(string(tc.context.Format), func(t *testing.T) {
|
||||
err := statsFormatWrite(tc.context, []StatsEntry{}, "linux", false)
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, out.String(), tc.expected)
|
||||
// Clean buffer
|
||||
out.Reset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestContainerStatsContextWriteWithNoStatsWindows(t *testing.T) {
|
||||
var out bytes.Buffer
|
||||
|
||||
cases := []struct {
|
||||
context formatter.Context
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
formatter.Context{
|
||||
Format: "{{.Container}}",
|
||||
Output: &out,
|
||||
},
|
||||
"",
|
||||
},
|
||||
{
|
||||
formatter.Context{
|
||||
Format: "table {{.Container}}\t{{.MemUsage}}",
|
||||
Output: &out,
|
||||
},
|
||||
"CONTAINER PRIV WORKING SET\n",
|
||||
},
|
||||
{
|
||||
formatter.Context{
|
||||
Format: "table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}",
|
||||
Output: &out,
|
||||
},
|
||||
"CONTAINER CPU % PRIV WORKING SET\n",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(string(tc.context.Format), func(t *testing.T) {
|
||||
err := statsFormatWrite(tc.context, []StatsEntry{}, "windows", false)
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, out.String(), tc.expected)
|
||||
out.Reset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestContainerStatsContextWriteTrunc(t *testing.T) {
|
||||
var out bytes.Buffer
|
||||
|
||||
contexts := []struct {
|
||||
context formatter.Context
|
||||
trunc bool
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
formatter.Context{
|
||||
Format: "{{.ID}}",
|
||||
Output: &out,
|
||||
},
|
||||
false,
|
||||
"b95a83497c9161c9b444e3d70e1a9dfba0c1840d41720e146a95a08ebf938afc\n",
|
||||
},
|
||||
{
|
||||
formatter.Context{
|
||||
Format: "{{.ID}}",
|
||||
Output: &out,
|
||||
},
|
||||
true,
|
||||
"b95a83497c91\n",
|
||||
},
|
||||
}
|
||||
|
||||
for _, context := range contexts {
|
||||
statsFormatWrite(context.context, []StatsEntry{{ID: "b95a83497c9161c9b444e3d70e1a9dfba0c1840d41720e146a95a08ebf938afc"}}, "linux", context.trunc)
|
||||
assert.Check(t, is.Equal(context.expected, out.String()))
|
||||
// Clean buffer
|
||||
out.Reset()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,208 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"runtime"
|
||||
"sync"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/pkg/ioutils"
|
||||
"github.com/docker/docker/pkg/stdcopy"
|
||||
"github.com/moby/term"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// The default escape key sequence: ctrl-p, ctrl-q
|
||||
// TODO: This could be moved to `pkg/term`.
|
||||
var defaultEscapeKeys = []byte{16, 17}
|
||||
|
||||
// A hijackedIOStreamer handles copying input to and output from streams to the
|
||||
// connection.
|
||||
type hijackedIOStreamer struct {
|
||||
streams command.Streams
|
||||
inputStream io.ReadCloser
|
||||
outputStream io.Writer
|
||||
errorStream io.Writer
|
||||
|
||||
resp types.HijackedResponse
|
||||
|
||||
tty bool
|
||||
detachKeys string
|
||||
}
|
||||
|
||||
// stream handles setting up the IO and then begins streaming stdin/stdout
|
||||
// to/from the hijacked connection, blocking until it is either done reading
|
||||
// output, the user inputs the detach key sequence when in TTY mode, or when
|
||||
// the given context is cancelled.
|
||||
func (h *hijackedIOStreamer) stream(ctx context.Context) error {
|
||||
restoreInput, err := h.setupInput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to setup input stream: %s", err)
|
||||
}
|
||||
|
||||
defer restoreInput()
|
||||
|
||||
outputDone := h.beginOutputStream(restoreInput)
|
||||
inputDone, detached := h.beginInputStream(restoreInput)
|
||||
|
||||
select {
|
||||
case err := <-outputDone:
|
||||
return err
|
||||
case <-inputDone:
|
||||
// Input stream has closed.
|
||||
if h.outputStream != nil || h.errorStream != nil {
|
||||
// Wait for output to complete streaming.
|
||||
select {
|
||||
case err := <-outputDone:
|
||||
return err
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
case err := <-detached:
|
||||
// Got a detach key sequence.
|
||||
return err
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
func (h *hijackedIOStreamer) setupInput() (restore func(), err error) {
|
||||
if h.inputStream == nil || !h.tty {
|
||||
// No need to setup input TTY.
|
||||
// The restore func is a nop.
|
||||
return func() {}, nil
|
||||
}
|
||||
|
||||
if err := setRawTerminal(h.streams); err != nil {
|
||||
return nil, fmt.Errorf("unable to set IO streams as raw terminal: %s", err)
|
||||
}
|
||||
|
||||
// Use sync.Once so we may call restore multiple times but ensure we
|
||||
// only restore the terminal once.
|
||||
var restoreOnce sync.Once
|
||||
restore = func() {
|
||||
restoreOnce.Do(func() {
|
||||
restoreTerminal(h.streams, h.inputStream)
|
||||
})
|
||||
}
|
||||
|
||||
// Wrap the input to detect detach escape sequence.
|
||||
// Use default escape keys if an invalid sequence is given.
|
||||
escapeKeys := defaultEscapeKeys
|
||||
if h.detachKeys != "" {
|
||||
customEscapeKeys, err := term.ToBytes(h.detachKeys)
|
||||
if err != nil {
|
||||
logrus.Warnf("invalid detach escape keys, using default: %s", err)
|
||||
} else {
|
||||
escapeKeys = customEscapeKeys
|
||||
}
|
||||
}
|
||||
|
||||
h.inputStream = ioutils.NewReadCloserWrapper(term.NewEscapeProxy(h.inputStream, escapeKeys), h.inputStream.Close)
|
||||
|
||||
return restore, nil
|
||||
}
|
||||
|
||||
func (h *hijackedIOStreamer) beginOutputStream(restoreInput func()) <-chan error {
|
||||
if h.outputStream == nil && h.errorStream == nil {
|
||||
// There is no need to copy output.
|
||||
return nil
|
||||
}
|
||||
|
||||
outputDone := make(chan error)
|
||||
go func() {
|
||||
var err error
|
||||
|
||||
// When TTY is ON, use regular copy
|
||||
if h.outputStream != nil && h.tty {
|
||||
_, err = io.Copy(h.outputStream, h.resp.Reader)
|
||||
// We should restore the terminal as soon as possible
|
||||
// once the connection ends so any following print
|
||||
// messages will be in normal type.
|
||||
restoreInput()
|
||||
} else {
|
||||
_, err = stdcopy.StdCopy(h.outputStream, h.errorStream, h.resp.Reader)
|
||||
}
|
||||
|
||||
logrus.Debug("[hijack] End of stdout")
|
||||
|
||||
if err != nil {
|
||||
logrus.Debugf("Error receiveStdout: %s", err)
|
||||
}
|
||||
|
||||
outputDone <- err
|
||||
}()
|
||||
|
||||
return outputDone
|
||||
}
|
||||
|
||||
func (h *hijackedIOStreamer) beginInputStream(restoreInput func()) (doneC <-chan struct{}, detachedC <-chan error) {
|
||||
inputDone := make(chan struct{})
|
||||
detached := make(chan error)
|
||||
|
||||
go func() {
|
||||
if h.inputStream != nil {
|
||||
_, err := io.Copy(h.resp.Conn, h.inputStream)
|
||||
// We should restore the terminal as soon as possible
|
||||
// once the connection ends so any following print
|
||||
// messages will be in normal type.
|
||||
restoreInput()
|
||||
|
||||
logrus.Debug("[hijack] End of stdin")
|
||||
|
||||
if _, ok := err.(term.EscapeError); ok {
|
||||
detached <- err
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
// This error will also occur on the receive
|
||||
// side (from stdout) where it will be
|
||||
// propagated back to the caller.
|
||||
logrus.Debugf("Error sendStdin: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := h.resp.CloseWrite(); err != nil {
|
||||
logrus.Debugf("Couldn't send EOF: %s", err)
|
||||
}
|
||||
|
||||
close(inputDone)
|
||||
}()
|
||||
|
||||
return inputDone, detached
|
||||
}
|
||||
|
||||
func setRawTerminal(streams command.Streams) error {
|
||||
if err := streams.In().SetRawTerminal(); err != nil {
|
||||
return err
|
||||
}
|
||||
return streams.Out().SetRawTerminal()
|
||||
}
|
||||
|
||||
// nolint: unparam
|
||||
func restoreTerminal(streams command.Streams, in io.Closer) error {
|
||||
streams.In().RestoreTerminal()
|
||||
streams.Out().RestoreTerminal()
|
||||
// WARNING: DO NOT REMOVE THE OS CHECKS !!!
|
||||
// For some reason this Close call blocks on darwin..
|
||||
// As the client exits right after, simply discard the close
|
||||
// until we find a better solution.
|
||||
//
|
||||
// This can also cause the client on Windows to get stuck in Win32 CloseHandle()
|
||||
// in some cases. See https://github.com/docker/docker/issues/28267#issuecomment-288237442
|
||||
// Tracked internally at Microsoft by VSO #11352156. In the
|
||||
// Windows case, you hit this if you are using the native/v2 console,
|
||||
// not the "legacy" console, and you start the client in a new window. eg
|
||||
// `start docker run --rm -it microsoft/nanoserver cmd /s /c echo foobar`
|
||||
// will hang. Remove start, and it won't repro.
|
||||
if in != nil && runtime.GOOS != "darwin" && runtime.GOOS != "windows" {
|
||||
return in.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/inspect"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type inspectOptions struct {
|
||||
format string
|
||||
size bool
|
||||
refs []string
|
||||
}
|
||||
|
||||
// newInspectCommand creates a new cobra.Command for `docker container inspect`
|
||||
func newInspectCommand(dockerCli command.Cli) *cobra.Command {
|
||||
var opts inspectOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "inspect [OPTIONS] CONTAINER [CONTAINER...]",
|
||||
Short: "Display detailed information on one or more containers",
|
||||
Args: cli.RequiresMinArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.refs = args
|
||||
return runInspect(dockerCli, opts)
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.StringVarP(&opts.format, "format", "f", "", "Format the output using the given Go template")
|
||||
flags.BoolVarP(&opts.size, "size", "s", false, "Display total file sizes")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runInspect(dockerCli command.Cli, opts inspectOptions) error {
|
||||
client := dockerCli.Client()
|
||||
ctx := context.Background()
|
||||
|
||||
getRefFunc := func(ref string) (interface{}, []byte, error) {
|
||||
return client.ContainerInspectWithRaw(ctx, ref, opts.size)
|
||||
}
|
||||
return inspect.Inspect(dockerCli.Out(), opts.refs, opts.format, getRefFunc)
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type killOptions struct {
|
||||
signal string
|
||||
|
||||
containers []string
|
||||
}
|
||||
|
||||
// NewKillCommand creates a new cobra.Command for `docker kill`
|
||||
func NewKillCommand(dockerCli command.Cli) *cobra.Command {
|
||||
var opts killOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "kill [OPTIONS] CONTAINER [CONTAINER...]",
|
||||
Short: "Kill one or more running containers",
|
||||
Args: cli.RequiresMinArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.containers = args
|
||||
return runKill(dockerCli, &opts)
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.StringVarP(&opts.signal, "signal", "s", "KILL", "Signal to send to the container")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runKill(dockerCli command.Cli, opts *killOptions) error {
|
||||
var errs []string
|
||||
ctx := context.Background()
|
||||
errChan := parallelOperation(ctx, opts.containers, func(ctx context.Context, container string) error {
|
||||
return dockerCli.Client().ContainerKill(ctx, container, opts.signal)
|
||||
})
|
||||
for _, name := range opts.containers {
|
||||
if err := <-errChan; err != nil {
|
||||
errs = append(errs, err.Error())
|
||||
} else {
|
||||
fmt.Fprintln(dockerCli.Out(), name)
|
||||
}
|
||||
}
|
||||
if len(errs) > 0 {
|
||||
return errors.New(strings.Join(errs, "\n"))
|
||||
}
|
||||
return nil
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue