Compare commits

..

81 Commits

Author SHA1 Message Date
665d244001 Merge pull request #196 from andrewhsu/fix-lock
[17.07] vndr libnetwork to bring in lock fix
2017-08-16 17:19:43 -07:00
b6c875c8bb vndr libnetwork to bring in lock fix
Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
2017-08-16 15:05:59 -07:00
3f7ca09fc6 Merge pull request #193 from andrewhsu/bump-rc3
[17.07] bump version to 17.07.0-ce-rc3
2017-08-15 11:00:43 -07:00
a9aafcc8d3 Merge pull request #192 from andrewhsu/cl
[17.07] update changelog for 17.07.0-ce-rc3
2017-08-15 11:00:24 -07:00
6224485fba Merge pull request #194 from andrewhsu/fix-slash
[17.07] backport Fix requests for docker host ending with slash
2017-08-14 18:13:49 -07:00
a8a6fce600 sync components/engine/client changes with cli vendor
Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
2017-08-14 17:57:55 -07:00
9fc218c23a Merge pull request #190 from andrewhsu/ln-fix-peer
[17.07] vndr libnetwork to bump_17.07 to fix peerDeleteOp
2017-08-14 17:33:11 -07:00
fbd80d2af6 Fix requests for docker host ending with slash
Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>(cherry picked from commit 823e88d4c4298c38130b9a387a45c47cf957a931)
Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
2017-08-14 15:36:10 -07:00
29fda6bf9b add experimental note for swarm plugins
Signed-off-by: Victor Vieux <victorvieux@gmail.com>
2017-08-14 14:33:07 -07:00
4ba586edd7 bump version to 17.07.0-ce-rc3
Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
2017-08-14 19:30:58 +00:00
82e46a2208 update changelog for 17.07.0-ce-rc3
Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
2017-08-14 19:28:47 +00:00
068b4a5b06 vndr libnetwork to bump_17.07 to fix peerDeleteOp
Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
2017-08-14 09:32:43 -07:00
92d80a6743 Merge pull request #188 from vieux/hide_swarm_p
[17.07] backport for Hide swarm plugins behind experimental flag
2017-08-14 09:05:56 -07:00
e1df46d2e2 hide swarm plugins behind experimental flag
Signed-off-by: Victor Vieux <victorvieux@gmail.com>
(cherry picked from commit 493002021094d72d405e1cd5bfa10b8080f67920)
Signed-off-by: Victor Vieux <victorvieux@gmail.com>
2017-08-09 08:57:18 -07:00
36ce6055c2 Merge pull request #186 from fcrisciani/ln-vnd-17.07
[17.07] vndr libnetwork to latest bump_17.07
2017-08-07 16:36:51 -07:00
c867dee41e [17.07] vndr libnetwork to latest bump_17.07
Signed-off-by: Flavio Crisciani <flavio.crisciani@docker.com>
2017-08-07 12:00:47 -07:00
82a59cdb2c Merge pull request #183 from andrewhsu/bump
bump version to 17.07.0-ce-rc2
2017-08-04 17:37:17 -07:00
282553a460 bump version to 17.07.0-ce-rc2
Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
2017-08-04 17:36:17 -07:00
a060b0ab9e Merge pull request #177 from thaJeztah/17.07-backport-stack-host-bridge-nets
[17.07] backport Skip inspect of built-in networks on stack deploy
2017-08-04 17:22:57 -07:00
49d3c0ffd5 Merge pull request #175 from vieux/fix_api_server_null
[17.07] backport Fix api server null pointer def on inspect/ls null ipam-driver networks
2017-08-04 17:01:56 -07:00
eb50cc77fa Merge pull request #176 from vieux/fix_make_plu
[17.07] backport Make plugins dir private
2017-08-04 17:01:07 -07:00
ef6b38478e Merge pull request #168 from thaJeztah/17.07-backport-keep-alive
[17.07] Enable TCP Keep-Alive in Docker client
2017-08-04 16:53:36 -07:00
4b977fe32c Merge pull request #181 from andrewhsu/v-ln
[17.07] vndr libnetwork to latest bump_17.07
2017-08-04 16:52:59 -07:00
ef0887e6d2 Merge pull request #182 from andrewhsu/make-vendor
[17.07] Add make vendor target
2017-08-04 16:30:21 -07:00
9be51c7a8e Merge pull request #143 from cpuguy83/cherry-pick-33960-17.07
[17.07] Fix error handling with not-exist errors on remove
2017-08-04 15:56:27 -07:00
6b8965d92f Merge pull request #152 from thaJeztah/17.07-backport-fix-awslogs
[17.07] Fix awslogs driver repeating last event - #34292
2017-08-04 15:55:22 -07:00
b7fcd3ebd2 Merge pull request #153 from thaJeztah/17.07-backport-fix-releaseableLayer-handling
[17.07] Fixing releaseableLayer handling of layer streams and mounts.
2017-08-04 15:54:13 -07:00
e5b51b4933 Merge pull request #155 from thaJeztah/17.07-backport-plugable-secret-api-docs
[17.07] backport plugable secret api docs
2017-08-04 15:52:48 -07:00
608cc09fab Merge pull request #170 from thaJeztah/17.07-docs-cherry-picks
[17.07] docs cherry picks
2017-08-04 15:49:00 -07:00
8cc93939f4 Merge pull request #178 from thaJeztah/17.07-backport-avoid-recursive-rlock
[17.07] cluster: Avoid recursive RLock
2017-08-04 15:45:07 -07:00
15bcae1956 vndr libnetwork to latest bump_17.07
Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
2017-08-04 14:29:19 -07:00
b159375e54 Add make vendor target
Signed-off-by: Tibor Vass <tibor@docker.com>
(cherry picked from commit 21205dab24)
Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
2017-08-04 14:17:35 -07:00
d66b5d38da Merge pull request #172 from vieux/fix-delete-container
[17.07] backport Fix delete container
2017-08-04 14:04:01 -07:00
4a06fe683e Merge pull request #174 from vieux/fix_changing_get_network
[17.07] backport Changing the get network request to...
2017-08-04 13:42:17 -07:00
f0d4715ce2 Merge pull request #173 from vieux/fix-pass-driver
[17.07] backport Fixing issue with driver opt not passed to drivers
2017-08-04 13:23:31 -07:00
4fd052876a cluster: Avoid recursive RLock
GetTasks can call GetService and GetNode with the read lock held. These
methods try to aquire the read side of the same lock. According to the
sync package documentation, this is not safe:

> If a goroutine holds a RWMutex for reading, it must not expect this or
> any other goroutine to be able to also take the read lock until the
> first read lock is released. In particular, this prohibits recursive
> read locking. This is to ensure that the lock eventually becomes
> available; a blocked Lock call excludes new readers from acquiring the
> lock.

Fix GetTasks to use the lower-level getService and getNode methods
instead. Also, use lockedManagerAction to simplify GetTasks.

Signed-off-by: Aaron Lehmann <aaron.lehmann@docker.com>
(cherry picked from commit bd4f66c8f1f6ad4a2f228a957f293bc157e13d9c)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2017-08-04 11:53:09 +02:00
9097af4c13 Skip inspects of built-in networks on stack deploy
Signed-off-by: Alex Mavrogiannis <alex.mavrogiannis@docker.com>
(cherry picked from commit 7f53c99dfe)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2017-08-04 11:16:44 +02:00
9ea6d317bb Make plugins dir private.
This prevents mounts in the plugins dir from leaking into other
namespaces which can prevent removal (`device or resource busy`),
particularly on older kernels.

Signed-off-by: Brian Goff <cpuguy83@gmail.com>
(cherry picked from commit 0c2821d6f2de692d105e50a399daa65169697cca)
Signed-off-by: Victor Vieux <victorvieux@gmail.com>
2017-08-03 18:44:55 -07:00
67dc2b7dac Fix api server null pointer def on inspect/ls null ipam-driver networks
- When a network is created with the null ipam driver, docker api server
  thread will deference a nil pointer on `docker network ls` and on
  `docker network inspect <nw>`. This because buildIpamResource()
  assumes a gateway address is always present, which is not correct.

Signed-off-by: Alessandro Boch <aboch@tetrationanalytics.com>
(cherry picked from commit beebfc0cf6240c8af511eb4d7e29314c8de6ddf2)
Signed-off-by: Victor Vieux <victorvieux@gmail.com>
2017-08-03 18:42:09 -07:00
4c81d47fbc Changing the get network request to return swarm scope predefined networks
Starting 17.06 swarm service create supports service creates with predefined
networks like host and bridge. Due to the nature of the feature, swarm manager
has a swarm scope predefined networks in addition to local scoped
predefined networks on all nodes. However network inspects for swarm scoped
predefined networks was not possible. The fix adds support for network inspect
for swarm scoped predefined networks.

Signed-off-by: Abhinandan Prativadi <abhi@docker.com>
(cherry picked from commit 5bfefb2d3662fa066ddf0d0e10cac93ee70f7ae8)
Signed-off-by: Victor Vieux <victorvieux@gmail.com>
2017-08-03 18:38:15 -07:00
a16dcf95b6 Fixing issue with driver opt not passed to drivers
Signed-off-by: Abhinandan Prativadi <abhi@docker.com>
(cherry picked from commit bcb55c62024419a2f8fa7679e1e068cc43425636)
Signed-off-by: Victor Vieux <victorvieux@gmail.com>
2017-08-03 18:34:11 -07:00
2358487e8e container: Fix Delete on nonexistent container
Delete needs to release names related to a container even if that
container isn't present in the db. However, slightly overzealous error
checking causes the transaction to get rolled back. Ignore the error
from Delete on the container itself, since it may not be present.

Signed-off-by: Aaron Lehmann <aaron.lehmann@docker.com>
(cherry picked from commit 1d9546fc62c559dbcbb3dbdce40318fb7c4d67a2)
Signed-off-by: Victor Vieux <victorvieux@gmail.com>
2017-08-03 18:23:06 -07:00
1e3666a731 Update API plugin response examples
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 4735c7663201ce1bf618e2aa505d7813a331be3f)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2017-08-03 22:14:29 +02:00
1451e5e581 Add Infinit plugin
Signed-off-by: Misty Stanley-Jones <misty@docker.com>
(cherry picked from commit 83e8551876)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2017-08-03 22:14:21 +02:00
fcf5a9d689 update service create and update options in commandline documentation
Signed-off-by: zebrilee <zebrilee@gmail.com>
(cherry picked from commit 5fe09164f5)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2017-08-03 22:14:14 +02:00
3402a4fe4f modify foo by container in order to clarify the documentation
Signed-off-by: zebrilee <zebrilee@gmail.com>
(cherry picked from commit 2d5f9d83e7)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2017-08-03 22:14:07 +02:00
598ea37b90 Docs: update filter options for docker container ps
The `is-task` filter was only documented in the usage
section, but this section is not used in the documentation.

This patch adds the missing filter, synchronises the
man page source, and does some slight rephrasing
and reformatting of the filters.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 46064f33f4)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2017-08-03 22:14:00 +02:00
965908e8ce Enable TCP Keep-Alive in Docker client
Some network environments may have NATs, proxies, or gateways which
kill idle connections. There are many Docker API operations which may
be idle for long periods of time (such as ContainerWait and ContainerAttach)
and may result in unexpected connection closures or hangs if TCP keepalives
are not used.

This patch updates the default HTTP transport used by the Docker client
package to enable TCP Keep-Alive with a keep-alive interval of 30 seconds.
It also sets a connect timeout of 30 seconds.

Docker-DCO-1.1-Signed-off-by: Josh Hawn <josh.hawn@docker.com> (github: jlhawn)

(cherry picked from commit 2831a04cba)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2017-08-03 21:33:02 +02:00
1466b46335 Fix RestartPolicy default value
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit fc48b5529dca3907ade273921a14906be796e333)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2017-08-01 17:01:31 +02:00
bc27a5b53b Add API documentation for plugable secret backends
Documents the API changes introduced in

0304c98d85404fe75a1b4a35d3c111931e062f41 and
08f7cf05268782a0dd8e4c41a4cc65fdf78d09f2

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit c8dad44c326d9d2131f94babbc535e7f442db290)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2017-08-01 17:01:24 +02:00
c63ba3a05c Fixing releaseableLayer handling of layer streams and mounts.
releaseableLayer includes automatic handling for creating a read/write layer and mounting it on a call to Mount(), but then does not correspondingly unmount the layer before trying to delete it, which will fail for some graphdrivers. Commit on a releaseable layer also leaks the tarstream for the layer. To fix this, the stream close is deferred in Commit and releaseRWLayer now correctly handles unmounting the layer before trying to delete it.  In addition, the changes include better error handling in Release() to make sure that errors are returned to the caller for failures on read/write layers instead of being ignored.# Please enter the commit message for your changes. Lines starting

Signed-off-by: Stefan Wernli <swernli@ntdev.microsoft.com>
(cherry picked from commit 1d457999c4540aacda68f834bdb3c6f220ce3fd5)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2017-08-01 14:16:46 +02:00
839e712162 Fix awslogs driver repeating last event - #34292
Signed-off-by: Justin Menga <justin.menga@gmail.com>
(cherry picked from commit 0fd5a0bab79f20f910cb7551ec34158a32e05f5a)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2017-08-01 01:02:42 +02:00
48acee0beb Fix error handling with not-exist errors on remove
Specifically, none of the graphdrivers are supposed to return a
not-exist type of error on remove (or at least that's how they are
currently handled).

Found that AUFS still had one case where a not-exist error could escape,
when checking if the directory is mounted we call a `Statfs` on the
path.

This fixes AUFS to not return an error in this case, but also
double-checks at the daemon level on layer remove that the error is not
a `not-exist` type of error.

Signed-off-by: Brian Goff <cpuguy83@gmail.com>
(cherry picked from commit d42dbdd3d48d0134f8bba7ead92a7067791dffab)
Signed-off-by: Brian Goff <cpuguy83@gmail.com>
2017-07-26 13:28:51 -04:00
8c4be39ddd Merge pull request #141 from andrewhsu/ver
bump version to 17.07.0-ce-rc1
2017-07-25 18:38:06 -07:00
450a1a63d7 bump version to 17.07.0-ce-rc1
Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
2017-07-25 18:35:19 -07:00
b3be2b02db Merge pull request #124 from vieux/17.07-changelog
[17.07.x] Changelog
2017-07-25 18:33:07 -07:00
3ddc583fc2 Merge pull request #139 from cyli/re-vendor-swarmkit-17.07
[17.07] Re-vendor swarmkit
2017-07-25 18:31:57 -07:00
b92b043ac1 Re-vendors swarmkit to include the following fix:
- https://github.com/docker/swarmkit/pull/2323 (fix for watch server being run only on leader)

Signed-off-by: Ying <ying.li@docker.com>
2017-07-25 16:16:43 -07:00
1c65e20e5b Merge pull request #136 from cyli/re-vendor-swarmkit-17.07
[17.07] Re-vendor swarmkit
2017-07-25 15:23:43 -07:00
1fef2502d2 Merge pull request #131 from vieux/backport-fix-live-restore
[17.07] Graceful upgrade of containerd and runc state files upon live-restore
2017-07-25 15:19:55 -07:00
edadfd04be Merge pull request #137 from seemethere/cherry_pick_packaging_3_11_40_41
[17.07] Cherry pick latest packaging commits
2017-07-25 14:02:47 -07:00
83e8a29ede Removes telemetry mentions from Fedora 26
Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
(cherry picked from commit a8c7e75bfd263b2f7445b06f749aba13aaeac789)
Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
2017-07-25 11:29:40 -07:00
0ff00e73c9 End Ubuntu 16.10 (Yakkety) support
Ubuntu 16.10 (Yakkety) reached it's "End of Life" on July 20, 2017:
http://fridge.ubuntu.com/2017/07/20/ubuntu-16-10-yakkety-yak-end-of-life-reached-on-july-20-2017/

As such we are removing support for the distribution for future versions
of Docker packages.

Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
(cherry picked from commit 36cc14cd480879391eeb9f470417698618f4cdb3)
Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
2017-07-25 10:31:41 -07:00
32709fc76b Update spec to align with new spec files
Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
(cherry picked from commit feb2f647c5570753a607b71bb476028c8d1b4e55)
Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
2017-07-25 10:30:18 -07:00
95fa7a2d20 Update go version, add distro/suite tags
Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
(cherry picked from commit 26b1bd9f3da46b907021ae66d310405057e29634)
Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
2017-07-25 10:30:18 -07:00
22f30a8bb1 build fedora 26 packages
Just released: https://docs.fedoraproject.org/en-US/Fedora/26/html/Release_Notes/

Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
(cherry picked from commit 412824b465d8334278bb9c02f0a5ee3d294826fd)
Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
2017-07-25 10:30:16 -07:00
e5f33c5d16 Revert "Added the metrics plugin to the RPM/DEB packages"
This reverts commit ceac22bf2240df483b1d3bd3a19ac681e665f910.

Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
(cherry picked from commit bda73f99e455bd3f041ccc843f094186ca8b8623)
Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
2017-07-25 10:20:14 -07:00
c203be7748 Tell build-deb to throw up errors
Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
(cherry picked from commit 3618e4aa262f154f79c804752d69110772e636a3)
Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
2017-07-25 10:19:29 -07:00
b9b61ead5a Add raspbian deb packages
(cherry picked from commit 1c61fdcafc4f6fa0e41c267cd857d0b60d3a2dbd)
Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
2017-07-25 10:19:20 -07:00
8ddc1eeedd Added the metrics plugin to the RPM/DEB packages
- centos
 - fedora
 - ubuntu
 - debian

Signed-off-by: Roberto Gandolfo Hashioka <roberto_hashioka@hotmail.com>
(cherry picked from commit ceac22bf2240df483b1d3bd3a19ac681e665f910)
Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
2017-07-25 10:18:39 -07:00
1dd11b3bb6 Re-vendors swarmkit to include the following fixes:
- https://github.com/docker/swarmkit/pull/2288 (Allow updates of failed services with restart policy "none")
- https://github.com/docker/swarmkit/pull/2304 (Reset restart history when task spec changes)
- https://github.com/docker/swarmkit/pull/2309 (updating the service spec version when rolling back)
- https://github.com/docker/swarmkit/pull/2310 (fix for slow swarm shutdown)

Signed-off-by: Ying <ying.li@docker.com>
2017-07-24 18:01:58 -07:00
9e07b412c7 add 17.07 changelog
Signed-off-by: Victor Vieux <victorvieux@gmail.com>
2017-07-24 17:10:41 -07:00
1b065d3124 [engine] Graceful upgrade of containerd and runc state files upon live-restore
Vendors new dependency github.com/crosbymichael/upgrade

Signed-off-by: Tibor Vass <tibor@docker.com>
(cherry picked from commit 358c36e930)
Signed-off-by: Victor Vieux <victorvieux@gmail.com>
2017-07-24 16:51:35 -07:00
bd4b12ce5d Merge pull request #128 from seemethere/cherry_pick_packaging_37
[17.07.x] Change make -C to a cd command
2017-07-24 14:57:23 -07:00
c249bb0aea Change make -C to a cd command
Tried out make -C in this scenario and it did not seem to function
correctly, changed to cd.

Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
(cherry picked from commit 3a548f8815d5308b197abea1e39f0a0a4939c4f2)
Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
2017-07-24 14:36:41 -07:00
cb16f8905b Merge pull request #126 from seemethere/cherry_pick_moby_34206
[17.07.x] Add go-autogen to integration tests
2017-07-21 15:26:19 -07:00
c4d547ce61 Add go-autogen to integration tests
Integration test were failing in trial runs for docker-ce 17.07 due to
the lack of go-autogen being sourced in `hack/make.sh`. This re-adds
go-autogen to be sourced for test-integration-cli so that we can
actually run tests without the error found in:
https://github.com/moby/moby/pull/33857

Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
(cherry picked from commit 3cdd471cac8193c34d8483255065c6c28a7b1645)
Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
2017-07-21 14:41:26 -07:00
00313efde4 Merge pull request #123 from seemethere/cherry_pick_packaging_25
[17.07] Add ubuntu-xenial aarch64 Dockerfile
2017-07-20 16:35:08 -07:00
8bd577e801 Add ubuntu-xenial aarch64 Dockerfile
Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
(cherry picked from commit 94cbc76b701b8b108f6a337b1695bd664f441bdc)
Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
2017-07-20 16:33:08 -07:00
24244c1083 Merge pull request #122 from seemethere/fix_1707_version
[17.07.x] Increment engine version to 17.07.0-dev
2017-07-20 15:20:35 -07:00
4ca7fba8e3 Increment engine version to 17.07
Release jobs will fail if the version files are out of sync due to how
`hack/make.sh` builds binaries (putting them into the version folder
according to `components/engine/VERSION` instead of the base `VERSION`
file)

Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
2017-07-20 14:44:56 -07:00
1530 changed files with 31840 additions and 47517 deletions

File diff suppressed because it is too large Load Diff

View File

@ -2,125 +2,6 @@
Want to contribute on Docker CE? Awesome!
This page contains information about reporting issues as well as some tips and
guidelines useful to experienced open source contributors. Finally, make sure
you read our [community guidelines](#docker-community-guidelines) before you
start participating.
## Topics
* [Reporting Security Issues](#reporting-security-issues)
* [Reporting Issues](#reporting-other-issues)
* [Submitting Pull Requests](#submitting-pull-requests)
* [Community Guidelines](#docker-community-guidelines)
## Reporting security issues
The Docker maintainers take security seriously. If you discover a security
issue, please bring it to their attention right away!
Please **DO NOT** file a public issue, instead send your report privately to
[security@docker.com](mailto:security@docker.com).
Security reports are greatly appreciated and we will publicly thank you for it.
We also like to send gifts&mdash;if you're into Docker schwag, make sure to let
us know. We currently do not offer a paid security bounty program, but are not
ruling it out in the future.
## Reporting other issues
There are separate issue-tracking repos for the end user Docker CE
products specialized for a platform. Find your issue or file a new issue
for the platform you are using:
* https://github.com/docker/for-linux
* https://github.com/docker/for-mac
* https://github.com/docker/for-win
* https://github.com/docker/for-aws
* https://github.com/docker/for-azure
When reporting issues, always include:
* The output of `docker version`.
* The output of `docker info`.
If presented with a template when creating an issue, please follow its directions.
Also include the steps required to reproduce the problem if possible and
applicable. This information will help us review and fix your issue faster.
When sending lengthy log-files, consider posting them as a gist (https://gist.github.com).
Don't forget to remove sensitive data from your logfiles before posting (you can
replace those parts with "REDACTED").
## Submitting pull requests
Please see the corresponding `CONTRIBUTING.md` file of each component for more information:
* Changes to the `engine` should be directed upstream to https://github.com/moby/moby
* Changes to the `cli` should be directed upstream to https://github.com/docker/cli
* Changes to the `packaging` should be directed upstream to https://github.com/docker/docker-ce-packaging
## Docker community guidelines
We want to keep the Docker community awesome, growing and collaborative. We need
your help to keep it that way. To help with this we've come up with some general
guidelines for the community as a whole:
* Be nice: Be courteous, respectful and polite to fellow community members.
Regional, racial, gender, or other abuse will not be tolerated. We like
nice people way better than mean ones!
* Encourage diversity and participation: Make everyone in our community feel
welcome, regardless of their background and the extent of their
contributions, and do everything possible to encourage participation in
our community.
* Keep it legal: Basically, don't get us in trouble. Share only content that
you own, do not share private or sensitive information, and don't break
the law.
* Stay on topic: Make sure that you are posting to the correct channel and
avoid off-topic discussions. Remember when you update an issue or respond
to an email you are potentially sending to a large number of people. Please
consider this before you update. Also remember that nobody likes spam.
* Don't send email to the maintainers: There's no need to send email to the
maintainers to ask them to investigate an issue or to take a look at a
pull request. Instead of sending an email, GitHub mentions should be
used to ping maintainers to review a pull request, a proposal or an
issue.
### Guideline violations — 3 strikes method
The point of this section is not to find opportunities to punish people, but we
do need a fair way to deal with people who are making our community suck.
1. First occurrence: We'll give you a friendly, but public reminder that the
behavior is inappropriate according to our guidelines.
2. Second occurrence: We will send you a private message with a warning that
any additional violations will result in removal from the community.
3. Third occurrence: Depending on the violation, we may need to delete or ban
your account.
**Notes:**
* Obvious spammers are banned on first occurrence. If we don't do this, we'll
have spam all over the place.
* Violations are forgiven after 6 months of good behavior, and we won't hold a
grudge.
* People who commit minor infractions will get some education, rather than
hammering them in the 3 strikes process.
* The rules apply equally to everyone in the community, no matter how much
you've contributed.
* Extreme violations of a threatening, abusive, destructive or illegal nature
will be addressed immediately and are not subject to 3 strikes or forgiveness.
* Contact abuse@docker.com to report abuse or appeal violations. In the case of
appeals, we know that mistakes happen, and we'll work with you to come up with a
fair solution if there has been a misunderstanding.

View File

@ -25,3 +25,6 @@ clean: ## clean the build artifacts
-$(MAKE) -C $(CLI_DIR) clean
-$(MAKE) -C $(ENGINE_DIR) clean
-$(MAKE) -C $(PACKAGING_DIR) clean
vendor: $(CLI_DIR)/build/docker
docker run --rm -it -v $(CLI_DIR):/go/src/github.com/docker/cli -v $(ENGINE_DIR):/go/src/github.com/docker/docker docker-cli-dev sh -c 'cd /go/src/github.com/docker/docker && git init && git add . && git -c user.name=user -c user.email=email@example.com commit -m first && cd /go/src/github.com/docker/cli && vndr; rm -rf /go/src/github.com/docker/docker/.git'

View File

@ -1 +1 @@
17.09.0-ce-rc1
17.07.0-ce-rc3

View File

@ -1,7 +1,6 @@
# Github code owners
# See https://github.com/blog/2392-introducing-code-owners
cli/command/stack/** @dnephin @vdemeester
cli/compose/** @dnephin @vdemeester
contrib/completion/bash/** @albers
contrib/completion/zsh/** @sdurrheimer

View File

@ -1,365 +0,0 @@
# Contributing to Docker
Want to hack on Docker? Awesome! We have a contributor's guide that explains
[setting up a Docker development environment and the contribution
process](https://docs.docker.com/opensource/project/who-written-for/).
This page contains information about reporting issues as well as some tips and
guidelines useful to experienced open source contributors. Finally, make sure
you read our [community guidelines](#docker-community-guidelines) before you
start participating.
## Topics
* [Reporting Security Issues](#reporting-security-issues)
* [Design and Cleanup Proposals](#design-and-cleanup-proposals)
* [Reporting Issues](#reporting-other-issues)
* [Quick Contribution Tips and Guidelines](#quick-contribution-tips-and-guidelines)
* [Community Guidelines](#docker-community-guidelines)
## Reporting security issues
The Docker maintainers take security seriously. If you discover a security
issue, please bring it to their attention right away!
Please **DO NOT** file a public issue, instead send your report privately to
[security@docker.com](mailto:security@docker.com).
Security reports are greatly appreciated and we will publicly thank you for it.
We also like to send gifts&mdash;if you're into Docker schwag, make sure to let
us know. We currently do not offer a paid security bounty program, but are not
ruling it out in the future.
## Reporting other issues
A great way to contribute to the project is to send a detailed report when you
encounter an issue. We always appreciate a well-written, thorough bug report,
and will thank you for it!
Check that [our issue database](https://github.com/docker/cli/issues)
doesn't already include that problem or suggestion before submitting an issue.
If you find a match, you can use the "subscribe" button to get notified on
updates. Do *not* leave random "+1" or "I have this too" comments, as they
only clutter the discussion, and don't help resolving it. However, if you
have ways to reproduce the issue or have additional information that may help
resolving the issue, please leave a comment.
When reporting issues, always include:
* The output of `docker version`.
* The output of `docker info`.
Also include the steps required to reproduce the problem if possible and
applicable. This information will help us review and fix your issue faster.
When sending lengthy log-files, consider posting them as a gist (https://gist.github.com).
Don't forget to remove sensitive data from your logfiles before posting (you can
replace those parts with "REDACTED").
## Quick contribution tips and guidelines
This section gives the experienced contributor some tips and guidelines.
### Pull requests are always welcome
Not sure if that typo is worth a pull request? Found a bug and know how to fix
it? Do it! We will appreciate it. Any significant improvement should be
documented as [a GitHub issue](https://github.com/docker/cli/issues) before
anybody starts working on it.
We are always thrilled to receive pull requests. We do our best to process them
quickly. If your pull request is not accepted on the first try,
don't get discouraged! Our contributor's guide explains [the review process we
use for simple changes](https://docs.docker.com/opensource/workflow/make-a-contribution/).
### Talking to other Docker users and contributors
<table class="tg">
<col width="45%">
<col width="65%">
<tr>
<td>Forums</td>
<td>
A public forum for users to discuss questions and explore current design patterns and
best practices about Docker and related projects in the Docker Ecosystem. To participate,
just log in with your Docker Hub account on <a href="https://forums.docker.com" target="_blank">https://forums.docker.com</a>.
</td>
</tr>
<tr>
<td>Community Slack</td>
<td>
The Docker Community has a dedicated Slack chat to discuss features and issues. You can sign-up <a href="https://community.docker.com/registrations/groups/4316" target="_blank">with this link</a>.
</td>
</tr>
<tr>
<td>Twitter</td>
<td>
You can follow <a href="https://twitter.com/docker/" target="_blank">Docker's Twitter feed</a>
to get updates on our products. You can also tweet us questions or just
share blogs or stories.
</td>
</tr>
<tr>
<td>Stack Overflow</td>
<td>
Stack Overflow has over 17000 Docker questions listed. We regularly
monitor <a href="https://stackoverflow.com/search?tab=newest&q=docker" target="_blank">Docker questions</a>
and so do many other knowledgeable Docker users.
</td>
</tr>
</table>
### Conventions
Fork the repository and make changes on your fork in a feature branch:
- If it's a bug fix branch, name it XXXX-something where XXXX is the number of
the issue.
- If it's a feature branch, create an enhancement issue to announce
your intentions, and name it XXXX-something where XXXX is the number of the
issue.
Submit unit tests for your changes. Go has a great test framework built in; use
it! Take a look at existing tests for inspiration. [Run the full test
suite](README.md) on your branch before
submitting a pull request.
Update the documentation when creating or modifying features. Test your
documentation changes for clarity, concision, and correctness, as well as a
clean documentation build. See our contributors guide for [our style
guide](https://docs.docker.com/opensource/doc-style) and instructions on [building
the documentation](https://docs.docker.com/opensource/project/test-and-docs/#build-and-test-the-documentation).
Write clean code. Universally formatted code promotes ease of writing, reading,
and maintenance. Always run `gofmt -s -w file.go` on each changed file before
committing your changes. Most editors have plug-ins that do this automatically.
Pull request descriptions should be as clear as possible and include a reference
to all the issues that they address.
Commit messages must start with a capitalized and short summary (max. 50 chars)
written in the imperative, followed by an optional, more detailed explanatory
text which is separated from the summary by an empty line.
Code review comments may be added to your pull request. Discuss, then make the
suggested modifications and push additional commits to your feature branch. Post
a comment after pushing. New commits show up in the pull request automatically,
but the reviewers are notified only when you comment.
Pull requests must be cleanly rebased on top of master without multiple branches
mixed into the PR.
**Git tip**: If your PR no longer merges cleanly, use `rebase master` in your
feature branch to update your pull request rather than `merge master`.
Before you make a pull request, squash your commits into logical units of work
using `git rebase -i` and `git push -f`. A logical unit of work is a consistent
set of patches that should be reviewed together: for example, upgrading the
version of a vendored dependency and taking advantage of its now available new
feature constitute two separate units of work. Implementing a new function and
calling it in another file constitute a single logical unit of work. The very
high majority of submissions should have a single commit, so if in doubt: squash
down to one.
After every commit, make sure the test suite passes. Include documentation
changes in the same pull request so that a revert would remove all traces of
the feature or fix.
Include an issue reference like `Closes #XXXX` or `Fixes #XXXX` in the pull request
description that close an issue. Including references automatically closes the issue
on a merge.
Please do not add yourself to the `AUTHORS` file, as it is regenerated regularly
from the Git history.
Please see the [Coding Style](#coding-style) for further guidelines.
### Merge approval
Docker maintainers use LGTM (Looks Good To Me) in comments on the code review to
indicate acceptance.
A change requires LGTMs from an absolute majority of the maintainers of each
component affected. For example, if a change affects `docs/` and `registry/`, it
needs an absolute majority from the maintainers of `docs/` AND, separately, an
absolute majority of the maintainers of `registry/`.
For more details, see the [MAINTAINERS](MAINTAINERS) page.
### Sign your work
The sign-off is a simple line at the end of the explanation for the patch. Your
signature certifies that you wrote the patch or otherwise have the right to pass
it on as an open-source patch. The rules are pretty simple: if you can certify
the below (from [developercertificate.org](http://developercertificate.org/)):
```
Developer Certificate of Origin
Version 1.1
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
660 York Street, Suite 102,
San Francisco, CA 94110 USA
Everyone is permitted to copy and distribute verbatim copies of this
license document, but changing it is not allowed.
Developer's Certificate of Origin 1.1
By making a contribution to this project, I certify that:
(a) The contribution was created in whole or in part by me and I
have the right to submit it under the open source license
indicated in the file; or
(b) The contribution is based upon previous work that, to the best
of my knowledge, is covered under an appropriate open source
license and I have the right under that license to submit that
work with modifications, whether created in whole or in part
by me, under the same open source license (unless I am
permitted to submit under a different license), as indicated
in the file; or
(c) The contribution was provided directly to me by some other
person who certified (a), (b) or (c) and I have not modified
it.
(d) I understand and agree that this project and the contribution
are public and that a record of the contribution (including all
personal information I submit with it, including my sign-off) is
maintained indefinitely and may be redistributed consistent with
this project or the open source license(s) involved.
```
Then you just add a line to every git commit message:
Signed-off-by: Joe Smith <joe.smith@email.com>
Use your real name (sorry, no pseudonyms or anonymous contributions.)
If you set your `user.name` and `user.email` git configs, you can sign your
commit automatically with `git commit -s`.
### How can I become a maintainer?
The procedures for adding new maintainers are explained in the
global [MAINTAINERS](https://github.com/docker/opensource/blob/master/MAINTAINERS)
file in the [https://github.com/docker/opensource/](https://github.com/docker/opensource/)
repository.
Don't forget: being a maintainer is a time investment. Make sure you
will have time to make yourself available. You don't have to be a
maintainer to make a difference on the project!
## Docker community guidelines
We want to keep the Docker community awesome, growing and collaborative. We need
your help to keep it that way. To help with this we've come up with some general
guidelines for the community as a whole:
* Be nice: Be courteous, respectful and polite to fellow community members:
no regional, racial, gender, or other abuse will be tolerated. We like
nice people way better than mean ones!
* Encourage diversity and participation: Make everyone in our community feel
welcome, regardless of their background and the extent of their
contributions, and do everything possible to encourage participation in
our community.
* Keep it legal: Basically, don't get us in trouble. Share only content that
you own, do not share private or sensitive information, and don't break
the law.
* Stay on topic: Make sure that you are posting to the correct channel and
avoid off-topic discussions. Remember when you update an issue or respond
to an email you are potentially sending to a large number of people. Please
consider this before you update. Also remember that nobody likes spam.
* Don't send email to the maintainers: There's no need to send email to the
maintainers to ask them to investigate an issue or to take a look at a
pull request. Instead of sending an email, GitHub mentions should be
used to ping maintainers to review a pull request, a proposal or an
issue.
### Guideline violations — 3 strikes method
The point of this section is not to find opportunities to punish people, but we
do need a fair way to deal with people who are making our community suck.
1. First occurrence: We'll give you a friendly, but public reminder that the
behavior is inappropriate according to our guidelines.
2. Second occurrence: We will send you a private message with a warning that
any additional violations will result in removal from the community.
3. Third occurrence: Depending on the violation, we may need to delete or ban
your account.
**Notes:**
* Obvious spammers are banned on first occurrence. If we don't do this, we'll
have spam all over the place.
* Violations are forgiven after 6 months of good behavior, and we won't hold a
grudge.
* People who commit minor infractions will get some education, rather than
hammering them in the 3 strikes process.
* The rules apply equally to everyone in the community, no matter how much
you've contributed.
* Extreme violations of a threatening, abusive, destructive or illegal nature
will be addressed immediately and are not subject to 3 strikes or forgiveness.
* Contact abuse@docker.com to report abuse or appeal violations. In the case of
appeals, we know that mistakes happen, and we'll work with you to come up with a
fair solution if there has been a misunderstanding.
## Coding Style
Unless explicitly stated, we follow all coding guidelines from the Go
community. While some of these standards may seem arbitrary, they somehow seem
to result in a solid, consistent codebase.
It is possible that the code base does not currently comply with these
guidelines. We are not looking for a massive PR that fixes this, since that
goes against the spirit of the guidelines. All new contributions should make a
best effort to clean up and make the code base better than they left it.
Obviously, apply your best judgement. Remember, the goal here is to make the
code base easier for humans to navigate and understand. Always keep that in
mind when nudging others to comply.
The rules:
1. All code should be formatted with `gofmt -s`.
2. All code should pass the default levels of
[`golint`](https://github.com/golang/lint).
3. All code should follow the guidelines covered in [Effective
Go](http://golang.org/doc/effective_go.html) and [Go Code Review
Comments](https://github.com/golang/go/wiki/CodeReviewComments).
4. Comment the code. Tell us the why, the history and the context.
5. Document _all_ declarations and methods, even private ones. Declare
expectations, caveats and anything else that may be important. If a type
gets exported, having the comments already there will ensure it's ready.
6. Variable name length should be proportional to its context and no longer.
`noCommaALongVariableNameLikeThisIsNotMoreClearWhenASimpleCommentWouldDo`.
In practice, short methods will have short variable names and globals will
have longer names.
7. No underscores in package names. If you need a compound name, step back,
and re-examine why you need a compound name. If you still think you need a
compound name, lose the underscore.
8. No utils or helpers packages. If a function is not general enough to
warrant its own package, it has not been written generally enough to be a
part of a util package. Just leave it unexported and well-documented.
9. All tests should run with `go test` and outside tooling should not be
required. No, we don't need another unit testing framework. Assertion
packages are acceptable if they provide _real_ incremental value.
10. Even though we call these "rules" above, they are actually just
guidelines. Since you've read all the rules, you now know that.
If you are having trouble getting into the mood of idiomatic Go, we recommend
reading through [Effective Go](https://golang.org/doc/effective_go.html). The
[Go Blog](https://blog.golang.org) is also a great resource. Drinking the
kool-aid is a lot easier than going thirsty.

View File

@ -1,12 +0,0 @@
wrappedNode(label: 'linux && x86_64', cleanWorkspace: true) {
timeout(time: 60, unit: 'MINUTES') {
stage "Git Checkout"
checkout scm
stage "Run end-to-end test suite"
sh "docker version"
sh "E2E_UNIQUE_ID=clie2e${BUILD_NUMBER} \
IMAGE_TAG=clie2e${BUILD_NUMBER} \
make -f docker.Makefile test-e2e"
}
}

View File

@ -3,75 +3,64 @@
#
all: binary
_:=$(shell ./scripts/warn-outside-container $(MAKECMDGOALS))
# remove build artifacts
.PHONY: clean
clean: ## remove build artifacts
clean:
rm -rf ./build/* cli/winresources/rsrc_* ./man/man[1-9] docs/yaml/gen
.PHONY: test-unit
test-unit: ## run unit test
./scripts/test/unit $(shell go list ./... | grep -vE '/vendor/|/e2e/')
# run go test
# the "-tags daemon" part is temporary
.PHONY: test
test: test-unit ## run tests
test:
./scripts/test/unit $(shell go list ./... | grep -v '/vendor/')
.PHONY: test-coverage
test-coverage: ## run test coverage
./scripts/test/unit-with-coverage $(shell go list ./... | grep -vE '/vendor/|/e2e/')
test-coverage:
./scripts/test/unit-with-coverage $(shell go list ./... | grep -v '/vendor/')
.PHONY: lint
lint: ## run all the lint tools
lint:
gometalinter --config gometalinter.json ./...
.PHONY: binary
binary: ## build executable for Linux
binary:
@echo "WARNING: binary creates a Linux executable. Use cross for macOS or Windows."
./scripts/build/binary
.PHONY: cross
cross: ## build executable for macOS and Windows
cross:
./scripts/build/cross
.PHONY: dynbinary
dynbinary: ## build dynamically linked binary
dynbinary:
./scripts/build/dynbinary
.PHONY: watch
watch: ## monitor file changes and run go test
watch:
./scripts/test/watch
vendor: vendor.conf ## check that vendor matches vendor.conf
# Check vendor matches vendor.conf
vendor: vendor.conf
vndr 2> /dev/null
scripts/validate/check-git-diff vendor
## generate man pages from go source and markdown
.PHONY: manpages
manpages: ## generate man pages from go source and markdown
manpages:
scripts/docs/generate-man.sh
## generate documentation YAML files consumed by docs repo
.PHONY: yamldocs
yamldocs: ## generate documentation YAML files consumed by docs repo
yamldocs:
scripts/docs/generate-yaml.sh
## Shellcheck validation
.PHONY: shellcheck
shellcheck: ## run shellcheck validation
shellcheck:
scripts/validate/shellcheck
.PHONY: help
help: ## print this help
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {sub("\\\\n",sprintf("\n%22c"," "), $$2);printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
cli/compose/schema/bindata.go: cli/compose/schema/data/*.json
go generate github.com/docker/cli/cli/compose/schema
compose-jsonschema: cli/compose/schema/bindata.go
scripts/validate/check-git-diff cli/compose/schema/bindata.go
.PHONY: ci-validate
ci-validate:
time make -B vendor
time make -B compose-jsonschema
time make manpages
time make yamldocs

View File

@ -29,12 +29,6 @@ Run all linting:
$ make -f docker.Makefile lint
```
List all the available targets:
```
$ make help
```
### In-container development environment
Start an interactive development environment:

View File

@ -1,87 +0,0 @@
# Testing
The following guidelines summarize the testing policy for docker/cli.
## Unit Test Suite
All code changes should have unit test coverage.
Error cases should be tested with unit tests.
Bug fixes should be covered by new unit tests or additional assertions in
existing unit tests.
### Details
The unit test suite follows the standard Go testing convention. Tests are
located in the package directory in `_test.go` files.
Unit tests should be named using the convention:
```
Test<Function Name><Test Case Name>
```
[Table tests](https://github.com/golang/go/wiki/TableDrivenTests) should be used
where appropriate, but may not be appropriate in all cases.
Assertions should be made using
[testify/assert](https://godoc.org/github.com/stretchr/testify/assert) and test
requirements should be verified using
[testify/require](https://godoc.org/github.com/stretchr/testify/require).
Fakes, and testing utilities can be found in
[internal/test](https://godoc.org/github.com/docker/cli/internal/test) and
[gotestyourself](https://godoc.org/github.com/gotestyourself/gotestyourself).
## End-to-End Test Suite
The end-to-end test suite tests a cli binary against a real API backend.
### Guidelines
Each feature (subcommand) should have a single end-to-end test for
the success case. The test should include all (or most) flags/options supported
by that feature.
In some rare cases a couple additional end-to-end tests may be written for a
sufficiently complex and critical feature (ex: `container run`, `service
create`, `service update`, and `docker build` may have ~3-5 cases each).
In some rare cases a sufficiently critical error paths may have a single
end-to-end test case.
In all other cases the behaviour should be covered by unit tests.
If a code change adds a new flag, that flag should be added to the existing
"success case" end-to-end test.
If a code change fixes a bug, that bug fix should be covered either by adding
assertions to the existing end-to-end test, or with one or more unit test.
### Details
The end-to-end test suite is located in
[./e2e](https://github.com/docker/cli/tree/master/e2e). Each directory in `e2e`
corresponds to a directory in `cli/command` and contains the tests for that
subcommand. Files in each directory should be named `<command>_test.go` where
command is the basename of the command (ex: the test for `docker stack deploy`
is found in `e2e/stack/deploy_test.go`).
Tests should be named using the convention:
```
Test<Command Basename>[<Test Case Name>]
```
where the test case name is only required when there are multiple test cases for
a single command.
End-to-end test should run the `docker` binary using
[gotestyourself/icmd](https://godoc.org/github.com/gotestyourself/gotestyourself/icmd)
and make assertions about the exit code, stdout, stderr, and local file system.
Any Docker image or registry operations should use `registry:5000/<image name>`
to communicate with the local instance of the Docker registry. To load
additional fixture images to the registry see
[scripts/test/e2e/run](https://github.com/docker/cli/blob/master/scripts/test/e2e/run).

View File

@ -1 +1 @@
17.09.0-ce-rc1
17.07.0-ce-rc3

View File

@ -71,8 +71,7 @@ jobs:
test-$CIRCLE_BUILD_NUM:/go/src/github.com/docker/cli/coverage.txt \
coverage.txt
apk add -U bash curl
curl -s https://codecov.io/bash | bash || \
echo 'Codecov failed to upload'
curl -s https://codecov.io/bash | bash
validate:
working_directory: /work
@ -90,7 +89,7 @@ jobs:
rm -f .dockerignore # include .git
docker build -f $dockerfile --tag cli-builder-with-git:$CIRCLE_BUILD_NUM .
docker run --rm cli-builder-with-git:$CIRCLE_BUILD_NUM \
make ci-validate
make -B vendor compose-jsonschema manpages yamldocs
shellcheck:
working_directory: /work
docker: [{image: 'docker:17.06-git'}]
@ -104,7 +103,7 @@ jobs:
echo "COPY . ." >> $dockerfile
docker build -f $dockerfile --tag cli-validator:$CIRCLE_BUILD_NUM .
docker run --rm cli-validator:$CIRCLE_BUILD_NUM \
make shellcheck
make -B shellcheck
workflows:
version: 2
ci:

View File

@ -5,9 +5,9 @@ import (
"strings"
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/testutil"
"github.com/docker/cli/cli/internal/test"
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/testutil"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
)
@ -20,11 +20,11 @@ func TestCheckpointCreateErrors(t *testing.T) {
}{
{
args: []string{"too-few-arguments"},
expectedError: "requires exactly 2 arguments",
expectedError: "requires exactly 2 argument(s)",
},
{
args: []string{"too", "many", "arguments"},
expectedError: "requires exactly 2 arguments",
expectedError: "requires exactly 2 argument(s)",
},
{
args: []string{"foo", "bar"},

View File

@ -1,13 +1,14 @@
package checkpoint
import (
"bytes"
"io/ioutil"
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/testutil"
"github.com/docker/cli/cli/internal/test"
"github.com/docker/docker/api/types"
"github.com/gotestyourself/gotestyourself/golden"
"github.com/docker/docker/pkg/testutil"
"github.com/docker/docker/pkg/testutil/golden"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
)
@ -36,9 +37,9 @@ func TestCheckpointListErrors(t *testing.T) {
}
for _, tc := range testCases {
cli := test.NewFakeCli(&fakeClient{
cli := test.NewFakeCliWithOutput(&fakeClient{
checkpointListFunc: tc.checkpointListFunc,
})
}, &bytes.Buffer{})
cmd := newListCommand(cli)
cmd.SetArgs(tc.args)
cmd.SetOutput(ioutil.Discard)
@ -48,7 +49,8 @@ func TestCheckpointListErrors(t *testing.T) {
func TestCheckpointListWithOptions(t *testing.T) {
var containerID, checkpointDir string
cli := test.NewFakeCli(&fakeClient{
buf := new(bytes.Buffer)
cli := test.NewFakeCliWithOutput(&fakeClient{
checkpointListFunc: func(container string, options types.CheckpointListOptions) ([]types.Checkpoint, error) {
containerID = container
checkpointDir = options.CheckpointDir
@ -56,12 +58,14 @@ func TestCheckpointListWithOptions(t *testing.T) {
{Name: "checkpoint-foo"},
}, nil
},
})
}, buf)
cmd := newListCommand(cli)
cmd.SetArgs([]string{"container-foo"})
cmd.Flags().Set("checkpoint-dir", "/dir/foo")
assert.NoError(t, cmd.Execute())
assert.Equal(t, "container-foo", containerID)
assert.Equal(t, "/dir/foo", checkpointDir)
golden.Assert(t, cli.OutBuffer().String(), "checkpoint-list-with-options.golden")
actual := buf.String()
expected := golden.Get(t, []byte(actual), "checkpoint-list-with-options.golden")
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
}

View File

@ -1,12 +1,13 @@
package checkpoint
import (
"bytes"
"io/ioutil"
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/testutil"
"github.com/docker/cli/cli/internal/test"
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/testutil"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
)
@ -19,11 +20,11 @@ func TestCheckpointRemoveErrors(t *testing.T) {
}{
{
args: []string{"too-few-arguments"},
expectedError: "requires exactly 2 arguments",
expectedError: "requires exactly 2 argument(s)",
},
{
args: []string{"too", "many", "arguments"},
expectedError: "requires exactly 2 arguments",
expectedError: "requires exactly 2 argument(s)",
},
{
args: []string{"foo", "bar"},
@ -35,9 +36,9 @@ func TestCheckpointRemoveErrors(t *testing.T) {
}
for _, tc := range testCases {
cli := test.NewFakeCli(&fakeClient{
cli := test.NewFakeCliWithOutput(&fakeClient{
checkpointDeleteFunc: tc.checkpointDeleteFunc,
})
}, &bytes.Buffer{})
cmd := newRemoveCommand(cli)
cmd.SetArgs(tc.args)
cmd.SetOutput(ioutil.Discard)
@ -47,14 +48,14 @@ func TestCheckpointRemoveErrors(t *testing.T) {
func TestCheckpointRemoveWithOptions(t *testing.T) {
var containerID, checkpointID, checkpointDir string
cli := test.NewFakeCli(&fakeClient{
cli := test.NewFakeCliWithOutput(&fakeClient{
checkpointDeleteFunc: func(container string, options types.CheckpointDeleteOptions) error {
containerID = container
checkpointID = options.CheckpointID
checkpointDir = options.CheckpointDir
return nil
},
})
}, &bytes.Buffer{})
cmd := newRemoveCommand(cli)
cmd.SetArgs([]string{"container-foo", "checkpoint-bar"})
cmd.Flags().Set("checkpoint-dir", "/dir/foo")

View File

@ -1,57 +0,0 @@
package command
import (
"os"
"testing"
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/cli/flags"
"github.com/docker/docker/api"
"github.com/docker/docker/client"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewAPIClientFromFlags(t *testing.T) {
host := "unix://path"
opts := &flags.CommonOptions{Hosts: []string{host}}
configFile := &configfile.ConfigFile{
HTTPHeaders: map[string]string{
"My-Header": "Custom-Value",
},
}
apiclient, err := NewAPIClientFromFlags(opts, configFile)
require.NoError(t, err)
assert.Equal(t, host, apiclient.DaemonHost())
expectedHeaders := map[string]string{
"My-Header": "Custom-Value",
"User-Agent": UserAgent(),
}
assert.Equal(t, expectedHeaders, apiclient.(*client.Client).CustomHTTPHeaders())
assert.Equal(t, api.DefaultVersion, apiclient.ClientVersion())
}
func TestNewAPIClientFromFlagsWithAPIVersionFromEnv(t *testing.T) {
customVersion := "v3.3.3"
defer patchEnvVariable(t, "DOCKER_API_VERSION", customVersion)()
opts := &flags.CommonOptions{}
configFile := &configfile.ConfigFile{}
apiclient, err := NewAPIClientFromFlags(opts, configFile)
require.NoError(t, err)
assert.Equal(t, customVersion, apiclient.ClientVersion())
}
// TODO: move to gotestyourself
func patchEnvVariable(t *testing.T, key, value string) func() {
oldValue, ok := os.LookupEnv(key)
require.NoError(t, os.Setenv(key, value))
return func() {
if !ok {
require.NoError(t, os.Unsetenv(key))
return
}
require.NoError(t, os.Setenv(key, oldValue))
}
}

View File

@ -7,11 +7,11 @@ import (
"strings"
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/testutil"
"github.com/docker/cli/cli/internal/test"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/swarm"
"github.com/gotestyourself/gotestyourself/golden"
"github.com/docker/docker/pkg/testutil"
"github.com/docker/docker/pkg/testutil/golden"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
)
@ -26,10 +26,10 @@ func TestConfigCreateErrors(t *testing.T) {
}{
{
args: []string{"too_few"},
expectedError: "requires exactly 2 arguments",
expectedError: "requires exactly 2 argument(s)",
},
{args: []string{"too", "many", "arguments"},
expectedError: "requires exactly 2 arguments",
expectedError: "requires exactly 2 argument(s)",
},
{
args: []string{"name", filepath.Join("testdata", configDataFile)},
@ -71,7 +71,8 @@ func TestConfigCreateWithName(t *testing.T) {
cmd := newConfigCreateCommand(cli)
cmd.SetArgs([]string{name, filepath.Join("testdata", configDataFile)})
assert.NoError(t, cmd.Execute())
golden.Assert(t, string(actual), configDataFile)
expected := golden.Get(t, actual, configDataFile)
assert.Equal(t, string(expected), string(actual))
assert.Equal(t, "ID-"+name, strings.TrimSpace(cli.OutBuffer().String()))
}

View File

@ -1,18 +1,19 @@
package config
import (
"bytes"
"fmt"
"io/ioutil"
"testing"
"time"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/cli/internal/test"
"github.com/docker/docker/api/types/swarm"
"github.com/pkg/errors"
// Import builders to get the builder function as package function
. "github.com/docker/cli/internal/test/builders"
"github.com/docker/cli/internal/test/testutil"
"github.com/gotestyourself/gotestyourself/golden"
. "github.com/docker/cli/cli/internal/test/builders"
"github.com/docker/docker/pkg/testutil"
"github.com/docker/docker/pkg/testutil/golden"
"github.com/stretchr/testify/assert"
)
@ -52,10 +53,11 @@ func TestConfigInspectErrors(t *testing.T) {
},
}
for _, tc := range testCases {
buf := new(bytes.Buffer)
cmd := newConfigInspectCommand(
test.NewFakeCli(&fakeClient{
test.NewFakeCliWithOutput(&fakeClient{
configInspectFunc: tc.configInspectFunc,
}),
}, buf),
)
cmd.SetArgs(tc.args)
for key, value := range tc.flags {
@ -93,11 +95,17 @@ func TestConfigInspectWithoutFormat(t *testing.T) {
},
}
for _, tc := range testCases {
cli := test.NewFakeCli(&fakeClient{configInspectFunc: tc.configInspectFunc})
cmd := newConfigInspectCommand(cli)
buf := new(bytes.Buffer)
cmd := newConfigInspectCommand(
test.NewFakeCliWithOutput(&fakeClient{
configInspectFunc: tc.configInspectFunc,
}, buf),
)
cmd.SetArgs(tc.args)
assert.NoError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("config-inspect-without-format.%s.golden", tc.name))
actual := buf.String()
expected := golden.Get(t, []byte(actual), fmt.Sprintf("config-inspect-without-format.%s.golden", tc.name))
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
}
}
@ -127,14 +135,18 @@ func TestConfigInspectWithFormat(t *testing.T) {
},
}
for _, tc := range testCases {
cli := test.NewFakeCli(&fakeClient{
configInspectFunc: tc.configInspectFunc,
})
cmd := newConfigInspectCommand(cli)
buf := new(bytes.Buffer)
cmd := newConfigInspectCommand(
test.NewFakeCliWithOutput(&fakeClient{
configInspectFunc: tc.configInspectFunc,
}, buf),
)
cmd.SetArgs(tc.args)
cmd.Flags().Set("format", tc.format)
assert.NoError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("config-inspect-with-format.%s.golden", tc.name))
actual := buf.String()
expected := golden.Get(t, []byte(actual), fmt.Sprintf("config-inspect-with-format.%s.golden", tc.name))
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
}
}
@ -160,14 +172,16 @@ func TestConfigInspectPretty(t *testing.T) {
},
}
for _, tc := range testCases {
cli := test.NewFakeCli(&fakeClient{
configInspectFunc: tc.configInspectFunc,
})
cmd := newConfigInspectCommand(cli)
buf := new(bytes.Buffer)
cmd := newConfigInspectCommand(
test.NewFakeCliWithOutput(&fakeClient{
configInspectFunc: tc.configInspectFunc,
}, buf))
cmd.SetArgs([]string{"configID"})
cmd.Flags().Set("pretty", "true")
assert.NoError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("config-inspect-pretty.%s.golden", tc.name))
actual := buf.String()
expected := golden.Get(t, []byte(actual), fmt.Sprintf("config-inspect-pretty.%s.golden", tc.name))
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
}
}

View File

@ -6,14 +6,14 @@ import (
"time"
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/cli/internal/test"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/swarm"
"github.com/pkg/errors"
// Import builders to get the builder function as package function
. "github.com/docker/cli/internal/test/builders"
"github.com/docker/cli/internal/test/testutil"
"github.com/gotestyourself/gotestyourself/golden"
. "github.com/docker/cli/cli/internal/test/builders"
"github.com/docker/docker/pkg/testutil"
"github.com/docker/docker/pkg/testutil/golden"
"github.com/stretchr/testify/assert"
)
@ -68,7 +68,9 @@ func TestConfigList(t *testing.T) {
cmd := newConfigListCommand(cli)
cmd.SetOutput(cli.OutBuffer())
assert.NoError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "config-list.golden")
actual := cli.OutBuffer().String()
expected := golden.Get(t, []byte(actual), "config-list.golden")
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
}
func TestConfigListWithQuietOption(t *testing.T) {
@ -85,7 +87,9 @@ func TestConfigListWithQuietOption(t *testing.T) {
cmd := newConfigListCommand(cli)
cmd.Flags().Set("quiet", "true")
assert.NoError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "config-list-with-quiet-option.golden")
actual := cli.OutBuffer().String()
expected := golden.Get(t, []byte(actual), "config-list-with-quiet-option.golden")
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
}
func TestConfigListWithConfigFormat(t *testing.T) {
@ -104,7 +108,9 @@ func TestConfigListWithConfigFormat(t *testing.T) {
})
cmd := newConfigListCommand(cli)
assert.NoError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "config-list-with-config-format.golden")
actual := cli.OutBuffer().String()
expected := golden.Get(t, []byte(actual), "config-list-with-config-format.golden")
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
}
func TestConfigListWithFormat(t *testing.T) {
@ -121,7 +127,9 @@ func TestConfigListWithFormat(t *testing.T) {
cmd := newConfigListCommand(cli)
cmd.Flags().Set("format", "{{ .Name }} {{ .Labels }}")
assert.NoError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "config-list-with-format.golden")
actual := cli.OutBuffer().String()
expected := golden.Get(t, []byte(actual), "config-list-with-format.golden")
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
}
func TestConfigListWithFilter(t *testing.T) {
@ -149,5 +157,7 @@ func TestConfigListWithFilter(t *testing.T) {
cmd.Flags().Set("filter", "name=foo")
cmd.Flags().Set("filter", "label=lbl1=Label-bar")
assert.NoError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "config-list-with-filter.golden")
actual := cli.OutBuffer().String()
expected := golden.Get(t, []byte(actual), "config-list-with-filter.golden")
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
}

View File

@ -1,12 +1,13 @@
package config
import (
"bytes"
"io/ioutil"
"strings"
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/testutil"
"github.com/docker/cli/cli/internal/test"
"github.com/docker/docker/pkg/testutil"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
)
@ -19,7 +20,7 @@ func TestConfigRemoveErrors(t *testing.T) {
}{
{
args: []string{},
expectedError: "requires at least 1 argument.",
expectedError: "requires at least 1 argument(s).",
},
{
args: []string{"foo"},
@ -30,10 +31,11 @@ func TestConfigRemoveErrors(t *testing.T) {
},
}
for _, tc := range testCases {
buf := new(bytes.Buffer)
cmd := newConfigRemoveCommand(
test.NewFakeCli(&fakeClient{
test.NewFakeCliWithOutput(&fakeClient{
configRemoveFunc: tc.configRemoveFunc,
}),
}, buf),
)
cmd.SetArgs(tc.args)
cmd.SetOutput(ioutil.Discard)
@ -43,25 +45,27 @@ func TestConfigRemoveErrors(t *testing.T) {
func TestConfigRemoveWithName(t *testing.T) {
names := []string{"foo", "bar"}
buf := new(bytes.Buffer)
var removedConfigs []string
cli := test.NewFakeCli(&fakeClient{
cli := test.NewFakeCliWithOutput(&fakeClient{
configRemoveFunc: func(name string) error {
removedConfigs = append(removedConfigs, name)
return nil
},
})
}, buf)
cmd := newConfigRemoveCommand(cli)
cmd.SetArgs(names)
assert.NoError(t, cmd.Execute())
assert.Equal(t, names, strings.Split(strings.TrimSpace(cli.OutBuffer().String()), "\n"))
assert.Equal(t, names, strings.Split(strings.TrimSpace(buf.String()), "\n"))
assert.Equal(t, names, removedConfigs)
}
func TestConfigRemoveContinueAfterError(t *testing.T) {
names := []string{"foo", "bar"}
buf := new(bytes.Buffer)
var removedConfigs []string
cli := test.NewFakeCli(&fakeClient{
cli := test.NewFakeCliWithOutput(&fakeClient{
configRemoveFunc: func(name string) error {
removedConfigs = append(removedConfigs, name)
if name == "foo" {
@ -69,7 +73,7 @@ func TestConfigRemoveContinueAfterError(t *testing.T) {
}
return nil
},
})
}, buf)
cmd := newConfigRemoveCommand(cli)
cmd.SetArgs(names)

View File

@ -1,8 +1,8 @@
ID: configID
Name: configName
ID: configID
Name: configName
Labels:
- lbl1=value1
Created at: 0001-01-01 00:00:00 +0000 utc
Updated at: 0001-01-01 00:00:00 +0000 utc
- lbl1=value1
Created at: 0001-01-01 00:00:00+0000 utc
Updated at: 0001-01-01 00:00:00+0000 utc
Data:
payload here

View File

@ -1,7 +1,7 @@
[
{
"ID": "ID-foo",
"Version": {},
"Version": {},
"CreatedAt": "0001-01-01T00:00:00Z",
"UpdatedAt": "0001-01-01T00:00:00Z",
"Spec": {
@ -13,7 +13,7 @@
},
{
"ID": "ID-bar",
"Version": {},
"Version": {},
"CreatedAt": "0001-01-01T00:00:00Z",
"UpdatedAt": "0001-01-01T00:00:00Z",
"Spec": {

View File

@ -1,9 +1,9 @@
[
{
"ID": "ID-foo",
"Version": {},
"CreatedAt": "0001-01-01T00:00:00Z",
"UpdatedAt": "0001-01-01T00:00:00Z",
"Version": {},
"CreatedAt": "0001-01-01T00:00:00Z",
"UpdatedAt": "0001-01-01T00:00:00Z",
"Spec": {
"Name": "foo",
"Labels": null

View File

@ -1,2 +1,2 @@
foo
foo
bar label=label-bar

View File

@ -1,2 +1,2 @@
foo
foo
bar label=label-bar

View File

@ -120,7 +120,18 @@ func runAttach(dockerCli command.Cli, opts *attachOptions) error {
}
if c.Config.Tty && dockerCli.Out().IsTerminal() {
resizeTTY(ctx, dockerCli, opts.container)
height, width := dockerCli.Out().GetTtySize()
// To handle the case where a user repeatedly attaches/detaches without resizing their
// terminal, the only way to get the shell prompt to display for attaches 2+ is to artificially
// resize it, then go back to normal. Without this, every attach after the first will
// require the user to manually resize or hit enter.
resizeTtyTo(ctx, client, opts.container, height+1, width+1, false)
// After the above resizing occurs, the call to MonitorTtySize below will handle resetting back
// to the actual size.
if err := MonitorTtySize(ctx, dockerCli, opts.container, false); err != nil {
logrus.Debugf("Error monitoring TTY size: %s", err)
}
}
streamer := hijackedIOStreamer{
@ -140,36 +151,14 @@ func runAttach(dockerCli command.Cli, opts *attachOptions) error {
if errAttach != nil {
return errAttach
}
return getExitStatus(ctx, dockerCli.Client(), opts.container)
}
func resizeTTY(ctx context.Context, dockerCli command.Cli, containerID string) {
height, width := dockerCli.Out().GetTtySize()
// To handle the case where a user repeatedly attaches/detaches without resizing their
// terminal, the only way to get the shell prompt to display for attaches 2+ is to artificially
// resize it, then go back to normal. Without this, every attach after the first will
// require the user to manually resize or hit enter.
resizeTtyTo(ctx, dockerCli.Client(), containerID, height+1, width+1, false)
// After the above resizing occurs, the call to MonitorTtySize below will handle resetting back
// to the actual size.
if err := MonitorTtySize(ctx, dockerCli, containerID, false); err != nil {
logrus.Debugf("Error monitoring TTY size: %s", err)
}
}
func getExitStatus(ctx context.Context, apiclient client.ContainerAPIClient, containerID string) error {
container, err := apiclient.ContainerInspect(ctx, containerID)
_, status, err := getExitCode(ctx, dockerCli, opts.container)
if err != nil {
// If we can't connect, then the daemon probably died.
if !client.IsErrConnectionFailed(err) {
return err
}
return cli.StatusError{StatusCode: -1}
return err
}
status := container.State.ExitCode
if status != 0 {
return cli.StatusError{StatusCode: status}
}
return nil
}

View File

@ -4,13 +4,10 @@ import (
"io/ioutil"
"testing"
"github.com/docker/cli/cli"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/testutil"
"github.com/docker/cli/cli/internal/test"
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/testutil"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"golang.org/x/net/context"
)
func TestNewAttachCommandErrors(t *testing.T) {
@ -70,48 +67,9 @@ func TestNewAttachCommandErrors(t *testing.T) {
},
}
for _, tc := range testCases {
cmd := NewAttachCommand(test.NewFakeCli(&fakeClient{inspectFunc: tc.containerInspectFunc}))
cmd := NewAttachCommand(test.NewFakeCli(&fakeClient{containerInspectFunc: tc.containerInspectFunc}))
cmd.SetOutput(ioutil.Discard)
cmd.SetArgs(tc.args)
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
}
}
func TestGetExitStatus(t *testing.T) {
containerID := "the exec id"
expecatedErr := errors.New("unexpected error")
testcases := []struct {
inspectError error
exitCode int
expectedError error
}{
{
inspectError: nil,
exitCode: 0,
},
{
inspectError: expecatedErr,
expectedError: expecatedErr,
},
{
exitCode: 15,
expectedError: cli.StatusError{StatusCode: 15},
},
}
for _, testcase := range testcases {
client := &fakeClient{
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
},
}
err := getExitStatus(context.Background(), client, containerID)
assert.Equal(t, testcase.expectedError, err)
}
}

View File

@ -1,73 +1,19 @@
package container
import (
"io"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/client"
"golang.org/x/net/context"
)
type fakeClient struct {
client.Client
inspectFunc func(string) (types.ContainerJSON, error)
execInspectFunc func(execID string) (types.ContainerExecInspect, error)
execCreateFunc func(container string, config types.ExecConfig) (types.IDResponse, error)
createContainerFunc func(config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, containerName string) (container.ContainerCreateCreatedBody, error)
imageCreateFunc func(parentReference string, options types.ImageCreateOptions) (io.ReadCloser, error)
infoFunc func() (types.Info, error)
containerInspectFunc func(string) (types.ContainerJSON, error)
}
func (f *fakeClient) ContainerInspect(_ context.Context, containerID string) (types.ContainerJSON, error) {
if f.inspectFunc != nil {
return f.inspectFunc(containerID)
func (cli *fakeClient) ContainerInspect(_ context.Context, containerID string) (types.ContainerJSON, error) {
if cli.containerInspectFunc != nil {
return cli.containerInspectFunc(containerID)
}
return types.ContainerJSON{}, nil
}
func (f *fakeClient) ContainerExecCreate(_ context.Context, container string, config types.ExecConfig) (types.IDResponse, error) {
if f.execCreateFunc != nil {
return f.execCreateFunc(container, config)
}
return types.IDResponse{}, nil
}
func (f *fakeClient) ContainerExecInspect(_ context.Context, execID string) (types.ContainerExecInspect, error) {
if f.execInspectFunc != nil {
return f.execInspectFunc(execID)
}
return types.ContainerExecInspect{}, nil
}
func (f *fakeClient) ContainerExecStart(ctx context.Context, execID string, config types.ExecStartCheck) error {
return nil
}
func (f *fakeClient) ContainerCreate(
_ context.Context,
config *container.Config,
hostConfig *container.HostConfig,
networkingConfig *network.NetworkingConfig,
containerName string,
) (container.ContainerCreateCreatedBody, error) {
if f.createContainerFunc != nil {
return f.createContainerFunc(config, hostConfig, networkingConfig, containerName)
}
return container.ContainerCreateCreatedBody{}, nil
}
func (f *fakeClient) ImageCreate(ctx context.Context, parentReference string, options types.ImageCreateOptions) (io.ReadCloser, error) {
if f.imageCreateFunc != nil {
return f.imageCreateFunc(parentReference, options)
}
return nil, nil
}
func (f *fakeClient) Info(_ context.Context) (types.Info, error) {
if f.infoFunc != nil {
return f.infoFunc()
}
return types.Info{}, nil
}

View File

@ -113,9 +113,6 @@ type cidFile struct {
}
func (cid *cidFile) Close() error {
if cid.file == nil {
return nil
}
cid.file.Close()
if cid.written {
@ -129,9 +126,6 @@ func (cid *cidFile) Close() error {
}
func (cid *cidFile) Write(id string) error {
if cid.file == nil {
return nil
}
if _, err := cid.file.Write([]byte(id)); err != nil {
return errors.Errorf("Failed to write the container ID to the file: %s", err)
}
@ -140,9 +134,6 @@ func (cid *cidFile) Write(id string) error {
}
func newCIDFile(path string) (*cidFile, error) {
if path == "" {
return &cidFile{}, nil
}
if _, err := os.Stat(path); err == nil {
return nil, errors.Errorf("Container ID file found, make sure the other container isn't running or delete %s", path)
}
@ -162,15 +153,19 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerConfig
stderr := dockerCli.Err()
var (
trustedRef reference.Canonical
namedRef reference.Named
containerIDFile *cidFile
trustedRef reference.Canonical
namedRef reference.Named
)
containerIDFile, err := newCIDFile(hostConfig.ContainerIDFile)
if err != nil {
return nil, err
cidfile := hostConfig.ContainerIDFile
if cidfile != "" {
var err error
if containerIDFile, err = newCIDFile(cidfile); err != nil {
return nil, err
}
defer containerIDFile.Close()
}
defer containerIDFile.Close()
ref, err := reference.ParseAnyReference(config.Image)
if err != nil {
@ -198,7 +193,7 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerConfig
fmt.Fprintf(stderr, "Unable to find image '%s' locally\n", reference.FamiliarString(namedRef))
// we don't want to write to stdout anything apart from container.ID
if err := pullImage(ctx, dockerCli, config.Image, stderr); err != nil {
if err = pullImage(ctx, dockerCli, config.Image, stderr); err != nil {
return nil, err
}
if taggedRef, ok := namedRef.(reference.NamedTagged); ok && trustedRef != nil {
@ -220,6 +215,10 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerConfig
for _, warning := range response.Warnings {
fmt.Fprintf(stderr, "WARNING: %s\n", warning)
}
err = containerIDFile.Write(response.ID)
return &response, err
if containerIDFile != nil {
if err = containerIDFile.Write(response.ID); err != nil {
return nil, err
}
}
return &response, nil
}

View File

@ -1,120 +0,0 @@
package container
import (
"context"
"io"
"io/ioutil"
"os"
"strings"
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/testutil"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network"
"github.com/gotestyourself/gotestyourself/fs"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCIDFileNoOPWithNoFilename(t *testing.T) {
file, err := newCIDFile("")
require.NoError(t, err)
assert.Equal(t, &cidFile{}, file)
assert.NoError(t, file.Write("id"))
assert.NoError(t, file.Close())
}
func TestNewCIDFileWhenFileAlreadyExists(t *testing.T) {
tempfile := fs.NewFile(t, "test-cid-file")
defer tempfile.Remove()
_, err := newCIDFile(tempfile.Path())
testutil.ErrorContains(t, err, "Container ID file found")
}
func TestCIDFileCloseWithNoWrite(t *testing.T) {
tempdir := fs.NewDir(t, "test-cid-file")
defer tempdir.Remove()
path := tempdir.Join("cidfile")
file, err := newCIDFile(path)
require.NoError(t, err)
assert.Equal(t, file.path, path)
assert.NoError(t, file.Close())
_, err = os.Stat(path)
assert.True(t, os.IsNotExist(err))
}
func TestCIDFileCloseWithWrite(t *testing.T) {
tempdir := fs.NewDir(t, "test-cid-file")
defer tempdir.Remove()
path := tempdir.Join("cidfile")
file, err := newCIDFile(path)
require.NoError(t, err)
content := "id"
assert.NoError(t, file.Write(content))
actual, err := ioutil.ReadFile(path)
require.NoError(t, err)
assert.Equal(t, content, string(actual))
assert.NoError(t, file.Close())
_, err = os.Stat(path)
require.NoError(t, err)
}
func TestCreateContainerPullsImageIfMissing(t *testing.T) {
imageName := "does-not-exist-locally"
responseCounter := 0
containerID := "abcdef"
client := &fakeClient{
createContainerFunc: func(
config *container.Config,
hostConfig *container.HostConfig,
networkingConfig *network.NetworkingConfig,
containerName string,
) (container.ContainerCreateCreatedBody, error) {
defer func() { responseCounter++ }()
switch responseCounter {
case 0:
return container.ContainerCreateCreatedBody{}, fakeNotFound{}
case 1:
return container.ContainerCreateCreatedBody{ID: containerID}, nil
default:
return container.ContainerCreateCreatedBody{}, errors.New("unexpected")
}
},
imageCreateFunc: func(parentReference string, options types.ImageCreateOptions) (io.ReadCloser, error) {
return ioutil.NopCloser(strings.NewReader("")), nil
},
infoFunc: func() (types.Info, error) {
return types.Info{IndexServerAddress: "http://indexserver"}, nil
},
}
cli := test.NewFakeCli(client)
config := &containerConfig{
Config: &container.Config{
Image: imageName,
},
HostConfig: &container.HostConfig{},
}
body, err := createContainer(context.Background(), cli, config, "name")
require.NoError(t, err)
expected := container.ContainerCreateCreatedBody{ID: containerID}
assert.Equal(t, expected, *body)
stderr := cli.ErrBuffer().String()
assert.Contains(t, stderr, "Unable to find image 'does-not-exist-locally:latest' locally")
}
type fakeNotFound struct{}
func (f fakeNotFound) NotFound() bool { return true }
func (f fakeNotFound) Error() string { return "error fake not found" }

View File

@ -7,12 +7,10 @@ import (
"github.com/Sirupsen/logrus"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/opts"
"github.com/docker/docker/api/types"
apiclient "github.com/docker/docker/client"
"github.com/docker/docker/pkg/promise"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"golang.org/x/net/context"
)
@ -24,13 +22,14 @@ type execOptions struct {
detach bool
user string
privileged bool
env opts.ListOpts
container string
command []string
env *opts.ListOpts
}
func newExecOptions() execOptions {
return execOptions{env: opts.NewListOpts(opts.ValidateEnv)}
func newExecOptions() *execOptions {
var values []string
return &execOptions{
env: opts.NewListOptsRef(&values, opts.ValidateEnv),
}
}
// NewExecCommand creates a new cobra.Command for `docker exec`
@ -42,9 +41,9 @@ func NewExecCommand(dockerCli command.Cli) *cobra.Command {
Short: "Run a command in a running container",
Args: cli.RequiresMinArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
options.container = args[0]
options.command = args[1:]
return runExec(dockerCli, options)
container := args[0]
execCmd := args[1:]
return runExec(dockerCli, options, container, execCmd)
},
}
@ -57,14 +56,27 @@ func NewExecCommand(dockerCli command.Cli) *cobra.Command {
flags.BoolVarP(&options.detach, "detach", "d", false, "Detached mode: run command in the background")
flags.StringVarP(&options.user, "user", "u", "", "Username or UID (format: <name|uid>[:<group|gid>])")
flags.BoolVarP(&options.privileged, "privileged", "", false, "Give extended privileges to the command")
flags.VarP(&options.env, "env", "e", "Set environment variables")
flags.VarP(options.env, "env", "e", "Set environment variables")
flags.SetAnnotation("env", "version", []string{"1.25"})
return cmd
}
func runExec(dockerCli command.Cli, options execOptions) error {
execConfig := parseExec(options, dockerCli.ConfigFile())
// nolint: gocyclo
func runExec(dockerCli command.Cli, options *execOptions, container string, execCmd []string) error {
execConfig, err := parseExec(options, execCmd)
// just in case the ParseExec does not exit
if container == "" || err != nil {
return cli.StatusError{StatusCode: 1}
}
if options.detachKeys != "" {
dockerCli.ConfigFile().DetachKeys = options.detachKeys
}
// Send client escape keys
execConfig.DetachKeys = dockerCli.ConfigFile().DetachKeys
ctx := context.Background()
client := dockerCli.Client()
@ -72,7 +84,7 @@ func runExec(dockerCli command.Cli, options execOptions) error {
// otherwise if we error out we will leak execIDs on the server (and
// there's no easy way to clean those up). But also in order to make "not
// exist" errors take precedence we do a dummy inspect first.
if _, err := client.ContainerInspect(ctx, options.container); err != nil {
if _, err := client.ContainerInspect(ctx, container); err != nil {
return err
}
if !execConfig.Detach {
@ -81,27 +93,27 @@ func runExec(dockerCli command.Cli, options execOptions) error {
}
}
response, err := client.ContainerExecCreate(ctx, options.container, *execConfig)
response, err := client.ContainerExecCreate(ctx, container, *execConfig)
if err != nil {
return err
}
execID := response.ID
if execID == "" {
return errors.New("exec ID empty")
fmt.Fprintln(dockerCli.Out(), "exec ID empty")
return nil
}
// Temp struct for execStart so that we don't need to transfer all the execConfig.
if execConfig.Detach {
execStartCheck := types.ExecStartCheck{
Detach: execConfig.Detach,
Tty: execConfig.Tty,
}
return client.ContainerExecStart(ctx, execID, execStartCheck)
}
return interactiveExec(ctx, dockerCli, execConfig, execID)
}
func interactiveExec(ctx context.Context, dockerCli command.Cli, execConfig *types.ExecConfig, execID string) error {
// Interactive exec requested.
var (
out, stderr io.Writer
@ -123,7 +135,6 @@ func interactiveExec(ctx context.Context, dockerCli command.Cli, execConfig *typ
}
}
client := dockerCli.Client()
resp, err := client.ContainerExecAttach(ctx, execID, *execConfig)
if err != nil {
return err
@ -154,35 +165,42 @@ func interactiveExec(ctx context.Context, dockerCli command.Cli, execConfig *typ
return err
}
return getExecExitStatus(ctx, client, execID)
var status int
if _, status, err = getExecExitCode(ctx, client, execID); err != nil {
return err
}
if status != 0 {
return cli.StatusError{StatusCode: status}
}
return nil
}
func getExecExitStatus(ctx context.Context, client apiclient.ContainerAPIClient, execID string) error {
// getExecExitCode perform an inspect on the exec command. It returns
// the running state and the exit code.
func getExecExitCode(ctx context.Context, client apiclient.ContainerAPIClient, execID string) (bool, int, error) {
resp, err := client.ContainerExecInspect(ctx, execID)
if err != nil {
// If we can't connect, then the daemon probably died.
if !apiclient.IsErrConnectionFailed(err) {
return err
return false, -1, err
}
return cli.StatusError{StatusCode: -1}
return false, -1, nil
}
status := resp.ExitCode
if status != 0 {
return cli.StatusError{StatusCode: status}
}
return nil
return resp.Running, resp.ExitCode, nil
}
// parseExec parses the specified args for the specified command and generates
// an ExecConfig from it.
func parseExec(opts execOptions, configFile *configfile.ConfigFile) *types.ExecConfig {
func parseExec(opts *execOptions, execCmd []string) (*types.ExecConfig, error) {
execConfig := &types.ExecConfig{
User: opts.user,
Privileged: opts.privileged,
Tty: opts.tty,
Cmd: opts.command,
Cmd: execCmd,
Detach: opts.detach,
Env: opts.env.GetAll(),
}
// If -d is not set, attach to everything by default
@ -194,10 +212,9 @@ func parseExec(opts execOptions, configFile *configfile.ConfigFile) *types.ExecC
}
}
if opts.detachKeys != "" {
execConfig.DetachKeys = opts.detachKeys
} else {
execConfig.DetachKeys = configFile.DetachKeys
if opts.env != nil {
execConfig.Env = opts.env.GetAll()
}
return execConfig
return execConfig, nil
}

View File

@ -4,203 +4,120 @@ import (
"io/ioutil"
"testing"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/testutil"
"github.com/docker/cli/opts"
"github.com/docker/cli/cli/internal/test"
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/testutil"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"golang.org/x/net/context"
"github.com/stretchr/testify/require"
)
func withDefaultOpts(options execOptions) execOptions {
options.env = opts.NewListOpts(opts.ValidateEnv)
if len(options.command) == 0 {
options.command = []string{"command"}
}
return options
type arguments struct {
options execOptions
execCmd []string
}
func TestParseExec(t *testing.T) {
testcases := []struct {
options execOptions
configFile configfile.ConfigFile
expected types.ExecConfig
}{
valids := map[*arguments]*types.ExecConfig{
{
expected: types.ExecConfig{
Cmd: []string{"command"},
AttachStdout: true,
AttachStderr: true,
},
options: withDefaultOpts(execOptions{}),
execCmd: []string{"command"},
}: {
Cmd: []string{"command"},
AttachStdout: true,
AttachStderr: true,
},
{
expected: types.ExecConfig{
Cmd: []string{"command1", "command2"},
AttachStdout: true,
AttachStderr: true,
},
options: withDefaultOpts(execOptions{
command: []string{"command1", "command2"},
}),
execCmd: []string{"command1", "command2"},
}: {
Cmd: []string{"command1", "command2"},
AttachStdout: true,
AttachStderr: true,
},
{
options: withDefaultOpts(execOptions{
options: execOptions{
interactive: true,
tty: true,
user: "uid",
}),
expected: types.ExecConfig{
User: "uid",
AttachStdin: true,
AttachStdout: true,
AttachStderr: true,
Tty: true,
Cmd: []string{"command"},
},
execCmd: []string{"command"},
}: {
User: "uid",
AttachStdin: true,
AttachStdout: true,
AttachStderr: true,
Tty: true,
Cmd: []string{"command"},
},
{
options: withDefaultOpts(execOptions{detach: true}),
expected: types.ExecConfig{
Detach: true,
Cmd: []string{"command"},
options: execOptions{
detach: true,
},
execCmd: []string{"command"},
}: {
AttachStdin: false,
AttachStdout: false,
AttachStderr: false,
Detach: true,
Cmd: []string{"command"},
},
{
options: withDefaultOpts(execOptions{
options: execOptions{
tty: true,
interactive: true,
detach: true,
}),
expected: types.ExecConfig{
Detach: true,
Tty: true,
Cmd: []string{"command"},
},
},
{
options: withDefaultOpts(execOptions{detach: true}),
configFile: configfile.ConfigFile{DetachKeys: "de"},
expected: types.ExecConfig{
Cmd: []string{"command"},
DetachKeys: "de",
Detach: true,
},
},
{
options: withDefaultOpts(execOptions{
detach: true,
detachKeys: "ab",
}),
configFile: configfile.ConfigFile{DetachKeys: "de"},
expected: types.ExecConfig{
Cmd: []string{"command"},
DetachKeys: "ab",
Detach: true,
},
execCmd: []string{"command"},
}: {
AttachStdin: false,
AttachStdout: false,
AttachStderr: false,
Detach: true,
Tty: true,
Cmd: []string{"command"},
},
}
for _, testcase := range testcases {
execConfig := parseExec(testcase.options, &testcase.configFile)
assert.Equal(t, testcase.expected, *execConfig)
}
}
func TestRunExec(t *testing.T) {
var testcases = []struct {
doc string
options execOptions
client fakeClient
expectedError string
expectedOut string
expectedErr string
}{
{
doc: "successful detach",
options: withDefaultOpts(execOptions{
container: "thecontainer",
detach: true,
}),
client: fakeClient{execCreateFunc: execCreateWithID},
},
{
doc: "inspect error",
options: newExecOptions(),
client: fakeClient{
inspectFunc: func(string) (types.ContainerJSON, error) {
return types.ContainerJSON{}, errors.New("failed inspect")
},
},
expectedError: "failed inspect",
},
{
doc: "missing exec ID",
options: newExecOptions(),
expectedError: "exec ID empty",
},
}
for _, testcase := range testcases {
t.Run(testcase.doc, func(t *testing.T) {
cli := test.NewFakeCli(&testcase.client)
err := runExec(cli, testcase.options)
if testcase.expectedError != "" {
testutil.ErrorContains(t, err, testcase.expectedError)
} else {
if !assert.NoError(t, err) {
return
}
}
assert.Equal(t, testcase.expectedOut, cli.OutBuffer().String())
assert.Equal(t, testcase.expectedErr, cli.ErrBuffer().String())
})
}
}
func execCreateWithID(_ string, _ types.ExecConfig) (types.IDResponse, error) {
return types.IDResponse{ID: "execid"}, nil
}
func TestGetExecExitStatus(t *testing.T) {
execID := "the exec id"
expecatedErr := errors.New("unexpected error")
testcases := []struct {
inspectError error
exitCode int
expectedError error
}{
{
inspectError: nil,
exitCode: 0,
},
{
inspectError: expecatedErr,
expectedError: expecatedErr,
},
{
exitCode: 15,
expectedError: cli.StatusError{StatusCode: 15},
},
}
for _, testcase := range testcases {
client := &fakeClient{
execInspectFunc: func(id string) (types.ContainerExecInspect, error) {
assert.Equal(t, execID, id)
return types.ContainerExecInspect{ExitCode: testcase.exitCode}, testcase.inspectError
},
for valid, expectedExecConfig := range valids {
execConfig, err := parseExec(&valid.options, valid.execCmd)
require.NoError(t, err)
if !compareExecConfig(expectedExecConfig, execConfig) {
t.Fatalf("Expected [%v] for %v, got [%v]", expectedExecConfig, valid, execConfig)
}
err := getExecExitStatus(context.Background(), client, execID)
assert.Equal(t, testcase.expectedError, err)
}
}
func compareExecConfig(config1 *types.ExecConfig, config2 *types.ExecConfig) bool {
if config1.AttachStderr != config2.AttachStderr {
return false
}
if config1.AttachStdin != config2.AttachStdin {
return false
}
if config1.AttachStdout != config2.AttachStdout {
return false
}
if config1.Detach != config2.Detach {
return false
}
if config1.Privileged != config2.Privileged {
return false
}
if config1.Tty != config2.Tty {
return false
}
if config1.User != config2.User {
return false
}
if len(config1.Cmd) != len(config2.Cmd) {
return false
}
for index, value := range config1.Cmd {
if value != config2.Cmd[index] {
return false
}
}
return true
}
func TestNewExecCommandErrors(t *testing.T) {
testCases := []struct {
name string
@ -218,7 +135,7 @@ func TestNewExecCommandErrors(t *testing.T) {
},
}
for _, tc := range testCases {
cli := test.NewFakeCli(&fakeClient{inspectFunc: tc.containerInspectFunc})
cli := test.NewFakeCli(&fakeClient{containerInspectFunc: tc.containerInspectFunc})
cmd := NewExecCommand(cli)
cmd.SetOutput(ioutil.Discard)
cmd.SetArgs(tc.args)

View File

@ -7,8 +7,8 @@ import (
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/cli/opts"
"github.com/docker/cli/templates"
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/templates"
"github.com/spf13/cobra"
"golang.org/x/net/context"
)

View File

@ -274,7 +274,7 @@ func addFlags(flags *pflag.FlagSet) *containerOptions {
// Low-level execution (cgroups, namespaces, ...)
flags.StringVar(&copts.cgroupParent, "cgroup-parent", "", "Optional parent cgroup for the container")
flags.StringVar(&copts.ipcMode, "ipc", "", "IPC mode to use")
flags.StringVar(&copts.ipcMode, "ipc", "", "IPC namespace to use")
flags.StringVar(&copts.isolation, "isolation", "", "Container isolation technology")
flags.StringVar(&copts.pidMode, "pid", "", "PID namespace to use")
flags.Var(&copts.shmSize, "shm-size", "Size of /dev/shm")
@ -421,6 +421,11 @@ func parse(flags *pflag.FlagSet, copts *containerOptions) (*containerConfig, err
return nil, err
}
ipcMode := container.IpcMode(copts.ipcMode)
if !ipcMode.Valid() {
return nil, errors.Errorf("--ipc: invalid IPC mode")
}
pidMode := container.PidMode(copts.pidMode)
if !pidMode.Valid() {
return nil, errors.Errorf("--pid: invalid PID mode")
@ -579,7 +584,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions) (*containerConfig, err
ExtraHosts: copts.extraHosts.GetAll(),
VolumesFrom: copts.volumesFrom.GetAll(),
NetworkMode: container.NetworkMode(copts.netMode),
IpcMode: container.IpcMode(copts.ipcMode),
IpcMode: ipcMode,
PidMode: pidMode,
UTSMode: utsMode,
UsernsMode: usernsMode,

View File

@ -1,6 +1,8 @@
package container
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"os"
@ -9,9 +11,10 @@ import (
"testing"
"time"
"github.com/docker/cli/internal/test/testutil"
"github.com/docker/docker/api/types/container"
networktypes "github.com/docker/docker/api/types/network"
"github.com/docker/docker/pkg/testutil"
"github.com/docker/docker/runconfig"
"github.com/docker/go-connections/nat"
"github.com/pkg/errors"
"github.com/spf13/pflag"
@ -112,7 +115,7 @@ func TestParseRunWithInvalidArgs(t *testing.T) {
}
// nolint: gocyclo
func TestParseWithVolumes(t *testing.T) {
func TestParseRunVolumes(t *testing.T) {
// A single volume
arr, tryit := setupPlatformVolume([]string{`/tmp`}, []string{`c:\tmp`})
@ -132,19 +135,19 @@ func TestParseWithVolumes(t *testing.T) {
t.Fatalf("Error parsing volume flags, %s is missing from volumes. Received %v", arr[1], config.Volumes)
}
// A single bind mount
// A single bind-mount
arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp`}, []string{os.Getenv("TEMP") + `:c:\containerTmp`})
if config, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || hostConfig.Binds[0] != arr[0] {
t.Fatalf("Error parsing volume flags, %q should mount-bind the path before the colon into the path after the colon. Received %v %v", arr[0], hostConfig.Binds, config.Volumes)
}
// Two bind mounts.
// Two bind-mounts.
arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp`, `/hostVar:/containerVar`}, []string{os.Getenv("ProgramData") + `:c:\ContainerPD`, os.Getenv("TEMP") + `:c:\containerTmp`})
if _, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], arr[0], arr[1]) != nil {
t.Fatalf("Error parsing volume flags, `%s and %s` did not mount-bind correctly. Received %v", arr[0], arr[1], hostConfig.Binds)
}
// Two bind mounts, first read-only, second read-write.
// Two bind-mounts, first read-only, second read-write.
// TODO Windows: The Windows version uses read-write as that's the only mode it supports. Can change this post TP4
arr, tryit = setupPlatformVolume(
[]string{`/hostTmp:/containerTmp:ro`, `/hostVar:/containerVar:rw`},
@ -363,12 +366,23 @@ func TestParseDevice(t *testing.T) {
}
func TestParseModes(t *testing.T) {
// ipc ko
_, _, _, err := parseRun([]string{"--ipc=container:", "img", "cmd"})
testutil.ErrorContains(t, err, "--ipc: invalid IPC mode")
// ipc ok
_, hostconfig, _, err := parseRun([]string{"--ipc=host", "img", "cmd"})
require.NoError(t, err)
if !hostconfig.IpcMode.Valid() {
t.Fatalf("Expected a valid IpcMode, got %v", hostconfig.IpcMode)
}
// pid ko
_, _, _, err := parseRun([]string{"--pid=container:", "img", "cmd"})
_, _, _, err = parseRun([]string{"--pid=container:", "img", "cmd"})
testutil.ErrorContains(t, err, "--pid: invalid PID mode")
// pid ok
_, hostconfig, _, err := parseRun([]string{"--pid=host", "img", "cmd"})
_, hostconfig, _, err = parseRun([]string{"--pid=host", "img", "cmd"})
require.NoError(t, err)
if !hostconfig.PidMode.Valid() {
t.Fatalf("Expected a valid PidMode, got %v", hostconfig.PidMode)
@ -581,6 +595,161 @@ func TestParseEntryPoint(t *testing.T) {
}
}
// This tests the cases for binds which are generated through
// DecodeContainerConfig rather than Parse()
// nolint: gocyclo
func TestDecodeContainerConfigVolumes(t *testing.T) {
// Root to root
bindsOrVols, _ := setupPlatformVolume([]string{`/:/`}, []string{os.Getenv("SystemDrive") + `\:c:\`})
if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil {
t.Fatalf("binds %v should have failed", bindsOrVols)
}
if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil {
t.Fatalf("volume %v should have failed", bindsOrVols)
}
// No destination path
bindsOrVols, _ = setupPlatformVolume([]string{`/tmp:`}, []string{os.Getenv("TEMP") + `\:`})
if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil {
t.Fatalf("binds %v should have failed", bindsOrVols)
}
if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil {
t.Fatalf("volume %v should have failed", bindsOrVols)
}
// // No destination path or mode
bindsOrVols, _ = setupPlatformVolume([]string{`/tmp::`}, []string{os.Getenv("TEMP") + `\::`})
if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil {
t.Fatalf("binds %v should have failed", bindsOrVols)
}
if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil {
t.Fatalf("volume %v should have failed", bindsOrVols)
}
// A whole lot of nothing
bindsOrVols = []string{`:`}
if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil {
t.Fatalf("binds %v should have failed", bindsOrVols)
}
if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil {
t.Fatalf("volume %v should have failed", bindsOrVols)
}
// A whole lot of nothing with no mode
bindsOrVols = []string{`::`}
if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil {
t.Fatalf("binds %v should have failed", bindsOrVols)
}
if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil {
t.Fatalf("volume %v should have failed", bindsOrVols)
}
// Too much including an invalid mode
wTmp := os.Getenv("TEMP")
bindsOrVols, _ = setupPlatformVolume([]string{`/tmp:/tmp:/tmp:/tmp`}, []string{wTmp + ":" + wTmp + ":" + wTmp + ":" + wTmp})
if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil {
t.Fatalf("binds %v should have failed", bindsOrVols)
}
if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil {
t.Fatalf("volume %v should have failed", bindsOrVols)
}
// Windows specific error tests
if runtime.GOOS == "windows" {
// Volume which does not include a drive letter
bindsOrVols = []string{`\tmp`}
if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil {
t.Fatalf("binds %v should have failed", bindsOrVols)
}
if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil {
t.Fatalf("volume %v should have failed", bindsOrVols)
}
// Root to C-Drive
bindsOrVols = []string{os.Getenv("SystemDrive") + `\:c:`}
if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil {
t.Fatalf("binds %v should have failed", bindsOrVols)
}
if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil {
t.Fatalf("volume %v should have failed", bindsOrVols)
}
// Container path that does not include a drive letter
bindsOrVols = []string{`c:\windows:\somewhere`}
if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil {
t.Fatalf("binds %v should have failed", bindsOrVols)
}
if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil {
t.Fatalf("volume %v should have failed", bindsOrVols)
}
}
// Linux-specific error tests
if runtime.GOOS != "windows" {
// Just root
bindsOrVols = []string{`/`}
if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil {
t.Fatalf("binds %v should have failed", bindsOrVols)
}
if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil {
t.Fatalf("volume %v should have failed", bindsOrVols)
}
// A single volume that looks like a bind mount passed in Volumes.
// This should be handled as a bind mount, not a volume.
vols := []string{`/foo:/bar`}
if config, hostConfig, err := callDecodeContainerConfig(vols, nil); err != nil {
t.Fatal("Volume /foo:/bar should have succeeded as a volume name")
} else if hostConfig.Binds != nil {
t.Fatalf("Error parsing volume flags, /foo:/bar should not mount-bind anything. Received %v", hostConfig.Binds)
} else if _, exists := config.Volumes[vols[0]]; !exists {
t.Fatalf("Error parsing volume flags, /foo:/bar is missing from volumes. Received %v", config.Volumes)
}
}
}
// callDecodeContainerConfig is a utility function used by TestDecodeContainerConfigVolumes
// to call DecodeContainerConfig. It effectively does what a client would
// do when calling the daemon by constructing a JSON stream of a
// ContainerConfigWrapper which is populated by the set of volume specs
// passed into it. It returns a config and a hostconfig which can be
// validated to ensure DecodeContainerConfig has manipulated the structures
// correctly.
func callDecodeContainerConfig(volumes []string, binds []string) (*container.Config, *container.HostConfig, error) {
var (
b []byte
err error
c *container.Config
h *container.HostConfig
)
w := runconfig.ContainerConfigWrapper{
Config: &container.Config{
Volumes: map[string]struct{}{},
},
HostConfig: &container.HostConfig{
NetworkMode: "none",
Binds: binds,
},
}
for _, v := range volumes {
w.Config.Volumes[v] = struct{}{}
}
if b, err = json.Marshal(w); err != nil {
return nil, nil, errors.Errorf("Error on marshal %s", err.Error())
}
c, h, _, err = runconfig.DecodeContainerConfig(bytes.NewReader(b))
if err != nil {
return nil, nil, errors.Errorf("Error parsing %s: %v", string(b), err)
}
if c == nil || h == nil {
return nil, nil, errors.Errorf("Empty config or hostconfig")
}
return c, h, err
}
func TestValidateDevice(t *testing.T) {
valid := []string{
"/home",

View File

@ -10,6 +10,7 @@ import (
"github.com/docker/docker/api/types/events"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/versions"
clientapi "github.com/docker/docker/client"
"golang.org/x/net/context"
)
@ -124,6 +125,20 @@ func legacyWaitExitOrRemoved(ctx context.Context, dockerCli *command.DockerCli,
return statusChan
}
// getExitCode performs an inspect on the container. It returns
// the running state and the exit code.
func getExitCode(ctx context.Context, dockerCli command.Cli, containerID string) (bool, int, error) {
c, err := dockerCli.Client().ContainerInspect(ctx, containerID)
if err != nil {
// If we can't connect, then the daemon probably died.
if !clientapi.IsErrConnectionFailed(err) {
return false, -1, err
}
return false, -1, nil
}
return c.State.Running, c.State.ExitCode, nil
}
func parallelOperation(ctx context.Context, containers []string, op func(ctx context.Context, container string) error) chan error {
if len(containers) == 0 {
return nil

View File

@ -1,10 +1,9 @@
package formatter
import (
"reflect"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func compareMultipleValues(t *testing.T, value, expected string) {
@ -23,5 +22,7 @@ func compareMultipleValues(t *testing.T, value, expected string) {
keyval := strings.Split(expected, "=")
expMap[keyval[0]] = keyval[1]
}
assert.Equal(t, expMap, entriesMap)
if !reflect.DeepEqual(expMap, entriesMap) {
t.Fatalf("Expected entries: %v, got: %v", expected, value)
}
}

View File

@ -7,7 +7,7 @@ import (
"text/tabwriter"
"text/template"
"github.com/docker/cli/templates"
"github.com/docker/docker/pkg/templates"
"github.com/pkg/errors"
)

View File

@ -79,13 +79,10 @@ func (c *historyContext) ID() string {
}
func (c *historyContext) CreatedAt() string {
return time.Unix(c.h.Created, 0).Format(time.RFC3339)
return units.HumanDuration(time.Now().UTC().Sub(time.Unix(c.h.Created, 0)))
}
func (c *historyContext) CreatedSince() string {
if !c.human {
return c.CreatedAt()
}
created := units.HumanDuration(time.Now().UTC().Sub(time.Unix(c.h.Created, 0)))
return created + " ago"
}

View File

@ -51,21 +51,17 @@ func TestHistoryContext_ID(t *testing.T) {
}
func TestHistoryContext_CreatedSince(t *testing.T) {
unixTime := time.Now().AddDate(0, 0, -7).Unix()
expected := "7 days ago"
var ctx historyContext
cases := []historyCase{
{
historyContext{
h: image.HistoryResponseItem{Created: time.Now().AddDate(0, 0, -7).Unix()},
h: image.HistoryResponseItem{Created: unixTime},
trunc: false,
human: true,
}, "7 days ago", ctx.CreatedSince,
},
{
historyContext{
h: image.HistoryResponseItem{Created: time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC).Unix()},
trunc: false,
human: false,
}, "2009-11-10T23:00:00Z", ctx.CreatedSince,
}, expected, ctx.CreatedSince,
},
}

View File

@ -79,16 +79,11 @@ func ImageWrite(ctx ImageContext, images []types.ImageSummary) error {
return ctx.Write(newImageContext(), render)
}
// needDigest determines whether the image digest should be ignored or not when writing image context
func needDigest(ctx ImageContext) bool {
return ctx.Digest || ctx.Format.Contains("{{.Digest}}")
}
func imageFormat(ctx ImageContext, images []types.ImageSummary, format func(subContext subContext) error) error {
for _, image := range images {
formatted := []*imageContext{}
images := []*imageContext{}
if isDangling(image) {
formatted = append(formatted, &imageContext{
images = append(images, &imageContext{
trunc: ctx.Trunc,
i: image,
repo: "<none>",
@ -96,9 +91,90 @@ func imageFormat(ctx ImageContext, images []types.ImageSummary, format func(subC
digest: "<none>",
})
} else {
formatted = imageFormatTaggedAndDigest(ctx, image)
repoTags := map[string][]string{}
repoDigests := map[string][]string{}
for _, refString := range image.RepoTags {
ref, err := reference.ParseNormalizedNamed(refString)
if err != nil {
continue
}
if nt, ok := ref.(reference.NamedTagged); ok {
familiarRef := reference.FamiliarName(ref)
repoTags[familiarRef] = append(repoTags[familiarRef], nt.Tag())
}
}
for _, refString := range image.RepoDigests {
ref, err := reference.ParseNormalizedNamed(refString)
if err != nil {
continue
}
if c, ok := ref.(reference.Canonical); ok {
familiarRef := reference.FamiliarName(ref)
repoDigests[familiarRef] = append(repoDigests[familiarRef], c.Digest().String())
}
}
for repo, tags := range repoTags {
digests := repoDigests[repo]
// Do not display digests as their own row
delete(repoDigests, repo)
if !ctx.Digest {
// Ignore digest references, just show tag once
digests = nil
}
for _, tag := range tags {
if len(digests) == 0 {
images = append(images, &imageContext{
trunc: ctx.Trunc,
i: image,
repo: repo,
tag: tag,
digest: "<none>",
})
continue
}
// Display the digests for each tag
for _, dgst := range digests {
images = append(images, &imageContext{
trunc: ctx.Trunc,
i: image,
repo: repo,
tag: tag,
digest: dgst,
})
}
}
}
// Show rows for remaining digest only references
for repo, digests := range repoDigests {
// If digests are displayed, show row per digest
if ctx.Digest {
for _, dgst := range digests {
images = append(images, &imageContext{
trunc: ctx.Trunc,
i: image,
repo: repo,
tag: "<none>",
digest: dgst,
})
}
} else {
images = append(images, &imageContext{
trunc: ctx.Trunc,
i: image,
repo: repo,
tag: "<none>",
})
}
}
}
for _, imageCtx := range formatted {
for _, imageCtx := range images {
if err := format(imageCtx); err != nil {
return err
}
@ -107,82 +183,6 @@ func imageFormat(ctx ImageContext, images []types.ImageSummary, format func(subC
return nil
}
func imageFormatTaggedAndDigest(ctx ImageContext, image types.ImageSummary) []*imageContext {
repoTags := map[string][]string{}
repoDigests := map[string][]string{}
images := []*imageContext{}
for _, refString := range image.RepoTags {
ref, err := reference.ParseNormalizedNamed(refString)
if err != nil {
continue
}
if nt, ok := ref.(reference.NamedTagged); ok {
familiarRef := reference.FamiliarName(ref)
repoTags[familiarRef] = append(repoTags[familiarRef], nt.Tag())
}
}
for _, refString := range image.RepoDigests {
ref, err := reference.ParseNormalizedNamed(refString)
if err != nil {
continue
}
if c, ok := ref.(reference.Canonical); ok {
familiarRef := reference.FamiliarName(ref)
repoDigests[familiarRef] = append(repoDigests[familiarRef], c.Digest().String())
}
}
addImage := func(repo, tag, digest string) {
image := &imageContext{
trunc: ctx.Trunc,
i: image,
repo: repo,
tag: tag,
digest: digest,
}
images = append(images, image)
}
for repo, tags := range repoTags {
digests := repoDigests[repo]
// Do not display digests as their own row
delete(repoDigests, repo)
if !needDigest(ctx) {
// Ignore digest references, just show tag once
digests = nil
}
for _, tag := range tags {
if len(digests) == 0 {
addImage(repo, tag, "<none>")
continue
}
// Display the digests for each tag
for _, dgst := range digests {
addImage(repo, tag, dgst)
}
}
}
// Show rows for remaining digest only references
for repo, digests := range repoDigests {
// If digests are displayed, show row per digest
if ctx.Digest {
for _, dgst := range digests {
addImage(repo, "<none>", dgst)
}
} else {
addImage(repo, "<none>", "")
}
}
return images
}
type imageContext struct {
HeaderContext
trunc bool

View File

@ -55,26 +55,6 @@ func TestImageContext(t *testing.T) {
i: types.ImageSummary{},
digest: "sha256:d149ab53f8718e987c3a3024bb8aa0e2caadf6c0328f1d9d850b2a2a67f2819a",
}, "sha256:d149ab53f8718e987c3a3024bb8aa0e2caadf6c0328f1d9d850b2a2a67f2819a", ctx.Digest},
{
imageContext{
i: types.ImageSummary{Containers: 10},
}, "10", ctx.Containers,
},
{
imageContext{
i: types.ImageSummary{VirtualSize: 10000},
}, "10kB", ctx.VirtualSize,
},
{
imageContext{
i: types.ImageSummary{SharedSize: 10000},
}, "10kB", ctx.SharedSize,
},
{
imageContext{
i: types.ImageSummary{SharedSize: 5000, VirtualSize: 20000},
}, "15kB", ctx.UniqueSize,
},
}
for _, c := range cases {
@ -82,8 +62,8 @@ func TestImageContext(t *testing.T) {
v := c.call()
if strings.Contains(v, ",") {
compareMultipleValues(t, v, c.expValue)
} else {
assert.Equal(t, c.expValue, v)
} else if v != c.expValue {
t.Fatalf("Expected %s, was %s\n", c.expValue, v)
}
}
}
@ -157,14 +137,6 @@ image <none>
},
"REPOSITORY\nimage\nimage\n<none>\n",
},
{
ImageContext{
Context: Context{
Format: NewImageFormat("table {{.Digest}}", true, false),
},
},
"DIGEST\nsha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf\n<none>\n<none>\n",
},
{
ImageContext{
Context: Context{

View File

@ -1,104 +0,0 @@
package formatter
import (
"strconv"
"strings"
registry "github.com/docker/docker/api/types/registry"
"github.com/docker/docker/pkg/stringutils"
)
const (
defaultSearchTableFormat = "table {{.Name}}\t{{.Description}}\t{{.StarCount}}\t{{.IsOfficial}}\t{{.IsAutomated}}"
starsHeader = "STARS"
officialHeader = "OFFICIAL"
automatedHeader = "AUTOMATED"
)
// NewSearchFormat returns a Format for rendering using a network Context
func NewSearchFormat(source string) Format {
switch source {
case "":
return defaultSearchTableFormat
case TableFormatKey:
return defaultSearchTableFormat
}
return Format(source)
}
// SearchWrite writes the context
func SearchWrite(ctx Context, results []registry.SearchResult, auto bool, stars int) error {
render := func(format func(subContext subContext) error) error {
for _, result := range results {
// --automated and -s, --stars are deprecated since Docker 1.12
if (auto && !result.IsAutomated) || (stars > result.StarCount) {
continue
}
searchCtx := &searchContext{trunc: ctx.Trunc, s: result}
if err := format(searchCtx); err != nil {
return err
}
}
return nil
}
searchCtx := searchContext{}
searchCtx.header = map[string]string{
"Name": nameHeader,
"Description": descriptionHeader,
"StarCount": starsHeader,
"IsOfficial": officialHeader,
"IsAutomated": automatedHeader,
}
return ctx.Write(&searchCtx, render)
}
type searchContext struct {
HeaderContext
trunc bool
json bool
s registry.SearchResult
}
func (c *searchContext) MarshalJSON() ([]byte, error) {
c.json = true
return marshalJSON(c)
}
func (c *searchContext) Name() string {
return c.s.Name
}
func (c *searchContext) Description() string {
desc := strings.Replace(c.s.Description, "\n", " ", -1)
desc = strings.Replace(desc, "\r", " ", -1)
if c.trunc {
desc = stringutils.Ellipsis(desc, 45)
}
return desc
}
func (c *searchContext) StarCount() string {
return strconv.Itoa(c.s.StarCount)
}
func (c *searchContext) formatBool(value bool) string {
switch {
case value && c.json:
return "true"
case value:
return "[OK]"
case c.json:
return "false"
default:
return ""
}
}
func (c *searchContext) IsOfficial() string {
return c.formatBool(c.s.IsOfficial)
}
func (c *searchContext) IsAutomated() string {
return c.formatBool(c.s.IsAutomated)
}

View File

@ -1,284 +0,0 @@
package formatter
import (
"bytes"
"encoding/json"
"strings"
"testing"
registrytypes "github.com/docker/docker/api/types/registry"
"github.com/docker/docker/pkg/stringutils"
"github.com/stretchr/testify/assert"
)
func TestSearchContext(t *testing.T) {
name := "nginx"
starCount := 5000
var ctx searchContext
cases := []struct {
searchCtx searchContext
expValue string
call func() string
}{
{searchContext{
s: registrytypes.SearchResult{Name: name},
}, name, ctx.Name},
{searchContext{
s: registrytypes.SearchResult{StarCount: starCount},
}, "5000", ctx.StarCount},
{searchContext{
s: registrytypes.SearchResult{IsOfficial: true},
}, "[OK]", ctx.IsOfficial},
{searchContext{
s: registrytypes.SearchResult{IsOfficial: false},
}, "", ctx.IsOfficial},
{searchContext{
s: registrytypes.SearchResult{IsAutomated: true},
}, "[OK]", ctx.IsAutomated},
{searchContext{
s: registrytypes.SearchResult{IsAutomated: false},
}, "", ctx.IsAutomated},
}
for _, c := range cases {
ctx = c.searchCtx
v := c.call()
if strings.Contains(v, ",") {
compareMultipleValues(t, v, c.expValue)
} else if v != c.expValue {
t.Fatalf("Expected %s, was %s\n", c.expValue, v)
}
}
}
func TestSearchContextDescription(t *testing.T) {
shortDescription := "Official build of Nginx."
longDescription := "Automated Nginx reverse proxy for docker containers"
descriptionWReturns := "Automated\nNginx reverse\rproxy\rfor docker\ncontainers"
var ctx searchContext
cases := []struct {
searchCtx searchContext
expValue string
call func() string
}{
{searchContext{
s: registrytypes.SearchResult{Description: shortDescription},
trunc: true,
}, shortDescription, ctx.Description},
{searchContext{
s: registrytypes.SearchResult{Description: shortDescription},
trunc: false,
}, shortDescription, ctx.Description},
{searchContext{
s: registrytypes.SearchResult{Description: longDescription},
trunc: false,
}, longDescription, ctx.Description},
{searchContext{
s: registrytypes.SearchResult{Description: longDescription},
trunc: true,
}, stringutils.Ellipsis(longDescription, 45), ctx.Description},
{searchContext{
s: registrytypes.SearchResult{Description: descriptionWReturns},
trunc: false,
}, longDescription, ctx.Description},
{searchContext{
s: registrytypes.SearchResult{Description: descriptionWReturns},
trunc: true,
}, stringutils.Ellipsis(longDescription, 45), ctx.Description},
}
for _, c := range cases {
ctx = c.searchCtx
v := c.call()
if strings.Contains(v, ",") {
compareMultipleValues(t, v, c.expValue)
} else if v != c.expValue {
t.Fatalf("Expected %s, was %s\n", c.expValue, v)
}
}
}
func TestSearchContextWrite(t *testing.T) {
cases := []struct {
context Context
expected string
}{
// Errors
{
Context{Format: "{{InvalidFunction}}"},
`Template parsing error: template: :1: function "InvalidFunction" not defined
`,
},
{
Context{Format: "{{nil}}"},
`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command
`,
},
// Table format
{
Context{Format: NewSearchFormat("table")},
`NAME DESCRIPTION STARS OFFICIAL AUTOMATED
result1 Official build 5000 [OK]
result2 Not official 5 [OK]
`,
},
{
Context{Format: NewSearchFormat("table {{.Name}}")},
`NAME
result1
result2
`,
},
// Custom Format
{
Context{Format: NewSearchFormat("{{.Name}}")},
`result1
result2
`,
},
// Custom Format with CreatedAt
{
Context{Format: NewSearchFormat("{{.Name}} {{.StarCount}}")},
`result1 5000
result2 5
`,
},
}
for _, testcase := range cases {
results := []registrytypes.SearchResult{
{Name: "result1", Description: "Official build", StarCount: 5000, IsOfficial: true, IsAutomated: false},
{Name: "result2", Description: "Not official", StarCount: 5, IsOfficial: false, IsAutomated: true},
}
out := bytes.NewBufferString("")
testcase.context.Output = out
err := SearchWrite(testcase.context, results, false, 0)
if err != nil {
assert.Error(t, err, testcase.expected)
} else {
assert.Equal(t, out.String(), testcase.expected)
}
}
}
func TestSearchContextWriteAutomated(t *testing.T) {
cases := []struct {
context Context
expected string
}{
// Table format
{
Context{Format: NewSearchFormat("table")},
`NAME DESCRIPTION STARS OFFICIAL AUTOMATED
result2 Not official 5 [OK]
`,
},
{
Context{Format: NewSearchFormat("table {{.Name}}")},
`NAME
result2
`,
},
}
for _, testcase := range cases {
results := []registrytypes.SearchResult{
{Name: "result1", Description: "Official build", StarCount: 5000, IsOfficial: true, IsAutomated: false},
{Name: "result2", Description: "Not official", StarCount: 5, IsOfficial: false, IsAutomated: true},
}
out := bytes.NewBufferString("")
testcase.context.Output = out
err := SearchWrite(testcase.context, results, true, 0)
if err != nil {
assert.Error(t, err, testcase.expected)
} else {
assert.Equal(t, out.String(), testcase.expected)
}
}
}
func TestSearchContextWriteStars(t *testing.T) {
cases := []struct {
context Context
expected string
}{
// Table format
{
Context{Format: NewSearchFormat("table")},
`NAME DESCRIPTION STARS OFFICIAL AUTOMATED
result1 Official build 5000 [OK]
`,
},
{
Context{Format: NewSearchFormat("table {{.Name}}")},
`NAME
result1
`,
},
}
for _, testcase := range cases {
results := []registrytypes.SearchResult{
{Name: "result1", Description: "Official build", StarCount: 5000, IsOfficial: true, IsAutomated: false},
{Name: "result2", Description: "Not official", StarCount: 5, IsOfficial: false, IsAutomated: true},
}
out := bytes.NewBufferString("")
testcase.context.Output = out
err := SearchWrite(testcase.context, results, false, 6)
if err != nil {
assert.Error(t, err, testcase.expected)
} else {
assert.Equal(t, out.String(), testcase.expected)
}
}
}
func TestSearchContextWriteJSON(t *testing.T) {
results := []registrytypes.SearchResult{
{Name: "result1", Description: "Official build", StarCount: 5000, IsOfficial: true, IsAutomated: false},
{Name: "result2", Description: "Not official", StarCount: 5, IsOfficial: false, IsAutomated: true},
}
expectedJSONs := []map[string]interface{}{
{"Name": "result1", "Description": "Official build", "StarCount": "5000", "IsOfficial": "true", "IsAutomated": "false"},
{"Name": "result2", "Description": "Not official", "StarCount": "5", "IsOfficial": "false", "IsAutomated": "true"},
}
out := bytes.NewBufferString("")
err := SearchWrite(Context{Format: "{{json .}}", Output: out}, results, false, 0)
if err != nil {
t.Fatal(err)
}
for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
t.Logf("Output: line %d: %s", i, line)
var m map[string]interface{}
if err := json.Unmarshal([]byte(line), &m); err != nil {
t.Fatal(err)
}
assert.Equal(t, m, expectedJSONs[i])
}
}
func TestSearchContextWriteJSONField(t *testing.T) {
results := []registrytypes.SearchResult{
{Name: "result1", Description: "Official build", StarCount: 5000, IsOfficial: true, IsAutomated: false},
{Name: "result2", Description: "Not official", StarCount: 5, IsOfficial: false, IsAutomated: true},
}
out := bytes.NewBufferString("")
err := SearchWrite(Context{Format: "{{json .Name}}", Output: out}, results, false, 0)
if err != nil {
t.Fatal(err)
}
for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
t.Logf("Output: line %d: %s", i, line)
var s string
if err := json.Unmarshal([]byte(line), &s); err != nil {
t.Fatal(err)
}
assert.Equal(t, s, results[i].Name)
}
}

View File

@ -518,11 +518,11 @@ func (c *serviceContext) Image() string {
}
func (c *serviceContext) Ports() string {
if c.service.Endpoint.Ports == nil {
if c.service.Spec.EndpointSpec == nil || c.service.Spec.EndpointSpec.Ports == nil {
return ""
}
ports := []string{}
for _, pConfig := range c.service.Endpoint.Ports {
for _, pConfig := range c.service.Spec.EndpointSpec.Ports {
if pConfig.PublishMode == swarm.PortConfigPublishModeIngress {
ports = append(ports, fmt.Sprintf("*:%d->%d/%s",
pConfig.PublishedPort,

View File

@ -96,14 +96,14 @@ bar
ID: "id_baz",
Spec: swarm.ServiceSpec{
Annotations: swarm.Annotations{Name: "baz"},
},
Endpoint: swarm.Endpoint{
Ports: []swarm.PortConfig{
{
PublishMode: "ingress",
PublishedPort: 80,
TargetPort: 8080,
Protocol: "tcp",
EndpointSpec: &swarm.EndpointSpec{
Ports: []swarm.PortConfig{
{
PublishMode: "ingress",
PublishedPort: 80,
TargetPort: 8080,
Protocol: "tcp",
},
},
},
},
@ -112,14 +112,14 @@ bar
ID: "id_bar",
Spec: swarm.ServiceSpec{
Annotations: swarm.Annotations{Name: "bar"},
},
Endpoint: swarm.Endpoint{
Ports: []swarm.PortConfig{
{
PublishMode: "ingress",
PublishedPort: 80,
TargetPort: 8080,
Protocol: "tcp",
EndpointSpec: &swarm.EndpointSpec{
Ports: []swarm.PortConfig{
{
PublishMode: "ingress",
PublishedPort: 80,
TargetPort: 8080,
Protocol: "tcp",
},
},
},
},
@ -152,14 +152,14 @@ func TestServiceContextWriteJSON(t *testing.T) {
ID: "id_baz",
Spec: swarm.ServiceSpec{
Annotations: swarm.Annotations{Name: "baz"},
},
Endpoint: swarm.Endpoint{
Ports: []swarm.PortConfig{
{
PublishMode: "ingress",
PublishedPort: 80,
TargetPort: 8080,
Protocol: "tcp",
EndpointSpec: &swarm.EndpointSpec{
Ports: []swarm.PortConfig{
{
PublishMode: "ingress",
PublishedPort: 80,
TargetPort: 8080,
Protocol: "tcp",
},
},
},
},
@ -168,14 +168,14 @@ func TestServiceContextWriteJSON(t *testing.T) {
ID: "id_bar",
Spec: swarm.ServiceSpec{
Annotations: swarm.Annotations{Name: "bar"},
},
Endpoint: swarm.Endpoint{
Ports: []swarm.PortConfig{
{
PublishMode: "ingress",
PublishedPort: 80,
TargetPort: 8080,
Protocol: "tcp",
EndpointSpec: &swarm.EndpointSpec{
Ports: []swarm.PortConfig{
{
PublishMode: "ingress",
PublishedPort: 80,
TargetPort: 8080,
Protocol: "tcp",
},
},
},
},

View File

@ -5,7 +5,7 @@ import (
"github.com/docker/docker/api/types/swarm"
// Import builders to get the builder function as package function
. "github.com/docker/cli/internal/test/builders"
. "github.com/docker/cli/cli/internal/test/builders"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"golang.org/x/net/context"

View File

@ -11,8 +11,8 @@ import (
"strings"
"testing"
"github.com/docker/cli/internal/test/testutil"
"github.com/docker/docker/pkg/archive"
"github.com/docker/docker/pkg/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

View File

@ -10,7 +10,7 @@ import (
"testing"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/cli/internal/test"
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/archive"
"github.com/stretchr/testify/assert"
@ -68,37 +68,3 @@ func TestRunBuildDockerfileFromStdinWithCompress(t *testing.T) {
sort.Strings(actual)
assert.Equal(t, []string{dockerfileName, ".dockerignore", "foo"}, actual)
}
// TestRunBuildFromLocalGitHubDirNonExistingRepo tests that build contexts
// starting with `github.com/` are special-cased, and the build command attempts
// to clone the remote repo.
func TestRunBuildFromGitHubSpecialCase(t *testing.T) {
cmd := NewBuildCommand(test.NewFakeCli(nil))
cmd.SetArgs([]string{"github.com/docker/no-such-repository"})
cmd.SetOutput(ioutil.Discard)
err := cmd.Execute()
assert.Error(t, err)
assert.Contains(t, err.Error(), "unable to prepare context: unable to 'git clone'")
}
// TestRunBuildFromLocalGitHubDirNonExistingRepo tests that a local directory
// starting with `github.com` takes precedence over the `github.com` special
// case.
func TestRunBuildFromLocalGitHubDir(t *testing.T) {
tmpDir, err := ioutil.TempDir("", "docker-build-from-local-dir-")
require.NoError(t, err)
defer os.RemoveAll(tmpDir)
buildDir := filepath.Join(tmpDir, "github.com", "docker", "no-such-repository")
err = os.MkdirAll(buildDir, 0777)
require.NoError(t, err)
err = ioutil.WriteFile(filepath.Join(buildDir, "Dockerfile"), []byte("FROM busybox\n"), 0644)
require.NoError(t, err)
client := test.NewFakeCli(&fakeClient{})
cmd := NewBuildCommand(client)
cmd.SetArgs([]string{buildDir})
cmd.SetOutput(ioutil.Discard)
err = cmd.Execute()
require.NoError(t, err)
}

View File

@ -3,13 +3,14 @@ package image
import (
"fmt"
"io/ioutil"
"regexp"
"testing"
"time"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/testutil"
"github.com/docker/cli/cli/internal/test"
"github.com/docker/docker/api/types/image"
"github.com/gotestyourself/gotestyourself/golden"
"github.com/docker/docker/pkg/testutil"
"github.com/docker/docker/pkg/testutil/golden"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
)
@ -24,7 +25,7 @@ func TestNewHistoryCommandErrors(t *testing.T) {
{
name: "wrong-args",
args: []string{},
expectedError: "requires exactly 1 argument.",
expectedError: "requires exactly 1 argument(s).",
},
{
name: "client-error",
@ -95,9 +96,11 @@ func TestNewHistoryCommandSuccess(t *testing.T) {
assert.NoError(t, err)
actual := cli.OutBuffer().String()
if tc.outputRegex == "" {
golden.Assert(t, actual, fmt.Sprintf("history-command-success.%s.golden", tc.name))
expected := string(golden.Get(t, []byte(actual), fmt.Sprintf("history-command-success.%s.golden", tc.name))[:])
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, expected)
} else {
assert.Regexp(t, tc.outputRegex, actual)
match, _ := regexp.MatchString(tc.outputRegex, actual)
assert.True(t, match)
}
}
}

View File

@ -1,14 +1,15 @@
package image
import (
"bytes"
"io"
"io/ioutil"
"strings"
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/testutil"
"github.com/docker/cli/cli/internal/test"
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/testutil"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
)
@ -23,7 +24,7 @@ func TestNewImportCommandErrors(t *testing.T) {
{
name: "wrong-args",
args: []string{},
expectedError: "requires at least 1 argument.",
expectedError: "requires at least 1 argument(s).",
},
{
name: "import-failed",
@ -35,7 +36,8 @@ func TestNewImportCommandErrors(t *testing.T) {
},
}
for _, tc := range testCases {
cmd := NewImportCommand(test.NewFakeCli(&fakeClient{imageImportFunc: tc.imageImportFunc}))
buf := new(bytes.Buffer)
cmd := NewImportCommand(test.NewFakeCliWithOutput(&fakeClient{imageImportFunc: tc.imageImportFunc}, buf))
cmd.SetOutput(ioutil.Discard)
cmd.SetArgs(tc.args)
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
@ -89,7 +91,8 @@ func TestNewImportCommandSuccess(t *testing.T) {
},
}
for _, tc := range testCases {
cmd := NewImportCommand(test.NewFakeCli(&fakeClient{imageImportFunc: tc.imageImportFunc}))
buf := new(bytes.Buffer)
cmd := NewImportCommand(test.NewFakeCliWithOutput(&fakeClient{imageImportFunc: tc.imageImportFunc}, buf))
cmd.SetOutput(ioutil.Discard)
cmd.SetArgs(tc.args)
assert.NoError(t, cmd.Execute())

View File

@ -1,14 +1,15 @@
package image
import (
"bytes"
"fmt"
"io/ioutil"
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/testutil"
"github.com/docker/cli/cli/internal/test"
"github.com/docker/docker/api/types"
"github.com/gotestyourself/gotestyourself/golden"
"github.com/docker/docker/pkg/testutil"
"github.com/docker/docker/pkg/testutil/golden"
"github.com/stretchr/testify/assert"
)
@ -21,11 +22,12 @@ func TestNewInspectCommandErrors(t *testing.T) {
{
name: "wrong-args",
args: []string{},
expectedError: "requires at least 1 argument.",
expectedError: "requires at least 1 argument(s).",
},
}
for _, tc := range testCases {
cmd := newInspectCommand(test.NewFakeCli(&fakeClient{}))
buf := new(bytes.Buffer)
cmd := newInspectCommand(test.NewFakeCliWithOutput(&fakeClient{}, buf))
cmd.SetOutput(ioutil.Discard)
cmd.SetArgs(tc.args)
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
@ -76,13 +78,15 @@ func TestNewInspectCommandSuccess(t *testing.T) {
}
for _, tc := range testCases {
imageInspectInvocationCount = 0
cli := test.NewFakeCli(&fakeClient{imageInspectFunc: tc.imageInspectFunc})
cmd := newInspectCommand(cli)
buf := new(bytes.Buffer)
cmd := newInspectCommand(test.NewFakeCliWithOutput(&fakeClient{imageInspectFunc: tc.imageInspectFunc}, buf))
cmd.SetOutput(ioutil.Discard)
cmd.SetArgs(tc.args)
err := cmd.Execute()
assert.NoError(t, err)
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("inspect-command-success.%s.golden", tc.name))
actual := buf.String()
expected := string(golden.Get(t, []byte(actual), fmt.Sprintf("inspect-command-success.%s.golden", tc.name))[:])
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, expected)
assert.Equal(t, imageInspectInvocationCount, tc.imageCount)
}
}

View File

@ -1,15 +1,16 @@
package image
import (
"bytes"
"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/cli/cli/internal/test"
"github.com/docker/docker/api/types"
"github.com/gotestyourself/gotestyourself/golden"
"github.com/docker/docker/pkg/testutil"
"github.com/docker/docker/pkg/testutil/golden"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
)
@ -24,7 +25,7 @@ func TestNewImagesCommandErrors(t *testing.T) {
{
name: "wrong-args",
args: []string{"arg1", "arg2"},
expectedError: "requires at most 1 argument.",
expectedError: "requires at most 1 argument(s).",
},
{
name: "failed-list",
@ -79,14 +80,17 @@ func TestNewImagesCommandSuccess(t *testing.T) {
},
}
for _, tc := range testCases {
cli := test.NewFakeCli(&fakeClient{imageListFunc: tc.imageListFunc})
buf := new(bytes.Buffer)
cli := test.NewFakeCliWithOutput(&fakeClient{imageListFunc: tc.imageListFunc}, buf)
cli.SetConfigFile(&configfile.ConfigFile{ImagesFormat: tc.imageFormat})
cmd := NewImagesCommand(cli)
cmd.SetOutput(ioutil.Discard)
cmd.SetArgs(tc.args)
err := cmd.Execute()
assert.NoError(t, err)
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("list-command-success.%s.golden", tc.name))
actual := buf.String()
expected := string(golden.Get(t, []byte(actual), fmt.Sprintf("list-command-success.%s.golden", tc.name))[:])
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, expected)
}
}

View File

@ -1,16 +1,17 @@
package image
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"strings"
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/testutil"
"github.com/docker/cli/cli/internal/test"
"github.com/docker/docker/api/types"
"github.com/gotestyourself/gotestyourself/golden"
"github.com/docker/docker/pkg/testutil"
"github.com/docker/docker/pkg/testutil/golden"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
)
@ -26,7 +27,7 @@ func TestNewLoadCommandErrors(t *testing.T) {
{
name: "wrong-args",
args: []string{"arg"},
expectedError: "accepts no arguments.",
expectedError: "accepts no argument(s).",
},
{
name: "input-to-terminal",
@ -91,12 +92,14 @@ func TestNewLoadCommandSuccess(t *testing.T) {
},
}
for _, tc := range testCases {
cli := test.NewFakeCli(&fakeClient{imageLoadFunc: tc.imageLoadFunc})
cmd := NewLoadCommand(cli)
buf := new(bytes.Buffer)
cmd := NewLoadCommand(test.NewFakeCliWithOutput(&fakeClient{imageLoadFunc: tc.imageLoadFunc}, buf))
cmd.SetOutput(ioutil.Discard)
cmd.SetArgs(tc.args)
err := cmd.Execute()
assert.NoError(t, err)
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("load-command-success.%s.golden", tc.name))
actual := buf.String()
expected := string(golden.Get(t, []byte(actual), fmt.Sprintf("load-command-success.%s.golden", tc.name))[:])
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, expected)
}
}

View File

@ -1,15 +1,16 @@
package image
import (
"bytes"
"fmt"
"io/ioutil"
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/testutil"
"github.com/docker/cli/cli/internal/test"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/gotestyourself/gotestyourself/golden"
"github.com/docker/docker/pkg/testutil"
"github.com/docker/docker/pkg/testutil/golden"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
)
@ -24,7 +25,7 @@ func TestNewPruneCommandErrors(t *testing.T) {
{
name: "wrong-args",
args: []string{"something"},
expectedError: "accepts no arguments.",
expectedError: "accepts no argument(s).",
},
{
name: "prune-error",
@ -36,9 +37,10 @@ func TestNewPruneCommandErrors(t *testing.T) {
},
}
for _, tc := range testCases {
cmd := NewPruneCommand(test.NewFakeCli(&fakeClient{
buf := new(bytes.Buffer)
cmd := NewPruneCommand(test.NewFakeCliWithOutput(&fakeClient{
imagesPruneFunc: tc.imagesPruneFunc,
}))
}, buf))
cmd.SetOutput(ioutil.Discard)
cmd.SetArgs(tc.args)
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
@ -83,12 +85,16 @@ func TestNewPruneCommandSuccess(t *testing.T) {
},
}
for _, tc := range testCases {
cli := test.NewFakeCli(&fakeClient{imagesPruneFunc: tc.imagesPruneFunc})
cmd := NewPruneCommand(cli)
buf := new(bytes.Buffer)
cmd := NewPruneCommand(test.NewFakeCliWithOutput(&fakeClient{
imagesPruneFunc: tc.imagesPruneFunc,
}, buf))
cmd.SetOutput(ioutil.Discard)
cmd.SetArgs(tc.args)
err := cmd.Execute()
assert.NoError(t, err)
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("prune-command-success.%s.golden", tc.name))
actual := buf.String()
expected := string(golden.Get(t, []byte(actual), fmt.Sprintf("prune-command-success.%s.golden", tc.name))[:])
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, expected)
}
}

View File

@ -5,9 +5,9 @@ import (
"io/ioutil"
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/testutil"
"github.com/gotestyourself/gotestyourself/golden"
"github.com/docker/cli/cli/internal/test"
"github.com/docker/docker/pkg/testutil"
"github.com/docker/docker/pkg/testutil/golden"
"github.com/stretchr/testify/assert"
)
@ -19,7 +19,7 @@ func TestNewPullCommandErrors(t *testing.T) {
}{
{
name: "wrong-args",
expectedError: "requires exactly 1 argument.",
expectedError: "requires exactly 1 argument(s).",
args: []string{},
},
{
@ -68,6 +68,8 @@ func TestNewPullCommandSuccess(t *testing.T) {
cmd.SetArgs(tc.args)
err := cmd.Execute()
assert.NoError(t, err)
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("pull-command-success.%s.golden", tc.name))
actual := cli.OutBuffer().String()
expected := string(golden.Get(t, []byte(actual), fmt.Sprintf("pull-command-success.%s.golden", tc.name))[:])
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, expected)
}
}

View File

@ -6,9 +6,9 @@ import (
"strings"
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/testutil"
"github.com/docker/cli/cli/internal/test"
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/testutil"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
)
@ -23,7 +23,7 @@ func TestNewPushCommandErrors(t *testing.T) {
{
name: "wrong-args",
args: []string{},
expectedError: "requires exactly 1 argument.",
expectedError: "requires exactly 1 argument(s).",
},
{
name: "invalid-name",

View File

@ -5,10 +5,10 @@ import (
"io/ioutil"
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/testutil"
"github.com/docker/cli/cli/internal/test"
"github.com/docker/docker/api/types"
"github.com/gotestyourself/gotestyourself/golden"
"github.com/docker/docker/pkg/testutil"
"github.com/docker/docker/pkg/testutil/golden"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
)
@ -29,7 +29,7 @@ func TestNewRemoveCommandErrors(t *testing.T) {
}{
{
name: "wrong args",
expectedError: "requires at least 1 argument.",
expectedError: "requires at least 1 argument(s).",
},
{
name: "ImageRemove fail",
@ -96,14 +96,16 @@ func TestNewRemoveCommandSuccess(t *testing.T) {
},
}
for _, tc := range testCases {
cli := test.NewFakeCli(&fakeClient{imageRemoveFunc: tc.imageRemoveFunc})
cmd := NewRemoveCommand(cli)
fakeCli := test.NewFakeCli(&fakeClient{imageRemoveFunc: tc.imageRemoveFunc})
cmd := NewRemoveCommand(fakeCli)
cmd.SetOutput(ioutil.Discard)
cmd.SetArgs(tc.args)
assert.NoError(t, cmd.Execute())
if tc.expectedErrMsg != "" {
assert.Equal(t, tc.expectedErrMsg, cli.ErrBuffer().String())
assert.Equal(t, tc.expectedErrMsg, fakeCli.ErrBuffer().String())
}
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("remove-command-success.%s.golden", tc.name))
actual := fakeCli.OutBuffer().String()
expected := string(golden.Get(t, []byte(actual), fmt.Sprintf("remove-command-success.%s.golden", tc.name))[:])
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, expected)
}
}

View File

@ -2,8 +2,6 @@ package image
import (
"io"
"os"
"path/filepath"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
@ -43,10 +41,6 @@ func runSave(dockerCli command.Cli, opts saveOptions) error {
return errors.New("cowardly refusing to save to a terminal. Use the -o flag or redirect")
}
if err := validateOutputPath(opts.output); err != nil {
return errors.Wrap(err, "failed to save image")
}
responseBody, err := dockerCli.Client().ImageSave(context.Background(), opts.images)
if err != nil {
return err
@ -60,13 +54,3 @@ func runSave(dockerCli command.Cli, opts saveOptions) error {
return command.CopyToFile(opts.output, responseBody)
}
func validateOutputPath(path string) error {
dir := filepath.Dir(path)
if dir != "" && dir != "." {
if _, err := os.Stat(dir); os.IsNotExist(err) {
return errors.Errorf("unable to validate output path: directory %q does not exist", dir)
}
}
return nil
}

View File

@ -1,14 +1,15 @@
package image
import (
"bytes"
"io"
"io/ioutil"
"os"
"strings"
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/testutil"
"github.com/docker/cli/cli/internal/test"
"github.com/docker/docker/pkg/testutil"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@ -25,7 +26,7 @@ func TestNewSaveCommandErrors(t *testing.T) {
{
name: "wrong args",
args: []string{},
expectedError: "requires at least 1 argument.",
expectedError: "requires at least 1 argument(s).",
},
{
name: "output to terminal",
@ -42,11 +43,6 @@ func TestNewSaveCommandErrors(t *testing.T) {
return ioutil.NopCloser(strings.NewReader("")), errors.Errorf("error saving image")
},
},
{
name: "output directory does not exist",
args: []string{"-o", "fakedir/out.tar", "arg1"},
expectedError: "failed to save image: unable to validate output path: directory \"fakedir\" does not exist",
},
}
for _, tc := range testCases {
cli := test.NewFakeCli(&fakeClient{imageSaveFunc: tc.imageSaveFunc})
@ -89,11 +85,11 @@ func TestNewSaveCommandSuccess(t *testing.T) {
},
}
for _, tc := range testCases {
cmd := NewSaveCommand(test.NewFakeCli(&fakeClient{
cmd := NewSaveCommand(test.NewFakeCliWithOutput(&fakeClient{
imageSaveFunc: func(images []string) (io.ReadCloser, error) {
return ioutil.NopCloser(strings.NewReader("")), nil
},
}))
}, new(bytes.Buffer)))
cmd.SetOutput(ioutil.Discard)
cmd.SetArgs(tc.args)
assert.NoError(t, cmd.Execute())

View File

@ -4,8 +4,8 @@ import (
"io/ioutil"
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/testutil"
"github.com/docker/cli/cli/internal/test"
"github.com/docker/docker/pkg/testutil"
"github.com/stretchr/testify/assert"
)
@ -15,7 +15,7 @@ func TestCliNewTagCommandErrors(t *testing.T) {
{"image1"},
{"image1", "image2", "image3"},
}
expectedError := "\"tag\" requires exactly 2 arguments."
expectedError := "\"tag\" requires exactly 2 argument(s)."
for _, args := range testCases {
cmd := NewTagCommand(test.NewFakeCli(&fakeClient{}))
cmd.SetArgs(args)

View File

@ -9,7 +9,7 @@ import (
"github.com/Sirupsen/logrus"
"github.com/docker/cli/cli"
"github.com/docker/cli/templates"
"github.com/docker/docker/pkg/templates"
"github.com/pkg/errors"
)

View File

@ -5,7 +5,7 @@ import (
"strings"
"testing"
"github.com/docker/cli/templates"
"github.com/docker/docker/pkg/templates"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

View File

@ -2,16 +2,13 @@ package network
import (
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/client"
"golang.org/x/net/context"
)
type fakeClient struct {
client.Client
networkCreateFunc func(ctx context.Context, name string, options types.NetworkCreate) (types.NetworkCreateResponse, error)
networkConnectFunc func(ctx context.Context, networkID, container string, config *network.EndpointSettings) error
networkDisconnectFunc func(ctx context.Context, networkID, container string, force bool) error
networkCreateFunc func(ctx context.Context, name string, options types.NetworkCreate) (types.NetworkCreateResponse, error)
}
func (c *fakeClient) NetworkCreate(ctx context.Context, name string, options types.NetworkCreate) (types.NetworkCreateResponse, error) {
@ -20,17 +17,3 @@ func (c *fakeClient) NetworkCreate(ctx context.Context, name string, options typ
}
return types.NetworkCreateResponse{}, nil
}
func (c *fakeClient) NetworkConnect(ctx context.Context, networkID, container string, config *network.EndpointSettings) error {
if c.networkConnectFunc != nil {
return c.networkConnectFunc(ctx, networkID, container, config)
}
return nil
}
func (c *fakeClient) NetworkDisconnect(ctx context.Context, networkID, container string, force bool) error {
if c.networkDisconnectFunc != nil {
return c.networkDisconnectFunc(ctx, networkID, container, force)
}
return nil
}

View File

@ -1,70 +0,0 @@
package network
import (
"io/ioutil"
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/testutil"
"github.com/docker/docker/api/types/network"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"golang.org/x/net/context"
)
func TestNetworkConnectErrors(t *testing.T) {
testCases := []struct {
args []string
networkConnectFunc func(ctx context.Context, networkID, container string, config *network.EndpointSettings) error
expectedError string
}{
{
expectedError: "requires exactly 2 arguments",
},
{
args: []string{"toto", "titi"},
networkConnectFunc: func(ctx context.Context, networkID, container string, config *network.EndpointSettings) error {
return errors.Errorf("error connecting network")
},
expectedError: "error connecting network",
},
}
for _, tc := range testCases {
cmd := newConnectCommand(
test.NewFakeCli(&fakeClient{
networkConnectFunc: tc.networkConnectFunc,
}),
)
cmd.SetArgs(tc.args)
cmd.SetOutput(ioutil.Discard)
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
}
}
func TestNetworkConnectWithFlags(t *testing.T) {
expectedOpts := []network.IPAMConfig{
{
"192.168.4.0/24",
"192.168.4.0/24",
"192.168.4.1/24",
map[string]string{},
},
}
cli := test.NewFakeCli(&fakeClient{
networkConnectFunc: func(ctx context.Context, networkID, container string, config *network.EndpointSettings) error {
assert.Equal(t, expectedOpts, config.IPAMConfig, "not expected driver error")
return nil
},
})
args := []string{"banana"}
cmd := newCreateCommand(cli)
cmd.SetArgs(args)
cmd.Flags().Set("driver", "foo")
cmd.Flags().Set("ip-range", "192.168.4.0/24")
cmd.Flags().Set("gateway", "192.168.4.1/24")
cmd.Flags().Set("subnet", "192.168.4.0/24")
assert.NoError(t, cmd.Execute())
}

View File

@ -5,10 +5,10 @@ import (
"strings"
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/testutil"
"github.com/docker/cli/cli/internal/test"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/pkg/testutil"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

View File

@ -1,41 +0,0 @@
package network
import (
"io/ioutil"
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/testutil"
"github.com/pkg/errors"
"golang.org/x/net/context"
)
func TestNetworkDisconnectErrors(t *testing.T) {
testCases := []struct {
args []string
networkDisconnectFunc func(ctx context.Context, networkID, container string, force bool) error
expectedError string
}{
{
expectedError: "requires exactly 2 arguments",
},
{
args: []string{"toto", "titi"},
networkDisconnectFunc: func(ctx context.Context, networkID, container string, force bool) error {
return errors.Errorf("error disconnecting network")
},
expectedError: "error disconnecting network",
},
}
for _, tc := range testCases {
cmd := newDisconnectCommand(
test.NewFakeCli(&fakeClient{
networkDisconnectFunc: tc.networkDisconnectFunc,
}),
)
cmd.SetArgs(tc.args)
cmd.SetOutput(ioutil.Discard)
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
}
}

View File

@ -4,12 +4,12 @@ import (
"io/ioutil"
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/cli/internal/test"
"github.com/docker/docker/api/types/swarm"
"github.com/pkg/errors"
// Import builders to get the builder function as package function
. "github.com/docker/cli/internal/test/builders"
"github.com/docker/cli/internal/test/testutil"
. "github.com/docker/cli/cli/internal/test/builders"
"github.com/docker/docker/pkg/testutil"
"github.com/stretchr/testify/assert"
)

View File

@ -1,18 +1,19 @@
package node
import (
"bytes"
"fmt"
"io/ioutil"
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/cli/internal/test"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/swarm"
"github.com/pkg/errors"
// Import builders to get the builder function as package function
. "github.com/docker/cli/internal/test/builders"
"github.com/docker/cli/internal/test/testutil"
"github.com/gotestyourself/gotestyourself/golden"
. "github.com/docker/cli/cli/internal/test/builders"
"github.com/docker/docker/pkg/testutil"
"github.com/docker/docker/pkg/testutil/golden"
"github.com/stretchr/testify/assert"
)
@ -66,11 +67,12 @@ func TestNodeInspectErrors(t *testing.T) {
},
}
for _, tc := range testCases {
buf := new(bytes.Buffer)
cmd := newInspectCommand(
test.NewFakeCli(&fakeClient{
test.NewFakeCliWithOutput(&fakeClient{
nodeInspectFunc: tc.nodeInspectFunc,
infoFunc: tc.infoFunc,
}))
}, buf))
cmd.SetArgs(tc.args)
for key, value := range tc.flags {
cmd.Flags().Set(key, value)
@ -107,13 +109,16 @@ func TestNodeInspectPretty(t *testing.T) {
},
}
for _, tc := range testCases {
cli := test.NewFakeCli(&fakeClient{
nodeInspectFunc: tc.nodeInspectFunc,
})
cmd := newInspectCommand(cli)
buf := new(bytes.Buffer)
cmd := newInspectCommand(
test.NewFakeCliWithOutput(&fakeClient{
nodeInspectFunc: tc.nodeInspectFunc,
}, buf))
cmd.SetArgs([]string{"nodeID"})
cmd.Flags().Set("pretty", "true")
assert.NoError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("node-inspect-pretty.%s.golden", tc.name))
actual := buf.String()
expected := golden.Get(t, []byte(actual), fmt.Sprintf("node-inspect-pretty.%s.golden", tc.name))
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
}
}

View File

@ -1,17 +1,19 @@
package node
import (
"bytes"
"io/ioutil"
"testing"
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/cli/internal/test"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/swarm"
"github.com/gotestyourself/gotestyourself/golden"
"github.com/docker/docker/pkg/testutil"
"github.com/docker/docker/pkg/testutil/golden"
"github.com/pkg/errors"
// Import builders to get the builder function as package function
. "github.com/docker/cli/internal/test/builders"
. "github.com/docker/cli/cli/internal/test/builders"
"github.com/stretchr/testify/assert"
)
@ -56,9 +58,9 @@ 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())),
*Node(NodeID("nodeID2"), Hostname("node-10-foo"), Manager()),
*Node(NodeID("nodeID3"), Hostname("node-1-foo")),
*Node(NodeID("nodeID1"), Hostname("nodeHostname1"), Manager(Leader())),
*Node(NodeID("nodeID2"), Hostname("nodeHostname2"), Manager()),
*Node(NodeID("nodeID3"), Hostname("nodeHostname3")),
}, nil
},
infoFunc: func() (types.Info, error) {
@ -72,25 +74,39 @@ func TestNodeList(t *testing.T) {
cmd := newListCommand(cli)
assert.NoError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "node-list-sort.golden")
out := cli.OutBuffer().String()
assert.Contains(t, out, `nodeID1 * nodeHostname1 Ready Active Leader`)
assert.Contains(t, out, `nodeID2 nodeHostname2 Ready Active Reachable`)
assert.Contains(t, out, `nodeID3 nodeHostname3 Ready Active`)
}
func TestNodeListQuietShouldOnlyPrintIDs(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
buf := new(bytes.Buffer)
cli := test.NewFakeCliWithOutput(&fakeClient{
nodeListFunc: func() ([]swarm.Node, error) {
return []swarm.Node{
*Node(NodeID("nodeID1")),
*Node(),
}, nil
},
})
}, buf)
cmd := newListCommand(cli)
cmd.Flags().Set("quiet", "true")
assert.NoError(t, cmd.Execute())
assert.Equal(t, cli.OutBuffer().String(), "nodeID1\n")
assert.Contains(t, buf.String(), "nodeID")
}
func TestNodeListDefaultFormatFromConfig(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
// Test case for #24090
func TestNodeListContainsHostname(t *testing.T) {
buf := new(bytes.Buffer)
cli := test.NewFakeCliWithOutput(&fakeClient{}, buf)
cmd := newListCommand(cli)
assert.NoError(t, cmd.Execute())
assert.Contains(t, buf.String(), "HOSTNAME")
}
func TestNodeListDefaultFormat(t *testing.T) {
buf := new(bytes.Buffer)
cli := test.NewFakeCliWithOutput(&fakeClient{
nodeListFunc: func() ([]swarm.Node, error) {
return []swarm.Node{
*Node(NodeID("nodeID1"), Hostname("nodeHostname1"), Manager(Leader())),
@ -105,17 +121,20 @@ func TestNodeListDefaultFormatFromConfig(t *testing.T) {
},
}, nil
},
})
}, buf)
cli.SetConfigFile(&configfile.ConfigFile{
NodesFormat: "{{.ID}}: {{.Hostname}} {{.Status}}/{{.ManagerStatus}}",
})
cmd := newListCommand(cli)
assert.NoError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "node-list-format-from-config.golden")
assert.Contains(t, buf.String(), `nodeID1: nodeHostname1 Ready/Leader`)
assert.Contains(t, buf.String(), `nodeID2: nodeHostname2 Ready/Reachable`)
assert.Contains(t, buf.String(), `nodeID3: nodeHostname3 Ready`)
}
func TestNodeListFormat(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
buf := new(bytes.Buffer)
cli := test.NewFakeCliWithOutput(&fakeClient{
nodeListFunc: func() ([]swarm.Node, error) {
return []swarm.Node{
*Node(NodeID("nodeID1"), Hostname("nodeHostname1"), Manager(Leader())),
@ -129,12 +148,32 @@ func TestNodeListFormat(t *testing.T) {
},
}, nil
},
})
}, buf)
cli.SetConfigFile(&configfile.ConfigFile{
NodesFormat: "{{.ID}}: {{.Hostname}} {{.Status}}/{{.ManagerStatus}}",
})
cmd := newListCommand(cli)
cmd.Flags().Set("format", "{{.Hostname}}: {{.ManagerStatus}}")
assert.NoError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "node-list-format-flag.golden")
assert.Contains(t, buf.String(), `nodeHostname1: Leader`)
assert.Contains(t, buf.String(), `nodeHostname2: Reachable`)
}
func TestNodeListOrder(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
nodeListFunc: func() ([]swarm.Node, error) {
return []swarm.Node{
*Node(Hostname("node-2-foo"), Manager(Leader())),
*Node(Hostname("node-10-foo"), Manager()),
*Node(Hostname("node-1-foo")),
}, nil
},
})
cmd := newListCommand(cli)
cmd.Flags().Set("format", "{{.Hostname}}: {{.ManagerStatus}}")
assert.NoError(t, cmd.Execute())
actual := cli.OutBuffer().String()
expected := golden.Get(t, []byte(actual), "node-list-sort.golden")
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
}

View File

@ -1,15 +1,16 @@
package node
import (
"bytes"
"io/ioutil"
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/cli/internal/test"
"github.com/docker/docker/api/types/swarm"
"github.com/pkg/errors"
// Import builders to get the builder function as package function
. "github.com/docker/cli/internal/test/builders"
"github.com/docker/cli/internal/test/testutil"
. "github.com/docker/cli/cli/internal/test/builders"
"github.com/docker/docker/pkg/testutil"
"github.com/stretchr/testify/assert"
)
@ -39,11 +40,12 @@ func TestNodePromoteErrors(t *testing.T) {
},
}
for _, tc := range testCases {
buf := new(bytes.Buffer)
cmd := newPromoteCommand(
test.NewFakeCli(&fakeClient{
test.NewFakeCliWithOutput(&fakeClient{
nodeInspectFunc: tc.nodeInspectFunc,
nodeUpdateFunc: tc.nodeUpdateFunc,
}))
}, buf))
cmd.SetArgs(tc.args)
cmd.SetOutput(ioutil.Discard)
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
@ -51,8 +53,9 @@ func TestNodePromoteErrors(t *testing.T) {
}
func TestNodePromoteNoChange(t *testing.T) {
buf := new(bytes.Buffer)
cmd := newPromoteCommand(
test.NewFakeCli(&fakeClient{
test.NewFakeCliWithOutput(&fakeClient{
nodeInspectFunc: func() (swarm.Node, []byte, error) {
return *Node(Manager()), []byte{}, nil
},
@ -62,14 +65,15 @@ func TestNodePromoteNoChange(t *testing.T) {
}
return nil
},
}))
}, buf))
cmd.SetArgs([]string{"nodeID"})
assert.NoError(t, cmd.Execute())
}
func TestNodePromoteMultipleNode(t *testing.T) {
buf := new(bytes.Buffer)
cmd := newPromoteCommand(
test.NewFakeCli(&fakeClient{
test.NewFakeCliWithOutput(&fakeClient{
nodeInspectFunc: func() (swarm.Node, []byte, error) {
return *Node(), []byte{}, nil
},
@ -79,7 +83,7 @@ func TestNodePromoteMultipleNode(t *testing.T) {
}
return nil
},
}))
}, buf))
cmd.SetArgs([]string{"nodeID1", "nodeID2"})
assert.NoError(t, cmd.Execute())
}

View File

@ -1,18 +1,20 @@
package node
import (
"bytes"
"fmt"
"io/ioutil"
"testing"
"time"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/cli/internal/test"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/swarm"
"github.com/pkg/errors"
// Import builders to get the builder function as package function
. "github.com/docker/cli/internal/test/builders"
"github.com/gotestyourself/gotestyourself/golden"
. "github.com/docker/cli/cli/internal/test/builders"
"github.com/docker/docker/pkg/testutil"
"github.com/docker/docker/pkg/testutil/golden"
"github.com/stretchr/testify/assert"
)
@ -48,13 +50,14 @@ func TestNodePsErrors(t *testing.T) {
},
}
for _, tc := range testCases {
cli := test.NewFakeCli(&fakeClient{
infoFunc: tc.infoFunc,
nodeInspectFunc: tc.nodeInspectFunc,
taskInspectFunc: tc.taskInspectFunc,
taskListFunc: tc.taskListFunc,
})
cmd := newPsCommand(cli)
buf := new(bytes.Buffer)
cmd := newPsCommand(
test.NewFakeCliWithOutput(&fakeClient{
infoFunc: tc.infoFunc,
nodeInspectFunc: tc.nodeInspectFunc,
taskInspectFunc: tc.taskInspectFunc,
taskListFunc: tc.taskListFunc,
}, buf))
cmd.SetArgs(tc.args)
for key, value := range tc.flags {
cmd.Flags().Set(key, value)
@ -111,18 +114,21 @@ func TestNodePs(t *testing.T) {
},
}
for _, tc := range testCases {
cli := test.NewFakeCli(&fakeClient{
infoFunc: tc.infoFunc,
nodeInspectFunc: tc.nodeInspectFunc,
taskInspectFunc: tc.taskInspectFunc,
taskListFunc: tc.taskListFunc,
})
cmd := newPsCommand(cli)
buf := new(bytes.Buffer)
cmd := newPsCommand(
test.NewFakeCliWithOutput(&fakeClient{
infoFunc: tc.infoFunc,
nodeInspectFunc: tc.nodeInspectFunc,
taskInspectFunc: tc.taskInspectFunc,
taskListFunc: tc.taskListFunc,
}, buf))
cmd.SetArgs(tc.args)
for key, value := range tc.flags {
cmd.Flags().Set(key, value)
}
assert.NoError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("node-ps.%s.golden", tc.name))
actual := buf.String()
expected := golden.Get(t, []byte(actual), fmt.Sprintf("node-ps.%s.golden", tc.name))
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
}
}

View File

@ -1,11 +1,12 @@
package node
import (
"bytes"
"io/ioutil"
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/testutil"
"github.com/docker/cli/cli/internal/test"
"github.com/docker/docker/pkg/testutil"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
)
@ -28,10 +29,11 @@ func TestNodeRemoveErrors(t *testing.T) {
},
}
for _, tc := range testCases {
buf := new(bytes.Buffer)
cmd := newRemoveCommand(
test.NewFakeCli(&fakeClient{
test.NewFakeCliWithOutput(&fakeClient{
nodeRemoveFunc: tc.nodeRemoveFunc,
}))
}, buf))
cmd.SetArgs(tc.args)
cmd.SetOutput(ioutil.Discard)
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
@ -39,7 +41,8 @@ func TestNodeRemoveErrors(t *testing.T) {
}
func TestNodeRemoveMultiple(t *testing.T) {
cmd := newRemoveCommand(test.NewFakeCli(&fakeClient{}))
buf := new(bytes.Buffer)
cmd := newRemoveCommand(test.NewFakeCliWithOutput(&fakeClient{}, buf))
cmd.SetArgs([]string{"nodeID1", "nodeID2"})
assert.NoError(t, cmd.Execute())
}

View File

@ -1,10 +1,10 @@
ID: nodeID
Name: defaultNodeName
Hostname: defaultNodeHostname
Joined at: 2009-11-10 23:00:00 +0000 utc
Hostname: defaultNodeHostname
Joined at: 2009-11-10 23:00:00 +0000 utc
Status:
State: Ready
Availability: Active
Availability: Active
Address: 127.0.0.1
Manager Status:
Address: 127.0.0.1
@ -15,10 +15,11 @@ Platform:
Architecture: x86_64
Resources:
CPUs: 0
Memory: 20MiB
Memory: 20 MiB
Plugins:
Network: bridge, overlay
Volume: local
Network: bridge, overlay
Volume: local
Engine Version: 1.13.0
Engine Labels:
- engine=label
- engine = label

View File

@ -1,10 +1,10 @@
ID: nodeID
Name: defaultNodeName
Hostname: defaultNodeHostname
Joined at: 2009-11-10 23:00:00 +0000 utc
Hostname: defaultNodeHostname
Joined at: 2009-11-10 23:00:00 +0000 utc
Status:
State: Ready
Availability: Active
Availability: Active
Address: 127.0.0.1
Manager Status:
Address: 127.0.0.1
@ -15,10 +15,11 @@ Platform:
Architecture: x86_64
Resources:
CPUs: 0
Memory: 20MiB
Memory: 20 MiB
Plugins:
Network: bridge, overlay
Volume: local
Network: bridge, overlay
Volume: local
Engine Version: 1.13.0
Engine Labels:
- engine=label
- engine = label

View File

@ -1,22 +1,23 @@
ID: nodeID
Name: defaultNodeName
Labels:
- lbl1=value1
Hostname: defaultNodeHostname
Joined at: 2009-11-10 23:00:00 +0000 utc
- lbl1 = value1
Hostname: defaultNodeHostname
Joined at: 2009-11-10 23:00:00 +0000 utc
Status:
State: Ready
Availability: Active
Availability: Active
Address: 127.0.0.1
Platform:
Operating System: linux
Architecture: x86_64
Resources:
CPUs: 0
Memory: 20MiB
Memory: 20 MiB
Plugins:
Network: bridge, overlay
Volume: local
Network: bridge, overlay
Volume: local
Engine Version: 1.13.0
Engine Labels:
- engine=label
- engine = label

View File

@ -1,2 +0,0 @@
nodeHostname1: Leader
nodeHostname2: Reachable

View File

@ -1,3 +0,0 @@
nodeID1: nodeHostname1 Ready/Leader
nodeID2: nodeHostname2 Ready/Reachable
nodeID3: nodeHostname3 Ready/

View File

@ -1,4 +1,3 @@
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
node-1-foo:
node-2-foo: Leader
node-10-foo: Reachable

View File

@ -1,2 +1,2 @@
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS
taskID rl02d5gwz6chzu7il5fhtb8be.1 myimage:mytag defaultNodeName Ready Ready 2 hours ago *:80->80/tcp
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS
taskID rl02d5gwz6chzu7il5fhtb8be.1 myimage:mytag defaultNodeName Ready Ready 2 hours ago *:80->80/tcp

View File

@ -1,4 +1,4 @@
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS
taskID1 failure.1 myimage:mytag defaultNodeName Ready Ready 2 hours ago "a task error"
taskID2 \_ failure.1 myimage:mytag defaultNodeName Ready Ready 3 hours ago "a task error"
taskID3 \_ failure.1 myimage:mytag defaultNodeName Ready Ready 4 hours ago "a task error"
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS
taskID1 failure.1 myimage:mytag defaultNodeName Ready Ready 2 hours ago "a task error"
taskID2 \_ failure.1 myimage:mytag defaultNodeName Ready Ready 3 hours ago "a task error"
taskID3 \_ failure.1 myimage:mytag defaultNodeName Ready Ready 4 hours ago "a task error"

View File

@ -4,12 +4,12 @@ import (
"io/ioutil"
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/cli/internal/test"
"github.com/docker/docker/api/types/swarm"
"github.com/pkg/errors"
// Import builders to get the builder function as package function
. "github.com/docker/cli/internal/test/builders"
"github.com/docker/cli/internal/test/testutil"
. "github.com/docker/cli/cli/internal/test/builders"
"github.com/docker/docker/pkg/testutil"
"github.com/stretchr/testify/assert"
)

View File

@ -1,21 +1,23 @@
package registry
import (
"fmt"
"sort"
"strings"
"text/tabwriter"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/cli/opts"
"github.com/docker/docker/api/types"
registrytypes "github.com/docker/docker/api/types/registry"
"github.com/docker/docker/pkg/stringutils"
"github.com/docker/docker/registry"
"github.com/spf13/cobra"
"golang.org/x/net/context"
)
type searchOptions struct {
format string
term string
noTrunc bool
limit int
@ -45,7 +47,6 @@ func NewSearchCommand(dockerCli command.Cli) *cobra.Command {
flags.BoolVar(&options.noTrunc, "no-trunc", false, "Don't truncate output")
flags.VarP(&options.filter, "filter", "f", "Filter output based on conditions provided")
flags.IntVar(&options.limit, "limit", registry.DefaultSearchLimit, "Max number of search results")
flags.StringVar(&options.format, "format", "", "Pretty-print search using a Go template")
flags.BoolVar(&options.automated, "automated", false, "Only show automated builds")
flags.UintVarP(&options.stars, "stars", "s", 0, "Only displays with at least x stars")
@ -88,12 +89,32 @@ func runSearch(dockerCli command.Cli, options searchOptions) error {
results := searchResultsByStars(unorderedResults)
sort.Sort(results)
searchCtx := formatter.Context{
Output: dockerCli.Out(),
Format: formatter.NewSearchFormat(options.format),
Trunc: !options.noTrunc,
w := tabwriter.NewWriter(dockerCli.Out(), 10, 1, 3, ' ', 0)
fmt.Fprintf(w, "NAME\tDESCRIPTION\tSTARS\tOFFICIAL\tAUTOMATED\n")
for _, res := range results {
// --automated and -s, --stars are deprecated since Docker 1.12
if (options.automated && !res.IsAutomated) || (int(options.stars) > res.StarCount) {
continue
}
desc := strings.Replace(res.Description, "\n", " ", -1)
desc = strings.Replace(desc, "\r", " ", -1)
if !options.noTrunc {
desc = stringutils.Ellipsis(desc, 45)
}
fmt.Fprintf(w, "%s\t%s\t%d\t", res.Name, desc, res.StarCount)
if res.IsOfficial {
fmt.Fprint(w, "[OK]")
}
fmt.Fprint(w, "\t")
if res.IsAutomated {
fmt.Fprint(w, "[OK]")
}
fmt.Fprint(w, "\n")
}
return formatter.SearchWrite(searchCtx, results, options.automated, int(options.stars))
w.Flush()
return nil
}
// searchResultsByStars sorts search results in descending order by number of stars.

View File

@ -7,9 +7,9 @@ import (
"github.com/stretchr/testify/assert"
"golang.org/x/net/context"
// Prevents a circular import with "github.com/docker/cli/internal/test"
// Prevents a circular import with "github.com/docker/cli/cli/internal/test"
. "github.com/docker/cli/cli/command"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/cli/internal/test"
"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
)

View File

@ -1,17 +1,18 @@
package secret
import (
"bytes"
"io/ioutil"
"path/filepath"
"reflect"
"strings"
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/testutil"
"github.com/docker/cli/cli/internal/test"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/swarm"
"github.com/gotestyourself/gotestyourself/golden"
"github.com/docker/docker/pkg/testutil"
"github.com/docker/docker/pkg/testutil/golden"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
)
@ -26,10 +27,10 @@ func TestSecretCreateErrors(t *testing.T) {
}{
{
args: []string{"too_few"},
expectedError: "requires exactly 2 arguments",
expectedError: "requires exactly 2 argument(s)",
},
{args: []string{"too", "many", "arguments"},
expectedError: "requires exactly 2 arguments",
expectedError: "requires exactly 2 argument(s)",
},
{
args: []string{"name", filepath.Join("testdata", secretDataFile)},
@ -53,8 +54,9 @@ func TestSecretCreateErrors(t *testing.T) {
func TestSecretCreateWithName(t *testing.T) {
name := "foo"
buf := new(bytes.Buffer)
var actual []byte
cli := test.NewFakeCli(&fakeClient{
cli := test.NewFakeCliWithOutput(&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)
@ -66,13 +68,14 @@ func TestSecretCreateWithName(t *testing.T) {
ID: "ID-" + spec.Name,
}, nil
},
})
}, buf)
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()))
expected := golden.Get(t, actual, secretDataFile)
assert.Equal(t, expected, actual)
assert.Equal(t, "ID-"+name, strings.TrimSpace(buf.String()))
}
func TestSecretCreateWithLabels(t *testing.T) {
@ -82,7 +85,8 @@ func TestSecretCreateWithLabels(t *testing.T) {
}
name := "foo"
cli := test.NewFakeCli(&fakeClient{
buf := new(bytes.Buffer)
cli := test.NewFakeCliWithOutput(&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)
@ -96,12 +100,12 @@ func TestSecretCreateWithLabels(t *testing.T) {
ID: "ID-" + spec.Name,
}, nil
},
})
}, buf)
cmd := newSecretCreateCommand(cli)
cmd.SetArgs([]string{name, filepath.Join("testdata", secretDataFile)})
cmd.Flags().Set("label", "lbl1=Label-foo")
cmd.Flags().Set("label", "lbl2=Label-bar")
assert.NoError(t, cmd.Execute())
assert.Equal(t, "ID-"+name, strings.TrimSpace(cli.OutBuffer().String()))
assert.Equal(t, "ID-"+name, strings.TrimSpace(buf.String()))
}

View File

@ -1,18 +1,19 @@
package secret
import (
"bytes"
"fmt"
"io/ioutil"
"testing"
"time"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/cli/internal/test"
"github.com/docker/docker/api/types/swarm"
"github.com/pkg/errors"
// Import builders to get the builder function as package function
. "github.com/docker/cli/internal/test/builders"
"github.com/docker/cli/internal/test/testutil"
"github.com/gotestyourself/gotestyourself/golden"
. "github.com/docker/cli/cli/internal/test/builders"
"github.com/docker/docker/pkg/testutil"
"github.com/docker/docker/pkg/testutil/golden"
"github.com/stretchr/testify/assert"
)
@ -52,10 +53,11 @@ func TestSecretInspectErrors(t *testing.T) {
},
}
for _, tc := range testCases {
buf := new(bytes.Buffer)
cmd := newSecretInspectCommand(
test.NewFakeCli(&fakeClient{
test.NewFakeCliWithOutput(&fakeClient{
secretInspectFunc: tc.secretInspectFunc,
}),
}, buf),
)
cmd.SetArgs(tc.args)
for key, value := range tc.flags {
@ -93,13 +95,17 @@ func TestSecretInspectWithoutFormat(t *testing.T) {
},
}
for _, tc := range testCases {
cli := test.NewFakeCli(&fakeClient{
secretInspectFunc: tc.secretInspectFunc,
})
cmd := newSecretInspectCommand(cli)
buf := new(bytes.Buffer)
cmd := newSecretInspectCommand(
test.NewFakeCliWithOutput(&fakeClient{
secretInspectFunc: tc.secretInspectFunc,
}, buf),
)
cmd.SetArgs(tc.args)
assert.NoError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("secret-inspect-without-format.%s.golden", tc.name))
actual := buf.String()
expected := golden.Get(t, []byte(actual), fmt.Sprintf("secret-inspect-without-format.%s.golden", tc.name))
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
}
}
@ -129,14 +135,18 @@ func TestSecretInspectWithFormat(t *testing.T) {
},
}
for _, tc := range testCases {
cli := test.NewFakeCli(&fakeClient{
secretInspectFunc: tc.secretInspectFunc,
})
cmd := newSecretInspectCommand(cli)
buf := new(bytes.Buffer)
cmd := newSecretInspectCommand(
test.NewFakeCliWithOutput(&fakeClient{
secretInspectFunc: tc.secretInspectFunc,
}, buf),
)
cmd.SetArgs(tc.args)
cmd.Flags().Set("format", tc.format)
assert.NoError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("secret-inspect-with-format.%s.golden", tc.name))
actual := buf.String()
expected := golden.Get(t, []byte(actual), fmt.Sprintf("secret-inspect-with-format.%s.golden", tc.name))
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
}
}
@ -161,13 +171,16 @@ func TestSecretInspectPretty(t *testing.T) {
},
}
for _, tc := range testCases {
cli := test.NewFakeCli(&fakeClient{
secretInspectFunc: tc.secretInspectFunc,
})
cmd := newSecretInspectCommand(cli)
buf := new(bytes.Buffer)
cmd := newSecretInspectCommand(
test.NewFakeCliWithOutput(&fakeClient{
secretInspectFunc: tc.secretInspectFunc,
}, buf))
cmd.SetArgs([]string{"secretID"})
cmd.Flags().Set("pretty", "true")
assert.NoError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("secret-inspect-pretty.%s.golden", tc.name))
actual := buf.String()
expected := golden.Get(t, []byte(actual), fmt.Sprintf("secret-inspect-pretty.%s.golden", tc.name))
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
}
}

View File

@ -7,14 +7,14 @@ import (
"time"
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/cli/internal/test"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/swarm"
"github.com/pkg/errors"
// Import builders to get the builder function as package function
. "github.com/docker/cli/internal/test/builders"
"github.com/docker/cli/internal/test/testutil"
"github.com/gotestyourself/gotestyourself/golden"
. "github.com/docker/cli/cli/internal/test/builders"
"github.com/docker/docker/pkg/testutil"
"github.com/docker/docker/pkg/testutil/golden"
"github.com/stretchr/testify/assert"
)
@ -36,10 +36,11 @@ func TestSecretListErrors(t *testing.T) {
},
}
for _, tc := range testCases {
buf := new(bytes.Buffer)
cmd := newSecretListCommand(
test.NewFakeCli(&fakeClient{
test.NewFakeCliWithOutput(&fakeClient{
secretListFunc: tc.secretListFunc,
}),
}, buf),
)
cmd.SetArgs(tc.args)
cmd.SetOutput(ioutil.Discard)
@ -49,7 +50,7 @@ func TestSecretListErrors(t *testing.T) {
func TestSecretList(t *testing.T) {
buf := new(bytes.Buffer)
cli := test.NewFakeCli(&fakeClient{
cli := test.NewFakeCliWithOutput(&fakeClient{
secretListFunc: func(options types.SecretListOptions) ([]swarm.Secret, error) {
return []swarm.Secret{
*Secret(SecretID("ID-foo"),
@ -66,15 +67,18 @@ func TestSecretList(t *testing.T) {
),
}, nil
},
})
}, buf)
cmd := newSecretListCommand(cli)
cmd.SetOutput(buf)
assert.NoError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "secret-list.golden")
actual := buf.String()
expected := golden.Get(t, []byte(actual), "secret-list.golden")
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
}
func TestSecretListWithQuietOption(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
buf := new(bytes.Buffer)
cli := test.NewFakeCliWithOutput(&fakeClient{
secretListFunc: func(options types.SecretListOptions) ([]swarm.Secret, error) {
return []swarm.Secret{
*Secret(SecretID("ID-foo"), SecretName("foo")),
@ -83,15 +87,18 @@ func TestSecretListWithQuietOption(t *testing.T) {
})),
}, nil
},
})
}, buf)
cmd := newSecretListCommand(cli)
cmd.Flags().Set("quiet", "true")
assert.NoError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "secret-list-with-quiet-option.golden")
actual := buf.String()
expected := golden.Get(t, []byte(actual), "secret-list-with-quiet-option.golden")
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
}
func TestSecretListWithConfigFormat(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
buf := new(bytes.Buffer)
cli := test.NewFakeCliWithOutput(&fakeClient{
secretListFunc: func(options types.SecretListOptions) ([]swarm.Secret, error) {
return []swarm.Secret{
*Secret(SecretID("ID-foo"), SecretName("foo")),
@ -100,17 +107,20 @@ func TestSecretListWithConfigFormat(t *testing.T) {
})),
}, nil
},
})
}, buf)
cli.SetConfigFile(&configfile.ConfigFile{
SecretFormat: "{{ .Name }} {{ .Labels }}",
})
cmd := newSecretListCommand(cli)
assert.NoError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "secret-list-with-config-format.golden")
actual := buf.String()
expected := golden.Get(t, []byte(actual), "secret-list-with-config-format.golden")
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
}
func TestSecretListWithFormat(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
buf := new(bytes.Buffer)
cli := test.NewFakeCliWithOutput(&fakeClient{
secretListFunc: func(options types.SecretListOptions) ([]swarm.Secret, error) {
return []swarm.Secret{
*Secret(SecretID("ID-foo"), SecretName("foo")),
@ -119,15 +129,18 @@ func TestSecretListWithFormat(t *testing.T) {
})),
}, nil
},
})
}, buf)
cmd := newSecretListCommand(cli)
cmd.Flags().Set("format", "{{ .Name }} {{ .Labels }}")
assert.NoError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "secret-list-with-format.golden")
actual := buf.String()
expected := golden.Get(t, []byte(actual), "secret-list-with-format.golden")
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
}
func TestSecretListWithFilter(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
buf := new(bytes.Buffer)
cli := test.NewFakeCliWithOutput(&fakeClient{
secretListFunc: func(options types.SecretListOptions) ([]swarm.Secret, error) {
assert.Equal(t, "foo", options.Filters.Get("name")[0], "foo")
assert.Equal(t, "lbl1=Label-bar", options.Filters.Get("label")[0])
@ -146,10 +159,12 @@ func TestSecretListWithFilter(t *testing.T) {
),
}, nil
},
})
}, buf)
cmd := newSecretListCommand(cli)
cmd.Flags().Set("filter", "name=foo")
cmd.Flags().Set("filter", "label=lbl1=Label-bar")
assert.NoError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "secret-list-with-filter.golden")
actual := buf.String()
expected := golden.Get(t, []byte(actual), "secret-list-with-filter.golden")
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
}

View File

@ -1,12 +1,13 @@
package secret
import (
"bytes"
"io/ioutil"
"strings"
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/testutil"
"github.com/docker/cli/cli/internal/test"
"github.com/docker/docker/pkg/testutil"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
)
@ -19,7 +20,7 @@ func TestSecretRemoveErrors(t *testing.T) {
}{
{
args: []string{},
expectedError: "requires at least 1 argument.",
expectedError: "requires at least 1 argument(s).",
},
{
args: []string{"foo"},
@ -30,10 +31,11 @@ func TestSecretRemoveErrors(t *testing.T) {
},
}
for _, tc := range testCases {
buf := new(bytes.Buffer)
cmd := newSecretRemoveCommand(
test.NewFakeCli(&fakeClient{
test.NewFakeCliWithOutput(&fakeClient{
secretRemoveFunc: tc.secretRemoveFunc,
}),
}, buf),
)
cmd.SetArgs(tc.args)
cmd.SetOutput(ioutil.Discard)
@ -43,25 +45,27 @@ func TestSecretRemoveErrors(t *testing.T) {
func TestSecretRemoveWithName(t *testing.T) {
names := []string{"foo", "bar"}
buf := new(bytes.Buffer)
var removedSecrets []string
cli := test.NewFakeCli(&fakeClient{
cli := test.NewFakeCliWithOutput(&fakeClient{
secretRemoveFunc: func(name string) error {
removedSecrets = append(removedSecrets, name)
return nil
},
})
}, buf)
cmd := newSecretRemoveCommand(cli)
cmd.SetArgs(names)
assert.NoError(t, cmd.Execute())
assert.Equal(t, names, strings.Split(strings.TrimSpace(cli.OutBuffer().String()), "\n"))
assert.Equal(t, names, strings.Split(strings.TrimSpace(buf.String()), "\n"))
assert.Equal(t, names, removedSecrets)
}
func TestSecretRemoveContinueAfterError(t *testing.T) {
names := []string{"foo", "bar"}
buf := new(bytes.Buffer)
var removedSecrets []string
cli := test.NewFakeCli(&fakeClient{
cli := test.NewFakeCliWithOutput(&fakeClient{
secretRemoveFunc: func(name string) error {
removedSecrets = append(removedSecrets, name)
if name == "foo" {
@ -69,7 +73,7 @@ func TestSecretRemoveContinueAfterError(t *testing.T) {
}
return nil
},
})
}, buf)
cmd := newSecretRemoveCommand(cli)
cmd.SetOutput(ioutil.Discard)

View File

@ -1,6 +1,6 @@
ID: secretID
Name: secretName
ID: secretID
Name: secretName
Labels:
- lbl1=value1
Created at: 0001-01-01 00:00:00 +0000 utc
Updated at: 0001-01-01 00:00:00 +0000 utc
- lbl1=value1
Created at: 0001-01-01 00:00:00+0000 utc
Updated at: 0001-01-01 00:00:00+0000 utc

View File

@ -1,7 +1,7 @@
[
{
"ID": "ID-foo",
"Version": {},
"Version": {},
"CreatedAt": "0001-01-01T00:00:00Z",
"UpdatedAt": "0001-01-01T00:00:00Z",
"Spec": {
@ -13,7 +13,7 @@
},
{
"ID": "ID-bar",
"Version": {},
"Version": {},
"CreatedAt": "0001-01-01T00:00:00Z",
"UpdatedAt": "0001-01-01T00:00:00Z",
"Spec": {

View File

@ -1,9 +1,9 @@
[
{
"ID": "ID-foo",
"Version": {},
"CreatedAt": "0001-01-01T00:00:00Z",
"UpdatedAt": "0001-01-01T00:00:00Z",
"Version": {},
"CreatedAt": "0001-01-01T00:00:00Z",
"UpdatedAt": "0001-01-01T00:00:00Z",
"Spec": {
"Name": "foo",
"Labels": null

View File

@ -1,2 +1,2 @@
foo
foo
bar label=label-bar

View File

@ -1,2 +1,2 @@
foo
foo
bar label=label-bar

View File

@ -1,64 +0,0 @@
package service
import (
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/client"
"golang.org/x/net/context"
// Import builders to get the builder function as package function
. "github.com/docker/cli/internal/test/builders"
)
type fakeClient struct {
client.Client
serviceInspectWithRawFunc func(ctx context.Context, serviceID string, options types.ServiceInspectOptions) (swarm.Service, []byte, error)
serviceUpdateFunc func(ctx context.Context, serviceID string, version swarm.Version, service swarm.ServiceSpec, options types.ServiceUpdateOptions) (types.ServiceUpdateResponse, error)
serviceListFunc func(context.Context, types.ServiceListOptions) ([]swarm.Service, error)
infoFunc func(ctx context.Context) (types.Info, error)
}
func (f *fakeClient) NodeList(ctx context.Context, options types.NodeListOptions) ([]swarm.Node, error) {
return nil, nil
}
func (f *fakeClient) TaskList(ctx context.Context, options types.TaskListOptions) ([]swarm.Task, error) {
return nil, nil
}
func (f *fakeClient) ServiceInspectWithRaw(ctx context.Context, serviceID string, options types.ServiceInspectOptions) (swarm.Service, []byte, error) {
if f.serviceInspectWithRawFunc != nil {
return f.serviceInspectWithRawFunc(ctx, serviceID, options)
}
return *Service(ServiceID(serviceID)), []byte{}, nil
}
func (f *fakeClient) ServiceList(ctx context.Context, options types.ServiceListOptions) ([]swarm.Service, error) {
if f.serviceListFunc != nil {
return f.serviceListFunc(ctx, options)
}
return nil, nil
}
func (f *fakeClient) ServiceUpdate(ctx context.Context, serviceID string, version swarm.Version, service swarm.ServiceSpec, options types.ServiceUpdateOptions) (types.ServiceUpdateResponse, error) {
if f.serviceUpdateFunc != nil {
return f.serviceUpdateFunc(ctx, serviceID, version, service, options)
}
return types.ServiceUpdateResponse{}, nil
}
func (f *fakeClient) Info(ctx context.Context) (types.Info, error) {
if f.infoFunc == nil {
return types.Info{}, nil
}
return f.infoFunc(ctx)
}
func newService(id string, name string) swarm.Service {
return swarm.Service{
ID: id,
Spec: swarm.ServiceSpec{Annotations: swarm.Annotations{Name: name}},
}
}

View File

@ -26,7 +26,6 @@ func NewServiceCommand(dockerCli *command.DockerCli) *cobra.Command {
newScaleCommand(dockerCli),
newUpdateCommand(dockerCli),
newLogsCommand(dockerCli),
newRollbackCommand(dockerCli),
)
return cmd
}

View File

@ -12,7 +12,7 @@ import (
"golang.org/x/net/context"
)
func newCreateCommand(dockerCli command.Cli) *cobra.Command {
func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command {
opts := newServiceOptions()
cmd := &cobra.Command{
@ -62,7 +62,7 @@ func newCreateCommand(dockerCli command.Cli) *cobra.Command {
return cmd
}
func runCreate(dockerCli command.Cli, flags *pflag.FlagSet, opts *serviceOptions) error {
func runCreate(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts *serviceOptions) error {
apiClient := dockerCli.Client()
createOpts := types.ServiceCreateOptions{}

View File

@ -15,7 +15,7 @@ import (
// waitOnService waits for the service to converge. It outputs a progress bar,
// if appropriate based on the CLI flags.
func waitOnService(ctx context.Context, dockerCli command.Cli, serviceID string, quiet bool) error {
func waitOnService(ctx context.Context, dockerCli *command.DockerCli, serviceID string, quiet bool) error {
errChan := make(chan error, 1)
pipeReader, pipeWriter := io.Pipe()

View File

@ -20,7 +20,7 @@ type inspectOptions struct {
pretty bool
}
func newInspectCommand(dockerCli command.Cli) *cobra.Command {
func newInspectCommand(dockerCli *command.DockerCli) *cobra.Command {
var opts inspectOptions
cmd := &cobra.Command{
@ -43,7 +43,7 @@ func newInspectCommand(dockerCli command.Cli) *cobra.Command {
return cmd
}
func runInspect(dockerCli command.Cli, opts inspectOptions) error {
func runInspect(dockerCli *command.DockerCli, opts inspectOptions) error {
client := dockerCli.Client()
ctx := context.Background()

View File

@ -2,9 +2,6 @@ package service
import (
"fmt"
"sort"
"vbom.ml/util/sortorder"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
@ -23,7 +20,7 @@ type listOptions struct {
filter opts.FilterOpt
}
func newListCommand(dockerCli command.Cli) *cobra.Command {
func newListCommand(dockerCli *command.DockerCli) *cobra.Command {
options := listOptions{filter: opts.NewFilterOpt()}
cmd := &cobra.Command{
@ -44,13 +41,7 @@ func newListCommand(dockerCli command.Cli) *cobra.Command {
return cmd
}
type byName []swarm.Service
func (n byName) Len() int { return len(n) }
func (n byName) Swap(i, j int) { n[i], n[j] = n[j], n[i] }
func (n byName) Less(i, j int) bool { return sortorder.NaturalLess(n[i].Spec.Name, n[j].Spec.Name) }
func runList(dockerCli command.Cli, options listOptions) error {
func runList(dockerCli *command.DockerCli, options listOptions) error {
ctx := context.Background()
client := dockerCli.Client()
@ -60,7 +51,6 @@ func runList(dockerCli command.Cli, options listOptions) error {
return err
}
sort.Sort(byName(services))
info := map[string]formatter.ServiceListInfo{}
if len(services) > 0 && !options.quiet {
// only non-empty services and not quiet, should we call TaskList and NodeList api

View File

@ -1,28 +0,0 @@
package service
import (
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/swarm"
"github.com/gotestyourself/gotestyourself/golden"
"github.com/stretchr/testify/assert"
"golang.org/x/net/context"
)
func TestServiceListOrder(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
serviceListFunc: func(ctx context.Context, options types.ServiceListOptions) ([]swarm.Service, error) {
return []swarm.Service{
newService("a57dbe8", "service-1-foo"),
newService("a57dbdd", "service-10-foo"),
newService("aaaaaaa", "service-2-foo"),
}, nil
},
})
cmd := newListCommand(cli)
cmd.Flags().Set("format", "{{.Name}}")
assert.NoError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "service-list-sort.golden")
}

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