Compare commits

..

20 Commits

Author SHA1 Message Date
d7062e5443 Merge pull request #285 from seemethere/bump_17110_rc2
[17.11] bump version to 17.11.0-ce-rc2
2017-11-01 12:14:22 -07:00
2b39e3eb1b Merge pull request #284 from seemethere/update_changelog_17110_rc2
[17.11] Update changelog for 17.11.0 rc2
2017-11-01 12:14:00 -07:00
019b6fc6ff Merge pull request #286 from andrewhsu/st
[17.11] backport Add containerd static compile
2017-11-01 12:13:29 -07:00
1d7a951815 Add containerd static compile
Fixes #35349

Signed-off-by: Michael Crosby <crosbymichael@gmail.com>
(cherry picked from commit 007db062f0c39bf31e1150aa26d045778ab7d24e)
Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
2017-10-31 14:11:42 -07:00
7c931da69a Merge pull request #283 from seemethere/add_artful
[17.11] Add Ubuntu Artful (17.10)
2017-10-31 13:48:25 -07:00
c87c16c585 Update changelog to include note about Artful
Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
2017-10-31 13:33:40 -07:00
7a42750bbc bump version to 17.11.0-ce-rc2
Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
2017-10-31 13:31:15 -07:00
419eadcc09 Update to 1.8.5
Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
(cherry picked from commit c16053e90143cd8a9a559b749407d938f0cb4456)
Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
2017-10-31 13:20:54 -07:00
a241bc8c28 Add Ubuntu 17.10, Artful
Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
(cherry picked from commit 1468941a50dba7e0a82a01cb938338041f028ef9)
Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
2017-10-31 13:20:54 -07:00
df7b627909 Merge pull request #282 from andrewhsu/v
[17.11] bump version to 17.11.0-ce-rc1
2017-10-30 17:24:29 -07:00
3d4e607150 Merge pull request #281 from andrewhsu/c
[17.11] update changelog for 17.11.0-ce-rc1
2017-10-30 17:00:03 -07:00
e2e7ecf1d7 update changelog for 17.11.0-ce-rc1
Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
2017-10-30 16:59:25 -07:00
f7aa41c4f5 Merge pull request #279 from andrewhsu/sk
[17.11] skip tests for now that are known to need adjustment
2017-10-30 16:57:21 -07:00
79e752a8a2 skip test DockerSuite.TestRmiImageIDForceWithRunningContainersAndMultipleTags
This integration-cli test needs to be fixed for change in `rmi -f`
behaviour.

Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
2017-10-30 15:41:04 -07:00
d17cdf8c45 skip test DockerSuite.TestRmiContainerImageNotFound
Skipping integration-cli test because it should be moved to an API test.

Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
2017-10-30 15:41:04 -07:00
a04fcc11ee skip test DockerSuite.TestCpFromErrDstParentNotExists
This integration-cli test is only testing CLI behaviour so should not be
in the engine side of tests.

Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
2017-10-30 15:41:04 -07:00
19d1dd5c8c Merge pull request #278 from riyazdf/update-tests-for-17.11
[17.11] update trust tests for 17.11
2017-10-30 15:36:21 -07:00
2b277deb2a bump version to 17.11.0-ce-rc1
Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
2017-10-30 15:04:12 -07:00
ec6b0a1a4a Blacklist tests, will be rewritten later on
Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
(cherry picked from commit 4e81e4fa4e)
Signed-off-by: Riyaz Faizullabhoy <riyaz.faizullabhoy@docker.com>
2017-10-30 19:12:26 +01:00
250b84ee88 update integration-cli tests for stderr output
Signed-off-by: Riyaz Faizullabhoy <riyaz.faizullabhoy@docker.com>
2017-10-30 18:24:39 +01:00
4207 changed files with 47351 additions and 397890 deletions

View File

@ -1,116 +1,20 @@
# Changelog
For more information on the list of deprecated flags and APIs please have a look at
https://docs.docker.com/engine/deprecated/ where you can find the target removal dates
# Changelog
## 18.03.0-ce (2018-03-DD)
Items starting with `DEPRECATE` are important deprecation notices. For more
information on the list of deprecated flags and APIs please have a look at
https://docs.docker.com/engine/deprecated/ where target removal dates can also
be found.
## 17.11.0-ce (2017-11-DD)
### Builder
IMPORTANT: Docker CE 17.11 is the first Docker release based on
[containerd 1.0 beta](https://github.com/containerd/containerd/releases/tag/v1.0.0-beta.2).
Docker CE 17.11 and later won't recognize containers started with
previous Docker versions. If using
[Live Restore](https://docs.docker.com/engine/admin/live-restore/#enable-the-live-restore-option),
you must stop all containers before upgrading to Docker CE 17.11.
If you don't, any containers started by Docker versions that predate
17.11 won't be recognized by Docker after the upgrade and will keep
running, un-managed, on the system.
* Switch to -buildmode=pie [moby/moby#34369](https://github.com/moby/moby/pull/34369)
* Allow Dockerfile to be outside of build-context [docker/cli#886](https://github.com/docker/cli/pull/886)
* Builder: fix wrong cache hits building from tars [moby/moby#36329](https://github.com/moby/moby/pull/36329)
- Fixes files leaking to other images in a multi-stage build [moby/moby#36338](https://github.com/moby/moby/pull/36338)
### Client
* Simplify the marshaling of compose types.Config [docker/cli#895](https://github.com/docker/cli/pull/895)
+ Add support for multiple composefile when deploying [docker/cli#569](https://github.com/docker/cli/pull/569)
- Fix broken Kubernetes stack flags [docker/cli#831](https://github.com/docker/cli/pull/831)
- Fix stack marshaling for Kubernetes [docker/cli#890](https://github.com/docker/cli/pull/890)
- Fix and simplify bash completion for service env, mounts and labels [docker/cli#682](https://github.com/docker/cli/pull/682)
- Fix `before` and `since` filter for `docker ps` [moby/moby#35938](https://github.com/moby/moby/pull/35938)
- Fix `--label-file` weird behavior [docker/cli#838](https://github.com/docker/cli/pull/838)
- Fix compilation of defaultCredentialStore() on unsupported platforms [docker/cli#872](https://github.com/docker/cli/pull/872)
* Improve and fix bash completion for images [docker/cli#717](https://github.com/docker/cli/pull/717)
+ Added check for empty source in bind mount [docker/cli#824](https://github.com/docker/cli/pull/824)
- Fix TLS from environment variables in client [moby/moby#36270](https://github.com/moby/moby/pull/36270)
* docker build now runs faster when registry-specific credential helper(s) are configured [docker/cli#840](https://github.com/docker/cli/pull/840)
* Update event filter zsh completion with `disable`, `enable`, `install` and `remove` [docker/cli#372](https://github.com/docker/cli/pull/372)
* Produce errors when empty ids are passed into inspect calls [moby/moby#36144](https://github.com/moby/moby/pull/36144)
* Marshall version for the k8s controller [docker/cli#891](https://github.com/docker/cli/pull/891)
* Set a non-zero timeout for HTTP client communication with plugin backend [docker/cli#883](https://github.com/docker/cli/pull/883)
+ Add DOCKER_TLS environment variable for --tls option [docker/cli#863](https://github.com/docker/cli/pull/863)
+ Add --template-driver option for secrets/configs [docker/cli#896](https://github.com/docker/cli/pull/896)
### Logging
* AWS logs - don't add new lines to maximum sized events [moby/moby#36078](https://github.com/moby/moby/pull/36078)
* Move log validator logic after plugins are loaded [moby/moby#36306](https://github.com/moby/moby/pull/36306)
* Support a proxy in Splunk log driver [moby/moby#36220](https://github.com/moby/moby/pull/36220)
- Fix log tail with empty logs [moby/moby#36305](https://github.com/moby/moby/pull/36305)
### Networking
* Libnetwork revendoring [moby/moby#36137](https://github.com/moby/moby/pull/36137)
- Fix for deadlock on exit with Memberlist revendor [docker/libnetwork#2040](https://github.com/docker/libnetwork/pull/2040)
* Fix user specified ndots option [docker/libnetwork#2065](https://github.com/docker/libnetwork/pull/2065)
- Fix to use ContainerID for Windows instead of SandboxID [docker/libnetwork#2010](https://github.com/docker/libnetwork/pull/2010)
* Verify NetworkingConfig to make sure EndpointSettings is not nil [moby/moby#36077](https://github.com/moby/moby/pull/36077)
- Fix `DockerNetworkInternalMode` issue [moby/moby#36298](https://github.com/moby/moby/pull/36298)
- Fix race in attachable network attachment [moby/moby#36191](https://github.com/moby/moby/pull/36191)
- Fix timeout issue of `InspectNetwork` on AArch64 [moby/moby#36257](https://github.com/moby/moby/pull/36257)
* Verbose info is missing for partial overlay ID [moby/moby#35989](https://github.com/moby/moby/pull/35989)
* Update `FindNetwork` to address network name duplications [moby/moby#30897](https://github.com/moby/moby/pull/30897)
* Disallow attaching ingress network [docker/swarmkit#2523](https://github.com/docker/swarmkit/pull/2523)
### Runtime
* Enable HotAdd for Windows [moby/moby#35414](https://github.com/moby/moby/pull/35414)
* LCOW: Graphdriver fix deadlock in hotRemoveVHDs [moby/moby#36114](https://github.com/moby/moby/pull/36114)
* LCOW: Regular mount if only one layer [moby/moby#36052](https://github.com/moby/moby/pull/36052)
* Remove interim env var LCOW_API_PLATFORM_IF_OMITTED [moby/moby#36269](https://github.com/moby/moby/pull/36269)
* Revendor Microsoft/opengcs @ v0.3.6 [moby/moby#36108](https://github.com/moby/moby/pull/36108)
- Fix issue of ExitCode and PID not show up in Task.Status.ContainerStatus [moby/moby#36150](https://github.com/moby/moby/pull/36150)
- Fix issue with plugin scanner going too deep [moby/moby#36119](https://github.com/moby/moby/pull/36119)
* Do not make graphdriver homes private mounts [moby/moby#36047](https://github.com/moby/moby/pull/36047)
* Do not recursive unmount on cleanup of zfs/btrfs [moby/moby#36237](https://github.com/moby/moby/pull/36237)
* Don't restore image if layer does not exist [moby/moby#36304](https://github.com/moby/moby/pull/36304)
* Adjust minimum API version for templated configs/secrets [moby/moby#36366](https://github.com/moby/moby/pull/36366)
* Bump containerd to 1.0.2 (cfd04396dc68220d1cecbe686a6cc3aa5ce3667c) [moby/moby#36308](https://github.com/moby/moby/pull/36308)
* Bump Golang to 1.9.4 [moby/moby#36243](https://github.com/moby/moby/pull/36243)
* Ensure daemon root is unmounted on shutdown [moby/moby#36107](https://github.com/moby/moby/pull/36107)
* Update runc to 6c55f98695e902427906eed2c799e566e3d3dfb5 [moby/moby#36222](https://github.com/moby/moby/pull/36222)
- Fix container cleanup on daemon restart [moby/moby#36249](https://github.com/moby/moby/pull/36249)
* Support SCTP port mapping (bump up API to v1.37) [moby/moby#33922](https://github.com/moby/moby/pull/33922)
* Support SCTP port mapping [docker/cli#278](https://github.com/docker/cli/pull/278)
- Fix Volumes property definition in ContainerConfig [moby/moby#35946](https://github.com/moby/moby/pull/35946)
* Bump moby and dependencies [docker/cli#829](https://github.com/docker/cli/pull/829)
* C.RWLayer: check for nil before use [moby/moby#36242](https://github.com/moby/moby/pull/36242)
+ Add `REMOVE` and `ORPHANED` to TaskState [moby/moby#36146](https://github.com/moby/moby/pull/36146)
- Fixed error detection using `IsErrNotFound` and `IsErrNotImplemented` for `ContainerStatPath`, `CopyFromContainer`, and `CopyToContainer` methods [moby/moby#35979](https://github.com/moby/moby/pull/35979)
+ Add an integration/internal/container helper package [moby/moby#36266](https://github.com/moby/moby/pull/36266)
+ Add canonical import path [moby/moby#36194](https://github.com/moby/moby/pull/36194)
+ Add/use container.Exec() to integration [moby/moby#36326](https://github.com/moby/moby/pull/36326)
- Fix "--node-generic-resource" singular/plural [moby/moby#36125](https://github.com/moby/moby/pull/36125)
* Daemon.cleanupContainer: nullify container RWLayer upon release [moby/moby#36160](https://github.com/moby/moby/pull/36160)
* Daemon: passdown the `--oom-kill-disable` option to containerd [moby/moby#36201](https://github.com/moby/moby/pull/36201)
* Display a warn message when there is binding ports and net mode is host [moby/moby#35510](https://github.com/moby/moby/pull/35510)
* Refresh containerd remotes on containerd restarted [moby/moby#36173](https://github.com/moby/moby/pull/36173)
* Set daemon root to use shared propagation [moby/moby#36096](https://github.com/moby/moby/pull/36096)
* Optimizations for recursive unmount [moby/moby#34379](https://github.com/moby/moby/pull/34379)
* Perform plugin mounts in the runtime [moby/moby#35829](https://github.com/moby/moby/pull/35829)
* Graphdriver: Fix RefCounter memory leak [moby/moby#36256](https://github.com/moby/moby/pull/36256)
* Use continuity fs package for volume copy [moby/moby#36290](https://github.com/moby/moby/pull/36290)
* Use proc/exe for reexec [moby/moby#36124](https://github.com/moby/moby/pull/36124)
+ Add API support for templated secrets and configs [moby/moby#33702](https://github.com/moby/moby/pull/33702) and [moby/moby#36366](https://github.com/moby/moby/pull/36366)
* Use rslave propagation for mounts from daemon root [moby/moby#36055](https://github.com/moby/moby/pull/36055)
+ Add /proc/keys to masked paths [moby/moby#36368](https://github.com/moby/moby/pull/36368)
* Bump Runc to 1.0.0-rc5 [moby/moby#36449](https://github.com/moby/moby/pull/36449)
- Fixes `runc exec` on big-endian architectures [moby/moby#36449](https://github.com/moby/moby/pull/36449)
* Use chroot when mount namespaces aren't provided [moby/moby#36449](https://github.com/moby/moby/pull/36449)
- Fix systemd slice expansion so that it could be consumed by cAdvisor [moby/moby#36449](https://github.com/moby/moby/pull/36449)
- Fix devices mounted with wrong uid/gid [moby/moby#36449](https://github.com/moby/moby/pull/36449)
### Swarm Mode
* Replace EC Private Key with PKCS#8 PEMs [docker/swarmkit#2246](https://github.com/docker/swarmkit/pull/2246)
* Fix IP overlap with empty EndpointSpec [docker/swarmkit #2505](https://github.com/docker/swarmkit/pull/2505)
* Add support for Support SCTP port mapping [docker/swarmkit#2298](https://github.com/docker/swarmkit/pull/2298)
* Do not reschedule tasks if only placement constraints change and are satisfied by the assigned node [docker/swarmkit#2496](https://github.com/docker/swarmkit/pull/2496)
* Ensure task reaper stopChan is closed no more than once [docker/swarmkit #2491](https://github.com/docker/swarmkit/pull/2491)
* Synchronization fixes [docker/swarmkit#2495](https://github.com/docker/swarmkit/pull/2495)
* Add log message to indicate message send retry if streaming unimplemented [docker/swarmkit#2483](https://github.com/docker/swarmkit/pull/2483)
* Debug logs for session, node events on dispatcher, heartbeats [docker/swarmkit#2486](https://github.com/docker/swarmkit/pull/2486)
+ Add swarm types to bash completion event type filter [docker/cli#888](https://github.com/docker/cli/pull/888)
- Fix issue where network inspect does not show Created time for networks in swarm scope [moby/moby#36095](https://github.com/moby/moby/pull/36095)
NOTE: This is the first release with official support for Ubuntu 17.10 (Artful)

View File

@ -1 +1 @@
18.03.0-ce-rc2
17.11.0-ce-rc2

View File

@ -5,5 +5,5 @@ cli/command/stack/** @dnephin @vdemeester
cli/compose/** @dnephin @vdemeester
contrib/completion/bash/** @albers
contrib/completion/zsh/** @sdurrheimer
docs/** @mistyhacks @vdemeester @thaJeztah
docs/** @mstanleyjones @vdemeester @thaJeztah
scripts/** @dnephin

View File

@ -1,12 +1,4 @@
# 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_386.syso
cli/winresources/rsrc_amd64.syso

View File

@ -1,475 +0,0 @@
# 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>
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@gandi.net>
AJ Bowen <aj@gandi.net> <amy@gandi.net>
Akihiro Matsushima <amatsusbit@gmail.com> <amatsus@users.noreply.github.com>
Akihiro Suda <suda.akihiro@lab.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>
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>
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>
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>
Chun Chen <ramichen@tencent.com> <chenchun.feed@gmail.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>
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>
Elan Ruusamäe <glen@pld-linux.org>
Elan Ruusamäe <glen@pld-linux.org> <glen@delfi.ee>
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>
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>
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>
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>
Jamie Hannaford <jamie@limetree.org> <jamie.hannaford@rackspace.com>
Jean-Baptiste Barth <jeanbaptiste.barth@gmail.com>
Jean-Baptiste Dalido <jeanbaptiste@appgratis.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 <jessfraz@google.com>
Jessica Frazelle <jessfraz@google.com> <acidburn@docker.com>
Jessica Frazelle <jessfraz@google.com> <acidburn@google.com>
Jessica Frazelle <jessfraz@google.com> <jess@docker.com>
Jessica Frazelle <jessfraz@google.com> <jess@mesosphere.com>
Jessica Frazelle <jessfraz@google.com> <jfrazelle@users.noreply.github.com>
Jessica Frazelle <jessfraz@google.com> <me@jessfraz.com>
Jessica Frazelle <jessfraz@google.com> <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 (VM) <John.Howard@microsoft.com>
John Howard (VM) <John.Howard@microsoft.com> <jhoward@microsoft.com>
John Howard (VM) <John.Howard@microsoft.com> <jhoward@ntdev.microsoft.com>
John Howard (VM) <John.Howard@microsoft.com> <jhowardmsft@users.noreply.github.com>
John Howard (VM) <John.Howard@microsoft.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 <jose@seatgeek.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>
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>
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>
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>
Mark Oates <fl0yd@me.com>
Markan Patel <mpatel678@gmail.com>
Markus Kortlang <hyp3rdino@googlemail.com> <markus.kortlang@lhsystems.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>
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 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>
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>
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>
Stephen Day <stephen.day@docker.com>
Stephen Day <stephen.day@docker.com> <stevvooe@users.noreply.github.com>
Steve Desmond <steve@vtsv.ca> <stevedesmond-ca@users.noreply.github.com>
Sun Gengze <690388648@qq.com>
Sun Jianbo <wonderflow.sun@gmail.com>
Sun Jianbo <wonderflow.sun@gmail.com> <wonderflow@zju.edu.cn>
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>
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>
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 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>
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 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>
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>
Zhu Kunjia <zhu.kunjia@zte.com.cn>
Zou Yu <zouyu7@huawei.com>

View File

@ -1,638 +0,0 @@
# 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>
Addam Hardy <addam.hardy@gmail.com>
Adolfo Ochagavía <aochagavia92@gmail.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@gandi.net>
Akihiro Suda <suda.akihiro@lab.ntt.co.jp>
Akim Demaille <akim.demaille@docker.com>
Alan Thompson <cloojure@gmail.com>
Albert Callarisa <shark234@gmail.com>
Aleksa Sarai <asarai@suse.de>
Alessandro Boch <aboch@tetrationanalytics.com>
Alex Mavrogiannis <alex.mavrogiannis@docker.com>
Alexander Boyd <alex@opengroove.org>
Alexander Larsson <alexl@redhat.com>
Alexander Morozov <lk4d4@docker.com>
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>
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>
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>
Anton Polonskiy <anton.polonskiy@gmail.com>
Antonio Murdaca <antonio.murdaca@gmail.com>
Antonis Kalipetis <akalipetis@gmail.com>
Anusha Ragunathan <anusha.ragunathan@docker.com>
Arash Deshmeh <adeshmeh@ca.ibm.com>
Arnaud Porterie <arnaud.porterie@docker.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 Firshman <ben@firshman.co.uk>
Benjamin Boudreau <boudreau.benjamin@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>
Boris Pruessmann <boris@pruessmann.org>
Bradley Cicenas <bradley.cicenas@gmail.com>
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>
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>
Ce Gao <ce.gao@outlook.com>
Cedric Davies <cedricda@microsoft.com>
Cezar Sa Espinola <cezarsa@gmail.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 Jones <tophj@linux.vnet.ibm.com>
Christy Perez <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>
Corey Farrell <git@cfware.com>
Cristian Staretu <cristian.staretu@gmail.com>
Daehyeok Mun <daehyeok@gmail.com>
Dafydd Crosby <dtcrsby@gmail.com>
dalanlan <dalanlan925@gmail.com>
Damien Nadé <github@livna.org>
Dan Cotora <dan@bluevision.ro>
Daniel Dao <dqminh@cloudflare.com>
Daniel Farrell <dfarrell@redhat.com>
Daniel Gasienica <daniel@gasienica.ch>
Daniel Goosen <daniel.goosen@surveysampling.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>
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 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 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>
Dmitry Gusev <dmitry.gusev@gmail.com>
Dmitry Smirnov <onlyjob@member.fsf.org>
Dmitry V. Krivenok <krivenok.dmitry@gmail.com>
Don Kjer <don.kjer@gmail.com>
Dong Chen <dongluo.chen@docker.com>
Doug Davis <dug@us.ibm.com>
Drew Erny <drew.erny@docker.com>
Ed Costello <epc@epcostello.com>
Eli Uriegas <eli.uriegas@docker.com>
Eli Uriegas <seemethere101@gmail.com>
Elias Faxö <elias.faxo@tre.se>
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>
Ethan Haynes <ethanhaynes@alumni.harvard.edu>
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>
Flavio Crisciani <flavio.crisciani@docker.com>
Florian Klein <florian.klein@free.fr>
Foysal Iqbal <foysal.iqbal.fb@gmail.com>
Fred Lifton <fred.lifton@docker.com>
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>
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>
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>
Helen Xie <chenjg@harmonycloud.cn>
Henning Sprang <henning.sprang@gmail.com>
Hernan Garcia <hernandanielg@gmail.com>
Hongbin Lu <hongbin034@gmail.com>
Hu Keping <hukeping@huawei.com>
Huayi Zhang <irachex@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>
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 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-Pierre Huynh <jean-pierre.huynh@ounet.fr>
Jean-Pierre Huynh <jp@moogsoft.com>
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 <jessfraz@google.com>
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>
Joao Fernandes <joao.fernandes@docker.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 (VM) <John.Howard@microsoft.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>
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>
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>
Jyrki Puttonen <jyrkiput@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>
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 Meredith <kevin.m.meredith@gmail.com>
Kevin Richardson <kevin@kevinrichardson.co>
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>
Kyle Spiers <kyle@spiers.me>
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>
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>
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>
Lukasz Zajaczkowski <Lukasz.Zajaczkowski@ts.fujitsu.com>
Lénaïc Huard <lhuard@amadeus.com>
Ma Shimiao <mashimiao.fnst@cn.fujitsu.com>
Mabin <bin.ma@huawei.com>
Madhav Puri <madhav.puri@gmail.com>
Madhu Venugopal <madhu@socketplane.io>
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>
Marcus Martins <marcus@docker.com>
Marianna Tessel <mtesselh@gmail.com>
Marius Sturm <marius@graylog.com>
Mark Oates <fl0yd@me.com>
Martin Mosegaard Amdisen <martin.amdisen@praqma.com>
Mary Anthony <mary.anthony@docker.com>
Mason Malone <mason.malone@gmail.com>
Mateusz Major <apkd@users.noreply.github.com>
Matt Gucci <matt9ucci@gmail.com>
Matt Robenolt <matt@ydekproductions.com>
Matthew Heon <mheon@redhat.com>
Matthieu Hauglustaine <matt.hauglustaine@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>
Misty Stanley-Jones <misty@docker.com>
Mohammad Banikazemi <mb@us.ibm.com>
Mohammed Aaqib Ansari <maaquib@gmail.com>
Moorthy RS <rsmoorthy@gmail.com>
Morgan Bauer <mbauer@us.ibm.com>
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>
Nace Oroz <orkica@gmail.com>
Nahum Shalman <nshalman@omniti.com>
Nalin Dahyabhai <nalin@redhat.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>
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>
Nishant Totla <nishanttotla@gmail.com>
NIWA Hideyuki <niwa.niwa@nifty.ne.jp>
Noah Treuhaft <noah.treuhaft@docker.com>
O.S. Tezer <ostezer@gmail.com>
ohmystack <jun.jiang02@ele.me>
Olle Jonsson <olle.jonsson@gmail.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 Weaver <pauweave@cisco.com>
Pavel Pospisil <pospispa@gmail.com>
Paweł Szczekutowicz <pszczekutowicz@gmail.com>
Peeyush Gupta <gpeeyush@linux.vnet.ibm.com>
Peter Edge <peter.edge@gmail.com>
Peter Jaffe <pjaffe@nevo.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>
pidster <pid@pidster.com>
pixelistik <pixelistik@users.noreply.github.com>
Pratik Karki <prertik@outlook.com>
Prayag Verma <prayag.verma@gmail.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>
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>
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>
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>
Ryan Belgrave <rmb1993@gmail.com>
Ryan Detzel <ryan.detzel@gmail.com>
Ryan Stelly <ryan.stelly@live.com>
Sainath Grandhi <sainath.grandhi@intel.com>
Sakeven Jiang <jc5930@sina.cn>
Sally O'Malley <somalley@redhat.com>
Sam Neirinck <sam@samneirinck.com>
Sambuddha Basu <sambuddhabasu1@gmail.com>
Samuel Karp <skarp@amazon.com>
Santhosh Manohar <santhosh@docker.com>
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>
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 <scherer_stefan@icloud.com>
Stefan Weil <sw@weilnetz.de>
Stephen Day <stephen.day@docker.com>
Stephen Rust <srust@blockbridge.com>
Steve Durrheimer <s.durrheimer@gmail.com>
Steven Burgess <steven.a.burgess@hotmail.com>
Subhajit Ghosh <isubuz.g@gmail.com>
Sun Jianbo <wonderflow.sun@gmail.com>
Sungwon Han <sungwon.han@navercorp.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>
Taylor Jones <monitorjbl@gmail.com>
Thatcher Peskens <thatcher@docker.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 <riccardi@systran.fr>
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 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 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>
Umesh Yadav <umesh4257@gmail.com>
Valentin Lorentz <progval+git@progval.net>
Veres Lajos <vlajos@gmail.com>
Victor Vieux <victor.vieux@docker.com>
Victoria Bialas <victoria.bialas@docker.com>
Viktor Stanchev <me@viktorstanchev.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 Long <long.wanglong@huawei.com>
Wang Ping <present.wp@icloud.com>
Wang Xing <hzwangxing@corp.netease.com>
Wang Yuexiao <wang.yuexiao@zte.com.cn>
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>
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>
Yunxiang Huang <hyxqshk@vip.qq.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>
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>

View File

@ -1,6 +1,6 @@
# Docker maintainers file
#
# This file describes who runs the docker/cli project and how.
# This file describes who runs the docker/docker 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.
@ -21,12 +21,23 @@
# a subsystem, they are responsible for doing so and holding the
# subsystem maintainers accountable. If ownership is unclear, they are the de facto owners.
# For each release (including minor releases), a "release captain" is assigned from the
# pool of core maintainers. Rotation is encouraged across all maintainers, to ensure
# the release process is clear and up-to-date.
people = [
"aaronlehmann",
"albers",
"aluzzardi",
"anusha",
"cpuguy83",
"crosbymichael",
"dnephin",
"ehazlett",
"johnstep",
"justincormack",
"mavenugo",
"mlaventure",
"stevvooe",
"tibor",
"tonistiigi",
@ -56,6 +67,7 @@
# - close an issue or pull request when it's inappropriate or off-topic
people = [
"ehazlett",
"programmerq",
"thajeztah"
]
@ -78,16 +90,41 @@
Email = "github@albersweb.de"
GitHub = "albers"
[people.aluzzardi]
Name = "Andrea Luzzardi"
Email = "al@docker.com"
GitHub = "aluzzardi"
[people.anusha]
Name = "Anusha Ragunathan"
Email = "anusha@docker.com"
GitHub = "anusha-ragunathan"
[people.cpuguy83]
Name = "Brian Goff"
Email = "cpuguy83@gmail.com"
GitHub = "cpuguy83"
[people.crosbymichael]
Name = "Michael Crosby"
Email = "crosbymichael@gmail.com"
GitHub = "crosbymichael"
[people.dnephin]
Name = "Daniel Nephin"
Email = "dnephin@gmail.com"
GitHub = "dnephin"
[people.ehazlett]
Name = "Evan Hazlett"
Email = "ejhazlett@gmail.com"
GitHub = "ehazlett"
[people.johnstep]
Name = "John Stephens"
Email = "johnstep@docker.com"
GitHub = "johnstep"
[people.justincormack]
Name = "Justin Cormack"
Email = "justin.cormack@docker.com"
@ -96,12 +133,17 @@
[people.misty]
Name = "Misty Stanley-Jones"
Email = "misty@docker.com"
GitHub = "mistyhacks"
GitHub = "mstanleyjones"
[people.programmerq]
Name = "Jeff Anderson"
Email = "jeff@docker.com"
GitHub = "programmerq"
[people.mlaventure]
Name = "Kenfe-Mickaël Laventure"
Email = "mickael.laventure@docker.com"
GitHub = "mlaventure"
[people.shykes]
Name = "Solomon Hykes"
Email = "solomon@docker.com"
GitHub = "shykes"
[people.stevvooe]
Name = "Stephen Day"

View File

@ -51,14 +51,9 @@ watch: ## monitor file changes and run go test
./scripts/test/watch
vendor: vendor.conf ## check that vendor matches vendor.conf
rm -rf vendor
bash -c 'vndr |& grep -v -i clone'
vndr 2> /dev/null
scripts/validate/check-git-diff vendor
.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

View File

@ -1 +1 @@
18.03.0-ce-rc2
17.11.0-ce-rc2

View File

@ -5,22 +5,16 @@ import (
"net"
"net/http"
"os"
"path/filepath"
"runtime"
"time"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/config"
cliconfig "github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/config/configfile"
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/trust"
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/sockets"
"github.com/docker/go-connections/tlsconfig"
@ -48,28 +42,24 @@ type Cli interface {
SetIn(in *InStream)
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
}
// 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 *InStream
out *OutStream
err io.Writer
client client.APIClient
serverInfo ServerInfo
clientInfo ClientInfo
configFile *configfile.ConfigFile
in *InStream
out *OutStream
err io.Writer
client client.APIClient
defaultVersion string
server ServerInfo
}
// DefaultVersion returns api.defaultVersion or DOCKER_API_VERSION if specified.
func (cli *DockerCli) DefaultVersion() string {
return cli.clientInfo.DefaultVersion
return cli.defaultVersion
}
// Client returns the APIClient
@ -114,27 +104,7 @@ func (cli *DockerCli) ConfigFile() *configfile.ConfigFile {
// 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 {
return cli.clientInfo
}
// 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)
return cli.server
}
// Initialize the dockerCli runs initialization that must happen after command
@ -155,36 +125,17 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions) error {
if err != nil {
return err
}
hasExperimental, err := isEnabled(cli.configFile.Experimental)
if err != nil {
return errors.Wrap(err, "Experimental field")
}
orchestrator := GetOrchestrator(hasExperimental, opts.Common.Orchestrator, cli.configFile.Orchestrator)
cli.clientInfo = ClientInfo{
DefaultVersion: cli.client.ClientVersion(),
HasExperimental: hasExperimental,
Orchestrator: orchestrator,
}
cli.initializeFromClient()
return nil
}
func isEnabled(value string) (bool, error) {
switch value {
case "enabled":
return true, nil
case "", "disabled":
return false, nil
default:
return false, errors.Errorf("%q is not valid, should be either enabled or disabled", value)
}
}
func (cli *DockerCli) initializeFromClient() {
cli.defaultVersion = cli.client.ClientVersion()
ping, err := cli.client.Ping(context.Background())
if err != nil {
// Default to true if we fail to connect to daemon
cli.serverInfo = ServerInfo{HasExperimental: true}
cli.server = ServerInfo{HasExperimental: true}
if ping.APIVersion != "" {
cli.client.NegotiateAPIVersionPing(ping)
@ -192,7 +143,7 @@ func (cli *DockerCli) initializeFromClient() {
return
}
cli.serverInfo = ServerInfo{
cli.server = ServerInfo{
HasExperimental: ping.Experimental,
OSType: ping.OSType,
}
@ -225,18 +176,6 @@ type ServerInfo struct {
OSType string
}
// ClientInfo stores details about the supported features of the client
type ClientInfo struct {
HasExperimental bool
DefaultVersion string
Orchestrator Orchestrator
}
// HasKubernetes checks if kubernetes orchestrator is enabled
func (c ClientInfo) HasKubernetes() bool {
return c.HasExperimental && c.Orchestrator == OrchestratorKubernetes
}
// NewDockerCli returns a DockerCli instance with IO output and error streams set by in, out and err.
func NewDockerCli(in io.ReadCloser, out, err io.Writer) *DockerCli {
return &DockerCli{in: NewInStream(in), out: NewOutStream(out), err: err}
@ -300,12 +239,12 @@ func newHTTPClient(host string, tlsOptions *tlsconfig.Options) (*http.Client, er
Timeout: 30 * time.Second,
}).DialContext,
}
hostURL, err := client.ParseHostURL(host)
proto, addr, _, err := client.ParseHost(host)
if err != nil {
return nil, err
}
sockets.ConfigureTransport(tr, hostURL.Scheme, hostURL.Host)
sockets.ConfigureTransport(tr, proto, addr)
return &http.Client{
Transport: tr,

View File

@ -1,18 +1,17 @@
package command
import (
"crypto/x509"
"os"
"testing"
cliconfig "github.com/docker/cli/cli/config"
"crypto/x509"
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/cli/flags"
"github.com/docker/cli/internal/test/testutil"
"github.com/docker/docker/api"
"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
"github.com/gotestyourself/gotestyourself/fs"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@ -125,155 +124,13 @@ func TestInitializeFromClient(t *testing.T) {
cli := &DockerCli{client: apiclient}
cli.initializeFromClient()
assert.Equal(t, testcase.expectedServer, cli.serverInfo)
assert.Equal(t, defaultVersion, cli.defaultVersion)
assert.Equal(t, testcase.expectedServer, cli.server)
assert.Equal(t, testcase.negotiated, apiclient.negotiated)
})
}
}
func TestExperimentalCLI(t *testing.T) {
defaultVersion := "v1.55"
var testcases = []struct {
doc string
configfile string
expectedExperimentalCLI bool
}{
{
doc: "default",
configfile: `{}`,
expectedExperimentalCLI: false,
},
{
doc: "experimental",
configfile: `{
"experimental": "enabled"
}`,
expectedExperimentalCLI: true,
},
}
for _, testcase := range testcases {
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,
}
cli := &DockerCli{client: apiclient, err: os.Stderr}
cliconfig.SetDir(dir.Path())
err := cli.Initialize(flags.NewClientOptions())
assert.NoError(t, err)
assert.Equal(t, testcase.expectedExperimentalCLI, cli.ClientInfo().HasExperimental)
})
}
}
func TestOrchestratorSwitch(t *testing.T) {
defaultVersion := "v0.00"
var testcases = []struct {
doc string
configfile string
envOrchestrator string
flagOrchestrator string
expectedOrchestrator string
expectedKubernetes bool
}{
{
doc: "default",
configfile: `{
"experimental": "enabled"
}`,
expectedOrchestrator: "swarm",
expectedKubernetes: false,
},
{
doc: "kubernetesIsExperimental",
configfile: `{
"experimental": "disabled",
"orchestrator": "kubernetes"
}`,
envOrchestrator: "kubernetes",
flagOrchestrator: "kubernetes",
expectedOrchestrator: "swarm",
expectedKubernetes: false,
},
{
doc: "kubernetesConfigFile",
configfile: `{
"experimental": "enabled",
"orchestrator": "kubernetes"
}`,
expectedOrchestrator: "kubernetes",
expectedKubernetes: true,
},
{
doc: "kubernetesEnv",
configfile: `{
"experimental": "enabled"
}`,
envOrchestrator: "kubernetes",
expectedOrchestrator: "kubernetes",
expectedKubernetes: true,
},
{
doc: "kubernetesFlag",
configfile: `{
"experimental": "enabled"
}`,
flagOrchestrator: "kubernetes",
expectedOrchestrator: "kubernetes",
expectedKubernetes: true,
},
{
doc: "envOverridesConfigFile",
configfile: `{
"experimental": "enabled",
"orchestrator": "kubernetes"
}`,
envOrchestrator: "swarm",
expectedOrchestrator: "swarm",
expectedKubernetes: false,
},
{
doc: "flagOverridesEnv",
configfile: `{
"experimental": "enabled"
}`,
envOrchestrator: "kubernetes",
flagOrchestrator: "swarm",
expectedOrchestrator: "swarm",
expectedKubernetes: false,
},
}
for _, testcase := range testcases {
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,
}
if testcase.envOrchestrator != "" {
defer patchEnvVariable(t, "DOCKER_ORCHESTRATOR", testcase.envOrchestrator)()
}
cli := &DockerCli{client: apiclient, err: os.Stderr}
cliconfig.SetDir(dir.Path())
options := flags.NewClientOptions()
if testcase.flagOrchestrator != "" {
options.Common.Orchestrator = testcase.flagOrchestrator
}
err := cli.Initialize(options)
assert.NoError(t, err)
assert.Equal(t, testcase.expectedKubernetes, cli.ClientInfo().HasKubernetes())
assert.Equal(t, testcase.expectedOrchestrator, string(cli.ClientInfo().Orchestrator))
})
}
}
func TestGetClientWithPassword(t *testing.T) {
expected := "password"

View File

@ -8,7 +8,6 @@ import (
"github.com/docker/cli/cli/command/config"
"github.com/docker/cli/cli/command/container"
"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"
@ -40,15 +39,12 @@ func AddCommands(cmd *cobra.Command, dockerCli *command.DockerCli) {
image.NewImageCommand(dockerCli),
image.NewBuildCommand(dockerCli),
// manifest
manifest.NewManifestCommand(dockerCli),
// node
node.NewNodeCommand(dockerCli),
// network
network.NewNetworkCommand(dockerCli),
// node
node.NewNodeCommand(dockerCli),
// plugin
plugin.NewPluginCommand(dockerCli),

View File

@ -10,14 +10,11 @@ import (
// 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": "",
},
Use: "config",
Short: "Manage Docker configs",
Args: cli.NoArgs,
RunE: command.ShowHelp(dockerCli.Err()),
Annotations: map[string]string{"version": "1.30"},
}
cmd.AddCommand(
newConfigListCommand(dockerCli),

View File

@ -16,10 +16,9 @@ import (
)
type createOptions struct {
name string
templateDriver string
file string
labels opts.ListOpts
name string
file string
labels opts.ListOpts
}
func newConfigCreateCommand(dockerCli command.Cli) *cobra.Command {
@ -29,7 +28,7 @@ func newConfigCreateCommand(dockerCli command.Cli) *cobra.Command {
cmd := &cobra.Command{
Use: "create [OPTIONS] CONFIG file|-",
Short: "Create a config from a file or STDIN",
Short: "Create a configuration file from a file or STDIN as content",
Args: cli.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
createOpts.name = args[0]
@ -39,8 +38,6 @@ func newConfigCreateCommand(dockerCli command.Cli) *cobra.Command {
}
flags := cmd.Flags()
flags.VarP(&createOpts.labels, "label", "l", "Config labels")
flags.StringVar(&createOpts.templateDriver, "template-driver", "", "Template driver")
flags.SetAnnotation("driver", "version", []string{"1.37"})
return cmd
}
@ -71,11 +68,7 @@ func runConfigCreate(dockerCli command.Cli, options createOptions) error {
},
Data: configData,
}
if options.templateDriver != "" {
spec.Templating = &swarm.Driver{
Name: options.templateDriver,
}
}
r, err := client.ConfigCreate(ctx, spec)
if err != nil {
return err

View File

@ -82,21 +82,14 @@ func TestConfigCreateWithLabels(t *testing.T) {
}
name := "foo"
data, err := ioutil.ReadFile(filepath.Join("testdata", configDataFile))
assert.NoError(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)
if spec.Name != name {
return types.ConfigCreateResponse{}, errors.Errorf("expected name %q, got %q", name, spec.Name)
}
if !reflect.DeepEqual(spec.Labels, expectedLabels) {
return types.ConfigCreateResponse{}, errors.Errorf("expected labels %v, got %v", expectedLabels, spec.Labels)
}
return types.ConfigCreateResponse{
@ -112,32 +105,3 @@ func TestConfigCreateWithLabels(t *testing.T) {
assert.NoError(t, cmd.Execute())
assert.Equal(t, "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.NoError(t, cmd.Execute())
assert.Equal(t, "ID-"+name, strings.TrimSpace(cli.OutBuffer().String()))
}

View File

@ -21,7 +21,7 @@ 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",
Short: "Display detailed information on one or more configuration files",
Args: cli.RequiresMinArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
opts.names = args

View File

@ -19,7 +19,7 @@ func newConfigRemoveCommand(dockerCli command.Cli) *cobra.Command {
return &cobra.Command{
Use: "rm CONFIG [CONFIG...]",
Aliases: []string{"remove"},
Short: "Remove one or more configs",
Short: "Remove one or more configuration files",
Args: cli.RequiresMinArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
opts := removeOptions{

View File

@ -1,14 +1,12 @@
package container
import (
"fmt"
"io"
"net/http/httputil"
"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"
@ -68,9 +66,6 @@ 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
@ -145,24 +140,7 @@ func runAttach(dockerCli command.Cli, opts *attachOptions) error {
if errAttach != nil {
return errAttach
}
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
return getExitStatus(ctx, dockerCli.Client(), opts.container)
}
func resizeTTY(ctx context.Context, dockerCli command.Cli, containerID string) {
@ -179,3 +157,19 @@ func resizeTTY(ctx context.Context, dockerCli command.Cli, containerID string) {
logrus.Debugf("Error monitoring TTY size: %s", err)
}
}
func getExitStatus(ctx context.Context, apiclient client.ContainerAPIClient, containerID string) error {
container, err := apiclient.ContainerInspect(ctx, containerID)
if err != nil {
// If we can't connect, then the daemon probably died.
if !client.IsErrConnectionFailed(err) {
return err
}
return cli.StatusError{StatusCode: -1}
}
status := container.State.ExitCode
if status != 0 {
return cli.StatusError{StatusCode: status}
}
return nil
}

View File

@ -1,7 +1,6 @@
package container
import (
"fmt"
"io/ioutil"
"testing"
@ -9,9 +8,9 @@ import (
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/testutil"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"golang.org/x/net/context"
)
func TestNewAttachCommandErrors(t *testing.T) {
@ -79,50 +78,40 @@ func TestNewAttachCommandErrors(t *testing.T) {
}
func TestGetExitStatus(t *testing.T) {
var (
expectedErr = fmt.Errorf("unexpected error")
errC = make(chan error, 1)
resultC = make(chan container.ContainerWaitOKBody, 1)
)
containerID := "the exec id"
expecatedErr := errors.New("unexpected error")
testcases := []struct {
result *container.ContainerWaitOKBody
err error
inspectError error
exitCode int
expectedError error
}{
{
result: &container.ContainerWaitOKBody{
StatusCode: 0,
},
inspectError: nil,
exitCode: 0,
},
{
err: expectedErr,
expectedError: expectedErr,
inspectError: expecatedErr,
expectedError: expecatedErr,
},
{
result: &container.ContainerWaitOKBody{
Error: &container.ContainerWaitOKBodyError{
expectedErr.Error(),
},
},
expectedError: expectedErr,
},
{
result: &container.ContainerWaitOKBody{
StatusCode: 15,
},
exitCode: 15,
expectedError: cli.StatusError{StatusCode: 15},
},
}
for _, testcase := range testcases {
if testcase.err != nil {
errC <- testcase.err
client := &fakeClient{
inspectFunc: func(id string) (types.ContainerJSON, error) {
assert.Equal(t, containerID, id)
return types.ContainerJSON{
ContainerJSONBase: &types.ContainerJSONBase{
State: &types.ContainerState{ExitCode: testcase.exitCode},
},
}, testcase.inspectError
},
}
if testcase.result != nil {
resultC <- *testcase.result
}
err := getExitStatus(errC, resultC)
err := getExitStatus(context.Background(), client, containerID)
assert.Equal(t, testcase.expectedError, err)
}
}

View File

@ -12,26 +12,12 @@ import (
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, 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)
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
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, containerName string) (container.ContainerCreateCreatedBody, error)
imageCreateFunc func(parentReference string, options types.ImageCreateOptions) (io.ReadCloser, error)
infoFunc func() (types.Info, error)
}
func (f *fakeClient) ContainerInspect(_ context.Context, containerID string) (types.ContainerJSON, error) {
@ -85,42 +71,3 @@ func (f *fakeClient) Info(_ context.Context) (types.Info, error) {
}
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
}

View File

@ -26,17 +26,13 @@ type copyOptions struct {
type copyDirection int
const (
fromContainer copyDirection = 1 << iota
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
@ -69,57 +65,58 @@ func NewCopyCommand(dockerCli command.Cli) *cobra.Command {
}
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,
}
dstContainer, dstPath := splitCpArg(opts.destination)
var direction copyDirection
if srcContainer != "" {
direction |= fromContainer
copyConfig.container = srcContainer
}
if destContainer != "" {
if dstContainer != "" {
direction |= toContainer
copyConfig.container = destContainer
}
cpParam := &cpConfig{
followLink: opts.followLink,
}
ctx := context.Background()
switch direction {
case fromContainer:
return copyFromContainer(ctx, dockerCli, copyConfig)
return copyFromContainer(ctx, dockerCli, srcContainer, srcPath, dstPath, cpParam)
case toContainer:
return copyToContainer(ctx, dockerCli, copyConfig)
return copyToContainer(ctx, dockerCli, srcPath, dstContainer, dstPath, cpParam, opts.copyUIDGID)
case acrossContainers:
// Copying between containers isn't supported.
return errors.New("copying between containers is not supported")
default:
// User didn't specify any container.
return errors.New("must specify at least one container source")
}
}
func statContainerPath(ctx context.Context, dockerCli command.Cli, containerName, path string) (types.ContainerPathStat, error) {
return dockerCli.Client().ContainerStatPath(ctx, containerName, path)
}
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
func copyFromContainer(ctx context.Context, dockerCli command.Cli, srcContainer, srcPath, dstPath string, cpParam *cpConfig) (err error) {
if dstPath != "-" {
// Get an absolute destination path.
dstPath, err = resolveLocalPath(dstPath)
@ -128,11 +125,10 @@ func copyFromContainer(ctx context.Context, dockerCli command.Cli, copyConfig cp
}
}
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 cpParam.followLink {
srcStat, err := statContainerPath(ctx, dockerCli, srcContainer, srcPath)
// If the destination is a symbolic link, we should follow it.
if err == nil && srcStat.Mode&os.ModeSymlink != 0 {
@ -149,17 +145,20 @@ func copyFromContainer(ctx context.Context, dockerCli command.Cli, copyConfig cp
}
content, stat, err := client.CopyFromContainer(ctx, copyConfig.container, srcPath)
content, stat, err := dockerCli.Client().CopyFromContainer(ctx, srcContainer, srcPath)
if err != nil {
return err
}
defer content.Close()
if dstPath == "-" {
_, err = io.Copy(dockerCli.Out(), content)
// Send the response to STDOUT.
_, err = io.Copy(os.Stdout, content)
return err
}
// Prepare source copy info.
srcInfo := archive.CopyInfo{
Path: srcPath,
Exists: true,
@ -172,17 +171,13 @@ func copyFromContainer(ctx context.Context, dockerCli command.Cli, copyConfig cp
_, srcBase := archive.SplitPathDirEntry(srcInfo.Path)
preArchive = archive.RebaseArchiveEntries(content, srcBase, srcInfo.RebaseName)
}
// See comments in the implementation of `archive.CopyTo` for exactly what
// goes into deciding how and whether the source archive needs to be
// altered for the correct copy behavior.
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
func copyToContainer(ctx context.Context, dockerCli command.Cli, srcPath, dstContainer, dstPath string, cpParam *cpConfig, copyUIDGID bool) (err error) {
if srcPath != "-" {
// Get an absolute source path.
srcPath, err = resolveLocalPath(srcPath)
@ -191,10 +186,14 @@ func copyToContainer(ctx context.Context, dockerCli command.Cli, copyConfig cpCo
}
}
client := dockerCli.Client()
// 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.
// Prepare destination copy info by stat-ing the container path.
dstInfo := archive.CopyInfo{Path: dstPath}
dstStat, err := client.ContainerStatPath(ctx, copyConfig.container, dstPath)
dstStat, err := statContainerPath(ctx, dockerCli, dstContainer, dstPath)
// If the destination is a symbolic link, we should evaluate it.
if err == nil && dstStat.Mode&os.ModeSymlink != 0 {
@ -206,7 +205,7 @@ func copyToContainer(ctx context.Context, dockerCli command.Cli, copyConfig cpCo
}
dstInfo.Path = linkTarget
dstStat, err = client.ContainerStatPath(ctx, copyConfig.container, linkTarget)
dstStat, err = statContainerPath(ctx, dockerCli, dstContainer, linkTarget)
}
// Ignore any error and assume that the parent directory of the destination
@ -225,14 +224,15 @@ func copyToContainer(ctx context.Context, dockerCli command.Cli, copyConfig cpCo
)
if srcPath == "-" {
// Use STDIN.
content = os.Stdin
resolvedDstPath = dstInfo.Path
if !dstInfo.IsDir {
return errors.Errorf("destination \"%s:%s\" must be a directory", copyConfig.container, dstPath)
return errors.Errorf("destination \"%s:%s\" must be a directory", dstContainer, dstPath)
}
} else {
// Prepare source copy info.
srcInfo, err := archive.CopyInfoSourcePath(srcPath, copyConfig.followLink)
srcInfo, err := archive.CopyInfoSourcePath(srcPath, cpParam.followLink)
if err != nil {
return err
}
@ -267,9 +267,10 @@ func copyToContainer(ctx context.Context, dockerCli command.Cli, copyConfig cpCo
options := types.CopyToContainerOptions{
AllowOverwriteDirWithFile: false,
CopyUIDGID: copyConfig.copyUIDGID,
CopyUIDGID: copyUIDGID,
}
return client.CopyToContainer(ctx, copyConfig.container, resolvedDstPath, content, options)
return dockerCli.Client().CopyToContainer(ctx, dstContainer, resolvedDstPath, content, options)
}
// We use `:` as a delimiter between CONTAINER and PATH, but `:` could also be

View File

@ -1,188 +0,0 @@
package container
import (
"io"
"io/ioutil"
"os"
"runtime"
"strings"
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/testutil"
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/archive"
"github.com/gotestyourself/gotestyourself/fs"
"github.com/gotestyourself/gotestyourself/skip"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
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.EqualError(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.Equal(t, "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)
require.NoError(t, err)
assert.Equal(t, tarContent, cli.OutBuffer().String())
assert.Equal(t, "", 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.Equal(t, "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)
require.NoError(t, err)
assert.Equal(t, "", cli.OutBuffer().String())
assert.Equal(t, "", cli.ErrBuffer().String())
content, err := ioutil.ReadFile(destDir.Join("file1"))
require.NoError(t, err)
assert.Equal(t, "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.Equal(t, "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)
testutil.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)
testutil.ErrorContains(t, err, "not a directory")
}
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"
}
testutil.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.IfCondition(t, testcase.os != "" && testcase.os != runtime.GOOS)
container, path := splitCpArg(testcase.path)
assert.Equal(t, testcase.expectedContainer, container)
assert.Equal(t, testcase.expectedPath, path)
})
}
}

View File

@ -21,8 +21,7 @@ import (
)
type createOptions struct {
name string
platform string
name string
}
// NewCreateCommand creates a new cobra.Command for `docker create`
@ -52,7 +51,6 @@ func NewCreateCommand(dockerCli command.Cli) *cobra.Command {
// with hostname
flags.Bool("help", false, "Print usage")
command.AddPlatformFlag(flags, &opts.platform)
command.AddTrustVerificationFlags(flags)
copts = addFlags(flags)
return cmd
@ -64,7 +62,7 @@ func runCreate(dockerCli command.Cli, flags *pflag.FlagSet, opts *createOptions,
reportError(dockerCli.Err(), "create", err.Error(), true)
return cli.StatusError{StatusCode: 125}
}
response, err := createContainer(context.Background(), dockerCli, containerConfig, opts.name, opts.platform)
response, err := createContainer(context.Background(), dockerCli, containerConfig, opts.name)
if err != nil {
return err
}
@ -72,7 +70,7 @@ func runCreate(dockerCli command.Cli, flags *pflag.FlagSet, opts *createOptions,
return nil
}
func pullImage(ctx context.Context, dockerCli command.Cli, image string, platform string, out io.Writer) error {
func pullImage(ctx context.Context, dockerCli command.Cli, image string, out io.Writer) error {
ref, err := reference.ParseNormalizedNamed(image)
if err != nil {
return err
@ -92,7 +90,6 @@ func pullImage(ctx context.Context, dockerCli command.Cli, image string, platfor
options := types.ImageCreateOptions{
RegistryAuth: encodedAuth,
Platform: platform,
}
responseBody, err := dockerCli.Client().ImageCreate(ctx, image, options)
@ -158,7 +155,7 @@ func newCIDFile(path string) (*cidFile, error) {
return &cidFile{path: path, file: f}, nil
}
func createContainer(ctx context.Context, dockerCli command.Cli, containerConfig *containerConfig, name string, platform string) (*container.ContainerCreateCreatedBody, error) {
func createContainer(ctx context.Context, dockerCli command.Cli, containerConfig *containerConfig, name string) (*container.ContainerCreateCreatedBody, error) {
config := containerConfig.Config
hostConfig := containerConfig.HostConfig
networkingConfig := containerConfig.NetworkingConfig
@ -201,7 +198,7 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerConfig
fmt.Fprintf(stderr, "Unable to find image '%s' locally\n", reference.FamiliarString(namedRef))
// we don't want to write to stdout anything apart from container.ID
if err := pullImage(ctx, dockerCli, config.Image, platform, stderr); err != nil {
if err := pullImage(ctx, dockerCli, config.Image, stderr); err != nil {
return nil, err
}
if taggedRef, ok := namedRef.(reference.NamedTagged); ok && trustedRef != nil {

View File

@ -5,7 +5,6 @@ import (
"io"
"io/ioutil"
"os"
"runtime"
"strings"
"testing"
@ -107,7 +106,7 @@ func TestCreateContainerPullsImageIfMissing(t *testing.T) {
},
HostConfig: &container.HostConfig{},
}
body, err := createContainer(context.Background(), cli, config, "name", runtime.GOOS)
body, err := createContainer(context.Background(), cli, config, "name")
require.NoError(t, err)
expected := container.ContainerCreateCreatedBody{ID: containerID}
assert.Equal(t, expected, *body)

View File

@ -24,7 +24,6 @@ type execOptions struct {
user string
privileged bool
env opts.ListOpts
workdir string
container string
command []string
}
@ -59,8 +58,6 @@ func NewExecCommand(dockerCli command.Cli) *cobra.Command {
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.StringVarP(&options.workdir, "workdir", "w", "", "Working directory inside the container")
flags.SetAnnotation("workdir", "version", []string{"1.35"})
return cmd
}
@ -193,7 +190,6 @@ func parseExec(opts execOptions, configFile *configfile.ConfigFile) *types.ExecC
Cmd: opts.command,
Detach: opts.detach,
Env: opts.env.GetAll(),
WorkingDir: opts.workdir,
}
// If -d is not set, attach to everything by default

View File

@ -1,165 +0,0 @@
package container
import (
"fmt"
"io/ioutil"
"testing"
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/testutil"
"github.com/docker/docker/api/types"
"github.com/stretchr/testify/assert"
// Import builders to get the builder function as package function
. "github.com/docker/cli/internal/test/builders"
"github.com/gotestyourself/gotestyourself/golden"
)
func TestContainerListErrors(t *testing.T) {
testCases := []struct {
args []string
flags map[string]string
containerListFunc func(types.ContainerListOptions) ([]types.Container, error)
expectedError string
}{
{
flags: map[string]string{
"format": "{{invalid}}",
},
expectedError: `function "invalid" not defined`,
},
{
flags: map[string]string{
"format": "{{join}}",
},
expectedError: `wrong number of args for join`,
},
{
containerListFunc: func(_ types.ContainerListOptions) ([]types.Container, error) {
return nil, fmt.Errorf("error listing containers")
},
expectedError: "error listing containers",
},
}
for _, tc := range testCases {
cmd := newListCommand(
test.NewFakeCli(&fakeClient{
containerListFunc: tc.containerListFunc,
}),
)
cmd.SetArgs(tc.args)
for key, value := range tc.flags {
cmd.Flags().Set(key, value)
}
cmd.SetOutput(ioutil.Discard)
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
}
}
func TestContainerListWithoutFormat(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
containerListFunc: func(_ types.ContainerListOptions) ([]types.Container, error) {
return []types.Container{
*Container("c1"),
*Container("c2", WithName("foo")),
*Container("c3", WithPort(80, 80, TCP), WithPort(81, 81, TCP), WithPort(82, 82, TCP)),
*Container("c4", WithPort(81, 81, UDP)),
*Container("c5", WithPort(82, 82, IP("8.8.8.8"), TCP)),
}, nil
},
})
cmd := newListCommand(cli)
assert.NoError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "container-list-without-format.golden")
}
func TestContainerListNoTrunc(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
containerListFunc: func(_ types.ContainerListOptions) ([]types.Container, error) {
return []types.Container{
*Container("c1"),
*Container("c2", WithName("foo/bar")),
}, nil
},
})
cmd := newListCommand(cli)
cmd.Flags().Set("no-trunc", "true")
assert.NoError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "container-list-without-format-no-trunc.golden")
}
// Test for GitHub issue docker/docker#21772
func TestContainerListNamesMultipleTime(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
containerListFunc: func(_ types.ContainerListOptions) ([]types.Container, error) {
return []types.Container{
*Container("c1"),
*Container("c2", WithName("foo/bar")),
}, nil
},
})
cmd := newListCommand(cli)
cmd.Flags().Set("format", "{{.Names}} {{.Names}}")
assert.NoError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "container-list-format-name-name.golden")
}
// Test for GitHub issue docker/docker#30291
func TestContainerListFormatTemplateWithArg(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
containerListFunc: func(_ types.ContainerListOptions) ([]types.Container, error) {
return []types.Container{
*Container("c1", WithLabel("some.label", "value")),
*Container("c2", WithName("foo/bar"), WithLabel("foo", "bar")),
}, nil
},
})
cmd := newListCommand(cli)
cmd.Flags().Set("format", `{{.Names}} {{.Label "some.label"}}`)
assert.NoError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "container-list-format-with-arg.golden")
}
func TestContainerListFormatSizeSetsOption(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
containerListFunc: func(options types.ContainerListOptions) ([]types.Container, error) {
assert.True(t, options.Size)
return []types.Container{}, nil
},
})
cmd := newListCommand(cli)
cmd.Flags().Set("format", `{{.Size}}`)
assert.NoError(t, cmd.Execute())
}
func TestContainerListWithConfigFormat(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
containerListFunc: func(_ types.ContainerListOptions) ([]types.Container, error) {
return []types.Container{
*Container("c1", WithLabel("some.label", "value")),
*Container("c2", WithName("foo/bar"), WithLabel("foo", "bar")),
}, nil
},
})
cli.SetConfigFile(&configfile.ConfigFile{
PsFormat: "{{ .Names }} {{ .Image }} {{ .Labels }}",
})
cmd := newListCommand(cli)
assert.NoError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "container-list-with-config-format.golden")
}
func TestContainerListWithFormat(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
containerListFunc: func(_ types.ContainerListOptions) ([]types.Container, error) {
return []types.Container{
*Container("c1", WithLabel("some.label", "value")),
*Container("c2", WithName("foo/bar"), WithLabel("foo", "bar")),
}, nil
},
})
cmd := newListCommand(cli)
cmd.Flags().Set("format", "{{ .Names }} {{ .Image }} {{ .Labels }}")
assert.NoError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "container-list-with-format.golden")
}

View File

@ -14,7 +14,6 @@ import (
type logsOptions struct {
follow bool
since string
until string
timestamps bool
details bool
tail string
@ -39,8 +38,6 @@ func NewLogsCommand(dockerCli command.Cli) *cobra.Command {
flags := cmd.Flags()
flags.BoolVarP(&opts.follow, "follow", "f", false, "Follow log output")
flags.StringVar(&opts.since, "since", "", "Show logs since timestamp (e.g. 2013-01-02T13:23:37) or relative (e.g. 42m for 42 minutes)")
flags.StringVar(&opts.until, "until", "", "Show logs before a timestamp (e.g. 2013-01-02T13:23:37) or relative (e.g. 42m for 42 minutes)")
flags.SetAnnotation("until", "version", []string{"1.35"})
flags.BoolVarP(&opts.timestamps, "timestamps", "t", false, "Show timestamps")
flags.BoolVar(&opts.details, "details", false, "Show extra details provided to logs")
flags.StringVar(&opts.tail, "tail", "all", "Number of lines to show from the end of the logs")
@ -54,7 +51,6 @@ func runLogs(dockerCli command.Cli, opts *logsOptions) error {
ShowStdout: true,
ShowStderr: true,
Since: opts.since,
Until: opts.until,
Timestamps: opts.timestamps,
Follow: opts.follow,
Tail: opts.tail,

View File

@ -1,62 +0,0 @@
package container
import (
"io"
"io/ioutil"
"strings"
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/testutil"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/stretchr/testify/assert"
)
var logFn = func(expectedOut string) func(string, types.ContainerLogsOptions) (io.ReadCloser, error) {
return func(container string, opts types.ContainerLogsOptions) (io.ReadCloser, error) {
return ioutil.NopCloser(strings.NewReader(expectedOut)), nil
}
}
func TestRunLogs(t *testing.T) {
inspectFn := func(containerID string) (types.ContainerJSON, error) {
return types.ContainerJSON{
Config: &container.Config{Tty: true},
ContainerJSONBase: &types.ContainerJSONBase{State: &types.ContainerState{Running: false}},
}, nil
}
var testcases = []struct {
doc string
options *logsOptions
client fakeClient
expectedError string
expectedOut string
expectedErr string
}{
{
doc: "successful logs",
expectedOut: "foo",
options: &logsOptions{},
client: fakeClient{logFunc: logFn("foo"), inspectFunc: inspectFn},
},
}
for _, testcase := range testcases {
t.Run(testcase.doc, func(t *testing.T) {
cli := test.NewFakeCli(&testcase.client)
err := runLogs(cli, testcase.options)
if testcase.expectedError != "" {
testutil.ErrorContains(t, err, testcase.expectedError)
} else {
if !assert.NoError(t, err) {
return
}
}
assert.Equal(t, testcase.expectedOut, cli.OutBuffer().String())
assert.Equal(t, testcase.expectedErr, cli.ErrBuffer().String())
})
}
}

View File

@ -145,7 +145,7 @@ func addFlags(flags *pflag.FlagSet) *containerOptions {
expose: opts.NewListOpts(nil),
extraHosts: opts.NewListOpts(opts.ValidateExtraHost),
groupAdd: opts.NewListOpts(nil),
labels: opts.NewListOpts(nil),
labels: opts.NewListOpts(opts.ValidateEnv),
labelsFile: opts.NewListOpts(nil),
linkLocalIPs: opts.NewListOpts(nil),
links: opts.NewListOpts(opts.ValidateLink),
@ -410,7 +410,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions) (*containerConfig, err
}
// collect all the environment variables for the container
envVariables, err := opts.ReadKVEnvStrings(copts.envFile.GetAll(), copts.env.GetAll())
envVariables, err := opts.ReadKVStrings(copts.envFile.GetAll(), copts.env.GetAll())
if err != nil {
return nil, err
}

View File

@ -29,7 +29,6 @@ type runOptions struct {
sigProxy bool
name string
detachKeys string
platform string
}
// NewRunCommand create a new `docker run` command
@ -63,7 +62,6 @@ func NewRunCommand(dockerCli command.Cli) *cobra.Command {
// with hostname
flags.Bool("help", false, "Print usage")
command.AddPlatformFlag(flags, &opts.platform)
command.AddTrustVerificationFlags(flags)
copts = addFlags(flags)
return cmd
@ -162,7 +160,7 @@ func runContainer(dockerCli command.Cli, opts *runOptions, copts *containerOptio
ctx, cancelFun := context.WithCancel(context.Background())
createResponse, err := createContainer(ctx, dockerCli, containerConfig, opts.name, opts.platform)
createResponse, err := createContainer(ctx, dockerCli, containerConfig, opts.name)
if err != nil {
reportError(stderr, cmdPath, err.Error(), true)
return runStartContainerErr(err)

View File

@ -1,25 +0,0 @@
package container
import (
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network"
"github.com/stretchr/testify/assert"
)
func TestRunLabel(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
createContainerFunc: func(_ *container.Config, _ *container.HostConfig, _ *network.NetworkingConfig, _ string) (container.ContainerCreateCreatedBody, error) {
return container.ContainerCreateCreatedBody{
ID: "id",
}, nil
},
Version: "1.36",
})
cmd := NewRunCommand(cli)
cmd.Flags().Set("detach", "true")
cmd.SetArgs([]string{"--label", "foo", "busybox"})
assert.NoError(t, cmd.Execute())
}

View File

@ -1,2 +0,0 @@
c1 busybox:latest some.label=value
c2 busybox:latest foo=bar

View File

@ -1,2 +0,0 @@
c1 busybox:latest some.label=value
c2 busybox:latest foo=bar

View File

@ -1,3 +0,0 @@
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
container_id busybox:latest "top" Less than a second ago Up 1 second c1
container_id busybox:latest "top" Less than a second ago Up 1 second c2,foo/bar

View File

@ -1,6 +0,0 @@
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
container_id busybox:latest "top" Less than a second ago Up 1 second c1
container_id busybox:latest "top" Less than a second ago Up 1 second c2
container_id busybox:latest "top" Less than a second ago Up 1 second 80-82/tcp c3
container_id busybox:latest "top" Less than a second ago Up 1 second 81/udp c4
container_id busybox:latest "top" Less than a second ago Up 1 second 8.8.8.8:82->82/tcp c5

View File

@ -37,12 +37,7 @@ func waitExitOrRemoved(ctx context.Context, dockerCli command.Cli, containerID s
go func() {
select {
case result := <-resultC:
if result.Error != nil {
logrus.Errorf("Error waiting for container: %v", result.Error.Message)
statusC <- 125
} else {
statusC <- int(result.StatusCode)
}
statusC <- int(result.StatusCode)
case err := <-errC:
logrus.Errorf("error waiting for container: %v", err)
statusC <- 125

View File

@ -1,69 +0,0 @@
package container
import (
"strings"
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/docker/api"
"github.com/docker/docker/api/types/container"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"golang.org/x/net/context"
)
func waitFn(cid string) (<-chan container.ContainerWaitOKBody, <-chan error) {
resC := make(chan container.ContainerWaitOKBody)
errC := make(chan error, 1)
var res container.ContainerWaitOKBody
go func() {
switch {
case strings.Contains(cid, "exit-code-42"):
res.StatusCode = 42
resC <- res
case strings.Contains(cid, "non-existent"):
err := errors.Errorf("No such container: %v", cid)
errC <- err
case strings.Contains(cid, "wait-error"):
res.Error = &container.ContainerWaitOKBodyError{Message: "removal failed"}
resC <- res
default:
// normal exit
resC <- res
}
}()
return resC, errC
}
func TestWaitExitOrRemoved(t *testing.T) {
testcases := []struct {
cid string
exitCode int
}{
{
cid: "normal-container",
exitCode: 0,
},
{
cid: "give-me-exit-code-42",
exitCode: 42,
},
{
cid: "i-want-a-wait-error",
exitCode: 125,
},
{
cid: "non-existent-container-id",
exitCode: 125,
},
}
client := test.NewFakeCli(&fakeClient{waitFunc: waitFn, Version: api.DefaultVersion})
for _, testcase := range testcases {
statusC := waitExitOrRemoved(context.Background(), client, testcase.cid, true)
exitCode := <-statusC
assert.Equal(t, testcase.exitCode, exitCode)
}
}

View File

@ -642,12 +642,9 @@ func TestDisplayablePorts(t *testing.T) {
PublicPort: 1024,
PrivatePort: 80,
Type: "udp",
}, {
PrivatePort: 12345,
Type: "sctp",
},
},
"80/tcp, 80/udp, 1024/tcp, 1024/udp, 12345/sctp, 1.1.1.1:1024->80/tcp, 1.1.1.1:1024->80/udp, 2.1.1.1:1024->80/tcp, 2.1.1.1:1024->80/udp, 1.1.1.1:80->1024/tcp, 1.1.1.1:80->1024/udp, 2.1.1.1:80->1024/tcp, 2.1.1.1:80->1024/udp",
"80/tcp, 80/udp, 1024/tcp, 1024/udp, 1.1.1.1:1024->80/tcp, 1.1.1.1:1024->80/udp, 2.1.1.1:1024->80/tcp, 2.1.1.1:1024->80/udp, 1.1.1.1:80->1024/tcp, 1.1.1.1:80->1024/udp, 2.1.1.1:80->1024/tcp, 2.1.1.1:80->1024/udp",
},
}

View File

@ -14,7 +14,7 @@ import (
)
const (
defaultNodeTableFormat = "table {{.ID}} {{if .Self}}*{{else}} {{ end }}\t{{.Hostname}}\t{{.Status}}\t{{.Availability}}\t{{.ManagerStatus}}\t{{.EngineVersion}}"
defaultNodeTableFormat = "table {{.ID}} {{if .Self}}*{{else}} {{ end }}\t{{.Hostname}}\t{{.Status}}\t{{.Availability}}\t{{.ManagerStatus}}"
nodeInspectPrettyTemplate Format = `ID: {{.ID}}
{{- if .Name }}
Name: {{.Name}}
@ -75,7 +75,6 @@ TLS Info:
hostnameHeader = "HOSTNAME"
availabilityHeader = "AVAILABILITY"
managerStatusHeader = "MANAGER STATUS"
engineVersionHeader = "ENGINE VERSION"
tlsStatusHeader = "TLS STATUS"
)
@ -116,7 +115,6 @@ func NodeWrite(ctx Context, nodes []swarm.Node, info types.Info) error {
"Status": statusHeader,
"Availability": availabilityHeader,
"ManagerStatus": managerStatusHeader,
"EngineVersion": engineVersionHeader,
"TLSStatus": tlsStatusHeader,
}
nodeCtx := nodeContext{}
@ -178,10 +176,6 @@ func (c *nodeContext) TLSStatus() string {
return "Needs Rotation"
}
func (c *nodeContext) EngineVersion() string {
return c.n.Description.Engine.EngineVersion
}
// NodeInspectWrite renders the context for a list of nodes
func NodeInspectWrite(ctx Context, refs []string, getRef inspect.GetRefFunc) error {
if ctx.Format != nodeInspectPrettyTemplate {

View File

@ -74,10 +74,10 @@ func TestNodeContextWrite(t *testing.T) {
// Table format
{
context: Context{Format: NewNodeFormat("table", false)},
expected: `ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS ENGINE VERSION
nodeID1 foobar_baz Foo Drain Leader 18.03.0-ce
nodeID2 foobar_bar Bar Active Reachable 1.2.3
nodeID3 foobar_boo Boo Active ` + "\n", // (to preserve whitespace)
expected: `ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS
nodeID1 foobar_baz Foo Drain Leader
nodeID2 foobar_bar Bar Active Reachable
nodeID3 foobar_boo Boo Active ` + "\n", // (to preserve whitespace)
clusterInfo: swarm.ClusterInfo{TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}},
},
{
@ -172,7 +172,6 @@ foobar_boo Unknown
Description: swarm.NodeDescription{
Hostname: "foobar_baz",
TLSInfo: swarm.TLSInfo{TrustRoot: "no"},
Engine: swarm.EngineDescription{EngineVersion: "18.03.0-ce"},
},
Status: swarm.NodeStatus{State: swarm.NodeState("foo")},
Spec: swarm.NodeSpec{Availability: swarm.NodeAvailability("drain")},
@ -183,7 +182,6 @@ foobar_boo Unknown
Description: swarm.NodeDescription{
Hostname: "foobar_bar",
TLSInfo: swarm.TLSInfo{TrustRoot: "hi"},
Engine: swarm.EngineDescription{EngineVersion: "1.2.3"},
},
Status: swarm.NodeStatus{State: swarm.NodeState("bar")},
Spec: swarm.NodeSpec{Availability: swarm.NodeAvailability("active")},
@ -217,17 +215,17 @@ func TestNodeContextWriteJSON(t *testing.T) {
}{
{
expected: []map[string]interface{}{
{"Availability": "", "Hostname": "foobar_baz", "ID": "nodeID1", "ManagerStatus": "", "Status": "", "Self": false, "TLSStatus": "Unknown", "EngineVersion": "1.2.3"},
{"Availability": "", "Hostname": "foobar_bar", "ID": "nodeID2", "ManagerStatus": "", "Status": "", "Self": false, "TLSStatus": "Unknown", "EngineVersion": ""},
{"Availability": "", "Hostname": "foobar_boo", "ID": "nodeID3", "ManagerStatus": "", "Status": "", "Self": false, "TLSStatus": "Unknown", "EngineVersion": "18.03.0-ce"},
{"Availability": "", "Hostname": "foobar_baz", "ID": "nodeID1", "ManagerStatus": "", "Status": "", "Self": false, "TLSStatus": "Unknown"},
{"Availability": "", "Hostname": "foobar_bar", "ID": "nodeID2", "ManagerStatus": "", "Status": "", "Self": false, "TLSStatus": "Unknown"},
{"Availability": "", "Hostname": "foobar_boo", "ID": "nodeID3", "ManagerStatus": "", "Status": "", "Self": false, "TLSStatus": "Unknown"},
},
info: types.Info{},
},
{
expected: []map[string]interface{}{
{"Availability": "", "Hostname": "foobar_baz", "ID": "nodeID1", "ManagerStatus": "", "Status": "", "Self": false, "TLSStatus": "Ready", "EngineVersion": "1.2.3"},
{"Availability": "", "Hostname": "foobar_bar", "ID": "nodeID2", "ManagerStatus": "", "Status": "", "Self": false, "TLSStatus": "Needs Rotation", "EngineVersion": ""},
{"Availability": "", "Hostname": "foobar_boo", "ID": "nodeID3", "ManagerStatus": "", "Status": "", "Self": false, "TLSStatus": "Unknown", "EngineVersion": "18.03.0-ce"},
{"Availability": "", "Hostname": "foobar_baz", "ID": "nodeID1", "ManagerStatus": "", "Status": "", "Self": false, "TLSStatus": "Ready"},
{"Availability": "", "Hostname": "foobar_bar", "ID": "nodeID2", "ManagerStatus": "", "Status": "", "Self": false, "TLSStatus": "Needs Rotation"},
{"Availability": "", "Hostname": "foobar_boo", "ID": "nodeID3", "ManagerStatus": "", "Status": "", "Self": false, "TLSStatus": "Unknown"},
},
info: types.Info{
Swarm: swarm.Info{
@ -242,9 +240,9 @@ func TestNodeContextWriteJSON(t *testing.T) {
for _, testcase := range cases {
nodes := []swarm.Node{
{ID: "nodeID1", Description: swarm.NodeDescription{Hostname: "foobar_baz", TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}, Engine: swarm.EngineDescription{EngineVersion: "1.2.3"}}},
{ID: "nodeID1", Description: swarm.NodeDescription{Hostname: "foobar_baz", TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}}},
{ID: "nodeID2", Description: swarm.NodeDescription{Hostname: "foobar_bar", TLSInfo: swarm.TLSInfo{TrustRoot: "no"}}},
{ID: "nodeID3", Description: swarm.NodeDescription{Hostname: "foobar_boo", Engine: swarm.EngineDescription{EngineVersion: "18.03.0-ce"}}},
{ID: "nodeID3", Description: swarm.NodeDescription{Hostname: "foobar_boo"}},
}
out := bytes.NewBufferString("")
err := NodeWrite(Context{Format: "{{json .}}", Output: out}, nodes, testcase.info)

View File

@ -2,7 +2,6 @@ package formatter
import (
"fmt"
"sort"
"strings"
"time"
@ -521,95 +520,19 @@ func (c *serviceContext) Image() string {
return image
}
type portRange struct {
pStart uint32
pEnd uint32
tStart uint32
tEnd uint32
protocol swarm.PortConfigProtocol
}
func (pr portRange) String() string {
var (
pub string
tgt string
)
if pr.pEnd > pr.pStart {
pub = fmt.Sprintf("%d-%d", pr.pStart, pr.pEnd)
} else {
pub = fmt.Sprintf("%d", pr.pStart)
}
if pr.tEnd > pr.tStart {
tgt = fmt.Sprintf("%d-%d", pr.tStart, pr.tEnd)
} else {
tgt = fmt.Sprintf("%d", pr.tStart)
}
return fmt.Sprintf("*:%s->%s/%s", pub, tgt, pr.protocol)
}
// Ports formats published ports on the ingress network for output.
//
// Where possible, ranges are grouped to produce a compact output:
// - multiple ports mapped to a single port (80->80, 81->80); is formatted as *:80-81->80
// - multiple consecutive ports on both sides; (80->80, 81->81) are formatted as: *:80-81->80-81
//
// The above should not be grouped together, i.e.:
// - 80->80, 81->81, 82->80 should be presented as : *:80-81->80-81, *:82->80
//
// TODO improve:
// - combine non-consecutive ports mapped to a single port (80->80, 81->80, 84->80, 86->80, 87->80); to be printed as *:80-81,84,86-87->80
// - combine tcp and udp mappings if their port-mapping is exactly the same (*:80-81->80-81/tcp+udp instead of *:80-81->80-81/tcp, *:80-81->80-81/udp)
func (c *serviceContext) Ports() string {
if c.service.Endpoint.Ports == nil {
return ""
}
pr := portRange{}
ports := []string{}
sort.Sort(byProtocolAndPublishedPort(c.service.Endpoint.Ports))
for _, p := range c.service.Endpoint.Ports {
if p.PublishMode == swarm.PortConfigPublishModeIngress {
prIsRange := pr.tEnd != pr.tStart
tOverlaps := p.TargetPort <= pr.tEnd
// Start a new port-range if:
// - the protocol is different from the current port-range
// - published or target port are not consecutive to the current port-range
// - the current port-range is a _range_, and the target port overlaps with the current range's target-ports
if p.Protocol != pr.protocol || p.PublishedPort-pr.pEnd > 1 || p.TargetPort-pr.tEnd > 1 || prIsRange && tOverlaps {
// start a new port-range, and print the previous port-range (if any)
if pr.pStart > 0 {
ports = append(ports, pr.String())
}
pr = portRange{
pStart: p.PublishedPort,
pEnd: p.PublishedPort,
tStart: p.TargetPort,
tEnd: p.TargetPort,
protocol: p.Protocol,
}
continue
}
pr.pEnd = p.PublishedPort
pr.tEnd = p.TargetPort
for _, pConfig := range c.service.Endpoint.Ports {
if pConfig.PublishMode == swarm.PortConfigPublishModeIngress {
ports = append(ports, fmt.Sprintf("*:%d->%d/%s",
pConfig.PublishedPort,
pConfig.TargetPort,
pConfig.Protocol,
))
}
}
if pr.pStart > 0 {
ports = append(ports, pr.String())
}
return strings.Join(ports, ", ")
}
type byProtocolAndPublishedPort []swarm.PortConfig
func (a byProtocolAndPublishedPort) Len() int { return len(a) }
func (a byProtocolAndPublishedPort) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a byProtocolAndPublishedPort) Less(i, j int) bool {
if a[i].Protocol == a[j].Protocol {
return a[i].PublishedPort < a[j].PublishedPort
}
return a[i].Protocol < a[j].Protocol
return strings.Join(ports, ",")
}

View File

@ -224,136 +224,3 @@ func TestServiceContextWriteJSONField(t *testing.T) {
assert.Equal(t, services[i].Spec.Name, s, msg)
}
}
func TestServiceContext_Ports(t *testing.T) {
c := serviceContext{
service: swarm.Service{
Endpoint: swarm.Endpoint{
Ports: []swarm.PortConfig{
{
Protocol: "tcp",
TargetPort: 80,
PublishedPort: 81,
PublishMode: "ingress",
},
{
Protocol: "tcp",
TargetPort: 80,
PublishedPort: 80,
PublishMode: "ingress",
},
{
Protocol: "tcp",
TargetPort: 95,
PublishedPort: 95,
PublishMode: "ingress",
},
{
Protocol: "tcp",
TargetPort: 90,
PublishedPort: 90,
PublishMode: "ingress",
},
{
Protocol: "tcp",
TargetPort: 91,
PublishedPort: 91,
PublishMode: "ingress",
},
{
Protocol: "tcp",
TargetPort: 92,
PublishedPort: 92,
PublishMode: "ingress",
},
{
Protocol: "tcp",
TargetPort: 93,
PublishedPort: 93,
PublishMode: "ingress",
},
{
Protocol: "tcp",
TargetPort: 94,
PublishedPort: 94,
PublishMode: "ingress",
},
{
Protocol: "udp",
TargetPort: 95,
PublishedPort: 95,
PublishMode: "ingress",
},
{
Protocol: "udp",
TargetPort: 90,
PublishedPort: 90,
PublishMode: "ingress",
},
{
Protocol: "udp",
TargetPort: 96,
PublishedPort: 96,
PublishMode: "ingress",
},
{
Protocol: "udp",
TargetPort: 91,
PublishedPort: 91,
PublishMode: "ingress",
},
{
Protocol: "udp",
TargetPort: 92,
PublishedPort: 92,
PublishMode: "ingress",
},
{
Protocol: "udp",
TargetPort: 93,
PublishedPort: 93,
PublishMode: "ingress",
},
{
Protocol: "udp",
TargetPort: 94,
PublishedPort: 94,
PublishMode: "ingress",
},
{
Protocol: "tcp",
TargetPort: 60,
PublishedPort: 60,
PublishMode: "ingress",
},
{
Protocol: "tcp",
TargetPort: 61,
PublishedPort: 61,
PublishMode: "ingress",
},
{
Protocol: "tcp",
TargetPort: 61,
PublishedPort: 62,
PublishMode: "ingress",
},
{
Protocol: "sctp",
TargetPort: 97,
PublishedPort: 97,
PublishMode: "ingress",
},
{
Protocol: "sctp",
TargetPort: 98,
PublishedPort: 98,
PublishMode: "ingress",
},
},
},
},
}
assert.Equal(t, "*:97-98->97-98/sctp, *:60-61->60-61/tcp, *:62->61/tcp, *:80-81->80/tcp, *:90-95->90-95/tcp, *:90-96->90-96/udp", c.Ports())
}

View File

@ -9,10 +9,8 @@ import (
"io"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"runtime"
"strings"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
@ -66,7 +64,6 @@ type buildOptions struct {
target string
imageIDFile string
stream bool
platform string
}
// dockerfileFromStdin returns true when the user specified that the Dockerfile
@ -138,7 +135,6 @@ func NewBuildCommand(dockerCli command.Cli) *cobra.Command {
flags.StringVar(&options.imageIDFile, "iidfile", "", "Write the image ID to the file")
command.AddTrustVerificationFlags(flags)
command.AddPlatformFlag(flags, &options.platform)
flags.BoolVar(&options.squash, "squash", false, "Squash newly built layers into a single new layer")
flags.SetAnnotation("squash", "experimental", nil)
@ -208,14 +204,6 @@ func runBuild(dockerCli command.Cli, options buildOptions) error {
buildCtx, relDockerfile, err = build.GetContextFromReader(dockerCli.In(), options.dockerfileName)
case isLocalDir(specifiedContext):
contextDir, relDockerfile, err = build.GetContextFromLocalDir(specifiedContext, options.dockerfileName)
if err == nil && strings.HasPrefix(relDockerfile, ".."+string(filepath.Separator)) {
// Dockerfile is outside of build-context; read the Dockerfile and pass it as dockerfileCtx
dockerfileCtx, err = os.Open(options.dockerfileName)
if err != nil {
return errors.Errorf("unable to open Dockerfile: %v", err)
}
defer dockerfileCtx.Close()
}
case urlutil.IsGitURL(specifiedContext):
tempDir, relDockerfile, err = build.GetContextFromGitURL(specifiedContext, options.dockerfileName)
case urlutil.IsURL(specifiedContext):
@ -263,7 +251,7 @@ func runBuild(dockerCli command.Cli, options buildOptions) error {
}
}
// replace Dockerfile if it was added from stdin or a file outside the build-context, and there is archive context
// replace Dockerfile if it was added from stdin and there is archive context
if dockerfileCtx != nil && buildCtx != nil {
buildCtx, relDockerfile, err = build.AddDockerfileToBuildContext(dockerfileCtx, buildCtx)
if err != nil {
@ -271,7 +259,7 @@ func runBuild(dockerCli command.Cli, options buildOptions) error {
}
}
// if streaming and Dockerfile was not from stdin then read from file
// if streaming and dockerfile was not from stdin then read from file
// to the same reader that is usually stdin
if options.stream && dockerfileCtx == nil {
dockerfileCtx, err = os.Open(relDockerfile)
@ -317,8 +305,8 @@ func runBuild(dockerCli command.Cli, options buildOptions) error {
progressOutput = &lastProgressOutput{output: progressOutput}
}
// if up to this point nothing has set the context then we must have another
// way for sending it(streaming) and set the context to the Dockerfile
// if up to this point nothing has set the context then we must have have
// another way for sending it(streaming) and set the context to the Dockerfile
if dockerfileCtx != nil && buildCtx == nil {
buildCtx = dockerfileCtx
}
@ -386,7 +374,6 @@ func runBuild(dockerCli command.Cli, options buildOptions) error {
ExtraHosts: options.extraHosts.GetAll(),
Target: options.target,
RemoteContext: remote,
Platform: options.platform,
}
if s != nil {

View File

@ -167,10 +167,6 @@ func GetContextFromGitURL(gitURL, dockerfileName string) (string, string, error)
return "", "", err
}
relDockerfile, err := getDockerfileRelPath(absContextDir, dockerfileName)
if err == nil && strings.HasPrefix(relDockerfile, ".."+string(filepath.Separator)) {
return "", "", errors.Errorf("the Dockerfile (%s) must be within the build context", dockerfileName)
}
return absContextDir, relDockerfile, err
}
@ -322,6 +318,10 @@ func getDockerfileRelPath(absContextDir, givenDockerfile string) (string, error)
return "", errors.Errorf("unable to get relative Dockerfile path: %v", err)
}
if strings.HasPrefix(relDockerfile, ".."+string(filepath.Separator)) {
return "", errors.Errorf("the Dockerfile (%s) must be within the build context", givenDockerfile)
}
return relDockerfile, nil
}

View File

@ -108,56 +108,6 @@ func TestRunBuildDockerfileFromStdinWithCompress(t *testing.T) {
assert.Equal(t, []string{dockerfileName, ".dockerignore", "foo"}, actual)
}
func TestRunBuildDockerfileOutsideContext(t *testing.T) {
dir := fs.NewDir(t, t.Name(),
fs.WithFile("data", "data file"),
)
defer dir.Remove()
// Dockerfile outside of build-context
df := fs.NewFile(t, t.Name(),
fs.WithContent(`
FROM FOOBAR
COPY data /data
`),
)
defer df.Remove()
dest, err := ioutil.TempDir("", t.Name())
require.NoError(t, err)
defer os.RemoveAll(dest)
var dockerfileName string
fakeImageBuild := func(_ context.Context, context io.Reader, options types.ImageBuildOptions) (types.ImageBuildResponse, error) {
buffer := new(bytes.Buffer)
tee := io.TeeReader(context, buffer)
assert.NoError(t, archive.Untar(tee, dest, nil))
dockerfileName = options.Dockerfile
body := new(bytes.Buffer)
return types.ImageBuildResponse{Body: ioutil.NopCloser(body)}, nil
}
cli := test.NewFakeCli(&fakeClient{imageBuildFunc: fakeImageBuild})
options := newBuildOptions()
options.context = dir.Path()
options.dockerfileName = df.Path()
err = runBuild(cli, options)
require.NoError(t, err)
files, err := ioutil.ReadDir(dest)
require.NoError(t, err)
var actual []string
for _, fileInfo := range files {
actual = append(actual, fileInfo.Name())
}
sort.Strings(actual)
assert.Equal(t, []string{dockerfileName, ".dockerignore", "data"}, actual)
}
// TestRunBuildFromLocalGitHubDirNonExistingRepo tests that build contexts
// starting with `github.com/` are special-cased, and the build command attempts
// to clone the remote repo.

View File

@ -14,9 +14,8 @@ import (
)
type pullOptions struct {
remote string
all bool
platform string
remote string
all bool
}
// NewPullCommand creates a new `docker pull` command
@ -36,8 +35,6 @@ func NewPullCommand(dockerCli command.Cli) *cobra.Command {
flags := cmd.Flags()
flags.BoolVarP(&opts.all, "all-tags", "a", false, "Download all tagged images in the repository")
command.AddPlatformFlag(flags, &opts.platform)
command.AddTrustVerificationFlags(flags)
return cmd
@ -66,9 +63,9 @@ func runPull(cli command.Cli, opts pullOptions) error {
// Check if reference has a digest
_, isCanonical := distributionRef.(reference.Canonical)
if command.IsTrusted() && !isCanonical {
err = trustedPull(ctx, cli, imgRefAndAuth, opts.platform)
err = trustedPull(ctx, cli, imgRefAndAuth)
} else {
err = imagePullPrivileged(ctx, cli, imgRefAndAuth, opts.all, opts.platform)
err = imagePullPrivileged(ctx, cli, imgRefAndAuth, opts.all)
}
if err != nil {
if strings.Contains(err.Error(), "when fetching 'plugin'") {

View File

@ -9,7 +9,6 @@ import (
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types"
apiclient "github.com/docker/docker/client"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
@ -57,13 +56,9 @@ func runRemove(dockerCli command.Cli, opts removeOptions, images []string) error
}
var errs []string
var fatalErr = false
for _, img := range images {
dels, err := client.ImageRemove(ctx, img, options)
if err != nil {
if !apiclient.IsErrNotFound(err) {
fatalErr = true
}
errs = append(errs, err.Error())
} else {
for _, del := range dels {
@ -78,10 +73,10 @@ func runRemove(dockerCli command.Cli, opts removeOptions, images []string) error
if len(errs) > 0 {
msg := strings.Join(errs, "\n")
if !opts.force || fatalErr {
if !opts.force {
return errors.New(msg)
}
fmt.Fprintln(dockerCli.Err(), msg)
fmt.Fprintf(dockerCli.Err(), msg)
}
return nil
}

View File

@ -13,18 +13,6 @@ import (
"github.com/stretchr/testify/assert"
)
type notFound struct {
imageID string
}
func (n notFound) Error() string {
return fmt.Sprintf("Error: No such image: %s", n.imageID)
}
func (n notFound) NotFound() bool {
return true
}
func TestNewRemoveCommandAlias(t *testing.T) {
cmd := newRemoveCommand(test.NewFakeCli(&fakeClient{}))
assert.True(t, cmd.HasAlias("rmi"))
@ -43,15 +31,6 @@ func TestNewRemoveCommandErrors(t *testing.T) {
name: "wrong args",
expectedError: "requires at least 1 argument.",
},
{
name: "ImageRemove fail with force option",
args: []string{"-f", "image1"},
expectedError: "error removing image",
imageRemoveFunc: func(image string, options types.ImageRemoveOptions) ([]types.ImageDeleteResponseItem, error) {
assert.Equal(t, "image1", image)
return []types.ImageDeleteResponseItem{}, errors.Errorf("error removing image")
},
},
{
name: "ImageRemove fail",
args: []string{"arg1"},
@ -64,14 +43,12 @@ func TestNewRemoveCommandErrors(t *testing.T) {
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
cmd := NewRemoveCommand(test.NewFakeCli(&fakeClient{
imageRemoveFunc: tc.imageRemoveFunc,
}))
cmd.SetOutput(ioutil.Discard)
cmd.SetArgs(tc.args)
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
})
cmd := NewRemoveCommand(test.NewFakeCli(&fakeClient{
imageRemoveFunc: tc.imageRemoveFunc,
}))
cmd.SetOutput(ioutil.Discard)
cmd.SetArgs(tc.args)
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
}
}
@ -80,7 +57,7 @@ func TestNewRemoveCommandSuccess(t *testing.T) {
name string
args []string
imageRemoveFunc func(image string, options types.ImageRemoveOptions) ([]types.ImageDeleteResponseItem, error)
expectedStderr string
expectedErrMsg string
}{
{
name: "Image Deleted",
@ -91,16 +68,14 @@ func TestNewRemoveCommandSuccess(t *testing.T) {
},
},
{
name: "Image not found with force option",
name: "Image Deleted with force option",
args: []string{"-f", "image1"},
imageRemoveFunc: func(image string, options types.ImageRemoveOptions) ([]types.ImageDeleteResponseItem, error) {
assert.Equal(t, "image1", image)
assert.Equal(t, true, options.Force)
return []types.ImageDeleteResponseItem{}, notFound{"image1"}
return []types.ImageDeleteResponseItem{}, errors.Errorf("error removing image")
},
expectedStderr: "Error: No such image: image1\n",
expectedErrMsg: "error removing image",
},
{
name: "Image Untagged",
args: []string{"image1"},
@ -121,14 +96,14 @@ func TestNewRemoveCommandSuccess(t *testing.T) {
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{imageRemoveFunc: tc.imageRemoveFunc})
cmd := NewRemoveCommand(cli)
cmd.SetOutput(ioutil.Discard)
cmd.SetArgs(tc.args)
assert.NoError(t, cmd.Execute())
assert.Equal(t, tc.expectedStderr, cli.ErrBuffer().String())
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("remove-command-success.%s.golden", tc.name))
})
cli := test.NewFakeCli(&fakeClient{imageRemoveFunc: tc.imageRemoveFunc})
cmd := NewRemoveCommand(cli)
cmd.SetOutput(ioutil.Discard)
cmd.SetArgs(tc.args)
assert.NoError(t, cmd.Execute())
if tc.expectedErrMsg != "" {
assert.Equal(t, tc.expectedErrMsg, cli.ErrBuffer().String())
}
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("remove-command-success.%s.golden", tc.name))
}
}

View File

@ -180,7 +180,7 @@ func imagePushPrivileged(ctx context.Context, cli command.Cli, authConfig types.
}
// trustedPull handles content trust pulling of an image
func trustedPull(ctx context.Context, cli command.Cli, imgRefAndAuth trust.ImageRefAndAuth, platform string) error {
func trustedPull(ctx context.Context, cli command.Cli, imgRefAndAuth trust.ImageRefAndAuth) error {
refs, err := getTrustedPullTargets(cli, imgRefAndAuth)
if err != nil {
return err
@ -202,7 +202,7 @@ func trustedPull(ctx context.Context, cli command.Cli, imgRefAndAuth trust.Image
if err != nil {
return err
}
if err := imagePullPrivileged(ctx, cli, updatedImgRefAndAuth, false, platform); err != nil {
if err := imagePullPrivileged(ctx, cli, updatedImgRefAndAuth, false); err != nil {
return err
}
@ -268,7 +268,7 @@ func getTrustedPullTargets(cli command.Cli, imgRefAndAuth trust.ImageRefAndAuth)
}
// imagePullPrivileged pulls the image and displays it to the output
func imagePullPrivileged(ctx context.Context, cli command.Cli, imgRefAndAuth trust.ImageRefAndAuth, all bool, platform string) error {
func imagePullPrivileged(ctx context.Context, cli command.Cli, imgRefAndAuth trust.ImageRefAndAuth, all bool) error {
ref := reference.FamiliarString(imgRefAndAuth.Reference())
encodedAuth, err := command.EncodeAuthToBase64(*imgRefAndAuth.AuthConfig())
@ -280,8 +280,8 @@ func imagePullPrivileged(ctx context.Context, cli command.Cli, imgRefAndAuth tru
RegistryAuth: encodedAuth,
PrivilegeFunc: requestPrivilege,
All: all,
Platform: platform,
}
responseBody, err := cli.Client().ImagePull(ctx, ref, options)
if err != nil {
return err

View File

@ -1,93 +0,0 @@
package manifest
import (
"fmt"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/manifest/store"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
type annotateOptions struct {
target string // the target manifest list name (also transaction ID)
image string // the manifest to annotate within the list
variant string // an architecture variant
os string
arch string
osFeatures []string
}
// NewAnnotateCommand creates a new `docker manifest annotate` command
func newAnnotateCommand(dockerCli command.Cli) *cobra.Command {
var opts annotateOptions
cmd := &cobra.Command{
Use: "annotate [OPTIONS] MANIFEST_LIST MANIFEST",
Short: "Add additional information to a local image manifest",
Args: cli.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
opts.target = args[0]
opts.image = args[1]
return runManifestAnnotate(dockerCli, opts)
},
}
flags := cmd.Flags()
flags.StringVar(&opts.os, "os", "", "Set operating system")
flags.StringVar(&opts.arch, "arch", "", "Set architecture")
flags.StringSliceVar(&opts.osFeatures, "os-features", []string{}, "Set operating system feature")
flags.StringVar(&opts.variant, "variant", "", "Set architecture variant")
return cmd
}
func runManifestAnnotate(dockerCli command.Cli, opts annotateOptions) error {
targetRef, err := normalizeReference(opts.target)
if err != nil {
return errors.Wrapf(err, "annotate: error parsing name for manifest list %s", opts.target)
}
imgRef, err := normalizeReference(opts.image)
if err != nil {
return errors.Wrapf(err, "annotate: error parsing name for manifest %s", opts.image)
}
manifestStore := dockerCli.ManifestStore()
imageManifest, err := manifestStore.Get(targetRef, imgRef)
switch {
case store.IsNotFound(err):
return fmt.Errorf("manifest for image %s does not exist in %s", opts.image, opts.target)
case err != nil:
return err
}
// Update the mf
if opts.os != "" {
imageManifest.Platform.OS = opts.os
}
if opts.arch != "" {
imageManifest.Platform.Architecture = opts.arch
}
for _, osFeature := range opts.osFeatures {
imageManifest.Platform.OSFeatures = appendIfUnique(imageManifest.Platform.OSFeatures, osFeature)
}
if opts.variant != "" {
imageManifest.Platform.Variant = opts.variant
}
if !isValidOSArch(imageManifest.Platform.OS, imageManifest.Platform.Architecture) {
return errors.Errorf("manifest entry for image has unsupported os/arch combination: %s/%s", opts.os, opts.arch)
}
return manifestStore.Save(targetRef, imgRef, imageManifest)
}
func appendIfUnique(list []string, str string) []string {
for _, s := range list {
if s == str {
return list
}
}
return append(list, str)
}

View File

@ -1,78 +0,0 @@
package manifest
import (
"io/ioutil"
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/testutil"
"github.com/gotestyourself/gotestyourself/golden"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestManifestAnnotateError(t *testing.T) {
testCases := []struct {
args []string
expectedError string
}{
{
args: []string{"too-few-arguments"},
expectedError: "requires exactly 2 arguments",
},
{
args: []string{"th!si'sa/fa!ke/li$t/name", "example.com/alpine:3.0"},
expectedError: "error parsing name for manifest list",
},
{
args: []string{"example.com/list:v1", "th!si'sa/fa!ke/im@ge/nam32"},
expectedError: "error parsing name for manifest",
},
}
for _, tc := range testCases {
cli := test.NewFakeCli(nil)
cmd := newAnnotateCommand(cli)
cmd.SetArgs(tc.args)
cmd.SetOutput(ioutil.Discard)
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
}
}
func TestManifestAnnotate(t *testing.T) {
store, cleanup := newTempManifestStore(t)
defer cleanup()
cli := test.NewFakeCli(nil)
cli.SetManifestStore(store)
namedRef := ref(t, "alpine:3.0")
imageManifest := fullImageManifest(t, namedRef)
err := store.Save(ref(t, "list:v1"), namedRef, imageManifest)
require.NoError(t, err)
cmd := newAnnotateCommand(cli)
cmd.SetArgs([]string{"example.com/list:v1", "example.com/fake:0.0"})
cmd.SetOutput(ioutil.Discard)
expectedError := "manifest for image example.com/fake:0.0 does not exist"
testutil.ErrorContains(t, cmd.Execute(), expectedError)
cmd.SetArgs([]string{"example.com/list:v1", "example.com/alpine:3.0"})
cmd.Flags().Set("os", "freebsd")
cmd.Flags().Set("arch", "fake")
cmd.Flags().Set("os-features", "feature1")
cmd.Flags().Set("variant", "v7")
expectedError = "manifest entry for image has unsupported os/arch combination"
testutil.ErrorContains(t, cmd.Execute(), expectedError)
cmd.Flags().Set("arch", "arm")
require.NoError(t, cmd.Execute())
cmd = newInspectCommand(cli)
err = cmd.Flags().Set("verbose", "true")
require.NoError(t, err)
cmd.SetArgs([]string{"example.com/list:v1", "example.com/alpine:3.0"})
require.NoError(t, cmd.Execute())
actual := cli.OutBuffer()
expected := golden.Get(t, "inspect-annotate.golden")
assert.Equal(t, string(expected), actual.String())
}

View File

@ -1,46 +0,0 @@
package manifest
import (
manifesttypes "github.com/docker/cli/cli/manifest/types"
"github.com/docker/cli/cli/registry/client"
"github.com/docker/distribution"
"github.com/docker/distribution/reference"
"github.com/opencontainers/go-digest"
"golang.org/x/net/context"
)
type fakeRegistryClient struct {
client.RegistryClient
getManifestFunc func(ctx context.Context, ref reference.Named) (manifesttypes.ImageManifest, error)
getManifestListFunc func(ctx context.Context, ref reference.Named) ([]manifesttypes.ImageManifest, error)
mountBlobFunc func(ctx context.Context, source reference.Canonical, target reference.Named) error
putManifestFunc func(ctx context.Context, source reference.Named, mf distribution.Manifest) (digest.Digest, error)
}
func (c *fakeRegistryClient) GetManifest(ctx context.Context, ref reference.Named) (manifesttypes.ImageManifest, error) {
if c.getManifestFunc != nil {
return c.getManifestFunc(ctx, ref)
}
return manifesttypes.ImageManifest{}, nil
}
func (c *fakeRegistryClient) GetManifestList(ctx context.Context, ref reference.Named) ([]manifesttypes.ImageManifest, error) {
if c.getManifestListFunc != nil {
return c.getManifestListFunc(ctx, ref)
}
return nil, nil
}
func (c *fakeRegistryClient) MountBlob(ctx context.Context, source reference.Canonical, target reference.Named) error {
if c.mountBlobFunc != nil {
return c.mountBlobFunc(ctx, source, target)
}
return nil
}
func (c *fakeRegistryClient) PutManifest(ctx context.Context, ref reference.Named, mf distribution.Manifest) (digest.Digest, error) {
if c.putManifestFunc != nil {
return c.putManifestFunc(ctx, ref, mf)
}
return digest.Digest(""), nil
}

View File

@ -1,45 +0,0 @@
package manifest
import (
"fmt"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
)
// NewManifestCommand returns a cobra command for `manifest` subcommands
func NewManifestCommand(dockerCli command.Cli) *cobra.Command {
// use dockerCli as command.Cli
cmd := &cobra.Command{
Use: "manifest COMMAND",
Short: "Manage Docker image manifests and manifest lists",
Long: manifestDescription,
Args: cli.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString())
},
Annotations: map[string]string{"experimentalCLI": ""},
}
cmd.AddCommand(
newCreateListCommand(dockerCli),
newInspectCommand(dockerCli),
newAnnotateCommand(dockerCli),
newPushListCommand(dockerCli),
)
return cmd
}
var manifestDescription = `
The **docker manifest** command has subcommands for managing image manifests and
manifest lists. A manifest list allows you to use one name to refer to the same image
built for multiple architectures.
To see help for a subcommand, use:
docker manifest CMD --help
For full details on using docker manifest lists, see the registry v2 specification.
`

View File

@ -1,82 +0,0 @@
package manifest
import (
"fmt"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/manifest/store"
"github.com/docker/docker/registry"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"golang.org/x/net/context"
)
type createOpts struct {
amend bool
insecure bool
}
func newCreateListCommand(dockerCli command.Cli) *cobra.Command {
opts := createOpts{}
cmd := &cobra.Command{
Use: "create MANFEST_LIST MANIFEST [MANIFEST...]",
Short: "Create a local manifest list for annotating and pushing to a registry",
Args: cli.RequiresMinArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
return createManifestList(dockerCli, args, opts)
},
}
flags := cmd.Flags()
flags.BoolVar(&opts.insecure, "insecure", false, "allow communication with an insecure registry")
flags.BoolVarP(&opts.amend, "amend", "a", false, "Amend an existing manifest list")
return cmd
}
func createManifestList(dockerCli command.Cli, args []string, opts createOpts) error {
newRef := args[0]
targetRef, err := normalizeReference(newRef)
if err != nil {
return errors.Wrapf(err, "error parsing name for manifest list %s", newRef)
}
_, err = registry.ParseRepositoryInfo(targetRef)
if err != nil {
return errors.Wrapf(err, "error parsing repository name for manifest list %s", newRef)
}
manifestStore := dockerCli.ManifestStore()
_, err = manifestStore.GetList(targetRef)
switch {
case store.IsNotFound(err):
// New manifest list
case err != nil:
return err
case !opts.amend:
return errors.Errorf("refusing to amend an existing manifest list with no --amend flag")
}
ctx := context.Background()
// Now create the local manifest list transaction by looking up the manifest schemas
// for the constituent images:
manifests := args[1:]
for _, manifestRef := range manifests {
namedRef, err := normalizeReference(manifestRef)
if err != nil {
// TODO: wrap error?
return err
}
manifest, err := getManifest(ctx, dockerCli, targetRef, namedRef, opts.insecure)
if err != nil {
return err
}
if err := manifestStore.Save(targetRef, namedRef, manifest); err != nil {
return err
}
}
fmt.Fprintf(dockerCli.Out(), "Created manifest list %s\n", targetRef.String())
return nil
}

View File

@ -1,117 +0,0 @@
package manifest
import (
"io/ioutil"
"testing"
manifesttypes "github.com/docker/cli/cli/manifest/types"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/testutil"
"github.com/docker/distribution/reference"
"github.com/gotestyourself/gotestyourself/golden"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/net/context"
)
func TestManifestCreateErrors(t *testing.T) {
testCases := []struct {
args []string
expectedError string
}{
{
args: []string{"too-few-arguments"},
expectedError: "requires at least 2 arguments",
},
{
args: []string{"th!si'sa/fa!ke/li$t/name", "example.com/alpine:3.0"},
expectedError: "error parsing name for manifest list",
},
}
for _, tc := range testCases {
cli := test.NewFakeCli(nil)
cmd := newCreateListCommand(cli)
cmd.SetArgs(tc.args)
cmd.SetOutput(ioutil.Discard)
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
}
}
// create a manifest list, then overwrite it, and inspect to see if the old one is still there
func TestManifestCreateAmend(t *testing.T) {
store, cleanup := newTempManifestStore(t)
defer cleanup()
cli := test.NewFakeCli(nil)
cli.SetManifestStore(store)
namedRef := ref(t, "alpine:3.0")
imageManifest := fullImageManifest(t, namedRef)
err := store.Save(ref(t, "list:v1"), namedRef, imageManifest)
require.NoError(t, err)
namedRef = ref(t, "alpine:3.1")
imageManifest = fullImageManifest(t, namedRef)
err = store.Save(ref(t, "list:v1"), namedRef, imageManifest)
require.NoError(t, err)
cmd := newCreateListCommand(cli)
cmd.SetArgs([]string{"example.com/list:v1", "example.com/alpine:3.1"})
cmd.Flags().Set("amend", "true")
cmd.SetOutput(ioutil.Discard)
err = cmd.Execute()
require.NoError(t, err)
// make a new cli to clear the buffers
cli = test.NewFakeCli(nil)
cli.SetManifestStore(store)
inspectCmd := newInspectCommand(cli)
inspectCmd.SetArgs([]string{"example.com/list:v1"})
require.NoError(t, inspectCmd.Execute())
actual := cli.OutBuffer()
expected := golden.Get(t, "inspect-manifest-list.golden")
assert.Equal(t, string(expected), actual.String())
}
// attempt to overwrite a saved manifest and get refused
func TestManifestCreateRefuseAmend(t *testing.T) {
store, cleanup := newTempManifestStore(t)
defer cleanup()
cli := test.NewFakeCli(nil)
cli.SetManifestStore(store)
namedRef := ref(t, "alpine:3.0")
imageManifest := fullImageManifest(t, namedRef)
err := store.Save(ref(t, "list:v1"), namedRef, imageManifest)
require.NoError(t, err)
cmd := newCreateListCommand(cli)
cmd.SetArgs([]string{"example.com/list:v1", "example.com/alpine:3.0"})
cmd.SetOutput(ioutil.Discard)
err = cmd.Execute()
assert.EqualError(t, err, "refusing to amend an existing manifest list with no --amend flag")
}
// attempt to make a manifest list without valid images
func TestManifestCreateNoManifest(t *testing.T) {
store, cleanup := newTempManifestStore(t)
defer cleanup()
cli := test.NewFakeCli(nil)
cli.SetManifestStore(store)
cli.SetRegistryClient(&fakeRegistryClient{
getManifestFunc: func(_ context.Context, ref reference.Named) (manifesttypes.ImageManifest, error) {
return manifesttypes.ImageManifest{}, errors.Errorf("No such image: %v", ref)
},
getManifestListFunc: func(ctx context.Context, ref reference.Named) ([]manifesttypes.ImageManifest, error) {
return nil, errors.Errorf("No such manifest: %s", ref)
},
})
cmd := newCreateListCommand(cli)
cmd.SetArgs([]string{"example.com/list:v1", "example.com/alpine:3.0"})
cmd.SetOutput(ioutil.Discard)
err := cmd.Execute()
assert.EqualError(t, err, "No such image: example.com/alpine:3.0")
}

View File

@ -1,147 +0,0 @@
package manifest
import (
"bytes"
"encoding/json"
"fmt"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/manifest/types"
"github.com/docker/distribution/manifest/manifestlist"
"github.com/docker/distribution/reference"
"github.com/docker/docker/registry"
"github.com/spf13/cobra"
"golang.org/x/net/context"
)
type inspectOptions struct {
ref string
list string
verbose bool
insecure bool
}
// NewInspectCommand creates a new `docker manifest inspect` command
func newInspectCommand(dockerCli command.Cli) *cobra.Command {
var opts inspectOptions
cmd := &cobra.Command{
Use: "inspect [OPTIONS] [MANIFEST_LIST] MANIFEST",
Short: "Display an image manifest, or manifest list",
Args: cli.RequiresRangeArgs(1, 2),
RunE: func(cmd *cobra.Command, args []string) error {
switch len(args) {
case 1:
opts.ref = args[0]
case 2:
opts.list = args[0]
opts.ref = args[1]
}
return runInspect(dockerCli, opts)
},
}
flags := cmd.Flags()
flags.BoolVar(&opts.insecure, "insecure", false, "allow communication with an insecure registry")
flags.BoolVarP(&opts.verbose, "verbose", "v", false, "Output additional info including layers and platform")
return cmd
}
func runInspect(dockerCli command.Cli, opts inspectOptions) error {
namedRef, err := normalizeReference(opts.ref)
if err != nil {
return err
}
// If list reference is provided, display the local manifest in a list
if opts.list != "" {
listRef, err := normalizeReference(opts.list)
if err != nil {
return err
}
imageManifest, err := dockerCli.ManifestStore().Get(listRef, namedRef)
if err != nil {
return err
}
return printManifest(dockerCli, imageManifest, opts)
}
// Try a local manifest list first
localManifestList, err := dockerCli.ManifestStore().GetList(namedRef)
if err == nil {
return printManifestList(dockerCli, namedRef, localManifestList, opts)
}
// Next try a remote manifest
ctx := context.Background()
registryClient := dockerCli.RegistryClient(opts.insecure)
imageManifest, err := registryClient.GetManifest(ctx, namedRef)
if err == nil {
return printManifest(dockerCli, imageManifest, opts)
}
// Finally try a remote manifest list
manifestList, err := registryClient.GetManifestList(ctx, namedRef)
if err != nil {
return err
}
return printManifestList(dockerCli, namedRef, manifestList, opts)
}
func printManifest(dockerCli command.Cli, manifest types.ImageManifest, opts inspectOptions) error {
buffer := new(bytes.Buffer)
if !opts.verbose {
_, raw, err := manifest.Payload()
if err != nil {
return err
}
if err := json.Indent(buffer, raw, "", "\t"); err != nil {
return err
}
fmt.Fprintln(dockerCli.Out(), buffer.String())
return nil
}
jsonBytes, err := json.MarshalIndent(manifest, "", "\t")
if err != nil {
return err
}
dockerCli.Out().Write(append(jsonBytes, '\n'))
return nil
}
func printManifestList(dockerCli command.Cli, namedRef reference.Named, list []types.ImageManifest, opts inspectOptions) error {
if !opts.verbose {
targetRepo, err := registry.ParseRepositoryInfo(namedRef)
if err != nil {
return err
}
manifests := []manifestlist.ManifestDescriptor{}
// More than one response. This is a manifest list.
for _, img := range list {
mfd, err := buildManifestDescriptor(targetRepo, img)
if err != nil {
return fmt.Errorf("error assembling ManifestDescriptor")
}
manifests = append(manifests, mfd)
}
deserializedML, err := manifestlist.FromDescriptors(manifests)
if err != nil {
return err
}
jsonBytes, err := deserializedML.MarshalJSON()
if err != nil {
return err
}
fmt.Fprintln(dockerCli.Out(), string(jsonBytes))
return nil
}
jsonBytes, err := json.MarshalIndent(list, "", "\t")
if err != nil {
return err
}
dockerCli.Out().Write(append(jsonBytes, '\n'))
return nil
}

View File

@ -1,131 +0,0 @@
package manifest
import (
"io/ioutil"
"os"
"testing"
"github.com/docker/cli/cli/manifest/store"
"github.com/docker/cli/cli/manifest/types"
manifesttypes "github.com/docker/cli/cli/manifest/types"
"github.com/docker/cli/internal/test"
"github.com/docker/distribution"
"github.com/docker/distribution/manifest/schema2"
"github.com/docker/distribution/reference"
"github.com/gotestyourself/gotestyourself/golden"
"github.com/opencontainers/go-digest"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/net/context"
)
func newTempManifestStore(t *testing.T) (store.Store, func()) {
tmpdir, err := ioutil.TempDir("", "test-manifest-storage")
require.NoError(t, err)
return store.NewStore(tmpdir), func() { os.RemoveAll(tmpdir) }
}
func ref(t *testing.T, name string) reference.Named {
named, err := reference.ParseNamed("example.com/" + name)
require.NoError(t, err)
return named
}
func fullImageManifest(t *testing.T, ref reference.Named) types.ImageManifest {
man, err := schema2.FromStruct(schema2.Manifest{
Versioned: schema2.SchemaVersion,
Config: distribution.Descriptor{
Digest: "sha256:7328f6f8b41890597575cbaadc884e7386ae0acc53b747401ebce5cf0d624560",
Size: 1520,
MediaType: schema2.MediaTypeImageConfig,
},
Layers: []distribution.Descriptor{
{
MediaType: schema2.MediaTypeLayer,
Size: 1990402,
Digest: "sha256:88286f41530e93dffd4b964e1db22ce4939fffa4a4c665dab8591fbab03d4926",
},
},
})
require.NoError(t, err)
// TODO: include image data for verbose inspect
return types.NewImageManifest(ref, digest.Digest("sha256:7328f6f8b41890597575cbaadc884e7386ae0acc53b747401ebce5cf0d62abcd"), types.Image{OS: "linux", Architecture: "amd64"}, man)
}
func TestInspectCommandLocalManifestNotFound(t *testing.T) {
store, cleanup := newTempManifestStore(t)
defer cleanup()
cli := test.NewFakeCli(nil)
cli.SetManifestStore(store)
cmd := newInspectCommand(cli)
cmd.SetOutput(ioutil.Discard)
cmd.SetArgs([]string{"example.com/list:v1", "example.com/alpine:3.0"})
err := cmd.Execute()
assert.EqualError(t, err, "No such manifest: example.com/alpine:3.0")
}
func TestInspectCommandNotFound(t *testing.T) {
store, cleanup := newTempManifestStore(t)
defer cleanup()
cli := test.NewFakeCli(nil)
cli.SetManifestStore(store)
cli.SetRegistryClient(&fakeRegistryClient{
getManifestFunc: func(_ context.Context, _ reference.Named) (manifesttypes.ImageManifest, error) {
return manifesttypes.ImageManifest{}, errors.New("missing")
},
getManifestListFunc: func(ctx context.Context, ref reference.Named) ([]manifesttypes.ImageManifest, error) {
return nil, errors.Errorf("No such manifest: %s", ref)
},
})
cmd := newInspectCommand(cli)
cmd.SetOutput(ioutil.Discard)
cmd.SetArgs([]string{"example.com/alpine:3.0"})
err := cmd.Execute()
assert.EqualError(t, err, "No such manifest: example.com/alpine:3.0")
}
func TestInspectCommandLocalManifest(t *testing.T) {
store, cleanup := newTempManifestStore(t)
defer cleanup()
cli := test.NewFakeCli(nil)
cli.SetManifestStore(store)
namedRef := ref(t, "alpine:3.0")
imageManifest := fullImageManifest(t, namedRef)
err := store.Save(ref(t, "list:v1"), namedRef, imageManifest)
require.NoError(t, err)
cmd := newInspectCommand(cli)
cmd.SetArgs([]string{"example.com/list:v1", "example.com/alpine:3.0"})
require.NoError(t, cmd.Execute())
actual := cli.OutBuffer()
expected := golden.Get(t, "inspect-manifest.golden")
assert.Equal(t, string(expected), actual.String())
}
func TestInspectcommandRemoteManifest(t *testing.T) {
store, cleanup := newTempManifestStore(t)
defer cleanup()
cli := test.NewFakeCli(nil)
cli.SetManifestStore(store)
cli.SetRegistryClient(&fakeRegistryClient{
getManifestFunc: func(_ context.Context, ref reference.Named) (manifesttypes.ImageManifest, error) {
return fullImageManifest(t, ref), nil
},
})
cmd := newInspectCommand(cli)
cmd.SetOutput(ioutil.Discard)
cmd.SetArgs([]string{"example.com/alpine:3.0"})
require.NoError(t, cmd.Execute())
actual := cli.OutBuffer()
expected := golden.Get(t, "inspect-manifest.golden")
assert.Equal(t, string(expected), actual.String())
}

View File

@ -1,273 +0,0 @@
package manifest
import (
"encoding/json"
"fmt"
"io"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/manifest/types"
registryclient "github.com/docker/cli/cli/registry/client"
"github.com/docker/distribution/manifest/manifestlist"
"github.com/docker/distribution/manifest/schema2"
"github.com/docker/distribution/reference"
"github.com/docker/docker/registry"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"golang.org/x/net/context"
)
type pushOpts struct {
insecure bool
purge bool
target string
}
type mountRequest struct {
ref reference.Named
manifest types.ImageManifest
}
type manifestBlob struct {
canonical reference.Canonical
os string
}
type pushRequest struct {
targetRef reference.Named
list *manifestlist.DeserializedManifestList
mountRequests []mountRequest
manifestBlobs []manifestBlob
insecure bool
}
func newPushListCommand(dockerCli command.Cli) *cobra.Command {
opts := pushOpts{}
cmd := &cobra.Command{
Use: "push [OPTIONS] MANIFEST_LIST",
Short: "Push a manifest list to a repository",
Args: cli.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
opts.target = args[0]
return runPush(dockerCli, opts)
},
}
flags := cmd.Flags()
flags.BoolVarP(&opts.purge, "purge", "p", false, "Remove the local manifest list after push")
flags.BoolVar(&opts.insecure, "insecure", false, "Allow push to an insecure registry")
return cmd
}
func runPush(dockerCli command.Cli, opts pushOpts) error {
targetRef, err := normalizeReference(opts.target)
if err != nil {
return err
}
manifests, err := dockerCli.ManifestStore().GetList(targetRef)
if err != nil {
return err
}
if len(manifests) == 0 {
return errors.Errorf("%s not found", targetRef)
}
pushRequest, err := buildPushRequest(manifests, targetRef, opts.insecure)
if err != nil {
return err
}
ctx := context.Background()
if err := pushList(ctx, dockerCli, pushRequest); err != nil {
return err
}
if opts.purge {
return dockerCli.ManifestStore().Remove(targetRef)
}
return nil
}
func buildPushRequest(manifests []types.ImageManifest, targetRef reference.Named, insecure bool) (pushRequest, error) {
req := pushRequest{targetRef: targetRef, insecure: insecure}
var err error
req.list, err = buildManifestList(manifests, targetRef)
if err != nil {
return req, err
}
targetRepo, err := registry.ParseRepositoryInfo(targetRef)
if err != nil {
return req, err
}
targetRepoName, err := registryclient.RepoNameForReference(targetRepo.Name)
if err != nil {
return req, err
}
for _, imageManifest := range manifests {
manifestRepoName, err := registryclient.RepoNameForReference(imageManifest.Ref)
if err != nil {
return req, err
}
repoName, _ := reference.WithName(manifestRepoName)
if repoName.Name() != targetRepoName {
blobs, err := buildBlobRequestList(imageManifest, repoName)
if err != nil {
return req, err
}
req.manifestBlobs = append(req.manifestBlobs, blobs...)
manifestPush, err := buildPutManifestRequest(imageManifest, targetRef)
if err != nil {
return req, err
}
req.mountRequests = append(req.mountRequests, manifestPush)
}
}
return req, nil
}
func buildManifestList(manifests []types.ImageManifest, targetRef reference.Named) (*manifestlist.DeserializedManifestList, error) {
targetRepoInfo, err := registry.ParseRepositoryInfo(targetRef)
if err != nil {
return nil, err
}
descriptors := []manifestlist.ManifestDescriptor{}
for _, imageManifest := range manifests {
if imageManifest.Platform.Architecture == "" || imageManifest.Platform.OS == "" {
return nil, errors.Errorf(
"manifest %s must have an OS and Architecture to be pushed to a registry", imageManifest.Ref)
}
descriptor, err := buildManifestDescriptor(targetRepoInfo, imageManifest)
if err != nil {
return nil, err
}
descriptors = append(descriptors, descriptor)
}
return manifestlist.FromDescriptors(descriptors)
}
func buildManifestDescriptor(targetRepo *registry.RepositoryInfo, imageManifest types.ImageManifest) (manifestlist.ManifestDescriptor, error) {
repoInfo, err := registry.ParseRepositoryInfo(imageManifest.Ref)
if err != nil {
return manifestlist.ManifestDescriptor{}, err
}
manifestRepoHostname := reference.Domain(repoInfo.Name)
targetRepoHostname := reference.Domain(targetRepo.Name)
if manifestRepoHostname != targetRepoHostname {
return manifestlist.ManifestDescriptor{}, errors.Errorf("cannot use source images from a different registry than the target image: %s != %s", manifestRepoHostname, targetRepoHostname)
}
mediaType, raw, err := imageManifest.Payload()
if err != nil {
return manifestlist.ManifestDescriptor{}, err
}
manifest := manifestlist.ManifestDescriptor{
Platform: imageManifest.Platform,
}
manifest.Descriptor.Digest = imageManifest.Digest
manifest.Size = int64(len(raw))
manifest.MediaType = mediaType
if err = manifest.Descriptor.Digest.Validate(); err != nil {
return manifestlist.ManifestDescriptor{}, errors.Wrapf(err,
"digest parse of image %q failed", imageManifest.Ref)
}
return manifest, nil
}
func buildBlobRequestList(imageManifest types.ImageManifest, repoName reference.Named) ([]manifestBlob, error) {
var blobReqs []manifestBlob
for _, blobDigest := range imageManifest.Blobs() {
canonical, err := reference.WithDigest(repoName, blobDigest)
if err != nil {
return nil, err
}
blobReqs = append(blobReqs, manifestBlob{canonical: canonical, os: imageManifest.Platform.OS})
}
return blobReqs, nil
}
// nolint: interfacer
func buildPutManifestRequest(imageManifest types.ImageManifest, targetRef reference.Named) (mountRequest, error) {
refWithoutTag, err := reference.WithName(targetRef.Name())
if err != nil {
return mountRequest{}, err
}
mountRef, err := reference.WithDigest(refWithoutTag, imageManifest.Digest)
if err != nil {
return mountRequest{}, err
}
// This indentation has to be added to ensure sha parity with the registry
v2ManifestBytes, err := json.MarshalIndent(imageManifest.SchemaV2Manifest, "", " ")
if err != nil {
return mountRequest{}, err
}
// indent only the DeserializedManifest portion of this, in order to maintain parity with the registry
// and not alter the sha
var v2Manifest schema2.DeserializedManifest
if err = v2Manifest.UnmarshalJSON(v2ManifestBytes); err != nil {
return mountRequest{}, err
}
imageManifest.SchemaV2Manifest = &v2Manifest
return mountRequest{ref: mountRef, manifest: imageManifest}, err
}
func pushList(ctx context.Context, dockerCli command.Cli, req pushRequest) error {
rclient := dockerCli.RegistryClient(req.insecure)
if err := mountBlobs(ctx, rclient, req.targetRef, req.manifestBlobs); err != nil {
return err
}
if err := pushReferences(ctx, dockerCli.Out(), rclient, req.mountRequests); err != nil {
return err
}
dgst, err := rclient.PutManifest(ctx, req.targetRef, req.list)
if err != nil {
return err
}
fmt.Fprintln(dockerCli.Out(), dgst.String())
return nil
}
func pushReferences(ctx context.Context, out io.Writer, client registryclient.RegistryClient, mounts []mountRequest) error {
for _, mount := range mounts {
newDigest, err := client.PutManifest(ctx, mount.ref, mount.manifest)
if err != nil {
return err
}
fmt.Fprintf(out, "Pushed ref %s with digest: %s\n", mount.ref, newDigest)
}
return nil
}
func mountBlobs(ctx context.Context, client registryclient.RegistryClient, ref reference.Named, blobs []manifestBlob) error {
for _, blob := range blobs {
err := client.MountBlob(ctx, blob.canonical, ref)
switch err.(type) {
case nil:
case registryclient.ErrBlobCreated:
if blob.os != "windows" {
return fmt.Errorf("error mounting %s to %s", blob.canonical, ref)
}
default:
return err
}
}
return nil
}

View File

@ -1,73 +0,0 @@
package manifest
import (
"io/ioutil"
"testing"
manifesttypes "github.com/docker/cli/cli/manifest/types"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/testutil"
"github.com/docker/distribution/reference"
"github.com/pkg/errors"
"github.com/stretchr/testify/require"
"golang.org/x/net/context"
)
func newFakeRegistryClient(t *testing.T) *fakeRegistryClient {
require.NoError(t, nil)
return &fakeRegistryClient{
getManifestFunc: func(_ context.Context, _ reference.Named) (manifesttypes.ImageManifest, error) {
return manifesttypes.ImageManifest{}, errors.New("")
},
getManifestListFunc: func(_ context.Context, _ reference.Named) ([]manifesttypes.ImageManifest, error) {
return nil, errors.Errorf("")
},
}
}
func TestManifestPushErrors(t *testing.T) {
testCases := []struct {
args []string
expectedError string
}{
{
args: []string{"one-arg", "extra-arg"},
expectedError: "requires exactly 1 argument",
},
{
args: []string{"th!si'sa/fa!ke/li$t/-name"},
expectedError: "invalid reference format",
},
}
for _, tc := range testCases {
cli := test.NewFakeCli(nil)
cmd := newPushListCommand(cli)
cmd.SetArgs(tc.args)
cmd.SetOutput(ioutil.Discard)
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
}
}
// store a one-image manifest list and puah it
func TestManifestPush(t *testing.T) {
store, sCleanup := newTempManifestStore(t)
defer sCleanup()
registry := newFakeRegistryClient(t)
cli := test.NewFakeCli(nil)
cli.SetManifestStore(store)
cli.SetRegistryClient(registry)
namedRef := ref(t, "alpine:3.0")
imageManifest := fullImageManifest(t, namedRef)
err := store.Save(ref(t, "list:v1"), namedRef, imageManifest)
require.NoError(t, err)
cmd := newPushListCommand(cli)
cmd.SetArgs([]string{"example.com/list:v1"})
err = cmd.Execute()
require.NoError(t, err)
}

View File

@ -1,28 +0,0 @@
{
"Ref": "example.com/alpine:3.0",
"Digest": "sha256:7328f6f8b41890597575cbaadc884e7386ae0acc53b747401ebce5cf0d62abcd",
"SchemaV2Manifest": {
"schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"config": {
"mediaType": "application/vnd.docker.container.image.v1+json",
"size": 1520,
"digest": "sha256:7328f6f8b41890597575cbaadc884e7386ae0acc53b747401ebce5cf0d624560"
},
"layers": [
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 1990402,
"digest": "sha256:88286f41530e93dffd4b964e1db22ce4939fffa4a4c665dab8591fbab03d4926"
}
]
},
"Platform": {
"architecture": "arm",
"os": "freebsd",
"os.features": [
"feature1"
],
"variant": "v7"
}
}

View File

@ -1,24 +0,0 @@
{
"schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.list.v2+json",
"manifests": [
{
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"size": 428,
"digest": "sha256:7328f6f8b41890597575cbaadc884e7386ae0acc53b747401ebce5cf0d62abcd",
"platform": {
"architecture": "amd64",
"os": "linux"
}
},
{
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"size": 428,
"digest": "sha256:7328f6f8b41890597575cbaadc884e7386ae0acc53b747401ebce5cf0d62abcd",
"platform": {
"architecture": "amd64",
"os": "linux"
}
}
]
}

View File

@ -1,16 +0,0 @@
{
"schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"config": {
"mediaType": "application/vnd.docker.container.image.v1+json",
"size": 1520,
"digest": "sha256:7328f6f8b41890597575cbaadc884e7386ae0acc53b747401ebce5cf0d624560"
},
"layers": [
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 1990402,
"digest": "sha256:88286f41530e93dffd4b964e1db22ce4939fffa4a4c665dab8591fbab03d4926"
}
]
}

View File

@ -1,79 +0,0 @@
package manifest
import (
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/manifest/store"
"github.com/docker/cli/cli/manifest/types"
"github.com/docker/distribution/reference"
"golang.org/x/net/context"
)
type osArch struct {
os string
arch string
}
// Remove any unsupported os/arch combo
// list of valid os/arch values (see "Optional Environment Variables" section
// of https://golang.org/doc/install/source
// Added linux/s390x as we know System z support already exists
var validOSArches = map[osArch]bool{
{os: "darwin", arch: "386"}: true,
{os: "darwin", arch: "amd64"}: true,
{os: "darwin", arch: "arm"}: true,
{os: "darwin", arch: "arm64"}: true,
{os: "dragonfly", arch: "amd64"}: true,
{os: "freebsd", arch: "386"}: true,
{os: "freebsd", arch: "amd64"}: true,
{os: "freebsd", arch: "arm"}: true,
{os: "linux", arch: "386"}: true,
{os: "linux", arch: "amd64"}: true,
{os: "linux", arch: "arm"}: true,
{os: "linux", arch: "arm64"}: true,
{os: "linux", arch: "ppc64le"}: true,
{os: "linux", arch: "mips64"}: true,
{os: "linux", arch: "mips64le"}: true,
{os: "linux", arch: "s390x"}: true,
{os: "netbsd", arch: "386"}: true,
{os: "netbsd", arch: "amd64"}: true,
{os: "netbsd", arch: "arm"}: true,
{os: "openbsd", arch: "386"}: true,
{os: "openbsd", arch: "amd64"}: true,
{os: "openbsd", arch: "arm"}: true,
{os: "plan9", arch: "386"}: true,
{os: "plan9", arch: "amd64"}: true,
{os: "solaris", arch: "amd64"}: true,
{os: "windows", arch: "386"}: true,
{os: "windows", arch: "amd64"}: true,
}
func isValidOSArch(os string, arch string) bool {
// check for existence of this combo
_, ok := validOSArches[osArch{os, arch}]
return ok
}
func normalizeReference(ref string) (reference.Named, error) {
namedRef, err := reference.ParseNormalizedNamed(ref)
if err != nil {
return nil, err
}
if _, isDigested := namedRef.(reference.Canonical); !isDigested {
return reference.TagNameOnly(namedRef), nil
}
return namedRef, nil
}
// getManifest from the local store, and fallback to the remote registry if it
// doesn't exist locally
func getManifest(ctx context.Context, dockerCli command.Cli, listRef, namedRef reference.Named, insecure bool) (types.ImageManifest, error) {
data, err := dockerCli.ManifestStore().Get(listRef, namedRef)
switch {
case store.IsNotFound(err):
return dockerCli.RegistryClient(insecure).GetManifest(ctx, namedRef)
case err != nil:
return types.ImageManifest{}, err
default:
return data, nil
}
}

View File

@ -14,14 +14,11 @@ import (
// NewNodeCommand returns a cobra command for `node` subcommands
func NewNodeCommand(dockerCli command.Cli) *cobra.Command {
cmd := &cobra.Command{
Use: "node",
Short: "Manage Swarm nodes",
Args: cli.NoArgs,
RunE: command.ShowHelp(dockerCli.Err()),
Annotations: map[string]string{
"version": "1.24",
"swarm": "",
},
Use: "node",
Short: "Manage Swarm nodes",
Args: cli.NoArgs,
RunE: command.ShowHelp(dockerCli.Err()),
Annotations: map[string]string{"version": "1.24"},
}
cmd.AddCommand(
newDemoteCommand(dockerCli),

View File

@ -56,8 +56,8 @@ func TestNodeList(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
nodeListFunc: func() ([]swarm.Node, error) {
return []swarm.Node{
*Node(NodeID("nodeID1"), Hostname("node-2-foo"), Manager(Leader()), EngineVersion(".")),
*Node(NodeID("nodeID2"), Hostname("node-10-foo"), Manager(), EngineVersion("18.03.0-ce")),
*Node(NodeID("nodeID1"), Hostname("node-2-foo"), Manager(Leader())),
*Node(NodeID("nodeID2"), Hostname("node-10-foo"), Manager()),
*Node(NodeID("nodeID3"), Hostname("node-1-foo")),
}, nil
},

View File

@ -1,4 +1,4 @@
ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS ENGINE VERSION
nodeID3 node-1-foo Ready Active 1.13.0
nodeID1 * node-2-foo Ready Active Leader .
nodeID2 node-10-foo Ready Active Reachable 18.03.0-ce
ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS
nodeID3 node-1-foo Ready Active
nodeID1 * node-2-foo Ready Active Leader
nodeID2 node-10-foo Ready Active Reachable

View File

@ -1,59 +0,0 @@
package command
import (
"fmt"
"os"
)
// Orchestrator type acts as an enum describing supported orchestrators.
type Orchestrator string
const (
// OrchestratorKubernetes orchestrator
OrchestratorKubernetes = Orchestrator("kubernetes")
// OrchestratorSwarm orchestrator
OrchestratorSwarm = Orchestrator("swarm")
orchestratorUnset = Orchestrator("unset")
defaultOrchestrator = OrchestratorSwarm
envVarDockerOrchestrator = "DOCKER_ORCHESTRATOR"
)
func normalize(flag string) Orchestrator {
switch flag {
case "kubernetes":
return OrchestratorKubernetes
case "swarm":
return OrchestratorSwarm
default:
return orchestratorUnset
}
}
// GetOrchestrator checks DOCKER_ORCHESTRATOR environment variable and configuration file
// orchestrator value and returns user defined Orchestrator.
func GetOrchestrator(isExperimental bool, flagValue, value string) Orchestrator {
// Non experimental CLI has kubernetes disabled
if !isExperimental {
return defaultOrchestrator
}
// Check flag
if o := normalize(flagValue); o != orchestratorUnset {
return o
}
// Check environment variable
env := os.Getenv(envVarDockerOrchestrator)
if o := normalize(env); o != orchestratorUnset {
return o
}
// Check specified orchestrator
if o := normalize(value); o != orchestratorUnset {
return o
}
if value != "" {
fmt.Fprintf(os.Stderr, "Specified orchestrator %q is invalid. Please use either kubernetes or swarm\n", value)
}
// Nothing set, use default orchestrator
return defaultOrchestrator
}

View File

@ -1,45 +0,0 @@
package plugin
import (
"io"
"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
"golang.org/x/net/context"
)
type fakeClient struct {
client.Client
pluginCreateFunc func(createContext io.Reader, createOptions types.PluginCreateOptions) error
pluginDisableFunc func(name string, disableOptions types.PluginDisableOptions) error
pluginEnableFunc func(name string, options types.PluginEnableOptions) error
pluginRemoveFunc func(name string, options types.PluginRemoveOptions) error
}
func (c *fakeClient) PluginCreate(ctx context.Context, createContext io.Reader, createOptions types.PluginCreateOptions) error {
if c.pluginCreateFunc != nil {
return c.pluginCreateFunc(createContext, createOptions)
}
return nil
}
func (c *fakeClient) PluginEnable(ctx context.Context, name string, enableOptions types.PluginEnableOptions) error {
if c.pluginEnableFunc != nil {
return c.pluginEnableFunc(name, enableOptions)
}
return nil
}
func (c *fakeClient) PluginDisable(context context.Context, name string, disableOptions types.PluginDisableOptions) error {
if c.pluginDisableFunc != nil {
return c.pluginDisableFunc(name, disableOptions)
}
return nil
}
func (c *fakeClient) PluginRemove(context context.Context, name string, removeOptions types.PluginRemoveOptions) error {
if c.pluginRemoveFunc != nil {
return c.pluginRemoveFunc(name, removeOptions)
}
return nil
}

View File

@ -1,114 +0,0 @@
package plugin
import (
"fmt"
"io"
"io/ioutil"
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/testutil"
"github.com/docker/docker/api/types"
"github.com/gotestyourself/gotestyourself/fs"
"github.com/stretchr/testify/assert"
)
func TestCreateErrors(t *testing.T) {
testCases := []struct {
args []string
expectedError string
}{
{
args: []string{},
expectedError: "requires at least 2 arguments",
},
{
args: []string{"INVALID_TAG", "context-dir"},
expectedError: "invalid",
},
{
args: []string{"plugin-foo", "nonexistent_context_dir"},
expectedError: "no such file or directory",
},
}
for _, tc := range testCases {
cli := test.NewFakeCli(&fakeClient{})
cmd := newCreateCommand(cli)
cmd.SetArgs(tc.args)
cmd.SetOutput(ioutil.Discard)
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
}
}
func TestCreateErrorOnFileAsContextDir(t *testing.T) {
tmpFile := fs.NewFile(t, "file-as-context-dir")
defer tmpFile.Remove()
cli := test.NewFakeCli(&fakeClient{})
cmd := newCreateCommand(cli)
cmd.SetArgs([]string{"plugin-foo", tmpFile.Path()})
cmd.SetOutput(ioutil.Discard)
testutil.ErrorContains(t, cmd.Execute(), "context must be a directory")
}
func TestCreateErrorOnContextDirWithoutConfig(t *testing.T) {
tmpDir := fs.NewDir(t, "plugin-create-test")
defer tmpDir.Remove()
cli := test.NewFakeCli(&fakeClient{})
cmd := newCreateCommand(cli)
cmd.SetArgs([]string{"plugin-foo", tmpDir.Path()})
cmd.SetOutput(ioutil.Discard)
testutil.ErrorContains(t, cmd.Execute(), "config.json: no such file or directory")
}
func TestCreateErrorOnInvalidConfig(t *testing.T) {
tmpDir := fs.NewDir(t, "plugin-create-test",
fs.WithDir("rootfs"),
fs.WithFile("config.json", "invalid-config-contents"))
defer tmpDir.Remove()
cli := test.NewFakeCli(&fakeClient{})
cmd := newCreateCommand(cli)
cmd.SetArgs([]string{"plugin-foo", tmpDir.Path()})
cmd.SetOutput(ioutil.Discard)
testutil.ErrorContains(t, cmd.Execute(), "invalid")
}
func TestCreateErrorFromDaemon(t *testing.T) {
tmpDir := fs.NewDir(t, "plugin-create-test",
fs.WithDir("rootfs"),
fs.WithFile("config.json", `{ "Name": "plugin-foo" }`))
defer tmpDir.Remove()
cli := test.NewFakeCli(&fakeClient{
pluginCreateFunc: func(createContext io.Reader, createOptions types.PluginCreateOptions) error {
return fmt.Errorf("Error creating plugin")
},
})
cmd := newCreateCommand(cli)
cmd.SetArgs([]string{"plugin-foo", tmpDir.Path()})
cmd.SetOutput(ioutil.Discard)
testutil.ErrorContains(t, cmd.Execute(), "Error creating plugin")
}
func TestCreatePlugin(t *testing.T) {
tmpDir := fs.NewDir(t, "plugin-create-test",
fs.WithDir("rootfs"),
fs.WithFile("config.json", `{ "Name": "plugin-foo" }`))
defer tmpDir.Remove()
cli := test.NewFakeCli(&fakeClient{
pluginCreateFunc: func(createContext io.Reader, createOptions types.PluginCreateOptions) error {
return nil
},
})
cmd := newCreateCommand(cli)
cmd.SetArgs([]string{"plugin-foo", tmpDir.Path()})
assert.NoError(t, cmd.Execute())
assert.Equal(t, "plugin-foo\n", cli.OutBuffer().String())
}

View File

@ -1,58 +0,0 @@
package plugin
import (
"fmt"
"io/ioutil"
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/testutil"
"github.com/docker/docker/api/types"
"github.com/stretchr/testify/assert"
)
func TestPluginDisableErrors(t *testing.T) {
testCases := []struct {
args []string
expectedError string
pluginDisableFunc func(name string, disableOptions types.PluginDisableOptions) error
}{
{
args: []string{},
expectedError: "requires exactly 1 argument",
},
{
args: []string{"too", "many", "arguments"},
expectedError: "requires exactly 1 argument",
},
{
args: []string{"plugin-foo"},
expectedError: "Error disabling plugin",
pluginDisableFunc: func(name string, disableOptions types.PluginDisableOptions) error {
return fmt.Errorf("Error disabling plugin")
},
},
}
for _, tc := range testCases {
cmd := newDisableCommand(
test.NewFakeCli(&fakeClient{
pluginDisableFunc: tc.pluginDisableFunc,
}))
cmd.SetArgs(tc.args)
cmd.SetOutput(ioutil.Discard)
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
}
}
func TestPluginDisable(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
pluginDisableFunc: func(name string, disableOptions types.PluginDisableOptions) error {
return nil
},
})
cmd := newDisableCommand(cli)
cmd.SetArgs([]string{"plugin-foo"})
assert.NoError(t, cmd.Execute())
assert.Equal(t, "plugin-foo\n", cli.OutBuffer().String())
}

View File

@ -30,7 +30,7 @@ func newEnableCommand(dockerCli command.Cli) *cobra.Command {
}
flags := cmd.Flags()
flags.IntVar(&opts.timeout, "timeout", 30, "HTTP client timeout (in seconds)")
flags.IntVar(&opts.timeout, "timeout", 0, "HTTP client timeout (in seconds)")
return cmd
}

View File

@ -1,70 +0,0 @@
package plugin
import (
"fmt"
"io/ioutil"
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/testutil"
"github.com/docker/docker/api/types"
"github.com/stretchr/testify/assert"
)
func TestPluginEnableErrors(t *testing.T) {
testCases := []struct {
args []string
flags map[string]string
pluginEnableFunc func(name string, options types.PluginEnableOptions) error
expectedError string
}{
{
args: []string{},
expectedError: "requires exactly 1 argument",
},
{
args: []string{"too-many", "arguments"},
expectedError: "requires exactly 1 argument",
},
{
args: []string{"plugin-foo"},
pluginEnableFunc: func(name string, options types.PluginEnableOptions) error {
return fmt.Errorf("failed to enable plugin")
},
expectedError: "failed to enable plugin",
},
{
args: []string{"plugin-foo"},
flags: map[string]string{
"timeout": "-1",
},
expectedError: "negative timeout -1 is invalid",
},
}
for _, tc := range testCases {
cmd := newEnableCommand(
test.NewFakeCli(&fakeClient{
pluginEnableFunc: tc.pluginEnableFunc,
}))
cmd.SetArgs(tc.args)
for key, value := range tc.flags {
cmd.Flags().Set(key, value)
}
cmd.SetOutput(ioutil.Discard)
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
}
}
func TestPluginEnable(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
pluginEnableFunc: func(name string, options types.PluginEnableOptions) error {
return nil
},
})
cmd := newEnableCommand(cli)
cmd.SetArgs([]string{"plugin-foo"})
assert.NoError(t, cmd.Execute())
assert.Equal(t, "plugin-foo\n", cli.OutBuffer().String())
}

View File

@ -1,71 +0,0 @@
package plugin
import (
"fmt"
"io/ioutil"
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/testutil"
"github.com/docker/docker/api/types"
"github.com/stretchr/testify/assert"
)
func TestRemoveErrors(t *testing.T) {
testCases := []struct {
args []string
pluginRemoveFunc func(name string, options types.PluginRemoveOptions) error
expectedError string
}{
{
args: []string{},
expectedError: "requires at least 1 argument",
},
{
args: []string{"plugin-foo"},
pluginRemoveFunc: func(name string, options types.PluginRemoveOptions) error {
return fmt.Errorf("Error removing plugin")
},
expectedError: "Error removing plugin",
},
}
for _, tc := range testCases {
cli := test.NewFakeCli(&fakeClient{
pluginRemoveFunc: tc.pluginRemoveFunc,
})
cmd := newRemoveCommand(cli)
cmd.SetArgs(tc.args)
cmd.SetOutput(ioutil.Discard)
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
}
}
func TestRemove(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
pluginRemoveFunc: func(name string, options types.PluginRemoveOptions) error {
return nil
},
})
cmd := newRemoveCommand(cli)
cmd.SetArgs([]string{"plugin-foo"})
assert.NoError(t, cmd.Execute())
assert.Equal(t, "plugin-foo\n", cli.OutBuffer().String())
}
func TestRemoveWithForceOption(t *testing.T) {
force := false
cli := test.NewFakeCli(&fakeClient{
pluginRemoveFunc: func(name string, options types.PluginRemoveOptions) error {
force = options.Force
return nil
},
})
cmd := newRemoveCommand(cli)
cmd.SetArgs([]string{"plugin-foo"})
cmd.Flags().Set("force", "true")
assert.NoError(t, cmd.Execute())
assert.True(t, force)
assert.Equal(t, "plugin-foo\n", cli.OutBuffer().String())
}

View File

@ -10,13 +10,14 @@ import (
"runtime"
"strings"
"golang.org/x/net/context"
"github.com/docker/distribution/reference"
"github.com/docker/docker/api/types"
registrytypes "github.com/docker/docker/api/types/registry"
"github.com/docker/docker/pkg/term"
"github.com/docker/docker/registry"
"github.com/pkg/errors"
"golang.org/x/net/context"
)
// ElectAuthServer returns the default registry to use (by asking the daemon)

View File

@ -10,14 +10,11 @@ import (
// NewSecretCommand returns a cobra command for `secret` subcommands
func NewSecretCommand(dockerCli command.Cli) *cobra.Command {
cmd := &cobra.Command{
Use: "secret",
Short: "Manage Docker secrets",
Args: cli.NoArgs,
RunE: command.ShowHelp(dockerCli.Err()),
Annotations: map[string]string{
"version": "1.25",
"swarm": "",
},
Use: "secret",
Short: "Manage Docker secrets",
Args: cli.NoArgs,
RunE: command.ShowHelp(dockerCli.Err()),
Annotations: map[string]string{"version": "1.25"},
}
cmd.AddCommand(
newSecretListCommand(dockerCli),

View File

@ -16,11 +16,10 @@ import (
)
type createOptions struct {
name string
driver string
templateDriver string
file string
labels opts.ListOpts
name string
driver string
file string
labels opts.ListOpts
}
func newSecretCreateCommand(dockerCli command.Cli) *cobra.Command {
@ -44,8 +43,6 @@ func newSecretCreateCommand(dockerCli command.Cli) *cobra.Command {
flags.VarP(&options.labels, "label", "l", "Secret labels")
flags.StringVarP(&options.driver, "driver", "d", "", "Secret driver")
flags.SetAnnotation("driver", "version", []string{"1.31"})
flags.StringVar(&options.templateDriver, "template-driver", "", "Template driver")
flags.SetAnnotation("driver", "version", []string{"1.37"})
return cmd
}
@ -74,11 +71,7 @@ func runSecretCreate(dockerCli command.Cli, options createOptions) error {
Name: options.driver,
}
}
if options.templateDriver != "" {
spec.Templating = &swarm.Driver{
Name: options.templateDriver,
}
}
r, err := client.SecretCreate(ctx, spec)
if err != nil {
return err

View File

@ -11,6 +11,7 @@ import (
"github.com/docker/cli/internal/test/testutil"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/swarm"
"github.com/gotestyourself/gotestyourself/golden"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
)
@ -51,22 +52,15 @@ func TestSecretCreateErrors(t *testing.T) {
func TestSecretCreateWithName(t *testing.T) {
name := "foo"
data, err := ioutil.ReadFile(filepath.Join("testdata", secretDataFile))
assert.NoError(t, err)
expected := swarm.SecretSpec{
Annotations: swarm.Annotations{
Name: name,
Labels: make(map[string]string),
},
Data: data,
}
var actual []byte
cli := test.NewFakeCli(&fakeClient{
secretCreateFunc: func(spec swarm.SecretSpec) (types.SecretCreateResponse, error) {
if !reflect.DeepEqual(spec, expected) {
return types.SecretCreateResponse{}, errors.Errorf("expected %+v, got %+v", expected, spec)
if spec.Name != name {
return types.SecretCreateResponse{}, errors.Errorf("expected name %q, got %q", name, spec.Name)
}
actual = spec.Data
return types.SecretCreateResponse{
ID: "ID-" + spec.Name,
}, nil
@ -76,6 +70,7 @@ func TestSecretCreateWithName(t *testing.T) {
cmd := newSecretCreateCommand(cli)
cmd.SetArgs([]string{name, filepath.Join("testdata", secretDataFile)})
assert.NoError(t, cmd.Execute())
golden.Assert(t, string(actual), secretDataFile)
assert.Equal(t, "ID-"+name, strings.TrimSpace(cli.OutBuffer().String()))
}
@ -91,7 +86,7 @@ func TestSecretCreateWithDriver(t *testing.T) {
return types.SecretCreateResponse{}, errors.Errorf("expected name %q, got %q", name, spec.Name)
}
if spec.Driver.Name != expectedDriver.Name {
if !reflect.DeepEqual(spec.Driver.Name, expectedDriver.Name) {
return types.SecretCreateResponse{}, errors.Errorf("expected driver %v, got %v", expectedDriver, spec.Labels)
}
@ -108,35 +103,6 @@ func TestSecretCreateWithDriver(t *testing.T) {
assert.Equal(t, "ID-"+name, strings.TrimSpace(cli.OutBuffer().String()))
}
func TestSecretCreateWithTemplatingDriver(t *testing.T) {
expectedDriver := &swarm.Driver{
Name: "template-driver",
}
name := "foo"
cli := test.NewFakeCli(&fakeClient{
secretCreateFunc: func(spec swarm.SecretSpec) (types.SecretCreateResponse, error) {
if spec.Name != name {
return types.SecretCreateResponse{}, errors.Errorf("expected name %q, got %q", name, spec.Name)
}
if spec.Templating.Name != expectedDriver.Name {
return types.SecretCreateResponse{}, errors.Errorf("expected driver %v, got %v", expectedDriver, spec.Labels)
}
return types.SecretCreateResponse{
ID: "ID-" + spec.Name,
}, nil
},
})
cmd := newSecretCreateCommand(cli)
cmd.SetArgs([]string{name})
cmd.Flags().Set("template-driver", expectedDriver.Name)
assert.NoError(t, cmd.Execute())
assert.Equal(t, "ID-"+name, strings.TrimSpace(cli.OutBuffer().String()))
}
func TestSecretCreateWithLabels(t *testing.T) {
expectedLabels := map[string]string{
"lbl1": "Label-foo",

View File

@ -16,7 +16,6 @@ type fakeClient struct {
serviceListFunc func(context.Context, types.ServiceListOptions) ([]swarm.Service, error)
taskListFunc func(context.Context, types.TaskListOptions) ([]swarm.Task, error)
infoFunc func(ctx context.Context) (types.Info, error)
networkInspectFunc func(ctx context.Context, networkID string, options types.NetworkInspectOptions) (types.NetworkResource, error)
}
func (f *fakeClient) NodeList(ctx context.Context, options types.NodeListOptions) ([]swarm.Node, error) {
@ -61,13 +60,6 @@ func (f *fakeClient) Info(ctx context.Context) (types.Info, error) {
return f.infoFunc(ctx)
}
func (f *fakeClient) NetworkInspect(ctx context.Context, networkID string, options types.NetworkInspectOptions) (types.NetworkResource, error) {
if f.networkInspectFunc != nil {
return f.networkInspectFunc(ctx, networkID, options)
}
return types.NetworkResource{}, nil
}
func newService(id string, name string) swarm.Service {
return swarm.Service{
ID: id,

View File

@ -10,14 +10,11 @@ import (
// NewServiceCommand returns a cobra command for `service` subcommands
func NewServiceCommand(dockerCli command.Cli) *cobra.Command {
cmd := &cobra.Command{
Use: "service",
Short: "Manage services",
Args: cli.NoArgs,
RunE: command.ShowHelp(dockerCli.Err()),
Annotations: map[string]string{
"version": "1.24",
"swarm": "",
},
Use: "service",
Short: "Manage services",
Args: cli.NoArgs,
RunE: command.ShowHelp(dockerCli.Err()),
Annotations: map[string]string{"version": "1.24"},
}
cmd.AddCommand(
newCreateCommand(dockerCli),

View File

@ -5,7 +5,6 @@ import (
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
cliopts "github.com/docker/cli/opts"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/versions"
"github.com/spf13/cobra"
@ -59,9 +58,6 @@ func newCreateCommand(dockerCli command.Cli) *cobra.Command {
flags.Var(&opts.hosts, flagHost, "Set one or more custom host-to-IP mappings (host:ip)")
flags.SetAnnotation(flagHost, "version", []string{"1.25"})
flags.Var(cliopts.NewListOptsRef(&opts.resources.resGenericResources, ValidateSingleGenericResource), "generic-resource", "User defined resources")
flags.SetAnnotation(flagHostAdd, "version", []string{"1.32"})
flags.SetInterspersed(false)
return cmd
}

View File

@ -1,105 +0,0 @@
package service
import (
"fmt"
"strings"
"github.com/pkg/errors"
"github.com/docker/docker/api/types/swarm"
swarmapi "github.com/docker/swarmkit/api"
"github.com/docker/swarmkit/api/genericresource"
)
// GenericResource is a concept that a user can use to advertise user-defined
// resources on a node and thus better place services based on these resources.
// E.g: NVIDIA GPUs, Intel FPGAs, ...
// See https://github.com/docker/swarmkit/blob/master/design/generic_resources.md
// ValidateSingleGenericResource validates that a single entry in the
// generic resource list is valid.
// i.e 'GPU=UID1' is valid however 'GPU:UID1' or 'UID1' isn't
func ValidateSingleGenericResource(val string) (string, error) {
if strings.Count(val, "=") < 1 {
return "", fmt.Errorf("invalid generic-resource format `%s` expected `name=value`", val)
}
return val, nil
}
// ParseGenericResources parses an array of Generic resourceResources
// Requesting Named Generic Resources for a service is not supported this
// is filtered here.
func ParseGenericResources(value []string) ([]swarm.GenericResource, error) {
if len(value) == 0 {
return nil, nil
}
resources, err := genericresource.Parse(value)
if err != nil {
return nil, errors.Wrapf(err, "invalid generic resource specification")
}
swarmResources := genericResourcesFromGRPC(resources)
for _, res := range swarmResources {
if res.NamedResourceSpec != nil {
return nil, fmt.Errorf("invalid generic-resource request `%s=%s`, Named Generic Resources is not supported for service create or update", res.NamedResourceSpec.Kind, res.NamedResourceSpec.Value)
}
}
return swarmResources, nil
}
// genericResourcesFromGRPC converts a GRPC GenericResource to a GenericResource
func genericResourcesFromGRPC(genericRes []*swarmapi.GenericResource) []swarm.GenericResource {
var generic []swarm.GenericResource
for _, res := range genericRes {
var current swarm.GenericResource
switch r := res.Resource.(type) {
case *swarmapi.GenericResource_DiscreteResourceSpec:
current.DiscreteResourceSpec = &swarm.DiscreteGenericResource{
Kind: r.DiscreteResourceSpec.Kind,
Value: r.DiscreteResourceSpec.Value,
}
case *swarmapi.GenericResource_NamedResourceSpec:
current.NamedResourceSpec = &swarm.NamedGenericResource{
Kind: r.NamedResourceSpec.Kind,
Value: r.NamedResourceSpec.Value,
}
}
generic = append(generic, current)
}
return generic
}
func buildGenericResourceMap(genericRes []swarm.GenericResource) (map[string]swarm.GenericResource, error) {
m := make(map[string]swarm.GenericResource)
for _, res := range genericRes {
if res.DiscreteResourceSpec == nil {
return nil, fmt.Errorf("invalid generic-resource `%+v` for service task", res)
}
_, ok := m[res.DiscreteResourceSpec.Kind]
if ok {
return nil, fmt.Errorf("duplicate generic-resource `%+v` for service task", res.DiscreteResourceSpec.Kind)
}
m[res.DiscreteResourceSpec.Kind] = res
}
return m, nil
}
func buildGenericResourceList(genericRes map[string]swarm.GenericResource) []swarm.GenericResource {
var l []swarm.GenericResource
for _, res := range genericRes {
l = append(l, res)
}
return l
}

View File

@ -1,22 +0,0 @@
package service
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestValidateSingleGenericResource(t *testing.T) {
incorrect := []string{"foo", "fooo-bar"}
correct := []string{"foo=bar", "bar=1", "foo=barbar"}
for _, v := range incorrect {
_, err := ValidateSingleGenericResource(v)
assert.Error(t, err)
}
for _, v := range correct {
_, err := ValidateSingleGenericResource(v)
assert.NoError(t, err)
}
}

View File

@ -222,30 +222,23 @@ func (opts updateOptions) rollbackConfig(flags *pflag.FlagSet) *swarm.UpdateConf
}
type resourceOptions struct {
limitCPU opts.NanoCPUs
limitMemBytes opts.MemBytes
resCPU opts.NanoCPUs
resMemBytes opts.MemBytes
resGenericResources []string
limitCPU opts.NanoCPUs
limitMemBytes opts.MemBytes
resCPU opts.NanoCPUs
resMemBytes opts.MemBytes
}
func (r *resourceOptions) ToResourceRequirements() (*swarm.ResourceRequirements, error) {
generic, err := ParseGenericResources(r.resGenericResources)
if err != nil {
return nil, err
}
func (r *resourceOptions) ToResourceRequirements() *swarm.ResourceRequirements {
return &swarm.ResourceRequirements{
Limits: &swarm.Resources{
NanoCPUs: r.limitCPU.Value(),
MemoryBytes: r.limitMemBytes.Value(),
},
Reservations: &swarm.Resources{
NanoCPUs: r.resCPU.Value(),
MemoryBytes: r.resMemBytes.Value(),
GenericResources: generic,
NanoCPUs: r.resCPU.Value(),
MemoryBytes: r.resMemBytes.Value(),
},
}, nil
}
}
type restartPolicyOptions struct {
@ -353,21 +346,22 @@ func (c *credentialSpecOpt) Value() *swarm.CredentialSpec {
return c.value
}
func resolveNetworkID(ctx context.Context, apiClient client.NetworkAPIClient, networkIDOrName string) (string, error) {
nw, err := apiClient.NetworkInspect(ctx, networkIDOrName, types.NetworkInspectOptions{Scope: "swarm"})
return nw.ID, err
}
func convertNetworks(networks opts.NetworkOpt) []swarm.NetworkAttachmentConfig {
func convertNetworks(ctx context.Context, apiClient client.NetworkAPIClient, networks opts.NetworkOpt) ([]swarm.NetworkAttachmentConfig, error) {
var netAttach []swarm.NetworkAttachmentConfig
for _, net := range networks.Value() {
networkIDOrName := net.Target
_, err := apiClient.NetworkInspect(ctx, networkIDOrName, types.NetworkInspectOptions{Scope: "swarm"})
if err != nil {
return nil, err
}
netAttach = append(netAttach, swarm.NetworkAttachmentConfig{
Target: net.Target,
Aliases: net.Aliases,
DriverOpts: net.DriverOpts,
})
}
return netAttach
sort.Sort(byNetworkTarget(netAttach))
return netAttach, nil
}
type endpointOptions struct {
@ -511,8 +505,6 @@ type serviceOptions struct {
healthcheck healthCheckOptions
secrets opts.SecretOpt
configs opts.ConfigOpt
isolation string
}
func newServiceOptions() *serviceOptions {
@ -560,7 +552,7 @@ func (options *serviceOptions) ToStopGracePeriod(flags *pflag.FlagSet) *time.Dur
func (options *serviceOptions) ToService(ctx context.Context, apiClient client.NetworkAPIClient, flags *pflag.FlagSet) (swarm.ServiceSpec, error) {
var service swarm.ServiceSpec
envVariables, err := opts.ReadKVEnvStrings(options.envFile.GetAll(), options.env.GetAll())
envVariables, err := opts.ReadKVStrings(options.envFile.GetAll(), options.env.GetAll())
if err != nil {
return service, err
}
@ -589,17 +581,7 @@ func (options *serviceOptions) ToService(ctx context.Context, apiClient client.N
return service, err
}
networks := convertNetworks(options.networks)
for i, net := range networks {
nwID, err := resolveNetworkID(ctx, apiClient, net.Target)
if err != nil {
return service, err
}
networks[i].Target = nwID
}
sort.Sort(byNetworkTarget(networks))
resources, err := options.resources.ToResourceRequirements()
networks, err := convertNetworks(ctx, apiClient, options.networks)
if err != nil {
return service, err
}
@ -632,10 +614,9 @@ func (options *serviceOptions) ToService(ctx context.Context, apiClient client.N
Hosts: convertExtraHostsToSwarmHosts(options.hosts.GetAll()),
StopGracePeriod: options.ToStopGracePeriod(flags),
Healthcheck: healthConfig,
Isolation: container.Isolation(options.isolation),
},
Networks: networks,
Resources: resources,
Resources: options.resources.ToResourceRequirements(),
RestartPolicy: options.restartPolicy.ToRestartPolicy(flags),
Placement: &swarm.Placement{
Constraints: options.constraints.GetAll(),
@ -803,8 +784,6 @@ func addServiceFlags(flags *pflag.FlagSet, opts *serviceOptions, defaultFlagValu
flags.StringVar(&opts.stopSignal, flagStopSignal, "", "Signal to stop the container")
flags.SetAnnotation(flagStopSignal, "version", []string{"1.28"})
flags.StringVar(&opts.isolation, flagIsolation, "", "Service container isolation mode")
flags.SetAnnotation(flagIsolation, "version", []string{"1.35"})
}
const (
@ -834,8 +813,6 @@ const (
flagEnvFile = "env-file"
flagEnvRemove = "env-rm"
flagEnvAdd = "env-add"
flagGenericResourcesRemove = "generic-resource-rm"
flagGenericResourcesAdd = "generic-resource-add"
flagGroup = "group"
flagGroupAdd = "group-add"
flagGroupRemove = "group-rm"
@ -902,5 +879,4 @@ const (
flagConfig = "config"
flagConfigAdd = "config-add"
flagConfigRemove = "config-rm"
flagIsolation = "isolation"
)

View File

@ -1,17 +1,12 @@
package service
import (
"context"
"fmt"
"testing"
"time"
"github.com/docker/cli/opts"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/swarm"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestMemBytesString(t *testing.T) {
@ -90,75 +85,3 @@ func TestHealthCheckOptionsToHealthConfigConflict(t *testing.T) {
_, err := opt.toHealthConfig()
assert.EqualError(t, err, "--no-healthcheck conflicts with --health-* options")
}
func TestResourceOptionsToResourceRequirements(t *testing.T) {
incorrectOptions := []resourceOptions{
{
resGenericResources: []string{"foo=bar", "foo=1"},
},
{
resGenericResources: []string{"foo=bar", "foo=baz"},
},
{
resGenericResources: []string{"foo=bar"},
},
{
resGenericResources: []string{"foo=1", "foo=2"},
},
}
for _, opt := range incorrectOptions {
_, err := opt.ToResourceRequirements()
assert.Error(t, err)
}
correctOptions := []resourceOptions{
{
resGenericResources: []string{"foo=1"},
},
{
resGenericResources: []string{"foo=1", "bar=2"},
},
}
for _, opt := range correctOptions {
r, err := opt.ToResourceRequirements()
assert.NoError(t, err)
assert.Len(t, r.Reservations.GenericResources, len(opt.resGenericResources))
}
}
func TestToServiceNetwork(t *testing.T) {
nws := []types.NetworkResource{
{Name: "aaa-network", ID: "id555"},
{Name: "mmm-network", ID: "id999"},
{Name: "zzz-network", ID: "id111"},
}
client := &fakeClient{
networkInspectFunc: func(ctx context.Context, networkID string, options types.NetworkInspectOptions) (types.NetworkResource, error) {
for _, network := range nws {
if network.ID == networkID || network.Name == networkID {
return network, nil
}
}
return types.NetworkResource{}, fmt.Errorf("network not found: %s", networkID)
},
}
nwo := opts.NetworkOpt{}
nwo.Set("zzz-network")
nwo.Set("mmm-network")
nwo.Set("aaa-network")
o := newServiceOptions()
o.mode = "replicated"
o.networks = nwo
ctx := context.Background()
flags := newCreateCommand(nil).Flags()
service, err := o.ToService(ctx, client, flags)
require.NoError(t, err)
assert.Equal(t, []swarm.NetworkAttachmentConfig{{Target: "id111"}, {Target: "id555"}, {Target: "id999"}}, service.TaskTemplate.Networks)
}

View File

@ -95,12 +95,6 @@ func newUpdateCommand(dockerCli command.Cli) *cobra.Command {
flags.Var(&options.hosts, flagHostAdd, "Add a custom host-to-IP mapping (host:ip)")
flags.SetAnnotation(flagHostAdd, "version", []string{"1.25"})
// Add needs parsing, Remove only needs the key
flags.Var(newListOptsVar(), flagGenericResourcesRemove, "Remove a Generic resource")
flags.SetAnnotation(flagHostAdd, "version", []string{"1.32"})
flags.Var(newListOptsVarWithValidator(ValidateSingleGenericResource), flagGenericResourcesAdd, "Add a Generic resource")
flags.SetAnnotation(flagHostAdd, "version", []string{"1.32"})
return cmd
}
@ -108,10 +102,6 @@ func newListOptsVar() *opts.ListOpts {
return opts.NewListOptsRef(&[]string{}, nil)
}
func newListOptsVarWithValidator(validator opts.ValidatorFctType) *opts.ListOpts {
return opts.NewListOptsRef(&[]string{}, validator)
}
// nolint: gocyclo
func runUpdate(dockerCli command.Cli, flags *pflag.FlagSet, options *serviceOptions, serviceID string) error {
apiClient := dockerCli.Client()
@ -279,14 +269,6 @@ func updateService(ctx context.Context, apiClient client.NetworkAPIClient, flags
}
}
updateIsolation := func(flag string, field *container.Isolation) error {
if flags.Changed(flag) {
val, _ := flags.GetString(flag)
*field = container.Isolation(val)
}
return nil
}
cspec := spec.TaskTemplate.ContainerSpec
task := &spec.TaskTemplate
@ -306,9 +288,6 @@ func updateService(ctx context.Context, apiClient client.NetworkAPIClient, flags
updateString(flagWorkdir, &cspec.Dir)
updateString(flagUser, &cspec.User)
updateString(flagHostname, &cspec.Hostname)
if err := updateIsolation(flagIsolation, &cspec.Isolation); err != nil {
return err
}
if err := updateMounts(flags, &cspec.Mounts); err != nil {
return err
}
@ -324,14 +303,6 @@ func updateService(ctx context.Context, apiClient client.NetworkAPIClient, flags
updateInt64Value(flagReserveMemory, &task.Resources.Reservations.MemoryBytes)
}
if err := addGenericResources(flags, task); err != nil {
return err
}
if err := removeGenericResources(flags, task); err != nil {
return err
}
updateDurationOpt(flagStopGracePeriod, &cspec.StopGracePeriod)
if anyChanged(flags, flagRestartCondition, flagRestartDelay, flagRestartMaxAttempts, flagRestartWindow) {
@ -488,72 +459,6 @@ func anyChanged(flags *pflag.FlagSet, fields ...string) bool {
return false
}
func addGenericResources(flags *pflag.FlagSet, spec *swarm.TaskSpec) error {
if !flags.Changed(flagGenericResourcesAdd) {
return nil
}
if spec.Resources == nil {
spec.Resources = &swarm.ResourceRequirements{}
}
if spec.Resources.Reservations == nil {
spec.Resources.Reservations = &swarm.Resources{}
}
values := flags.Lookup(flagGenericResourcesAdd).Value.(*opts.ListOpts).GetAll()
generic, err := ParseGenericResources(values)
if err != nil {
return err
}
m, err := buildGenericResourceMap(spec.Resources.Reservations.GenericResources)
if err != nil {
return err
}
for _, toAddRes := range generic {
m[toAddRes.DiscreteResourceSpec.Kind] = toAddRes
}
spec.Resources.Reservations.GenericResources = buildGenericResourceList(m)
return nil
}
func removeGenericResources(flags *pflag.FlagSet, spec *swarm.TaskSpec) error {
// Can only be Discrete Resources
if !flags.Changed(flagGenericResourcesRemove) {
return nil
}
if spec.Resources == nil {
spec.Resources = &swarm.ResourceRequirements{}
}
if spec.Resources.Reservations == nil {
spec.Resources.Reservations = &swarm.Resources{}
}
values := flags.Lookup(flagGenericResourcesRemove).Value.(*opts.ListOpts).GetAll()
m, err := buildGenericResourceMap(spec.Resources.Reservations.GenericResources)
if err != nil {
return err
}
for _, toRemoveRes := range values {
if _, ok := m[toRemoveRes]; !ok {
return fmt.Errorf("could not find generic-resource `%s` to remove it", toRemoveRes)
}
delete(m, toRemoveRes)
}
spec.Resources.Reservations.GenericResources = buildGenericResourceList(m)
return nil
}
func updatePlacementConstraints(flags *pflag.FlagSet, placement *swarm.Placement) {
if flags.Changed(flagConstraintAdd) {
values := flags.Lookup(flagConstraintAdd).Value.(*opts.ListOpts).GetAll()
@ -1119,16 +1024,14 @@ func updateNetworks(ctx context.Context, apiClient client.NetworkAPIClient, flag
if flags.Changed(flagNetworkAdd) {
values := flags.Lookup(flagNetworkAdd).Value.(*opts.NetworkOpt)
networks := convertNetworks(*values)
networks, err := convertNetworks(ctx, apiClient, *values)
if err != nil {
return err
}
for _, network := range networks {
nwID, err := resolveNetworkID(ctx, apiClient, network.Target)
if err != nil {
return err
}
if _, exists := existingNetworks[nwID]; exists {
if _, exists := existingNetworks[network.Target]; exists {
return errors.Errorf("service is already attached to network %s", network.Target)
}
network.Target = nwID
newNetworks = append(newNetworks, network)
existingNetworks[network.Target] = struct{}{}
}

View File

@ -1,7 +1,6 @@
package service
import (
"fmt"
"reflect"
"sort"
"testing"
@ -519,137 +518,3 @@ func TestUpdateStopSignal(t *testing.T) {
updateService(nil, nil, flags, spec)
assert.Equal(t, "SIGWINCH", cspec.StopSignal)
}
func TestUpdateIsolationValid(t *testing.T) {
flags := newUpdateCommand(nil).Flags()
err := flags.Set("isolation", "process")
require.NoError(t, err)
spec := swarm.ServiceSpec{
TaskTemplate: swarm.TaskSpec{
ContainerSpec: &swarm.ContainerSpec{},
},
}
err = updateService(context.Background(), nil, flags, &spec)
require.NoError(t, err)
assert.Equal(t, container.IsolationProcess, spec.TaskTemplate.ContainerSpec.Isolation)
}
func TestUpdateIsolationInvalid(t *testing.T) {
// validation depends on daemon os / version so validation should be done on the daemon side
flags := newUpdateCommand(nil).Flags()
err := flags.Set("isolation", "test")
require.NoError(t, err)
spec := swarm.ServiceSpec{
TaskTemplate: swarm.TaskSpec{
ContainerSpec: &swarm.ContainerSpec{},
},
}
err = updateService(context.Background(), nil, flags, &spec)
require.NoError(t, err)
assert.Equal(t, container.Isolation("test"), spec.TaskTemplate.ContainerSpec.Isolation)
}
func TestAddGenericResources(t *testing.T) {
task := &swarm.TaskSpec{}
flags := newUpdateCommand(nil).Flags()
assert.Nil(t, addGenericResources(flags, task))
flags.Set(flagGenericResourcesAdd, "foo=1")
assert.NoError(t, addGenericResources(flags, task))
assert.Len(t, task.Resources.Reservations.GenericResources, 1)
// Checks that foo isn't added a 2nd time
flags = newUpdateCommand(nil).Flags()
flags.Set(flagGenericResourcesAdd, "bar=1")
assert.NoError(t, addGenericResources(flags, task))
assert.Len(t, task.Resources.Reservations.GenericResources, 2)
}
func TestRemoveGenericResources(t *testing.T) {
task := &swarm.TaskSpec{}
flags := newUpdateCommand(nil).Flags()
assert.Nil(t, removeGenericResources(flags, task))
flags.Set(flagGenericResourcesRemove, "foo")
assert.Error(t, removeGenericResources(flags, task))
flags = newUpdateCommand(nil).Flags()
flags.Set(flagGenericResourcesAdd, "foo=1")
addGenericResources(flags, task)
flags = newUpdateCommand(nil).Flags()
flags.Set(flagGenericResourcesAdd, "bar=1")
addGenericResources(flags, task)
flags = newUpdateCommand(nil).Flags()
flags.Set(flagGenericResourcesRemove, "foo")
assert.NoError(t, removeGenericResources(flags, task))
assert.Len(t, task.Resources.Reservations.GenericResources, 1)
}
func TestUpdateNetworks(t *testing.T) {
ctx := context.Background()
nws := []types.NetworkResource{
{Name: "aaa-network", ID: "id555"},
{Name: "mmm-network", ID: "id999"},
{Name: "zzz-network", ID: "id111"},
}
client := &fakeClient{
networkInspectFunc: func(ctx context.Context, networkID string, options types.NetworkInspectOptions) (types.NetworkResource, error) {
for _, network := range nws {
if network.ID == networkID || network.Name == networkID {
return network, nil
}
}
return types.NetworkResource{}, fmt.Errorf("network not found: %s", networkID)
},
}
svc := swarm.ServiceSpec{
TaskTemplate: swarm.TaskSpec{
ContainerSpec: &swarm.ContainerSpec{},
Networks: []swarm.NetworkAttachmentConfig{
{Target: "id999"},
},
},
}
flags := newUpdateCommand(nil).Flags()
err := flags.Set(flagNetworkAdd, "aaa-network")
require.NoError(t, err)
err = updateService(ctx, client, flags, &svc)
require.NoError(t, err)
assert.Equal(t, []swarm.NetworkAttachmentConfig{{Target: "id555"}, {Target: "id999"}}, svc.TaskTemplate.Networks)
flags = newUpdateCommand(nil).Flags()
err = flags.Set(flagNetworkAdd, "aaa-network")
require.NoError(t, err)
err = updateService(ctx, client, flags, &svc)
assert.EqualError(t, err, "service is already attached to network aaa-network")
assert.Equal(t, []swarm.NetworkAttachmentConfig{{Target: "id555"}, {Target: "id999"}}, svc.TaskTemplate.Networks)
flags = newUpdateCommand(nil).Flags()
err = flags.Set(flagNetworkAdd, "id555")
require.NoError(t, err)
err = updateService(ctx, client, flags, &svc)
assert.EqualError(t, err, "service is already attached to network id555")
assert.Equal(t, []swarm.NetworkAttachmentConfig{{Target: "id555"}, {Target: "id999"}}, svc.TaskTemplate.Networks)
flags = newUpdateCommand(nil).Flags()
err = flags.Set(flagNetworkRemove, "id999")
require.NoError(t, err)
err = updateService(ctx, client, flags, &svc)
assert.NoError(t, err)
assert.Equal(t, []swarm.NetworkAttachmentConfig{{Target: "id555"}}, svc.TaskTemplate.Networks)
flags = newUpdateCommand(nil).Flags()
err = flags.Set(flagNetworkAdd, "mmm-network")
require.NoError(t, err)
err = flags.Set(flagNetworkRemove, "aaa-network")
require.NoError(t, err)
err = updateService(ctx, client, flags, &svc)
assert.NoError(t, err)
assert.Equal(t, []swarm.NetworkAttachmentConfig{{Target: "id999"}}, svc.TaskTemplate.Networks)
}

View File

@ -9,30 +9,19 @@ import (
// NewStackCommand returns a cobra command for `stack` subcommands
func NewStackCommand(dockerCli command.Cli) *cobra.Command {
cmd := &cobra.Command{
Use: "stack",
Short: "Manage Docker stacks",
Args: cli.NoArgs,
RunE: command.ShowHelp(dockerCli.Err()),
Annotations: map[string]string{
"kubernetes": "",
"swarm": "",
"version": "1.25",
},
Use: "stack",
Short: "Manage Docker stacks",
Args: cli.NoArgs,
RunE: command.ShowHelp(dockerCli.Err()),
Annotations: map[string]string{"version": "1.25"},
}
cmd.AddCommand(
newDeployCommand(dockerCli),
newListCommand(dockerCli),
newPsCommand(dockerCli),
newRemoveCommand(dockerCli),
newServicesCommand(dockerCli),
newPsCommand(dockerCli),
)
flags := cmd.PersistentFlags()
flags.String("namespace", "default", "Kubernetes namespace to use")
flags.SetAnnotation("namespace", "kubernetes", nil)
flags.SetAnnotation("namespace", "experimentalCLI", nil)
flags.String("kubeconfig", "", "Kubernetes config file")
flags.SetAnnotation("kubeconfig", "kubernetes", nil)
flags.SetAnnotation("kubeconfig", "experimentalCLI", nil)
return cmd
}
@ -41,10 +30,6 @@ func NewTopLevelDeployCommand(dockerCli command.Cli) *cobra.Command {
cmd := newDeployCommand(dockerCli)
// Remove the aliases at the top level
cmd.Aliases = []string{}
cmd.Annotations = map[string]string{
"experimental": "",
"swarm": "",
"version": "1.25",
}
cmd.Annotations = map[string]string{"experimental": "", "version": "1.25"}
return cmd
}

View File

@ -1,4 +1,4 @@
package swarm
package stack
import (
"github.com/docker/cli/cli/compose/convert"

View File

@ -1,16 +1,36 @@
package stack
import (
"fmt"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/stack/kubernetes"
"github.com/docker/cli/cli/command/stack/options"
"github.com/docker/cli/cli/command/stack/swarm"
"github.com/docker/cli/cli/compose/convert"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/api/types/versions"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"golang.org/x/net/context"
)
const (
defaultNetworkDriver = "overlay"
resolveImageAlways = "always"
resolveImageChanged = "changed"
resolveImageNever = "never"
)
type deployOptions struct {
bundlefile string
composefile string
namespace string
resolveImage string
sendRegistryAuth bool
prune bool
}
func newDeployCommand(dockerCli command.Cli) *cobra.Command {
var opts options.Deploy
var opts deployOptions
cmd := &cobra.Command{
Use: "deploy [OPTIONS] STACK",
@ -18,32 +38,85 @@ func newDeployCommand(dockerCli command.Cli) *cobra.Command {
Short: "Deploy a new stack or update an existing stack",
Args: cli.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
opts.Namespace = args[0]
if dockerCli.ClientInfo().HasKubernetes() {
kli, err := kubernetes.WrapCli(dockerCli, cmd)
if err != nil {
return err
}
return kubernetes.RunDeploy(kli, opts)
}
return swarm.RunDeploy(dockerCli, opts)
opts.namespace = args[0]
return runDeploy(dockerCli, opts)
},
}
flags := cmd.Flags()
flags.StringVar(&opts.Bundlefile, "bundle-file", "", "Path to a Distributed Application Bundle file")
flags.SetAnnotation("bundle-file", "experimental", nil)
flags.SetAnnotation("bundle-file", "swarm", nil)
flags.StringSliceVarP(&opts.Composefiles, "compose-file", "c", []string{}, "Path to a Compose file")
flags.SetAnnotation("compose-file", "version", []string{"1.25"})
flags.BoolVar(&opts.SendRegistryAuth, "with-registry-auth", false, "Send registry authentication details to Swarm agents")
flags.SetAnnotation("with-registry-auth", "swarm", nil)
flags.BoolVar(&opts.Prune, "prune", false, "Prune services that are no longer referenced")
addBundlefileFlag(&opts.bundlefile, flags)
addComposefileFlag(&opts.composefile, flags)
addRegistryAuthFlag(&opts.sendRegistryAuth, flags)
flags.BoolVar(&opts.prune, "prune", false, "Prune services that are no longer referenced")
flags.SetAnnotation("prune", "version", []string{"1.27"})
flags.SetAnnotation("prune", "swarm", nil)
flags.StringVar(&opts.ResolveImage, "resolve-image", swarm.ResolveImageAlways,
`Query the registry to resolve image digest and supported platforms ("`+swarm.ResolveImageAlways+`"|"`+swarm.ResolveImageChanged+`"|"`+swarm.ResolveImageNever+`")`)
flags.StringVar(&opts.resolveImage, "resolve-image", resolveImageAlways,
`Query the registry to resolve image digest and supported platforms ("`+resolveImageAlways+`"|"`+resolveImageChanged+`"|"`+resolveImageNever+`")`)
flags.SetAnnotation("resolve-image", "version", []string{"1.30"})
flags.SetAnnotation("resolve-image", "swarm", nil)
return cmd
}
func runDeploy(dockerCli command.Cli, opts deployOptions) error {
ctx := context.Background()
if err := validateResolveImageFlag(dockerCli, &opts); err != nil {
return err
}
switch {
case opts.bundlefile == "" && opts.composefile == "":
return errors.Errorf("Please specify either a bundle file (with --bundle-file) or a Compose file (with --compose-file).")
case opts.bundlefile != "" && opts.composefile != "":
return errors.Errorf("You cannot specify both a bundle file and a Compose file.")
case opts.bundlefile != "":
return deployBundle(ctx, dockerCli, opts)
default:
return deployCompose(ctx, dockerCli, opts)
}
}
// validateResolveImageFlag validates the opts.resolveImage command line option
// and also turns image resolution off if the version is older than 1.30
func validateResolveImageFlag(dockerCli command.Cli, opts *deployOptions) error {
if opts.resolveImage != resolveImageAlways && opts.resolveImage != resolveImageChanged && opts.resolveImage != resolveImageNever {
return errors.Errorf("Invalid option %s for flag --resolve-image", opts.resolveImage)
}
// client side image resolution should not be done when the supported
// server version is older than 1.30
if versions.LessThan(dockerCli.Client().ClientVersion(), "1.30") {
opts.resolveImage = resolveImageNever
}
return nil
}
// checkDaemonIsSwarmManager does an Info API call to verify that the daemon is
// a swarm manager. This is necessary because we must create networks before we
// create services, but the API call for creating a network does not return a
// proper status code when it can't create a network in the "global" scope.
func checkDaemonIsSwarmManager(ctx context.Context, dockerCli command.Cli) error {
info, err := dockerCli.Client().Info(ctx)
if err != nil {
return err
}
if !info.Swarm.ControlAvailable {
return errors.New("this node is not a swarm manager. Use \"docker swarm init\" or \"docker swarm join\" to connect this node to swarm and try again")
}
return nil
}
// pruneServices removes services that are no longer referenced in the source
func pruneServices(ctx context.Context, dockerCli command.Cli, namespace convert.Namespace, services map[string]struct{}) {
client := dockerCli.Client()
oldServices, err := getServices(ctx, client, namespace.Name())
if err != nil {
fmt.Fprintf(dockerCli.Err(), "Failed to list services: %s", err)
}
pruneServices := []swarm.Service{}
for _, service := range oldServices {
if _, exists := services[namespace.Descope(service.Spec.Name)]; !exists {
pruneServices = append(pruneServices, service)
}
}
removeServices(ctx, dockerCli, pruneServices)
}

View File

@ -1,23 +1,16 @@
package swarm
package stack
import (
"fmt"
"io"
"os"
"golang.org/x/net/context"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/bundlefile"
"github.com/docker/cli/cli/command/stack/options"
"github.com/docker/cli/cli/compose/convert"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/swarm"
"github.com/pkg/errors"
)
func deployBundle(ctx context.Context, dockerCli command.Cli, opts options.Deploy) error {
bundle, err := loadBundlefile(dockerCli.Err(), opts.Namespace, opts.Bundlefile)
func deployBundle(ctx context.Context, dockerCli command.Cli, opts deployOptions) error {
bundle, err := loadBundlefile(dockerCli.Err(), opts.namespace, opts.bundlefile)
if err != nil {
return err
}
@ -26,9 +19,9 @@ func deployBundle(ctx context.Context, dockerCli command.Cli, opts options.Deplo
return err
}
namespace := convert.NewNamespace(opts.Namespace)
namespace := convert.NewNamespace(opts.namespace)
if opts.Prune {
if opts.prune {
services := map[string]struct{}{}
for service := range bundle.Services {
services[service] = struct{}{}
@ -94,31 +87,5 @@ func deployBundle(ctx context.Context, dockerCli command.Cli, opts options.Deplo
if err := createNetworks(ctx, dockerCli, namespace, networks); err != nil {
return err
}
return deployServices(ctx, dockerCli, services, namespace, opts.SendRegistryAuth, opts.ResolveImage)
}
func loadBundlefile(stderr io.Writer, namespace string, path string) (*bundlefile.Bundlefile, error) {
defaultPath := fmt.Sprintf("%s.dab", namespace)
if path == "" {
path = defaultPath
}
if _, err := os.Stat(path); err != nil {
return nil, errors.Errorf(
"Bundle %s not found. Specify the path with --file",
path)
}
fmt.Fprintf(stderr, "Loading bundle from %s\n", path)
reader, err := os.Open(path)
if err != nil {
return nil, err
}
defer reader.Close()
bundle, err := bundlefile.LoadFile(reader)
if err != nil {
return nil, errors.Errorf("Error reading %s: %v\n", path, err)
}
return bundle, err
return deployServices(ctx, dockerCli, services, namespace, opts.sendRegistryAuth, opts.resolveImage)
}

View File

@ -1,12 +1,17 @@
package swarm
package stack
import (
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"sort"
"strings"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/stack/loader"
"github.com/docker/cli/cli/command/stack/options"
"github.com/docker/cli/cli/compose/convert"
"github.com/docker/cli/cli/compose/loader"
composetypes "github.com/docker/cli/cli/compose/types"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
@ -17,19 +22,41 @@ import (
"golang.org/x/net/context"
)
func deployCompose(ctx context.Context, dockerCli command.Cli, opts options.Deploy) error {
config, err := loader.LoadComposefile(dockerCli, opts)
func deployCompose(ctx context.Context, dockerCli command.Cli, opts deployOptions) error {
configDetails, err := getConfigDetails(opts.composefile, dockerCli.In())
if err != nil {
return err
}
config, err := loader.Load(configDetails)
if err != nil {
if fpe, ok := err.(*loader.ForbiddenPropertiesError); ok {
return errors.Errorf("Compose file contains unsupported options:\n\n%s\n",
propertyWarnings(fpe.Properties))
}
return err
}
unsupportedProperties := loader.GetUnsupportedProperties(configDetails)
if len(unsupportedProperties) > 0 {
fmt.Fprintf(dockerCli.Err(), "Ignoring unsupported options: %s\n\n",
strings.Join(unsupportedProperties, ", "))
}
deprecatedProperties := loader.GetDeprecatedProperties(configDetails)
if len(deprecatedProperties) > 0 {
fmt.Fprintf(dockerCli.Err(), "Ignoring deprecated options:\n\n%s\n\n",
propertyWarnings(deprecatedProperties))
}
if err := checkDaemonIsSwarmManager(ctx, dockerCli); err != nil {
return err
}
namespace := convert.NewNamespace(opts.Namespace)
namespace := convert.NewNamespace(opts.namespace)
if opts.Prune {
if opts.prune {
services := map[string]struct{}{}
for _, service := range config.Services {
services[service.Name] = struct{}{}
@ -66,7 +93,7 @@ func deployCompose(ctx context.Context, dockerCli command.Cli, opts options.Depl
if err != nil {
return err
}
return deployServices(ctx, dockerCli, services, namespace, opts.SendRegistryAuth, opts.ResolveImage)
return deployServices(ctx, dockerCli, services, namespace, opts.sendRegistryAuth, opts.resolveImage)
}
func getServicesDeclaredNetworks(serviceConfigs []composetypes.ServiceConfig) map[string]struct{} {
@ -83,6 +110,79 @@ func getServicesDeclaredNetworks(serviceConfigs []composetypes.ServiceConfig) ma
return serviceNetworks
}
func propertyWarnings(properties map[string]string) string {
var msgs []string
for name, description := range properties {
msgs = append(msgs, fmt.Sprintf("%s: %s", name, description))
}
sort.Strings(msgs)
return strings.Join(msgs, "\n\n")
}
func getConfigDetails(composefile string, stdin io.Reader) (composetypes.ConfigDetails, error) {
var details composetypes.ConfigDetails
if composefile == "-" {
workingDir, err := os.Getwd()
if err != nil {
return details, err
}
details.WorkingDir = workingDir
} else {
absPath, err := filepath.Abs(composefile)
if err != nil {
return details, err
}
details.WorkingDir = filepath.Dir(absPath)
}
configFile, err := getConfigFile(composefile, stdin)
if err != nil {
return details, err
}
// TODO: support multiple files
details.ConfigFiles = []composetypes.ConfigFile{*configFile}
details.Environment, err = buildEnvironment(os.Environ())
return details, err
}
func buildEnvironment(env []string) (map[string]string, error) {
result := make(map[string]string, len(env))
for _, s := range env {
// if value is empty, s is like "K=", not "K".
if !strings.Contains(s, "=") {
return result, errors.Errorf("unexpected environment %q", s)
}
kv := strings.SplitN(s, "=", 2)
result[kv[0]] = kv[1]
}
return result, nil
}
func getConfigFile(filename string, stdin io.Reader) (*composetypes.ConfigFile, error) {
var bytes []byte
var err error
if filename == "-" {
bytes, err = ioutil.ReadAll(stdin)
} else {
bytes, err = ioutil.ReadFile(filename)
}
if err != nil {
return nil, err
}
config, err := loader.ParseYAML(bytes)
if err != nil {
return nil, err
}
return &composetypes.ConfigFile{
Filename: filename,
Config: config,
}, nil
}
func validateExternalNetworks(
ctx context.Context,
client dockerclient.NetworkAPIClient,
@ -148,13 +248,13 @@ func createConfigs(
case err == nil:
// config already exists, then we update that
if err := client.ConfigUpdate(ctx, config.ID, config.Meta.Version, configSpec); err != nil {
return errors.Wrapf(err, "failed to update config %s", configSpec.Name)
errors.Wrapf(err, "failed to update config %s", configSpec.Name)
}
case apiclient.IsErrNotFound(err):
// config does not exist, then we create a new one.
fmt.Fprintf(dockerCli.Out(), "Creating config %s\n", configSpec.Name)
if _, err := client.ConfigCreate(ctx, configSpec); err != nil {
return errors.Wrapf(err, "failed to create config %s", configSpec.Name)
errors.Wrapf(err, "failed to create config %s", configSpec.Name)
}
default:
return err
@ -239,7 +339,7 @@ func deployServices(
updateOpts := types.ServiceUpdateOptions{EncodedRegistryAuth: encodedAuth}
switch {
case resolveImage == ResolveImageAlways || (resolveImage == ResolveImageChanged && image != service.Spec.Labels[convert.LabelImage]):
case resolveImage == resolveImageAlways || (resolveImage == resolveImageChanged && image != service.Spec.Labels[convert.LabelImage]):
// image should be updated by the server using QueryRegistry
updateOpts.QueryRegistry = true
case image == service.Spec.Labels[convert.LabelImage]:
@ -269,7 +369,7 @@ func deployServices(
createOpts := types.ServiceCreateOptions{EncodedRegistryAuth: encodedAuth}
// query registry if flag disabling it was not set
if resolveImage == ResolveImageAlways || resolveImage == ResolveImageChanged {
if resolveImage == resolveImageAlways || resolveImage == resolveImageChanged {
createOpts.QueryRegistry = true
}

View File

@ -1,16 +1,56 @@
package swarm
package stack
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/docker/cli/internal/test/network"
"github.com/docker/cli/internal/test/testutil"
"github.com/docker/docker/api/types"
"github.com/gotestyourself/gotestyourself/fs"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/net/context"
)
func TestGetConfigDetails(t *testing.T) {
content := `
version: "3.0"
services:
foo:
image: alpine:3.5
`
file := fs.NewFile(t, "test-get-config-details", fs.WithContent(content))
defer file.Remove()
details, err := getConfigDetails(file.Path(), nil)
require.NoError(t, err)
assert.Equal(t, filepath.Dir(file.Path()), details.WorkingDir)
require.Len(t, details.ConfigFiles, 1)
assert.Equal(t, "3.0", details.ConfigFiles[0].Config["version"])
assert.Len(t, details.Environment, len(os.Environ()))
}
func TestGetConfigDetailsStdin(t *testing.T) {
content := `
version: "3.0"
services:
foo:
image: alpine:3.5
`
details, err := getConfigDetails("-", strings.NewReader(content))
require.NoError(t, err)
cwd, err := os.Getwd()
require.NoError(t, err)
assert.Equal(t, cwd, details.WorkingDir)
require.Len(t, details.ConfigFiles, 1)
assert.Equal(t, "3.0", details.ConfigFiles[0].Config["version"])
assert.Len(t, details.Environment, len(os.Environ()))
}
type notFound struct {
error
}

View File

@ -1,4 +1,4 @@
package swarm
package stack
import (
"testing"
@ -92,7 +92,7 @@ func TestServiceUpdateResolveImageChanged(t *testing.T) {
},
},
}
err := deployServices(ctx, client, spec, namespace, false, ResolveImageChanged)
err := deployServices(ctx, client, spec, namespace, false, resolveImageChanged)
assert.NoError(t, err)
assert.Equal(t, testcase.expectedQueryRegistry, receivedOptions.QueryRegistry)
assert.Equal(t, testcase.expectedImage, receivedService.TaskTemplate.ContainerSpec.Image)

View File

@ -1,32 +0,0 @@
package kubernetes
import (
"fmt"
apiv1beta1 "github.com/docker/cli/kubernetes/compose/v1beta1"
log "github.com/sirupsen/logrus"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
)
// APIPresent checks that an API is installed.
func APIPresent(config *rest.Config) error {
log.Debugf("check API present at %s", config.Host)
clients, err := kubernetes.NewForConfig(config)
if err != nil {
return err
}
groups, err := clients.Discovery().ServerGroups()
if err != nil {
return err
}
for _, group := range groups.Groups {
if group.Name == apiv1beta1.SchemeGroupVersion.Group {
return nil
}
}
return fmt.Errorf("could not find %s api. Install it on your cluster first", apiv1beta1.SchemeGroupVersion.Group)
}

View File

@ -1,76 +0,0 @@
package kubernetes
import (
"fmt"
"os"
"path/filepath"
"github.com/docker/cli/cli/command"
composev1beta1 "github.com/docker/cli/kubernetes/client/clientset_generated/clientset/typed/compose/v1beta1"
"github.com/docker/docker/pkg/homedir"
"github.com/spf13/cobra"
restclient "k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
)
// KubeCli holds kubernetes specifics (client, namespace) with the command.Cli
type KubeCli struct {
command.Cli
kubeConfig *restclient.Config
kubeNamespace string
}
// WrapCli wraps command.Cli with kubernetes specifics
func WrapCli(dockerCli command.Cli, cmd *cobra.Command) (*KubeCli, error) {
var err error
cli := &KubeCli{
Cli: dockerCli,
kubeNamespace: "default",
}
if cmd.Flags().Changed("namespace") {
cli.kubeNamespace, err = cmd.Flags().GetString("namespace")
if err != nil {
return nil, err
}
}
kubeConfig := ""
if cmd.Flags().Changed("kubeconfig") {
kubeConfig, err = cmd.Flags().GetString("kubeconfig")
if err != nil {
return nil, err
}
}
if kubeConfig == "" {
if config := os.Getenv("KUBECONFIG"); config != "" {
kubeConfig = config
} else {
kubeConfig = filepath.Join(homedir.Get(), ".kube/config")
}
}
config, err := clientcmd.BuildConfigFromFlags("", kubeConfig)
if err != nil {
return nil, fmt.Errorf("Failed to load kubernetes configuration file '%s'", kubeConfig)
}
cli.kubeConfig = config
return cli, nil
}
func (c *KubeCli) composeClient() (*Factory, error) {
return NewFactory(c.kubeNamespace, c.kubeConfig)
}
func (c *KubeCli) stacks() (composev1beta1.StackInterface, error) {
err := APIPresent(c.kubeConfig)
if err != nil {
return nil, err
}
clientSet, err := composev1beta1.NewForConfig(c.kubeConfig)
if err != nil {
return nil, err
}
return clientSet.Stacks(c.kubeNamespace), nil
}

View File

@ -1,71 +0,0 @@
package kubernetes
import (
appsv1beta2 "k8s.io/client-go/kubernetes/typed/apps/v1beta2"
typesappsv1beta2 "k8s.io/client-go/kubernetes/typed/apps/v1beta2"
corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
restclient "k8s.io/client-go/rest"
)
// Factory is the kubernetes client factory
type Factory struct {
namespace string
config *restclient.Config
coreClientSet *corev1.CoreV1Client
appsClientSet *appsv1beta2.AppsV1beta2Client
}
// NewFactory creates a kubernetes client factory
func NewFactory(namespace string, config *restclient.Config) (*Factory, error) {
coreClientSet, err := corev1.NewForConfig(config)
if err != nil {
return nil, err
}
appsClientSet, err := appsv1beta2.NewForConfig(config)
if err != nil {
return nil, err
}
return &Factory{
namespace: namespace,
config: config,
coreClientSet: coreClientSet,
appsClientSet: appsClientSet,
}, nil
}
// ConfigMaps returns a client for kubernetes's config maps
func (s *Factory) ConfigMaps() corev1.ConfigMapInterface {
return s.coreClientSet.ConfigMaps(s.namespace)
}
// Secrets returns a client for kubernetes's secrets
func (s *Factory) Secrets() corev1.SecretInterface {
return s.coreClientSet.Secrets(s.namespace)
}
// Services returns a client for kubernetes's secrets
func (s *Factory) Services() corev1.ServiceInterface {
return s.coreClientSet.Services(s.namespace)
}
// Pods returns a client for kubernetes's pods
func (s *Factory) Pods() corev1.PodInterface {
return s.coreClientSet.Pods(s.namespace)
}
// Nodes returns a client for kubernetes's nodes
func (s *Factory) Nodes() corev1.NodeInterface {
return s.coreClientSet.Nodes()
}
// ReplicationControllers returns a client for kubernetes replication controllers
func (s *Factory) ReplicationControllers() corev1.ReplicationControllerInterface {
return s.coreClientSet.ReplicationControllers(s.namespace)
}
// ReplicaSets return a client for kubernetes replace sets
func (s *Factory) ReplicaSets() typesappsv1beta2.ReplicaSetInterface {
return s.appsClientSet.ReplicaSets(s.namespace)
}

View File

@ -1,54 +0,0 @@
package kubernetes
import (
"fmt"
"sort"
composetypes "github.com/docker/cli/cli/compose/types"
apiv1beta1 "github.com/docker/cli/kubernetes/compose/v1beta1"
"github.com/docker/cli/kubernetes/labels"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
)
// IsColliding verify that services defined in the stack collides with already deployed services
func IsColliding(services corev1.ServiceInterface, stack *apiv1beta1.Stack, cfg *composetypes.Config) error {
stackObjects := getServices(cfg)
for _, srv := range stackObjects {
if err := verify(services, stack.Name, srv); err != nil {
return err
}
}
return nil
}
// verify checks wether the service is already present in kubernetes.
// If we find the service by name but it doesn't have our label or it has a different value
// than the stack name for the label, we fail (i.e. it will collide)
func verify(services corev1.ServiceInterface, stackName string, service string) error {
svc, err := services.Get(service, metav1.GetOptions{})
if err == nil {
if key, ok := svc.ObjectMeta.Labels[labels.ForStackName]; ok {
if key != stackName {
return fmt.Errorf("service %s already present in stack named %s", service, key)
}
return nil
}
return fmt.Errorf("service %s already present in the cluster", service)
}
return nil
}
func getServices(cfg *composetypes.Config) []string {
services := make([]string, len(cfg.Services))
for i := range cfg.Services {
services[i] = cfg.Services[i].Name
}
sort.Strings(services)
return services
}

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