Compare commits

..

11 Commits

Author SHA1 Message Date
44e2def671 Merge pull request #376 from jose-bigio/18.01_bumpVersion
[18.01] bump version to 18.01.0-ce-rc1
2018-01-03 17:27:27 -08:00
f0e6477914 Merge pull request #379 from jose-bigio/18.01_changelog
[18.01] Changelog for 18.01
2018-01-03 17:27:00 -08:00
a971796cc5 Changelog for 18.01
Signed-off-by: jose-bigio <jose.bigio@docker.com>
2018-01-03 17:23:44 -08:00
4dd1acceb6 Merge pull request #380 from andrewhsu/arm64
[18.01] Download support of images with multi-arch manifest
2018-01-03 17:20:11 -08:00
85c031e751 Download support of images with multi-arch manifest
Currently we only support 'application/vnd.docker.distribution.manifest.v2+json'
manifest images download, with more multi-arch images used, we need to support
download images with 'application/vnd.docker.distribution.manifest.list.v2+json'
format(aka "fat manifest"), else we will fail to download those multi-arch ones.

This PR adds 'application/vnd.docker.distribution.manifest.list.v2+json' manifest
support, thus we can download both multi-arch and legacy images.

Signed-off-by: Dennis Chen <dennis.chen@arm.com>
(cherry picked from commit 0af5db511ed1ed5beab0feb09a2a96347a263410)
Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
2018-01-03 16:33:05 -08:00
41aec7703b Merge pull request #378 from thaJeztah/18.01-revert-api-bump
[18.01] Revert "Bump API version to 1.36"
2018-01-03 16:01:53 -08:00
e03fd4a1dd Merge pull request #377 from andrewhsu/tt
[18.01] skip DockerTrustSuite tests for 18.01
2018-01-03 16:01:37 -08:00
252489e04a Revert "Bump API version to 1.36"
There were no changes made yet to the API, so no need to bump
the API version for this release

This reverts commit c1e982f2ee85580687e2d355af3ca444ada14d01.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2018-01-03 23:31:02 +01:00
5742bd3ccf update integration-cli tests for stderr output
Signed-off-by: Riyaz Faizullabhoy <riyaz.faizullabhoy@docker.com>
(cherry picked from commit 250b84ee88)
Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
(cherry picked from commit d256539bf4)
Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
2018-01-03 13:33:06 -08:00
e3571070d5 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>
(cherry picked from commit ec6b0a1a4a)
Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
(cherry picked from commit fbfecebc0a)
Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
2018-01-03 13:33:05 -08:00
efb4eb2d0e bump version to 18.01.0-ce-rc1
Signed-off-by: jose-bigio <jose.bigio@docker.com>
2018-01-03 13:24:28 -08:00
2678 changed files with 19293 additions and 56195 deletions

View File

@ -1,120 +1,63 @@
# 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.
## 18.01.0-ce (2018-01-DD)
### Builder
* 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)
* Fix files not being deleted if user-namespaces are enabled [moby/moby#35822](https://github.com/moby/moby/pull/35822)
- Add support for expanding environment-variables in `docker commit --change ...` [moby/moby#35582](https://github.com/moby/moby/pull/35582)
### 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)
+ Move `docker trust` commands out of experimental [docker/cli#934](https://github.com/docker/cli/pull/934) [docker/cli#935](https://github.com/docker/cli/pull/935) [docker/cli#944](https://github.com/docker/cli/pull/944)
* Return errors from client in stack deploy configs [docker/cli#757](https://github.com/docker/cli/pull/757)
- Fix description of filter flag in prune commands [docker/cli#774](https://github.com/docker/cli/pull/774)
+ Add "pid" to unsupported options list [docker/cli#768](https://github.com/docker/cli/pull/768)
+ Add support for experimental Cli configuration [docker/cli#758](https://github.com/docker/cli/pull/758)
+ Add support for generic resources to bash completion [docker/cli#749](https://github.com/docker/cli/pull/749)
- Fix error in zsh completion script for docker exec [docker/cli#751](https://github.com/docker/cli/pull/751)
+ Add a debug message when client closes websocket attach connection [moby/moby#35720](https://github.com/moby/moby/pull/35720)
- Fix bash completion for `"docker swarm"` [docker/cli#772](https://github.com/docker/cli/pull/772)
### Documentation
* Correct references to `--publish` long syntax in docs [docker/cli#746](https://github.com/docker/cli/pull/746)
* Corrected descriptions for MAC_ADMIN and MAC_OVERRIDE [docker/cli#761](https://github.com/docker/cli/pull/761)
* Updated developer doc to explain external CLI [moby/moby#35681](https://github.com/moby/moby/pull/35681)
- Fix `"on-failure"` restart policy being documented as "failure" [docker/cli#754](https://github.com/docker/cli/pull/754)
- Fix anchors to "Storage driver options" [docker/cli#748](https://github.com/docker/cli/pull/748)
### Experimental
+ Add kubernetes support to `docker stack` command [docker/cli#721](https://github.com/docker/cli/pull/721)
* Don't append the container id to custom directory checkpoints. [moby/moby#35694](https://github.com/moby/moby/pull/35694)
### 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)
* Fix daemon crash when using the GELF log driver over TCP when the GELF server goes down [moby/moby#35765](https://github.com/moby/moby/pull/35765)
- Fix awslogs batch size calculation for large logs [moby/moby#35726](https://github.com/moby/moby/pull/35726)
### 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)
- Prevent implicit removal of the ingress network [moby/moby#36538](https://github.com/moby/moby/pull/36538)
- Fix stale HNS endpoints on Windows [moby/moby#36603](https://github.com/moby/moby/pull/36603)
- IPAM fixes for duplicate IP addresses [docker/libnetwork#2104](https://github.com/docker/libnetwork/pull/2104) [docker/libnetwork#2105](https://github.com/docker/libnetwork/pull/2105)
- Windows: Fix to allow docker service to start on Windows VM [docker/libnetwork#1916](https://github.com/docker/libnetwork/pull/1916)
- Fix for docker intercepting DNS requests on ICS network [docker/libnetwork#2014](https://github.com/docker/libnetwork/pull/2014)
+ Windows: Added a new network creation driver option [docker/libnetwork#2021](https://github.com/docker/libnetwork/pull/2021)
### 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)
- Fix read-only containers with IPC private mounts `/dev/shm` read-only [moby/moby#36526](https://github.com/moby/moby/pull/36526)
* Re-validate Mounts on container start [moby/moby#35833](https://github.com/moby/moby/pull/35833)
- Fix overlay2 storage driver inside a user namespace [moby/moby#35794](https://github.com/moby/moby/pull/35794)
* Zfs: fix busy error on container stop [moby/moby#35674](https://github.com/moby/moby/pull/35674)
- Fix #35843 regression on health check workingdir [moby/moby#35845](https://github.com/moby/moby/pull/35845)
- Fix VFS graph driver failure to initialize because of failure to setup fs quota [moby/moby#35827](https://github.com/moby/moby/pull/35827)
- Fix containerd events being processed twice [moby/moby#35896](https://github.com/moby/moby/pull/35896)
### 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)
- Fix published ports not being updated if a service has the same number of host-mode published ports with Published Port 0 [docker/swarmkit#2376](https://github.com/docker/swarmkit/pull/2376)
* Make the task termination order deterministic [docker/swarmkit#2265](https://github.com/docker/swarmkit/pull/2265)

View File

@ -1 +1 @@
18.03.0-ce-rc4
18.01.0-ce-rc1

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

@ -232,7 +232,6 @@ 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>
@ -282,7 +281,6 @@ 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>
@ -399,7 +397,6 @@ 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>

View File

@ -17,7 +17,6 @@ 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>
@ -108,7 +107,6 @@ 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>
@ -180,7 +178,6 @@ 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>
@ -237,7 +234,6 @@ 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>
@ -318,7 +314,6 @@ 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>
@ -385,7 +380,6 @@ 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>
@ -426,7 +420,6 @@ 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>
@ -527,7 +520,6 @@ 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>
@ -546,7 +538,6 @@ 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>
@ -555,7 +546,6 @@ 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>
@ -604,7 +594,6 @@ 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>

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"
@ -98,10 +135,15 @@
Email = "misty@docker.com"
GitHub = "mistyhacks"
[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,8 +51,7 @@ 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

View File

@ -1 +1 @@
18.03.0-ce-rc4
18.01.0-ce-rc1

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"
@ -51,8 +45,6 @@ type Cli interface {
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.
@ -122,21 +114,6 @@ 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)
}
// Initialize the dockerCli runs initialization that must happen after command
// line flags are parsed.
func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions) error {
@ -300,12 +277,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

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

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

@ -16,22 +16,11 @@ type fakeClient struct {
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
}
func (f *fakeClient) ContainerInspect(_ context.Context, containerID string) (types.ContainerJSON, error) {
@ -106,21 +95,3 @@ func (f *fakeClient) ContainerLogs(_ context.Context, container string, 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

@ -3,7 +3,6 @@ package container
import (
"io"
"io/ioutil"
"os"
"runtime"
"strings"
"testing"
@ -112,33 +111,6 @@ func TestRunCopyFromContainerToFilesystemMissingDestinationDirectory(t *testing.
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

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

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

@ -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"
@ -208,14 +206,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 +253,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 +261,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)

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

@ -81,7 +81,7 @@ func runRemove(dockerCli command.Cli, opts removeOptions, images []string) error
if !opts.force || fatalErr {
return errors.New(msg)
}
fmt.Fprintln(dockerCli.Err(), msg)
fmt.Fprintf(dockerCli.Err(), msg)
}
return nil
}

View File

@ -98,7 +98,7 @@ func TestNewRemoveCommandSuccess(t *testing.T) {
assert.Equal(t, true, options.Force)
return []types.ImageDeleteResponseItem{}, notFound{"image1"}
},
expectedStderr: "Error: No such image: image1\n",
expectedStderr: "Error: No such image: image1",
},
{

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

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

@ -21,9 +21,9 @@ const (
func normalize(flag string) Orchestrator {
switch flag {
case "kubernetes":
case "kubernetes", "k8s":
return OrchestratorKubernetes
case "swarm":
case "swarm", "swarmkit":
return OrchestratorSwarm
default:
return orchestratorUnset

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

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

@ -353,21 +353,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 {
@ -560,7 +561,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,15 +590,10 @@ 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
networks, err := convertNetworks(ctx, apiClient, options.networks)
if err != nil {
return service, err
}
sort.Sort(byNetworkTarget(networks))
resources, err := options.resources.ToResourceRequirements()
if err != nil {

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) {
@ -128,37 +123,3 @@ func TestResourceOptionsToResourceRequirements(t *testing.T) {
}
}
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

@ -1119,16 +1119,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"
@ -587,69 +586,3 @@ func TestRemoveGenericResources(t *testing.T) {
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,15 +9,11 @@ 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),
@ -41,10 +37,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

@ -34,7 +34,7 @@ func newDeployCommand(dockerCli command.Cli) *cobra.Command {
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.StringVarP(&opts.Composefile, "compose-file", "c", "", "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)

View File

@ -27,15 +27,15 @@ func WrapCli(dockerCli command.Cli, cmd *cobra.Command) (*KubeCli, error) {
Cli: dockerCli,
kubeNamespace: "default",
}
if cmd.Flags().Changed("namespace") {
cli.kubeNamespace, err = cmd.Flags().GetString("namespace")
if cmd.PersistentFlags().Changed("namespace") {
cli.kubeNamespace, err = cmd.PersistentFlags().GetString("namespace")
if err != nil {
return nil, err
}
}
kubeConfig := ""
if cmd.Flags().Changed("kubeconfig") {
kubeConfig, err = cmd.Flags().GetString("kubeconfig")
if cmd.PersistentFlags().Changed("kubeconfig") {
kubeConfig, err = cmd.PersistentFlags().GetString("kubeconfig")
if err != nil {
return nil, err
}

View File

@ -5,9 +5,8 @@ import (
"io/ioutil"
"path"
"github.com/docker/cli/cli/command/stack/loader"
"github.com/docker/cli/cli/command/stack/options"
composetypes "github.com/docker/cli/cli/compose/types"
composeTypes "github.com/docker/cli/cli/compose/types"
"github.com/pkg/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
@ -17,20 +16,9 @@ import (
func RunDeploy(dockerCli *KubeCli, opts options.Deploy) error {
cmdOut := dockerCli.Out()
// Check arguments
if len(opts.Composefiles) == 0 {
return errors.Errorf("Please specify only one compose file (with --compose-file).")
if opts.Composefile == "" {
return errors.Errorf("Please specify a Compose file (with --compose-file).")
}
// Parse the compose file
cfg, err := loader.LoadComposefile(dockerCli, opts)
if err != nil {
return err
}
stack, err := LoadStack(opts.Namespace, *cfg)
if err != nil {
return err
}
// Initialize clients
stacks, err := dockerCli.stacks()
if err != nil {
@ -48,6 +36,12 @@ func RunDeploy(dockerCli *KubeCli, opts options.Deploy) error {
Pods: pods,
}
// Parse the compose file
stack, cfg, err := LoadStack(opts.Namespace, opts.Composefile)
if err != nil {
return err
}
// FIXME(vdemeester) handle warnings server-side
if err = IsColliding(services, stack, cfg); err != nil {
return err
@ -88,7 +82,7 @@ func RunDeploy(dockerCli *KubeCli, opts options.Deploy) error {
}
// createFileBasedConfigMaps creates a Kubernetes ConfigMap for each Compose global file-based config.
func createFileBasedConfigMaps(stackName string, globalConfigs map[string]composetypes.ConfigObjConfig, configMaps corev1.ConfigMapInterface) error {
func createFileBasedConfigMaps(stackName string, globalConfigs map[string]composeTypes.ConfigObjConfig, configMaps corev1.ConfigMapInterface) error {
for name, config := range globalConfigs {
if config.File == "" {
continue
@ -108,7 +102,7 @@ func createFileBasedConfigMaps(stackName string, globalConfigs map[string]compos
return nil
}
func serviceNames(cfg *composetypes.Config) []string {
func serviceNames(cfg *composeTypes.Config) []string {
names := []string{}
for _, service := range cfg.Services {
@ -119,7 +113,7 @@ func serviceNames(cfg *composetypes.Config) []string {
}
// createFileBasedSecrets creates a Kubernetes Secret for each Compose global file-based secret.
func createFileBasedSecrets(stackName string, globalSecrets map[string]composetypes.SecretConfig, secrets corev1.SecretInterface) error {
func createFileBasedSecrets(stackName string, globalSecrets map[string]composeTypes.SecretConfig, secrets corev1.SecretInterface) error {
for name, secret := range globalSecrets {
if secret.File == "" {
continue

View File

@ -1,24 +1,169 @@
package kubernetes
import (
"bufio"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/docker/cli/cli/compose/loader"
"github.com/docker/cli/cli/compose/template"
composetypes "github.com/docker/cli/cli/compose/types"
apiv1beta1 "github.com/docker/cli/kubernetes/compose/v1beta1"
"github.com/pkg/errors"
yaml "gopkg.in/yaml.v2"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// LoadStack loads a stack from a Compose config, with a given name.
func LoadStack(name string, cfg composetypes.Config) (*apiv1beta1.Stack, error) {
res, err := yaml.Marshal(cfg)
if err != nil {
return nil, err
// LoadStack loads a stack from a Compose file, with a given name.
// FIXME(vdemeester) remove this and use cli/compose/loader for both swarm and kubernetes
func LoadStack(name, composeFile string) (*apiv1beta1.Stack, *composetypes.Config, error) {
if composeFile == "" {
return nil, nil, errors.New("compose-file must be set")
}
workingDir, err := os.Getwd()
if err != nil {
return nil, nil, err
}
composePath := composeFile
if !strings.HasPrefix(composePath, "/") {
composePath = filepath.Join(workingDir, composeFile)
}
if _, err := os.Stat(composePath); os.IsNotExist(err) {
return nil, nil, errors.Errorf("no compose file found in %s", filepath.Dir(composePath))
}
binary, err := ioutil.ReadFile(composePath)
if err != nil {
return nil, nil, errors.Wrap(err, "cannot read compose file")
}
env := env(workingDir)
return load(name, binary, workingDir, env)
}
func load(name string, binary []byte, workingDir string, env map[string]string) (*apiv1beta1.Stack, *composetypes.Config, error) {
processed, err := template.Substitute(string(binary), func(key string) (string, bool) { return env[key], true })
if err != nil {
return nil, nil, errors.Wrap(err, "cannot load compose file")
}
parsed, err := loader.ParseYAML([]byte(processed))
if err != nil {
return nil, nil, errors.Wrapf(err, "cannot load compose file")
}
cfg, err := loader.Load(composetypes.ConfigDetails{
WorkingDir: workingDir,
ConfigFiles: []composetypes.ConfigFile{
{
Config: parsed,
},
},
})
if err != nil {
return nil, nil, errors.Wrapf(err, "cannot load compose file")
}
result, err := processEnvFiles(processed, parsed, cfg)
if err != nil {
return nil, nil, errors.Wrapf(err, "cannot load compose file")
}
return &apiv1beta1.Stack{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Spec: apiv1beta1.StackSpec{
ComposeFile: string(res),
ComposeFile: result,
},
}, nil
}, cfg, nil
}
type iMap = map[string]interface{}
func processEnvFiles(input string, parsed map[string]interface{}, config *composetypes.Config) (string, error) {
changed := false
for _, svc := range config.Services {
if len(svc.EnvFile) == 0 {
continue
}
// Load() processed the env_file for us, we just need to inject back into
// the intermediate representation
env := iMap{}
for k, v := range svc.Environment {
env[k] = v
}
parsed["services"].(iMap)[svc.Name].(iMap)["environment"] = env
delete(parsed["services"].(iMap)[svc.Name].(iMap), "env_file")
changed = true
}
if !changed {
return input, nil
}
res, err := yaml.Marshal(parsed)
if err != nil {
return "", err
}
return string(res), nil
}
func env(workingDir string) map[string]string {
// Apply .env file first
config := readEnvFile(filepath.Join(workingDir, ".env"))
// Apply env variables
for k, v := range envToMap(os.Environ()) {
config[k] = v
}
return config
}
func readEnvFile(path string) map[string]string {
config := map[string]string{}
file, err := os.Open(path)
if err != nil {
return config // Ignore
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(strings.TrimSpace(line), "#") {
continue
}
parts := strings.SplitN(line, "=", 2)
if len(parts) == 2 {
key := parts[0]
value := parts[1]
config[key] = value
}
}
return config
}
func envToMap(env []string) map[string]string {
config := map[string]string{}
for _, value := range env {
parts := strings.SplitN(value, "=", 2)
key := parts[0]
value := parts[1]
config[key] = value
}
return config
}

View File

@ -3,44 +3,32 @@ package kubernetes
import (
"testing"
composetypes "github.com/docker/cli/cli/compose/types"
apiv1beta1 "github.com/docker/cli/kubernetes/compose/v1beta1"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func TestLoadStack(t *testing.T) {
s, err := LoadStack("foo", composetypes.Config{
Version: "3.1",
Filename: "banana",
Services: []composetypes.ServiceConfig{
{
Name: "foo",
Image: "foo",
},
{
Name: "bar",
Image: "bar",
},
},
})
require.NoError(t, err)
require.Equal(t, &apiv1beta1.Stack{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
},
Spec: apiv1beta1.StackSpec{
ComposeFile: string(`version: "3.1"
services:
bar:
image: bar
foo:
image: foo
networks: {}
volumes: {}
secrets: {}
configs: {}
`),
},
}, s)
func TestPlaceholders(t *testing.T) {
env := map[string]string{
"TAG": "_latest_",
"K1": "V1",
"K2": "V2",
}
prefix := "version: '3'\nvolumes:\n data:\n external:\n name: "
var tests = []struct {
input string
expectedOutput string
}{
{prefix + "BEFORE${TAG}AFTER", prefix + "BEFORE_latest_AFTER"},
{prefix + "BEFORE${K1}${K2}AFTER", prefix + "BEFOREV1V2AFTER"},
{prefix + "BEFORE$TAG AFTER", prefix + "BEFORE_latest_ AFTER"},
{prefix + "BEFORE$$TAG AFTER", prefix + "BEFORE$TAG AFTER"},
{prefix + "BEFORE $UNKNOWN AFTER", prefix + "BEFORE AFTER"},
}
for _, test := range tests {
output, _, err := load("stack", []byte(test.input), ".", env)
require.NoError(t, err)
assert.Equal(t, test.expectedOutput, output.Spec.ComposeFile)
}
}

View File

@ -1,152 +0,0 @@
package loader
import (
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"sort"
"strings"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/stack/options"
"github.com/docker/cli/cli/compose/loader"
"github.com/docker/cli/cli/compose/schema"
composetypes "github.com/docker/cli/cli/compose/types"
"github.com/pkg/errors"
)
// LoadComposefile parse the composefile specified in the cli and returns its Config and version.
func LoadComposefile(dockerCli command.Cli, opts options.Deploy) (*composetypes.Config, error) {
configDetails, err := getConfigDetails(opts.Composefiles, dockerCli.In())
if err != nil {
return nil, err
}
dicts := getDictsFrom(configDetails.ConfigFiles)
config, err := loader.Load(configDetails)
if err != nil {
if fpe, ok := err.(*loader.ForbiddenPropertiesError); ok {
return nil, errors.Errorf("Compose file contains unsupported options:\n\n%s\n",
propertyWarnings(fpe.Properties))
}
return nil, err
}
unsupportedProperties := loader.GetUnsupportedProperties(dicts...)
if len(unsupportedProperties) > 0 {
fmt.Fprintf(dockerCli.Err(), "Ignoring unsupported options: %s\n\n",
strings.Join(unsupportedProperties, ", "))
}
deprecatedProperties := loader.GetDeprecatedProperties(dicts...)
if len(deprecatedProperties) > 0 {
fmt.Fprintf(dockerCli.Err(), "Ignoring deprecated options:\n\n%s\n\n",
propertyWarnings(deprecatedProperties))
}
return config, nil
}
func getDictsFrom(configFiles []composetypes.ConfigFile) []map[string]interface{} {
dicts := []map[string]interface{}{}
for _, configFile := range configFiles {
dicts = append(dicts, configFile.Config)
}
return dicts
}
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(composefiles []string, stdin io.Reader) (composetypes.ConfigDetails, error) {
var details composetypes.ConfigDetails
if len(composefiles) == 0 {
return details, errors.New("no composefile(s)")
}
if composefiles[0] == "-" && len(composefiles) == 1 {
workingDir, err := os.Getwd()
if err != nil {
return details, err
}
details.WorkingDir = workingDir
} else {
absPath, err := filepath.Abs(composefiles[0])
if err != nil {
return details, err
}
details.WorkingDir = filepath.Dir(absPath)
}
var err error
details.ConfigFiles, err = loadConfigFiles(composefiles, stdin)
if err != nil {
return details, err
}
// Take the first file version (2 files can't have different version)
details.Version = schema.Version(details.ConfigFiles[0].Config)
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 loadConfigFiles(filenames []string, stdin io.Reader) ([]composetypes.ConfigFile, error) {
var configFiles []composetypes.ConfigFile
for _, filename := range filenames {
configFile, err := loadConfigFile(filename, stdin)
if err != nil {
return configFiles, err
}
configFiles = append(configFiles, *configFile)
}
return configFiles, nil
}
func loadConfigFile(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
}

View File

@ -1,47 +0,0 @@
package loader
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/gotestyourself/gotestyourself/fs"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
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([]string{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([]string{"-"}, 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()))
}

View File

@ -5,7 +5,7 @@ import "github.com/docker/cli/opts"
// Deploy holds docker stack deploy options
type Deploy struct {
Bundlefile string
Composefiles []string
Composefile string
Namespace string
ResolveImage string
SendRegistryAuth bool

View File

@ -29,9 +29,9 @@ func RunDeploy(dockerCli command.Cli, opts options.Deploy) error {
}
switch {
case opts.Bundlefile == "" && len(opts.Composefiles) == 0:
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 != "" && len(opts.Composefiles) != 0:
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)

View File

@ -2,11 +2,17 @@ package swarm
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"
@ -18,11 +24,33 @@ import (
)
func deployCompose(ctx context.Context, dockerCli command.Cli, opts options.Deploy) error {
config, err := loader.LoadComposefile(dockerCli, opts)
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
}
@ -83,6 +111,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,

View File

@ -1,16 +1,56 @@
package swarm
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

@ -9,12 +9,14 @@ import (
// NewTrustCommand returns a cobra command for `trust` subcommands
func NewTrustCommand(dockerCli command.Cli) *cobra.Command {
cmd := &cobra.Command{
Use: "trust",
Short: "Manage trust on Docker images",
Args: cli.NoArgs,
RunE: command.ShowHelp(dockerCli.Err()),
Use: "trust",
Short: "Manage trust on Docker images (experimental)",
Args: cli.NoArgs,
RunE: command.ShowHelp(dockerCli.Err()),
Annotations: map[string]string{"experimentalCLI": ""},
}
cmd.AddCommand(
newViewCommand(dockerCli),
newRevokeCommand(dockerCli),
newSignCommand(dockerCli),
newTrustKeyCommand(dockerCli),

View File

@ -2,7 +2,6 @@ package trust
import (
"encoding/json"
"fmt"
"sort"
"github.com/docker/cli/cli"
@ -12,55 +11,24 @@ import (
"github.com/theupdateframework/notary/tuf/data"
)
type inspectOptions struct {
remotes []string
// FIXME(n4ss): this is consistent with `docker service inspect` but we should provide
// a `--format` flag too. (format and pretty-print should be exclusive)
prettyPrint bool
}
func newInspectCommand(dockerCli command.Cli) *cobra.Command {
options := inspectOptions{}
cmd := &cobra.Command{
Use: "inspect IMAGE[:TAG] [IMAGE[:TAG]...]",
Short: "Return low-level information about keys and signatures",
Args: cli.RequiresMinArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
options.remotes = args
return runInspect(dockerCli, options)
return runInspect(dockerCli, args)
},
}
flags := cmd.Flags()
flags.BoolVar(&options.prettyPrint, "pretty", false, "Print the information in a human friendly format")
return cmd
}
func runInspect(dockerCli command.Cli, opts inspectOptions) error {
if opts.prettyPrint {
var err error
for index, remote := range opts.remotes {
if err = prettyPrintTrustInfo(dockerCli, remote); err != nil {
return err
}
// Additional separator between the inspection output of each image
if index < len(opts.remotes)-1 {
fmt.Fprint(dockerCli.Out(), "\n\n")
}
}
return err
}
func runInspect(dockerCli command.Cli, remotes []string) error {
getRefFunc := func(ref string) (interface{}, []byte, error) {
i, err := getRepoTrustInfo(dockerCli, ref)
return nil, i, err
}
return inspect.Inspect(dockerCli.Out(), opts.remotes, "", getRefFunc)
return inspect.Inspect(dockerCli.Out(), remotes, "", getRefFunc)
}
func getRepoTrustInfo(cli command.Cli, remote string) ([]byte, error) {

View File

@ -18,7 +18,12 @@ func TestTrustInspectCommandErrors(t *testing.T) {
}{
{
name: "not-enough-args",
expectedError: "requires at least 1 argument",
expectedError: "requires exactly 1 argument",
},
{
name: "too-many-args",
args: []string{"remote1", "remote2"},
expectedError: "requires exactly 1 argument",
},
{
name: "sha-reference",
@ -32,9 +37,8 @@ func TestTrustInspectCommandErrors(t *testing.T) {
},
}
for _, tc := range testCases {
cmd := newInspectCommand(
cmd := newViewCommand(
test.NewFakeCli(&fakeClient{}))
cmd.Flags().Set("pretty", "true")
cmd.SetArgs(tc.args)
cmd.SetOutput(ioutil.Discard)
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)

View File

@ -10,7 +10,7 @@ import (
func newTrustKeyCommand(dockerCli command.Streams) *cobra.Command {
cmd := &cobra.Command{
Use: "key",
Short: "Manage keys for signing Docker images",
Short: "Manage keys for signing Docker images (experimental)",
Args: cli.NoArgs,
RunE: command.ShowHelp(dockerCli.Err()),
}

View File

@ -243,5 +243,8 @@ func addStagedSigner(notaryRepo client.Repository, newSigner data.RoleName, sign
if err := notaryRepo.AddDelegationRoleAndKeys(trust.ReleasesRole, signerKeys); err != nil {
return err
}
return notaryRepo.AddDelegationPaths(trust.ReleasesRole, []string{""})
if err := notaryRepo.AddDelegationPaths(trust.ReleasesRole, []string{""}); err != nil {
return err
}
return nil
}

View File

@ -10,7 +10,7 @@ import (
func newTrustSignerCommand(dockerCli command.Cli) *cobra.Command {
cmd := &cobra.Command{
Use: "signer",
Short: "Manage entities who can sign Docker images",
Short: "Manage entities who can sign Docker images (experimental)",
Args: cli.NoArgs,
RunE: command.ShowHelp(dockerCli.Err()),
}

View File

@ -131,5 +131,8 @@ func removeSingleSigner(cli command.Cli, repoName, signerName string, forceYes b
if err = notaryRepo.RemoveDelegationRole(signerDelegation); err != nil {
return err
}
return notaryRepo.Publish()
if err = notaryRepo.Publish(); err != nil {
return err
}
return nil
}

View File

@ -1,10 +0,0 @@
Signatures for signed-repo:green
SIGNED TAG DIGEST SIGNERS
green 677265656e2d646967657374 (Repo Admin)
Administrative keys for signed-repo:green
Repository Key: targetsID
Root Key: rootID

View File

@ -1,14 +0,0 @@
No signatures for signed-repo:unsigned
List of signers and their keys for signed-repo:unsigned
SIGNER KEYS
alice A
bob B
Administrative keys for signed-repo:unsigned
Repository Key: targetsID
Root Key: rootID

View File

@ -1,10 +1,6 @@
Signatures for signed-repo
SIGNED TAG DIGEST SIGNERS
green 677265656e2d646967657374 (Repo Admin)
Administrative keys for signed-repo
Repository Key: targetsID
Root Key: rootID
Administrative keys for signed-repo:
Repository Key: targetsID
Root Key: rootID

View File

@ -1,18 +1,14 @@
Signatures for signed-repo
SIGNED TAG DIGEST SIGNERS
blue 626c75652d646967657374 alice
green 677265656e2d646967657374 (Repo Admin)
red 7265642d646967657374 alice, bob
List of signers and their keys for signed-repo
List of signers and their keys for signed-repo:
SIGNER KEYS
alice A
bob B
Administrative keys for signed-repo
Repository Key: targetsID
Root Key: rootID
Administrative keys for signed-repo:
Repository Key: targetsID
Root Key: rootID

View File

@ -0,0 +1,6 @@
SIGNED TAG DIGEST SIGNERS
green 677265656e2d646967657374 (Repo Admin)
Administrative keys for signed-repo:
Repository Key: targetsID
Root Key: rootID

View File

@ -0,0 +1,13 @@
No signatures for signed-repo:unsigned
List of signers and their keys for signed-repo:
SIGNER KEYS
alice A
bob B
Administrative keys for signed-repo:
Repository Key: targetsID
Root Key: rootID

View File

@ -4,21 +4,34 @@ import (
"fmt"
"io"
"sort"
"strings"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/formatter"
"github.com/spf13/cobra"
"github.com/theupdateframework/notary/client"
)
func prettyPrintTrustInfo(cli command.Cli, remote string) error {
func newViewCommand(dockerCli command.Cli) *cobra.Command {
cmd := &cobra.Command{
Use: "view IMAGE[:TAG]",
Short: "Display detailed information about keys and signatures",
Args: cli.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return viewTrustInfo(dockerCli, args[0])
},
}
return cmd
}
func viewTrustInfo(cli command.Cli, remote string) error {
signatureRows, adminRolesWithSigs, delegationRoles, err := lookupTrustInfo(cli, remote)
if err != nil {
return err
}
if len(signatureRows) > 0 {
fmt.Fprintf(cli.Out(), "\nSignatures for %s\n\n", remote)
if err := printSignatures(cli.Out(), signatureRows); err != nil {
return err
}
@ -29,14 +42,14 @@ func prettyPrintTrustInfo(cli command.Cli, remote string) error {
// If we do not have additional signers, do not display
if len(signerRoleToKeyIDs) > 0 {
fmt.Fprintf(cli.Out(), "\nList of signers and their keys for %s\n\n", remote)
fmt.Fprintf(cli.Out(), "\nList of signers and their keys for %s:\n\n", strings.Split(remote, ":")[0])
if err := printSignerInfo(cli.Out(), signerRoleToKeyIDs); err != nil {
return err
}
}
// This will always have the root and targets information
fmt.Fprintf(cli.Out(), "\nAdministrative keys for %s\n\n", remote)
fmt.Fprintf(cli.Out(), "\nAdministrative keys for %s:\n", strings.Split(remote, ":")[0])
printSortedAdminKeys(cli.Out(), adminRolesWithSigs)
return nil
}
@ -44,9 +57,7 @@ func prettyPrintTrustInfo(cli command.Cli, remote string) error {
func printSortedAdminKeys(out io.Writer, adminRoles []client.RoleWithSignatures) {
sort.Slice(adminRoles, func(i, j int) bool { return adminRoles[i].Name > adminRoles[j].Name })
for _, adminRole := range adminRoles {
if formattedAdminRole := formatAdminRole(adminRole); formattedAdminRole != "" {
fmt.Fprintf(out, " %s", formattedAdminRole)
}
fmt.Fprintf(out, "%s", formatAdminRole(adminRole))
}
}

View File

@ -16,13 +16,11 @@ import (
"github.com/theupdateframework/notary/tuf/data"
)
// TODO(n4ss): remove common tests with the regular inspect command
type fakeClient struct {
dockerClient.Client
}
func TestTrustInspectPrettyCommandErrors(t *testing.T) {
func TestTrustViewCommandErrors(t *testing.T) {
testCases := []struct {
name string
args []string
@ -30,7 +28,12 @@ func TestTrustInspectPrettyCommandErrors(t *testing.T) {
}{
{
name: "not-enough-args",
expectedError: "requires at least 1 argument",
expectedError: "requires exactly 1 argument",
},
{
name: "too-many-args",
args: []string{"remote1", "remote2"},
expectedError: "requires exactly 1 argument",
},
{
name: "sha-reference",
@ -44,115 +47,104 @@ func TestTrustInspectPrettyCommandErrors(t *testing.T) {
},
}
for _, tc := range testCases {
cmd := newInspectCommand(
cmd := newViewCommand(
test.NewFakeCli(&fakeClient{}))
cmd.SetArgs(tc.args)
cmd.SetOutput(ioutil.Discard)
cmd.Flags().Set("pretty", "true")
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
}
}
func TestTrustInspectPrettyCommandOfflineErrors(t *testing.T) {
func TestTrustViewCommandOfflineErrors(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(getOfflineNotaryRepository)
cmd := newInspectCommand(cli)
cmd.Flags().Set("pretty", "true")
cmd := newViewCommand(cli)
cmd.SetArgs([]string{"nonexistent-reg-name.io/image"})
cmd.SetOutput(ioutil.Discard)
testutil.ErrorContains(t, cmd.Execute(), "No signatures or cannot access nonexistent-reg-name.io/image")
cli = test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(getOfflineNotaryRepository)
cmd = newInspectCommand(cli)
cmd.Flags().Set("pretty", "true")
cmd = newViewCommand(cli)
cmd.SetArgs([]string{"nonexistent-reg-name.io/image:tag"})
cmd.SetOutput(ioutil.Discard)
testutil.ErrorContains(t, cmd.Execute(), "No signatures or cannot access nonexistent-reg-name.io/image")
}
func TestTrustInspectPrettyCommandUninitializedErrors(t *testing.T) {
func TestTrustViewCommandUninitializedErrors(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(getUninitializedNotaryRepository)
cmd := newInspectCommand(cli)
cmd.Flags().Set("pretty", "true")
cmd := newViewCommand(cli)
cmd.SetArgs([]string{"reg/unsigned-img"})
cmd.SetOutput(ioutil.Discard)
testutil.ErrorContains(t, cmd.Execute(), "No signatures or cannot access reg/unsigned-img")
cli = test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(getUninitializedNotaryRepository)
cmd = newInspectCommand(cli)
cmd.Flags().Set("pretty", "true")
cmd = newViewCommand(cli)
cmd.SetArgs([]string{"reg/unsigned-img:tag"})
cmd.SetOutput(ioutil.Discard)
testutil.ErrorContains(t, cmd.Execute(), "No signatures or cannot access reg/unsigned-img:tag")
}
func TestTrustInspectPrettyCommandEmptyNotaryRepoErrors(t *testing.T) {
func TestTrustViewCommandEmptyNotaryRepoErrors(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(getEmptyTargetsNotaryRepository)
cmd := newInspectCommand(cli)
cmd.Flags().Set("pretty", "true")
cmd := newViewCommand(cli)
cmd.SetArgs([]string{"reg/img:unsigned-tag"})
cmd.SetOutput(ioutil.Discard)
assert.NoError(t, cmd.Execute())
assert.Contains(t, cli.OutBuffer().String(), "No signatures for reg/img:unsigned-tag")
assert.Contains(t, cli.OutBuffer().String(), "Administrative keys for reg/img")
assert.Contains(t, cli.OutBuffer().String(), "Administrative keys for reg/img:")
cli = test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(getEmptyTargetsNotaryRepository)
cmd = newInspectCommand(cli)
cmd.Flags().Set("pretty", "true")
cmd = newViewCommand(cli)
cmd.SetArgs([]string{"reg/img"})
cmd.SetOutput(ioutil.Discard)
assert.NoError(t, cmd.Execute())
assert.Contains(t, cli.OutBuffer().String(), "No signatures for reg/img")
assert.Contains(t, cli.OutBuffer().String(), "Administrative keys for reg/img")
assert.Contains(t, cli.OutBuffer().String(), "Administrative keys for reg/img:")
}
func TestTrustInspectPrettyCommandFullRepoWithoutSigners(t *testing.T) {
func TestTrustViewCommandFullRepoWithoutSigners(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(getLoadedWithNoSignersNotaryRepository)
cmd := newInspectCommand(cli)
cmd.Flags().Set("pretty", "true")
cmd := newViewCommand(cli)
cmd.SetArgs([]string{"signed-repo"})
assert.NoError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "trust-inspect-pretty-full-repo-no-signers.golden")
golden.Assert(t, cli.OutBuffer().String(), "trust-view-full-repo-no-signers.golden")
}
func TestTrustInspectPrettyCommandOneTagWithoutSigners(t *testing.T) {
func TestTrustViewCommandOneTagWithoutSigners(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(getLoadedWithNoSignersNotaryRepository)
cmd := newInspectCommand(cli)
cmd.Flags().Set("pretty", "true")
cmd := newViewCommand(cli)
cmd.SetArgs([]string{"signed-repo:green"})
assert.NoError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "trust-inspect-pretty-one-tag-no-signers.golden")
golden.Assert(t, cli.OutBuffer().String(), "trust-view-one-tag-no-signers.golden")
}
func TestTrustInspectPrettyCommandFullRepoWithSigners(t *testing.T) {
func TestTrustViewCommandFullRepoWithSigners(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(getLoadedNotaryRepository)
cmd := newInspectCommand(cli)
cmd.Flags().Set("pretty", "true")
cmd := newViewCommand(cli)
cmd.SetArgs([]string{"signed-repo"})
assert.NoError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "trust-inspect-pretty-full-repo-with-signers.golden")
golden.Assert(t, cli.OutBuffer().String(), "trust-view-full-repo-with-signers.golden")
}
func TestTrustInspectPrettyCommandUnsignedTagInSignedRepo(t *testing.T) {
func TestTrustViewCommandUnsignedTagInSignedRepo(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(getLoadedNotaryRepository)
cmd := newInspectCommand(cli)
cmd.Flags().Set("pretty", "true")
cmd := newViewCommand(cli)
cmd.SetArgs([]string{"signed-repo:unsigned"})
assert.NoError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "trust-inspect-pretty-unsigned-tag-with-signers.golden")
golden.Assert(t, cli.OutBuffer().String(), "trust-view-unsigned-tag-with-signers.golden")
}
func TestNotaryRoleToSigner(t *testing.T) {

View File

@ -22,7 +22,7 @@ func NewPruneCommand(dockerCli command.Cli) *cobra.Command {
cmd := &cobra.Command{
Use: "prune [OPTIONS]",
Short: "Remove all unused local volumes",
Short: "Remove all unused volumes",
Args: cli.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
spaceReclaimed, output, err := runPrune(dockerCli, options)
@ -45,7 +45,7 @@ func NewPruneCommand(dockerCli command.Cli) *cobra.Command {
return cmd
}
const warning = `WARNING! This will remove all local volumes not used by at least one container.
const warning = `WARNING! This will remove all volumes not used by at least one container.
Are you sure you want to continue?`
func runPrune(dockerCli command.Cli, options pruneOptions) (spaceReclaimed uint64, output string, err error) {

View File

@ -1,2 +1,2 @@
WARNING! This will remove all local volumes not used by at least one container.
WARNING! This will remove all volumes not used by at least one container.
Are you sure you want to continue? [y/N] Total reclaimed space: 0B

View File

@ -1,4 +1,4 @@
WARNING! This will remove all local volumes not used by at least one container.
WARNING! This will remove all volumes not used by at least one container.
Are you sure you want to continue? [y/N] Deleted Volumes:
foo
bar

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