Compare commits

..

66 Commits

Author SHA1 Message Date
afdb6d44a8 Merge pull request #253 from andrewhsu/v
[17.09] bump version 17.09.0-ce
2017-09-26 15:24:58 -07:00
3b0f381088 Merge pull request #252 from andrewhsu/cl
[17.09] update changelog for 17.09.0-ce release
2017-09-26 15:24:36 -07:00
09d58a6cc0 update changelog for 17.09.0-ce release
Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
2017-09-26 15:21:58 -07:00
8056485bad bump version 17.09.0-ce
Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
2017-09-22 09:13:41 -07:00
2357fb28b5 Merge pull request #243 from andrewhsu/rc3
[17.09] bump version to 17.09.0-ce-rc3
2017-09-20 18:12:40 -07:00
9661f00ed4 Merge pull request #244 from andrewhsu/cl
[17.09] update changelog for 17.09.0-ce-rc3
2017-09-20 18:12:19 -07:00
db97c3db91 Merge pull request #249 from andrewhsu/hcsshim
[17.09] vndr Microsoft/hcsshim to v0.6.5
2017-09-20 17:47:21 -07:00
fe19ba678a Merge pull request #239 from thaJeztah/17.09-fix-relabel-config-files
[17.09] Relabel config files.
2017-09-20 17:36:02 -07:00
d86b81fcce Merge pull request #247 from thaJeztah/17.09-may_detach_mount
[17.09] back port Automatically set `may_detach_mounts=1` on startup
2017-09-20 17:24:41 -07:00
0a7586971f Merge pull request #245 from thaJeztah/17.09-close_overlay2_pipe
[17.09] back port Close pipe if mountFrom failed.
2017-09-20 17:20:25 -07:00
5fd57722d1 Merge pull request #233 from thaJeztah/backport-fixed-raw-splunk-logger
[17.09] Fixed `raw` mode splunk logger
2017-09-20 17:18:18 -07:00
4856a9ec15 Merge pull request #242 from vieux/bump_swarmkit_vendor
[17.09] vndr docker/swarmkit to dcd1f2d for snapshot size fix
2017-09-20 17:13:43 -07:00
6a80ba3b8a Merge pull request #241 from thaJeztah/17.09-fix_selinux_with_mount_api
[17.09] Set selinux label on local volumes from mounts API
2017-09-20 17:09:11 -07:00
508a2630a3 Merge pull request #236 from thaJeztah/17.09-fix-secrets-and-configs-idempotence
[17.09] sort secrets and configs to ensure idempotence
2017-09-20 17:08:07 -07:00
1ae6ae93c4 Merge pull request #231 from thaJeztah/backport-remove-cors-headers-flag
[17.09] Backport remove cors headers flag
2017-09-20 17:07:44 -07:00
ccdf90d524 Merge pull request #237 from thaJeztah/17.09-more-yaml-information
[17.09] Add more information to the generated YAML for documentation
2017-09-20 16:33:29 -07:00
79ca5680b8 Merge pull request #232 from thaJeztah/backport-fix-system-df
[17.09] Fix variable shadowing causing LayersSize to be reported as 0
2017-09-20 13:56:23 -07:00
1f4bf6c347 vndr Microsoft/hcsshim to v0.6.5
Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
2017-09-20 13:16:35 -07:00
0f16d9b90a Automatically set may_detach_mounts=1 on startup
This is kernel config available in RHEL7.4 based kernels that enables
mountpoint removal where the mountpoint exists in other namespaces.
In particular this is important for making this pattern work:

```
umount -l /some/path
rm -r /some/path
```

Where `/some/path` exists in another mount namespace.
Setting this value will prevent `device or resource busy` errors when
attempting to the removal of `/some/path` in the example.

This setting is the default, and non-configurable, on upstream kernels
since 3.15.

Signed-off-by: Brian Goff <cpuguy83@gmail.com>
(cherry picked from commit 83c2152de503012195bd26069fd8fbd2dea4b32f)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2017-09-20 20:35:33 +02:00
a825d7bfdf Close pipe if mountFrom failed.
Signed-off-by: Shukui Yang <yangshukui@huawei.com>
(cherry picked from commit 9f38923901352459bb621d0b3587a6517e67eeb3)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2017-09-20 19:32:44 +02:00
af4a1fc87e update changelog for 17.09.0-ce-rc3
Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
2017-09-19 20:34:25 -07:00
a7f4500a2c bump version to 17.09.0-ce-rc3
Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
2017-09-19 20:31:48 -07:00
5fb8e37ec7 bump swarmkit to dcd1f2d56afc08827d060fdb8ad222b00b1b6000
Signed-off-by: Victor Vieux <victorvieux@gmail.com>
2017-09-19 16:53:46 -07:00
da6e49e1f7 Set selinux label on local volumes from mounts API
When using a volume via the `Binds` API, a shared selinux label is
automatically set.
The `Mounts` API is not setting this, which makes volumes specified via
the mounts API useless when selinux is enabled.

This fix adopts the same selinux label for volumes on the mounts API as on
binds.
Note in the case of both the `Binds` API and the `Mounts` API, the
selinux label is only applied when the volume driver is the `local`
driver.

Signed-off-by: Brian Goff <cpuguy83@gmail.com>
(cherry picked from commit 5bbf5cc671ec8007bf8e0416799fff01d6a79b7e)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2017-09-19 21:57:56 +02:00
fc12f83f79 Some cleanup of mount create API test
Signed-off-by: Daniel Nephin <dnephin@docker.com>
(cherry picked from commit 58b96aced87b33c4175fa5d3422289f763ab599d)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2017-09-19 21:57:46 +02:00
3d9fc736e9 Relabel config files.
Without relabel these files, SELinux-enabled containers will show
"permission denied" errors for configuration files mounted with
`docker server create ... --config ... ...`.

Signed-off-by: Wenxuan Zhao <viz@linux.com>
(cherry picked from commit 472c03a8c364090afb88258b3dd9748183c29d05)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2017-09-19 17:53:50 +02:00
390c0782bc Add more information about commands to generated YAML docs
This patch adds aditional information about commands to the YAML files
that are generated for the reference documentation.

The following fields are added for each command:

Property          | Type      | Description
------------------|-----------|---------------------------------------------------------------------------------------
deprecated        | Boolean   | Indicates if the command is marked deprecated
min_api_version   | String    | The API version required to use this command (e.g. "1.23")
experimental      | Boolean   | Indicates if the command requires the daemon to run with experimental features enabled

For example (taken from the experimental `docker checkpoint create` command):

    command: docker checkpoint create
    short: Create a checkpoint from a running container
    long: Create a checkpoint from a running container
    usage: docker checkpoint create [OPTIONS] CONTAINER CHECKPOINT
    pname: docker checkpoint
    plink: docker_checkpoint.yaml
    options:
    - option: checkpoint-dir
      value_type: string
      description: Use a custom checkpoint storage directory
      deprecated: false
      experimental: false
    - option: leave-running
      value_type: bool
      default_value: "false"
      description: Leave the container running after checkpoint
      deprecated: false
      experimental: false
    deprecated: false
    min_api_version: "1.25"
    experimental: true

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>

(cherry picked from commit 1f48e75c5c)

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2017-09-19 14:17:58 +02:00
f238e40c68 Add more information about command flags to generated YAML docs
This patch adds aditional information about command flags to the YAML files
that are generated for the reference documentation.

The following fields are added for each flag:

Property          | Type      | Description
------------------|-----------|---------------------------------------------------------------------------------------
value_type        | String    | The "type" of value to be passed to this flag (e.g., `uint64`, `list`)
deprecated        | Boolean   | Indicates if the flag is marked deprecated
min_api_version   | String    | The API version required to use this flag (e.g. "1.23")
experimental      | Boolean   | Indicates if the flag requires the daemon to run with experimental features enabled

For example (taken from the `docker image build` command):

    - option: security-opt
      value_type: stringSlice
      default_value: '[]'
      description: Security options
      deprecated: false
      experimental: false
    - option: shm-size
      value_type: bytes
      default_value: "0"
      description: Size of /dev/shm
      deprecated: false
      experimental: false
    - option: squash
      value_type: bool
      default_value: "false"
      description: Squash newly built layers into a single new layer
      deprecated: false
      min_api_version: "1.25"
      experimental: true

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>

(cherry picked from commit a8ba6f93d9)

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2017-09-19 14:17:51 +02:00
2a4728f860 sort secrets and configs to ensure idempotence
`docker stack deploy` keeps restarting services it doesn't need to (no changes)
because the entries' order gets randomized at some previous (de)serialization.
Maybe it would be worth looking into this at a higher level and ensure
all (de)serialization happens in an ordered collection.

This quick fix sorts secrets and configs (in place, mutably) which ensures the
same order for each run.

Based on
https://github.com/moby/moby/pull/30506

Fixes
https://github.com/moby/moby/issues/34746

Signed-off-by: Peter Nagy <xificurC@gmail.com>
(cherry picked from commit 27e8bdf32b)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2017-09-19 11:21:42 +02:00
1ad05ae326 Fixed raw mode splunk logger
Splunk HEC does not accept log events with an empty string or a
whitespace-only string.

Signed-off-by: Florian Noeding <florian@noeding.com>
(cherry picked from commit 5f6d6a5093a4db799f9c1a6bb82eed1eea13ec0c)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2017-09-19 11:00:53 +02:00
393d90ba3d Fix variable shadowing causing LayersSize to be reported as 0
Signed-off-by: Cezar Sa Espinola <cezarsa@gmail.com>
(cherry picked from commit 313bc1e339fa4292d9ef5bc74acc82436eab7e1e)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2017-09-19 10:16:52 +02:00
2a3140fcb7 Update deprecated.md for "api-enable-cors"
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 7c69bf1d24)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2017-09-18 21:50:51 +02:00
66a5055240 Remove deprecated --enable-api-cors flag
The `--enable-api-cors` flag was deprecated in f3dd2db4ff7de1399a49af928cd3eae4fccf8764,
and marked for removal in docker 17.09 through 85f92ef3590b386ea17e3948262725a2d3ce4db5.

This patch removes the deprecated flag.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 7d4eab554379524c2421a7ecd3319d1d087d1de2)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2017-09-18 21:50:42 +02:00
363a3e75cd Merge pull request #229 from andrewhsu/static
[17.09] backport fix to get static builds going
2017-09-13 17:57:50 -07:00
22cec742b1 Fix fetching LVM2 sources
Version 2.02.173 has disappeared, let's revert back to latest stable
one.

https://github.com/moby/moby/issues/34843

Signed-off-by: Kir Kolyshkin <kolyshkin@gmail.com>
(cherry picked from commit a436d8a634392f9e82b6930a560d56900d887ce7)
Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
2017-09-13 17:05:37 -07:00
17db25d098 Merge pull request #227 from andrewhsu/rc2
[17.09] bump version to 17.09.0-ce-rc2
2017-09-13 14:05:25 -07:00
3e2dc53d19 Merge pull request #228 from andrewhsu/cl
[17.09] update changelog for 17.09.0-ce-rc2
2017-09-13 14:05:05 -07:00
efac66c62b Merge pull request #221 from vieux/updatecli
[17.09] cli updated vendoring
2017-09-13 14:00:13 -07:00
9e5777a749 LCOW: Spot fix for multi-os image pulling
Signed-off-by: John Howard <jhoward@microsoft.com>
(cherry picked from commit b291f5a31728f7ff6386bb37f15e7c0885d3b2a7)
Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
2017-09-13 12:38:48 -07:00
c6e21043d8 Use a local copy of ParseLogDetails
It's being removed from client/

Signed-off-by: Daniel Nephin <dnephin@docker.com>
(cherry picked from commit a747389bf4)
Signed-off-by: Victor Vieux <victorvieux@gmail.com>
2017-09-13 10:46:14 -07:00
7034626ac6 vndr github.com/docker/docker to 84144a8 to fix stuff
Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
(cherry picked from commit 850b46e67c)
Signed-off-by: Victor Vieux <victorvieux@gmail.com>
2017-09-12 17:53:14 -07:00
1fa0e54ee7 remove unused vndr github.com/docker/libtrust
Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
(cherry picked from commit 536da4a07a)
Signed-off-by: Victor Vieux <victorvieux@gmail.com>
2017-09-12 16:02:43 -07:00
5f31b291fb vndr golang.org/x/sys to 07c1829
To satisfy requirements of the vndr of docker/docker

Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
(cherry picked from commit 50696bbf72)
Signed-off-by: Victor Vieux <victorvieux@gmail.com>
2017-09-12 16:02:36 -07:00
172695debf vndr docker/docker to ea220e7 to bring in fix for arm
Primarily to bring in fix for "Clear Architecture field in platform
constraint for arm architectures".

Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
(cherry picked from commit ef027b6d72)
Signed-off-by: Victor Vieux <victorvieux@gmail.com>
2017-09-12 16:02:29 -07:00
49f307c88f Revert "Add skip for known failure"
This reverts commit b33e8d49d4.

Signed-off-by: Victor Vieux <victorvieux@gmail.com>
2017-09-12 15:52:22 -07:00
e6067b8038 Remove unused depdendencies from vendor
Signed-off-by: Daniel Nephin <dnephin@docker.com>
(cherry picked from commit 724f03bb23)
Signed-off-by: Victor Vieux <victorvieux@gmail.com>
2017-09-08 16:03:45 -07:00
49c9ca25c0 updated vendoring
Signed-off-by: Simon Ferquel <simon.ferquel@docker.com>
(cherry picked from commit a0113c3a44)
Signed-off-by: Victor Vieux <victorvieux@gmail.com>
2017-09-08 16:03:45 -07:00
21154792a5 Merge pull request #225 from seemethere/fix_metalinter_conversion_errors
[17.09] Fix metalinter conversion errors
2017-09-08 16:00:36 -07:00
be7f90619c Merge pull request #226 from seemethere/fix_test_run_environment
[17.09] Fix test run environment
2017-09-07 21:39:12 -07:00
4231d771bb update changelog for 17.09.0-ce-rc2
Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
2017-09-07 13:34:31 -07:00
b4a675c535 bump version to 17.09.0-ce-rc2
Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
2017-09-07 13:12:13 -07:00
dff328c8a6 Fix a bad assumption
If the empty variable happens to be sorted to the end of the list then TrimSpace()
would remove it. Instead only strip the single trailing newline.

Signed-off-by: Daniel Nephin <dnephin@docker.com>
(cherry picked from commit fff605c3b3557acf6bf793813d695fba59d7fa21)
Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
2017-09-07 11:40:46 -07:00
2b763fa480 overlay gd: fix build for 32-bit ARM
This commit reverts a hunk of commit 2f5f0af3f ("Add unconvert linter")
and adds a hint for unconvert linter to ignore excessive conversion as
it is required on 32-bit platforms (e.g. armhf).

The exact error on armhf is this:

	19:06:45 ---> Making bundle: dynbinary (in bundles/17.06.0-dev/dynbinary)
	19:06:48 Building: bundles/17.06.0-dev/dynbinary-daemon/dockerd-17.06.0-dev
	19:10:58 # github.com/docker/docker/daemon/graphdriver/overlay
	19:10:58 daemon/graphdriver/overlay/copy.go:161: cannot use stat.Atim.Sec (type int32) as type int64 in argument to time.Unix
	19:10:58 daemon/graphdriver/overlay/copy.go:161: cannot use stat.Atim.Nsec (type int32) as type int64 in argument to time.Unix
	19:10:58 daemon/graphdriver/overlay/copy.go:162: cannot use stat.Mtim.Sec (type int32) as type int64 in argument to time.Unix
	19:10:58 daemon/graphdriver/overlay/copy.go:162: cannot use stat.Mtim.Nsec (type int32) as type int64 in argument to time.Unix

Signed-off-by: Kir Kolyshkin <kolyshkin@gmail.com>
(cherry picked from commit 0120303d8ea4a1d9f8920ac4dba80521072e8536)
Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
2017-09-06 16:07:27 -07:00
8227e4e739 Fix 'make all' for other platforms
Since commit d7e2c4ce7 ("Use gometalinter for linting") command
"make all" fails on all the non-default platforms (i.e. ARMs, PPC, and
s390) in this way:

	# make all
	...
	Congratulations!  All commits are properly signed with the DCO!
	/go/src/github.com/docker/docker/hack/validate/gometalinter: line 6: gometalinter: command not found
	Makefile:105: recipe for target 'all' failed
	make: *** [all] Error 127

Make sure gometalinter is installed for those platforms

Signed-off-by: Kir Kolyshkin <kolyshkin@gmail.com>
(cherry picked from commit 0c0047e67f8ee73f73465ac60ccebbd16260ab46)
Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
2017-09-06 16:07:27 -07:00
a0a9bbd3f2 re-enable some conversion for darwin-amd64 platform
Signed-off-by: Simon Ferquel <simon.ferquel@docker.com>
(cherry picked from commit 7c9e64a2e1891bf1020d9312a18741c734fa1cd6)
Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
2017-09-06 16:06:15 -07:00
ae21824df8 Merge pull request #223 from seemethere/bump_17090rc1
[17.09] bump version to 17.09.0-ce-rc1
2017-09-06 15:22:01 -07:00
ec4c78a39b Merge pull request #214 from vieux/17.09-changelog
[17.09] add 17.09 changelog
2017-09-06 15:21:47 -07:00
aa56783035 Merge pull request #224 from seemethere/fix_integration_tests_1709
[17.09] Changes error check form NotNil to IsNil
2017-09-06 15:18:32 -07:00
b33e8d49d4 Add skip for known failure
Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
2017-09-06 13:41:55 -07:00
5d1587e61e Changes error check form NotNil to IsNil
rmi -f always returns a 0 exit code so these tests needed to be changed
accordingly.

Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
2017-09-06 11:30:29 -07:00
8574cd6b8a bump version to 17.09.0-ce-rc1
Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
2017-09-05 17:06:13 -07:00
54ddbdea25 Merge pull request #222 from vieux/fix_inspect
[17.09] Fix integration suite and propagate failures
2017-09-05 16:49:27 -07:00
efd69761fc Fix integration suite and propagate failures
Failures from the integration suite were not propagating to the outter shell
for some reason. Handle the failure with an if exit 1.

Signed-off-by: Daniel Nephin <dnephin@docker.com>
(cherry picked from commit 96707bc600747257e82917ca079fa5006d636b2c)
Signed-off-by: Victor Vieux <victorvieux@gmail.com>
2017-09-05 14:58:05 -07:00
c973c430a9 Merge pull request #220 from vieux/fix_nano_tests
[17.09] backport: force inspect test format
2017-09-05 14:43:58 -07:00
e3e65f6470 force inspect test format
Signed-off-by: Victor Vieux <victorvieux@gmail.com>
(cherry picked from commit 8e6567cb837e1c885de5146517557c7a5d8a5f17)
Signed-off-by: Victor Vieux <victorvieux@gmail.com>
2017-09-05 13:37:34 -07:00
f7ec4a69b3 add 17.09 changelog
Signed-off-by: Victor Vieux <victorvieux@gmail.com>
2017-09-01 10:32:17 -07:00
997 changed files with 16500 additions and 35065 deletions

1
.gitignore vendored
View File

@ -1 +0,0 @@
.helpers/

View File

@ -5,50 +5,66 @@ information on the list of deprecated flags and APIs please have a look at
https://docs.docker.com/engine/deprecated/ where target removal dates can also
be found.
## 17.10.0-ce (2017-10-17)
IMPORTANT: Starting with this release, `docker service create`, `docker service update`,
`docker service scale` and `docker service rollback` use non-detached mode as default,
use `--detach` to keep the old behaviour.
## 17.09.0-ce (2017-09-26)
### Builder
* Reset uid/gid to 0 in uploaded build context to share build cache with other clients [docker/cli#513](https://github.com/docker/cli/pull/513)
+ Add support for `ADD` urls without any sub path [moby/moby#34217](https://github.com/moby/moby/pull/34217)
+ Add `--chown` flag to `ADD/COPY` commands in Dockerfile [moby/moby#34263](https://github.com/moby/moby/pull/34263)
* Fix cloning unneeded files while building from git repositories [moby/moby#33704](https://github.com/moby/moby/pull/33704)
### Client
* Move output of `docker stack rm` to stdout [docker/cli#491](https://github.com/docker/cli/pull/491)
* Use natural sort secrets and configs in cli [docker/cli#307](https://github.com/docker/cli/pull/307)
* Use non-detached mode as default for `docker service` commands [docker/cli#525](https://github.com/docker/cli/pull/525)
* Set APIVersion on the client, even when Ping fails [docker/cli#546](https://github.com/docker/cli/pull/546)
- Fix loader error with different build syntax in `docker stack deploy` [docker/cli#544](https://github.com/docker/cli/pull/544)
* Change the default output format for `docker container stats` to show `CONTAINER ID` and `NAME` [docker/cli#565](https://github.com/docker/cli/pull/565)
+ Add `--no-trunc` flag to `docker container stats` [docker/cli#565](https://github.com/docker/cli/pull/565)
+ Add experimental `docker trust`: `view`, `revoke`, `sign` subcommands [docker/cli#472](https://github.com/docker/cli/pull/472)
- Various doc and shell completion fixes [docker/cli#610](https://github.com/docker/cli/pull/610) [docker/cli#611](https://github.com/docker/cli/pull/611) [docker/cli#618](https://github.com/docker/cli/pull/618) [docker/cli#580](https://github.com/docker/cli/pull/580) [docker/cli#598](https://github.com/docker/cli/pull/698) [docker/cli#603](https://github.com/docker/cli/pull/603)
* Allow extension fields in the v3.4 version of the compose format [docker/cli#452](https://github.com/docker/cli/pull/452)
* Make compose file allow to specify names for non-external volume [docker/cli#306](https://github.com/docker/cli/pull/306)
* Support `--compose-file -` as stdin [docker/cli#347](https://github.com/docker/cli/pull/347)
* Support `start_period` for healthcheck in Docker Compose [docker/cli#475](https://github.com/docker/cli/pull/475)
+ Add support for `stop-signal` in docker stack commands [docker/cli#388](https://github.com/docker/cli/pull/388)
+ Add support for update order in compose deployments [docker/cli#360](https://github.com/docker/cli/pull/360)
+ Add ulimits to unsupported compose fields [docker/cli#482](https://github.com/docker/cli/pull/482)
+ Add `--format` to `docker-search` [docker/cli#440](https://github.com/docker/cli/pull/440)
* Show images digests when `{{.Digest}}` is in format [docker/cli#439](https://github.com/docker/cli/pull/439)
* Print output of `docker stack rm` on `stdout` instead of `stderr` [docker/cli#491](https://github.com/docker/cli/pull/491)
- Fix `docker history --format '{{json .}}'` printing human-readable timestamps instead of ISO8601 when `--human=true` [docker/cli#438](https://github.com/docker/cli/pull/438)
- Fix idempotence of `docker stack deploy` when secrets or configs are used [docker/cli#509](https://github.com/docker/cli/pull/509)
- Fix presentation of random host ports [docker/cli#404](https://github.com/docker/cli/pull/404)
- Fix redundant service restarts when service created with multiple secrets [moby/moby#34746](https://github.com/moby/moby/issues/34746)
### Logging
- Fix Splunk logger not transmitting log data when tag is empty and raw-mode is used [moby/moby#34520](https://github.com/moby/moby/pull/34520)
### Networking
* Enabling ILB/ELB on windows using per-node, per-network LB endpoint [moby/moby#34674](https://github.com/moby/moby/pull/34674)
* Overlay fix for transient IP reuse [docker/libnetwork#1935](https://github.com/docker/libnetwork/pull/1935)
* Serializing bitseq alloc [docker/libnetwork#1788](https://github.com/docker/libnetwork/pull/1788)
- Disable hostname lookup on chain exists check [docker/libnetwork#1974](https://github.com/docker/libnetwork/pull/1974)
+ Add the control plane MTU option in the daemon config [moby/moby#34103](https://github.com/moby/moby/pull/34103)
+ Add service virtual IP to sandbox's loopback address [docker/libnetwork#1877](https://github.com/docker/libnetwork/pull/1877)
### Runtime
* LCOW: Add UVM debugability by grabbing logs before tear-down [moby/moby#34846](https://github.com/moby/moby/pull/34846)
* LCOW: Prepare work for bind mounts [moby/moby#34258](https://github.com/moby/moby/pull/34258)
* LCOW: Support for docker cp, ADD/COPY on build [moby/moby#34252](https://github.com/moby/moby/pull/34252)
* LCOW: VHDX boot to readonly [moby/moby#34754](https://github.com/moby/moby/pull/34754)
* Volume: evaluate symlinks before relabeling mount source [moby/moby#34792](https://github.com/moby/moby/pull/34792)
- Fixing docker cp to allow new target file name in a host symlinked directory [moby/moby#31993](https://github.com/moby/moby/pull/31993)
+ Add support for Windows version filtering on pull [moby/moby#35090](https://github.com/moby/moby/pull/35090)
* Graphdriver: promote overlay2 over aufs [moby/moby#34430](https://github.com/moby/moby/pull/34430)
* LCOW: Additional flags for VHD boot [moby/moby#34451](https://github.com/moby/moby/pull/34451)
* LCOW: Don't block export [moby/moby#34448](https://github.com/moby/moby/pull/34448)
* LCOW: Dynamic sandbox management [moby/moby#34170](https://github.com/moby/moby/pull/34170)
* LCOW: Force Hyper-V Isolation [moby/moby#34468](https://github.com/moby/moby/pull/34468)
* LCOW: Move toolsScratchPath to /tmp [moby/moby#34396](https://github.com/moby/moby/pull/34396)
* LCOW: Remove hard-coding [moby/moby#34398](https://github.com/moby/moby/pull/34398)
* LCOW: WORKDIR correct handling [moby/moby#34405](https://github.com/moby/moby/pull/34405)
* Windows: named pipe mounts [moby/moby#33852](https://github.com/moby/moby/pull/33852)
- Fix "permission denied" errors when accessing volume with SELinux enforcing mode [moby/moby#34684](https://github.com/moby/moby/pull/34684)
- Fix layers size reported as `0` in `docker system df` [moby/moby#34826](https://github.com/moby/moby/pull/34826)
- Fix some "device or resource busy" errors when removing containers on RHEL 7.4 based kernels [moby/moby#34886](https://github.com/moby/moby/pull/34886)
### Swarm mode
* Produce an error if `docker swarm init --force-new-cluster` is executed on worker nodes [moby/moby#34881](https://github.com/moby/moby/pull/34881)
+ Add support for `.Node.Hostname` templating in swarm services [moby/moby#34686](https://github.com/moby/moby/pull/34686)
* Increase gRPC request timeout to 20 seconds for sending snapshots [docker/swarmkit#2391](https://github.com/docker/swarmkit/pull/2391)
- Do not filter nodes if logdriver is set to `none` [docker/swarmkit#2396](https://github.com/docker/swarmkit/pull/2396)
+ Adding ipam options to ipam driver requests [docker/swarmkit#2324](https://github.com/docker/swarmkit/pull/2324)
* Include whether the managers in the swarm are autolocked as part of `docker info` [docker/cli#471](https://github.com/docker/cli/pull/471)
+ Add 'docker service rollback' subcommand [docker/cli#205](https://github.com/docker/cli/pull/205)
- Fix managers failing to join if the gRPC snapshot is larger than 4MB [docker/swarmkit#2375](https://github.com/docker/swarmkit/pull/2375)
- Fix "permission denied" errors for configuration file in SELinux-enabled containers [moby/moby#34732](https://github.com/moby/moby/pull/34732)
- Fix services failing to deploy on ARM nodes [moby/moby#34021](https://github.com/moby/moby/pull/34021)
### Packaging
+ Build scripts for ppc64el on Ubuntu [docker/docker-ce-packaging#43](https://github.com/docker/docker-ce-packaging/pull/43)
### Deprecation
+ Remove deprecated `--enable-api-cors` daemon flag [moby/moby#34821](https://github.com/moby/moby/pull/34821)

View File

@ -1,56 +1,27 @@
CLI_DIR:=$(CURDIR)/components/cli
ENGINE_DIR:=$(CURDIR)/components/engine
PACKAGING_DIR:=$(CURDIR)/components/packaging
MOBY_COMPONENTS_SHA=f79265f1412af0a68aadd11e1d2f374446f3681b
MOBY_COMPONENTS_URL=https://raw.githubusercontent.com/shykes/moby-extras/$(MOBY_COMPONENTS_SHA)/cmd/moby-components
MOBY_COMPONENTS=.helpers/moby-components-$(MOBY_COMPONENTS_SHA)
VERSION=$(shell cat VERSION)
.PHONY: help
help: ## show make targets
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {sub("\\\\n",sprintf("\n%22c"," "), $$2);printf " \033[36m%-20s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
.PHONY: test-integration-cli
test-integration-cli: $(CLI_DIR)/build/docker ## test integration of cli and engine
$(MAKE) -C $(ENGINE_DIR) DOCKER_CLI_PATH=$< test-integration-cli
$(CLI_DIR)/build/docker:
$(MAKE) -C $(CLI_DIR) -f docker.Makefile build
.PHONY: deb
deb: ## build deb packages
$(MAKE) VERSION=$(VERSION) CLI_DIR=$(CLI_DIR) ENGINE_DIR=$(ENGINE_DIR) -C $(PACKAGING_DIR) deb
.PHONY: rpm
rpm: ## build rpm packages
$(MAKE) VERSION=$(VERSION) CLI_DIR=$(CLI_DIR) ENGINE_DIR=$(ENGINE_DIR) -C $(PACKAGING_DIR) rpm
.PHONY: static
static: ## build static packages
$(MAKE) VERSION=$(VERSION) CLI_DIR=$(CLI_DIR) ENGINE_DIR=$(ENGINE_DIR) -C $(PACKAGING_DIR) static
.PHONY: clean
clean: ## clean the build artifacts
-$(MAKE) -C $(CLI_DIR) clean
-$(MAKE) -C $(ENGINE_DIR) clean
-$(MAKE) -C $(PACKAGING_DIR) clean
$(MOBY_COMPONENTS):
mkdir -p .helpers
curl -fsSL $(MOBY_COMPONENTS_URL) > $(MOBY_COMPONENTS)
chmod +x $(MOBY_COMPONENTS)
.PHONY: update-components
update-components: update-components-cli update-components-engine update-components-packaging ## udpate components using moby extra tool
.PHONY: update-components-cli
update-components-cli: $(MOBY_COMPONENTS)
$(MOBY_COMPONENTS) update cli
.PHONY: update-components-engine
update-components-engine: $(MOBY_COMPONENTS)
$(MOBY_COMPONENTS) update engine
.PHONY: update-components-packaging
update-components-packaging: $(MOBY_COMPONENTS)
$(MOBY_COMPONENTS) update packaging

View File

@ -1 +1 @@
17.10.0-ce
17.09.0-ce

View File

@ -27,7 +27,6 @@
people = [
"aaronlehmann",
"albers",
"aluzzardi",
"anusha",
"cpuguy83",
@ -85,11 +84,6 @@
Email = "aaron.lehmann@docker.com"
GitHub = "aaronlehmann"
[people.albers]
Name = "Harald Albers"
Email = "github@albersweb.de"
GitHub = "albers"
[people.aluzzardi]
Name = "Andrea Luzzardi"
Email = "al@docker.com"

View File

@ -34,14 +34,6 @@ binary: ## build executable for Linux
cross: ## build executable for macOS and Windows
./scripts/build/cross
.PHONY: binary-windows
binary-windows: ## build executable for Windows
./scripts/build/windows
.PHONY: binary-osx
binary-osx: ## build executable for macOS
./scripts/build/osx
.PHONY: dynbinary
dynbinary: ## build dynamically linked binary
./scripts/build/dynbinary

View File

@ -1,4 +1,4 @@
[![build status](https://circleci.com/gh/docker/cli.svg?style=shield)](https://circleci.com/gh/docker/cli/tree/master) [![Build Status](https://jenkins.dockerproject.org/job/docker/job/cli/job/master/badge/icon)](https://jenkins.dockerproject.org/job/docker/job/cli/job/master/)
[![build status](https://circleci.com/gh/docker/cli.svg?style=shield)](https://circleci.com/gh/docker/cli/tree/master)
docker/cli
==========

View File

@ -1 +1 @@
17.10.0-ce
17.09.0-ce

View File

@ -12,14 +12,11 @@ import (
cliconfig "github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/config/configfile"
cliflags "github.com/docker/cli/cli/flags"
"github.com/docker/cli/cli/trust"
dopts "github.com/docker/cli/opts"
"github.com/docker/docker/api"
"github.com/docker/docker/client"
"github.com/docker/go-connections/sockets"
"github.com/docker/go-connections/tlsconfig"
"github.com/docker/notary"
notaryclient "github.com/docker/notary/client"
"github.com/docker/notary/passphrase"
"github.com/pkg/errors"
"github.com/spf13/cobra"
@ -42,7 +39,6 @@ type Cli interface {
SetIn(in *InStream)
ConfigFile() *configfile.ConfigFile
ServerInfo() ServerInfo
NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (notaryclient.Repository, error)
}
// DockerCli is an instance the docker command line client.
@ -115,58 +111,44 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions) error {
var err error
cli.client, err = NewAPIClientFromFlags(opts.Common, cli.configFile)
if tlsconfig.IsErrEncryptedKey(err) {
var (
passwd string
giveup bool
)
passRetriever := passphrase.PromptRetrieverWithInOut(cli.In(), cli.Out(), nil)
newClient := func(password string) (client.APIClient, error) {
opts.Common.TLSOptions.Passphrase = password
return NewAPIClientFromFlags(opts.Common, cli.configFile)
for attempts := 0; tlsconfig.IsErrEncryptedKey(err); attempts++ {
// some code and comments borrowed from notary/trustmanager/keystore.go
passwd, giveup, err = passRetriever("private", "encrypted TLS private", false, attempts)
// Check if the passphrase retriever got an error or if it is telling us to give up
if giveup || err != nil {
return errors.Wrap(err, "private key is encrypted, but could not get passphrase")
}
opts.Common.TLSOptions.Passphrase = passwd
cli.client, err = NewAPIClientFromFlags(opts.Common, cli.configFile)
}
cli.client, err = getClientWithPassword(passRetriever, newClient)
}
if err != nil {
return err
}
cli.initializeFromClient()
return nil
}
func (cli *DockerCli) initializeFromClient() {
cli.defaultVersion = cli.client.ClientVersion()
ping, err := cli.client.Ping(context.Background())
if err != nil {
if ping, err := cli.client.Ping(context.Background()); err == nil {
cli.server = ServerInfo{
HasExperimental: ping.Experimental,
OSType: ping.OSType,
}
cli.client.NegotiateAPIVersionPing(ping)
} else {
// Default to true if we fail to connect to daemon
cli.server = ServerInfo{HasExperimental: true}
if ping.APIVersion != "" {
cli.client.NegotiateAPIVersionPing(ping)
}
return
}
cli.server = ServerInfo{
HasExperimental: ping.Experimental,
OSType: ping.OSType,
}
cli.client.NegotiateAPIVersionPing(ping)
}
func getClientWithPassword(passRetriever notary.PassRetriever, newClient func(password string) (client.APIClient, error)) (client.APIClient, error) {
for attempts := 0; ; attempts++ {
passwd, giveup, err := passRetriever("private", "encrypted TLS private", false, attempts)
if giveup || err != nil {
return nil, errors.Wrap(err, "private key is encrypted, but could not get passphrase")
}
apiclient, err := newClient(passwd)
if !tlsconfig.IsErrEncryptedKey(err) {
return apiclient, err
}
}
}
// NotaryClient provides a Notary Repository to interact with signed metadata for an image
func (cli *DockerCli) NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (notaryclient.Repository, error) {
return trust.GetNotaryRepository(cli.In(), cli.Out(), UserAgent(), imgRefAndAuth.RepoInfo(), imgRefAndAuth.AuthConfig(), actions...)
return nil
}
// ServerInfo stores details about the supported features and platform of the

View File

@ -4,18 +4,12 @@ import (
"os"
"testing"
"crypto/x509"
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/cli/flags"
"github.com/docker/cli/internal/test/testutil"
"github.com/docker/docker/api"
"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/net/context"
)
func TestNewAPIClientFromFlags(t *testing.T) {
@ -49,7 +43,7 @@ func TestNewAPIClientFromFlagsWithAPIVersionFromEnv(t *testing.T) {
assert.Equal(t, customVersion, apiclient.ClientVersion())
}
// TODO: use gotestyourself/env.Patch
// 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))
@ -61,138 +55,3 @@ func patchEnvVariable(t *testing.T, key, value string) func() {
require.NoError(t, os.Setenv(key, oldValue))
}
}
type fakeClient struct {
client.Client
pingFunc func() (types.Ping, error)
version string
negotiated bool
}
func (c *fakeClient) Ping(_ context.Context) (types.Ping, error) {
return c.pingFunc()
}
func (c *fakeClient) ClientVersion() string {
return c.version
}
func (c *fakeClient) NegotiateAPIVersionPing(types.Ping) {
c.negotiated = true
}
func TestInitializeFromClient(t *testing.T) {
defaultVersion := "v1.55"
var testcases = []struct {
doc string
pingFunc func() (types.Ping, error)
expectedServer ServerInfo
negotiated bool
}{
{
doc: "successful ping",
pingFunc: func() (types.Ping, error) {
return types.Ping{Experimental: true, OSType: "linux", APIVersion: "v1.30"}, nil
},
expectedServer: ServerInfo{HasExperimental: true, OSType: "linux"},
negotiated: true,
},
{
doc: "failed ping, no API version",
pingFunc: func() (types.Ping, error) {
return types.Ping{}, errors.New("failed")
},
expectedServer: ServerInfo{HasExperimental: true},
},
{
doc: "failed ping, with API version",
pingFunc: func() (types.Ping, error) {
return types.Ping{APIVersion: "v1.33"}, errors.New("failed")
},
expectedServer: ServerInfo{HasExperimental: true},
negotiated: true,
},
}
for _, testcase := range testcases {
t.Run(testcase.doc, func(t *testing.T) {
apiclient := &fakeClient{
pingFunc: testcase.pingFunc,
version: defaultVersion,
}
cli := &DockerCli{client: apiclient}
cli.initializeFromClient()
assert.Equal(t, defaultVersion, cli.defaultVersion)
assert.Equal(t, testcase.expectedServer, cli.server)
assert.Equal(t, testcase.negotiated, apiclient.negotiated)
})
}
}
func TestGetClientWithPassword(t *testing.T) {
expected := "password"
var testcases = []struct {
doc string
password string
retrieverErr error
retrieverGiveup bool
newClientErr error
expectedErr string
}{
{
doc: "successful connect",
password: expected,
},
{
doc: "password retriever exhausted",
retrieverGiveup: true,
retrieverErr: errors.New("failed"),
expectedErr: "private key is encrypted, but could not get passphrase",
},
{
doc: "password retriever error",
retrieverErr: errors.New("failed"),
expectedErr: "failed",
},
{
doc: "newClient error",
newClientErr: errors.New("failed to connect"),
expectedErr: "failed to connect",
},
}
for _, testcase := range testcases {
t.Run(testcase.doc, func(t *testing.T) {
passRetriever := func(_, _ string, _ bool, attempts int) (passphrase string, giveup bool, err error) {
// Always return an invalid pass first to test iteration
switch attempts {
case 0:
return "something else", false, nil
default:
return testcase.password, testcase.retrieverGiveup, testcase.retrieverErr
}
}
newClient := func(currentPassword string) (client.APIClient, error) {
if testcase.newClientErr != nil {
return nil, testcase.newClientErr
}
if currentPassword == expected {
return &client.Client{}, nil
}
return &client.Client{}, x509.IncorrectPasswordError
}
_, err := getClientWithPassword(passRetriever, newClient)
if testcase.expectedErr != "" {
testutil.ErrorContains(t, err, testcase.expectedErr)
return
}
assert.NoError(t, err)
})
}
}

View File

@ -17,7 +17,6 @@ import (
"github.com/docker/cli/cli/command/stack"
"github.com/docker/cli/cli/command/swarm"
"github.com/docker/cli/cli/command/system"
"github.com/docker/cli/cli/command/trust"
"github.com/docker/cli/cli/command/volume"
"github.com/spf13/cobra"
)
@ -70,9 +69,6 @@ func AddCommands(cmd *cobra.Command, dockerCli *command.DockerCli) {
// swarm
swarm.NewSwarmCommand(dockerCli),
// trust
trust.NewTrustCommand(dockerCli),
// volume
volume.NewVolumeCommand(dockerCli),

View File

@ -1,27 +1,15 @@
package config
import (
"sort"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/cli/opts"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/swarm"
"github.com/spf13/cobra"
"golang.org/x/net/context"
"vbom.ml/util/sortorder"
)
type byConfigName []swarm.Config
func (r byConfigName) Len() int { return len(r) }
func (r byConfigName) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
func (r byConfigName) Less(i, j int) bool {
return sortorder.NaturalLess(r[i].Spec.Name, r[j].Spec.Name)
}
type listOptions struct {
quiet bool
format string
@ -67,8 +55,6 @@ func runConfigList(dockerCli command.Cli, options listOptions) error {
}
}
sort.Sort(byConfigName(configs))
configCtx := formatter.Context{
Output: dockerCli.Out(),
Format: formatter.NewConfigFormat(format, options.quiet),

View File

@ -50,20 +50,14 @@ func TestConfigList(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
configListFunc: func(options types.ConfigListOptions) ([]swarm.Config, error) {
return []swarm.Config{
*Config(ConfigID("ID-1-foo"),
ConfigName("1-foo"),
*Config(ConfigID("ID-foo"),
ConfigName("foo"),
ConfigVersion(swarm.Version{Index: 10}),
ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
),
*Config(ConfigID("ID-10-foo"),
ConfigName("10-foo"),
ConfigVersion(swarm.Version{Index: 11}),
ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
),
*Config(ConfigID("ID-2-foo"),
ConfigName("2-foo"),
*Config(ConfigID("ID-bar"),
ConfigName("bar"),
ConfigVersion(swarm.Version{Index: 11}),
ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
@ -72,8 +66,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-sort.golden")
golden.Assert(t, cli.OutBuffer().String(), "config-list.golden")
}
func TestConfigListWithQuietOption(t *testing.T) {

View File

@ -1,4 +0,0 @@
ID NAME CREATED UPDATED
ID-1-foo 1-foo 2 hours ago About an hour ago
ID-2-foo 2-foo 2 hours ago About an hour ago
ID-10-foo 10-foo 2 hours ago About an hour ago

View File

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

View File

@ -1,3 +1,3 @@
ID NAME CREATED UPDATED
ID-bar bar 2 hours ago About an hour ago
ID-foo foo 2 hours ago About an hour ago
ID-bar bar 2 hours ago About an hour ago

View File

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

View File

@ -1,2 +1,2 @@
ID-bar
ID-foo
ID-bar

View File

@ -0,0 +1,3 @@
ID NAME CREATED UPDATED
ID-foo foo 2 hours ago About an hour ago
ID-bar bar 2 hours ago About an hour ago

View File

@ -113,7 +113,7 @@ func resolveLocalPath(localPath string) (absPath string, err error) {
return
}
return archive.PreserveTrailingDotOrSeparator(absPath, localPath, filepath.Separator), nil
return archive.PreserveTrailingDotOrSeparator(absPath, localPath), nil
}
func copyFromContainer(ctx context.Context, dockerCli *command.DockerCli, srcContainer, srcPath, dstPath string, cpParam *cpConfig) (err error) {

View File

@ -10,6 +10,7 @@ import (
"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/sirupsen/logrus"
"github.com/spf13/cobra"
@ -105,6 +106,7 @@ func interactiveExec(ctx context.Context, dockerCli command.Cli, execConfig *typ
var (
out, stderr io.Writer
in io.ReadCloser
errCh chan error
)
if execConfig.AttachStdin {
@ -127,25 +129,19 @@ func interactiveExec(ctx context.Context, dockerCli command.Cli, execConfig *typ
return err
}
defer resp.Close()
errCh = promise.Go(func() error {
streamer := hijackedIOStreamer{
streams: dockerCli,
inputStream: in,
outputStream: out,
errorStream: stderr,
resp: resp,
tty: execConfig.Tty,
detachKeys: execConfig.DetachKeys,
}
errCh := make(chan error, 1)
go func() {
defer close(errCh)
errCh <- func() error {
streamer := hijackedIOStreamer{
streams: dockerCli,
inputStream: in,
outputStream: out,
errorStream: stderr,
resp: resp,
tty: execConfig.Tty,
detachKeys: execConfig.DetachKeys,
}
return streamer.stream(ctx)
}()
}()
return streamer.stream(ctx)
})
if execConfig.Tty && dockerCli.In().IsTerminal() {
if err := MonitorTtySize(ctx, dockerCli, execID, true); err != nil {

View File

@ -185,7 +185,6 @@ func setRawTerminal(streams command.Streams) error {
return streams.Out().SetRawTerminal()
}
// nolint: unparam
func restoreTerminal(streams command.Streams, in io.Closer) error {
streams.In().RestoreTerminal()
streams.Out().RestoreTerminal()

View File

@ -43,7 +43,6 @@ func TestValidateAttach(t *testing.T) {
}
}
// nolint: unparam
func parseRun(args []string) (*container.Config, *container.HostConfig, *networktypes.NetworkingConfig, error) {
flags := pflag.NewFlagSet("run", pflag.ContinueOnError)
flags.SetOutput(ioutil.Discard)

View File

@ -15,6 +15,7 @@ import (
"github.com/docker/cli/opts"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/pkg/promise"
"github.com/docker/docker/pkg/signal"
"github.com/docker/docker/pkg/term"
"github.com/pkg/errors"
@ -290,27 +291,22 @@ func attachContainer(
return nil, errAttach
}
ch := make(chan error, 1)
*errCh = ch
*errCh = promise.Go(func() error {
streamer := hijackedIOStreamer{
streams: dockerCli,
inputStream: in,
outputStream: out,
errorStream: cerr,
resp: resp,
tty: config.Tty,
detachKeys: options.DetachKeys,
}
go func() {
ch <- func() error {
streamer := hijackedIOStreamer{
streams: dockerCli,
inputStream: in,
outputStream: out,
errorStream: cerr,
resp: resp,
tty: config.Tty,
detachKeys: options.DetachKeys,
}
if errHijack := streamer.stream(ctx); errHijack != nil {
return errHijack
}
return errAttach
}()
}()
if errHijack := streamer.stream(ctx); errHijack != nil {
return errHijack
}
return errAttach
})
return resp.Close, nil
}

View File

@ -9,6 +9,7 @@ import (
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/promise"
"github.com/docker/docker/pkg/signal"
"github.com/docker/docker/pkg/term"
"github.com/pkg/errors"
@ -102,28 +103,23 @@ func runStart(dockerCli *command.DockerCli, opts *startOptions) error {
return errAttach
}
defer resp.Close()
cErr := promise.Go(func() error {
streamer := hijackedIOStreamer{
streams: dockerCli,
inputStream: in,
outputStream: dockerCli.Out(),
errorStream: dockerCli.Err(),
resp: resp,
tty: c.Config.Tty,
detachKeys: options.DetachKeys,
}
cErr := make(chan error, 1)
go func() {
cErr <- func() error {
streamer := hijackedIOStreamer{
streams: dockerCli,
inputStream: in,
outputStream: dockerCli.Out(),
errorStream: dockerCli.Err(),
resp: resp,
tty: c.Config.Tty,
detachKeys: options.DetachKeys,
}
errHijack := streamer.stream(ctx)
if errHijack == nil {
return errAttach
}
return errHijack
}()
}()
errHijack := streamer.stream(ctx)
if errHijack == nil {
return errAttach
}
return errHijack
})
// 3. We should open a channel for receiving status code of the container
// no matter it's detached, removed on daemon side(--rm) or exit normally.

View File

@ -21,7 +21,6 @@ import (
type statsOptions struct {
all bool
noStream bool
noTrunc bool
format string
containers []string
}
@ -43,7 +42,6 @@ func NewStatsCommand(dockerCli *command.DockerCli) *cobra.Command {
flags := cmd.Flags()
flags.BoolVarP(&opts.all, "all", "a", false, "Show all containers (default shows just running)")
flags.BoolVar(&opts.noStream, "no-stream", false, "Disable streaming stats and only pull the first result")
flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Do not truncate output")
flags.StringVar(&opts.format, "format", "", "Pretty-print images using a Go template")
return cmd
}
@ -216,7 +214,7 @@ func runStats(dockerCli *command.DockerCli, opts *statsOptions) error {
ccstats = append(ccstats, c.GetStatistics())
}
cStats.mu.Unlock()
if err = formatter.ContainerStatsWrite(statsCtx, ccstats, daemonOSType, !opts.noTrunc); err != nil {
if err = formatter.ContainerStatsWrite(statsCtx, ccstats, daemonOSType); err != nil {
break
}
if len(cStats.cs) == 0 && !showAll {

View File

@ -10,7 +10,6 @@ import (
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/stringid"
"github.com/gotestyourself/gotestyourself/golden"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@ -231,7 +230,10 @@ size: 0B
// Special headers for customized table format
{
Context{Format: NewContainerFormat(`table {{truncate .ID 5}}\t{{json .Image}} {{.RunningFor}}/{{title .Status}}/{{pad .Ports 2 2}}.{{upper .Names}} {{lower .Status}}`, false, true)},
string(golden.Get(t, "container-context-write-special-headers.golden")),
`CONTAINER ID IMAGE CREATED/STATUS/ PORTS .NAMES STATUS
conta "ubuntu" 24 hours ago//.FOOBAR_BAZ
conta "ubuntu" 24 hours ago//.FOOBAR_BAR
`,
},
}

View File

@ -4,7 +4,6 @@ import (
"bytes"
"testing"
"github.com/gotestyourself/gotestyourself/golden"
"github.com/stretchr/testify/assert"
)
@ -84,7 +83,12 @@ Build Cache 0B
Format: NewDiskUsageFormat("table {{.Type}}\t{{.Active}}"),
},
},
string(golden.Get(t, "disk-usage-context-write-custom.golden")),
`TYPE ACTIVE
Images 0
Containers 0
Local Volumes 0
Build Cache
`,
},
// Raw Format
{
@ -93,7 +97,31 @@ Build Cache 0B
Format: NewDiskUsageFormat("raw"),
},
},
string(golden.Get(t, "disk-usage-raw-format.golden")),
`type: Images
total: 0
active: 0
size: 0B
reclaimable: 0B
type: Containers
total: 0
active: 0
size: 0B
reclaimable: 0B
type: Local Volumes
total: 0
active: 0
size: 0B
reclaimable: 0B
type: Build Cache
total:
active:
size: 0B
reclaimable: 0B
`,
},
}

View File

@ -8,7 +8,6 @@ import (
registrytypes "github.com/docker/docker/api/types/registry"
"github.com/docker/docker/pkg/stringutils"
"github.com/gotestyourself/gotestyourself/golden"
"github.com/stretchr/testify/assert"
)
@ -121,7 +120,10 @@ func TestSearchContextWrite(t *testing.T) {
// Table format
{
Context{Format: NewSearchFormat("table")},
string(golden.Get(t, "search-context-write-table.golden")),
`NAME DESCRIPTION STARS OFFICIAL AUTOMATED
result1 Official build 5000 [OK]
result2 Not official 5 [OK]
`,
},
{
Context{Format: NewSearchFormat("table {{.Name}}")},
@ -208,7 +210,9 @@ func TestSearchContextWriteStars(t *testing.T) {
// Table format
{
Context{Format: NewSearchFormat("table")},
string(golden.Get(t, "search-context-write-stars-table.golden")),
`NAME DESCRIPTION STARS OFFICIAL AUTOMATED
result1 Official build 5000 [OK]
`,
},
{
Context{Format: NewSearchFormat("table {{.Name}}")},

View File

@ -12,20 +12,19 @@ import (
)
const (
defaultSecretTableFormat = "table {{.ID}}\t{{.Name}}\t{{.Driver}}\t{{.CreatedAt}}\t{{.UpdatedAt}}"
defaultSecretTableFormat = "table {{.ID}}\t{{.Name}}\t{{.CreatedAt}}\t{{.UpdatedAt}}"
secretIDHeader = "ID"
secretCreatedHeader = "CREATED"
secretUpdatedHeader = "UPDATED"
secretInspectPrettyTemplate Format = `ID: {{.ID}}
Name: {{.Name}}
secretInspectPrettyTemplate Format = `ID: {{.ID}}
Name: {{.Name}}
{{- if .Labels }}
Labels:
{{- range $k, $v := .Labels }}
- {{ $k }}{{if $v }}={{ $v }}{{ end }}
{{- end }}{{ end }}
Driver: {{.Driver}}
Created at: {{.CreatedAt}}
Updated at: {{.UpdatedAt}}`
Created at: {{.CreatedAt}}
Updated at: {{.UpdatedAt}}`
)
// NewSecretFormat returns a Format for rendering using a secret Context
@ -62,7 +61,6 @@ func newSecretContext() *secretContext {
sCtx.header = map[string]string{
"ID": secretIDHeader,
"Name": nameHeader,
"Driver": driverHeader,
"CreatedAt": secretCreatedHeader,
"UpdatedAt": secretUpdatedHeader,
"Labels": labelsHeader,
@ -91,13 +89,6 @@ func (c *secretContext) CreatedAt() string {
return units.HumanDuration(time.Now().UTC().Sub(c.s.Meta.CreatedAt)) + " ago"
}
func (c *secretContext) Driver() string {
if c.s.Spec.Driver == nil {
return ""
}
return c.s.Spec.Driver.Name
}
func (c *secretContext) UpdatedAt() string {
return units.HumanDuration(time.Now().UTC().Sub(c.s.Meta.UpdatedAt)) + " ago"
}
@ -162,13 +153,6 @@ func (ctx *secretInspectContext) Labels() map[string]string {
return ctx.Secret.Spec.Labels
}
func (ctx *secretInspectContext) Driver() string {
if ctx.Secret.Spec.Driver == nil {
return ""
}
return ctx.Secret.Spec.Driver.Name
}
func (ctx *secretInspectContext) CreatedAt() string {
return command.PrettyPrint(ctx.Secret.CreatedAt)
}

View File

@ -28,9 +28,9 @@ func TestSecretContextFormatWrite(t *testing.T) {
},
// Table format
{Context{Format: NewSecretFormat("table", false)},
`ID NAME DRIVER CREATED UPDATED
1 passwords Less than a second ago Less than a second ago
2 id_rsa Less than a second ago Less than a second ago
`ID NAME CREATED UPDATED
1 passwords Less than a second ago Less than a second ago
2 id_rsa Less than a second ago Less than a second ago
`},
{Context{Format: NewSecretFormat("table {{.Name}}", true)},
`NAME

View File

@ -8,7 +8,6 @@ import (
"testing"
"github.com/docker/docker/api/types/swarm"
"github.com/gotestyourself/gotestyourself/golden"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@ -60,7 +59,21 @@ bar
// Raw Format
{
Context{Format: NewServiceListFormat("raw", false)},
string(golden.Get(t, "service-context-write-raw.golden")),
`id: id_baz
name: baz
mode: global
replicas: 2/4
image:
ports: *:80->8080/tcp
id: id_bar
name: bar
mode: replicated
replicas: 2/4
image:
ports: *:80->8080/tcp
`,
},
{
Context{Format: NewServiceListFormat("raw", true)},

View File

@ -4,14 +4,13 @@ import (
"fmt"
"sync"
"github.com/docker/docker/pkg/stringid"
units "github.com/docker/go-units"
)
const (
winOSType = "windows"
defaultStatsTableFormat = "table {{.ID}}\t{{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}\t{{.NetIO}}\t{{.BlockIO}}\t{{.PIDs}}"
winDefaultStatsTableFormat = "table {{.ID}}\t{{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.NetIO}}\t{{.BlockIO}}"
defaultStatsTableFormat = "table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}\t{{.NetIO}}\t{{.BlockIO}}\t{{.PIDs}}"
winDefaultStatsTableFormat = "table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.NetIO}}\t{{.BlockIO}}"
containerHeader = "CONTAINER"
cpuPercHeader = "CPU %"
@ -115,13 +114,12 @@ func NewContainerStats(container string) *ContainerStats {
}
// ContainerStatsWrite renders the context for a list of containers statistics
func ContainerStatsWrite(ctx Context, containerStats []StatsEntry, osType string, trunc bool) error {
func ContainerStatsWrite(ctx Context, containerStats []StatsEntry, osType string) error {
render := func(format func(subContext subContext) error) error {
for _, cstats := range containerStats {
containerStatsCtx := &containerStatsContext{
s: cstats,
os: osType,
trunc: trunc,
s: cstats,
os: osType,
}
if err := format(containerStatsCtx); err != nil {
return err
@ -151,9 +149,8 @@ func ContainerStatsWrite(ctx Context, containerStats []StatsEntry, osType string
type containerStatsContext struct {
HeaderContext
s StatsEntry
os string
trunc bool
s StatsEntry
os string
}
func (c *containerStatsContext) MarshalJSON() ([]byte, error) {
@ -172,9 +169,6 @@ func (c *containerStatsContext) Name() string {
}
func (c *containerStatsContext) ID() string {
if c.trunc {
return stringid.TruncateID(c.s.ID)
}
return c.s.ID
}

View File

@ -114,7 +114,7 @@ container2 --
}
var out bytes.Buffer
te.context.Output = &out
err := ContainerStatsWrite(te.context, stats, "linux", false)
err := ContainerStatsWrite(te.context, stats, "linux")
if err != nil {
assert.EqualError(t, err, te.expected)
} else {
@ -180,7 +180,7 @@ container2 -- --
}
var out bytes.Buffer
te.context.Output = &out
err := ContainerStatsWrite(te.context, stats, "windows", false)
err := ContainerStatsWrite(te.context, stats, "windows")
if err != nil {
assert.EqualError(t, err, te.expected)
} else {
@ -220,7 +220,7 @@ func TestContainerStatsContextWriteWithNoStats(t *testing.T) {
}
for _, context := range contexts {
ContainerStatsWrite(context.context, []StatsEntry{}, "linux", false)
ContainerStatsWrite(context.context, []StatsEntry{}, "linux")
assert.Equal(t, context.expected, out.String())
// Clean buffer
out.Reset()
@ -258,41 +258,7 @@ func TestContainerStatsContextWriteWithNoStatsWindows(t *testing.T) {
}
for _, context := range contexts {
ContainerStatsWrite(context.context, []StatsEntry{}, "windows", false)
assert.Equal(t, context.expected, out.String())
// Clean buffer
out.Reset()
}
}
func TestContainerStatsContextWriteTrunc(t *testing.T) {
var out bytes.Buffer
contexts := []struct {
context Context
trunc bool
expected string
}{
{
Context{
Format: "{{.ID}}",
Output: &out,
},
false,
"b95a83497c9161c9b444e3d70e1a9dfba0c1840d41720e146a95a08ebf938afc\n",
},
{
Context{
Format: "{{.ID}}",
Output: &out,
},
true,
"b95a83497c91\n",
},
}
for _, context := range contexts {
ContainerStatsWrite(context.context, []StatsEntry{{ID: "b95a83497c9161c9b444e3d70e1a9dfba0c1840d41720e146a95a08ebf938afc"}}, "linux", context.trunc)
ContainerStatsWrite(context.context, []StatsEntry{}, "windows")
assert.Equal(t, context.expected, out.String())
// Clean buffer
out.Reset()

View File

@ -7,7 +7,6 @@ import (
"testing"
"github.com/docker/docker/api/types/swarm"
"github.com/gotestyourself/gotestyourself/golden"
"github.com/stretchr/testify/assert"
)
@ -34,7 +33,10 @@ taskID2
},
{
Context{Format: NewTaskFormat("table {{.Name}}\t{{.Node}}\t{{.Ports}}", false)},
string(golden.Get(t, "task-context-write-table-custom.golden")),
`NAME NODE PORTS
foobar_baz foo1
foobar_bar foo2
`,
},
{
Context{Format: NewTaskFormat("table {{.Name}}", true)},

View File

@ -1,3 +0,0 @@
CONTAINER ID IMAGE CREATED/STATUS/ PORTS .NAMES STATUS
conta "ubuntu" 24 hours ago//.FOOBAR_BAZ
conta "ubuntu" 24 hours ago//.FOOBAR_BAR

View File

@ -1,5 +0,0 @@
TYPE ACTIVE
Images 0
Containers 0
Local Volumes 0
Build Cache

View File

@ -1,24 +0,0 @@
type: Images
total: 0
active: 0
size: 0B
reclaimable: 0B
type: Containers
total: 0
active: 0
size: 0B
reclaimable: 0B
type: Local Volumes
total: 0
active: 0
size: 0B
reclaimable: 0B
type: Build Cache
total:
active:
size: 0B
reclaimable: 0B

View File

@ -1,2 +0,0 @@
NAME DESCRIPTION STARS OFFICIAL AUTOMATED
result1 Official build 5000 [OK]

View File

@ -1,3 +0,0 @@
NAME DESCRIPTION STARS OFFICIAL AUTOMATED
result1 Official build 5000 [OK]
result2 Not official 5 [OK]

View File

@ -1,14 +0,0 @@
id: id_baz
name: baz
mode: global
replicas: 2/4
image:
ports: *:80->8080/tcp
id: id_bar
name: bar
mode: replicated
replicas: 2/4
image:
ports: *:80->8080/tcp

View File

@ -1,3 +0,0 @@
NAME NODE PORTS
foobar_baz foo1
foobar_bar foo2

View File

@ -1,150 +0,0 @@
package formatter
import (
"sort"
"strings"
"github.com/docker/docker/pkg/stringid"
)
const (
defaultTrustTagTableFormat = "table {{.SignedTag}}\t{{.Digest}}\t{{.Signers}}"
signedTagNameHeader = "SIGNED TAG"
trustedDigestHeader = "DIGEST"
signersHeader = "SIGNERS"
defaultSignerInfoTableFormat = "table {{.Signer}}\t{{.Keys}}"
signerNameHeader = "SIGNER"
keysHeader = "KEYS"
)
// SignedTagInfo represents all formatted information needed to describe a signed tag:
// Name: name of the signed tag
// Digest: hex encoded digest of the contents
// Signers: list of entities who signed the tag
type SignedTagInfo struct {
Name string
Digest string
Signers []string
}
// SignerInfo represents all formatted information needed to describe a signer:
// Name: name of the signer role
// Keys: the keys associated with the signer
type SignerInfo struct {
Name string
Keys []string
}
// NewTrustTagFormat returns a Format for rendering using a trusted tag Context
func NewTrustTagFormat() Format {
return defaultTrustTagTableFormat
}
// NewSignerInfoFormat returns a Format for rendering a signer role info Context
func NewSignerInfoFormat() Format {
return defaultSignerInfoTableFormat
}
// TrustTagWrite writes the context
func TrustTagWrite(ctx Context, signedTagInfoList []SignedTagInfo) error {
render := func(format func(subContext subContext) error) error {
for _, signedTag := range signedTagInfoList {
if err := format(&trustTagContext{s: signedTag}); err != nil {
return err
}
}
return nil
}
trustTagCtx := trustTagContext{}
trustTagCtx.header = trustTagHeaderContext{
"SignedTag": signedTagNameHeader,
"Digest": trustedDigestHeader,
"Signers": signersHeader,
}
return ctx.Write(&trustTagCtx, render)
}
type trustTagHeaderContext map[string]string
type trustTagContext struct {
HeaderContext
s SignedTagInfo
}
// SignedTag returns the name of the signed tag
func (c *trustTagContext) SignedTag() string {
return c.s.Name
}
// Digest returns the hex encoded digest associated with this signed tag
func (c *trustTagContext) Digest() string {
return c.s.Digest
}
// Signers returns the sorted list of entities who signed this tag
func (c *trustTagContext) Signers() string {
sort.Strings(c.s.Signers)
return strings.Join(c.s.Signers, ", ")
}
// SignerInfoWrite writes the context
func SignerInfoWrite(ctx Context, signerInfoList []SignerInfo) error {
render := func(format func(subContext subContext) error) error {
for _, signerInfo := range signerInfoList {
if err := format(&signerInfoContext{
trunc: ctx.Trunc,
s: signerInfo,
}); err != nil {
return err
}
}
return nil
}
signerInfoCtx := signerInfoContext{}
signerInfoCtx.header = signerInfoHeaderContext{
"Signer": signerNameHeader,
"Keys": keysHeader,
}
return ctx.Write(&signerInfoCtx, render)
}
type signerInfoHeaderContext map[string]string
type signerInfoContext struct {
HeaderContext
trunc bool
s SignerInfo
}
// Keys returns the sorted list of keys associated with the signer
func (c *signerInfoContext) Keys() string {
sort.Strings(c.s.Keys)
truncatedKeys := []string{}
if c.trunc {
for _, keyID := range c.s.Keys {
truncatedKeys = append(truncatedKeys, stringid.TruncateID(keyID))
}
return strings.Join(truncatedKeys, ", ")
}
return strings.Join(c.s.Keys, ", ")
}
// Signer returns the name of the signer
func (c *signerInfoContext) Signer() string {
return c.s.Name
}
// SignerInfoList helps sort []SignerInfo by signer names
type SignerInfoList []SignerInfo
func (signerInfoComp SignerInfoList) Len() int {
return len(signerInfoComp)
}
func (signerInfoComp SignerInfoList) Less(i, j int) bool {
return signerInfoComp[i].Name < signerInfoComp[j].Name
}
func (signerInfoComp SignerInfoList) Swap(i, j int) {
signerInfoComp[i], signerInfoComp[j] = signerInfoComp[j], signerInfoComp[i]
}

View File

@ -1,238 +0,0 @@
package formatter
import (
"bytes"
"testing"
"github.com/docker/docker/pkg/stringid"
"github.com/stretchr/testify/assert"
)
func TestTrustTag(t *testing.T) {
digest := stringid.GenerateRandomID()
trustedTag := "tag"
var ctx trustTagContext
cases := []struct {
trustTagCtx trustTagContext
expValue string
call func() string
}{
{
trustTagContext{
s: SignedTagInfo{Name: trustedTag,
Digest: digest,
Signers: nil,
},
},
digest,
ctx.Digest,
},
{
trustTagContext{
s: SignedTagInfo{Name: trustedTag,
Digest: digest,
Signers: nil,
},
},
trustedTag,
ctx.SignedTag,
},
// Empty signers makes a row with empty string
{
trustTagContext{
s: SignedTagInfo{Name: trustedTag,
Digest: digest,
Signers: nil,
},
},
"",
ctx.Signers,
},
{
trustTagContext{
s: SignedTagInfo{Name: trustedTag,
Digest: digest,
Signers: []string{"alice", "bob", "claire"},
},
},
"alice, bob, claire",
ctx.Signers,
},
// alphabetic signing on Signers
{
trustTagContext{
s: SignedTagInfo{Name: trustedTag,
Digest: digest,
Signers: []string{"claire", "bob", "alice"},
},
},
"alice, bob, claire",
ctx.Signers,
},
}
for _, c := range cases {
ctx = c.trustTagCtx
v := c.call()
if v != c.expValue {
t.Fatalf("Expected %s, was %s\n", c.expValue, v)
}
}
}
func TestTrustTagContextWrite(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: NewTrustTagFormat(),
},
`SIGNED TAG DIGEST SIGNERS
tag1 deadbeef alice
tag2 aaaaaaaa alice, bob
tag3 bbbbbbbb
`,
},
}
for _, testcase := range cases {
signedTags := []SignedTagInfo{
{Name: "tag1", Digest: "deadbeef", Signers: []string{"alice"}},
{Name: "tag2", Digest: "aaaaaaaa", Signers: []string{"alice", "bob"}},
{Name: "tag3", Digest: "bbbbbbbb", Signers: []string{}},
}
out := bytes.NewBufferString("")
testcase.context.Output = out
err := TrustTagWrite(testcase.context, signedTags)
if err != nil {
assert.EqualError(t, err, testcase.expected)
} else {
assert.Equal(t, testcase.expected, out.String())
}
}
}
// With no trust data, the TrustTagWrite will print an empty table:
// it's up to the caller to decide whether or not to print this versus an error
func TestTrustTagContextEmptyWrite(t *testing.T) {
emptyCase := struct {
context Context
expected string
}{
Context{
Format: NewTrustTagFormat(),
},
`SIGNED TAG DIGEST SIGNERS
`,
}
emptySignedTags := []SignedTagInfo{}
out := bytes.NewBufferString("")
emptyCase.context.Output = out
err := TrustTagWrite(emptyCase.context, emptySignedTags)
assert.NoError(t, err)
assert.Equal(t, emptyCase.expected, out.String())
}
func TestSignerInfoContextEmptyWrite(t *testing.T) {
emptyCase := struct {
context Context
expected string
}{
Context{
Format: NewSignerInfoFormat(),
},
`SIGNER KEYS
`,
}
emptySignerInfo := []SignerInfo{}
out := bytes.NewBufferString("")
emptyCase.context.Output = out
err := SignerInfoWrite(emptyCase.context, emptySignerInfo)
assert.NoError(t, err)
assert.Equal(t, emptyCase.expected, out.String())
}
func TestSignerInfoContextWrite(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: NewSignerInfoFormat(),
Trunc: true,
},
`SIGNER KEYS
alice key11, key12
bob key21
eve foobarbazqux, key31, key32
`,
},
// No truncation
{
Context{
Format: NewSignerInfoFormat(),
},
`SIGNER KEYS
alice key11, key12
bob key21
eve foobarbazquxquux, key31, key32
`,
},
}
for _, testcase := range cases {
signerInfo := SignerInfoList{
{Name: "alice", Keys: []string{"key11", "key12"}},
{Name: "bob", Keys: []string{"key21"}},
{Name: "eve", Keys: []string{"key31", "key32", "foobarbazquxquux"}},
}
out := bytes.NewBufferString("")
testcase.context.Output = out
err := SignerInfoWrite(testcase.context, signerInfo)
if err != nil {
assert.EqualError(t, err, testcase.expected)
} else {
assert.Equal(t, testcase.expected, out.String())
}
}
}

View File

@ -21,7 +21,6 @@ import (
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/pkg/archive"
"github.com/docker/docker/pkg/idtools"
"github.com/docker/docker/pkg/jsonmessage"
"github.com/docker/docker/pkg/progress"
"github.com/docker/docker/pkg/streamformatter"
@ -244,7 +243,6 @@ func runBuild(dockerCli command.Cli, options buildOptions) error {
excludes = build.TrimBuildFilesFromExcludes(excludes, relDockerfile, options.dockerfileFromStdin())
buildCtx, err = archive.TarWithOptions(contextDir, &archive.TarOptions{
ExcludePatterns: excludes,
ChownOpts: &idtools.IDPair{UID: 0, GID: 0},
})
if err != nil {
return err
@ -378,13 +376,13 @@ func runBuild(dockerCli command.Cli, options buildOptions) error {
if s != nil {
go func() {
logrus.Debugf("running session: %v", s.ID())
logrus.Debugf("running session: %v", s.UUID())
if err := s.Run(ctx, dockerCli.Client().DialSession); err != nil {
logrus.Error(err)
cancel() // cancel progress context
}
}()
buildOptions.SessionID = s.ID()
buildOptions.SessionID = s.UUID()
}
response, err := dockerCli.Client().ImageBuild(ctx, body, buildOptions)

View File

@ -53,9 +53,7 @@ func addDirToSession(session *session.Session, contextDir string, progressOutput
p := &sizeProgress{out: progressOutput, action: "Streaming build context to Docker daemon"}
workdirProvider := filesync.NewFSSyncProvider([]filesync.SyncedDir{
{Dir: contextDir, Excludes: excludes},
})
workdirProvider := filesync.NewFSSyncProvider(contextDir, excludes)
session.Allow(workdirProvider)
// this will be replaced on parallel build jobs. keep the current

View File

@ -6,57 +6,18 @@ import (
"io/ioutil"
"os"
"path/filepath"
"runtime"
"sort"
"syscall"
"testing"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/internal/test"
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/archive"
"github.com/gotestyourself/gotestyourself/fs"
"github.com/gotestyourself/gotestyourself/skip"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/net/context"
)
func TestRunBuildResetsUidAndGidInContext(t *testing.T) {
skip.IfCondition(t, runtime.GOOS == "windows", "uid and gid not relevant on windows")
dest := fs.NewDir(t, "test-build-context-dest")
defer dest.Remove()
fakeImageBuild := func(_ context.Context, context io.Reader, options types.ImageBuildOptions) (types.ImageBuildResponse, error) {
assert.NoError(t, archive.Untar(context, dest.Path(), nil))
body := new(bytes.Buffer)
return types.ImageBuildResponse{Body: ioutil.NopCloser(body)}, nil
}
cli := test.NewFakeCli(&fakeClient{imageBuildFunc: fakeImageBuild})
dir := fs.NewDir(t, "test-build-context",
fs.WithFile("foo", "some content", fs.AsUser(65534, 65534)),
fs.WithFile("Dockerfile", `
FROM alpine:3.6
COPY foo bar /
`),
)
defer dir.Remove()
options := newBuildOptions()
options.context = dir.Path()
err := runBuild(cli, options)
require.NoError(t, err)
files, err := ioutil.ReadDir(dest.Path())
require.NoError(t, err)
for _, fileInfo := range files {
assert.Equal(t, uint32(0), fileInfo.Sys().(*syscall.Stat_t).Uid)
assert.Equal(t, uint32(0), fileInfo.Sys().(*syscall.Stat_t).Gid)
}
}
func TestRunBuildDockerfileFromStdinWithCompress(t *testing.T) {
dest, err := ioutil.TempDir("", "test-build-compress-dest")
require.NoError(t, err)

View File

@ -6,8 +6,8 @@ import (
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/trust"
"github.com/docker/distribution/reference"
"github.com/docker/docker/registry"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"golang.org/x/net/context"
@ -40,32 +40,39 @@ func NewPullCommand(dockerCli command.Cli) *cobra.Command {
return cmd
}
func runPull(cli command.Cli, opts pullOptions) error {
func runPull(dockerCli command.Cli, opts pullOptions) error {
distributionRef, err := reference.ParseNormalizedNamed(opts.remote)
switch {
case err != nil:
if err != nil {
return err
case opts.all && !reference.IsNameOnly(distributionRef):
}
if opts.all && !reference.IsNameOnly(distributionRef) {
return errors.New("tag can't be used with --all-tags/-a")
case !opts.all && reference.IsNameOnly(distributionRef):
}
if !opts.all && reference.IsNameOnly(distributionRef) {
distributionRef = reference.TagNameOnly(distributionRef)
if tagged, ok := distributionRef.(reference.Tagged); ok {
fmt.Fprintf(cli.Out(), "Using default tag: %s\n", tagged.Tag())
fmt.Fprintf(dockerCli.Out(), "Using default tag: %s\n", tagged.Tag())
}
}
ctx := context.Background()
imgRefAndAuth, err := trust.GetImageReferencesAndAuth(ctx, AuthResolver(cli), distributionRef.String())
// Resolve the Repository name from fqn to RepositoryInfo
repoInfo, err := registry.ParseRepositoryInfo(distributionRef)
if err != nil {
return err
}
ctx := context.Background()
authConfig := command.ResolveAuthConfig(ctx, dockerCli, repoInfo.Index)
requestPrivilege := command.RegistryAuthenticationPrivilegedFunc(dockerCli, repoInfo.Index, "pull")
// Check if reference has a digest
_, isCanonical := distributionRef.(reference.Canonical)
if command.IsTrusted() && !isCanonical {
err = trustedPull(ctx, cli, imgRefAndAuth)
err = trustedPull(ctx, dockerCli, repoInfo, distributionRef, authConfig, requestPrivilege)
} else {
err = imagePullPrivileged(ctx, cli, imgRefAndAuth, opts.all)
err = imagePullPrivileged(ctx, dockerCli, authConfig, reference.FamiliarString(distributionRef), requestPrivilege, opts.all)
}
if err != nil {
if strings.Contains(err.Error(), "when fetching 'plugin'") {
@ -73,5 +80,6 @@ func runPull(cli command.Cli, opts pullOptions) error {
}
return err
}
return nil
}

View File

@ -2,14 +2,11 @@ package image
import (
"fmt"
"io"
"io/ioutil"
"strings"
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/testutil"
"github.com/docker/docker/api/types"
"github.com/gotestyourself/gotestyourself/golden"
"github.com/stretchr/testify/assert"
)
@ -35,6 +32,11 @@ func TestNewPullCommandErrors(t *testing.T) {
expectedError: "tag can't be used with --all-tags/-a",
args: []string{"--all-tags", "image:tag"},
},
{
name: "pull-error",
args: []string{"--disable-content-trust=false", "image:tag"},
expectedError: "you are not authorized to perform this operation: server returned 401.",
},
}
for _, tc := range testCases {
cli := test.NewFakeCli(&fakeClient{})
@ -47,28 +49,20 @@ func TestNewPullCommandErrors(t *testing.T) {
func TestNewPullCommandSuccess(t *testing.T) {
testCases := []struct {
name string
args []string
expectedTag string
name string
args []string
}{
{
name: "simple",
args: []string{"image:tag"},
expectedTag: "image:tag",
name: "simple",
args: []string{"image:tag"},
},
{
name: "simple-no-tag",
args: []string{"image"},
expectedTag: "image:latest",
name: "simple-no-tag",
args: []string{"image"},
},
}
for _, tc := range testCases {
cli := test.NewFakeCli(&fakeClient{
imagePullFunc: func(ref string, options types.ImagePullOptions) (io.ReadCloser, error) {
assert.Equal(t, tc.expectedTag, ref, tc.name)
return ioutil.NopCloser(strings.NewReader("")), nil
},
})
cli := test.NewFakeCli(&fakeClient{})
cmd := NewPullCommand(cli)
cmd.SetOutput(ioutil.Discard)
cmd.SetArgs(tc.args)

View File

@ -48,7 +48,7 @@ func runPush(dockerCli command.Cli, remote string) error {
requestPrivilege := command.RegistryAuthenticationPrivilegedFunc(dockerCli, repoInfo.Index, "push")
if command.IsTrusted() {
return TrustedPush(ctx, dockerCli, repoInfo, ref, authConfig, requestPrivilege)
return trustedPush(ctx, dockerCli, repoInfo, ref, authConfig, requestPrivilege)
}
responseBody, err := imagePushPrivileged(ctx, dockerCli, authConfig, ref, requestPrivilege)

View File

@ -5,18 +5,18 @@ import (
"encoding/json"
"fmt"
"io"
"path"
"sort"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/trust"
"github.com/docker/distribution/reference"
"github.com/docker/docker/api/types"
registrytypes "github.com/docker/docker/api/types/registry"
"github.com/docker/docker/pkg/jsonmessage"
"github.com/docker/docker/registry"
"github.com/docker/notary/client"
"github.com/docker/notary/tuf/data"
digest "github.com/opencontainers/go-digest"
"github.com/opencontainers/go-digest"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"golang.org/x/net/context"
@ -28,8 +28,8 @@ type target struct {
size int64
}
// TrustedPush handles content trust pushing of an image
func TrustedPush(ctx context.Context, cli command.Cli, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig types.AuthConfig, requestPrivilege types.RequestPrivilegeFunc) error {
// trustedPush handles content trust pushing of an image
func trustedPush(ctx context.Context, cli command.Cli, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig types.AuthConfig, requestPrivilege types.RequestPrivilegeFunc) error {
responseBody, err := imagePushPrivileged(ctx, cli, authConfig, ref, requestPrivilege)
if err != nil {
return err
@ -103,25 +103,25 @@ func PushTrustedReference(streams command.Streams, repoInfo *registry.Repository
fmt.Fprintln(streams.Out(), "Signing and pushing trust metadata")
repo, err := trust.GetNotaryRepository(streams.In(), streams.Out(), command.UserAgent(), repoInfo, &authConfig, "push", "pull")
repo, err := trust.GetNotaryRepository(streams, repoInfo, authConfig, "push", "pull")
if err != nil {
fmt.Fprintf(streams.Out(), "Error establishing connection to notary repository: %s\n", err)
return err
}
// get the latest repository metadata so we can figure out which roles to sign
_, err = repo.ListTargets()
err = repo.Update(false)
switch err.(type) {
case client.ErrRepoNotInitialized, client.ErrRepositoryNotExist:
keys := repo.GetCryptoService().ListKeys(data.CanonicalRootRole)
keys := repo.CryptoService.ListKeys(data.CanonicalRootRole)
var rootKeyID string
// always select the first root key
if len(keys) > 0 {
sort.Strings(keys)
rootKeyID = keys[0]
} else {
rootPublicKey, err := repo.GetCryptoService().Create(data.CanonicalRootRole, "", data.ECDSAKey)
rootPublicKey, err := repo.CryptoService.Create(data.CanonicalRootRole, "", data.ECDSAKey)
if err != nil {
return err
}
@ -136,7 +136,7 @@ func PushTrustedReference(streams command.Streams, repoInfo *registry.Repository
err = repo.AddTarget(target, data.CanonicalTargetsRole)
case nil:
// already initialized and we have successfully downloaded the latest metadata
err = AddTargetToAllSignableRoles(repo, target)
err = addTargetToAllSignableRoles(repo, target)
default:
return trust.NotaryError(repoInfo.Name.Name(), err)
}
@ -154,16 +154,51 @@ func PushTrustedReference(streams command.Streams, repoInfo *registry.Repository
return nil
}
// AddTargetToAllSignableRoles attempts to add the image target to all the top level delegation roles we can
// Attempt to add the image target to all the top level delegation roles we can
// (based on whether we have the signing key and whether the role's path allows
// us to).
// If there are no delegation roles, we add to the targets role.
func AddTargetToAllSignableRoles(repo client.Repository, target *client.Target) error {
signableRoles, err := trust.GetSignableRoles(repo, target)
func addTargetToAllSignableRoles(repo *client.NotaryRepository, target *client.Target) error {
var signableRoles []string
// translate the full key names, which includes the GUN, into just the key IDs
allCanonicalKeyIDs := make(map[string]struct{})
for fullKeyID := range repo.CryptoService.ListAllKeys() {
allCanonicalKeyIDs[path.Base(fullKeyID)] = struct{}{}
}
allDelegationRoles, err := repo.GetDelegationRoles()
if err != nil {
return err
}
// if there are no delegation roles, then just try to sign it into the targets role
if len(allDelegationRoles) == 0 {
return repo.AddTarget(target, data.CanonicalTargetsRole)
}
// there are delegation roles, find every delegation role we have a key for, and
// attempt to sign into into all those roles.
for _, delegationRole := range allDelegationRoles {
// We do not support signing any delegation role that isn't a direct child of the targets role.
// Also don't bother checking the keys if we can't add the target
// to this role due to path restrictions
if path.Dir(delegationRole.Name) != data.CanonicalTargetsRole || !delegationRole.CheckPaths(target.Name) {
continue
}
for _, canonicalKeyID := range delegationRole.KeyIDs {
if _, ok := allCanonicalKeyIDs[canonicalKeyID]; ok {
signableRoles = append(signableRoles, delegationRole.Name)
break
}
}
}
if len(signableRoles) == 0 {
return errors.Errorf("no valid signing keys for delegation roles")
}
return repo.AddTarget(target, signableRoles...)
}
@ -182,13 +217,57 @@ func imagePushPrivileged(ctx context.Context, cli command.Cli, authConfig types.
}
// trustedPull handles content trust pulling of an image
func trustedPull(ctx context.Context, cli command.Cli, imgRefAndAuth trust.ImageRefAndAuth) error {
refs, err := getTrustedPullTargets(cli, imgRefAndAuth)
func trustedPull(ctx context.Context, cli command.Cli, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig types.AuthConfig, requestPrivilege types.RequestPrivilegeFunc) error {
var refs []target
notaryRepo, err := trust.GetNotaryRepository(cli, repoInfo, authConfig, "pull")
if err != nil {
fmt.Fprintf(cli.Out(), "Error establishing connection to trust repository: %s\n", err)
return err
}
ref := imgRefAndAuth.Reference()
if tagged, isTagged := ref.(reference.NamedTagged); !isTagged {
// List all targets
targets, err := notaryRepo.ListTargets(trust.ReleasesRole, data.CanonicalTargetsRole)
if err != nil {
return trust.NotaryError(ref.Name(), err)
}
for _, tgt := range targets {
t, err := convertTarget(tgt.Target)
if err != nil {
fmt.Fprintf(cli.Out(), "Skipping target for %q\n", reference.FamiliarName(ref))
continue
}
// Only list tags in the top level targets role or the releases delegation role - ignore
// all other delegation roles
if tgt.Role != trust.ReleasesRole && tgt.Role != data.CanonicalTargetsRole {
continue
}
refs = append(refs, t)
}
if len(refs) == 0 {
return trust.NotaryError(ref.Name(), errors.Errorf("No trusted tags for %s", ref.Name()))
}
} else {
t, err := notaryRepo.GetTargetByName(tagged.Tag(), trust.ReleasesRole, data.CanonicalTargetsRole)
if err != nil {
return trust.NotaryError(ref.Name(), err)
}
// Only get the tag if it's in the top level targets role or the releases delegation role
// ignore it if it's in any other delegation roles
if t.Role != trust.ReleasesRole && t.Role != data.CanonicalTargetsRole {
return trust.NotaryError(ref.Name(), errors.Errorf("No trust data for %s", tagged.Tag()))
}
logrus.Debugf("retrieving target for %s role\n", t.Role)
r, err := convertTarget(t.Target)
if err != nil {
return err
}
refs = append(refs, r)
}
for i, r := range refs {
displayTag := r.name
if displayTag != "" {
@ -200,11 +279,7 @@ func trustedPull(ctx context.Context, cli command.Cli, imgRefAndAuth trust.Image
if err != nil {
return err
}
updatedImgRefAndAuth, err := trust.GetImageReferencesAndAuth(ctx, AuthResolver(cli), trustedRef.String())
if err != nil {
return err
}
if err := imagePullPrivileged(ctx, cli, updatedImgRefAndAuth, false); err != nil {
if err := imagePullPrivileged(ctx, cli, authConfig, reference.FamiliarString(trustedRef), requestPrivilege, false); err != nil {
return err
}
@ -220,65 +295,13 @@ func trustedPull(ctx context.Context, cli command.Cli, imgRefAndAuth trust.Image
return nil
}
func getTrustedPullTargets(cli command.Cli, imgRefAndAuth trust.ImageRefAndAuth) ([]target, error) {
notaryRepo, err := cli.NotaryClient(imgRefAndAuth, trust.ActionsPullOnly)
if err != nil {
fmt.Fprintf(cli.Out(), "Error establishing connection to trust repository: %s\n", err)
return nil, err
}
ref := imgRefAndAuth.Reference()
tagged, isTagged := ref.(reference.NamedTagged)
if !isTagged {
// List all targets
targets, err := notaryRepo.ListTargets(trust.ReleasesRole, data.CanonicalTargetsRole)
if err != nil {
return nil, trust.NotaryError(ref.Name(), err)
}
var refs []target
for _, tgt := range targets {
t, err := convertTarget(tgt.Target)
if err != nil {
fmt.Fprintf(cli.Out(), "Skipping target for %q\n", reference.FamiliarName(ref))
continue
}
// Only list tags in the top level targets role or the releases delegation role - ignore
// all other delegation roles
if tgt.Role != trust.ReleasesRole && tgt.Role != data.CanonicalTargetsRole {
continue
}
refs = append(refs, t)
}
if len(refs) == 0 {
return nil, trust.NotaryError(ref.Name(), errors.Errorf("No trusted tags for %s", ref.Name()))
}
return refs, nil
}
t, err := notaryRepo.GetTargetByName(tagged.Tag(), trust.ReleasesRole, data.CanonicalTargetsRole)
if err != nil {
return nil, trust.NotaryError(ref.Name(), err)
}
// Only get the tag if it's in the top level targets role or the releases delegation role
// ignore it if it's in any other delegation roles
if t.Role != trust.ReleasesRole && t.Role != data.CanonicalTargetsRole {
return nil, trust.NotaryError(ref.Name(), errors.Errorf("No trust data for %s", tagged.Tag()))
}
logrus.Debugf("retrieving target for %s role\n", t.Role)
r, err := convertTarget(t.Target)
return []target{r}, err
}
// imagePullPrivileged pulls the image and displays it to the output
func imagePullPrivileged(ctx context.Context, cli command.Cli, imgRefAndAuth trust.ImageRefAndAuth, all bool) error {
ref := reference.FamiliarString(imgRefAndAuth.Reference())
func imagePullPrivileged(ctx context.Context, cli command.Cli, authConfig types.AuthConfig, ref string, requestPrivilege types.RequestPrivilegeFunc, all bool) error {
encodedAuth, err := command.EncodeAuthToBase64(*imgRefAndAuth.AuthConfig())
encodedAuth, err := command.EncodeAuthToBase64(authConfig)
if err != nil {
return err
}
requestPrivilege := command.RegistryAuthenticationPrivilegedFunc(cli, imgRefAndAuth.RepoInfo().Index, "pull")
options := types.ImagePullOptions{
RegistryAuth: encodedAuth,
PrivilegeFunc: requestPrivilege,
@ -312,7 +335,7 @@ func TrustedReference(ctx context.Context, cli command.Cli, ref reference.NamedT
// Resolve the Auth config relevant for this server
authConfig := command.ResolveAuthConfig(ctx, cli, repoInfo.Index)
notaryRepo, err := trust.GetNotaryRepository(cli.In(), cli.Out(), command.UserAgent(), repoInfo, &authConfig, "pull")
notaryRepo, err := trust.GetNotaryRepository(cli, repoInfo, authConfig, "pull")
if err != nil {
fmt.Fprintf(cli.Out(), "Error establishing connection to trust repository: %s\n", err)
return nil, err
@ -325,7 +348,7 @@ func TrustedReference(ctx context.Context, cli command.Cli, ref reference.NamedT
// Only list tags in the top level targets role or the releases delegation role - ignore
// all other delegation roles
if t.Role != trust.ReleasesRole && t.Role != data.CanonicalTargetsRole {
return nil, trust.NotaryError(repoInfo.Name.Name(), client.ErrNoSuchTarget(ref.Tag()))
return nil, trust.NotaryError(repoInfo.Name.Name(), errors.Errorf("No trust data for %s", ref.Tag()))
}
r, err := convertTarget(t.Target)
if err != nil {
@ -359,10 +382,3 @@ func TagTrusted(ctx context.Context, cli command.Cli, trustedRef reference.Canon
return cli.Client().ImageTag(ctx, trustedFamiliarRef, familiarRef)
}
// AuthResolver returns an auth resolver function from a command.Cli
func AuthResolver(cli command.Cli) func(ctx context.Context, index *registrytypes.IndexInfo) types.AuthConfig {
return func(ctx context.Context, index *registrytypes.IndexInfo) types.AuthConfig {
return command.ResolveAuthConfig(ctx, cli, index)
}
}

View File

@ -1,18 +1,12 @@
package image
import (
"io/ioutil"
"os"
"testing"
"github.com/docker/cli/cli/trust"
registrytypes "github.com/docker/docker/api/types/registry"
"github.com/docker/docker/registry"
"github.com/docker/notary/client"
"github.com/docker/notary/passphrase"
"github.com/docker/notary/trustpinning"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func unsetENV() {
@ -61,15 +55,3 @@ func TestNonOfficialTrustServer(t *testing.T) {
t.Fatalf("Expected server to be %s, got %s", expectedStr, output)
}
}
func TestAddTargetToAllSignableRolesError(t *testing.T) {
tmpDir, err := ioutil.TempDir("", "notary-test-")
assert.NoError(t, err)
defer os.RemoveAll(tmpDir)
notaryRepo, err := client.NewFileCachedRepository(tmpDir, "gun", "https://localhost", nil, passphrase.ConstantRetriever("password"), trustpinning.TrustPinConfig{})
require.NoError(t, err)
target := client.Target{}
err = AddTargetToAllSignableRoles(notaryRepo, &target)
assert.EqualError(t, err, "client is offline")
}

View File

@ -12,7 +12,6 @@ type fakeClient struct {
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
networkListFunc func(ctx context.Context, options types.NetworkListOptions) ([]types.NetworkResource, error)
}
func (c *fakeClient) NetworkCreate(ctx context.Context, name string, options types.NetworkCreate) (types.NetworkCreateResponse, error) {
@ -35,10 +34,3 @@ func (c *fakeClient) NetworkDisconnect(ctx context.Context, networkID, container
}
return nil
}
func (c *fakeClient) NetworkList(ctx context.Context, options types.NetworkListOptions) ([]types.NetworkResource, error) {
if c.networkListFunc != nil {
return c.networkListFunc(ctx, options)
}
return []types.NetworkResource{}, nil
}

View File

@ -1,69 +0,0 @@
package network
import (
"testing"
"io/ioutil"
"strings"
"github.com/docker/cli/internal/test"
. "github.com/docker/cli/internal/test/builders"
"github.com/docker/cli/internal/test/testutil"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/gotestyourself/gotestyourself/golden"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"golang.org/x/net/context"
)
func TestNetworkListErrors(t *testing.T) {
testCases := []struct {
networkListFunc func(ctx context.Context, options types.NetworkListOptions) ([]types.NetworkResource, error)
expectedError string
}{
{
networkListFunc: func(ctx context.Context, options types.NetworkListOptions) ([]types.NetworkResource, error) {
return []types.NetworkResource{}, errors.Errorf("error creating network")
},
expectedError: "error creating network",
},
}
for _, tc := range testCases {
cmd := newListCommand(
test.NewFakeCli(&fakeClient{
networkListFunc: tc.networkListFunc,
}),
)
cmd.SetOutput(ioutil.Discard)
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
}
}
func TestNetworkListWithFlags(t *testing.T) {
filterArgs := filters.NewArgs()
filterArgs.Add("image.name", "ubuntu")
expectedOpts := types.NetworkListOptions{
Filters: filterArgs,
}
cli := test.NewFakeCli(&fakeClient{
networkListFunc: func(ctx context.Context, options types.NetworkListOptions) ([]types.NetworkResource, error) {
assert.Equal(t, expectedOpts, options, "not expected options error")
return []types.NetworkResource{*NetworkResource(NetworkResourceID("123454321"),
NetworkResourceName("network_1"),
NetworkResourceDriver("09.7.01"),
NetworkResourceScope("global"))}, nil
},
})
cmd := newListCommand(cli)
cmd.Flags().Set("filter", "image.name=ubuntu")
assert.NoError(t, cmd.Execute())
golden.Assert(t, strings.TrimSpace(cli.OutBuffer().String()), "network-list.golden")
}

View File

@ -1,2 +0,0 @@
NETWORK ID NAME DRIVER SCOPE
123454321 network_1 09.7.01 global

View File

@ -65,12 +65,10 @@ func (s pluginRegistryService) ResolveRepository(name reference.Named) (repoInfo
return
}
func newRegistryService() (registry.Service, error) {
svc, err := registry.NewService(registry.ServiceOptions{V2Only: true})
if err != nil {
return nil, err
func newRegistryService() registry.Service {
return pluginRegistryService{
Service: registry.NewService(registry.ServiceOptions{V2Only: true}),
}
return pluginRegistryService{Service: svc}, nil
}
func buildPullConfig(ctx context.Context, dockerCli *command.DockerCli, opts pluginOptions, cmdName string) (types.PluginInstallOptions, error) {
@ -98,11 +96,7 @@ func buildPullConfig(ctx context.Context, dockerCli *command.DockerCli, opts plu
}
ctx := context.Background()
svc, err := newRegistryService()
if err != nil {
return types.PluginInstallOptions{}, err
}
trusted, err := image.TrustedReference(ctx, dockerCli, nt, svc)
trusted, err := image.TrustedReference(ctx, dockerCli, nt, newRegistryService())
if err != nil {
return types.PluginInstallOptions{}, err
}

View File

@ -17,7 +17,6 @@ import (
type createOptions struct {
name string
driver string
file string
labels opts.ListOpts
}
@ -28,21 +27,17 @@ func newSecretCreateCommand(dockerCli command.Cli) *cobra.Command {
}
cmd := &cobra.Command{
Use: "create [OPTIONS] SECRET [file|-]",
Use: "create [OPTIONS] SECRET file|-",
Short: "Create a secret from a file or STDIN as content",
Args: cli.RequiresRangeArgs(1, 2),
Args: cli.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
options.name = args[0]
if len(args) == 2 {
options.file = args[1]
}
options.file = args[1]
return runSecretCreate(dockerCli, options)
},
}
flags := cmd.Flags()
flags.VarP(&options.labels, "label", "l", "Secret labels")
flags.StringVarP(&options.driver, "driver", "d", "", "Secret driver")
flags.SetAnnotation("driver", "version", []string{"1.31"})
return cmd
}
@ -51,14 +46,21 @@ func runSecretCreate(dockerCli command.Cli, options createOptions) error {
client := dockerCli.Client()
ctx := context.Background()
if options.driver != "" && options.file != "" {
return errors.Errorf("When using secret driver secret data must be empty")
var in io.Reader = dockerCli.In()
if options.file != "-" {
file, err := system.OpenSequential(options.file)
if err != nil {
return err
}
in = file
defer file.Close()
}
secretData, err := readSecretData(dockerCli.In(), options.file)
secretData, err := ioutil.ReadAll(in)
if err != nil {
return errors.Errorf("Error reading content from %q: %v", options.file, err)
}
spec := swarm.SecretSpec{
Annotations: swarm.Annotations{
Name: options.name,
@ -66,11 +68,6 @@ func runSecretCreate(dockerCli command.Cli, options createOptions) error {
},
Data: secretData,
}
if options.driver != "" {
spec.Driver = &swarm.Driver{
Name: options.driver,
}
}
r, err := client.SecretCreate(ctx, spec)
if err != nil {
@ -80,23 +77,3 @@ func runSecretCreate(dockerCli command.Cli, options createOptions) error {
fmt.Fprintln(dockerCli.Out(), r.ID)
return nil
}
func readSecretData(in io.ReadCloser, file string) ([]byte, error) {
// Read secret value from external driver
if file == "" {
return nil, nil
}
if file != "-" {
var err error
in, err = system.OpenSequential(file)
if err != nil {
return nil, err
}
defer in.Close()
}
data, err := ioutil.ReadAll(in)
if err != nil {
return nil, err
}
return data, nil
}

View File

@ -24,11 +24,12 @@ func TestSecretCreateErrors(t *testing.T) {
secretCreateFunc func(swarm.SecretSpec) (types.SecretCreateResponse, error)
expectedError string
}{
{args: []string{"too", "many", "arguments"},
expectedError: "requires at least 1 and at most 2 arguments",
{
args: []string{"too_few"},
expectedError: "requires exactly 2 arguments",
},
{args: []string{"create", "--driver", "driver", "-"},
expectedError: "secret data must be empty",
{args: []string{"too", "many", "arguments"},
expectedError: "requires exactly 2 arguments",
},
{
args: []string{"name", filepath.Join("testdata", secretDataFile)},
@ -74,35 +75,6 @@ func TestSecretCreateWithName(t *testing.T) {
assert.Equal(t, "ID-"+name, strings.TrimSpace(cli.OutBuffer().String()))
}
func TestSecretCreateWithDriver(t *testing.T) {
expectedDriver := &swarm.Driver{
Name: "secret-driver",
}
name := "foo"
cli := test.NewFakeCli(&fakeClient{
secretCreateFunc: func(spec swarm.SecretSpec) (types.SecretCreateResponse, error) {
if spec.Name != name {
return types.SecretCreateResponse{}, errors.Errorf("expected name %q, got %q", name, spec.Name)
}
if !reflect.DeepEqual(spec.Driver.Name, expectedDriver.Name) {
return types.SecretCreateResponse{}, errors.Errorf("expected driver %v, got %v", expectedDriver, spec.Labels)
}
return types.SecretCreateResponse{
ID: "ID-" + spec.Name,
}, nil
},
})
cmd := newSecretCreateCommand(cli)
cmd.SetArgs([]string{name})
cmd.Flags().Set("driver", expectedDriver.Name)
assert.NoError(t, cmd.Execute())
assert.Equal(t, "ID-"+name, strings.TrimSpace(cli.OutBuffer().String()))
}
func TestSecretCreateWithLabels(t *testing.T) {
expectedLabels := map[string]string{
"lbl1": "Label-foo",

View File

@ -154,7 +154,6 @@ func TestSecretInspectPretty(t *testing.T) {
}),
SecretID("secretID"),
SecretName("secretName"),
SecretDriver("driver"),
SecretCreatedAt(time.Time{}),
SecretUpdatedAt(time.Time{}),
), []byte{}, nil

View File

@ -1,27 +1,15 @@
package secret
import (
"sort"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/cli/opts"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/swarm"
"github.com/spf13/cobra"
"golang.org/x/net/context"
"vbom.ml/util/sortorder"
)
type bySecretName []swarm.Secret
func (r bySecretName) Len() int { return len(r) }
func (r bySecretName) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
func (r bySecretName) Less(i, j int) bool {
return sortorder.NaturalLess(r[i].Spec.Name, r[j].Spec.Name)
}
type listOptions struct {
quiet bool
format string
@ -65,9 +53,6 @@ func runSecretList(dockerCli command.Cli, options listOptions) error {
format = formatter.TableFormatKey
}
}
sort.Sort(bySecretName(secrets))
secretCtx := formatter.Context{
Output: dockerCli.Out(),
Format: formatter.NewSecretFormat(format, options.quiet),

View File

@ -1,6 +1,7 @@
package secret
import (
"bytes"
"io/ioutil"
"testing"
"time"
@ -47,35 +48,29 @@ func TestSecretListErrors(t *testing.T) {
}
func TestSecretList(t *testing.T) {
buf := new(bytes.Buffer)
cli := test.NewFakeCli(&fakeClient{
secretListFunc: func(options types.SecretListOptions) ([]swarm.Secret, error) {
return []swarm.Secret{
*Secret(SecretID("ID-1-foo"),
SecretName("1-foo"),
*Secret(SecretID("ID-foo"),
SecretName("foo"),
SecretVersion(swarm.Version{Index: 10}),
SecretCreatedAt(time.Now().Add(-2*time.Hour)),
SecretUpdatedAt(time.Now().Add(-1*time.Hour)),
),
*Secret(SecretID("ID-10-foo"),
SecretName("10-foo"),
*Secret(SecretID("ID-bar"),
SecretName("bar"),
SecretVersion(swarm.Version{Index: 11}),
SecretCreatedAt(time.Now().Add(-2*time.Hour)),
SecretUpdatedAt(time.Now().Add(-1*time.Hour)),
SecretDriver("driver"),
),
*Secret(SecretID("ID-2-foo"),
SecretName("2-foo"),
SecretVersion(swarm.Version{Index: 11}),
SecretCreatedAt(time.Now().Add(-2*time.Hour)),
SecretUpdatedAt(time.Now().Add(-1*time.Hour)),
SecretDriver("driver"),
),
}, nil
},
})
cmd := newSecretListCommand(cli)
cmd.SetOutput(buf)
assert.NoError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "secret-list-sort.golden")
golden.Assert(t, cli.OutBuffer().String(), "secret-list.golden")
}
func TestSecretListWithQuietOption(t *testing.T) {

View File

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

View File

@ -1,4 +0,0 @@
ID NAME DRIVER CREATED UPDATED
ID-1-foo 1-foo 2 hours ago About an hour ago
ID-2-foo 2-foo driver 2 hours ago About an hour ago
ID-10-foo 10-foo driver 2 hours ago About an hour ago

View File

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

View File

@ -1,3 +1,3 @@
ID NAME DRIVER CREATED UPDATED
ID-bar bar 2 hours ago About an hour ago
ID-foo foo 2 hours ago About an hour ago
ID NAME CREATED UPDATED
ID-foo foo 2 hours ago About an hour ago
ID-bar bar 2 hours ago About an hour ago

View File

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

View File

@ -1,2 +1,2 @@
ID-bar
ID-foo
ID-bar

View File

@ -0,0 +1,3 @@
ID NAME CREATED UPDATED
ID-foo foo 2 hours ago About an hour ago
ID-bar bar 2 hours ago About an hour ago

View File

@ -123,7 +123,8 @@ func runCreate(dockerCli command.Cli, flags *pflag.FlagSet, opts *serviceOptions
fmt.Fprintf(dockerCli.Out(), "%s\n", response.ID)
if opts.detach || versions.LessThan(apiClient.ClientVersion(), "1.29") {
if opts.detach {
warnDetachDefault(dockerCli.Err(), apiClient.ClientVersion(), flags, "created")
return nil
}

View File

@ -1,12 +1,15 @@
package service
import (
"fmt"
"io"
"io/ioutil"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/service/progress"
"github.com/docker/docker/api/types/versions"
"github.com/docker/docker/pkg/jsonmessage"
"github.com/spf13/pflag"
"golang.org/x/net/context"
)
@ -31,3 +34,11 @@ func waitOnService(ctx context.Context, dockerCli command.Cli, serviceID string,
}
return err
}
// warnDetachDefault warns about the --detach flag future change if it's supported.
func warnDetachDefault(err io.Writer, clientVersion string, flags *pflag.FlagSet, msg string) {
if !flags.Changed("detach") && versions.GreaterThanOrEqualTo(clientVersion, "1.29") {
fmt.Fprintf(err, "Since --detach=false was not specified, tasks will be %s in the background.\n"+
"In a future release, --detach=false will become the default.\n", msg)
}
}

View File

@ -0,0 +1,40 @@
package service
import (
"bytes"
"testing"
"github.com/spf13/pflag"
"github.com/stretchr/testify/assert"
)
func TestWarnDetachDefault(t *testing.T) {
var detach bool
flags := pflag.NewFlagSet("test", pflag.ContinueOnError)
addDetachFlag(flags, &detach)
var tests = []struct {
detach bool
version string
expectWarning bool
}{
{true, "1.28", false},
{true, "1.29", false},
{false, "1.28", false},
{false, "1.29", true},
}
for _, test := range tests {
out := new(bytes.Buffer)
flags.Lookup(flagDetach).Changed = test.detach
warnDetachDefault(out, test.version, flags, "")
if test.expectWarning {
assert.NotEmpty(t, out.String(), "expected warning")
} else {
assert.Empty(t, out.String(), "expected no warning")
}
}
}

View File

@ -691,7 +691,7 @@ func buildServiceDefaultFlagMapping() flagDefaults {
}
func addDetachFlag(flags *pflag.FlagSet, detach *bool) {
flags.BoolVarP(detach, flagDetach, "d", false, "Exit immediately instead of waiting for the service to converge")
flags.BoolVarP(detach, flagDetach, "d", true, "Exit immediately instead of waiting for the service to converge")
flags.SetAnnotation(flagDetach, "version", []string{"1.29"})
}

View File

@ -7,8 +7,8 @@ import (
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/versions"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
func newRollbackCommand(dockerCli command.Cli) *cobra.Command {
@ -19,7 +19,7 @@ func newRollbackCommand(dockerCli command.Cli) *cobra.Command {
Short: "Revert changes to a service's configuration",
Args: cli.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return runRollback(dockerCli, options, args[0])
return runRollback(dockerCli, cmd.Flags(), options, args[0])
},
Tags: map[string]string{"version": "1.31"},
}
@ -31,7 +31,7 @@ func newRollbackCommand(dockerCli command.Cli) *cobra.Command {
return cmd
}
func runRollback(dockerCli command.Cli, options *serviceOptions, serviceID string) error {
func runRollback(dockerCli command.Cli, flags *pflag.FlagSet, options *serviceOptions, serviceID string) error {
apiClient := dockerCli.Client()
ctx := context.Background()
@ -56,7 +56,8 @@ func runRollback(dockerCli command.Cli, options *serviceOptions, serviceID strin
fmt.Fprintf(dockerCli.Out(), "%s\n", serviceID)
if options.detach || versions.LessThan(apiClient.ClientVersion(), "1.29") {
if options.detach {
warnDetachDefault(dockerCli.Err(), apiClient.ClientVersion(), flags, "rolled back")
return nil
}

View File

@ -10,9 +10,9 @@ import (
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/versions"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
type scaleOptions struct {
@ -27,7 +27,7 @@ func newScaleCommand(dockerCli command.Cli) *cobra.Command {
Short: "Scale one or multiple replicated services",
Args: scaleArgs,
RunE: func(cmd *cobra.Command, args []string) error {
return runScale(dockerCli, options, args)
return runScale(dockerCli, cmd.Flags(), options, args)
},
}
@ -54,7 +54,7 @@ func scaleArgs(cmd *cobra.Command, args []string) error {
return nil
}
func runScale(dockerCli command.Cli, options *scaleOptions, args []string) error {
func runScale(dockerCli command.Cli, flags *pflag.FlagSet, options *scaleOptions, args []string) error {
var errs []string
var serviceIDs []string
ctx := context.Background()
@ -79,7 +79,9 @@ func runScale(dockerCli command.Cli, options *scaleOptions, args []string) error
}
if len(serviceIDs) > 0 {
if !options.detach && versions.GreaterThanOrEqualTo(dockerCli.Client().ClientVersion(), "1.29") {
if options.detach {
warnDetachDefault(dockerCli.Err(), dockerCli.Client().ClientVersion(), flags, "scaled")
} else {
for _, serviceID := range serviceIDs {
if err := waitOnService(ctx, dockerCli, serviceID, false); err != nil {
errs = append(errs, fmt.Sprintf("%s: %v", serviceID, err))

View File

@ -59,7 +59,7 @@ func trustedResolveDigest(ctx context.Context, cli command.Cli, ref reference.Na
authConfig := command.ResolveAuthConfig(ctx, cli, repoInfo.Index)
notaryRepo, err := trust.GetNotaryRepository(cli.In(), cli.Out(), command.UserAgent(), repoInfo, &authConfig, "pull")
notaryRepo, err := trust.GetNotaryRepository(cli, repoInfo, authConfig, "pull")
if err != nil {
return nil, errors.Wrap(err, "error establishing connection to trust repository")
}

View File

@ -216,7 +216,8 @@ func runUpdate(dockerCli command.Cli, flags *pflag.FlagSet, options *serviceOpti
fmt.Fprintf(dockerCli.Out(), "%s\n", serviceID)
if options.detach || versions.LessThan(apiClient.ClientVersion(), "1.29") {
if options.detach {
warnDetachDefault(dockerCli.Err(), dockerCli.Client().ClientVersion(), flags, "updated")
return nil
}

View File

@ -104,12 +104,13 @@ func checkDaemonIsSwarmManager(ctx context.Context, dockerCli command.Cli) error
}
// pruneServices removes services that are no longer referenced in the source
func pruneServices(ctx context.Context, dockerCli command.Cli, namespace convert.Namespace, services map[string]struct{}) {
func pruneServices(ctx context.Context, dockerCli command.Cli, namespace convert.Namespace, services map[string]struct{}) bool {
client := dockerCli.Client()
oldServices, err := getServices(ctx, client, namespace.Name())
if err != nil {
fmt.Fprintf(dockerCli.Err(), "Failed to list services: %s", err)
return true
}
pruneServices := []swarm.Service{}
@ -118,5 +119,5 @@ func pruneServices(ctx context.Context, dockerCli command.Cli, namespace convert
pruneServices = append(pruneServices, service)
}
}
removeServices(ctx, dockerCli, pruneServices)
return removeServices(ctx, dockerCli, pruneServices)
}

View File

@ -59,7 +59,6 @@ func TestStackPsEmptyStack(t *testing.T) {
})
cmd := newPsCommand(fakeCli)
cmd.SetArgs([]string{"foo"})
cmd.SetOutput(ioutil.Discard)
assert.Error(t, cmd.Execute())
assert.EqualError(t, cmd.Execute(), "nothing found in stack: foo")

View File

@ -103,7 +103,7 @@ func removeServices(
var hasError bool
sort.Slice(services, sortServiceByName(services))
for _, service := range services {
fmt.Fprintf(dockerCli.Out(), "Removing service %s\n", service.Spec.Name)
fmt.Fprintf(dockerCli.Err(), "Removing service %s\n", service.Spec.Name)
if err := dockerCli.Client().ServiceRemove(ctx, service.ID); err != nil {
hasError = true
fmt.Fprintf(dockerCli.Err(), "Failed to remove service %s: %s", service.ID, err)
@ -119,7 +119,7 @@ func removeNetworks(
) bool {
var hasError bool
for _, network := range networks {
fmt.Fprintf(dockerCli.Out(), "Removing network %s\n", network.Name)
fmt.Fprintf(dockerCli.Err(), "Removing network %s\n", network.Name)
if err := dockerCli.Client().NetworkRemove(ctx, network.ID); err != nil {
hasError = true
fmt.Fprintf(dockerCli.Err(), "Failed to remove network %s: %s", network.ID, err)
@ -135,7 +135,7 @@ func removeSecrets(
) bool {
var hasError bool
for _, secret := range secrets {
fmt.Fprintf(dockerCli.Out(), "Removing secret %s\n", secret.Spec.Name)
fmt.Fprintf(dockerCli.Err(), "Removing secret %s\n", secret.Spec.Name)
if err := dockerCli.Client().SecretRemove(ctx, secret.ID); err != nil {
hasError = true
fmt.Fprintf(dockerCli.Err(), "Failed to remove secret %s: %s", secret.ID, err)
@ -151,7 +151,7 @@ func removeConfigs(
) bool {
var hasError bool
for _, config := range configs {
fmt.Fprintf(dockerCli.Out(), "Removing config %s\n", config.Spec.Name)
fmt.Fprintf(dockerCli.Err(), "Removing config %s\n", config.Spec.Name)
if err := dockerCli.Client().ConfigRemove(ctx, config.ID); err != nil {
hasError = true
fmt.Fprintf(dockerCli.Err(), "Failed to remove config %s: %s", config.ID, err)

View File

@ -101,13 +101,7 @@ func TestRemoveStackSkipEmpty(t *testing.T) {
cmd.SetArgs([]string{"foo", "bar"})
assert.NoError(t, cmd.Execute())
expectedList := []string{"Removing service bar_service1",
"Removing service bar_service2",
"Removing secret bar_secret1",
"Removing config bar_config1",
"Removing network bar_network1\n",
}
assert.Equal(t, strings.Join(expectedList, "\n"), fakeCli.OutBuffer().String())
assert.Equal(t, "", fakeCli.OutBuffer().String())
assert.Contains(t, fakeCli.ErrBuffer().String(), "Nothing found in stack: foo\n")
assert.Equal(t, allServiceIDs, fakeClient.removedServices)
assert.Equal(t, allNetworkIDs, fakeClient.removedNetworks)

View File

@ -15,20 +15,24 @@ import (
"golang.org/x/net/context"
)
type unlockOptions struct{}
func newUnlockCommand(dockerCli command.Cli) *cobra.Command {
opts := unlockOptions{}
cmd := &cobra.Command{
Use: "unlock",
Short: "Unlock swarm",
Args: cli.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
return runUnlock(dockerCli)
return runUnlock(dockerCli, opts)
},
}
return cmd
}
func runUnlock(dockerCli command.Cli) error {
func runUnlock(dockerCli command.Cli, opts unlockOptions) error {
client := dockerCli.Client()
ctx := context.Background()

View File

@ -15,6 +15,7 @@ import (
"github.com/docker/cli/templates"
"github.com/docker/docker/api/types"
eventtypes "github.com/docker/docker/api/types/events"
"github.com/docker/docker/pkg/jsonlog"
"github.com/spf13/cobra"
"golang.org/x/net/context"
)
@ -103,18 +104,14 @@ func makeTemplate(format string) (*template.Template, error) {
return tmpl, tmpl.Execute(ioutil.Discard, &eventtypes.Message{})
}
// rfc3339NanoFixed is similar to time.RFC3339Nano, except it pads nanoseconds
// zeros to maintain a fixed number of characters
const rfc3339NanoFixed = "2006-01-02T15:04:05.000000000Z07:00"
// prettyPrintEvent prints all types of event information.
// Each output includes the event type, actor id, name and action.
// Actor attributes are printed at the end if the actor has any.
func prettyPrintEvent(out io.Writer, event eventtypes.Message) error {
if event.TimeNano != 0 {
fmt.Fprintf(out, "%s ", time.Unix(0, event.TimeNano).Format(rfc3339NanoFixed))
fmt.Fprintf(out, "%s ", time.Unix(0, event.TimeNano).Format(jsonlog.RFC3339NanoFixed))
} else if event.Time != 0 {
fmt.Fprintf(out, "%s ", time.Unix(event.Time, 0).Format(rfc3339NanoFixed))
fmt.Fprintf(out, "%s ", time.Unix(event.Time, 0).Format(jsonlog.RFC3339NanoFixed))
}
fmt.Fprintf(out, "%s %s %s", event.Type, event.Action, event.Actor.ID)

View File

@ -55,50 +55,109 @@ func runInfo(dockerCli *command.DockerCli, opts *infoOptions) error {
// nolint: gocyclo
func prettyPrintInfo(dockerCli command.Cli, info types.Info) error {
fmt.Fprintln(dockerCli.Out(), "Containers:", info.Containers)
fmt.Fprintln(dockerCli.Out(), " Running:", info.ContainersRunning)
fmt.Fprintln(dockerCli.Out(), " Paused:", info.ContainersPaused)
fmt.Fprintln(dockerCli.Out(), " Stopped:", info.ContainersStopped)
fmt.Fprintln(dockerCli.Out(), "Images:", info.Images)
fprintlnNonEmpty(dockerCli.Out(), "Server Version:", info.ServerVersion)
fprintlnNonEmpty(dockerCli.Out(), "Storage Driver:", info.Driver)
fmt.Fprintf(dockerCli.Out(), "Containers: %d\n", info.Containers)
fmt.Fprintf(dockerCli.Out(), " Running: %d\n", info.ContainersRunning)
fmt.Fprintf(dockerCli.Out(), " Paused: %d\n", info.ContainersPaused)
fmt.Fprintf(dockerCli.Out(), " Stopped: %d\n", info.ContainersStopped)
fmt.Fprintf(dockerCli.Out(), "Images: %d\n", info.Images)
fprintfIfNotEmpty(dockerCli.Out(), "Server Version: %s\n", info.ServerVersion)
fprintfIfNotEmpty(dockerCli.Out(), "Storage Driver: %s\n", info.Driver)
if info.DriverStatus != nil {
for _, pair := range info.DriverStatus {
fmt.Fprintf(dockerCli.Out(), " %s: %s\n", pair[0], pair[1])
}
}
if info.SystemStatus != nil {
for _, pair := range info.SystemStatus {
fmt.Fprintf(dockerCli.Out(), "%s: %s\n", pair[0], pair[1])
}
}
fprintlnNonEmpty(dockerCli.Out(), "Logging Driver:", info.LoggingDriver)
fprintlnNonEmpty(dockerCli.Out(), "Cgroup Driver:", info.CgroupDriver)
fprintfIfNotEmpty(dockerCli.Out(), "Logging Driver: %s\n", info.LoggingDriver)
fprintfIfNotEmpty(dockerCli.Out(), "Cgroup Driver: %s\n", info.CgroupDriver)
fmt.Fprintln(dockerCli.Out(), "Plugins:")
fmt.Fprintln(dockerCli.Out(), " Volume:", strings.Join(info.Plugins.Volume, " "))
fmt.Fprintln(dockerCli.Out(), " Network:", strings.Join(info.Plugins.Network, " "))
fmt.Fprintf(dockerCli.Out(), "Plugins:\n")
fmt.Fprintf(dockerCli.Out(), " Volume:")
fmt.Fprintf(dockerCli.Out(), " %s", strings.Join(info.Plugins.Volume, " "))
fmt.Fprintf(dockerCli.Out(), "\n")
fmt.Fprintf(dockerCli.Out(), " Network:")
fmt.Fprintf(dockerCli.Out(), " %s", strings.Join(info.Plugins.Network, " "))
fmt.Fprintf(dockerCli.Out(), "\n")
if len(info.Plugins.Authorization) != 0 {
fmt.Fprintln(dockerCli.Out(), " Authorization:", strings.Join(info.Plugins.Authorization, " "))
fmt.Fprintf(dockerCli.Out(), " Authorization:")
fmt.Fprintf(dockerCli.Out(), " %s", strings.Join(info.Plugins.Authorization, " "))
fmt.Fprintf(dockerCli.Out(), "\n")
}
fmt.Fprintln(dockerCli.Out(), " Log:", strings.Join(info.Plugins.Log, " "))
fmt.Fprintf(dockerCli.Out(), " Log:")
fmt.Fprintf(dockerCli.Out(), " %s", strings.Join(info.Plugins.Log, " "))
fmt.Fprintf(dockerCli.Out(), "\n")
fmt.Fprintln(dockerCli.Out(), "Swarm:", info.Swarm.LocalNodeState)
printSwarmInfo(dockerCli, info)
fmt.Fprintf(dockerCli.Out(), "Swarm: %v\n", info.Swarm.LocalNodeState)
if info.Swarm.LocalNodeState != swarm.LocalNodeStateInactive && info.Swarm.LocalNodeState != swarm.LocalNodeStateLocked {
fmt.Fprintf(dockerCli.Out(), " NodeID: %s\n", info.Swarm.NodeID)
if info.Swarm.Error != "" {
fmt.Fprintf(dockerCli.Out(), " Error: %v\n", info.Swarm.Error)
}
fmt.Fprintf(dockerCli.Out(), " Is Manager: %v\n", info.Swarm.ControlAvailable)
if info.Swarm.Cluster != nil && info.Swarm.ControlAvailable && info.Swarm.Error == "" && info.Swarm.LocalNodeState != swarm.LocalNodeStateError {
fmt.Fprintf(dockerCli.Out(), " ClusterID: %s\n", info.Swarm.Cluster.ID)
fmt.Fprintf(dockerCli.Out(), " Managers: %d\n", info.Swarm.Managers)
fmt.Fprintf(dockerCli.Out(), " Nodes: %d\n", info.Swarm.Nodes)
fmt.Fprintf(dockerCli.Out(), " Orchestration:\n")
taskHistoryRetentionLimit := int64(0)
if info.Swarm.Cluster.Spec.Orchestration.TaskHistoryRetentionLimit != nil {
taskHistoryRetentionLimit = *info.Swarm.Cluster.Spec.Orchestration.TaskHistoryRetentionLimit
}
fmt.Fprintf(dockerCli.Out(), " Task History Retention Limit: %d\n", taskHistoryRetentionLimit)
fmt.Fprintf(dockerCli.Out(), " Raft:\n")
fmt.Fprintf(dockerCli.Out(), " Snapshot Interval: %d\n", info.Swarm.Cluster.Spec.Raft.SnapshotInterval)
if info.Swarm.Cluster.Spec.Raft.KeepOldSnapshots != nil {
fmt.Fprintf(dockerCli.Out(), " Number of Old Snapshots to Retain: %d\n", *info.Swarm.Cluster.Spec.Raft.KeepOldSnapshots)
}
fmt.Fprintf(dockerCli.Out(), " Heartbeat Tick: %d\n", info.Swarm.Cluster.Spec.Raft.HeartbeatTick)
fmt.Fprintf(dockerCli.Out(), " Election Tick: %d\n", info.Swarm.Cluster.Spec.Raft.ElectionTick)
fmt.Fprintf(dockerCli.Out(), " Dispatcher:\n")
fmt.Fprintf(dockerCli.Out(), " Heartbeat Period: %s\n", units.HumanDuration(info.Swarm.Cluster.Spec.Dispatcher.HeartbeatPeriod))
fmt.Fprintf(dockerCli.Out(), " CA Configuration:\n")
fmt.Fprintf(dockerCli.Out(), " Expiry Duration: %s\n", units.HumanDuration(info.Swarm.Cluster.Spec.CAConfig.NodeCertExpiry))
fmt.Fprintf(dockerCli.Out(), " Force Rotate: %d\n", info.Swarm.Cluster.Spec.CAConfig.ForceRotate)
fprintfIfNotEmpty(dockerCli.Out(), " Signing CA Certificate: \n%s\n\n", strings.TrimSpace(info.Swarm.Cluster.Spec.CAConfig.SigningCACert))
if len(info.Swarm.Cluster.Spec.CAConfig.ExternalCAs) > 0 {
fmt.Fprintf(dockerCli.Out(), " External CAs:\n")
for _, entry := range info.Swarm.Cluster.Spec.CAConfig.ExternalCAs {
fmt.Fprintf(dockerCli.Out(), " %s: %s\n", entry.Protocol, entry.URL)
}
}
fmt.Fprintf(dockerCli.Out(), " Autolock Managers: %v\n", info.Swarm.Cluster.Spec.EncryptionConfig.AutoLockManagers)
fmt.Fprintf(dockerCli.Out(), " Root Rotation In Progress: %v\n", info.Swarm.Cluster.RootRotationInProgress)
}
fmt.Fprintf(dockerCli.Out(), " Node Address: %s\n", info.Swarm.NodeAddr)
managers := []string{}
for _, entry := range info.Swarm.RemoteManagers {
managers = append(managers, entry.Addr)
}
if len(managers) > 0 {
sort.Strings(managers)
fmt.Fprintf(dockerCli.Out(), " Manager Addresses:\n")
for _, entry := range managers {
fmt.Fprintf(dockerCli.Out(), " %s\n", entry)
}
}
}
if len(info.Runtimes) > 0 {
fmt.Fprint(dockerCli.Out(), "Runtimes:")
fmt.Fprintf(dockerCli.Out(), "Runtimes:")
for name := range info.Runtimes {
fmt.Fprintf(dockerCli.Out(), " %s", name)
}
fmt.Fprint(dockerCli.Out(), "\n")
fmt.Fprintln(dockerCli.Out(), "Default Runtime:", info.DefaultRuntime)
fmt.Fprintf(dockerCli.Out(), "Default Runtime: %s\n", info.DefaultRuntime)
}
if info.OSType == "linux" {
fmt.Fprintln(dockerCli.Out(), "Init Binary:", info.InitBinary)
fmt.Fprintf(dockerCli.Out(), "Init Binary: %v\n", info.InitBinary)
for _, ci := range []struct {
Name string
@ -112,23 +171,23 @@ func prettyPrintInfo(dockerCli command.Cli, info types.Info) error {
if ci.Commit.ID != ci.Commit.Expected {
fmt.Fprintf(dockerCli.Out(), " (expected: %s)", ci.Commit.Expected)
}
fmt.Fprint(dockerCli.Out(), "\n")
fmt.Fprintf(dockerCli.Out(), "\n")
}
if len(info.SecurityOptions) != 0 {
kvs, err := types.DecodeSecurityOptions(info.SecurityOptions)
if err != nil {
return err
}
fmt.Fprintln(dockerCli.Out(), "Security Options:")
fmt.Fprintf(dockerCli.Out(), "Security Options:\n")
for _, so := range kvs {
fmt.Fprintln(dockerCli.Out(), " "+so.Name)
fmt.Fprintf(dockerCli.Out(), " %s\n", so.Name)
for _, o := range so.Options {
switch o.Key {
case "profile":
if o.Value != "default" {
fmt.Fprintln(dockerCli.Err(), " WARNING: You're not using the default seccomp profile")
fmt.Fprintf(dockerCli.Err(), " WARNING: You're not using the default seccomp profile\n")
}
fmt.Fprintln(dockerCli.Out(), " Profile:", o.Value)
fmt.Fprintf(dockerCli.Out(), " Profile: %s\n", o.Value)
}
}
}
@ -137,44 +196,44 @@ func prettyPrintInfo(dockerCli command.Cli, info types.Info) error {
// Isolation only has meaning on a Windows daemon.
if info.OSType == "windows" {
fmt.Fprintln(dockerCli.Out(), "Default Isolation:", info.Isolation)
fmt.Fprintf(dockerCli.Out(), "Default Isolation: %v\n", info.Isolation)
}
fprintlnNonEmpty(dockerCli.Out(), "Kernel Version:", info.KernelVersion)
fprintlnNonEmpty(dockerCli.Out(), "Operating System:", info.OperatingSystem)
fprintlnNonEmpty(dockerCli.Out(), "OSType:", info.OSType)
fprintlnNonEmpty(dockerCli.Out(), "Architecture:", info.Architecture)
fmt.Fprintln(dockerCli.Out(), "CPUs:", info.NCPU)
fmt.Fprintln(dockerCli.Out(), "Total Memory:", units.BytesSize(float64(info.MemTotal)))
fprintlnNonEmpty(dockerCli.Out(), "Name:", info.Name)
fprintlnNonEmpty(dockerCli.Out(), "ID:", info.ID)
fmt.Fprintln(dockerCli.Out(), "Docker Root Dir:", info.DockerRootDir)
fmt.Fprintln(dockerCli.Out(), "Debug Mode (client):", debug.IsEnabled())
fmt.Fprintln(dockerCli.Out(), "Debug Mode (server):", info.Debug)
fprintfIfNotEmpty(dockerCli.Out(), "Kernel Version: %s\n", info.KernelVersion)
fprintfIfNotEmpty(dockerCli.Out(), "Operating System: %s\n", info.OperatingSystem)
fprintfIfNotEmpty(dockerCli.Out(), "OSType: %s\n", info.OSType)
fprintfIfNotEmpty(dockerCli.Out(), "Architecture: %s\n", info.Architecture)
fmt.Fprintf(dockerCli.Out(), "CPUs: %d\n", info.NCPU)
fmt.Fprintf(dockerCli.Out(), "Total Memory: %s\n", units.BytesSize(float64(info.MemTotal)))
fprintfIfNotEmpty(dockerCli.Out(), "Name: %s\n", info.Name)
fprintfIfNotEmpty(dockerCli.Out(), "ID: %s\n", info.ID)
fmt.Fprintf(dockerCli.Out(), "Docker Root Dir: %s\n", info.DockerRootDir)
fmt.Fprintf(dockerCli.Out(), "Debug Mode (client): %v\n", debug.IsEnabled())
fmt.Fprintf(dockerCli.Out(), "Debug Mode (server): %v\n", info.Debug)
if info.Debug {
fmt.Fprintln(dockerCli.Out(), " File Descriptors:", info.NFd)
fmt.Fprintln(dockerCli.Out(), " Goroutines:", info.NGoroutines)
fmt.Fprintln(dockerCli.Out(), " System Time:", info.SystemTime)
fmt.Fprintln(dockerCli.Out(), " EventsListeners:", info.NEventsListener)
fmt.Fprintf(dockerCli.Out(), " File Descriptors: %d\n", info.NFd)
fmt.Fprintf(dockerCli.Out(), " Goroutines: %d\n", info.NGoroutines)
fmt.Fprintf(dockerCli.Out(), " System Time: %s\n", info.SystemTime)
fmt.Fprintf(dockerCli.Out(), " EventsListeners: %d\n", info.NEventsListener)
}
fprintlnNonEmpty(dockerCli.Out(), "HTTP Proxy:", info.HTTPProxy)
fprintlnNonEmpty(dockerCli.Out(), "HTTPS Proxy:", info.HTTPSProxy)
fprintlnNonEmpty(dockerCli.Out(), "No Proxy:", info.NoProxy)
fprintfIfNotEmpty(dockerCli.Out(), "Http Proxy: %s\n", info.HTTPProxy)
fprintfIfNotEmpty(dockerCli.Out(), "Https Proxy: %s\n", info.HTTPSProxy)
fprintfIfNotEmpty(dockerCli.Out(), "No Proxy: %s\n", info.NoProxy)
if info.IndexServerAddress != "" {
u := dockerCli.ConfigFile().AuthConfigs[info.IndexServerAddress].Username
if len(u) > 0 {
fmt.Fprintln(dockerCli.Out(), "Username:", u)
fmt.Fprintf(dockerCli.Out(), "Username: %v\n", u)
}
fmt.Fprintln(dockerCli.Out(), "Registry:", info.IndexServerAddress)
fmt.Fprintf(dockerCli.Out(), "Registry: %v\n", info.IndexServerAddress)
}
if info.Labels != nil {
fmt.Fprintln(dockerCli.Out(), "Labels:")
for _, lbl := range info.Labels {
fmt.Fprintln(dockerCli.Out(), " "+lbl)
for _, attribute := range info.Labels {
fmt.Fprintf(dockerCli.Out(), " %s\n", attribute)
}
// TODO: Engine labels with duplicate keys has been deprecated in 1.13 and will be error out
// after 3 release cycles (17.12). For now, a WARNING will be generated. The following will
@ -193,15 +252,20 @@ func prettyPrintInfo(dockerCli command.Cli, info types.Info) error {
}
}
fmt.Fprintln(dockerCli.Out(), "Experimental:", info.ExperimentalBuild)
fprintlnNonEmpty(dockerCli.Out(), "Cluster Store:", info.ClusterStore)
fprintlnNonEmpty(dockerCli.Out(), "Cluster Advertise:", info.ClusterAdvertise)
fmt.Fprintf(dockerCli.Out(), "Experimental: %v\n", info.ExperimentalBuild)
if info.ClusterStore != "" {
fmt.Fprintf(dockerCli.Out(), "Cluster Store: %s\n", info.ClusterStore)
}
if info.ClusterAdvertise != "" {
fmt.Fprintf(dockerCli.Out(), "Cluster Advertise: %s\n", info.ClusterAdvertise)
}
if info.RegistryConfig != nil && (len(info.RegistryConfig.InsecureRegistryCIDRs) > 0 || len(info.RegistryConfig.IndexConfigs) > 0) {
fmt.Fprintln(dockerCli.Out(), "Insecure Registries:")
for _, registry := range info.RegistryConfig.IndexConfigs {
if !registry.Secure {
fmt.Fprintln(dockerCli.Out(), " "+registry.Name)
fmt.Fprintf(dockerCli.Out(), " %s\n", registry.Name)
}
}
@ -214,12 +278,11 @@ func prettyPrintInfo(dockerCli command.Cli, info types.Info) error {
if info.RegistryConfig != nil && len(info.RegistryConfig.Mirrors) > 0 {
fmt.Fprintln(dockerCli.Out(), "Registry Mirrors:")
for _, mirror := range info.RegistryConfig.Mirrors {
fmt.Fprintln(dockerCli.Out(), " "+mirror)
fmt.Fprintf(dockerCli.Out(), " %s\n", mirror)
}
}
fmt.Fprintln(dockerCli.Out(), "Live Restore Enabled:", info.LiveRestoreEnabled)
fmt.Fprint(dockerCli.Out(), "\n")
fmt.Fprintf(dockerCli.Out(), "Live Restore Enabled: %v\n\n", info.LiveRestoreEnabled)
// Only output these warnings if the server does not support these features
if info.OSType != "windows" {
@ -263,63 +326,6 @@ func prettyPrintInfo(dockerCli command.Cli, info types.Info) error {
return nil
}
func printSwarmInfo(dockerCli command.Cli, info types.Info) {
if info.Swarm.LocalNodeState == swarm.LocalNodeStateInactive || info.Swarm.LocalNodeState == swarm.LocalNodeStateLocked {
return
}
fmt.Fprintln(dockerCli.Out(), " NodeID:", info.Swarm.NodeID)
if info.Swarm.Error != "" {
fmt.Fprintln(dockerCli.Out(), " Error:", info.Swarm.Error)
}
fmt.Fprintln(dockerCli.Out(), " Is Manager:", info.Swarm.ControlAvailable)
if info.Swarm.Cluster != nil && info.Swarm.ControlAvailable && info.Swarm.Error == "" && info.Swarm.LocalNodeState != swarm.LocalNodeStateError {
fmt.Fprintln(dockerCli.Out(), " ClusterID:", info.Swarm.Cluster.ID)
fmt.Fprintln(dockerCli.Out(), " Managers:", info.Swarm.Managers)
fmt.Fprintln(dockerCli.Out(), " Nodes:", info.Swarm.Nodes)
fmt.Fprintln(dockerCli.Out(), " Orchestration:")
taskHistoryRetentionLimit := int64(0)
if info.Swarm.Cluster.Spec.Orchestration.TaskHistoryRetentionLimit != nil {
taskHistoryRetentionLimit = *info.Swarm.Cluster.Spec.Orchestration.TaskHistoryRetentionLimit
}
fmt.Fprintln(dockerCli.Out(), " Task History Retention Limit:", taskHistoryRetentionLimit)
fmt.Fprintln(dockerCli.Out(), " Raft:")
fmt.Fprintln(dockerCli.Out(), " Snapshot Interval:", info.Swarm.Cluster.Spec.Raft.SnapshotInterval)
if info.Swarm.Cluster.Spec.Raft.KeepOldSnapshots != nil {
fmt.Fprintf(dockerCli.Out(), " Number of Old Snapshots to Retain: %d\n", *info.Swarm.Cluster.Spec.Raft.KeepOldSnapshots)
}
fmt.Fprintln(dockerCli.Out(), " Heartbeat Tick:", info.Swarm.Cluster.Spec.Raft.HeartbeatTick)
fmt.Fprintln(dockerCli.Out(), " Election Tick:", info.Swarm.Cluster.Spec.Raft.ElectionTick)
fmt.Fprintln(dockerCli.Out(), " Dispatcher:")
fmt.Fprintln(dockerCli.Out(), " Heartbeat Period:", units.HumanDuration(info.Swarm.Cluster.Spec.Dispatcher.HeartbeatPeriod))
fmt.Fprintln(dockerCli.Out(), " CA Configuration:")
fmt.Fprintln(dockerCli.Out(), " Expiry Duration:", units.HumanDuration(info.Swarm.Cluster.Spec.CAConfig.NodeCertExpiry))
fmt.Fprintln(dockerCli.Out(), " Force Rotate:", info.Swarm.Cluster.Spec.CAConfig.ForceRotate)
if caCert := strings.TrimSpace(info.Swarm.Cluster.Spec.CAConfig.SigningCACert); caCert != "" {
fmt.Fprintf(dockerCli.Out(), " Signing CA Certificate: \n%s\n\n", caCert)
}
if len(info.Swarm.Cluster.Spec.CAConfig.ExternalCAs) > 0 {
fmt.Fprintln(dockerCli.Out(), " External CAs:")
for _, entry := range info.Swarm.Cluster.Spec.CAConfig.ExternalCAs {
fmt.Fprintf(dockerCli.Out(), " %s: %s\n", entry.Protocol, entry.URL)
}
}
fmt.Fprintln(dockerCli.Out(), " Autolock Managers:", info.Swarm.Cluster.Spec.EncryptionConfig.AutoLockManagers)
fmt.Fprintln(dockerCli.Out(), " Root Rotation In Progress:", info.Swarm.Cluster.RootRotationInProgress)
}
fmt.Fprintln(dockerCli.Out(), " Node Address:", info.Swarm.NodeAddr)
if len(info.Swarm.RemoteManagers) > 0 {
managers := []string{}
for _, entry := range info.Swarm.RemoteManagers {
managers = append(managers, entry.Addr)
}
sort.Strings(managers)
fmt.Fprintln(dockerCli.Out(), " Manager Addresses:")
for _, entry := range managers {
fmt.Fprintf(dockerCli.Out(), " %s\n", entry)
}
}
}
func printStorageDriverWarnings(dockerCli command.Cli, info types.Info) {
if info.DriverStatus == nil {
return
@ -368,8 +374,9 @@ func formatInfo(dockerCli *command.DockerCli, info types.Info, format string) er
return err
}
func fprintlnNonEmpty(w io.Writer, label, value string) {
func fprintfIfNotEmpty(w io.Writer, format, value string) (int, error) {
if value != "" {
fmt.Fprintln(w, label, value)
return fmt.Fprintf(w, format, value)
}
return 0, nil
}

View File

@ -1,413 +0,0 @@
package trust
import (
"github.com/docker/cli/cli/trust"
"github.com/docker/notary/client"
"github.com/docker/notary/client/changelist"
"github.com/docker/notary/cryptoservice"
"github.com/docker/notary/passphrase"
"github.com/docker/notary/storage"
"github.com/docker/notary/trustmanager"
"github.com/docker/notary/tuf/data"
"github.com/docker/notary/tuf/signed"
)
// Sample mock CLI interfaces
func getOfflineNotaryRepository(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (client.Repository, error) {
return OfflineNotaryRepository{}, nil
}
// OfflineNotaryRepository is a mock Notary repository that is offline
type OfflineNotaryRepository struct{}
func (o OfflineNotaryRepository) Initialize(rootKeyIDs []string, serverManagedRoles ...data.RoleName) error {
return storage.ErrOffline{}
}
func (o OfflineNotaryRepository) InitializeWithCertificate(rootKeyIDs []string, rootCerts []data.PublicKey, serverManagedRoles ...data.RoleName) error {
return storage.ErrOffline{}
}
func (o OfflineNotaryRepository) Publish() error {
return storage.ErrOffline{}
}
func (o OfflineNotaryRepository) AddTarget(target *client.Target, roles ...data.RoleName) error {
return nil
}
func (o OfflineNotaryRepository) RemoveTarget(targetName string, roles ...data.RoleName) error {
return nil
}
func (o OfflineNotaryRepository) ListTargets(roles ...data.RoleName) ([]*client.TargetWithRole, error) {
return nil, storage.ErrOffline{}
}
func (o OfflineNotaryRepository) GetTargetByName(name string, roles ...data.RoleName) (*client.TargetWithRole, error) {
return nil, storage.ErrOffline{}
}
func (o OfflineNotaryRepository) GetAllTargetMetadataByName(name string) ([]client.TargetSignedStruct, error) {
return nil, storage.ErrOffline{}
}
func (o OfflineNotaryRepository) GetChangelist() (changelist.Changelist, error) {
return changelist.NewMemChangelist(), nil
}
func (o OfflineNotaryRepository) ListRoles() ([]client.RoleWithSignatures, error) {
return nil, storage.ErrOffline{}
}
func (o OfflineNotaryRepository) GetDelegationRoles() ([]data.Role, error) {
return nil, storage.ErrOffline{}
}
func (o OfflineNotaryRepository) AddDelegation(name data.RoleName, delegationKeys []data.PublicKey, paths []string) error {
return nil
}
func (o OfflineNotaryRepository) AddDelegationRoleAndKeys(name data.RoleName, delegationKeys []data.PublicKey) error {
return nil
}
func (o OfflineNotaryRepository) AddDelegationPaths(name data.RoleName, paths []string) error {
return nil
}
func (o OfflineNotaryRepository) RemoveDelegationKeysAndPaths(name data.RoleName, keyIDs, paths []string) error {
return nil
}
func (o OfflineNotaryRepository) RemoveDelegationRole(name data.RoleName) error {
return nil
}
func (o OfflineNotaryRepository) RemoveDelegationPaths(name data.RoleName, paths []string) error {
return nil
}
func (o OfflineNotaryRepository) RemoveDelegationKeys(name data.RoleName, keyIDs []string) error {
return nil
}
func (o OfflineNotaryRepository) ClearDelegationPaths(name data.RoleName) error {
return nil
}
func (o OfflineNotaryRepository) Witness(roles ...data.RoleName) ([]data.RoleName, error) {
return nil, nil
}
func (o OfflineNotaryRepository) RotateKey(role data.RoleName, serverManagesKey bool, keyList []string) error {
return storage.ErrOffline{}
}
func (o OfflineNotaryRepository) GetCryptoService() signed.CryptoService {
return nil
}
func (o OfflineNotaryRepository) SetLegacyVersions(version int) {}
func (o OfflineNotaryRepository) GetGUN() data.GUN {
return data.GUN("gun")
}
func getUninitializedNotaryRepository(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (client.Repository, error) {
return UninitializedNotaryRepository{}, nil
}
// UninitializedNotaryRepository is a mock Notary repository that is uninintialized
// it builds on top of the OfflineNotaryRepository, instead returning ErrRepositoryNotExist
// for any online operation
type UninitializedNotaryRepository struct {
OfflineNotaryRepository
}
func (u UninitializedNotaryRepository) Initialize(rootKeyIDs []string, serverManagedRoles ...data.RoleName) error {
return client.ErrRepositoryNotExist{}
}
func (u UninitializedNotaryRepository) InitializeWithCertificate(rootKeyIDs []string, rootCerts []data.PublicKey, serverManagedRoles ...data.RoleName) error {
return client.ErrRepositoryNotExist{}
}
func (u UninitializedNotaryRepository) Publish() error {
return client.ErrRepositoryNotExist{}
}
func (u UninitializedNotaryRepository) ListTargets(roles ...data.RoleName) ([]*client.TargetWithRole, error) {
return nil, client.ErrRepositoryNotExist{}
}
func (u UninitializedNotaryRepository) GetTargetByName(name string, roles ...data.RoleName) (*client.TargetWithRole, error) {
return nil, client.ErrRepositoryNotExist{}
}
func (u UninitializedNotaryRepository) GetAllTargetMetadataByName(name string) ([]client.TargetSignedStruct, error) {
return nil, client.ErrRepositoryNotExist{}
}
func (u UninitializedNotaryRepository) ListRoles() ([]client.RoleWithSignatures, error) {
return nil, client.ErrRepositoryNotExist{}
}
func (u UninitializedNotaryRepository) GetDelegationRoles() ([]data.Role, error) {
return nil, client.ErrRepositoryNotExist{}
}
func (u UninitializedNotaryRepository) RotateKey(role data.RoleName, serverManagesKey bool, keyList []string) error {
return client.ErrRepositoryNotExist{}
}
func getEmptyTargetsNotaryRepository(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (client.Repository, error) {
return EmptyTargetsNotaryRepository{}, nil
}
// EmptyTargetsNotaryRepository is a mock Notary repository that is initialized
// but does not have any signed targets
type EmptyTargetsNotaryRepository struct {
OfflineNotaryRepository
}
func (e EmptyTargetsNotaryRepository) Initialize(rootKeyIDs []string, serverManagedRoles ...data.RoleName) error {
return nil
}
func (e EmptyTargetsNotaryRepository) InitializeWithCertificate(rootKeyIDs []string, rootCerts []data.PublicKey, serverManagedRoles ...data.RoleName) error {
return nil
}
func (e EmptyTargetsNotaryRepository) Publish() error {
return nil
}
func (e EmptyTargetsNotaryRepository) ListTargets(roles ...data.RoleName) ([]*client.TargetWithRole, error) {
return []*client.TargetWithRole{}, nil
}
func (e EmptyTargetsNotaryRepository) GetTargetByName(name string, roles ...data.RoleName) (*client.TargetWithRole, error) {
return nil, client.ErrNoSuchTarget(name)
}
func (e EmptyTargetsNotaryRepository) GetAllTargetMetadataByName(name string) ([]client.TargetSignedStruct, error) {
return nil, client.ErrNoSuchTarget(name)
}
func (e EmptyTargetsNotaryRepository) ListRoles() ([]client.RoleWithSignatures, error) {
return []client.RoleWithSignatures{}, nil
}
func (e EmptyTargetsNotaryRepository) GetDelegationRoles() ([]data.Role, error) {
return []data.Role{}, nil
}
func (e EmptyTargetsNotaryRepository) RotateKey(role data.RoleName, serverManagesKey bool, keyList []string) error {
return nil
}
func getLoadedNotaryRepository(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (client.Repository, error) {
return LoadedNotaryRepository{}, nil
}
// LoadedNotaryRepository is a mock Notary repository that is loaded with targets, delegations, and keys
type LoadedNotaryRepository struct {
EmptyTargetsNotaryRepository
statefulCryptoService signed.CryptoService
}
// LoadedNotaryRepository has three delegations:
// - targets/releases: includes keys A and B
// - targets/alice: includes key A
// - targets/bob: includes key B
var loadedReleasesRole = data.DelegationRole{
BaseRole: data.BaseRole{
Name: "targets/releases",
Keys: map[string]data.PublicKey{"A": nil, "B": nil},
Threshold: 1,
},
}
var loadedAliceRole = data.DelegationRole{
BaseRole: data.BaseRole{
Name: "targets/alice",
Keys: map[string]data.PublicKey{"A": nil},
Threshold: 1,
},
}
var loadedBobRole = data.DelegationRole{
BaseRole: data.BaseRole{
Name: "targets/bob",
Keys: map[string]data.PublicKey{"B": nil},
Threshold: 1,
},
}
var loadedDelegationRoles = []data.Role{
{
Name: loadedReleasesRole.Name,
RootRole: data.RootRole{
KeyIDs: []string{"A", "B"},
Threshold: 1,
},
},
{
Name: loadedAliceRole.Name,
RootRole: data.RootRole{
KeyIDs: []string{"A"},
Threshold: 1,
},
},
{
Name: loadedBobRole.Name,
RootRole: data.RootRole{
KeyIDs: []string{"B"},
Threshold: 1,
},
},
}
var loadedTargetsRole = data.DelegationRole{
BaseRole: data.BaseRole{
Name: data.CanonicalTargetsRole,
Keys: map[string]data.PublicKey{"C": nil},
Threshold: 1,
},
}
// LoadedNotaryRepository has three targets:
// - red: signed by targets/releases, targets/alice, targets/bob
// - blue: signed by targets/releases, targets/alice
// - green: signed by targets/releases
var loadedRedTarget = client.Target{
Name: "red",
Hashes: data.Hashes{"sha256": []byte("red-digest")},
}
var loadedBlueTarget = client.Target{
Name: "blue",
Hashes: data.Hashes{"sha256": []byte("blue-digest")},
}
var loadedGreenTarget = client.Target{
Name: "green",
Hashes: data.Hashes{"sha256": []byte("green-digest")},
}
var loadedTargets = []client.TargetSignedStruct{
// red is signed by all three delegations
{Target: loadedRedTarget, Role: loadedReleasesRole},
{Target: loadedRedTarget, Role: loadedAliceRole},
{Target: loadedRedTarget, Role: loadedBobRole},
// blue is signed by targets/releases, targets/alice
{Target: loadedBlueTarget, Role: loadedReleasesRole},
{Target: loadedBlueTarget, Role: loadedAliceRole},
// green is signed by targets/releases
{Target: loadedGreenTarget, Role: loadedReleasesRole},
}
func (l LoadedNotaryRepository) ListRoles() ([]client.RoleWithSignatures, error) {
rootRole := data.Role{
RootRole: data.RootRole{
KeyIDs: []string{"rootID"},
Threshold: 1,
},
Name: data.CanonicalRootRole,
}
targetsRole := data.Role{
RootRole: data.RootRole{
KeyIDs: []string{"targetsID"},
Threshold: 1,
},
Name: data.CanonicalTargetsRole,
}
return []client.RoleWithSignatures{{Role: rootRole}, {Role: targetsRole}}, nil
}
func (l LoadedNotaryRepository) ListTargets(roles ...data.RoleName) ([]*client.TargetWithRole, error) {
filteredTargets := []*client.TargetWithRole{}
for _, tgt := range loadedTargets {
if len(roles) == 0 || (len(roles) > 0 && roles[0] == tgt.Role.Name) {
filteredTargets = append(filteredTargets, &client.TargetWithRole{Target: tgt.Target, Role: tgt.Role.Name})
}
}
return filteredTargets, nil
}
func (l LoadedNotaryRepository) GetTargetByName(name string, roles ...data.RoleName) (*client.TargetWithRole, error) {
for _, tgt := range loadedTargets {
if name == tgt.Target.Name {
if len(roles) == 0 || (len(roles) > 0 && roles[0] == tgt.Role.Name) {
return &client.TargetWithRole{Target: tgt.Target, Role: tgt.Role.Name}, nil
}
}
}
return nil, client.ErrNoSuchTarget(name)
}
func (l LoadedNotaryRepository) GetAllTargetMetadataByName(name string) ([]client.TargetSignedStruct, error) {
if name == "" {
return loadedTargets, nil
}
filteredTargets := []client.TargetSignedStruct{}
for _, tgt := range loadedTargets {
if name == tgt.Target.Name {
filteredTargets = append(filteredTargets, tgt)
}
}
if len(filteredTargets) == 0 {
return nil, client.ErrNoSuchTarget(name)
}
return filteredTargets, nil
}
func (l LoadedNotaryRepository) GetGUN() data.GUN {
return data.GUN("signed-repo")
}
func (l LoadedNotaryRepository) GetDelegationRoles() ([]data.Role, error) {
return loadedDelegationRoles, nil
}
func (l LoadedNotaryRepository) GetCryptoService() signed.CryptoService {
if l.statefulCryptoService == nil {
// give it an in-memory cryptoservice with a root key and targets key
l.statefulCryptoService = cryptoservice.NewCryptoService(trustmanager.NewKeyMemoryStore(passphrase.ConstantRetriever("password")))
l.statefulCryptoService.AddKey(data.CanonicalRootRole, l.GetGUN(), nil)
l.statefulCryptoService.AddKey(data.CanonicalTargetsRole, l.GetGUN(), nil)
}
return l.statefulCryptoService
}
func getLoadedWithNoSignersNotaryRepository(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (client.Repository, error) {
return LoadedWithNoSignersNotaryRepository{}, nil
}
// LoadedWithNoSignersNotaryRepository is a mock Notary repository that is loaded with targets but no delegations
// it only contains the green target
type LoadedWithNoSignersNotaryRepository struct {
LoadedNotaryRepository
}
func (l LoadedWithNoSignersNotaryRepository) ListTargets(roles ...data.RoleName) ([]*client.TargetWithRole, error) {
filteredTargets := []*client.TargetWithRole{}
for _, tgt := range loadedTargets {
if len(roles) == 0 || (len(roles) > 0 && roles[0] == tgt.Role.Name) {
filteredTargets = append(filteredTargets, &client.TargetWithRole{Target: tgt.Target, Role: tgt.Role.Name})
}
}
return filteredTargets, nil
}
func (l LoadedWithNoSignersNotaryRepository) GetTargetByName(name string, roles ...data.RoleName) (*client.TargetWithRole, error) {
if name == "" || name == loadedGreenTarget.Name {
return &client.TargetWithRole{Target: loadedGreenTarget, Role: data.CanonicalTargetsRole}, nil
}
return nil, client.ErrNoSuchTarget(name)
}
func (l LoadedWithNoSignersNotaryRepository) GetAllTargetMetadataByName(name string) ([]client.TargetSignedStruct, error) {
if name == "" || name == loadedGreenTarget.Name {
return []client.TargetSignedStruct{{Target: loadedGreenTarget, Role: loadedTargetsRole}}, nil
}
return nil, client.ErrNoSuchTarget(name)
}
func (l LoadedWithNoSignersNotaryRepository) GetDelegationRoles() ([]data.Role, error) {
return []data.Role{}, nil
}

View File

@ -1,23 +0,0 @@
package trust
import (
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
)
// NewTrustCommand returns a cobra command for `trust` subcommands
func NewTrustCommand(dockerCli command.Cli) *cobra.Command {
cmd := &cobra.Command{
Use: "trust",
Short: "Manage trust on Docker images (experimental)",
Args: cli.NoArgs,
RunE: command.ShowHelp(dockerCli.Err()),
}
cmd.AddCommand(
newViewCommand(dockerCli),
newRevokeCommand(dockerCli),
newSignCommand(dockerCli),
)
return cmd
}

View File

@ -1,33 +0,0 @@
package trust
import (
"strings"
"github.com/docker/cli/cli/trust"
"github.com/docker/notary/client"
"github.com/docker/notary/tuf/data"
)
const releasedRoleName = "Repo Admin"
// check if a role name is "released": either targets/releases or targets TUF roles
func isReleasedTarget(role data.RoleName) bool {
return role == data.CanonicalTargetsRole || role == trust.ReleasesRole
}
// convert TUF role name to a human-understandable signer name
func notaryRoleToSigner(tufRole data.RoleName) string {
// don't show a signer for "targets" or "targets/releases"
if isReleasedTarget(data.RoleName(tufRole.String())) {
return releasedRoleName
}
return strings.TrimPrefix(tufRole.String(), "targets/")
}
func clearChangeList(notaryRepo client.Repository) error {
cl, err := notaryRepo.GetChangelist()
if err != nil {
return err
}
return cl.Clear("")
}

View File

@ -1,125 +0,0 @@
package trust
import (
"context"
"fmt"
"os"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/image"
"github.com/docker/cli/cli/trust"
"github.com/docker/notary/client"
"github.com/docker/notary/tuf/data"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
type revokeOptions struct {
forceYes bool
}
func newRevokeCommand(dockerCli command.Cli) *cobra.Command {
options := revokeOptions{}
cmd := &cobra.Command{
Use: "revoke [OPTIONS] IMAGE[:TAG]",
Short: "Remove trust for an image",
Args: cli.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return revokeTrust(dockerCli, args[0], options)
},
}
flags := cmd.Flags()
flags.BoolVarP(&options.forceYes, "yes", "y", false, "Do not prompt for confirmation")
return cmd
}
func revokeTrust(cli command.Cli, remote string, options revokeOptions) error {
ctx := context.Background()
imgRefAndAuth, err := trust.GetImageReferencesAndAuth(ctx, image.AuthResolver(cli), remote)
if err != nil {
return err
}
tag := imgRefAndAuth.Tag()
if imgRefAndAuth.Tag() == "" && imgRefAndAuth.Digest() != "" {
return fmt.Errorf("cannot use a digest reference for IMAGE:TAG")
}
if imgRefAndAuth.Tag() == "" && !options.forceYes {
deleteRemote := command.PromptForConfirmation(os.Stdin, cli.Out(), fmt.Sprintf("Please confirm you would like to delete all signature data for %s?", remote))
if !deleteRemote {
fmt.Fprintf(cli.Out(), "\nAborting action.\n")
return nil
}
}
notaryRepo, err := cli.NotaryClient(imgRefAndAuth, trust.ActionsPushAndPull)
if err != nil {
return err
}
if err = clearChangeList(notaryRepo); err != nil {
return err
}
defer clearChangeList(notaryRepo)
if err := revokeSignature(notaryRepo, tag); err != nil {
return errors.Wrapf(err, "could not remove signature for %s", remote)
}
fmt.Fprintf(cli.Out(), "Successfully deleted signature for %s\n", remote)
return nil
}
func revokeSignature(notaryRepo client.Repository, tag string) error {
if tag != "" {
// Revoke signature for the specified tag
if err := revokeSingleSig(notaryRepo, tag); err != nil {
return err
}
} else {
// revoke all signatures for the image, as no tag was given
if err := revokeAllSigs(notaryRepo); err != nil {
return err
}
}
// Publish change
return notaryRepo.Publish()
}
func revokeSingleSig(notaryRepo client.Repository, tag string) error {
releasedTargetWithRole, err := notaryRepo.GetTargetByName(tag, trust.ReleasesRole, data.CanonicalTargetsRole)
if err != nil {
return err
}
releasedTarget := releasedTargetWithRole.Target
return getSignableRolesForTargetAndRemove(releasedTarget, notaryRepo)
}
func revokeAllSigs(notaryRepo client.Repository) error {
releasedTargetWithRoleList, err := notaryRepo.ListTargets(trust.ReleasesRole, data.CanonicalTargetsRole)
if err != nil {
return err
}
if len(releasedTargetWithRoleList) == 0 {
return fmt.Errorf("no signed tags to remove")
}
// we need all the roles that signed each released target so we can remove from all roles.
for _, releasedTargetWithRole := range releasedTargetWithRoleList {
// remove from all roles
if err := getSignableRolesForTargetAndRemove(releasedTargetWithRole.Target, notaryRepo); err != nil {
return err
}
}
return nil
}
// get all the roles that signed the target and removes it from all roles.
func getSignableRolesForTargetAndRemove(releasedTarget client.Target, notaryRepo client.Repository) error {
signableRoles, err := trust.GetSignableRoles(notaryRepo, &releasedTarget)
if err != nil {
return err
}
// remove from all roles
return notaryRepo.RemoveTarget(releasedTarget.Name, signableRoles...)
}

View File

@ -1,148 +0,0 @@
package trust
import (
"io/ioutil"
"os"
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/testutil"
"github.com/docker/notary/client"
"github.com/docker/notary/passphrase"
"github.com/docker/notary/trustpinning"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestTrustRevokeCommandErrors(t *testing.T) {
testCases := []struct {
name string
args []string
expectedError string
}{
{
name: "not-enough-args",
expectedError: "requires exactly 1 argument",
},
{
name: "too-many-args",
args: []string{"remote1", "remote2"},
expectedError: "requires exactly 1 argument",
},
{
name: "sha-reference",
args: []string{"870d292919d01a0af7e7f056271dc78792c05f55f49b9b9012b6d89725bd9abd"},
expectedError: "invalid repository name",
},
{
name: "invalid-img-reference",
args: []string{"ALPINE"},
expectedError: "invalid reference format",
},
{
name: "digest-reference",
args: []string{"ubuntu@sha256:45b23dee08af5e43a7fea6c4cf9c25ccf269ee113168c19722f87876677c5cb2"},
expectedError: "cannot use a digest reference for IMAGE:TAG",
},
}
for _, tc := range testCases {
cmd := newRevokeCommand(
test.NewFakeCli(&fakeClient{}))
cmd.SetArgs(tc.args)
cmd.SetOutput(ioutil.Discard)
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
}
}
func TestTrustRevokeCommandOfflineErrors(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(getOfflineNotaryRepository)
cmd := newRevokeCommand(cli)
cmd.SetArgs([]string{"reg-name.io/image"})
cmd.SetOutput(ioutil.Discard)
assert.NoError(t, cmd.Execute())
assert.Contains(t, cli.OutBuffer().String(), "Please confirm you would like to delete all signature data for reg-name.io/image? [y/N] \nAborting action.")
cli = test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(getOfflineNotaryRepository)
cmd = newRevokeCommand(cli)
cmd.SetArgs([]string{"reg-name.io/image", "-y"})
cmd.SetOutput(ioutil.Discard)
cli = test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(getOfflineNotaryRepository)
cmd = newRevokeCommand(cli)
cmd.SetArgs([]string{"reg-name.io/image:tag"})
cmd.SetOutput(ioutil.Discard)
testutil.ErrorContains(t, cmd.Execute(), "could not remove signature for reg-name.io/image:tag: client is offline")
}
func TestTrustRevokeCommandUninitializedErrors(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(getUninitializedNotaryRepository)
cmd := newRevokeCommand(cli)
cmd.SetArgs([]string{"reg-name.io/image"})
cmd.SetOutput(ioutil.Discard)
assert.NoError(t, cmd.Execute())
assert.Contains(t, cli.OutBuffer().String(), "Please confirm you would like to delete all signature data for reg-name.io/image? [y/N] \nAborting action.")
cli = test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(getUninitializedNotaryRepository)
cmd = newRevokeCommand(cli)
cmd.SetArgs([]string{"reg-name.io/image", "-y"})
cmd.SetOutput(ioutil.Discard)
testutil.ErrorContains(t, cmd.Execute(), "could not remove signature for reg-name.io/image: does not have trust data for")
cli = test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(getUninitializedNotaryRepository)
cmd = newRevokeCommand(cli)
cmd.SetArgs([]string{"reg-name.io/image:tag"})
cmd.SetOutput(ioutil.Discard)
testutil.ErrorContains(t, cmd.Execute(), "could not remove signature for reg-name.io/image:tag: does not have trust data for")
}
func TestTrustRevokeCommandEmptyNotaryRepo(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(getEmptyTargetsNotaryRepository)
cmd := newRevokeCommand(cli)
cmd.SetArgs([]string{"reg-name.io/image"})
cmd.SetOutput(ioutil.Discard)
assert.NoError(t, cmd.Execute())
assert.Contains(t, cli.OutBuffer().String(), "Please confirm you would like to delete all signature data for reg-name.io/image? [y/N] \nAborting action.")
cli = test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(getEmptyTargetsNotaryRepository)
cmd = newRevokeCommand(cli)
cmd.SetArgs([]string{"reg-name.io/image", "-y"})
cmd.SetOutput(ioutil.Discard)
testutil.ErrorContains(t, cmd.Execute(), "could not remove signature for reg-name.io/image: no signed tags to remove")
cli = test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(getEmptyTargetsNotaryRepository)
cmd = newRevokeCommand(cli)
cmd.SetArgs([]string{"reg-name.io/image:tag"})
cmd.SetOutput(ioutil.Discard)
testutil.ErrorContains(t, cmd.Execute(), "could not remove signature for reg-name.io/image:tag: No valid trust data for tag")
}
func TestNewRevokeTrustAllSigConfirmation(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(getEmptyTargetsNotaryRepository)
cmd := newRevokeCommand(cli)
cmd.SetArgs([]string{"alpine"})
assert.NoError(t, cmd.Execute())
assert.Contains(t, cli.OutBuffer().String(), "Please confirm you would like to delete all signature data for alpine? [y/N] \nAborting action.")
}
func TestGetSignableRolesForTargetAndRemoveError(t *testing.T) {
tmpDir, err := ioutil.TempDir("", "notary-test-")
assert.NoError(t, err)
defer os.RemoveAll(tmpDir)
notaryRepo, err := client.NewFileCachedRepository(tmpDir, "gun", "https://localhost", nil, passphrase.ConstantRetriever("password"), trustpinning.TrustPinConfig{})
require.NoError(t, err)
target := client.Target{}
err = getSignableRolesForTargetAndRemove(target, notaryRepo)
assert.EqualError(t, err, "client is offline")
}

View File

@ -1,227 +0,0 @@
package trust
import (
"context"
"fmt"
"io"
"path"
"sort"
"strings"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/image"
"github.com/docker/cli/cli/trust"
"github.com/docker/notary/client"
"github.com/docker/notary/tuf/data"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
func newSignCommand(dockerCli command.Cli) *cobra.Command {
cmd := &cobra.Command{
Use: "sign IMAGE:TAG",
Short: "Sign an image",
Args: cli.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return runSignImage(dockerCli, args[0])
},
}
return cmd
}
func runSignImage(cli command.Cli, imageName string) error {
ctx := context.Background()
imgRefAndAuth, err := trust.GetImageReferencesAndAuth(ctx, image.AuthResolver(cli), imageName)
if err != nil {
return err
}
if err := validateTag(imgRefAndAuth); err != nil {
return err
}
notaryRepo, err := cli.NotaryClient(imgRefAndAuth, trust.ActionsPushAndPull)
if err != nil {
return trust.NotaryError(imgRefAndAuth.Reference().Name(), err)
}
if err = clearChangeList(notaryRepo); err != nil {
return err
}
defer clearChangeList(notaryRepo)
// get the latest repository metadata so we can figure out which roles to sign
if _, err = notaryRepo.ListTargets(); err != nil {
switch err.(type) {
case client.ErrRepoNotInitialized, client.ErrRepositoryNotExist:
// before initializing a new repo, check that the image exists locally:
if err := checkLocalImageExistence(ctx, cli, imageName); err != nil {
return err
}
userRole := data.RoleName(path.Join(data.CanonicalTargetsRole.String(), imgRefAndAuth.AuthConfig().Username))
if err := initNotaryRepoWithSigners(notaryRepo, userRole); err != nil {
return trust.NotaryError(imgRefAndAuth.Reference().Name(), err)
}
fmt.Fprintf(cli.Out(), "Created signer: %s\n", imgRefAndAuth.AuthConfig().Username)
fmt.Fprintf(cli.Out(), "Finished initializing signed repository for %s\n", imageName)
default:
return trust.NotaryError(imgRefAndAuth.RepoInfo().Name.Name(), err)
}
}
requestPrivilege := command.RegistryAuthenticationPrivilegedFunc(cli, imgRefAndAuth.RepoInfo().Index, "push")
target, err := createTarget(notaryRepo, imgRefAndAuth.Tag())
if err != nil {
switch err := err.(type) {
case client.ErrNoSuchTarget, client.ErrRepositoryNotExist:
// Fail fast if the image doesn't exist locally
if err := checkLocalImageExistence(ctx, cli, imageName); err != nil {
return err
}
return image.TrustedPush(ctx, cli, imgRefAndAuth.RepoInfo(), imgRefAndAuth.Reference(), *imgRefAndAuth.AuthConfig(), requestPrivilege)
default:
return err
}
}
return signAndPublishToTarget(cli.Out(), imgRefAndAuth, notaryRepo, target)
}
func signAndPublishToTarget(out io.Writer, imgRefAndAuth trust.ImageRefAndAuth, notaryRepo client.Repository, target client.Target) error {
tag := imgRefAndAuth.Tag()
fmt.Fprintf(out, "Signing and pushing trust metadata for %s\n", imgRefAndAuth.Name())
existingSigInfo, err := getExistingSignatureInfoForReleasedTag(notaryRepo, tag)
if err != nil {
return err
}
err = image.AddTargetToAllSignableRoles(notaryRepo, &target)
if err == nil {
prettyPrintExistingSignatureInfo(out, existingSigInfo)
err = notaryRepo.Publish()
}
if err != nil {
return errors.Wrapf(err, "failed to sign %q:%s", imgRefAndAuth.RepoInfo().Name.Name(), tag)
}
fmt.Fprintf(out, "Successfully signed %q:%s\n", imgRefAndAuth.RepoInfo().Name.Name(), tag)
return nil
}
func validateTag(imgRefAndAuth trust.ImageRefAndAuth) error {
tag := imgRefAndAuth.Tag()
if tag == "" {
if imgRefAndAuth.Digest() != "" {
return fmt.Errorf("cannot use a digest reference for IMAGE:TAG")
}
return fmt.Errorf("No tag specified for %s", imgRefAndAuth.Name())
}
return nil
}
func checkLocalImageExistence(ctx context.Context, cli command.Cli, imageName string) error {
_, _, err := cli.Client().ImageInspectWithRaw(ctx, imageName)
return err
}
func createTarget(notaryRepo client.Repository, tag string) (client.Target, error) {
target := &client.Target{}
var err error
if tag == "" {
return *target, fmt.Errorf("No tag specified")
}
target.Name = tag
target.Hashes, target.Length, err = getSignedManifestHashAndSize(notaryRepo, tag)
return *target, err
}
func getSignedManifestHashAndSize(notaryRepo client.Repository, tag string) (data.Hashes, int64, error) {
targets, err := notaryRepo.GetAllTargetMetadataByName(tag)
if err != nil {
return nil, 0, err
}
return getReleasedTargetHashAndSize(targets, tag)
}
func getReleasedTargetHashAndSize(targets []client.TargetSignedStruct, tag string) (data.Hashes, int64, error) {
for _, tgt := range targets {
if isReleasedTarget(tgt.Role.Name) {
return tgt.Target.Hashes, tgt.Target.Length, nil
}
}
return nil, 0, client.ErrNoSuchTarget(tag)
}
func getExistingSignatureInfoForReleasedTag(notaryRepo client.Repository, tag string) (trustTagRow, error) {
targets, err := notaryRepo.GetAllTargetMetadataByName(tag)
if err != nil {
return trustTagRow{}, err
}
releasedTargetInfoList := matchReleasedSignatures(targets)
if len(releasedTargetInfoList) == 0 {
return trustTagRow{}, nil
}
return releasedTargetInfoList[0], nil
}
func prettyPrintExistingSignatureInfo(out io.Writer, existingSigInfo trustTagRow) {
sort.Strings(existingSigInfo.Signers)
joinedSigners := strings.Join(existingSigInfo.Signers, ", ")
fmt.Fprintf(out, "Existing signatures for tag %s digest %s from:\n%s\n", existingSigInfo.TagName, existingSigInfo.HashHex, joinedSigners)
}
func initNotaryRepoWithSigners(notaryRepo client.Repository, newSigner data.RoleName) error {
rootKey, err := getOrGenerateNotaryKey(notaryRepo, data.CanonicalRootRole)
if err != nil {
return err
}
rootKeyID := rootKey.ID()
// Initialize the notary repository with a remotely managed snapshot key
if err := notaryRepo.Initialize([]string{rootKeyID}, data.CanonicalSnapshotRole); err != nil {
return err
}
signerKey, err := getOrGenerateNotaryKey(notaryRepo, newSigner)
if err != nil {
return err
}
addStagedSigner(notaryRepo, newSigner, []data.PublicKey{signerKey})
return notaryRepo.Publish()
}
// generates an ECDSA key without a GUN for the specified role
func getOrGenerateNotaryKey(notaryRepo client.Repository, role data.RoleName) (data.PublicKey, error) {
// use the signer name in the PEM headers if this is a delegation key
if data.IsDelegation(role) {
role = data.RoleName(notaryRoleToSigner(role))
}
keys := notaryRepo.GetCryptoService().ListKeys(role)
var err error
var key data.PublicKey
// always select the first key by ID
if len(keys) > 0 {
sort.Strings(keys)
keyID := keys[0]
privKey, _, err := notaryRepo.GetCryptoService().GetPrivateKey(keyID)
if err != nil {
return nil, err
}
key = data.PublicKeyFromPrivate(privKey)
} else {
key, err = notaryRepo.GetCryptoService().Create(role, "", data.ECDSAKey)
if err != nil {
return nil, err
}
}
return key, nil
}
// stages changes to add a signer with the specified name and key(s). Adds to targets/<name> and targets/releases
func addStagedSigner(notaryRepo client.Repository, newSigner data.RoleName, signerKeys []data.PublicKey) {
// create targets/<username>
notaryRepo.AddDelegationRoleAndKeys(newSigner, signerKeys)
notaryRepo.AddDelegationPaths(newSigner, []string{""})
// create targets/releases
notaryRepo.AddDelegationRoleAndKeys(trust.ReleasesRole, signerKeys)
notaryRepo.AddDelegationPaths(trust.ReleasesRole, []string{""})
}

View File

@ -1,297 +0,0 @@
package trust
import (
"encoding/json"
"io/ioutil"
"os"
"testing"
"bytes"
"github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/trust"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/testutil"
"github.com/docker/notary"
"github.com/docker/notary/client"
"github.com/docker/notary/client/changelist"
"github.com/docker/notary/passphrase"
"github.com/docker/notary/trustpinning"
"github.com/docker/notary/tuf/data"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const passwd = "password"
func TestTrustSignCommandErrors(t *testing.T) {
testCases := []struct {
name string
args []string
expectedError string
}{
{
name: "not-enough-args",
expectedError: "requires exactly 1 argument",
},
{
name: "too-many-args",
args: []string{"image", "tag"},
expectedError: "requires exactly 1 argument",
},
{
name: "sha-reference",
args: []string{"870d292919d01a0af7e7f056271dc78792c05f55f49b9b9012b6d89725bd9abd"},
expectedError: "invalid repository name",
},
{
name: "invalid-img-reference",
args: []string{"ALPINE:latest"},
expectedError: "invalid reference format",
},
{
name: "no-tag",
args: []string{"reg/img"},
expectedError: "No tag specified for reg/img",
},
{
name: "digest-reference",
args: []string{"ubuntu@sha256:45b23dee08af5e43a7fea6c4cf9c25ccf269ee113168c19722f87876677c5cb2"},
expectedError: "cannot use a digest reference for IMAGE:TAG",
},
}
// change to a tmpdir
tmpDir, err := ioutil.TempDir("", "docker-sign-test-")
assert.NoError(t, err)
defer os.RemoveAll(tmpDir)
config.SetDir(tmpDir)
for _, tc := range testCases {
cmd := newSignCommand(
test.NewFakeCli(&fakeClient{}))
cmd.SetArgs(tc.args)
cmd.SetOutput(ioutil.Discard)
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
}
}
func TestTrustSignCommandOfflineErrors(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(getOfflineNotaryRepository)
cmd := newSignCommand(cli)
cmd.SetArgs([]string{"reg-name.io/image:tag"})
cmd.SetOutput(ioutil.Discard)
assert.Error(t, cmd.Execute())
testutil.ErrorContains(t, cmd.Execute(), "client is offline")
}
func TestGetOrGenerateNotaryKey(t *testing.T) {
tmpDir, err := ioutil.TempDir("", "notary-test-")
assert.NoError(t, err)
defer os.RemoveAll(tmpDir)
notaryRepo, err := client.NewFileCachedRepository(tmpDir, "gun", "https://localhost", nil, passphrase.ConstantRetriever(passwd), trustpinning.TrustPinConfig{})
assert.NoError(t, err)
// repo is empty, try making a root key
rootKeyA, err := getOrGenerateNotaryKey(notaryRepo, data.CanonicalRootRole)
assert.NoError(t, err)
assert.NotNil(t, rootKeyA)
// we should only have one newly generated key
allKeys := notaryRepo.GetCryptoService().ListAllKeys()
assert.Len(t, allKeys, 1)
assert.NotNil(t, notaryRepo.GetCryptoService().GetKey(rootKeyA.ID()))
// this time we should get back the same key if we ask for another root key
rootKeyB, err := getOrGenerateNotaryKey(notaryRepo, data.CanonicalRootRole)
assert.NoError(t, err)
assert.NotNil(t, rootKeyB)
// we should only have one newly generated key
allKeys = notaryRepo.GetCryptoService().ListAllKeys()
assert.Len(t, allKeys, 1)
assert.NotNil(t, notaryRepo.GetCryptoService().GetKey(rootKeyB.ID()))
// The key we retrieved should be identical to the one we generated
assert.Equal(t, rootKeyA, rootKeyB)
// Now also try with a delegation key
releasesKey, err := getOrGenerateNotaryKey(notaryRepo, data.RoleName(trust.ReleasesRole))
assert.NoError(t, err)
assert.NotNil(t, releasesKey)
// we should now have two keys
allKeys = notaryRepo.GetCryptoService().ListAllKeys()
assert.Len(t, allKeys, 2)
assert.NotNil(t, notaryRepo.GetCryptoService().GetKey(releasesKey.ID()))
// The key we retrieved should be identical to the one we generated
assert.NotEqual(t, releasesKey, rootKeyA)
assert.NotEqual(t, releasesKey, rootKeyB)
}
func TestAddStageSigners(t *testing.T) {
tmpDir, err := ioutil.TempDir("", "notary-test-")
assert.NoError(t, err)
defer os.RemoveAll(tmpDir)
notaryRepo, err := client.NewFileCachedRepository(tmpDir, "gun", "https://localhost", nil, passphrase.ConstantRetriever(passwd), trustpinning.TrustPinConfig{})
assert.NoError(t, err)
// stage targets/user
userRole := data.RoleName("targets/user")
userKey := data.NewPublicKey("algoA", []byte("a"))
addStagedSigner(notaryRepo, userRole, []data.PublicKey{userKey})
// check the changelist for four total changes: two on targets/releases and two on targets/user
cl, err := notaryRepo.GetChangelist()
assert.NoError(t, err)
changeList := cl.List()
assert.Len(t, changeList, 4)
// ordering is determinstic:
// first change is for targets/user key creation
newSignerKeyChange := changeList[0]
expectedJSON, err := json.Marshal(&changelist.TUFDelegation{
NewThreshold: notary.MinThreshold,
AddKeys: data.KeyList([]data.PublicKey{userKey}),
})
require.NoError(t, err)
expectedChange := changelist.NewTUFChange(
changelist.ActionCreate,
userRole,
changelist.TypeTargetsDelegation,
"", // no path for delegations
expectedJSON,
)
assert.Equal(t, expectedChange, newSignerKeyChange)
// second change is for targets/user getting all paths
newSignerPathsChange := changeList[1]
expectedJSON, err = json.Marshal(&changelist.TUFDelegation{
AddPaths: []string{""},
})
require.NoError(t, err)
expectedChange = changelist.NewTUFChange(
changelist.ActionCreate,
userRole,
changelist.TypeTargetsDelegation,
"", // no path for delegations
expectedJSON,
)
assert.Equal(t, expectedChange, newSignerPathsChange)
releasesRole := data.RoleName("targets/releases")
// third change is for targets/releases key creation
releasesKeyChange := changeList[2]
expectedJSON, err = json.Marshal(&changelist.TUFDelegation{
NewThreshold: notary.MinThreshold,
AddKeys: data.KeyList([]data.PublicKey{userKey}),
})
require.NoError(t, err)
expectedChange = changelist.NewTUFChange(
changelist.ActionCreate,
releasesRole,
changelist.TypeTargetsDelegation,
"", // no path for delegations
expectedJSON,
)
assert.Equal(t, expectedChange, releasesKeyChange)
// fourth change is for targets/releases getting all paths
releasesPathsChange := changeList[3]
expectedJSON, err = json.Marshal(&changelist.TUFDelegation{
AddPaths: []string{""},
})
require.NoError(t, err)
expectedChange = changelist.NewTUFChange(
changelist.ActionCreate,
releasesRole,
changelist.TypeTargetsDelegation,
"", // no path for delegations
expectedJSON,
)
assert.Equal(t, expectedChange, releasesPathsChange)
}
func TestGetSignedManifestHashAndSize(t *testing.T) {
tmpDir, err := ioutil.TempDir("", "notary-test-")
assert.NoError(t, err)
defer os.RemoveAll(tmpDir)
notaryRepo, err := client.NewFileCachedRepository(tmpDir, "gun", "https://localhost", nil, passphrase.ConstantRetriever(passwd), trustpinning.TrustPinConfig{})
assert.NoError(t, err)
target := &client.Target{}
target.Hashes, target.Length, err = getSignedManifestHashAndSize(notaryRepo, "test")
assert.EqualError(t, err, "client is offline")
}
func TestGetReleasedTargetHashAndSize(t *testing.T) {
oneReleasedTgt := []client.TargetSignedStruct{}
// make and append 3 non-released signatures on the "unreleased" target
unreleasedTgt := client.Target{Name: "unreleased", Hashes: data.Hashes{notary.SHA256: []byte("hash")}}
for _, unreleasedRole := range []string{"targets/a", "targets/b", "targets/c"} {
oneReleasedTgt = append(oneReleasedTgt, client.TargetSignedStruct{Role: mockDelegationRoleWithName(unreleasedRole), Target: unreleasedTgt})
}
_, _, err := getReleasedTargetHashAndSize(oneReleasedTgt, "unreleased")
assert.EqualError(t, err, "No valid trust data for unreleased")
releasedTgt := client.Target{Name: "released", Hashes: data.Hashes{notary.SHA256: []byte("released-hash")}}
oneReleasedTgt = append(oneReleasedTgt, client.TargetSignedStruct{Role: mockDelegationRoleWithName("targets/releases"), Target: releasedTgt})
hash, _, _ := getReleasedTargetHashAndSize(oneReleasedTgt, "unreleased")
assert.Equal(t, data.Hashes{notary.SHA256: []byte("released-hash")}, hash)
}
func TestCreateTarget(t *testing.T) {
tmpDir, err := ioutil.TempDir("", "notary-test-")
assert.NoError(t, err)
defer os.RemoveAll(tmpDir)
notaryRepo, err := client.NewFileCachedRepository(tmpDir, "gun", "https://localhost", nil, passphrase.ConstantRetriever(passwd), trustpinning.TrustPinConfig{})
assert.NoError(t, err)
_, err = createTarget(notaryRepo, "")
assert.EqualError(t, err, "No tag specified")
_, err = createTarget(notaryRepo, "1")
assert.EqualError(t, err, "client is offline")
}
func TestGetExistingSignatureInfoForReleasedTag(t *testing.T) {
tmpDir, err := ioutil.TempDir("", "notary-test-")
assert.NoError(t, err)
defer os.RemoveAll(tmpDir)
notaryRepo, err := client.NewFileCachedRepository(tmpDir, "gun", "https://localhost", nil, passphrase.ConstantRetriever(passwd), trustpinning.TrustPinConfig{})
assert.NoError(t, err)
_, err = getExistingSignatureInfoForReleasedTag(notaryRepo, "test")
assert.EqualError(t, err, "client is offline")
}
func TestPrettyPrintExistingSignatureInfo(t *testing.T) {
buf := bytes.NewBuffer(nil)
signers := []string{"Bob", "Alice", "Carol"}
existingSig := trustTagRow{trustTagKey{"tagName", "abc123"}, signers}
prettyPrintExistingSignatureInfo(buf, existingSig)
assert.Contains(t, buf.String(), "Existing signatures for tag tagName digest abc123 from:\nAlice, Bob, Carol")
}
func TestSignCommandChangeListIsCleanedOnError(t *testing.T) {
tmpDir, err := ioutil.TempDir("", "docker-sign-test-")
assert.NoError(t, err)
defer os.RemoveAll(tmpDir)
config.SetDir(tmpDir)
cli := test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(getLoadedNotaryRepository)
cmd := newSignCommand(cli)
cmd.SetArgs([]string{"ubuntu:latest"})
cmd.SetOutput(ioutil.Discard)
err = cmd.Execute()
require.Error(t, err)
notaryRepo, err := client.NewFileCachedRepository(tmpDir, "docker.io/library/ubuntu", "https://localhost", nil, passphrase.ConstantRetriever(passwd), trustpinning.TrustPinConfig{})
assert.NoError(t, err)
cl, err := notaryRepo.GetChangelist()
require.NoError(t, err)
assert.Equal(t, len(cl.List()), 0)
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,227 +0,0 @@
package trust
import (
"context"
"encoding/hex"
"fmt"
"io"
"sort"
"strings"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/cli/cli/command/image"
"github.com/docker/cli/cli/trust"
"github.com/docker/notary"
"github.com/docker/notary/client"
"github.com/docker/notary/tuf/data"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
// trustTagKey represents a unique signed tag and hex-encoded hash pair
type trustTagKey struct {
TagName string
HashHex string
}
// trustTagRow encodes all human-consumable information for a signed tag, including signers
type trustTagRow struct {
trustTagKey
Signers []string
}
type trustTagRowList []trustTagRow
func (tagComparator trustTagRowList) Len() int {
return len(tagComparator)
}
func (tagComparator trustTagRowList) Less(i, j int) bool {
return tagComparator[i].TagName < tagComparator[j].TagName
}
func (tagComparator trustTagRowList) Swap(i, j int) {
tagComparator[i], tagComparator[j] = tagComparator[j], tagComparator[i]
}
func newViewCommand(dockerCli command.Cli) *cobra.Command {
cmd := &cobra.Command{
Use: "view IMAGE[:TAG]",
Short: "Display detailed information about keys and signatures",
Args: cli.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return lookupTrustInfo(dockerCli, args[0])
},
}
return cmd
}
func lookupTrustInfo(cli command.Cli, remote string) error {
ctx := context.Background()
imgRefAndAuth, err := trust.GetImageReferencesAndAuth(ctx, image.AuthResolver(cli), remote)
if err != nil {
return err
}
tag := imgRefAndAuth.Tag()
notaryRepo, err := cli.NotaryClient(imgRefAndAuth, trust.ActionsPullOnly)
if err != nil {
return trust.NotaryError(imgRefAndAuth.Reference().Name(), err)
}
if err = clearChangeList(notaryRepo); err != nil {
return err
}
defer clearChangeList(notaryRepo)
// Retrieve all released signatures, match them, and pretty print them
allSignedTargets, err := notaryRepo.GetAllTargetMetadataByName(tag)
if err != nil {
logrus.Debug(trust.NotaryError(imgRefAndAuth.Reference().Name(), err))
// print an empty table if we don't have signed targets, but have an initialized notary repo
if _, ok := err.(client.ErrNoSuchTarget); !ok {
return fmt.Errorf("No signatures or cannot access %s", remote)
}
}
signatureRows := matchReleasedSignatures(allSignedTargets)
if len(signatureRows) > 0 {
if err := printSignatures(cli.Out(), signatureRows); err != nil {
return err
}
} else {
fmt.Fprintf(cli.Out(), "\nNo signatures for %s\n\n", remote)
}
// get the administrative roles
adminRolesWithSigs, err := notaryRepo.ListRoles()
if err != nil {
return fmt.Errorf("No signers for %s", remote)
}
// get delegation roles with the canonical key IDs
delegationRoles, err := notaryRepo.GetDelegationRoles()
if err != nil {
logrus.Debugf("no delegation roles found, or error fetching them for %s: %v", remote, err)
}
signerRoleToKeyIDs := getDelegationRoleToKeyMap(delegationRoles)
// If we do not have additional signers, do not display
if len(signerRoleToKeyIDs) > 0 {
fmt.Fprintf(cli.Out(), "\nList of signers and their keys for %s:\n\n", strings.Split(remote, ":")[0])
if err := printSignerInfo(cli.Out(), signerRoleToKeyIDs); err != nil {
return err
}
}
// This will always have the root and targets information
fmt.Fprintf(cli.Out(), "\nAdministrative keys for %s:\n", strings.Split(remote, ":")[0])
printSortedAdminKeys(cli.Out(), adminRolesWithSigs)
return nil
}
func printSortedAdminKeys(out io.Writer, adminRoles []client.RoleWithSignatures) {
sort.Slice(adminRoles, func(i, j int) bool { return adminRoles[i].Name > adminRoles[j].Name })
for _, adminRole := range adminRoles {
fmt.Fprintf(out, "%s", formatAdminRole(adminRole))
}
}
func formatAdminRole(roleWithSigs client.RoleWithSignatures) string {
adminKeyList := roleWithSigs.KeyIDs
sort.Strings(adminKeyList)
var role string
switch roleWithSigs.Name {
case data.CanonicalTargetsRole:
role = "Repository Key"
case data.CanonicalRootRole:
role = "Root Key"
default:
return ""
}
return fmt.Sprintf("%s:\t%s\n", role, strings.Join(adminKeyList, ", "))
}
func getDelegationRoleToKeyMap(rawDelegationRoles []data.Role) map[string][]string {
signerRoleToKeyIDs := make(map[string][]string)
for _, delRole := range rawDelegationRoles {
switch delRole.Name {
case trust.ReleasesRole, data.CanonicalRootRole, data.CanonicalSnapshotRole, data.CanonicalTargetsRole, data.CanonicalTimestampRole:
continue
default:
signerRoleToKeyIDs[notaryRoleToSigner(delRole.Name)] = delRole.KeyIDs
}
}
return signerRoleToKeyIDs
}
// aggregate all signers for a "released" hash+tagname pair. To be "released," the tag must have been
// signed into the "targets" or "targets/releases" role. Output is sorted by tag name
func matchReleasedSignatures(allTargets []client.TargetSignedStruct) trustTagRowList {
signatureRows := trustTagRowList{}
// do a first pass to get filter on tags signed into "targets" or "targets/releases"
releasedTargetRows := map[trustTagKey][]string{}
for _, tgt := range allTargets {
if isReleasedTarget(tgt.Role.Name) {
releasedKey := trustTagKey{tgt.Target.Name, hex.EncodeToString(tgt.Target.Hashes[notary.SHA256])}
releasedTargetRows[releasedKey] = []string{}
}
}
// now fill out all signers on released keys
for _, tgt := range allTargets {
targetKey := trustTagKey{tgt.Target.Name, hex.EncodeToString(tgt.Target.Hashes[notary.SHA256])}
// only considered released targets
if _, ok := releasedTargetRows[targetKey]; ok && !isReleasedTarget(tgt.Role.Name) {
releasedTargetRows[targetKey] = append(releasedTargetRows[targetKey], notaryRoleToSigner(tgt.Role.Name))
}
}
// compile the final output as a sorted slice
for targetKey, signers := range releasedTargetRows {
signatureRows = append(signatureRows, trustTagRow{targetKey, signers})
}
sort.Sort(signatureRows)
return signatureRows
}
// pretty print with ordered rows
func printSignatures(out io.Writer, signatureRows trustTagRowList) error {
trustTagCtx := formatter.Context{
Output: out,
Format: formatter.NewTrustTagFormat(),
}
// convert the formatted type before printing
formattedTags := []formatter.SignedTagInfo{}
for _, sigRow := range signatureRows {
formattedSigners := sigRow.Signers
if len(formattedSigners) == 0 {
formattedSigners = append(formattedSigners, fmt.Sprintf("(%s)", releasedRoleName))
}
formattedTags = append(formattedTags, formatter.SignedTagInfo{
Name: sigRow.TagName,
Digest: sigRow.HashHex,
Signers: formattedSigners,
})
}
return formatter.TrustTagWrite(trustTagCtx, formattedTags)
}
func printSignerInfo(out io.Writer, roleToKeyIDs map[string][]string) error {
signerInfoCtx := formatter.Context{
Output: out,
Format: formatter.NewSignerInfoFormat(),
Trunc: true,
}
formattedSignerInfo := formatter.SignerInfoList{}
for name, keyIDs := range roleToKeyIDs {
formattedSignerInfo = append(formattedSignerInfo, formatter.SignerInfo{
Name: name,
Keys: keyIDs,
})
}
sort.Sort(formattedSignerInfo)
return formatter.SignerInfoWrite(signerInfoCtx, formattedSignerInfo)
}

View File

@ -1,433 +0,0 @@
package trust
import (
"encoding/hex"
"io/ioutil"
"testing"
"github.com/docker/cli/cli/trust"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/testutil"
dockerClient "github.com/docker/docker/client"
"github.com/docker/notary"
"github.com/docker/notary/client"
"github.com/docker/notary/tuf/data"
"github.com/gotestyourself/gotestyourself/golden"
"github.com/stretchr/testify/assert"
)
type fakeClient struct {
dockerClient.Client
}
func TestTrustInspectCommandErrors(t *testing.T) {
testCases := []struct {
name string
args []string
expectedError string
}{
{
name: "not-enough-args",
expectedError: "requires exactly 1 argument",
},
{
name: "too-many-args",
args: []string{"remote1", "remote2"},
expectedError: "requires exactly 1 argument",
},
{
name: "sha-reference",
args: []string{"870d292919d01a0af7e7f056271dc78792c05f55f49b9b9012b6d89725bd9abd"},
expectedError: "invalid repository name",
},
{
name: "invalid-img-reference",
args: []string{"ALPINE"},
expectedError: "invalid reference format",
},
}
for _, tc := range testCases {
cmd := newViewCommand(
test.NewFakeCli(&fakeClient{}))
cmd.SetArgs(tc.args)
cmd.SetOutput(ioutil.Discard)
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
}
}
func TestTrustInspectCommandOfflineErrors(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(getOfflineNotaryRepository)
cmd := newViewCommand(cli)
cmd.SetArgs([]string{"nonexistent-reg-name.io/image"})
cmd.SetOutput(ioutil.Discard)
testutil.ErrorContains(t, cmd.Execute(), "No signatures or cannot access nonexistent-reg-name.io/image")
cli = test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(getOfflineNotaryRepository)
cmd = newViewCommand(cli)
cmd.SetArgs([]string{"nonexistent-reg-name.io/image:tag"})
cmd.SetOutput(ioutil.Discard)
testutil.ErrorContains(t, cmd.Execute(), "No signatures or cannot access nonexistent-reg-name.io/image")
}
func TestTrustInspectCommandUninitializedErrors(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(getUninitializedNotaryRepository)
cmd := newViewCommand(cli)
cmd.SetArgs([]string{"reg/unsigned-img"})
cmd.SetOutput(ioutil.Discard)
testutil.ErrorContains(t, cmd.Execute(), "No signatures or cannot access reg/unsigned-img")
cli = test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(getUninitializedNotaryRepository)
cmd = newViewCommand(cli)
cmd.SetArgs([]string{"reg/unsigned-img:tag"})
cmd.SetOutput(ioutil.Discard)
testutil.ErrorContains(t, cmd.Execute(), "No signatures or cannot access reg/unsigned-img:tag")
}
func TestTrustInspectCommandEmptyNotaryRepoErrors(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(getEmptyTargetsNotaryRepository)
cmd := newViewCommand(cli)
cmd.SetArgs([]string{"reg/img:unsigned-tag"})
cmd.SetOutput(ioutil.Discard)
assert.NoError(t, cmd.Execute())
assert.Contains(t, cli.OutBuffer().String(), "No signatures for reg/img:unsigned-tag")
assert.Contains(t, cli.OutBuffer().String(), "Administrative keys for reg/img:")
cli = test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(getEmptyTargetsNotaryRepository)
cmd = newViewCommand(cli)
cmd.SetArgs([]string{"reg/img"})
cmd.SetOutput(ioutil.Discard)
assert.NoError(t, cmd.Execute())
assert.Contains(t, cli.OutBuffer().String(), "No signatures for reg/img")
assert.Contains(t, cli.OutBuffer().String(), "Administrative keys for reg/img:")
}
func TestTrustInspectCommandFullRepoWithoutSigners(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(getLoadedWithNoSignersNotaryRepository)
cmd := newViewCommand(cli)
cmd.SetArgs([]string{"signed-repo"})
assert.NoError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "trust-inspect-full-repo-no-signers.golden")
}
func TestTrustInspectCommandOneTagWithoutSigners(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(getLoadedWithNoSignersNotaryRepository)
cmd := newViewCommand(cli)
cmd.SetArgs([]string{"signed-repo:green"})
assert.NoError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "trust-inspect-one-tag-no-signers.golden")
}
func TestTrustInspectCommandFullRepoWithSigners(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(getLoadedNotaryRepository)
cmd := newViewCommand(cli)
cmd.SetArgs([]string{"signed-repo"})
assert.NoError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "trust-inspect-full-repo-with-signers.golden")
}
func TestTrustInspectCommandUnsignedTagInSignedRepo(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(getLoadedNotaryRepository)
cmd := newViewCommand(cli)
cmd.SetArgs([]string{"signed-repo:unsigned"})
assert.NoError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "trust-inspect-unsigned-tag-with-signers.golden")
}
func TestNotaryRoleToSigner(t *testing.T) {
assert.Equal(t, releasedRoleName, notaryRoleToSigner(data.CanonicalTargetsRole))
assert.Equal(t, releasedRoleName, notaryRoleToSigner(trust.ReleasesRole))
assert.Equal(t, "signer", notaryRoleToSigner("targets/signer"))
assert.Equal(t, "docker/signer", notaryRoleToSigner("targets/docker/signer"))
// It's nonsense for other base roles to have signed off on a target, but this function leaves role names intact
for _, role := range data.BaseRoles {
if role == data.CanonicalTargetsRole {
continue
}
assert.Equal(t, role.String(), notaryRoleToSigner(role))
}
assert.Equal(t, "notarole", notaryRoleToSigner(data.RoleName("notarole")))
}
// check if a role name is "released": either targets/releases or targets TUF roles
func TestIsReleasedTarget(t *testing.T) {
assert.True(t, isReleasedTarget(trust.ReleasesRole))
for _, role := range data.BaseRoles {
assert.Equal(t, role == data.CanonicalTargetsRole, isReleasedTarget(role))
}
assert.False(t, isReleasedTarget(data.RoleName("targets/not-releases")))
assert.False(t, isReleasedTarget(data.RoleName("random")))
assert.False(t, isReleasedTarget(data.RoleName("targets/releases/subrole")))
}
// creates a mock delegation with a given name and no keys
func mockDelegationRoleWithName(name string) data.DelegationRole {
baseRole := data.NewBaseRole(
data.RoleName(name),
notary.MinThreshold,
)
return data.DelegationRole{baseRole, []string{}}
}
func TestMatchEmptySignatures(t *testing.T) {
// first try empty targets
emptyTgts := []client.TargetSignedStruct{}
matchedSigRows := matchReleasedSignatures(emptyTgts)
assert.Empty(t, matchedSigRows)
}
func TestMatchUnreleasedSignatures(t *testing.T) {
// try an "unreleased" target with 3 signatures, 0 rows will appear
unreleasedTgts := []client.TargetSignedStruct{}
tgt := client.Target{Name: "unreleased", Hashes: data.Hashes{notary.SHA256: []byte("hash")}}
for _, unreleasedRole := range []string{"targets/a", "targets/b", "targets/c"} {
unreleasedTgts = append(unreleasedTgts, client.TargetSignedStruct{Role: mockDelegationRoleWithName(unreleasedRole), Target: tgt})
}
matchedSigRows := matchReleasedSignatures(unreleasedTgts)
assert.Empty(t, matchedSigRows)
}
func TestMatchOneReleasedSingleSignature(t *testing.T) {
// now try only 1 "released" target with no additional sigs, 1 row will appear with 0 signers
oneReleasedTgt := []client.TargetSignedStruct{}
// make and append the "released" target to our mock input
releasedTgt := client.Target{Name: "released", Hashes: data.Hashes{notary.SHA256: []byte("released-hash")}}
oneReleasedTgt = append(oneReleasedTgt, client.TargetSignedStruct{Role: mockDelegationRoleWithName("targets/releases"), Target: releasedTgt})
// make and append 3 non-released signatures on the "unreleased" target
unreleasedTgt := client.Target{Name: "unreleased", Hashes: data.Hashes{notary.SHA256: []byte("hash")}}
for _, unreleasedRole := range []string{"targets/a", "targets/b", "targets/c"} {
oneReleasedTgt = append(oneReleasedTgt, client.TargetSignedStruct{Role: mockDelegationRoleWithName(unreleasedRole), Target: unreleasedTgt})
}
matchedSigRows := matchReleasedSignatures(oneReleasedTgt)
assert.Len(t, matchedSigRows, 1)
outputRow := matchedSigRows[0]
// Empty signers because "targets/releases" doesn't show up
assert.Empty(t, outputRow.Signers)
assert.Equal(t, releasedTgt.Name, outputRow.TagName)
assert.Equal(t, hex.EncodeToString(releasedTgt.Hashes[notary.SHA256]), outputRow.HashHex)
}
func TestMatchOneReleasedMultiSignature(t *testing.T) {
// now try only 1 "released" target with 3 additional sigs, 1 row will appear with 3 signers
oneReleasedTgt := []client.TargetSignedStruct{}
// make and append the "released" target to our mock input
releasedTgt := client.Target{Name: "released", Hashes: data.Hashes{notary.SHA256: []byte("released-hash")}}
oneReleasedTgt = append(oneReleasedTgt, client.TargetSignedStruct{Role: mockDelegationRoleWithName("targets/releases"), Target: releasedTgt})
// make and append 3 non-released signatures on both the "released" and "unreleased" targets
unreleasedTgt := client.Target{Name: "unreleased", Hashes: data.Hashes{notary.SHA256: []byte("hash")}}
for _, unreleasedRole := range []string{"targets/a", "targets/b", "targets/c"} {
oneReleasedTgt = append(oneReleasedTgt, client.TargetSignedStruct{Role: mockDelegationRoleWithName(unreleasedRole), Target: unreleasedTgt})
oneReleasedTgt = append(oneReleasedTgt, client.TargetSignedStruct{Role: mockDelegationRoleWithName(unreleasedRole), Target: releasedTgt})
}
matchedSigRows := matchReleasedSignatures(oneReleasedTgt)
assert.Len(t, matchedSigRows, 1)
outputRow := matchedSigRows[0]
// We should have three signers
assert.Equal(t, outputRow.Signers, []string{"a", "b", "c"})
assert.Equal(t, releasedTgt.Name, outputRow.TagName)
assert.Equal(t, hex.EncodeToString(releasedTgt.Hashes[notary.SHA256]), outputRow.HashHex)
}
func TestMatchMultiReleasedMultiSignature(t *testing.T) {
// now try 3 "released" targets with additional sigs to show 3 rows as follows:
// target-a is signed by targets/releases and targets/a - a will be the signer
// target-b is signed by targets/releases, targets/a, targets/b - a and b will be the signers
// target-c is signed by targets/releases, targets/a, targets/b, targets/c - a, b, and c will be the signers
multiReleasedTgts := []client.TargetSignedStruct{}
// make target-a, target-b, and target-c
targetA := client.Target{Name: "target-a", Hashes: data.Hashes{notary.SHA256: []byte("target-a-hash")}}
targetB := client.Target{Name: "target-b", Hashes: data.Hashes{notary.SHA256: []byte("target-b-hash")}}
targetC := client.Target{Name: "target-c", Hashes: data.Hashes{notary.SHA256: []byte("target-c-hash")}}
// have targets/releases "sign" on all of these targets so they are released
multiReleasedTgts = append(multiReleasedTgts, client.TargetSignedStruct{Role: mockDelegationRoleWithName("targets/releases"), Target: targetA})
multiReleasedTgts = append(multiReleasedTgts, client.TargetSignedStruct{Role: mockDelegationRoleWithName("targets/releases"), Target: targetB})
multiReleasedTgts = append(multiReleasedTgts, client.TargetSignedStruct{Role: mockDelegationRoleWithName("targets/releases"), Target: targetC})
// targets/a signs off on all three targets (target-a, target-b, target-c):
for _, tgt := range []client.Target{targetA, targetB, targetC} {
multiReleasedTgts = append(multiReleasedTgts, client.TargetSignedStruct{Role: mockDelegationRoleWithName("targets/a"), Target: tgt})
}
// targets/b signs off on the final two targets (target-b, target-c):
for _, tgt := range []client.Target{targetB, targetC} {
multiReleasedTgts = append(multiReleasedTgts, client.TargetSignedStruct{Role: mockDelegationRoleWithName("targets/b"), Target: tgt})
}
// targets/c only signs off on the last target (target-c):
multiReleasedTgts = append(multiReleasedTgts, client.TargetSignedStruct{Role: mockDelegationRoleWithName("targets/c"), Target: targetC})
matchedSigRows := matchReleasedSignatures(multiReleasedTgts)
assert.Len(t, matchedSigRows, 3)
// note that the output is sorted by tag name, so we can reliably index to validate data:
outputTargetA := matchedSigRows[0]
assert.Equal(t, outputTargetA.Signers, []string{"a"})
assert.Equal(t, targetA.Name, outputTargetA.TagName)
assert.Equal(t, hex.EncodeToString(targetA.Hashes[notary.SHA256]), outputTargetA.HashHex)
outputTargetB := matchedSigRows[1]
assert.Equal(t, outputTargetB.Signers, []string{"a", "b"})
assert.Equal(t, targetB.Name, outputTargetB.TagName)
assert.Equal(t, hex.EncodeToString(targetB.Hashes[notary.SHA256]), outputTargetB.HashHex)
outputTargetC := matchedSigRows[2]
assert.Equal(t, outputTargetC.Signers, []string{"a", "b", "c"})
assert.Equal(t, targetC.Name, outputTargetC.TagName)
assert.Equal(t, hex.EncodeToString(targetC.Hashes[notary.SHA256]), outputTargetC.HashHex)
}
func TestMatchReleasedSignatureFromTargets(t *testing.T) {
// now try only 1 "released" target with no additional sigs, one rows will appear
oneReleasedTgt := []client.TargetSignedStruct{}
// make and append the "released" target to our mock input
releasedTgt := client.Target{Name: "released", Hashes: data.Hashes{notary.SHA256: []byte("released-hash")}}
oneReleasedTgt = append(oneReleasedTgt, client.TargetSignedStruct{Role: mockDelegationRoleWithName(data.CanonicalTargetsRole.String()), Target: releasedTgt})
matchedSigRows := matchReleasedSignatures(oneReleasedTgt)
assert.Len(t, matchedSigRows, 1)
outputRow := matchedSigRows[0]
// Empty signers because "targets" doesn't show up
assert.Empty(t, outputRow.Signers)
assert.Equal(t, releasedTgt.Name, outputRow.TagName)
assert.Equal(t, hex.EncodeToString(releasedTgt.Hashes[notary.SHA256]), outputRow.HashHex)
}
func TestGetSignerRolesWithKeyIDs(t *testing.T) {
roles := []data.Role{
{
RootRole: data.RootRole{
KeyIDs: []string{"key11"},
},
Name: "targets/alice",
},
{
RootRole: data.RootRole{
KeyIDs: []string{"key21", "key22"},
},
Name: "targets/releases",
},
{
RootRole: data.RootRole{
KeyIDs: []string{"key31"},
},
Name: data.CanonicalTargetsRole,
},
{
RootRole: data.RootRole{
KeyIDs: []string{"key41", "key01"},
},
Name: data.CanonicalRootRole,
},
{
RootRole: data.RootRole{
KeyIDs: []string{"key51"},
},
Name: data.CanonicalSnapshotRole,
},
{
RootRole: data.RootRole{
KeyIDs: []string{"key61"},
},
Name: data.CanonicalTimestampRole,
},
{
RootRole: data.RootRole{
KeyIDs: []string{"key71", "key72"},
},
Name: "targets/bob",
},
}
expectedSignerRoleToKeyIDs := map[string][]string{
"alice": {"key11"},
"bob": {"key71", "key72"},
}
var roleWithSigs []client.RoleWithSignatures
for _, role := range roles {
roleWithSig := client.RoleWithSignatures{Role: role, Signatures: nil}
roleWithSigs = append(roleWithSigs, roleWithSig)
}
signerRoleToKeyIDs := getDelegationRoleToKeyMap(roles)
assert.Equal(t, expectedSignerRoleToKeyIDs, signerRoleToKeyIDs)
}
func TestFormatAdminRole(t *testing.T) {
aliceRole := data.Role{
RootRole: data.RootRole{
KeyIDs: []string{"key11"},
},
Name: "targets/alice",
}
aliceRoleWithSigs := client.RoleWithSignatures{Role: aliceRole, Signatures: nil}
assert.Equal(t, "", formatAdminRole(aliceRoleWithSigs))
releasesRole := data.Role{
RootRole: data.RootRole{
KeyIDs: []string{"key11"},
},
Name: "targets/releases",
}
releasesRoleWithSigs := client.RoleWithSignatures{Role: releasesRole, Signatures: nil}
assert.Equal(t, "", formatAdminRole(releasesRoleWithSigs))
timestampRole := data.Role{
RootRole: data.RootRole{
KeyIDs: []string{"key11"},
},
Name: data.CanonicalTimestampRole,
}
timestampRoleWithSigs := client.RoleWithSignatures{Role: timestampRole, Signatures: nil}
assert.Equal(t, "", formatAdminRole(timestampRoleWithSigs))
snapshotRole := data.Role{
RootRole: data.RootRole{
KeyIDs: []string{"key11"},
},
Name: data.CanonicalSnapshotRole,
}
snapshotRoleWithSigs := client.RoleWithSignatures{Role: snapshotRole, Signatures: nil}
assert.Equal(t, "", formatAdminRole(snapshotRoleWithSigs))
rootRole := data.Role{
RootRole: data.RootRole{
KeyIDs: []string{"key11"},
},
Name: data.CanonicalRootRole,
}
rootRoleWithSigs := client.RoleWithSignatures{Role: rootRole, Signatures: nil}
assert.Equal(t, "Root Key:\tkey11\n", formatAdminRole(rootRoleWithSigs))
targetsRole := data.Role{
RootRole: data.RootRole{
KeyIDs: []string{"key99", "abc", "key11"},
},
Name: data.CanonicalTargetsRole,
}
targetsRoleWithSigs := client.RoleWithSignatures{Role: targetsRole, Signatures: nil}
assert.Equal(t, "Repository Key:\tabc, key11, key99\n", formatAdminRole(targetsRoleWithSigs))
}

View File

@ -2,21 +2,6 @@ version: "3.4"
services:
foo:
build:
context: ./dir
dockerfile: Dockerfile
args:
foo: bar
target: foo
network: foo
cache_from:
- foo
- bar
labels: [FOO=BAR]
cap_add:
- ALL

View File

@ -221,7 +221,6 @@ func createTransformHook() mapstructure.DecodeHookFuncType {
reflect.TypeOf(types.Labels{}): transformMappingOrListFunc("=", false),
reflect.TypeOf(types.MappingWithColon{}): transformMappingOrListFunc(":", false),
reflect.TypeOf(types.ServiceVolumeConfig{}): transformServiceVolumeConfig,
reflect.TypeOf(types.BuildConfig{}): transformBuildConfig,
}
return func(_ reflect.Type, target reflect.Type, data interface{}) (interface{}, error) {
@ -564,17 +563,6 @@ func transformStringSourceMap(data interface{}) (interface{}, error) {
}
}
func transformBuildConfig(data interface{}) (interface{}, error) {
switch value := data.(type) {
case string:
return map[string]interface{}{"context": value}, nil
case map[string]interface{}:
return data, nil
default:
return data, errors.Errorf("invalid type %T for service build", value)
}
}
func transformServiceVolumeConfig(data interface{}) (interface{}, error) {
switch value := data.(type) {
case string:

View File

@ -10,7 +10,6 @@ import (
"github.com/docker/cli/cli/compose/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func buildConfigDetails(source map[string]interface{}, env map[string]string) types.ConfigDetails {
@ -165,13 +164,17 @@ var sampleConfig = types.Config{
func TestParseYAML(t *testing.T) {
dict, err := ParseYAML([]byte(sampleYAML))
require.NoError(t, err)
if !assert.NoError(t, err) {
return
}
assert.Equal(t, sampleDict, dict)
}
func TestLoad(t *testing.T) {
actual, err := Load(buildConfigDetails(sampleDict, nil))
require.NoError(t, err)
if !assert.NoError(t, err) {
return
}
assert.Equal(t, serviceSort(sampleConfig.Services), serviceSort(actual.Services))
assert.Equal(t, sampleConfig.Networks, actual.Networks)
assert.Equal(t, sampleConfig.Volumes, actual.Volumes)
@ -188,9 +191,11 @@ secrets:
super:
external: true
`)
require.NoError(t, err)
assert.Len(t, actual.Services, 1)
assert.Len(t, actual.Secrets, 1)
if !assert.NoError(t, err) {
return
}
assert.Equal(t, len(actual.Services), 1)
assert.Equal(t, len(actual.Secrets), 1)
}
func TestLoadV33(t *testing.T) {
@ -206,15 +211,19 @@ configs:
super:
external: true
`)
require.NoError(t, err)
require.Len(t, actual.Services, 1)
if !assert.NoError(t, err) {
return
}
assert.Equal(t, len(actual.Services), 1)
assert.Equal(t, actual.Services[0].CredentialSpec.File, "/foo")
require.Len(t, actual.Configs, 1)
assert.Equal(t, len(actual.Configs), 1)
}
func TestParseAndLoad(t *testing.T) {
actual, err := loadYAML(sampleYAML)
require.NoError(t, err)
if !assert.NoError(t, err) {
return
}
assert.Equal(t, serviceSort(sampleConfig.Services), serviceSort(actual.Services))
assert.Equal(t, sampleConfig.Networks, actual.Networks)
assert.Equal(t, sampleConfig.Volumes, actual.Volumes)
@ -222,15 +231,15 @@ func TestParseAndLoad(t *testing.T) {
func TestInvalidTopLevelObjectType(t *testing.T) {
_, err := loadYAML("1")
require.Error(t, err)
assert.Error(t, err)
assert.Contains(t, err.Error(), "Top-level object must be a mapping")
_, err = loadYAML("\"hello\"")
require.Error(t, err)
assert.Error(t, err)
assert.Contains(t, err.Error(), "Top-level object must be a mapping")
_, err = loadYAML("[\"hello\"]")
require.Error(t, err)
assert.Error(t, err)
assert.Contains(t, err.Error(), "Top-level object must be a mapping")
}
@ -241,7 +250,7 @@ version: "3"
foo:
image: busybox
`)
require.Error(t, err)
assert.Error(t, err)
assert.Contains(t, err.Error(), "Non-string key at top level: 123")
_, err = loadYAML(`
@ -252,7 +261,7 @@ services:
123:
image: busybox
`)
require.Error(t, err)
assert.Error(t, err)
assert.Contains(t, err.Error(), "Non-string key in services: 123")
_, err = loadYAML(`
@ -266,7 +275,7 @@ networks:
config:
- 123: oh dear
`)
require.Error(t, err)
assert.Error(t, err)
assert.Contains(t, err.Error(), "Non-string key in networks.default.ipam.config[0]: 123")
_, err = loadYAML(`
@ -277,7 +286,7 @@ services:
environment:
1: FOO
`)
require.Error(t, err)
assert.Error(t, err)
assert.Contains(t, err.Error(), "Non-string key in services.dict-env.environment: 1")
}
@ -288,7 +297,7 @@ services:
foo:
image: busybox
`)
require.NoError(t, err)
assert.NoError(t, err)
_, err = loadYAML(`
version: "3.0"
@ -296,7 +305,7 @@ services:
foo:
image: busybox
`)
require.NoError(t, err)
assert.NoError(t, err)
}
func TestUnsupportedVersion(t *testing.T) {
@ -306,7 +315,7 @@ services:
foo:
image: busybox
`)
require.Error(t, err)
assert.Error(t, err)
assert.Contains(t, err.Error(), "version")
_, err = loadYAML(`
@ -315,7 +324,7 @@ services:
foo:
image: busybox
`)
require.Error(t, err)
assert.Error(t, err)
assert.Contains(t, err.Error(), "version")
}
@ -326,7 +335,7 @@ services:
foo:
image: busybox
`)
require.Error(t, err)
assert.Error(t, err)
assert.Contains(t, err.Error(), "version must be a string")
}
@ -345,7 +354,7 @@ services:
- foo:
image: busybox
`)
require.Error(t, err)
assert.Error(t, err)
assert.Contains(t, err.Error(), "services must be a mapping")
_, err = loadYAML(`
@ -353,7 +362,7 @@ version: "3"
services:
foo: busybox
`)
require.Error(t, err)
assert.Error(t, err)
assert.Contains(t, err.Error(), "services.foo must be a mapping")
_, err = loadYAML(`
@ -362,7 +371,7 @@ networks:
- default:
driver: bridge
`)
require.Error(t, err)
assert.Error(t, err)
assert.Contains(t, err.Error(), "networks must be a mapping")
_, err = loadYAML(`
@ -370,7 +379,7 @@ version: "3"
networks:
default: bridge
`)
require.Error(t, err)
assert.Error(t, err)
assert.Contains(t, err.Error(), "networks.default must be a mapping")
_, err = loadYAML(`
@ -379,7 +388,7 @@ volumes:
- data:
driver: local
`)
require.Error(t, err)
assert.Error(t, err)
assert.Contains(t, err.Error(), "volumes must be a mapping")
_, err = loadYAML(`
@ -387,7 +396,7 @@ version: "3"
volumes:
data: local
`)
require.Error(t, err)
assert.Error(t, err)
assert.Contains(t, err.Error(), "volumes.data must be a mapping")
}
@ -398,7 +407,7 @@ services:
foo:
image: ["busybox", "latest"]
`)
require.Error(t, err)
assert.Error(t, err)
assert.Contains(t, err.Error(), "services.foo.image must be a string")
}
@ -449,7 +458,7 @@ services:
environment:
FOO: ["1"]
`)
require.Error(t, err)
assert.Error(t, err)
assert.Contains(t, err.Error(), "services.dict-env.environment.FOO must be a string, number or null")
}
@ -461,7 +470,7 @@ services:
image: busybox
environment: "FOO=1"
`)
require.Error(t, err)
assert.Error(t, err)
assert.Contains(t, err.Error(), "services.dict-env.environment must be a mapping")
}
@ -488,7 +497,7 @@ volumes:
"FOO": "foo",
})
require.NoError(t, err)
assert.NoError(t, err)
expectedLabels := types.Labels{
"home1": home,
@ -508,46 +517,24 @@ version: "3"
services:
web:
image: web
build:
context: ./web
build: ./web
links:
- bar
db:
image: db
build:
context: ./db
build: ./db
`))
require.NoError(t, err)
assert.NoError(t, err)
configDetails := buildConfigDetails(dict, nil)
_, err = Load(configDetails)
require.NoError(t, err)
assert.NoError(t, err)
unsupported := GetUnsupportedProperties(configDetails)
assert.Equal(t, []string{"build", "links"}, unsupported)
}
func TestBuildProperties(t *testing.T) {
dict, err := ParseYAML([]byte(`
version: "3"
services:
web:
image: web
build: .
links:
- bar
db:
image: db
build:
context: ./db
`))
require.NoError(t, err)
configDetails := buildConfigDetails(dict, nil)
_, err = Load(configDetails)
require.NoError(t, err)
}
func TestDeprecatedProperties(t *testing.T) {
dict, err := ParseYAML([]byte(`
version: "3"
@ -560,15 +547,15 @@ services:
container_name: db
expose: ["5434"]
`))
require.NoError(t, err)
assert.NoError(t, err)
configDetails := buildConfigDetails(dict, nil)
_, err = Load(configDetails)
require.NoError(t, err)
assert.NoError(t, err)
deprecated := GetDeprecatedProperties(configDetails)
assert.Len(t, deprecated, 2)
assert.Equal(t, 2, len(deprecated))
assert.Contains(t, deprecated, "container_name")
assert.Contains(t, deprecated, "expose")
}
@ -587,12 +574,12 @@ services:
service: foo
`)
require.Error(t, err)
assert.Error(t, err)
assert.IsType(t, &ForbiddenPropertiesError{}, err)
fmt.Println(err)
forbidden := err.(*ForbiddenPropertiesError).Properties
assert.Len(t, forbidden, 2)
assert.Equal(t, 2, len(forbidden))
assert.Contains(t, forbidden, "volume_driver")
assert.Contains(t, forbidden, "extends")
}
@ -608,7 +595,7 @@ func TestInvalidResource(t *testing.T) {
impossible:
x: 1
`)
require.Error(t, err)
assert.Error(t, err)
assert.Contains(t, err.Error(), "Additional property impossible is not allowed")
}
@ -621,7 +608,7 @@ volumes:
driver: foobar
`)
require.Error(t, err)
assert.Error(t, err)
assert.Contains(t, err.Error(), "conflicting parameters \"external\" and \"driver\" specified for volume")
assert.Contains(t, err.Error(), "external_volume")
}
@ -636,7 +623,7 @@ volumes:
beep: boop
`)
require.Error(t, err)
assert.Error(t, err)
assert.Contains(t, err.Error(), "conflicting parameters \"external\" and \"driver_opts\" specified for volume")
assert.Contains(t, err.Error(), "external_volume")
}
@ -651,7 +638,7 @@ volumes:
- beep=boop
`)
require.Error(t, err)
assert.Error(t, err)
assert.Contains(t, err.Error(), "conflicting parameters \"external\" and \"labels\" specified for volume")
assert.Contains(t, err.Error(), "external_volume")
}
@ -666,7 +653,8 @@ volumes:
name: external_name
`)
require.Error(t, err)
assert.Error(t, err)
fmt.Println(err)
assert.Contains(t, err.Error(), "volume.external.name and volume.name conflict; only use volume.name")
assert.Contains(t, err.Error(), "external_volume")
}
@ -681,30 +669,23 @@ func uint64Ptr(value uint64) *uint64 {
func TestFullExample(t *testing.T) {
bytes, err := ioutil.ReadFile("full-example.yml")
require.NoError(t, err)
assert.NoError(t, err)
homeDir := "/home/foo"
env := map[string]string{"HOME": homeDir, "QUX": "qux_from_environment"}
config, err := loadYAMLWithEnv(string(bytes), env)
require.NoError(t, err)
if !assert.NoError(t, err) {
return
}
workingDir, err := os.Getwd()
require.NoError(t, err)
assert.NoError(t, err)
stopGracePeriod := time.Duration(20 * time.Second)
expectedServiceConfig := types.ServiceConfig{
Name: "foo",
Build: types.BuildConfig{
Context: "./dir",
Dockerfile: "Dockerfile",
Args: map[string]*string{"foo": strPtr("bar")},
Target: "foo",
Network: "foo",
CacheFrom: []string{"foo", "bar"},
Labels: map[string]string{"FOO": "BAR"},
},
CapAdd: []string{"ALL"},
CapDrop: []string{"NET_ADMIN", "SYS_ADMIN"},
CgroupParent: "m-executor-abcd",
@ -1088,7 +1069,9 @@ networks:
mynet2:
driver: bridge
`)
require.NoError(t, err)
if !assert.NoError(t, err) {
return
}
expected := map[string]types.NetworkConfig{
"mynet1": {
@ -1122,7 +1105,9 @@ services:
target: 22
published: 10022
`)
require.NoError(t, err)
if !assert.NoError(t, err) {
return
}
expected := []types.ServicePortConfig{
{
@ -1185,7 +1170,7 @@ services:
},
}
assert.Len(t, config.Services, 1)
assert.Equal(t, 1, len(config.Services))
assert.Equal(t, expected, config.Services[0].Ports)
}
@ -1203,7 +1188,9 @@ services:
volumes:
foo: {}
`)
require.NoError(t, err)
if !assert.NoError(t, err) {
return
}
expected := types.ServiceVolumeConfig{
Type: "volume",
@ -1212,7 +1199,7 @@ volumes:
ReadOnly: true,
}
require.Len(t, config.Services, 1)
assert.Len(t, config.Services[0].Volumes, 1)
assert.Equal(t, 1, len(config.Services))
assert.Equal(t, 1, len(config.Services[0].Volumes))
assert.Equal(t, expected, config.Services[0].Volumes[0])
}

View File

@ -79,7 +79,6 @@ type Config struct {
type ServiceConfig struct {
Name string
Build BuildConfig
CapAdd []string `mapstructure:"cap_add"`
CapDrop []string `mapstructure:"cap_drop"`
CgroupParent string `mapstructure:"cgroup_parent"`
@ -127,18 +126,6 @@ type ServiceConfig struct {
WorkingDir string `mapstructure:"working_dir"`
}
// BuildConfig is a type for build
// using the same format at libcompose: https://github.com/docker/libcompose/blob/master/yaml/build.go#L12
type BuildConfig struct {
Context string
Dockerfile string
Args MappingWithEquals
Labels Labels
CacheFrom StringList `mapstructure:"cache_from"`
Network string
Target string
}
// ShellCommand is a string or list of string args
type ShellCommand []string

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