Compare commits
421 Commits
v17.09.1-c
...
v17.10.0-c
| Author | SHA1 | Date | |
|---|---|---|---|
| f4ffd2511c | |||
| ec6b6de1c8 | |||
| 8bac3f632b | |||
| 646a76c31b | |||
| af94197a36 | |||
| 2f11502622 | |||
| 24ee573f35 | |||
| 4d73e16a14 | |||
| 1311aaf76e | |||
| 6f56735237 | |||
| e274827392 | |||
| e7f51b183f | |||
| 792e8f9a82 | |||
| 8b13940b3b | |||
| 550052ebae | |||
| b0596beda5 | |||
| 6fc2c6b8c5 | |||
| f26232e8f6 | |||
| 018b6401f5 | |||
| 56bd88c226 | |||
| fc8971da3b | |||
| 9f566ba32b | |||
| 785d4378ad | |||
| e9a538eda6 | |||
| cdee6d83f4 | |||
| c76d10969f | |||
| 02f6ddc6ae | |||
| 125ffb8b1b | |||
| 9c61899f29 | |||
| 76922d8690 | |||
| f437cf754f | |||
| 2adb51e303 | |||
| a761ee3d4d | |||
| e1484d2ff8 | |||
| d82f9fe3f3 | |||
| 73ecdcee10 | |||
| c8e9afef61 | |||
| d866876d86 | |||
| 8ab000a5f4 | |||
| c8ede6fcc6 | |||
| 0d46b8e710 | |||
| 58f592c0d9 | |||
| e58d3abea8 | |||
| 4f818dd6f7 | |||
| d73806305a | |||
| 4e81e4fa4e | |||
| 8a6c4c93b6 | |||
| 7b99808cac | |||
| fab4b40e38 | |||
| 079f5eb5e5 | |||
| db0a220cda | |||
| 71bb1e8c44 | |||
| 328a5917ad | |||
| 09bc512154 | |||
| 4b8b813d22 | |||
| 0e6592d05e | |||
| 4c4e74cebe | |||
| 28e35b96d0 | |||
| c9fbb737e9 | |||
| c9e8020d97 | |||
| 0b195d3e47 | |||
| 6230f06c47 | |||
| 5edb9ec08c | |||
| 907b966c7e | |||
| 4034b44eca | |||
| 80c3ad8db1 | |||
| d62e09de17 | |||
| 5fe53bab44 | |||
| ae273c63d1 | |||
| 4330196b5a | |||
| e25514ef0c | |||
| d6eb3e21a4 | |||
| 747a47b477 | |||
| d7aeee3eda | |||
| 8d23cb5213 | |||
| ca5a5c5ca1 | |||
| f6d296e26e | |||
| 00bb761ead | |||
| 747646833d | |||
| b4a6a3d358 | |||
| 915d4f9741 | |||
| a58b05b91c | |||
| 4d03073a2c | |||
| d14643fb47 | |||
| 240408899b | |||
| 4262667057 | |||
| 7ed1303eb4 | |||
| 689b8ff233 | |||
| 5a67fa7a37 | |||
| bb1eca37a9 | |||
| e4f32567d3 | |||
| fa385958f4 | |||
| 23c421f3c5 | |||
| de74f6b50c | |||
| f99bd6dc82 | |||
| dead314452 | |||
| 8a5301f81b | |||
| 1a654751d3 | |||
| 7fd47ecc0d | |||
| 09b100cab2 | |||
| 6424aeb9fa | |||
| 57c6d20473 | |||
| f321e1ed9d | |||
| 2a54e5d16e | |||
| e3c3856028 | |||
| 86f10d67d5 | |||
| 399de7ee20 | |||
| 57dd97eab1 | |||
| 671d46ed3d | |||
| 9e59512fe4 | |||
| 1fe51e0bb7 | |||
| 5dbfedf3f9 | |||
| c4c68bf819 | |||
| b987e63572 | |||
| cbf8cc6faf | |||
| b4098a4e18 | |||
| 48757849a4 | |||
| ec4868350d | |||
| fa79e0059e | |||
| 3366e8951a | |||
| 401172966f | |||
| 1608188995 | |||
| f367f50630 | |||
| 7d644fb209 | |||
| 4701b66889 | |||
| a001c9d5c7 | |||
| 0c6f170345 | |||
| 101f740d40 | |||
| 658351133f | |||
| 3c4574bac3 | |||
| 025e41d2ae | |||
| 702bac1553 | |||
| e0bca90fda | |||
| 94f2b59302 | |||
| 188ec7af2a | |||
| a59fc63f3b | |||
| 7c83ece8ef | |||
| 1659928f34 | |||
| 51f87f33d4 | |||
| 93ba24cc42 | |||
| b53048a314 | |||
| e42a2df102 | |||
| e92c4686dd | |||
| 97f2ec42fa | |||
| e8d4ecb361 | |||
| ad5c7cb4bb | |||
| 644ae3a3f1 | |||
| 1115dc5cef | |||
| 530ee183f1 | |||
| 742798f154 | |||
| 86beae9d05 | |||
| 0975184f91 | |||
| 0c54b3a041 | |||
| 522e6cc8ec | |||
| 0b2721b9c1 | |||
| e38ecccceb | |||
| 954a7de027 | |||
| 08619d0c7c | |||
| 5c2be22475 | |||
| fdeab20153 | |||
| 28762b7242 | |||
| 0f097012c8 | |||
| 753a551b2e | |||
| a0ff43ff51 | |||
| f3a4b20d25 | |||
| 278bf3cb85 | |||
| afac6be123 | |||
| 441f6cef6d | |||
| 6771dc87e0 | |||
| 793cd3af24 | |||
| 2d68241660 | |||
| d1d8439f3f | |||
| 6700f361c5 | |||
| 3a50fe644a | |||
| e22297f2e2 | |||
| 9f6a445ef2 | |||
| aebe8e8ce7 | |||
| 0dfdf37ab2 | |||
| ccdce91e65 | |||
| e55d5634bf | |||
| 028b015443 | |||
| 1e4bfaa04a | |||
| 5cb2c664ef | |||
| da0223d365 | |||
| 20c1a2b928 | |||
| 9337e13113 | |||
| 6280c12bb9 | |||
| 315e815656 | |||
| 7e0e805bca | |||
| cdbfcd2c1d | |||
| 36e37bb56e | |||
| 62cdceef74 | |||
| de0f898e39 | |||
| e2fa4c4d55 | |||
| 7d19908a7b | |||
| 95b34532ce | |||
| c59fc2b89e | |||
| 48266cefa7 | |||
| 0ee27e86a8 | |||
| bb1c0c5aa8 | |||
| d9942eee10 | |||
| 5338e5013a | |||
| 46ea757ea5 | |||
| 95004c9da9 | |||
| d382cdf023 | |||
| 6a9321461a | |||
| a5ceeb03a8 | |||
| ddb0ee3757 | |||
| cb0c1a12c4 | |||
| 84b2d8c7a7 | |||
| 1411476434 | |||
| 00de807109 | |||
| 13c06c5996 | |||
| f7daf26c0f | |||
| 8828a6255a | |||
| 6daf6c2b58 | |||
| da00b40eb4 | |||
| 07893693ca | |||
| 889843574b | |||
| 822098983e | |||
| 1cc4bb63c2 | |||
| 1cc36398f9 | |||
| a17ffddbf6 | |||
| f8cc2fb021 | |||
| b07dcb72fb | |||
| 7ac80a2691 | |||
| 3303de2b82 | |||
| b8677707a9 | |||
| 61af2a7b87 | |||
| fab90b5b50 | |||
| f01963dfea | |||
| dd976e6170 | |||
| 0a503dae01 | |||
| 50bc0b5a39 | |||
| 9f902b9066 | |||
| 5421741502 | |||
| 133eff770e | |||
| e01e198f54 | |||
| 3a4f4fd3c4 | |||
| dd4630ca9c | |||
| a14ef1d5fc | |||
| e3dc487328 | |||
| a4c2f1d0c4 | |||
| c25f7e149b | |||
| c53f4217c9 | |||
| 322ee8f812 | |||
| 8fb9073dbb | |||
| 5681a2921d | |||
| af2e8abbf0 | |||
| 347269ee44 | |||
| 19d23646ea | |||
| 35f91d208e | |||
| 4609e2004f | |||
| a8090896a0 | |||
| d8ebaa5838 | |||
| a785d9466c | |||
| 892a44a627 | |||
| 2a72ee1fde | |||
| adba854430 | |||
| 2a306cdba9 | |||
| 04f1d90792 | |||
| eae91199bd | |||
| b66e771f48 | |||
| 3223c51c8a | |||
| b43594fccc | |||
| a5b24016fd | |||
| 5086fdcfde | |||
| ae2d031a76 | |||
| 70abc439d8 | |||
| a641c1b7f0 | |||
| aeb89eb179 | |||
| cddfe04f6a | |||
| 14427ba493 | |||
| 29ef2e0479 | |||
| f34f36be87 | |||
| 44733ed80a | |||
| 6cc83af533 | |||
| 82ba6d5d3e | |||
| 414afe084c | |||
| a13cdc241b | |||
| 454535ed22 | |||
| 58906ac91c | |||
| 4d643fb18a | |||
| f23d5bec57 | |||
| abba79d025 | |||
| 2801aaa58e | |||
| e3234a8303 | |||
| fe0ff1cb68 | |||
| 559acd0f8b | |||
| 16788ee2e5 | |||
| 9c49974512 | |||
| b3c31e9800 | |||
| ac38bbaf63 | |||
| 97847089f1 | |||
| 1546d32eb0 | |||
| c95e22b252 | |||
| 6c749ced41 | |||
| 023fe0b32f | |||
| 93617a3f74 | |||
| c30f13232a | |||
| 803b5f0e94 | |||
| 63f4bb52b4 | |||
| 4fe1c1584a | |||
| 150f7f6a55 | |||
| 46def61677 | |||
| c752d6bc2e | |||
| d011c4127f | |||
| 08356f8921 | |||
| 69753f2d25 | |||
| 57a43e8ab0 | |||
| 07369b8b77 | |||
| 1a8ec36d61 | |||
| 7742f2c56c | |||
| 8453ea8ddd | |||
| f7e5e8fdc8 | |||
| 002e234c45 | |||
| b402c14275 | |||
| ad105d4aec | |||
| 806d838e90 | |||
| 523435549c | |||
| 9d7caf267c | |||
| 99b37c7406 | |||
| 57c34191f2 | |||
| 4219a83be8 | |||
| a87f17c01b | |||
| 053e3db35e | |||
| e18e2f39e3 | |||
| 6e9f643bb2 | |||
| eae7cc4c29 | |||
| ca1972776d | |||
| baffbb54c8 | |||
| fda4a38658 | |||
| 55cb898a72 | |||
| 18c29c5c5f | |||
| 1d65628454 | |||
| 52611dee7a | |||
| a300c8a80f | |||
| 3e4a18de4d | |||
| ee7b60968b | |||
| 7f5ba34576 | |||
| 528c150b8c | |||
| 99e268ef3c | |||
| 599a2ae3b1 | |||
| 591014017f | |||
| 2c45bc6a92 | |||
| 8e2757c531 | |||
| 033298b8f6 | |||
| 95956aa7ef | |||
| 1b8f2ad866 | |||
| e388be9473 | |||
| af0adf66d4 | |||
| 4b9c2f5fd5 | |||
| fdd3e06a77 | |||
| 30a4e0ab8e | |||
| fbd21afa80 | |||
| 94dd311bdc | |||
| 9211237d01 | |||
| b363a8f8a7 | |||
| 66ae0e70f8 | |||
| d7ed53048b | |||
| b543555e86 | |||
| 58743c32f5 | |||
| 810209d155 | |||
| f1b816d854 | |||
| 2e7d5b113f | |||
| 5899c64938 | |||
| 56f259f37d | |||
| 0d3330814c | |||
| d7504f67d1 | |||
| 980d05f2ea | |||
| 5f49aab36a | |||
| 6f155f233b | |||
| a8b7a6b071 | |||
| 3aa4929239 | |||
| eec1d4ef54 | |||
| 567b25fb28 | |||
| 5063459b89 | |||
| 74473d87d6 | |||
| 11b376603e | |||
| 4a595ff1d9 | |||
| 6fc4c48771 | |||
| e65a64c87e | |||
| cb4257d5fb | |||
| 9f20b18453 | |||
| c87a482794 | |||
| 8db4d9c803 | |||
| cc7b3620f0 | |||
| 33519380e5 | |||
| 67de569278 | |||
| 6ee8cdfa2e | |||
| 0bd644faaf | |||
| fd94302cba | |||
| 54efaca689 | |||
| 50ebf89669 | |||
| 57918230aa | |||
| f109c25168 | |||
| 33d6fdcfda | |||
| 28cf6767d3 | |||
| 56b8d8a2a0 | |||
| 33179f56a2 | |||
| e76adc6a0e | |||
| 30986cbc05 | |||
| db0c7d8918 | |||
| 2f974e7293 | |||
| 09edca8b4e | |||
| 55c903723c | |||
| 444c2e65a4 | |||
| 4bafd44516 | |||
| 0699e6b42a | |||
| ee07288be3 | |||
| 2578773887 | |||
| d99a5ff6bf | |||
| 58ef7a0132 | |||
| 35c8905315 | |||
| 3c0eab373a | |||
| 6eb6b7d9d2 | |||
| b812e1036b | |||
| b132c15093 | |||
| ce23144495 | |||
| 934616e542 | |||
| 615f3700ae |
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
.helpers/
|
||||
124
CHANGELOG.md
124
CHANGELOG.md
@ -5,110 +5,50 @@ 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.09.1-ce (2017-12-07)
|
||||
## 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.
|
||||
|
||||
### Builder
|
||||
|
||||
- Fix config leakage on shared parent stage [moby/moby#33753](https://github.com/moby/moby/issues/33753)
|
||||
- Warn on empty continuation lines only, not on comment-only lines [moby/moby#35004](https://github.com/moby/moby/pull/35004)
|
||||
* 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)
|
||||
|
||||
### Client
|
||||
|
||||
- Set API version on Client even when Ping fails [docker/cli#546](https://github.com/docker/cli/pull/546)
|
||||
|
||||
### Networking
|
||||
|
||||
- Overlay fix for transient IP reuse [docker/libnetwork#2016](https://github.com/docker/libnetwork/pull/2016)
|
||||
- Fix reapTime logic in NetworkDB and handle DNS cleanup for attachable container [docker/libnetwork#2017](https://github.com/docker/libnetwork/pull/2017)
|
||||
- Disable hostname lookup on chain exists check [docker/libnetwork#2019](https://github.com/docker/libnetwork/pull/2019)
|
||||
- Fix lint issues [docker/libnetwork#2020](https://github.com/docker/libnetwork/pull/2020)
|
||||
- Restore error type in FindNetwork [moby/moby#35634](https://github.com/moby/moby/pull/35634)
|
||||
|
||||
### Runtime
|
||||
|
||||
- Protect `health monitor` Go channel [moby/moby#35482](https://github.com/moby/moby/pull/35482)
|
||||
- Fix test failure on stopped container [moby/moby#34730](https://github.com/moby/moby/pull/34730)
|
||||
- Fix leaking container/exec state [moby/moby#35484](https://github.com/moby/moby/pull/35484)
|
||||
- Add /proc/scsi to masked paths (patch to work around [CVE-2017-16539](http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-16539)) [moby/moby/#35399](https://github.com/moby/moby/pull/35399)
|
||||
- Vendor tar-split: fix to prevent memory exhaustion issue that could crash Docker daemon [moby/moby/#35424](https://github.com/moby/moby/pull/35424) Fixes [CVE-2017-14992](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-14992)
|
||||
- Fix P/Z HubPullSuite tests [moby/moby#34837](https://github.com/moby/moby/pull/34837)
|
||||
+ Windows: Add support for version filtering on pull [moby/moby#35090](https://github.com/moby/moby/pull/35090)
|
||||
- Windows: Stop filtering Windows manifest lists by version [moby/moby#35117](https://github.com/moby/moby/pull/35117)
|
||||
- Use rslave instead of rprivate in chroot archive [moby/moby/#35217](https://github.com/moby/moby/pull/35217)
|
||||
- Remove container rootfs mountPath after unmount [moby/moby#34573](https://github.com/moby/moby/pull/34573)
|
||||
- Fix honoring tmpfs size of user /dev/shm mount [moby/moby#35316](https://github.com/moby/moby/pull/35316)
|
||||
- Don't abort when setting may_detach_mounts (log the error instead) [moby/moby#35172](https://github.com/moby/moby/pull/35172)
|
||||
- Fix version comparison when negotiating the API version [moby/moby#35008](https://github.com/moby/moby/pull/35008)
|
||||
|
||||
### Swarm mode
|
||||
|
||||
* Increase gRPC request timeout when sending snapshots [docker/swarmkit#2404](https://github.com/docker/swarmkit/pull/2404)
|
||||
- Fix node filtering when there is no log driver [docker/swarmkit#2442](https://github.com/docker/swarmkit/pull/2442)
|
||||
- Add an error on attempt to change cluster name [docker/swarmkit/#2454](https://github.com/docker/swarmkit/pull/2454)
|
||||
- Delete node attachments when node is removed [docker/swarmkit/#2456](https://github.com/docker/swarmkit/pull/2456)
|
||||
- Provide custom gRPC dialer to override default proxy dialer [docker/swarmkit/#2457](https://github.com/docker/swarmkit/pull/2457)
|
||||
- Avoids recursive readlock on swarm info [moby/moby#35388](https://github.com/moby/moby/pull/35388)
|
||||
|
||||
## 17.09.0-ce (2017-09-26)
|
||||
|
||||
### Builder
|
||||
|
||||
+ 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
|
||||
|
||||
* 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)
|
||||
* 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)
|
||||
|
||||
### Networking
|
||||
|
||||
+ 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)
|
||||
* 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)
|
||||
|
||||
### Runtime
|
||||
|
||||
* 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)
|
||||
* 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)
|
||||
|
||||
### Swarm mode
|
||||
|
||||
* 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)
|
||||
* 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)
|
||||
|
||||
29
Makefile
29
Makefile
@ -1,27 +1,56 @@
|
||||
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
|
||||
|
||||
@ -27,6 +27,7 @@
|
||||
|
||||
people = [
|
||||
"aaronlehmann",
|
||||
"albers",
|
||||
"aluzzardi",
|
||||
"anusha",
|
||||
"cpuguy83",
|
||||
@ -84,6 +85,11 @@
|
||||
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"
|
||||
|
||||
@ -34,6 +34,14 @@ 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
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
[](https://circleci.com/gh/docker/cli/tree/master)
|
||||
[](https://circleci.com/gh/docker/cli/tree/master) [](https://jenkins.dockerproject.org/job/docker/job/cli/job/master/)
|
||||
|
||||
docker/cli
|
||||
==========
|
||||
|
||||
@ -1 +1 @@
|
||||
17.09.1-ce
|
||||
17.10.0-ce
|
||||
|
||||
@ -12,12 +12,14 @@ 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"
|
||||
@ -40,6 +42,7 @@ 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.
|
||||
@ -161,6 +164,11 @@ func getClientWithPassword(passRetriever notary.PassRetriever, newClient func(pa
|
||||
}
|
||||
}
|
||||
|
||||
// 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...)
|
||||
}
|
||||
|
||||
// ServerInfo stores details about the supported features and platform of the
|
||||
// server
|
||||
type ServerInfo struct {
|
||||
|
||||
@ -17,6 +17,7 @@ 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"
|
||||
)
|
||||
@ -69,6 +70,9 @@ func AddCommands(cmd *cobra.Command, dockerCli *command.DockerCli) {
|
||||
// swarm
|
||||
swarm.NewSwarmCommand(dockerCli),
|
||||
|
||||
// trust
|
||||
trust.NewTrustCommand(dockerCli),
|
||||
|
||||
// volume
|
||||
volume.NewVolumeCommand(dockerCli),
|
||||
|
||||
|
||||
@ -1,15 +1,27 @@
|
||||
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
|
||||
@ -55,6 +67,8 @@ func runConfigList(dockerCli command.Cli, options listOptions) error {
|
||||
}
|
||||
}
|
||||
|
||||
sort.Sort(byConfigName(configs))
|
||||
|
||||
configCtx := formatter.Context{
|
||||
Output: dockerCli.Out(),
|
||||
Format: formatter.NewConfigFormat(format, options.quiet),
|
||||
|
||||
@ -50,14 +50,20 @@ func TestConfigList(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
configListFunc: func(options types.ConfigListOptions) ([]swarm.Config, error) {
|
||||
return []swarm.Config{
|
||||
*Config(ConfigID("ID-foo"),
|
||||
ConfigName("foo"),
|
||||
*Config(ConfigID("ID-1-foo"),
|
||||
ConfigName("1-foo"),
|
||||
ConfigVersion(swarm.Version{Index: 10}),
|
||||
ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
|
||||
ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
|
||||
),
|
||||
*Config(ConfigID("ID-bar"),
|
||||
ConfigName("bar"),
|
||||
*Config(ConfigID("ID-10-foo"),
|
||||
ConfigName("10-foo"),
|
||||
ConfigVersion(swarm.Version{Index: 11}),
|
||||
ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
|
||||
ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
|
||||
),
|
||||
*Config(ConfigID("ID-2-foo"),
|
||||
ConfigName("2-foo"),
|
||||
ConfigVersion(swarm.Version{Index: 11}),
|
||||
ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
|
||||
ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
|
||||
@ -66,9 +72,8 @@ func TestConfigList(t *testing.T) {
|
||||
},
|
||||
})
|
||||
cmd := newConfigListCommand(cli)
|
||||
cmd.SetOutput(cli.OutBuffer())
|
||||
assert.NoError(t, cmd.Execute())
|
||||
golden.Assert(t, cli.OutBuffer().String(), "config-list.golden")
|
||||
golden.Assert(t, cli.OutBuffer().String(), "config-list-sort.golden")
|
||||
}
|
||||
|
||||
func TestConfigListWithQuietOption(t *testing.T) {
|
||||
|
||||
4
components/cli/cli/command/config/testdata/config-list-sort.golden
vendored
Normal file
4
components/cli/cli/command/config/testdata/config-list-sort.golden
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
ID NAME CREATED UPDATED
|
||||
ID-1-foo 1-foo 2 hours ago About an hour ago
|
||||
ID-2-foo 2-foo 2 hours ago About an hour ago
|
||||
ID-10-foo 10-foo 2 hours ago About an hour ago
|
||||
@ -1,2 +1,2 @@
|
||||
foo
|
||||
bar label=label-bar
|
||||
foo
|
||||
|
||||
@ -1,3 +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
|
||||
ID-foo foo 2 hours ago About an hour ago
|
||||
|
||||
@ -1,2 +1,2 @@
|
||||
foo
|
||||
bar label=label-bar
|
||||
foo
|
||||
|
||||
@ -1,2 +1,2 @@
|
||||
ID-foo
|
||||
ID-bar
|
||||
ID-foo
|
||||
|
||||
@ -1,3 +0,0 @@
|
||||
ID NAME CREATED UPDATED
|
||||
ID-foo foo 2 hours ago About an hour ago
|
||||
ID-bar bar 2 hours ago About an hour ago
|
||||
@ -113,7 +113,7 @@ func resolveLocalPath(localPath string) (absPath string, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
return archive.PreserveTrailingDotOrSeparator(absPath, localPath), nil
|
||||
return archive.PreserveTrailingDotOrSeparator(absPath, localPath, filepath.Separator), nil
|
||||
}
|
||||
|
||||
func copyFromContainer(ctx context.Context, dockerCli *command.DockerCli, srcContainer, srcPath, dstPath string, cpParam *cpConfig) (err error) {
|
||||
|
||||
@ -10,7 +10,6 @@ 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"
|
||||
@ -106,7 +105,6 @@ func interactiveExec(ctx context.Context, dockerCli command.Cli, execConfig *typ
|
||||
var (
|
||||
out, stderr io.Writer
|
||||
in io.ReadCloser
|
||||
errCh chan error
|
||||
)
|
||||
|
||||
if execConfig.AttachStdin {
|
||||
@ -129,19 +127,25 @@ 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,
|
||||
}
|
||||
|
||||
return streamer.stream(ctx)
|
||||
})
|
||||
errCh := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
defer close(errCh)
|
||||
errCh <- func() error {
|
||||
streamer := hijackedIOStreamer{
|
||||
streams: dockerCli,
|
||||
inputStream: in,
|
||||
outputStream: out,
|
||||
errorStream: stderr,
|
||||
resp: resp,
|
||||
tty: execConfig.Tty,
|
||||
detachKeys: execConfig.DetachKeys,
|
||||
}
|
||||
|
||||
return streamer.stream(ctx)
|
||||
}()
|
||||
}()
|
||||
|
||||
if execConfig.Tty && dockerCli.In().IsTerminal() {
|
||||
if err := MonitorTtySize(ctx, dockerCli, execID, true); err != nil {
|
||||
|
||||
@ -185,6 +185,7 @@ 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()
|
||||
|
||||
@ -43,6 +43,7 @@ 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)
|
||||
|
||||
@ -15,7 +15,6 @@ 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"
|
||||
@ -291,22 +290,27 @@ func attachContainer(
|
||||
return nil, errAttach
|
||||
}
|
||||
|
||||
*errCh = promise.Go(func() error {
|
||||
streamer := hijackedIOStreamer{
|
||||
streams: dockerCli,
|
||||
inputStream: in,
|
||||
outputStream: out,
|
||||
errorStream: cerr,
|
||||
resp: resp,
|
||||
tty: config.Tty,
|
||||
detachKeys: options.DetachKeys,
|
||||
}
|
||||
ch := make(chan error, 1)
|
||||
*errCh = ch
|
||||
|
||||
if errHijack := streamer.stream(ctx); errHijack != nil {
|
||||
return errHijack
|
||||
}
|
||||
return errAttach
|
||||
})
|
||||
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
|
||||
}()
|
||||
}()
|
||||
return resp.Close, nil
|
||||
}
|
||||
|
||||
|
||||
@ -9,7 +9,6 @@ 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"
|
||||
@ -103,23 +102,28 @@ 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,
|
||||
}
|
||||
|
||||
errHijack := streamer.stream(ctx)
|
||||
if errHijack == nil {
|
||||
return errAttach
|
||||
}
|
||||
return errHijack
|
||||
})
|
||||
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
|
||||
}()
|
||||
}()
|
||||
|
||||
// 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.
|
||||
|
||||
@ -21,6 +21,7 @@ import (
|
||||
type statsOptions struct {
|
||||
all bool
|
||||
noStream bool
|
||||
noTrunc bool
|
||||
format string
|
||||
containers []string
|
||||
}
|
||||
@ -42,6 +43,7 @@ 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
|
||||
}
|
||||
@ -214,7 +216,7 @@ func runStats(dockerCli *command.DockerCli, opts *statsOptions) error {
|
||||
ccstats = append(ccstats, c.GetStatistics())
|
||||
}
|
||||
cStats.mu.Unlock()
|
||||
if err = formatter.ContainerStatsWrite(statsCtx, ccstats, daemonOSType); err != nil {
|
||||
if err = formatter.ContainerStatsWrite(statsCtx, ccstats, daemonOSType, !opts.noTrunc); err != nil {
|
||||
break
|
||||
}
|
||||
if len(cStats.cs) == 0 && !showAll {
|
||||
|
||||
@ -10,6 +10,7 @@ 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"
|
||||
)
|
||||
@ -230,10 +231,7 @@ 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)},
|
||||
`CONTAINER ID IMAGE CREATED/STATUS/ PORTS .NAMES STATUS
|
||||
conta "ubuntu" 24 hours ago//.FOOBAR_BAZ
|
||||
conta "ubuntu" 24 hours ago//.FOOBAR_BAR
|
||||
`,
|
||||
string(golden.Get(t, "container-context-write-special-headers.golden")),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/gotestyourself/gotestyourself/golden"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@ -83,12 +84,7 @@ Build Cache 0B
|
||||
Format: NewDiskUsageFormat("table {{.Type}}\t{{.Active}}"),
|
||||
},
|
||||
},
|
||||
`TYPE ACTIVE
|
||||
Images 0
|
||||
Containers 0
|
||||
Local Volumes 0
|
||||
Build Cache
|
||||
`,
|
||||
string(golden.Get(t, "disk-usage-context-write-custom.golden")),
|
||||
},
|
||||
// Raw Format
|
||||
{
|
||||
@ -97,31 +93,7 @@ Build Cache
|
||||
Format: NewDiskUsageFormat("raw"),
|
||||
},
|
||||
},
|
||||
`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
|
||||
|
||||
`,
|
||||
string(golden.Get(t, "disk-usage-raw-format.golden")),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@ -8,6 +8,7 @@ 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"
|
||||
)
|
||||
|
||||
@ -120,10 +121,7 @@ func TestSearchContextWrite(t *testing.T) {
|
||||
// Table format
|
||||
{
|
||||
Context{Format: NewSearchFormat("table")},
|
||||
`NAME DESCRIPTION STARS OFFICIAL AUTOMATED
|
||||
result1 Official build 5000 [OK]
|
||||
result2 Not official 5 [OK]
|
||||
`,
|
||||
string(golden.Get(t, "search-context-write-table.golden")),
|
||||
},
|
||||
{
|
||||
Context{Format: NewSearchFormat("table {{.Name}}")},
|
||||
@ -210,9 +208,7 @@ func TestSearchContextWriteStars(t *testing.T) {
|
||||
// Table format
|
||||
{
|
||||
Context{Format: NewSearchFormat("table")},
|
||||
`NAME DESCRIPTION STARS OFFICIAL AUTOMATED
|
||||
result1 Official build 5000 [OK]
|
||||
`,
|
||||
string(golden.Get(t, "search-context-write-stars-table.golden")),
|
||||
},
|
||||
{
|
||||
Context{Format: NewSearchFormat("table {{.Name}}")},
|
||||
|
||||
@ -12,19 +12,20 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
defaultSecretTableFormat = "table {{.ID}}\t{{.Name}}\t{{.CreatedAt}}\t{{.UpdatedAt}}"
|
||||
defaultSecretTableFormat = "table {{.ID}}\t{{.Name}}\t{{.Driver}}\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 }}
|
||||
Created at: {{.CreatedAt}}
|
||||
Updated at: {{.UpdatedAt}}`
|
||||
Driver: {{.Driver}}
|
||||
Created at: {{.CreatedAt}}
|
||||
Updated at: {{.UpdatedAt}}`
|
||||
)
|
||||
|
||||
// NewSecretFormat returns a Format for rendering using a secret Context
|
||||
@ -61,6 +62,7 @@ func newSecretContext() *secretContext {
|
||||
sCtx.header = map[string]string{
|
||||
"ID": secretIDHeader,
|
||||
"Name": nameHeader,
|
||||
"Driver": driverHeader,
|
||||
"CreatedAt": secretCreatedHeader,
|
||||
"UpdatedAt": secretUpdatedHeader,
|
||||
"Labels": labelsHeader,
|
||||
@ -89,6 +91,13 @@ 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"
|
||||
}
|
||||
@ -153,6 +162,13 @@ 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)
|
||||
}
|
||||
|
||||
@ -28,9 +28,9 @@ func TestSecretContextFormatWrite(t *testing.T) {
|
||||
},
|
||||
// Table format
|
||||
{Context{Format: NewSecretFormat("table", false)},
|
||||
`ID NAME CREATED UPDATED
|
||||
1 passwords Less than a second ago Less than a second ago
|
||||
2 id_rsa Less than a second ago Less than a second ago
|
||||
`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
|
||||
`},
|
||||
{Context{Format: NewSecretFormat("table {{.Name}}", true)},
|
||||
`NAME
|
||||
|
||||
@ -8,6 +8,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/gotestyourself/gotestyourself/golden"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@ -59,21 +60,7 @@ bar
|
||||
// Raw Format
|
||||
{
|
||||
Context{Format: NewServiceListFormat("raw", false)},
|
||||
`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
|
||||
|
||||
`,
|
||||
string(golden.Get(t, "service-context-write-raw.golden")),
|
||||
},
|
||||
{
|
||||
Context{Format: NewServiceListFormat("raw", true)},
|
||||
|
||||
@ -4,13 +4,14 @@ import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
units "github.com/docker/go-units"
|
||||
)
|
||||
|
||||
const (
|
||||
winOSType = "windows"
|
||||
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}}"
|
||||
defaultStatsTableFormat = "table {{.ID}}\t{{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}\t{{.NetIO}}\t{{.BlockIO}}\t{{.PIDs}}"
|
||||
winDefaultStatsTableFormat = "table {{.ID}}\t{{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.NetIO}}\t{{.BlockIO}}"
|
||||
|
||||
containerHeader = "CONTAINER"
|
||||
cpuPercHeader = "CPU %"
|
||||
@ -114,12 +115,13 @@ func NewContainerStats(container string) *ContainerStats {
|
||||
}
|
||||
|
||||
// ContainerStatsWrite renders the context for a list of containers statistics
|
||||
func ContainerStatsWrite(ctx Context, containerStats []StatsEntry, osType string) error {
|
||||
func ContainerStatsWrite(ctx Context, containerStats []StatsEntry, osType string, trunc bool) error {
|
||||
render := func(format func(subContext subContext) error) error {
|
||||
for _, cstats := range containerStats {
|
||||
containerStatsCtx := &containerStatsContext{
|
||||
s: cstats,
|
||||
os: osType,
|
||||
s: cstats,
|
||||
os: osType,
|
||||
trunc: trunc,
|
||||
}
|
||||
if err := format(containerStatsCtx); err != nil {
|
||||
return err
|
||||
@ -149,8 +151,9 @@ func ContainerStatsWrite(ctx Context, containerStats []StatsEntry, osType string
|
||||
|
||||
type containerStatsContext struct {
|
||||
HeaderContext
|
||||
s StatsEntry
|
||||
os string
|
||||
s StatsEntry
|
||||
os string
|
||||
trunc bool
|
||||
}
|
||||
|
||||
func (c *containerStatsContext) MarshalJSON() ([]byte, error) {
|
||||
@ -169,6 +172,9 @@ func (c *containerStatsContext) Name() string {
|
||||
}
|
||||
|
||||
func (c *containerStatsContext) ID() string {
|
||||
if c.trunc {
|
||||
return stringid.TruncateID(c.s.ID)
|
||||
}
|
||||
return c.s.ID
|
||||
}
|
||||
|
||||
|
||||
@ -114,7 +114,7 @@ container2 --
|
||||
}
|
||||
var out bytes.Buffer
|
||||
te.context.Output = &out
|
||||
err := ContainerStatsWrite(te.context, stats, "linux")
|
||||
err := ContainerStatsWrite(te.context, stats, "linux", false)
|
||||
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")
|
||||
err := ContainerStatsWrite(te.context, stats, "windows", false)
|
||||
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")
|
||||
ContainerStatsWrite(context.context, []StatsEntry{}, "linux", false)
|
||||
assert.Equal(t, context.expected, out.String())
|
||||
// Clean buffer
|
||||
out.Reset()
|
||||
@ -258,7 +258,41 @@ func TestContainerStatsContextWriteWithNoStatsWindows(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, context := range contexts {
|
||||
ContainerStatsWrite(context.context, []StatsEntry{}, "windows")
|
||||
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)
|
||||
assert.Equal(t, context.expected, out.String())
|
||||
// Clean buffer
|
||||
out.Reset()
|
||||
|
||||
@ -7,6 +7,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/gotestyourself/gotestyourself/golden"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@ -33,10 +34,7 @@ taskID2
|
||||
},
|
||||
{
|
||||
Context{Format: NewTaskFormat("table {{.Name}}\t{{.Node}}\t{{.Ports}}", false)},
|
||||
`NAME NODE PORTS
|
||||
foobar_baz foo1
|
||||
foobar_bar foo2
|
||||
`,
|
||||
string(golden.Get(t, "task-context-write-table-custom.golden")),
|
||||
},
|
||||
{
|
||||
Context{Format: NewTaskFormat("table {{.Name}}", true)},
|
||||
|
||||
3
components/cli/cli/command/formatter/testdata/container-context-write-special-headers.golden
vendored
Normal file
3
components/cli/cli/command/formatter/testdata/container-context-write-special-headers.golden
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
CONTAINER ID IMAGE CREATED/STATUS/ PORTS .NAMES STATUS
|
||||
conta "ubuntu" 24 hours ago//.FOOBAR_BAZ
|
||||
conta "ubuntu" 24 hours ago//.FOOBAR_BAR
|
||||
5
components/cli/cli/command/formatter/testdata/disk-usage-context-write-custom.golden
vendored
Normal file
5
components/cli/cli/command/formatter/testdata/disk-usage-context-write-custom.golden
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
TYPE ACTIVE
|
||||
Images 0
|
||||
Containers 0
|
||||
Local Volumes 0
|
||||
Build Cache
|
||||
24
components/cli/cli/command/formatter/testdata/disk-usage-raw-format.golden
vendored
Normal file
24
components/cli/cli/command/formatter/testdata/disk-usage-raw-format.golden
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
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
|
||||
|
||||
2
components/cli/cli/command/formatter/testdata/search-context-write-stars-table.golden
vendored
Normal file
2
components/cli/cli/command/formatter/testdata/search-context-write-stars-table.golden
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
NAME DESCRIPTION STARS OFFICIAL AUTOMATED
|
||||
result1 Official build 5000 [OK]
|
||||
3
components/cli/cli/command/formatter/testdata/search-context-write-table.golden
vendored
Normal file
3
components/cli/cli/command/formatter/testdata/search-context-write-table.golden
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
NAME DESCRIPTION STARS OFFICIAL AUTOMATED
|
||||
result1 Official build 5000 [OK]
|
||||
result2 Not official 5 [OK]
|
||||
14
components/cli/cli/command/formatter/testdata/service-context-write-raw.golden
vendored
Normal file
14
components/cli/cli/command/formatter/testdata/service-context-write-raw.golden
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
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
|
||||
|
||||
3
components/cli/cli/command/formatter/testdata/task-context-write-table-custom.golden
vendored
Normal file
3
components/cli/cli/command/formatter/testdata/task-context-write-table-custom.golden
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
NAME NODE PORTS
|
||||
foobar_baz foo1
|
||||
foobar_bar foo2
|
||||
150
components/cli/cli/command/formatter/trust.go
Normal file
150
components/cli/cli/command/formatter/trust.go
Normal file
@ -0,0 +1,150 @@
|
||||
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]
|
||||
}
|
||||
238
components/cli/cli/command/formatter/trust_test.go
Normal file
238
components/cli/cli/command/formatter/trust_test.go
Normal file
@ -0,0 +1,238 @@
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -21,6 +21,7 @@ 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"
|
||||
@ -243,6 +244,7 @@ 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
|
||||
@ -376,13 +378,13 @@ func runBuild(dockerCli command.Cli, options buildOptions) error {
|
||||
|
||||
if s != nil {
|
||||
go func() {
|
||||
logrus.Debugf("running session: %v", s.UUID())
|
||||
logrus.Debugf("running session: %v", s.ID())
|
||||
if err := s.Run(ctx, dockerCli.Client().DialSession); err != nil {
|
||||
logrus.Error(err)
|
||||
cancel() // cancel progress context
|
||||
}
|
||||
}()
|
||||
buildOptions.SessionID = s.UUID()
|
||||
buildOptions.SessionID = s.ID()
|
||||
}
|
||||
|
||||
response, err := dockerCli.Client().ImageBuild(ctx, body, buildOptions)
|
||||
|
||||
@ -53,7 +53,9 @@ func addDirToSession(session *session.Session, contextDir string, progressOutput
|
||||
|
||||
p := &sizeProgress{out: progressOutput, action: "Streaming build context to Docker daemon"}
|
||||
|
||||
workdirProvider := filesync.NewFSSyncProvider(contextDir, excludes)
|
||||
workdirProvider := filesync.NewFSSyncProvider([]filesync.SyncedDir{
|
||||
{Dir: contextDir, Excludes: excludes},
|
||||
})
|
||||
session.Allow(workdirProvider)
|
||||
|
||||
// this will be replaced on parallel build jobs. keep the current
|
||||
|
||||
@ -6,18 +6,57 @@ 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)
|
||||
|
||||
@ -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,39 +40,32 @@ func NewPullCommand(dockerCli command.Cli) *cobra.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runPull(dockerCli command.Cli, opts pullOptions) error {
|
||||
func runPull(cli command.Cli, opts pullOptions) error {
|
||||
distributionRef, err := reference.ParseNormalizedNamed(opts.remote)
|
||||
if err != nil {
|
||||
switch {
|
||||
case err != nil:
|
||||
return err
|
||||
}
|
||||
if opts.all && !reference.IsNameOnly(distributionRef) {
|
||||
case opts.all && !reference.IsNameOnly(distributionRef):
|
||||
return errors.New("tag can't be used with --all-tags/-a")
|
||||
}
|
||||
|
||||
if !opts.all && reference.IsNameOnly(distributionRef) {
|
||||
case !opts.all && reference.IsNameOnly(distributionRef):
|
||||
distributionRef = reference.TagNameOnly(distributionRef)
|
||||
if tagged, ok := distributionRef.(reference.Tagged); ok {
|
||||
fmt.Fprintf(dockerCli.Out(), "Using default tag: %s\n", tagged.Tag())
|
||||
fmt.Fprintf(cli.Out(), "Using default tag: %s\n", tagged.Tag())
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve the Repository name from fqn to RepositoryInfo
|
||||
repoInfo, err := registry.ParseRepositoryInfo(distributionRef)
|
||||
ctx := context.Background()
|
||||
imgRefAndAuth, err := trust.GetImageReferencesAndAuth(ctx, AuthResolver(cli), distributionRef.String())
|
||||
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, dockerCli, repoInfo, distributionRef, authConfig, requestPrivilege)
|
||||
err = trustedPull(ctx, cli, imgRefAndAuth)
|
||||
} else {
|
||||
err = imagePullPrivileged(ctx, dockerCli, authConfig, reference.FamiliarString(distributionRef), requestPrivilege, opts.all)
|
||||
err = imagePullPrivileged(ctx, cli, imgRefAndAuth, opts.all)
|
||||
}
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "when fetching 'plugin'") {
|
||||
@ -80,6 +73,5 @@ func runPull(dockerCli command.Cli, opts pullOptions) error {
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -2,11 +2,14 @@ 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"
|
||||
)
|
||||
@ -32,11 +35,6 @@ 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{})
|
||||
@ -49,20 +47,28 @@ func TestNewPullCommandErrors(t *testing.T) {
|
||||
|
||||
func TestNewPullCommandSuccess(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
args []string
|
||||
name string
|
||||
args []string
|
||||
expectedTag string
|
||||
}{
|
||||
{
|
||||
name: "simple",
|
||||
args: []string{"image:tag"},
|
||||
name: "simple",
|
||||
args: []string{"image:tag"},
|
||||
expectedTag: "image:tag",
|
||||
},
|
||||
{
|
||||
name: "simple-no-tag",
|
||||
args: []string{"image"},
|
||||
name: "simple-no-tag",
|
||||
args: []string{"image"},
|
||||
expectedTag: "image:latest",
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
cli := test.NewFakeCli(&fakeClient{})
|
||||
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
|
||||
},
|
||||
})
|
||||
cmd := NewPullCommand(cli)
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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"
|
||||
"github.com/opencontainers/go-digest"
|
||||
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, repoInfo, authConfig, "push", "pull")
|
||||
repo, err := trust.GetNotaryRepository(streams.In(), streams.Out(), command.UserAgent(), 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.Update(false)
|
||||
_, err = repo.ListTargets()
|
||||
|
||||
switch err.(type) {
|
||||
case client.ErrRepoNotInitialized, client.ErrRepositoryNotExist:
|
||||
keys := repo.CryptoService.ListKeys(data.CanonicalRootRole)
|
||||
keys := repo.GetCryptoService().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.CryptoService.Create(data.CanonicalRootRole, "", data.ECDSAKey)
|
||||
rootPublicKey, err := repo.GetCryptoService().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,51 +154,16 @@ func PushTrustedReference(streams command.Streams, repoInfo *registry.Repository
|
||||
return nil
|
||||
}
|
||||
|
||||
// Attempt to add the image target to all the top level delegation roles we can
|
||||
// AddTargetToAllSignableRoles attempts 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.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()
|
||||
func AddTargetToAllSignableRoles(repo client.Repository, target *client.Target) error {
|
||||
signableRoles, err := trust.GetSignableRoles(repo, target)
|
||||
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...)
|
||||
}
|
||||
|
||||
@ -217,57 +182,13 @@ 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, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig types.AuthConfig, requestPrivilege types.RequestPrivilegeFunc) error {
|
||||
var refs []target
|
||||
|
||||
notaryRepo, err := trust.GetNotaryRepository(cli, repoInfo, authConfig, "pull")
|
||||
func trustedPull(ctx context.Context, cli command.Cli, imgRefAndAuth trust.ImageRefAndAuth) error {
|
||||
refs, err := getTrustedPullTargets(cli, imgRefAndAuth)
|
||||
if err != nil {
|
||||
fmt.Fprintf(cli.Out(), "Error establishing connection to trust repository: %s\n", err)
|
||||
return err
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
ref := imgRefAndAuth.Reference()
|
||||
for i, r := range refs {
|
||||
displayTag := r.name
|
||||
if displayTag != "" {
|
||||
@ -279,7 +200,11 @@ func trustedPull(ctx context.Context, cli command.Cli, repoInfo *registry.Reposi
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := imagePullPrivileged(ctx, cli, authConfig, reference.FamiliarString(trustedRef), requestPrivilege, false); err != nil {
|
||||
updatedImgRefAndAuth, err := trust.GetImageReferencesAndAuth(ctx, AuthResolver(cli), trustedRef.String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := imagePullPrivileged(ctx, cli, updatedImgRefAndAuth, false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -295,13 +220,65 @@ func trustedPull(ctx context.Context, cli command.Cli, repoInfo *registry.Reposi
|
||||
return nil
|
||||
}
|
||||
|
||||
// imagePullPrivileged pulls the image and displays it to the output
|
||||
func imagePullPrivileged(ctx context.Context, cli command.Cli, authConfig types.AuthConfig, ref string, requestPrivilege types.RequestPrivilegeFunc, all bool) error {
|
||||
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
|
||||
}
|
||||
|
||||
encodedAuth, err := command.EncodeAuthToBase64(authConfig)
|
||||
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())
|
||||
|
||||
encodedAuth, err := command.EncodeAuthToBase64(*imgRefAndAuth.AuthConfig())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
requestPrivilege := command.RegistryAuthenticationPrivilegedFunc(cli, imgRefAndAuth.RepoInfo().Index, "pull")
|
||||
options := types.ImagePullOptions{
|
||||
RegistryAuth: encodedAuth,
|
||||
PrivilegeFunc: requestPrivilege,
|
||||
@ -335,7 +312,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, repoInfo, authConfig, "pull")
|
||||
notaryRepo, err := trust.GetNotaryRepository(cli.In(), cli.Out(), command.UserAgent(), repoInfo, &authConfig, "pull")
|
||||
if err != nil {
|
||||
fmt.Fprintf(cli.Out(), "Error establishing connection to trust repository: %s\n", err)
|
||||
return nil, err
|
||||
@ -348,7 +325,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(), errors.Errorf("No trust data for %s", ref.Tag()))
|
||||
return nil, trust.NotaryError(repoInfo.Name.Name(), client.ErrNoSuchTarget(ref.Tag()))
|
||||
}
|
||||
r, err := convertTarget(t.Target)
|
||||
if err != nil {
|
||||
@ -382,3 +359,10 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,12 +1,18 @@
|
||||
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() {
|
||||
@ -55,3 +61,15 @@ 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")
|
||||
}
|
||||
|
||||
@ -12,6 +12,7 @@ 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) {
|
||||
@ -34,3 +35,10 @@ 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
|
||||
}
|
||||
|
||||
69
components/cli/cli/command/network/list_test.go
Normal file
69
components/cli/cli/command/network/list_test.go
Normal file
@ -0,0 +1,69 @@
|
||||
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")
|
||||
}
|
||||
2
components/cli/cli/command/network/testdata/network-list.golden
vendored
Normal file
2
components/cli/cli/command/network/testdata/network-list.golden
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
NETWORK ID NAME DRIVER SCOPE
|
||||
123454321 network_1 09.7.01 global
|
||||
@ -65,10 +65,12 @@ func (s pluginRegistryService) ResolveRepository(name reference.Named) (repoInfo
|
||||
return
|
||||
}
|
||||
|
||||
func newRegistryService() registry.Service {
|
||||
return pluginRegistryService{
|
||||
Service: registry.NewService(registry.ServiceOptions{V2Only: true}),
|
||||
func newRegistryService() (registry.Service, error) {
|
||||
svc, err := registry.NewService(registry.ServiceOptions{V2Only: true})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pluginRegistryService{Service: svc}, nil
|
||||
}
|
||||
|
||||
func buildPullConfig(ctx context.Context, dockerCli *command.DockerCli, opts pluginOptions, cmdName string) (types.PluginInstallOptions, error) {
|
||||
@ -96,7 +98,11 @@ func buildPullConfig(ctx context.Context, dockerCli *command.DockerCli, opts plu
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
trusted, err := image.TrustedReference(ctx, dockerCli, nt, newRegistryService())
|
||||
svc, err := newRegistryService()
|
||||
if err != nil {
|
||||
return types.PluginInstallOptions{}, err
|
||||
}
|
||||
trusted, err := image.TrustedReference(ctx, dockerCli, nt, svc)
|
||||
if err != nil {
|
||||
return types.PluginInstallOptions{}, err
|
||||
}
|
||||
|
||||
@ -17,6 +17,7 @@ import (
|
||||
|
||||
type createOptions struct {
|
||||
name string
|
||||
driver string
|
||||
file string
|
||||
labels opts.ListOpts
|
||||
}
|
||||
@ -27,17 +28,21 @@ 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.ExactArgs(2),
|
||||
Args: cli.RequiresRangeArgs(1, 2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
options.name = args[0]
|
||||
options.file = args[1]
|
||||
if len(args) == 2 {
|
||||
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
|
||||
}
|
||||
@ -46,21 +51,14 @@ func runSecretCreate(dockerCli command.Cli, options createOptions) error {
|
||||
client := dockerCli.Client()
|
||||
ctx := context.Background()
|
||||
|
||||
var in io.Reader = dockerCli.In()
|
||||
if options.file != "-" {
|
||||
file, err := system.OpenSequential(options.file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
in = file
|
||||
defer file.Close()
|
||||
if options.driver != "" && options.file != "" {
|
||||
return errors.Errorf("When using secret driver secret data must be empty")
|
||||
}
|
||||
|
||||
secretData, err := ioutil.ReadAll(in)
|
||||
secretData, err := readSecretData(dockerCli.In(), options.file)
|
||||
if err != nil {
|
||||
return errors.Errorf("Error reading content from %q: %v", options.file, err)
|
||||
}
|
||||
|
||||
spec := swarm.SecretSpec{
|
||||
Annotations: swarm.Annotations{
|
||||
Name: options.name,
|
||||
@ -68,6 +66,11 @@ 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 {
|
||||
@ -77,3 +80,23 @@ 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
|
||||
}
|
||||
|
||||
@ -24,12 +24,11 @@ func TestSecretCreateErrors(t *testing.T) {
|
||||
secretCreateFunc func(swarm.SecretSpec) (types.SecretCreateResponse, error)
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
args: []string{"too_few"},
|
||||
expectedError: "requires exactly 2 arguments",
|
||||
},
|
||||
{args: []string{"too", "many", "arguments"},
|
||||
expectedError: "requires exactly 2 arguments",
|
||||
expectedError: "requires at least 1 and at most 2 arguments",
|
||||
},
|
||||
{args: []string{"create", "--driver", "driver", "-"},
|
||||
expectedError: "secret data must be empty",
|
||||
},
|
||||
{
|
||||
args: []string{"name", filepath.Join("testdata", secretDataFile)},
|
||||
@ -75,6 +74,35 @@ 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",
|
||||
|
||||
@ -154,6 +154,7 @@ func TestSecretInspectPretty(t *testing.T) {
|
||||
}),
|
||||
SecretID("secretID"),
|
||||
SecretName("secretName"),
|
||||
SecretDriver("driver"),
|
||||
SecretCreatedAt(time.Time{}),
|
||||
SecretUpdatedAt(time.Time{}),
|
||||
), []byte{}, nil
|
||||
|
||||
@ -1,15 +1,27 @@
|
||||
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
|
||||
@ -53,6 +65,9 @@ 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),
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
package secret
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
"time"
|
||||
@ -48,29 +47,35 @@ 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-foo"),
|
||||
SecretName("foo"),
|
||||
*Secret(SecretID("ID-1-foo"),
|
||||
SecretName("1-foo"),
|
||||
SecretVersion(swarm.Version{Index: 10}),
|
||||
SecretCreatedAt(time.Now().Add(-2*time.Hour)),
|
||||
SecretUpdatedAt(time.Now().Add(-1*time.Hour)),
|
||||
),
|
||||
*Secret(SecretID("ID-bar"),
|
||||
SecretName("bar"),
|
||||
*Secret(SecretID("ID-10-foo"),
|
||||
SecretName("10-foo"),
|
||||
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.golden")
|
||||
golden.Assert(t, cli.OutBuffer().String(), "secret-list-sort.golden")
|
||||
}
|
||||
|
||||
func TestSecretListWithQuietOption(t *testing.T) {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
ID: secretID
|
||||
Name: secretName
|
||||
ID: secretID
|
||||
Name: secretName
|
||||
Labels:
|
||||
- lbl1=value1
|
||||
Created at: 0001-01-01 00:00:00 +0000 utc
|
||||
Updated at: 0001-01-01 00:00:00 +0000 utc
|
||||
Driver: driver
|
||||
Created at: 0001-01-01 00:00:00 +0000 utc
|
||||
Updated at: 0001-01-01 00:00:00 +0000 utc
|
||||
|
||||
4
components/cli/cli/command/secret/testdata/secret-list-sort.golden
vendored
Normal file
4
components/cli/cli/command/secret/testdata/secret-list-sort.golden
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
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
|
||||
@ -1,2 +1,2 @@
|
||||
foo
|
||||
bar label=label-bar
|
||||
foo
|
||||
|
||||
@ -1,3 +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
|
||||
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
|
||||
|
||||
@ -1,2 +1,2 @@
|
||||
foo
|
||||
bar label=label-bar
|
||||
foo
|
||||
|
||||
@ -1,2 +1,2 @@
|
||||
ID-foo
|
||||
ID-bar
|
||||
ID-foo
|
||||
|
||||
@ -1,3 +0,0 @@
|
||||
ID NAME CREATED UPDATED
|
||||
ID-foo foo 2 hours ago About an hour ago
|
||||
ID-bar bar 2 hours ago About an hour ago
|
||||
@ -123,8 +123,7 @@ func runCreate(dockerCli command.Cli, flags *pflag.FlagSet, opts *serviceOptions
|
||||
|
||||
fmt.Fprintf(dockerCli.Out(), "%s\n", response.ID)
|
||||
|
||||
if opts.detach {
|
||||
warnDetachDefault(dockerCli.Err(), apiClient.ClientVersion(), flags, "created")
|
||||
if opts.detach || versions.LessThan(apiClient.ClientVersion(), "1.29") {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@ -1,15 +1,12 @@
|
||||
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"
|
||||
)
|
||||
|
||||
@ -34,11 +31,3 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,40 +0,0 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -691,7 +691,7 @@ func buildServiceDefaultFlagMapping() flagDefaults {
|
||||
}
|
||||
|
||||
func addDetachFlag(flags *pflag.FlagSet, detach *bool) {
|
||||
flags.BoolVarP(detach, flagDetach, "d", true, "Exit immediately instead of waiting for the service to converge")
|
||||
flags.BoolVarP(detach, flagDetach, "d", false, "Exit immediately instead of waiting for the service to converge")
|
||||
flags.SetAnnotation(flagDetach, "version", []string{"1.29"})
|
||||
}
|
||||
|
||||
|
||||
@ -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, cmd.Flags(), options, args[0])
|
||||
return runRollback(dockerCli, 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, flags *pflag.FlagSet, options *serviceOptions, serviceID string) error {
|
||||
func runRollback(dockerCli command.Cli, options *serviceOptions, serviceID string) error {
|
||||
apiClient := dockerCli.Client()
|
||||
ctx := context.Background()
|
||||
|
||||
@ -56,8 +56,7 @@ func runRollback(dockerCli command.Cli, flags *pflag.FlagSet, options *serviceOp
|
||||
|
||||
fmt.Fprintf(dockerCli.Out(), "%s\n", serviceID)
|
||||
|
||||
if options.detach {
|
||||
warnDetachDefault(dockerCli.Err(), apiClient.ClientVersion(), flags, "rolled back")
|
||||
if options.detach || versions.LessThan(apiClient.ClientVersion(), "1.29") {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@ -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, cmd.Flags(), options, args)
|
||||
return runScale(dockerCli, options, args)
|
||||
},
|
||||
}
|
||||
|
||||
@ -54,7 +54,7 @@ func scaleArgs(cmd *cobra.Command, args []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func runScale(dockerCli command.Cli, flags *pflag.FlagSet, options *scaleOptions, args []string) error {
|
||||
func runScale(dockerCli command.Cli, options *scaleOptions, args []string) error {
|
||||
var errs []string
|
||||
var serviceIDs []string
|
||||
ctx := context.Background()
|
||||
@ -79,9 +79,7 @@ func runScale(dockerCli command.Cli, flags *pflag.FlagSet, options *scaleOptions
|
||||
}
|
||||
|
||||
if len(serviceIDs) > 0 {
|
||||
if options.detach {
|
||||
warnDetachDefault(dockerCli.Err(), dockerCli.Client().ClientVersion(), flags, "scaled")
|
||||
} else {
|
||||
if !options.detach && versions.GreaterThanOrEqualTo(dockerCli.Client().ClientVersion(), "1.29") {
|
||||
for _, serviceID := range serviceIDs {
|
||||
if err := waitOnService(ctx, dockerCli, serviceID, false); err != nil {
|
||||
errs = append(errs, fmt.Sprintf("%s: %v", serviceID, err))
|
||||
|
||||
@ -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, repoInfo, authConfig, "pull")
|
||||
notaryRepo, err := trust.GetNotaryRepository(cli.In(), cli.Out(), command.UserAgent(), repoInfo, &authConfig, "pull")
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error establishing connection to trust repository")
|
||||
}
|
||||
|
||||
@ -216,8 +216,7 @@ func runUpdate(dockerCli command.Cli, flags *pflag.FlagSet, options *serviceOpti
|
||||
|
||||
fmt.Fprintf(dockerCli.Out(), "%s\n", serviceID)
|
||||
|
||||
if options.detach {
|
||||
warnDetachDefault(dockerCli.Err(), dockerCli.Client().ClientVersion(), flags, "updated")
|
||||
if options.detach || versions.LessThan(apiClient.ClientVersion(), "1.29") {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@ -104,13 +104,12 @@ 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{}) bool {
|
||||
func pruneServices(ctx context.Context, dockerCli command.Cli, namespace convert.Namespace, services map[string]struct{}) {
|
||||
client := dockerCli.Client()
|
||||
|
||||
oldServices, err := getServices(ctx, client, namespace.Name())
|
||||
if err != nil {
|
||||
fmt.Fprintf(dockerCli.Err(), "Failed to list services: %s", err)
|
||||
return true
|
||||
}
|
||||
|
||||
pruneServices := []swarm.Service{}
|
||||
@ -119,5 +118,5 @@ func pruneServices(ctx context.Context, dockerCli command.Cli, namespace convert
|
||||
pruneServices = append(pruneServices, service)
|
||||
}
|
||||
}
|
||||
return removeServices(ctx, dockerCli, pruneServices)
|
||||
removeServices(ctx, dockerCli, pruneServices)
|
||||
}
|
||||
|
||||
@ -59,6 +59,7 @@ 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")
|
||||
|
||||
@ -103,7 +103,7 @@ func removeServices(
|
||||
var hasError bool
|
||||
sort.Slice(services, sortServiceByName(services))
|
||||
for _, service := range services {
|
||||
fmt.Fprintf(dockerCli.Err(), "Removing service %s\n", service.Spec.Name)
|
||||
fmt.Fprintf(dockerCli.Out(), "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.Err(), "Removing network %s\n", network.Name)
|
||||
fmt.Fprintf(dockerCli.Out(), "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.Err(), "Removing secret %s\n", secret.Spec.Name)
|
||||
fmt.Fprintf(dockerCli.Out(), "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.Err(), "Removing config %s\n", config.Spec.Name)
|
||||
fmt.Fprintf(dockerCli.Out(), "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)
|
||||
|
||||
@ -101,7 +101,13 @@ func TestRemoveStackSkipEmpty(t *testing.T) {
|
||||
cmd.SetArgs([]string{"foo", "bar"})
|
||||
|
||||
assert.NoError(t, cmd.Execute())
|
||||
assert.Equal(t, "", fakeCli.OutBuffer().String())
|
||||
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.Contains(t, fakeCli.ErrBuffer().String(), "Nothing found in stack: foo\n")
|
||||
assert.Equal(t, allServiceIDs, fakeClient.removedServices)
|
||||
assert.Equal(t, allNetworkIDs, fakeClient.removedNetworks)
|
||||
|
||||
@ -15,24 +15,20 @@ 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, opts)
|
||||
return runUnlock(dockerCli)
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runUnlock(dockerCli command.Cli, opts unlockOptions) error {
|
||||
func runUnlock(dockerCli command.Cli) error {
|
||||
client := dockerCli.Client()
|
||||
ctx := context.Background()
|
||||
|
||||
|
||||
@ -15,7 +15,6 @@ 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"
|
||||
)
|
||||
@ -104,14 +103,18 @@ 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(jsonlog.RFC3339NanoFixed))
|
||||
fmt.Fprintf(out, "%s ", time.Unix(0, event.TimeNano).Format(rfc3339NanoFixed))
|
||||
} else if event.Time != 0 {
|
||||
fmt.Fprintf(out, "%s ", time.Unix(event.Time, 0).Format(jsonlog.RFC3339NanoFixed))
|
||||
fmt.Fprintf(out, "%s ", time.Unix(event.Time, 0).Format(rfc3339NanoFixed))
|
||||
}
|
||||
|
||||
fmt.Fprintf(out, "%s %s %s", event.Type, event.Action, event.Actor.ID)
|
||||
|
||||
@ -55,109 +55,50 @@ func runInfo(dockerCli *command.DockerCli, opts *infoOptions) error {
|
||||
|
||||
// nolint: gocyclo
|
||||
func prettyPrintInfo(dockerCli command.Cli, info types.Info) error {
|
||||
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)
|
||||
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)
|
||||
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])
|
||||
}
|
||||
}
|
||||
fprintfIfNotEmpty(dockerCli.Out(), "Logging Driver: %s\n", info.LoggingDriver)
|
||||
fprintfIfNotEmpty(dockerCli.Out(), "Cgroup Driver: %s\n", info.CgroupDriver)
|
||||
fprintlnNonEmpty(dockerCli.Out(), "Logging Driver:", info.LoggingDriver)
|
||||
fprintlnNonEmpty(dockerCli.Out(), "Cgroup Driver:", info.CgroupDriver)
|
||||
|
||||
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")
|
||||
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, " "))
|
||||
|
||||
if len(info.Plugins.Authorization) != 0 {
|
||||
fmt.Fprintf(dockerCli.Out(), " Authorization:")
|
||||
fmt.Fprintf(dockerCli.Out(), " %s", strings.Join(info.Plugins.Authorization, " "))
|
||||
fmt.Fprintf(dockerCli.Out(), "\n")
|
||||
fmt.Fprintln(dockerCli.Out(), " Authorization:", strings.Join(info.Plugins.Authorization, " "))
|
||||
}
|
||||
|
||||
fmt.Fprintf(dockerCli.Out(), " Log:")
|
||||
fmt.Fprintf(dockerCli.Out(), " %s", strings.Join(info.Plugins.Log, " "))
|
||||
fmt.Fprintf(dockerCli.Out(), "\n")
|
||||
fmt.Fprintln(dockerCli.Out(), " Log:", strings.Join(info.Plugins.Log, " "))
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
fmt.Fprintln(dockerCli.Out(), "Swarm:", info.Swarm.LocalNodeState)
|
||||
printSwarmInfo(dockerCli, info)
|
||||
|
||||
if len(info.Runtimes) > 0 {
|
||||
fmt.Fprintf(dockerCli.Out(), "Runtimes:")
|
||||
fmt.Fprint(dockerCli.Out(), "Runtimes:")
|
||||
for name := range info.Runtimes {
|
||||
fmt.Fprintf(dockerCli.Out(), " %s", name)
|
||||
}
|
||||
fmt.Fprint(dockerCli.Out(), "\n")
|
||||
fmt.Fprintf(dockerCli.Out(), "Default Runtime: %s\n", info.DefaultRuntime)
|
||||
fmt.Fprintln(dockerCli.Out(), "Default Runtime:", info.DefaultRuntime)
|
||||
}
|
||||
|
||||
if info.OSType == "linux" {
|
||||
fmt.Fprintf(dockerCli.Out(), "Init Binary: %v\n", info.InitBinary)
|
||||
fmt.Fprintln(dockerCli.Out(), "Init Binary:", info.InitBinary)
|
||||
|
||||
for _, ci := range []struct {
|
||||
Name string
|
||||
@ -171,23 +112,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.Fprintf(dockerCli.Out(), "\n")
|
||||
fmt.Fprint(dockerCli.Out(), "\n")
|
||||
}
|
||||
if len(info.SecurityOptions) != 0 {
|
||||
kvs, err := types.DecodeSecurityOptions(info.SecurityOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(dockerCli.Out(), "Security Options:\n")
|
||||
fmt.Fprintln(dockerCli.Out(), "Security Options:")
|
||||
for _, so := range kvs {
|
||||
fmt.Fprintf(dockerCli.Out(), " %s\n", so.Name)
|
||||
fmt.Fprintln(dockerCli.Out(), " "+so.Name)
|
||||
for _, o := range so.Options {
|
||||
switch o.Key {
|
||||
case "profile":
|
||||
if o.Value != "default" {
|
||||
fmt.Fprintf(dockerCli.Err(), " WARNING: You're not using the default seccomp profile\n")
|
||||
fmt.Fprintln(dockerCli.Err(), " WARNING: You're not using the default seccomp profile")
|
||||
}
|
||||
fmt.Fprintf(dockerCli.Out(), " Profile: %s\n", o.Value)
|
||||
fmt.Fprintln(dockerCli.Out(), " Profile:", o.Value)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -196,44 +137,44 @@ func prettyPrintInfo(dockerCli command.Cli, info types.Info) error {
|
||||
|
||||
// Isolation only has meaning on a Windows daemon.
|
||||
if info.OSType == "windows" {
|
||||
fmt.Fprintf(dockerCli.Out(), "Default Isolation: %v\n", info.Isolation)
|
||||
fmt.Fprintln(dockerCli.Out(), "Default Isolation:", info.Isolation)
|
||||
}
|
||||
|
||||
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)
|
||||
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)
|
||||
|
||||
if info.Debug {
|
||||
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)
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
fprintlnNonEmpty(dockerCli.Out(), "HTTP Proxy:", info.HTTPProxy)
|
||||
fprintlnNonEmpty(dockerCli.Out(), "HTTPS Proxy:", info.HTTPSProxy)
|
||||
fprintlnNonEmpty(dockerCli.Out(), "No Proxy:", info.NoProxy)
|
||||
|
||||
if info.IndexServerAddress != "" {
|
||||
u := dockerCli.ConfigFile().AuthConfigs[info.IndexServerAddress].Username
|
||||
if len(u) > 0 {
|
||||
fmt.Fprintf(dockerCli.Out(), "Username: %v\n", u)
|
||||
fmt.Fprintln(dockerCli.Out(), "Username:", u)
|
||||
}
|
||||
fmt.Fprintf(dockerCli.Out(), "Registry: %v\n", info.IndexServerAddress)
|
||||
fmt.Fprintln(dockerCli.Out(), "Registry:", info.IndexServerAddress)
|
||||
}
|
||||
|
||||
if info.Labels != nil {
|
||||
fmt.Fprintln(dockerCli.Out(), "Labels:")
|
||||
for _, attribute := range info.Labels {
|
||||
fmt.Fprintf(dockerCli.Out(), " %s\n", attribute)
|
||||
for _, lbl := range info.Labels {
|
||||
fmt.Fprintln(dockerCli.Out(), " "+lbl)
|
||||
}
|
||||
// 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
|
||||
@ -252,20 +193,15 @@ func prettyPrintInfo(dockerCli command.Cli, info types.Info) error {
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
fmt.Fprintln(dockerCli.Out(), "Experimental:", info.ExperimentalBuild)
|
||||
fprintlnNonEmpty(dockerCli.Out(), "Cluster Store:", info.ClusterStore)
|
||||
fprintlnNonEmpty(dockerCli.Out(), "Cluster Advertise:", 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.Fprintf(dockerCli.Out(), " %s\n", registry.Name)
|
||||
fmt.Fprintln(dockerCli.Out(), " "+registry.Name)
|
||||
}
|
||||
}
|
||||
|
||||
@ -278,11 +214,12 @@ 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.Fprintf(dockerCli.Out(), " %s\n", mirror)
|
||||
fmt.Fprintln(dockerCli.Out(), " "+mirror)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Fprintf(dockerCli.Out(), "Live Restore Enabled: %v\n\n", info.LiveRestoreEnabled)
|
||||
fmt.Fprintln(dockerCli.Out(), "Live Restore Enabled:", info.LiveRestoreEnabled)
|
||||
fmt.Fprint(dockerCli.Out(), "\n")
|
||||
|
||||
// Only output these warnings if the server does not support these features
|
||||
if info.OSType != "windows" {
|
||||
@ -326,6 +263,63 @@ 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
|
||||
@ -374,9 +368,8 @@ func formatInfo(dockerCli *command.DockerCli, info types.Info, format string) er
|
||||
return err
|
||||
}
|
||||
|
||||
func fprintfIfNotEmpty(w io.Writer, format, value string) (int, error) {
|
||||
func fprintlnNonEmpty(w io.Writer, label, value string) {
|
||||
if value != "" {
|
||||
return fmt.Fprintf(w, format, value)
|
||||
fmt.Fprintln(w, label, value)
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
413
components/cli/cli/command/trust/client_test.go
Normal file
413
components/cli/cli/command/trust/client_test.go
Normal file
@ -0,0 +1,413 @@
|
||||
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
|
||||
}
|
||||
23
components/cli/cli/command/trust/cmd.go
Normal file
23
components/cli/cli/command/trust/cmd.go
Normal file
@ -0,0 +1,23 @@
|
||||
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
|
||||
}
|
||||
33
components/cli/cli/command/trust/helpers.go
Normal file
33
components/cli/cli/command/trust/helpers.go
Normal file
@ -0,0 +1,33 @@
|
||||
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("")
|
||||
}
|
||||
125
components/cli/cli/command/trust/revoke.go
Normal file
125
components/cli/cli/command/trust/revoke.go
Normal file
@ -0,0 +1,125 @@
|
||||
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...)
|
||||
}
|
||||
148
components/cli/cli/command/trust/revoke_test.go
Normal file
148
components/cli/cli/command/trust/revoke_test.go
Normal file
@ -0,0 +1,148 @@
|
||||
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")
|
||||
}
|
||||
227
components/cli/cli/command/trust/sign.go
Normal file
227
components/cli/cli/command/trust/sign.go
Normal file
@ -0,0 +1,227 @@
|
||||
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{""})
|
||||
}
|
||||
297
components/cli/cli/command/trust/sign_test.go
Normal file
297
components/cli/cli/command/trust/sign_test.go
Normal file
@ -0,0 +1,297 @@
|
||||
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)
|
||||
}
|
||||
6
components/cli/cli/command/trust/testdata/trust-inspect-full-repo-no-signers.golden
vendored
Normal file
6
components/cli/cli/command/trust/testdata/trust-inspect-full-repo-no-signers.golden
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
SIGNED TAG DIGEST SIGNERS
|
||||
green 677265656e2d646967657374 (Repo Admin)
|
||||
|
||||
Administrative keys for signed-repo:
|
||||
Repository Key: targetsID
|
||||
Root Key: rootID
|
||||
14
components/cli/cli/command/trust/testdata/trust-inspect-full-repo-with-signers.golden
vendored
Normal file
14
components/cli/cli/command/trust/testdata/trust-inspect-full-repo-with-signers.golden
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
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
|
||||
6
components/cli/cli/command/trust/testdata/trust-inspect-one-tag-no-signers.golden
vendored
Normal file
6
components/cli/cli/command/trust/testdata/trust-inspect-one-tag-no-signers.golden
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
SIGNED TAG DIGEST SIGNERS
|
||||
green 677265656e2d646967657374 (Repo Admin)
|
||||
|
||||
Administrative keys for signed-repo:
|
||||
Repository Key: targetsID
|
||||
Root Key: rootID
|
||||
13
components/cli/cli/command/trust/testdata/trust-inspect-unsigned-tag-with-signers.golden
vendored
Normal file
13
components/cli/cli/command/trust/testdata/trust-inspect-unsigned-tag-with-signers.golden
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
|
||||
No signatures for signed-repo:unsigned
|
||||
|
||||
|
||||
List of signers and their keys for signed-repo:
|
||||
|
||||
SIGNER KEYS
|
||||
alice A
|
||||
bob B
|
||||
|
||||
Administrative keys for signed-repo:
|
||||
Repository Key: targetsID
|
||||
Root Key: rootID
|
||||
227
components/cli/cli/command/trust/view.go
Normal file
227
components/cli/cli/command/trust/view.go
Normal file
@ -0,0 +1,227 @@
|
||||
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)
|
||||
}
|
||||
433
components/cli/cli/command/trust/view_test.go
Normal file
433
components/cli/cli/command/trust/view_test.go
Normal file
@ -0,0 +1,433 @@
|
||||
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))
|
||||
}
|
||||
@ -2,6 +2,21 @@ 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
|
||||
|
||||
|
||||
@ -221,6 +221,7 @@ 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) {
|
||||
@ -563,6 +564,17 @@ 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:
|
||||
|
||||
@ -10,6 +10,7 @@ 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 {
|
||||
@ -164,17 +165,13 @@ var sampleConfig = types.Config{
|
||||
|
||||
func TestParseYAML(t *testing.T) {
|
||||
dict, err := ParseYAML([]byte(sampleYAML))
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, sampleDict, dict)
|
||||
}
|
||||
|
||||
func TestLoad(t *testing.T) {
|
||||
actual, err := Load(buildConfigDetails(sampleDict, nil))
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, serviceSort(sampleConfig.Services), serviceSort(actual.Services))
|
||||
assert.Equal(t, sampleConfig.Networks, actual.Networks)
|
||||
assert.Equal(t, sampleConfig.Volumes, actual.Volumes)
|
||||
@ -191,11 +188,9 @@ secrets:
|
||||
super:
|
||||
external: true
|
||||
`)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
assert.Equal(t, len(actual.Services), 1)
|
||||
assert.Equal(t, len(actual.Secrets), 1)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, actual.Services, 1)
|
||||
assert.Len(t, actual.Secrets, 1)
|
||||
}
|
||||
|
||||
func TestLoadV33(t *testing.T) {
|
||||
@ -211,19 +206,15 @@ configs:
|
||||
super:
|
||||
external: true
|
||||
`)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
assert.Equal(t, len(actual.Services), 1)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, actual.Services, 1)
|
||||
assert.Equal(t, actual.Services[0].CredentialSpec.File, "/foo")
|
||||
assert.Equal(t, len(actual.Configs), 1)
|
||||
require.Len(t, actual.Configs, 1)
|
||||
}
|
||||
|
||||
func TestParseAndLoad(t *testing.T) {
|
||||
actual, err := loadYAML(sampleYAML)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, serviceSort(sampleConfig.Services), serviceSort(actual.Services))
|
||||
assert.Equal(t, sampleConfig.Networks, actual.Networks)
|
||||
assert.Equal(t, sampleConfig.Volumes, actual.Volumes)
|
||||
@ -231,15 +222,15 @@ func TestParseAndLoad(t *testing.T) {
|
||||
|
||||
func TestInvalidTopLevelObjectType(t *testing.T) {
|
||||
_, err := loadYAML("1")
|
||||
assert.Error(t, err)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "Top-level object must be a mapping")
|
||||
|
||||
_, err = loadYAML("\"hello\"")
|
||||
assert.Error(t, err)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "Top-level object must be a mapping")
|
||||
|
||||
_, err = loadYAML("[\"hello\"]")
|
||||
assert.Error(t, err)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "Top-level object must be a mapping")
|
||||
}
|
||||
|
||||
@ -250,7 +241,7 @@ version: "3"
|
||||
foo:
|
||||
image: busybox
|
||||
`)
|
||||
assert.Error(t, err)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "Non-string key at top level: 123")
|
||||
|
||||
_, err = loadYAML(`
|
||||
@ -261,7 +252,7 @@ services:
|
||||
123:
|
||||
image: busybox
|
||||
`)
|
||||
assert.Error(t, err)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "Non-string key in services: 123")
|
||||
|
||||
_, err = loadYAML(`
|
||||
@ -275,7 +266,7 @@ networks:
|
||||
config:
|
||||
- 123: oh dear
|
||||
`)
|
||||
assert.Error(t, err)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "Non-string key in networks.default.ipam.config[0]: 123")
|
||||
|
||||
_, err = loadYAML(`
|
||||
@ -286,7 +277,7 @@ services:
|
||||
environment:
|
||||
1: FOO
|
||||
`)
|
||||
assert.Error(t, err)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "Non-string key in services.dict-env.environment: 1")
|
||||
}
|
||||
|
||||
@ -297,7 +288,7 @@ services:
|
||||
foo:
|
||||
image: busybox
|
||||
`)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = loadYAML(`
|
||||
version: "3.0"
|
||||
@ -305,7 +296,7 @@ services:
|
||||
foo:
|
||||
image: busybox
|
||||
`)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestUnsupportedVersion(t *testing.T) {
|
||||
@ -315,7 +306,7 @@ services:
|
||||
foo:
|
||||
image: busybox
|
||||
`)
|
||||
assert.Error(t, err)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "version")
|
||||
|
||||
_, err = loadYAML(`
|
||||
@ -324,7 +315,7 @@ services:
|
||||
foo:
|
||||
image: busybox
|
||||
`)
|
||||
assert.Error(t, err)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "version")
|
||||
}
|
||||
|
||||
@ -335,7 +326,7 @@ services:
|
||||
foo:
|
||||
image: busybox
|
||||
`)
|
||||
assert.Error(t, err)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "version must be a string")
|
||||
}
|
||||
|
||||
@ -354,7 +345,7 @@ services:
|
||||
- foo:
|
||||
image: busybox
|
||||
`)
|
||||
assert.Error(t, err)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "services must be a mapping")
|
||||
|
||||
_, err = loadYAML(`
|
||||
@ -362,7 +353,7 @@ version: "3"
|
||||
services:
|
||||
foo: busybox
|
||||
`)
|
||||
assert.Error(t, err)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "services.foo must be a mapping")
|
||||
|
||||
_, err = loadYAML(`
|
||||
@ -371,7 +362,7 @@ networks:
|
||||
- default:
|
||||
driver: bridge
|
||||
`)
|
||||
assert.Error(t, err)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "networks must be a mapping")
|
||||
|
||||
_, err = loadYAML(`
|
||||
@ -379,7 +370,7 @@ version: "3"
|
||||
networks:
|
||||
default: bridge
|
||||
`)
|
||||
assert.Error(t, err)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "networks.default must be a mapping")
|
||||
|
||||
_, err = loadYAML(`
|
||||
@ -388,7 +379,7 @@ volumes:
|
||||
- data:
|
||||
driver: local
|
||||
`)
|
||||
assert.Error(t, err)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "volumes must be a mapping")
|
||||
|
||||
_, err = loadYAML(`
|
||||
@ -396,7 +387,7 @@ version: "3"
|
||||
volumes:
|
||||
data: local
|
||||
`)
|
||||
assert.Error(t, err)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "volumes.data must be a mapping")
|
||||
}
|
||||
|
||||
@ -407,7 +398,7 @@ services:
|
||||
foo:
|
||||
image: ["busybox", "latest"]
|
||||
`)
|
||||
assert.Error(t, err)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "services.foo.image must be a string")
|
||||
}
|
||||
|
||||
@ -458,7 +449,7 @@ services:
|
||||
environment:
|
||||
FOO: ["1"]
|
||||
`)
|
||||
assert.Error(t, err)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "services.dict-env.environment.FOO must be a string, number or null")
|
||||
}
|
||||
|
||||
@ -470,7 +461,7 @@ services:
|
||||
image: busybox
|
||||
environment: "FOO=1"
|
||||
`)
|
||||
assert.Error(t, err)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "services.dict-env.environment must be a mapping")
|
||||
}
|
||||
|
||||
@ -497,7 +488,7 @@ volumes:
|
||||
"FOO": "foo",
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
expectedLabels := types.Labels{
|
||||
"home1": home,
|
||||
@ -517,24 +508,46 @@ version: "3"
|
||||
services:
|
||||
web:
|
||||
image: web
|
||||
build: ./web
|
||||
build:
|
||||
context: ./web
|
||||
links:
|
||||
- bar
|
||||
db:
|
||||
image: db
|
||||
build: ./db
|
||||
build:
|
||||
context: ./db
|
||||
`))
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
configDetails := buildConfigDetails(dict, nil)
|
||||
|
||||
_, err = Load(configDetails)
|
||||
assert.NoError(t, err)
|
||||
require.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"
|
||||
@ -547,15 +560,15 @@ services:
|
||||
container_name: db
|
||||
expose: ["5434"]
|
||||
`))
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
configDetails := buildConfigDetails(dict, nil)
|
||||
|
||||
_, err = Load(configDetails)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
deprecated := GetDeprecatedProperties(configDetails)
|
||||
assert.Equal(t, 2, len(deprecated))
|
||||
assert.Len(t, deprecated, 2)
|
||||
assert.Contains(t, deprecated, "container_name")
|
||||
assert.Contains(t, deprecated, "expose")
|
||||
}
|
||||
@ -574,12 +587,12 @@ services:
|
||||
service: foo
|
||||
`)
|
||||
|
||||
assert.Error(t, err)
|
||||
require.Error(t, err)
|
||||
assert.IsType(t, &ForbiddenPropertiesError{}, err)
|
||||
fmt.Println(err)
|
||||
forbidden := err.(*ForbiddenPropertiesError).Properties
|
||||
|
||||
assert.Equal(t, 2, len(forbidden))
|
||||
assert.Len(t, forbidden, 2)
|
||||
assert.Contains(t, forbidden, "volume_driver")
|
||||
assert.Contains(t, forbidden, "extends")
|
||||
}
|
||||
@ -595,7 +608,7 @@ func TestInvalidResource(t *testing.T) {
|
||||
impossible:
|
||||
x: 1
|
||||
`)
|
||||
assert.Error(t, err)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "Additional property impossible is not allowed")
|
||||
}
|
||||
|
||||
@ -608,7 +621,7 @@ volumes:
|
||||
driver: foobar
|
||||
`)
|
||||
|
||||
assert.Error(t, err)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "conflicting parameters \"external\" and \"driver\" specified for volume")
|
||||
assert.Contains(t, err.Error(), "external_volume")
|
||||
}
|
||||
@ -623,7 +636,7 @@ volumes:
|
||||
beep: boop
|
||||
`)
|
||||
|
||||
assert.Error(t, err)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "conflicting parameters \"external\" and \"driver_opts\" specified for volume")
|
||||
assert.Contains(t, err.Error(), "external_volume")
|
||||
}
|
||||
@ -638,7 +651,7 @@ volumes:
|
||||
- beep=boop
|
||||
`)
|
||||
|
||||
assert.Error(t, err)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "conflicting parameters \"external\" and \"labels\" specified for volume")
|
||||
assert.Contains(t, err.Error(), "external_volume")
|
||||
}
|
||||
@ -653,8 +666,7 @@ volumes:
|
||||
name: external_name
|
||||
`)
|
||||
|
||||
assert.Error(t, err)
|
||||
fmt.Println(err)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "volume.external.name and volume.name conflict; only use volume.name")
|
||||
assert.Contains(t, err.Error(), "external_volume")
|
||||
}
|
||||
@ -669,23 +681,30 @@ func uint64Ptr(value uint64) *uint64 {
|
||||
|
||||
func TestFullExample(t *testing.T) {
|
||||
bytes, err := ioutil.ReadFile("full-example.yml")
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
homeDir := "/home/foo"
|
||||
env := map[string]string{"HOME": homeDir, "QUX": "qux_from_environment"}
|
||||
config, err := loadYAMLWithEnv(string(bytes), env)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
workingDir, err := os.Getwd()
|
||||
assert.NoError(t, err)
|
||||
require.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",
|
||||
@ -1069,9 +1088,7 @@ networks:
|
||||
mynet2:
|
||||
driver: bridge
|
||||
`)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := map[string]types.NetworkConfig{
|
||||
"mynet1": {
|
||||
@ -1105,9 +1122,7 @@ services:
|
||||
target: 22
|
||||
published: 10022
|
||||
`)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := []types.ServicePortConfig{
|
||||
{
|
||||
@ -1170,7 +1185,7 @@ services:
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, 1, len(config.Services))
|
||||
assert.Len(t, config.Services, 1)
|
||||
assert.Equal(t, expected, config.Services[0].Ports)
|
||||
}
|
||||
|
||||
@ -1188,9 +1203,7 @@ services:
|
||||
volumes:
|
||||
foo: {}
|
||||
`)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := types.ServiceVolumeConfig{
|
||||
Type: "volume",
|
||||
@ -1199,7 +1212,7 @@ volumes:
|
||||
ReadOnly: true,
|
||||
}
|
||||
|
||||
assert.Equal(t, 1, len(config.Services))
|
||||
assert.Equal(t, 1, len(config.Services[0].Volumes))
|
||||
require.Len(t, config.Services, 1)
|
||||
assert.Len(t, config.Services[0].Volumes, 1)
|
||||
assert.Equal(t, expected, config.Services[0].Volumes[0])
|
||||
}
|
||||
|
||||
@ -79,6 +79,7 @@ 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"`
|
||||
@ -126,6 +127,18 @@ 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
|
||||
|
||||
|
||||
@ -7,13 +7,15 @@ import (
|
||||
// DetectDefaultStore return the default credentials store for the platform if
|
||||
// the store executable is available.
|
||||
func DetectDefaultStore(store string) string {
|
||||
platformDefault := defaultCredentialsStore()
|
||||
|
||||
// user defined or no default for platform
|
||||
if store != "" || defaultCredentialsStore == "" {
|
||||
if store != "" || platformDefault == "" {
|
||||
return store
|
||||
}
|
||||
|
||||
if _, err := exec.LookPath(remoteCredentialsPrefix + defaultCredentialsStore); err == nil {
|
||||
return defaultCredentialsStore
|
||||
if _, err := exec.LookPath(remoteCredentialsPrefix + platformDefault); err == nil {
|
||||
return platformDefault
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user