Compare commits
458 Commits
v17.11.0-c
...
v17.12.0-c
| Author | SHA1 | Date | |
|---|---|---|---|
| f9cde631a2 | |||
| 264ab31364 | |||
| 5d7e8f3778 | |||
| 74e076ab06 | |||
| 184c27d785 | |||
| 33d771ed94 | |||
| 50e759c017 | |||
| bde1eb8281 | |||
| fb8384a555 | |||
| 86127edfd5 | |||
| d2b3575a03 | |||
| d256539bf4 | |||
| fbfecebc0a | |||
| f73a0ff35c | |||
| 5f5c42593a | |||
| fcde1eb2b7 | |||
| b52085fc2e | |||
| ee2f9437b6 | |||
| 8e51e59c8a | |||
| a4b9862912 | |||
| 718cc1a071 | |||
| 704b884e52 | |||
| 521d1c4f0b | |||
| 9b5e4afbf7 | |||
| c6d5cb9ce0 | |||
| d7a82cda3b | |||
| b8234e6bfc | |||
| aa9a4c4bff | |||
| 29d0c2b494 | |||
| 96784302eb | |||
| 6046d5cbe8 | |||
| c3ea8d4b44 | |||
| 499d1359a4 | |||
| 00a55a319d | |||
| 947ed56f66 | |||
| 3916dafe17 | |||
| e8e8613267 | |||
| 9d2c3018e2 | |||
| 7661258841 | |||
| dced91d6f0 | |||
| 02ebcae82d | |||
| da56fc4967 | |||
| 8afbe15ccf | |||
| 4d38282082 | |||
| f0f2c2b953 | |||
| 31d8edcd80 | |||
| a505f09c7f | |||
| c9781de504 | |||
| 0aafcdcf36 | |||
| 8f42e63aa7 | |||
| 3ef7b79d93 | |||
| 7b947af7df | |||
| 8f8bda1238 | |||
| 378c0c6887 | |||
| 4a137d3ed6 | |||
| d02e9392a1 | |||
| 5deca4bbe1 | |||
| c898dfbd95 | |||
| 928454da48 | |||
| 41a5926818 | |||
| 1ffad27f0c | |||
| 2990f42efe | |||
| 8d97955cea | |||
| 0ec86524ed | |||
| 70c95e6ecb | |||
| e96b772de5 | |||
| 52db2af288 | |||
| 8024cf3175 | |||
| dfe161a946 | |||
| cf5224adb2 | |||
| 8ca0b6eb11 | |||
| 9ece0a8dbe | |||
| a394769cf9 | |||
| a369ceccde | |||
| 4d1f5d1e42 | |||
| abf2603ea4 | |||
| d27e345310 | |||
| e1f6e66d66 | |||
| 12e67ce519 | |||
| a5a325a05f | |||
| dfd519caff | |||
| 0c8a47d019 | |||
| 327b80ad82 | |||
| e4dde67875 | |||
| 403fcf5047 | |||
| 8a469be8e0 | |||
| cf1d97228e | |||
| 6ada520445 | |||
| 57c8d64422 | |||
| 2ee063b98e | |||
| 7287b1b196 | |||
| f28070bdef | |||
| a9ea77004e | |||
| 8d18b0cfab | |||
| d76f409f13 | |||
| f70de4cf86 | |||
| 8f588eed28 | |||
| 8b1197a568 | |||
| 08c25433ac | |||
| cbec60ecae | |||
| d47afe4a42 | |||
| 14e1e40a4e | |||
| c4fbbf057e | |||
| 47fdbea6d3 | |||
| bf78150b9b | |||
| 31daed866f | |||
| b2d74e0a1a | |||
| 395b31b896 | |||
| 7534a636c1 | |||
| 469adac6a0 | |||
| 9b80ff5665 | |||
| 5016fee3dc | |||
| 9a7838eb22 | |||
| 29875cbb85 | |||
| f1232b72bc | |||
| 3cd9275696 | |||
| b63d8b8465 | |||
| e57d9082bb | |||
| 5b5e980ec3 | |||
| 20a2865e53 | |||
| f62beecbb7 | |||
| 148bfd3114 | |||
| 6a8cf843af | |||
| 559fcde9d4 | |||
| 5f26edda74 | |||
| b611814dc6 | |||
| f67476ad43 | |||
| b8226cb988 | |||
| 19fca6eb66 | |||
| 553ad48935 | |||
| a8cf119f39 | |||
| 4d4ec05575 | |||
| a620b9f6fd | |||
| 4a8c6447a6 | |||
| 87f648cd90 | |||
| 9d718a77b7 | |||
| d99ad61b69 | |||
| ad76847e4b | |||
| c4cb5e12ad | |||
| 9818aadf77 | |||
| 617fb8ceb7 | |||
| 3c7ba9bf1b | |||
| a88b24bfeb | |||
| 1072fa08f7 | |||
| d9f34cfcb3 | |||
| 3e4063cddb | |||
| 006ed302fe | |||
| 02055337d8 | |||
| ea17d4a2f5 | |||
| f8c7f6621d | |||
| b1e7ee7a82 | |||
| 7e9b53fd42 | |||
| 9b7953e812 | |||
| a79583a886 | |||
| fe41a825e2 | |||
| 264f9d5a4f | |||
| 40b8aa0152 | |||
| 29f2890225 | |||
| cbde088d23 | |||
| 1433c3b1eb | |||
| 5a1411dbf0 | |||
| 1901e3d893 | |||
| 856222b133 | |||
| bac9dc2670 | |||
| 87cfb875f7 | |||
| a0c42a8dee | |||
| 7f2221039e | |||
| 6be246bf8b | |||
| 67203bb69e | |||
| 99c53b25ef | |||
| 6d8eeeb431 | |||
| 0569c43fe3 | |||
| 877f5d0f1f | |||
| 608a03b9d5 | |||
| 85f5db8154 | |||
| efd7b774aa | |||
| b7dabb480c | |||
| 2598a6cd6a | |||
| 6025db4712 | |||
| 00e6357d85 | |||
| 405736f2fc | |||
| 77a39cce80 | |||
| 025793bb76 | |||
| 4ba8859964 | |||
| 2eb0bb6fa6 | |||
| 6c65651aa2 | |||
| f62d716bb0 | |||
| a94e4ed04a | |||
| 0637acf0b1 | |||
| 94ae8f10c7 | |||
| 016b82fbf3 | |||
| 2ddb9ceeba | |||
| bc89af9929 | |||
| ccb264e334 | |||
| dcbc5aef35 | |||
| 44cd9b0708 | |||
| 03d850d672 | |||
| 5c33ebfbc1 | |||
| 9a7fb33c58 | |||
| c3f989d26b | |||
| 249ba5d150 | |||
| 6b9bdea9e7 | |||
| 59e5477bf3 | |||
| 52ebdfbe5d | |||
| c7f0d04c5e | |||
| b02966efb1 | |||
| a737bf6704 | |||
| 8a8ecdb59d | |||
| c34d673f0c | |||
| 12aa8ed3d8 | |||
| dd75028635 | |||
| 7830091c4b | |||
| cb2dc1cb0b | |||
| b88ff8a5c2 | |||
| db4f5614fc | |||
| 3ceb534c1b | |||
| 9f84ce63a5 | |||
| 34cc3d6434 | |||
| 3d2eab6bf0 | |||
| 2dae51fa3d | |||
| 26b47c13d1 | |||
| 4a5f32be3b | |||
| e6fc3af770 | |||
| 86481c358f | |||
| 605885e518 | |||
| 7ed4a89d21 | |||
| 6a9797ecaa | |||
| b26b223eb7 | |||
| c9b8719b0b | |||
| a653d42fdb | |||
| 6063e62a1f | |||
| d0d7235731 | |||
| 9282ca8404 | |||
| d4007aa747 | |||
| 4e6f892dc4 | |||
| c051f4a135 | |||
| 6e7cb1931d | |||
| 69bca6d97b | |||
| 63aefcb844 | |||
| 3cf67e0ba5 | |||
| 325141c0ef | |||
| 276378cfb9 | |||
| 408fe754a6 | |||
| 5289d7ae1c | |||
| 1da9654cd5 | |||
| 454988ca17 | |||
| c23d72ffe9 | |||
| 38076c34e8 | |||
| be9497fbf4 | |||
| 23f0e9211c | |||
| e533bd8d7a | |||
| 5ddac3b8e8 | |||
| efb2ed5d5e | |||
| afb12468a8 | |||
| 0acfb79b17 | |||
| f9712fa620 | |||
| c5471a4a0e | |||
| 7e12c15274 | |||
| 94aae8ab65 | |||
| 6937a74b64 | |||
| 3be11a0388 | |||
| 83579492a6 | |||
| 0db60dff81 | |||
| a1c54edb95 | |||
| 884d22e3a4 | |||
| 42bb16a0de | |||
| 4d629f82fb | |||
| f78101e94a | |||
| e26667240b | |||
| ac513c6164 | |||
| 9ef2726179 | |||
| 8771152e42 | |||
| cffc0c967c | |||
| 55c5f024c9 | |||
| 3647d18f55 | |||
| ce594a83b1 | |||
| ae3d074947 | |||
| 005014677c | |||
| 6ad432f442 | |||
| 8b37566333 | |||
| cf38fd827e | |||
| 68402034cc | |||
| e9b472f2e1 | |||
| 4bd3446b2b | |||
| d8cda430a7 | |||
| 15db98f6fa | |||
| 2cc0ade7cc | |||
| 6f6f835501 | |||
| 523ff46adb | |||
| 7b0db0e401 | |||
| 31c1f92623 | |||
| ca97ddf678 | |||
| 146e3d30b9 | |||
| ce5c429be8 | |||
| 2ca8cee39b | |||
| 10c7697134 | |||
| 60d1b81b0d | |||
| 66527d3a87 | |||
| 293c21ca60 | |||
| 6a81f7db2e | |||
| 2ee46bd08b | |||
| 628966a8d4 | |||
| e18edc6a6b | |||
| ca631a390a | |||
| f9f5db27c1 | |||
| 8369aeca8e | |||
| aa1c9fab4f | |||
| 6729c4835f | |||
| 7cf0bd287e | |||
| f5ae1accb6 | |||
| 5aa9076d56 | |||
| 50743f5357 | |||
| a1783b999b | |||
| 9379e66070 | |||
| 75f89d26b4 | |||
| a754767196 | |||
| d77f572b3a | |||
| d94b0d4775 | |||
| dcbe5b1182 | |||
| 98da414257 | |||
| 32672a8e2b | |||
| 8d5384d97c | |||
| 366ab9966c | |||
| 2f7df6c0dd | |||
| 7918ee610f | |||
| 84fe2b7d9a | |||
| ca892e4f1a | |||
| 2bb50cbf78 | |||
| c716c066e5 | |||
| bcc50083eb | |||
| 61e5c2b6ed | |||
| e0e98f749d | |||
| 6dd3e92796 | |||
| e52bfcca39 | |||
| 8a571d7e05 | |||
| 3e8332108b | |||
| 1fce7e41ce | |||
| ce00560e17 | |||
| 49cfe581ba | |||
| 96ed9e5d58 | |||
| 95cc30e089 | |||
| dfd681c198 | |||
| edb2c3c5b1 | |||
| 22a9ee4f49 | |||
| dbf5fa6264 | |||
| c18684ecbe | |||
| 755085f5c8 | |||
| 19146bf34e | |||
| e49f1e9075 | |||
| 72aededa94 | |||
| 22f416c376 | |||
| ce8d7a3b24 | |||
| 9302e76682 | |||
| 7cfb40e0bf | |||
| bab1ef9f35 | |||
| 127d797434 | |||
| aee7705a5f | |||
| e5124fd7ef | |||
| ae808b0eb3 | |||
| f0b2e74732 | |||
| 59e0e866f3 | |||
| 63e16f39ad | |||
| dff2ac1e17 | |||
| 70ca1da8d3 | |||
| 18aef1e39d | |||
| b9b2e53e40 | |||
| cffa465570 | |||
| 6edab5bbbd | |||
| 61e2a38de5 | |||
| 84f5991ee5 | |||
| 5de35938b7 | |||
| ab9214e3c9 | |||
| 7264e2410c | |||
| fc04cb890d | |||
| 593039ced7 | |||
| ca401e539a | |||
| 4aac83aaac | |||
| 73e10347de | |||
| d055b6f5d3 | |||
| fc26552dd2 | |||
| 5ead3bed6b | |||
| e3495e0554 | |||
| d2b0c1cba5 | |||
| 4a715bb927 | |||
| 304c69a1ec | |||
| 9136cbe399 | |||
| cbad1c68f7 | |||
| 0cd754e14e | |||
| 633f82e30c | |||
| fd4c48b1d3 | |||
| 9f6163c9a9 | |||
| 3e0fab4d2e | |||
| c2ca3d9c02 | |||
| b878f3dd1f | |||
| 564091fdbb | |||
| eefbd135ae | |||
| 7559d4940b | |||
| 4380d9b1d1 | |||
| bd820b2e31 | |||
| a1f2fd42a7 | |||
| 45def429d3 | |||
| 9a9cb24385 | |||
| a60239224c | |||
| b9e1e46d81 | |||
| 89d76e74a5 | |||
| 242504f367 | |||
| 8146500811 | |||
| d488311b88 | |||
| 4d81559763 | |||
| 0faeccd9ce | |||
| 86be218c45 | |||
| a92483bbdf | |||
| 0bcfa85080 | |||
| fb29de3583 | |||
| 2f5eef6e9a | |||
| 0954691f2f | |||
| 7bfb66c670 | |||
| 7afaa41128 | |||
| 31ecf1d5a7 | |||
| 8126d722b0 | |||
| dbab169d62 | |||
| 9e1b28e972 | |||
| 5e3a8a19fe | |||
| cf57f4873b | |||
| f9bbb79bc5 | |||
| 7fe5d41e3a | |||
| 77f73751db | |||
| b1394ae4a5 | |||
| d76235b88c | |||
| 3c035adcf7 | |||
| bf8eb18312 | |||
| 95c878aac5 | |||
| b5133b1bd8 | |||
| 06a81bf15d | |||
| 6b11145c27 | |||
| 356175fe57 | |||
| 379ed62f4b | |||
| 70aedb437f | |||
| f64b1b1e67 | |||
| f85a09c5e7 | |||
| f817bbeeb6 | |||
| 57801c5154 | |||
| b617f5a57f | |||
| b96759ddb9 | |||
| 74cd535226 | |||
| 0f02c6fd5a | |||
| 1377e81acb | |||
| 52f9a6e75e | |||
| 1c3f4cf12c | |||
| e18e9addc6 | |||
| 24d0fa40df | |||
| a2e7813d7e | |||
| e06cccc917 | |||
| a34160d786 | |||
| 7cd305fd99 | |||
| d069c92fec | |||
| 5103c6279f | |||
| 87c0724076 |
87
CHANGELOG.md
87
CHANGELOG.md
@ -1 +1,88 @@
|
||||
# Changelog
|
||||
|
||||
Items starting with `DEPRECATE` are important deprecation notices. For more
|
||||
information on the list of deprecated flags and APIs please have a look at
|
||||
https://docs.docker.com/engine/deprecated/ where target removal dates can also
|
||||
be found.
|
||||
|
||||
## 17.12.0-ce (2017-12-DD)
|
||||
|
||||
|
||||
### Builder
|
||||
|
||||
- Fix build cache hash for broken symlink [moby/moby#34271](https://github.com/moby/moby/pull/34271)
|
||||
- Fix long stream sync [moby/moby#35404](https://github.com/moby/moby/pull/35404)
|
||||
- Fix dockerfile parser failing silently on long tokens [moby/moby#35429](https://github.com/moby/moby/pull/35429)
|
||||
|
||||
### Client
|
||||
|
||||
* Remove secret/config duplication in cli/compose [docker/cli#671](https://github.com/docker/cli/pull/671)
|
||||
* Add `--local` flag to `docker trust sign` [docker/cli#575](https://github.com/docker/cli/pull/575)
|
||||
* Add `docker trust inspect` [docker/cli#694](https://github.com/docker/cli/pull/694)
|
||||
+ Add `name` field to secrets and configs to allow interpolation in Compose files [docker/cli#668](https://github.com/docker/cli/pull/668)
|
||||
+ Add `--isolation` for setting swarm service isolation mode [docker/cli#426](https://github.com/docker/cli/pull/426)
|
||||
* Remove deprecated "daemon" subcommand [docker/cli#689](https://github.com/docker/cli/pull/689)
|
||||
- Fix behaviour of `rmi -f` with unexpected errors [docker/cli#654](https://github.com/docker/cli/pull/654)
|
||||
* Integrated Generic resource in service create [docker/cli#429](https://github.com/docker/cli/pull/429)
|
||||
|
||||
### Documentation
|
||||
|
||||
* Update API version history for 1.35 [moby/moby#35724](https://github.com/moby/moby/pull/35724)
|
||||
|
||||
### Logging
|
||||
|
||||
* Logentries driver line-only=true []byte output fix [moby/moby#35612](https://github.com/moby/moby/pull/35612)
|
||||
* Logentries line-only logopt fix to maintain backwards compatibility [moby/moby#35628](https://github.com/moby/moby/pull/35628)
|
||||
+ Add `--until` flag for docker logs [moby/moby#32914](https://github.com/moby/moby/pull/32914)
|
||||
+ Add gelf log driver plugin to Windows build [moby/moby#35073](https://github.com/moby/moby/pull/35073)
|
||||
* Set timeout on splunk batch send [moby/moby#35496](https://github.com/moby/moby/pull/35496)
|
||||
|
||||
### Networking
|
||||
|
||||
* Move load balancer sandbox creation/deletion into libnetwork [moby/moby#35422](https://github.com/moby/moby/pull/35422)
|
||||
* Only chown network files within container metadata [moby/moby#34224](https://github.com/moby/moby/pull/34224)
|
||||
* Restore error type in FindNetwork [moby/moby#35634](https://github.com/moby/moby/pull/35634)
|
||||
- Fix consumes MIME type for NetworkConnect [moby/moby#35542](https://github.com/moby/moby/pull/35542)
|
||||
+ Added support for persisting Windows network driver specific options [moby/moby#35563](https://github.com/moby/moby/pull/35563)
|
||||
- Fix timeout on netlink sockets and watchmiss leak [moby/moby#35677](https://github.com/moby/moby/pull/35677)
|
||||
+ New daemon config for networking diagnosis [moby/moby#35677](https://github.com/moby/moby/pull/35677)
|
||||
|
||||
### Runtime
|
||||
|
||||
* Update to containerd v1.0.0 [moby/moby#35707](https://github.com/moby/moby/pull/35707)
|
||||
* Have VFS graphdriver use accelerated in-kernel copy [moby/moby#35537](https://github.com/moby/moby/pull/35537)
|
||||
* Introduce `workingdir` option for docker exec [moby/moby#35661](https://github.com/moby/moby/pull/35661)
|
||||
* Bump Go to 1.9.2 [moby/moby#33892](https://github.com/moby/moby/pull/33892) [docker/cli#716](https://github.com/docker/cli/pull/716)
|
||||
* `/dev` should not be readonly with `--readonly` flag [moby/moby#35344](https://github.com/moby/moby/pull/35344)
|
||||
+ Add custom build-time Graphdrivers priority list [moby/moby#35522](https://github.com/moby/moby/pull/35522)
|
||||
* LCOW: CLI changes to add platform flag - pull, run, create and build [docker/cli#474](https://github.com/docker/cli/pull/474)
|
||||
* Fix width/height on Windoes for `docker exec` [moby/moby#35631](https://github.com/moby/moby/pull/35631)
|
||||
* Detect overlay2 support on pre-4.0 kernels [moby/moby#35527](https://github.com/moby/moby/pull/35527)
|
||||
* Devicemapper: remove container rootfs mountPath after umount [moby/moby#34573](https://github.com/moby/moby/pull/34573)
|
||||
* Disallow overlay/overlay2 on top of NFS [moby/moby#35483](https://github.com/moby/moby/pull/35483)
|
||||
- Fix potential panic during plugin set. [moby/moby#35632](https://github.com/moby/moby/pull/35632)
|
||||
- Fix some issues with locking on the container [moby/moby#35501](https://github.com/moby/moby/pull/35501)
|
||||
- Fixup some issues with plugin refcounting [moby/moby#35265](https://github.com/moby/moby/pull/35265)
|
||||
+ Add missing lock in ProcessEvent [moby/moby#35516](https://github.com/moby/moby/pull/35516)
|
||||
+ Add vfs quota support [moby/moby#35231](https://github.com/moby/moby/pull/35231)
|
||||
* Skip empty directories on prior graphdriver detection [moby/moby#35528](https://github.com/moby/moby/pull/35528)
|
||||
* Skip xfs quota tests when running in user namespace [moby/moby#35526](https://github.com/moby/moby/pull/35526)
|
||||
+ Added SubSecondPrecision to config option. [moby/moby#35529](https://github.com/moby/moby/pull/35529)
|
||||
* Update fsnotify to fix deadlock in removing watch [moby/moby#35453](https://github.com/moby/moby/pull/35453)
|
||||
- Fix "duplicate mount point" when `--tmpfs /dev/shm` is used [moby/moby#35467](https://github.com/moby/moby/pull/35467)
|
||||
- Fix honoring tmpfs-size for user `/dev/shm` mount [moby/moby#35316](https://github.com/moby/moby/pull/35316)
|
||||
- Fix EBUSY errors under overlayfs and v4.13+ kernels [moby/moby#34948](https://github.com/moby/moby/pull/34948)
|
||||
* Container: protect health monitor channel [moby/moby#35482](https://github.com/moby/moby/pull/35482)
|
||||
* Container: protect the health status with mutex [moby/moby#35517](https://github.com/moby/moby/pull/35517)
|
||||
* Container: update real-time resources [moby/moby#33731](https://github.com/moby/moby/pull/33731)
|
||||
* Create labels when volume exists only remotely [moby/moby#34896](https://github.com/moby/moby/pull/34896)
|
||||
* Fix leaking container/exec state [moby/moby#35484](https://github.com/moby/moby/pull/35484)
|
||||
|
||||
### Swarm Mode
|
||||
|
||||
+ Added support for swarm service isolation mode [moby/moby#34424](https://github.com/moby/moby/pull/34424)
|
||||
|
||||
### Packaging
|
||||
|
||||
+ Added Packaging for Fedora 27 [docker/docker-ce-packaging#59](https://github.com/docker/docker-ce-packaging/pull/59)
|
||||
* Changed default versioning scheme to 0.0.0-dev unless specified for packaging [docker/docker-ce-packaging#67](https://github.com/docker/docker-ce-packaging/pull/67)
|
||||
|
||||
2
components/cli/.github/CODEOWNERS
vendored
2
components/cli/.github/CODEOWNERS
vendored
@ -5,5 +5,5 @@ cli/command/stack/** @dnephin @vdemeester
|
||||
cli/compose/** @dnephin @vdemeester
|
||||
contrib/completion/bash/** @albers
|
||||
contrib/completion/zsh/** @sdurrheimer
|
||||
docs/** @mstanleyjones @vdemeester @thaJeztah
|
||||
docs/** @mistyhacks @vdemeester @thaJeztah
|
||||
scripts/** @dnephin
|
||||
|
||||
@ -133,7 +133,7 @@
|
||||
[people.misty]
|
||||
Name = "Misty Stanley-Jones"
|
||||
Email = "misty@docker.com"
|
||||
GitHub = "mstanleyjones"
|
||||
GitHub = "mistyhacks"
|
||||
|
||||
[people.mlaventure]
|
||||
Name = "Kenfe-Mickaël Laventure"
|
||||
|
||||
@ -1 +1 @@
|
||||
17.11.0-dev
|
||||
17.12.0-ce-rc2
|
||||
|
||||
@ -12,12 +12,15 @@ import (
|
||||
|
||||
type fakeClient struct {
|
||||
client.Client
|
||||
inspectFunc func(string) (types.ContainerJSON, error)
|
||||
execInspectFunc func(execID string) (types.ContainerExecInspect, error)
|
||||
execCreateFunc func(container string, config types.ExecConfig) (types.IDResponse, error)
|
||||
createContainerFunc func(config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, containerName string) (container.ContainerCreateCreatedBody, error)
|
||||
imageCreateFunc func(parentReference string, options types.ImageCreateOptions) (io.ReadCloser, error)
|
||||
infoFunc func() (types.Info, error)
|
||||
inspectFunc func(string) (types.ContainerJSON, error)
|
||||
execInspectFunc func(execID string) (types.ContainerExecInspect, error)
|
||||
execCreateFunc func(container string, config types.ExecConfig) (types.IDResponse, error)
|
||||
createContainerFunc func(config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, containerName string) (container.ContainerCreateCreatedBody, error)
|
||||
imageCreateFunc func(parentReference string, options types.ImageCreateOptions) (io.ReadCloser, error)
|
||||
infoFunc func() (types.Info, error)
|
||||
containerStatPathFunc func(container, path string) (types.ContainerPathStat, error)
|
||||
containerCopyFromFunc func(container, srcPath string) (io.ReadCloser, types.ContainerPathStat, error)
|
||||
logFunc func(string, types.ContainerLogsOptions) (io.ReadCloser, error)
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerInspect(_ context.Context, containerID string) (types.ContainerJSON, error) {
|
||||
@ -71,3 +74,24 @@ func (f *fakeClient) Info(_ context.Context) (types.Info, error) {
|
||||
}
|
||||
return types.Info{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerStatPath(_ context.Context, container, path string) (types.ContainerPathStat, error) {
|
||||
if f.containerStatPathFunc != nil {
|
||||
return f.containerStatPathFunc(container, path)
|
||||
}
|
||||
return types.ContainerPathStat{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) CopyFromContainer(_ context.Context, container, srcPath string) (io.ReadCloser, types.ContainerPathStat, error) {
|
||||
if f.containerCopyFromFunc != nil {
|
||||
return f.containerCopyFromFunc(container, srcPath)
|
||||
}
|
||||
return nil, types.ContainerPathStat{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerLogs(_ context.Context, container string, options types.ContainerLogsOptions) (io.ReadCloser, error) {
|
||||
if f.logFunc != nil {
|
||||
return f.logFunc(container, options)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@ -26,13 +26,17 @@ type copyOptions struct {
|
||||
type copyDirection int
|
||||
|
||||
const (
|
||||
fromContainer copyDirection = (1 << iota)
|
||||
fromContainer copyDirection = 1 << iota
|
||||
toContainer
|
||||
acrossContainers = fromContainer | toContainer
|
||||
)
|
||||
|
||||
type cpConfig struct {
|
||||
followLink bool
|
||||
copyUIDGID bool
|
||||
sourcePath string
|
||||
destPath string
|
||||
container string
|
||||
}
|
||||
|
||||
// NewCopyCommand creates a new `docker cp` command
|
||||
@ -65,58 +69,57 @@ func NewCopyCommand(dockerCli command.Cli) *cobra.Command {
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
|
||||
flags.BoolVarP(&opts.followLink, "follow-link", "L", false, "Always follow symbol link in SRC_PATH")
|
||||
flags.BoolVarP(&opts.copyUIDGID, "archive", "a", false, "Archive mode (copy all uid/gid information)")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runCopy(dockerCli command.Cli, opts copyOptions) error {
|
||||
srcContainer, srcPath := splitCpArg(opts.source)
|
||||
dstContainer, dstPath := splitCpArg(opts.destination)
|
||||
destContainer, destPath := splitCpArg(opts.destination)
|
||||
|
||||
copyConfig := cpConfig{
|
||||
followLink: opts.followLink,
|
||||
copyUIDGID: opts.copyUIDGID,
|
||||
sourcePath: srcPath,
|
||||
destPath: destPath,
|
||||
}
|
||||
|
||||
var direction copyDirection
|
||||
if srcContainer != "" {
|
||||
direction |= fromContainer
|
||||
copyConfig.container = srcContainer
|
||||
}
|
||||
if dstContainer != "" {
|
||||
if destContainer != "" {
|
||||
direction |= toContainer
|
||||
}
|
||||
|
||||
cpParam := &cpConfig{
|
||||
followLink: opts.followLink,
|
||||
copyConfig.container = destContainer
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
switch direction {
|
||||
case fromContainer:
|
||||
return copyFromContainer(ctx, dockerCli, srcContainer, srcPath, dstPath, cpParam)
|
||||
return copyFromContainer(ctx, dockerCli, copyConfig)
|
||||
case toContainer:
|
||||
return copyToContainer(ctx, dockerCli, srcPath, dstContainer, dstPath, cpParam, opts.copyUIDGID)
|
||||
return copyToContainer(ctx, dockerCli, copyConfig)
|
||||
case acrossContainers:
|
||||
// Copying between containers isn't supported.
|
||||
return errors.New("copying between containers is not supported")
|
||||
default:
|
||||
// User didn't specify any container.
|
||||
return errors.New("must specify at least one container source")
|
||||
}
|
||||
}
|
||||
|
||||
func statContainerPath(ctx context.Context, dockerCli command.Cli, containerName, path string) (types.ContainerPathStat, error) {
|
||||
return dockerCli.Client().ContainerStatPath(ctx, containerName, path)
|
||||
}
|
||||
|
||||
func resolveLocalPath(localPath string) (absPath string, err error) {
|
||||
if absPath, err = filepath.Abs(localPath); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return archive.PreserveTrailingDotOrSeparator(absPath, localPath, filepath.Separator), nil
|
||||
}
|
||||
|
||||
func copyFromContainer(ctx context.Context, dockerCli command.Cli, srcContainer, srcPath, dstPath string, cpParam *cpConfig) (err error) {
|
||||
func copyFromContainer(ctx context.Context, dockerCli command.Cli, copyConfig cpConfig) (err error) {
|
||||
dstPath := copyConfig.destPath
|
||||
srcPath := copyConfig.sourcePath
|
||||
|
||||
if dstPath != "-" {
|
||||
// Get an absolute destination path.
|
||||
dstPath, err = resolveLocalPath(dstPath)
|
||||
@ -125,10 +128,11 @@ func copyFromContainer(ctx context.Context, dockerCli command.Cli, srcContainer,
|
||||
}
|
||||
}
|
||||
|
||||
client := dockerCli.Client()
|
||||
// if client requests to follow symbol link, then must decide target file to be copied
|
||||
var rebaseName string
|
||||
if cpParam.followLink {
|
||||
srcStat, err := statContainerPath(ctx, dockerCli, srcContainer, srcPath)
|
||||
if copyConfig.followLink {
|
||||
srcStat, err := client.ContainerStatPath(ctx, copyConfig.container, srcPath)
|
||||
|
||||
// If the destination is a symbolic link, we should follow it.
|
||||
if err == nil && srcStat.Mode&os.ModeSymlink != 0 {
|
||||
@ -145,20 +149,17 @@ func copyFromContainer(ctx context.Context, dockerCli command.Cli, srcContainer,
|
||||
|
||||
}
|
||||
|
||||
content, stat, err := dockerCli.Client().CopyFromContainer(ctx, srcContainer, srcPath)
|
||||
content, stat, err := client.CopyFromContainer(ctx, copyConfig.container, srcPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer content.Close()
|
||||
|
||||
if dstPath == "-" {
|
||||
// Send the response to STDOUT.
|
||||
_, err = io.Copy(os.Stdout, content)
|
||||
|
||||
_, err = io.Copy(dockerCli.Out(), content)
|
||||
return err
|
||||
}
|
||||
|
||||
// Prepare source copy info.
|
||||
srcInfo := archive.CopyInfo{
|
||||
Path: srcPath,
|
||||
Exists: true,
|
||||
@ -171,13 +172,17 @@ func copyFromContainer(ctx context.Context, dockerCli command.Cli, srcContainer,
|
||||
_, srcBase := archive.SplitPathDirEntry(srcInfo.Path)
|
||||
preArchive = archive.RebaseArchiveEntries(content, srcBase, srcInfo.RebaseName)
|
||||
}
|
||||
// See comments in the implementation of `archive.CopyTo` for exactly what
|
||||
// goes into deciding how and whether the source archive needs to be
|
||||
// altered for the correct copy behavior.
|
||||
return archive.CopyTo(preArchive, srcInfo, dstPath)
|
||||
}
|
||||
|
||||
func copyToContainer(ctx context.Context, dockerCli command.Cli, srcPath, dstContainer, dstPath string, cpParam *cpConfig, copyUIDGID bool) (err error) {
|
||||
// In order to get the copy behavior right, we need to know information
|
||||
// about both the source and destination. The API is a simple tar
|
||||
// archive/extract API but we can use the stat info header about the
|
||||
// destination to be more informed about exactly what the destination is.
|
||||
func copyToContainer(ctx context.Context, dockerCli command.Cli, copyConfig cpConfig) (err error) {
|
||||
srcPath := copyConfig.sourcePath
|
||||
dstPath := copyConfig.destPath
|
||||
|
||||
if srcPath != "-" {
|
||||
// Get an absolute source path.
|
||||
srcPath, err = resolveLocalPath(srcPath)
|
||||
@ -186,14 +191,10 @@ func copyToContainer(ctx context.Context, dockerCli command.Cli, srcPath, dstCon
|
||||
}
|
||||
}
|
||||
|
||||
// In order to get the copy behavior right, we need to know information
|
||||
// about both the source and destination. The API is a simple tar
|
||||
// archive/extract API but we can use the stat info header about the
|
||||
// destination to be more informed about exactly what the destination is.
|
||||
|
||||
client := dockerCli.Client()
|
||||
// Prepare destination copy info by stat-ing the container path.
|
||||
dstInfo := archive.CopyInfo{Path: dstPath}
|
||||
dstStat, err := statContainerPath(ctx, dockerCli, dstContainer, dstPath)
|
||||
dstStat, err := client.ContainerStatPath(ctx, copyConfig.container, dstPath)
|
||||
|
||||
// If the destination is a symbolic link, we should evaluate it.
|
||||
if err == nil && dstStat.Mode&os.ModeSymlink != 0 {
|
||||
@ -205,7 +206,7 @@ func copyToContainer(ctx context.Context, dockerCli command.Cli, srcPath, dstCon
|
||||
}
|
||||
|
||||
dstInfo.Path = linkTarget
|
||||
dstStat, err = statContainerPath(ctx, dockerCli, dstContainer, linkTarget)
|
||||
dstStat, err = client.ContainerStatPath(ctx, copyConfig.container, linkTarget)
|
||||
}
|
||||
|
||||
// Ignore any error and assume that the parent directory of the destination
|
||||
@ -224,15 +225,14 @@ func copyToContainer(ctx context.Context, dockerCli command.Cli, srcPath, dstCon
|
||||
)
|
||||
|
||||
if srcPath == "-" {
|
||||
// Use STDIN.
|
||||
content = os.Stdin
|
||||
resolvedDstPath = dstInfo.Path
|
||||
if !dstInfo.IsDir {
|
||||
return errors.Errorf("destination \"%s:%s\" must be a directory", dstContainer, dstPath)
|
||||
return errors.Errorf("destination \"%s:%s\" must be a directory", copyConfig.container, dstPath)
|
||||
}
|
||||
} else {
|
||||
// Prepare source copy info.
|
||||
srcInfo, err := archive.CopyInfoSourcePath(srcPath, cpParam.followLink)
|
||||
srcInfo, err := archive.CopyInfoSourcePath(srcPath, copyConfig.followLink)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -267,10 +267,9 @@ func copyToContainer(ctx context.Context, dockerCli command.Cli, srcPath, dstCon
|
||||
|
||||
options := types.CopyToContainerOptions{
|
||||
AllowOverwriteDirWithFile: false,
|
||||
CopyUIDGID: copyUIDGID,
|
||||
CopyUIDGID: copyConfig.copyUIDGID,
|
||||
}
|
||||
|
||||
return dockerCli.Client().CopyToContainer(ctx, dstContainer, resolvedDstPath, content, options)
|
||||
return client.CopyToContainer(ctx, copyConfig.container, resolvedDstPath, content, options)
|
||||
}
|
||||
|
||||
// We use `:` as a delimiter between CONTAINER and PATH, but `:` could also be
|
||||
|
||||
160
components/cli/cli/command/container/cp_test.go
Normal file
160
components/cli/cli/command/container/cp_test.go
Normal file
@ -0,0 +1,160 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/cli/internal/test/testutil"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/pkg/archive"
|
||||
"github.com/gotestyourself/gotestyourself/fs"
|
||||
"github.com/gotestyourself/gotestyourself/skip"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRunCopyWithInvalidArguments(t *testing.T) {
|
||||
var testcases = []struct {
|
||||
doc string
|
||||
options copyOptions
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
doc: "copy between container",
|
||||
options: copyOptions{
|
||||
source: "first:/path",
|
||||
destination: "second:/path",
|
||||
},
|
||||
expectedErr: "copying between containers is not supported",
|
||||
},
|
||||
{
|
||||
doc: "copy without a container",
|
||||
options: copyOptions{
|
||||
source: "./source",
|
||||
destination: "./dest",
|
||||
},
|
||||
expectedErr: "must specify at least one container source",
|
||||
},
|
||||
}
|
||||
for _, testcase := range testcases {
|
||||
t.Run(testcase.doc, func(t *testing.T) {
|
||||
err := runCopy(test.NewFakeCli(nil), testcase.options)
|
||||
assert.EqualError(t, err, testcase.expectedErr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunCopyFromContainerToStdout(t *testing.T) {
|
||||
tarContent := "the tar content"
|
||||
|
||||
fakeClient := &fakeClient{
|
||||
containerCopyFromFunc: func(container, srcPath string) (io.ReadCloser, types.ContainerPathStat, error) {
|
||||
assert.Equal(t, "container", container)
|
||||
return ioutil.NopCloser(strings.NewReader(tarContent)), types.ContainerPathStat{}, nil
|
||||
},
|
||||
}
|
||||
options := copyOptions{source: "container:/path", destination: "-"}
|
||||
cli := test.NewFakeCli(fakeClient)
|
||||
err := runCopy(cli, options)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tarContent, cli.OutBuffer().String())
|
||||
assert.Equal(t, "", cli.ErrBuffer().String())
|
||||
}
|
||||
|
||||
func TestRunCopyFromContainerToFilesystem(t *testing.T) {
|
||||
destDir := fs.NewDir(t, "cp-test",
|
||||
fs.WithFile("file1", "content\n"))
|
||||
defer destDir.Remove()
|
||||
|
||||
fakeClient := &fakeClient{
|
||||
containerCopyFromFunc: func(container, srcPath string) (io.ReadCloser, types.ContainerPathStat, error) {
|
||||
assert.Equal(t, "container", container)
|
||||
readCloser, err := archive.TarWithOptions(destDir.Path(), &archive.TarOptions{})
|
||||
return readCloser, types.ContainerPathStat{}, err
|
||||
},
|
||||
}
|
||||
options := copyOptions{source: "container:/path", destination: destDir.Path()}
|
||||
cli := test.NewFakeCli(fakeClient)
|
||||
err := runCopy(cli, options)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "", cli.OutBuffer().String())
|
||||
assert.Equal(t, "", cli.ErrBuffer().String())
|
||||
|
||||
content, err := ioutil.ReadFile(destDir.Join("file1"))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "content\n", string(content))
|
||||
}
|
||||
|
||||
func TestRunCopyFromContainerToFilesystemMissingDestinationDirectory(t *testing.T) {
|
||||
destDir := fs.NewDir(t, "cp-test",
|
||||
fs.WithFile("file1", "content\n"))
|
||||
defer destDir.Remove()
|
||||
|
||||
fakeClient := &fakeClient{
|
||||
containerCopyFromFunc: func(container, srcPath string) (io.ReadCloser, types.ContainerPathStat, error) {
|
||||
assert.Equal(t, "container", container)
|
||||
readCloser, err := archive.TarWithOptions(destDir.Path(), &archive.TarOptions{})
|
||||
return readCloser, types.ContainerPathStat{}, err
|
||||
},
|
||||
}
|
||||
|
||||
options := copyOptions{
|
||||
source: "container:/path",
|
||||
destination: destDir.Join("missing", "foo"),
|
||||
}
|
||||
cli := test.NewFakeCli(fakeClient)
|
||||
err := runCopy(cli, options)
|
||||
testutil.ErrorContains(t, err, destDir.Join("missing"))
|
||||
}
|
||||
|
||||
func TestSplitCpArg(t *testing.T) {
|
||||
var testcases = []struct {
|
||||
doc string
|
||||
path string
|
||||
os string
|
||||
expectedContainer string
|
||||
expectedPath string
|
||||
}{
|
||||
{
|
||||
doc: "absolute path with colon",
|
||||
os: "linux",
|
||||
path: "/abs/path:withcolon",
|
||||
expectedPath: "/abs/path:withcolon",
|
||||
},
|
||||
{
|
||||
doc: "relative path with colon",
|
||||
path: "./relative:path",
|
||||
expectedPath: "./relative:path",
|
||||
},
|
||||
{
|
||||
doc: "absolute path with drive",
|
||||
os: "windows",
|
||||
path: `d:\abs\path`,
|
||||
expectedPath: `d:\abs\path`,
|
||||
},
|
||||
{
|
||||
doc: "no separator",
|
||||
path: "relative/path",
|
||||
expectedPath: "relative/path",
|
||||
},
|
||||
{
|
||||
doc: "with separator",
|
||||
path: "container:/opt/foo",
|
||||
expectedPath: "/opt/foo",
|
||||
expectedContainer: "container",
|
||||
},
|
||||
}
|
||||
for _, testcase := range testcases {
|
||||
t.Run(testcase.doc, func(t *testing.T) {
|
||||
skip.IfCondition(t, testcase.os != "" && testcase.os != runtime.GOOS)
|
||||
|
||||
container, path := splitCpArg(testcase.path)
|
||||
assert.Equal(t, testcase.expectedContainer, container)
|
||||
assert.Equal(t, testcase.expectedPath, path)
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -21,7 +21,8 @@ import (
|
||||
)
|
||||
|
||||
type createOptions struct {
|
||||
name string
|
||||
name string
|
||||
platform string
|
||||
}
|
||||
|
||||
// NewCreateCommand creates a new cobra.Command for `docker create`
|
||||
@ -51,6 +52,7 @@ func NewCreateCommand(dockerCli command.Cli) *cobra.Command {
|
||||
// with hostname
|
||||
flags.Bool("help", false, "Print usage")
|
||||
|
||||
command.AddPlatformFlag(flags, &opts.platform)
|
||||
command.AddTrustVerificationFlags(flags)
|
||||
copts = addFlags(flags)
|
||||
return cmd
|
||||
@ -62,7 +64,7 @@ func runCreate(dockerCli command.Cli, flags *pflag.FlagSet, opts *createOptions,
|
||||
reportError(dockerCli.Err(), "create", err.Error(), true)
|
||||
return cli.StatusError{StatusCode: 125}
|
||||
}
|
||||
response, err := createContainer(context.Background(), dockerCli, containerConfig, opts.name)
|
||||
response, err := createContainer(context.Background(), dockerCli, containerConfig, opts.name, opts.platform)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -70,7 +72,7 @@ func runCreate(dockerCli command.Cli, flags *pflag.FlagSet, opts *createOptions,
|
||||
return nil
|
||||
}
|
||||
|
||||
func pullImage(ctx context.Context, dockerCli command.Cli, image string, out io.Writer) error {
|
||||
func pullImage(ctx context.Context, dockerCli command.Cli, image string, platform string, out io.Writer) error {
|
||||
ref, err := reference.ParseNormalizedNamed(image)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -90,6 +92,7 @@ func pullImage(ctx context.Context, dockerCli command.Cli, image string, out io.
|
||||
|
||||
options := types.ImageCreateOptions{
|
||||
RegistryAuth: encodedAuth,
|
||||
Platform: platform,
|
||||
}
|
||||
|
||||
responseBody, err := dockerCli.Client().ImageCreate(ctx, image, options)
|
||||
@ -155,7 +158,7 @@ func newCIDFile(path string) (*cidFile, error) {
|
||||
return &cidFile{path: path, file: f}, nil
|
||||
}
|
||||
|
||||
func createContainer(ctx context.Context, dockerCli command.Cli, containerConfig *containerConfig, name string) (*container.ContainerCreateCreatedBody, error) {
|
||||
func createContainer(ctx context.Context, dockerCli command.Cli, containerConfig *containerConfig, name string, platform string) (*container.ContainerCreateCreatedBody, error) {
|
||||
config := containerConfig.Config
|
||||
hostConfig := containerConfig.HostConfig
|
||||
networkingConfig := containerConfig.NetworkingConfig
|
||||
@ -198,7 +201,7 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerConfig
|
||||
fmt.Fprintf(stderr, "Unable to find image '%s' locally\n", reference.FamiliarString(namedRef))
|
||||
|
||||
// we don't want to write to stdout anything apart from container.ID
|
||||
if err := pullImage(ctx, dockerCli, config.Image, stderr); err != nil {
|
||||
if err := pullImage(ctx, dockerCli, config.Image, platform, stderr); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if taggedRef, ok := namedRef.(reference.NamedTagged); ok && trustedRef != nil {
|
||||
|
||||
@ -5,6 +5,7 @@ import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@ -106,7 +107,7 @@ func TestCreateContainerPullsImageIfMissing(t *testing.T) {
|
||||
},
|
||||
HostConfig: &container.HostConfig{},
|
||||
}
|
||||
body, err := createContainer(context.Background(), cli, config, "name")
|
||||
body, err := createContainer(context.Background(), cli, config, "name", runtime.GOOS)
|
||||
require.NoError(t, err)
|
||||
expected := container.ContainerCreateCreatedBody{ID: containerID}
|
||||
assert.Equal(t, expected, *body)
|
||||
|
||||
@ -24,6 +24,7 @@ type execOptions struct {
|
||||
user string
|
||||
privileged bool
|
||||
env opts.ListOpts
|
||||
workdir string
|
||||
container string
|
||||
command []string
|
||||
}
|
||||
@ -58,6 +59,8 @@ func NewExecCommand(dockerCli command.Cli) *cobra.Command {
|
||||
flags.BoolVarP(&options.privileged, "privileged", "", false, "Give extended privileges to the command")
|
||||
flags.VarP(&options.env, "env", "e", "Set environment variables")
|
||||
flags.SetAnnotation("env", "version", []string{"1.25"})
|
||||
flags.StringVarP(&options.workdir, "workdir", "w", "", "Working directory inside the container")
|
||||
flags.SetAnnotation("workdir", "version", []string{"1.35"})
|
||||
|
||||
return cmd
|
||||
}
|
||||
@ -190,6 +193,7 @@ func parseExec(opts execOptions, configFile *configfile.ConfigFile) *types.ExecC
|
||||
Cmd: opts.command,
|
||||
Detach: opts.detach,
|
||||
Env: opts.env.GetAll(),
|
||||
WorkingDir: opts.workdir,
|
||||
}
|
||||
|
||||
// If -d is not set, attach to everything by default
|
||||
|
||||
@ -14,6 +14,7 @@ import (
|
||||
type logsOptions struct {
|
||||
follow bool
|
||||
since string
|
||||
until string
|
||||
timestamps bool
|
||||
details bool
|
||||
tail string
|
||||
@ -38,6 +39,8 @@ func NewLogsCommand(dockerCli command.Cli) *cobra.Command {
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVarP(&opts.follow, "follow", "f", false, "Follow log output")
|
||||
flags.StringVar(&opts.since, "since", "", "Show logs since timestamp (e.g. 2013-01-02T13:23:37) or relative (e.g. 42m for 42 minutes)")
|
||||
flags.StringVar(&opts.until, "until", "", "Show logs before a timestamp (e.g. 2013-01-02T13:23:37) or relative (e.g. 42m for 42 minutes)")
|
||||
flags.SetAnnotation("until", "version", []string{"1.35"})
|
||||
flags.BoolVarP(&opts.timestamps, "timestamps", "t", false, "Show timestamps")
|
||||
flags.BoolVar(&opts.details, "details", false, "Show extra details provided to logs")
|
||||
flags.StringVar(&opts.tail, "tail", "all", "Number of lines to show from the end of the logs")
|
||||
@ -51,6 +54,7 @@ func runLogs(dockerCli command.Cli, opts *logsOptions) error {
|
||||
ShowStdout: true,
|
||||
ShowStderr: true,
|
||||
Since: opts.since,
|
||||
Until: opts.until,
|
||||
Timestamps: opts.timestamps,
|
||||
Follow: opts.follow,
|
||||
Tail: opts.tail,
|
||||
|
||||
62
components/cli/cli/command/container/logs_test.go
Normal file
62
components/cli/cli/command/container/logs_test.go
Normal file
@ -0,0 +1,62 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/cli/internal/test/testutil"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var logFn = func(expectedOut string) func(string, types.ContainerLogsOptions) (io.ReadCloser, error) {
|
||||
return func(container string, opts types.ContainerLogsOptions) (io.ReadCloser, error) {
|
||||
return ioutil.NopCloser(strings.NewReader(expectedOut)), nil
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunLogs(t *testing.T) {
|
||||
inspectFn := func(containerID string) (types.ContainerJSON, error) {
|
||||
return types.ContainerJSON{
|
||||
Config: &container.Config{Tty: true},
|
||||
ContainerJSONBase: &types.ContainerJSONBase{State: &types.ContainerState{Running: false}},
|
||||
}, nil
|
||||
}
|
||||
|
||||
var testcases = []struct {
|
||||
doc string
|
||||
options *logsOptions
|
||||
client fakeClient
|
||||
expectedError string
|
||||
expectedOut string
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
doc: "successful logs",
|
||||
expectedOut: "foo",
|
||||
options: &logsOptions{},
|
||||
client: fakeClient{logFunc: logFn("foo"), inspectFunc: inspectFn},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testcase := range testcases {
|
||||
t.Run(testcase.doc, func(t *testing.T) {
|
||||
cli := test.NewFakeCli(&testcase.client)
|
||||
|
||||
err := runLogs(cli, testcase.options)
|
||||
if testcase.expectedError != "" {
|
||||
testutil.ErrorContains(t, err, testcase.expectedError)
|
||||
} else {
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
}
|
||||
assert.Equal(t, testcase.expectedOut, cli.OutBuffer().String())
|
||||
assert.Equal(t, testcase.expectedErr, cli.ErrBuffer().String())
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -29,6 +29,7 @@ type runOptions struct {
|
||||
sigProxy bool
|
||||
name string
|
||||
detachKeys string
|
||||
platform string
|
||||
}
|
||||
|
||||
// NewRunCommand create a new `docker run` command
|
||||
@ -62,6 +63,7 @@ func NewRunCommand(dockerCli command.Cli) *cobra.Command {
|
||||
// with hostname
|
||||
flags.Bool("help", false, "Print usage")
|
||||
|
||||
command.AddPlatformFlag(flags, &opts.platform)
|
||||
command.AddTrustVerificationFlags(flags)
|
||||
copts = addFlags(flags)
|
||||
return cmd
|
||||
@ -160,7 +162,7 @@ func runContainer(dockerCli command.Cli, opts *runOptions, copts *containerOptio
|
||||
|
||||
ctx, cancelFun := context.WithCancel(context.Background())
|
||||
|
||||
createResponse, err := createContainer(ctx, dockerCli, containerConfig, opts.name)
|
||||
createResponse, err := createContainer(ctx, dockerCli, containerConfig, opts.name, opts.platform)
|
||||
if err != nil {
|
||||
reportError(stderr, cmdPath, err.Error(), true)
|
||||
return runStartContainerErr(err)
|
||||
|
||||
@ -64,6 +64,7 @@ type buildOptions struct {
|
||||
target string
|
||||
imageIDFile string
|
||||
stream bool
|
||||
platform string
|
||||
}
|
||||
|
||||
// dockerfileFromStdin returns true when the user specified that the Dockerfile
|
||||
@ -135,6 +136,7 @@ func NewBuildCommand(dockerCli command.Cli) *cobra.Command {
|
||||
flags.StringVar(&options.imageIDFile, "iidfile", "", "Write the image ID to the file")
|
||||
|
||||
command.AddTrustVerificationFlags(flags)
|
||||
command.AddPlatformFlag(flags, &options.platform)
|
||||
|
||||
flags.BoolVar(&options.squash, "squash", false, "Squash newly built layers into a single new layer")
|
||||
flags.SetAnnotation("squash", "experimental", nil)
|
||||
@ -305,8 +307,8 @@ func runBuild(dockerCli command.Cli, options buildOptions) error {
|
||||
progressOutput = &lastProgressOutput{output: progressOutput}
|
||||
}
|
||||
|
||||
// if up to this point nothing has set the context then we must have have
|
||||
// another way for sending it(streaming) and set the context to the Dockerfile
|
||||
// if up to this point nothing has set the context then we must have another
|
||||
// way for sending it(streaming) and set the context to the Dockerfile
|
||||
if dockerfileCtx != nil && buildCtx == nil {
|
||||
buildCtx = dockerfileCtx
|
||||
}
|
||||
@ -374,6 +376,7 @@ func runBuild(dockerCli command.Cli, options buildOptions) error {
|
||||
ExtraHosts: options.extraHosts.GetAll(),
|
||||
Target: options.target,
|
||||
RemoteContext: remote,
|
||||
Platform: options.platform,
|
||||
}
|
||||
|
||||
if s != nil {
|
||||
|
||||
@ -14,8 +14,9 @@ import (
|
||||
)
|
||||
|
||||
type pullOptions struct {
|
||||
remote string
|
||||
all bool
|
||||
remote string
|
||||
all bool
|
||||
platform string
|
||||
}
|
||||
|
||||
// NewPullCommand creates a new `docker pull` command
|
||||
@ -35,6 +36,8 @@ func NewPullCommand(dockerCli command.Cli) *cobra.Command {
|
||||
flags := cmd.Flags()
|
||||
|
||||
flags.BoolVarP(&opts.all, "all-tags", "a", false, "Download all tagged images in the repository")
|
||||
|
||||
command.AddPlatformFlag(flags, &opts.platform)
|
||||
command.AddTrustVerificationFlags(flags)
|
||||
|
||||
return cmd
|
||||
@ -63,9 +66,9 @@ func runPull(cli command.Cli, opts pullOptions) error {
|
||||
// Check if reference has a digest
|
||||
_, isCanonical := distributionRef.(reference.Canonical)
|
||||
if command.IsTrusted() && !isCanonical {
|
||||
err = trustedPull(ctx, cli, imgRefAndAuth)
|
||||
err = trustedPull(ctx, cli, imgRefAndAuth, opts.platform)
|
||||
} else {
|
||||
err = imagePullPrivileged(ctx, cli, imgRefAndAuth, opts.all)
|
||||
err = imagePullPrivileged(ctx, cli, imgRefAndAuth, opts.all, opts.platform)
|
||||
}
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "when fetching 'plugin'") {
|
||||
|
||||
@ -9,6 +9,7 @@ import (
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/docker/api/types"
|
||||
apiclient "github.com/docker/docker/client"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@ -56,9 +57,13 @@ func runRemove(dockerCli command.Cli, opts removeOptions, images []string) error
|
||||
}
|
||||
|
||||
var errs []string
|
||||
var fatalErr = false
|
||||
for _, img := range images {
|
||||
dels, err := client.ImageRemove(ctx, img, options)
|
||||
if err != nil {
|
||||
if !apiclient.IsErrNotFound(err) {
|
||||
fatalErr = true
|
||||
}
|
||||
errs = append(errs, err.Error())
|
||||
} else {
|
||||
for _, del := range dels {
|
||||
@ -73,7 +78,7 @@ func runRemove(dockerCli command.Cli, opts removeOptions, images []string) error
|
||||
|
||||
if len(errs) > 0 {
|
||||
msg := strings.Join(errs, "\n")
|
||||
if !opts.force {
|
||||
if !opts.force || fatalErr {
|
||||
return errors.New(msg)
|
||||
}
|
||||
fmt.Fprintf(dockerCli.Err(), msg)
|
||||
|
||||
@ -13,6 +13,18 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type notFound struct {
|
||||
imageID string
|
||||
}
|
||||
|
||||
func (n notFound) Error() string {
|
||||
return fmt.Sprintf("Error: No such image: %s", n.imageID)
|
||||
}
|
||||
|
||||
func (n notFound) NotFound() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func TestNewRemoveCommandAlias(t *testing.T) {
|
||||
cmd := newRemoveCommand(test.NewFakeCli(&fakeClient{}))
|
||||
assert.True(t, cmd.HasAlias("rmi"))
|
||||
@ -31,6 +43,15 @@ func TestNewRemoveCommandErrors(t *testing.T) {
|
||||
name: "wrong args",
|
||||
expectedError: "requires at least 1 argument.",
|
||||
},
|
||||
{
|
||||
name: "ImageRemove fail with force option",
|
||||
args: []string{"-f", "image1"},
|
||||
expectedError: "error removing image",
|
||||
imageRemoveFunc: func(image string, options types.ImageRemoveOptions) ([]types.ImageDeleteResponseItem, error) {
|
||||
assert.Equal(t, "image1", image)
|
||||
return []types.ImageDeleteResponseItem{}, errors.Errorf("error removing image")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ImageRemove fail",
|
||||
args: []string{"arg1"},
|
||||
@ -43,12 +64,14 @@ func TestNewRemoveCommandErrors(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
cmd := NewRemoveCommand(test.NewFakeCli(&fakeClient{
|
||||
imageRemoveFunc: tc.imageRemoveFunc,
|
||||
}))
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cmd := NewRemoveCommand(test.NewFakeCli(&fakeClient{
|
||||
imageRemoveFunc: tc.imageRemoveFunc,
|
||||
}))
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -57,7 +80,7 @@ func TestNewRemoveCommandSuccess(t *testing.T) {
|
||||
name string
|
||||
args []string
|
||||
imageRemoveFunc func(image string, options types.ImageRemoveOptions) ([]types.ImageDeleteResponseItem, error)
|
||||
expectedErrMsg string
|
||||
expectedStderr string
|
||||
}{
|
||||
{
|
||||
name: "Image Deleted",
|
||||
@ -68,14 +91,16 @@ func TestNewRemoveCommandSuccess(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Image Deleted with force option",
|
||||
name: "Image not found with force option",
|
||||
args: []string{"-f", "image1"},
|
||||
imageRemoveFunc: func(image string, options types.ImageRemoveOptions) ([]types.ImageDeleteResponseItem, error) {
|
||||
assert.Equal(t, "image1", image)
|
||||
return []types.ImageDeleteResponseItem{}, errors.Errorf("error removing image")
|
||||
assert.Equal(t, true, options.Force)
|
||||
return []types.ImageDeleteResponseItem{}, notFound{"image1"}
|
||||
},
|
||||
expectedErrMsg: "error removing image",
|
||||
expectedStderr: "Error: No such image: image1",
|
||||
},
|
||||
|
||||
{
|
||||
name: "Image Untagged",
|
||||
args: []string{"image1"},
|
||||
@ -96,14 +121,14 @@ func TestNewRemoveCommandSuccess(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
cli := test.NewFakeCli(&fakeClient{imageRemoveFunc: tc.imageRemoveFunc})
|
||||
cmd := NewRemoveCommand(cli)
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
assert.NoError(t, cmd.Execute())
|
||||
if tc.expectedErrMsg != "" {
|
||||
assert.Equal(t, tc.expectedErrMsg, cli.ErrBuffer().String())
|
||||
}
|
||||
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("remove-command-success.%s.golden", tc.name))
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{imageRemoveFunc: tc.imageRemoveFunc})
|
||||
cmd := NewRemoveCommand(cli)
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
assert.NoError(t, cmd.Execute())
|
||||
assert.Equal(t, tc.expectedStderr, cli.ErrBuffer().String())
|
||||
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("remove-command-success.%s.golden", tc.name))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -180,7 +180,7 @@ func imagePushPrivileged(ctx context.Context, cli command.Cli, authConfig types.
|
||||
}
|
||||
|
||||
// trustedPull handles content trust pulling of an image
|
||||
func trustedPull(ctx context.Context, cli command.Cli, imgRefAndAuth trust.ImageRefAndAuth) error {
|
||||
func trustedPull(ctx context.Context, cli command.Cli, imgRefAndAuth trust.ImageRefAndAuth, platform string) error {
|
||||
refs, err := getTrustedPullTargets(cli, imgRefAndAuth)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -202,7 +202,7 @@ func trustedPull(ctx context.Context, cli command.Cli, imgRefAndAuth trust.Image
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := imagePullPrivileged(ctx, cli, updatedImgRefAndAuth, false); err != nil {
|
||||
if err := imagePullPrivileged(ctx, cli, updatedImgRefAndAuth, false, platform); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -268,7 +268,7 @@ func getTrustedPullTargets(cli command.Cli, imgRefAndAuth trust.ImageRefAndAuth)
|
||||
}
|
||||
|
||||
// imagePullPrivileged pulls the image and displays it to the output
|
||||
func imagePullPrivileged(ctx context.Context, cli command.Cli, imgRefAndAuth trust.ImageRefAndAuth, all bool) error {
|
||||
func imagePullPrivileged(ctx context.Context, cli command.Cli, imgRefAndAuth trust.ImageRefAndAuth, all bool, platform string) error {
|
||||
ref := reference.FamiliarString(imgRefAndAuth.Reference())
|
||||
|
||||
encodedAuth, err := command.EncodeAuthToBase64(*imgRefAndAuth.AuthConfig())
|
||||
@ -280,8 +280,8 @@ func imagePullPrivileged(ctx context.Context, cli command.Cli, imgRefAndAuth tru
|
||||
RegistryAuth: encodedAuth,
|
||||
PrivilegeFunc: requestPrivilege,
|
||||
All: all,
|
||||
Platform: platform,
|
||||
}
|
||||
|
||||
responseBody, err := cli.Client().ImagePull(ctx, ref, options)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
cliopts "github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/versions"
|
||||
"github.com/spf13/cobra"
|
||||
@ -58,6 +59,9 @@ func newCreateCommand(dockerCli command.Cli) *cobra.Command {
|
||||
flags.Var(&opts.hosts, flagHost, "Set one or more custom host-to-IP mappings (host:ip)")
|
||||
flags.SetAnnotation(flagHost, "version", []string{"1.25"})
|
||||
|
||||
flags.Var(cliopts.NewListOptsRef(&opts.resources.resGenericResources, ValidateSingleGenericResource), "generic-resource", "User defined resources")
|
||||
flags.SetAnnotation(flagHostAdd, "version", []string{"1.32"})
|
||||
|
||||
flags.SetInterspersed(false)
|
||||
return cmd
|
||||
}
|
||||
|
||||
105
components/cli/cli/command/service/generic_resource_opts.go
Normal file
105
components/cli/cli/command/service/generic_resource_opts.go
Normal file
@ -0,0 +1,105 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
swarmapi "github.com/docker/swarmkit/api"
|
||||
"github.com/docker/swarmkit/api/genericresource"
|
||||
)
|
||||
|
||||
// GenericResource is a concept that a user can use to advertise user-defined
|
||||
// resources on a node and thus better place services based on these resources.
|
||||
// E.g: NVIDIA GPUs, Intel FPGAs, ...
|
||||
// See https://github.com/docker/swarmkit/blob/master/design/generic_resources.md
|
||||
|
||||
// ValidateSingleGenericResource validates that a single entry in the
|
||||
// generic resource list is valid.
|
||||
// i.e 'GPU=UID1' is valid however 'GPU:UID1' or 'UID1' isn't
|
||||
func ValidateSingleGenericResource(val string) (string, error) {
|
||||
if strings.Count(val, "=") < 1 {
|
||||
return "", fmt.Errorf("invalid generic-resource format `%s` expected `name=value`", val)
|
||||
}
|
||||
|
||||
return val, nil
|
||||
}
|
||||
|
||||
// ParseGenericResources parses an array of Generic resourceResources
|
||||
// Requesting Named Generic Resources for a service is not supported this
|
||||
// is filtered here.
|
||||
func ParseGenericResources(value []string) ([]swarm.GenericResource, error) {
|
||||
if len(value) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
resources, err := genericresource.Parse(value)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "invalid generic resource specification")
|
||||
}
|
||||
|
||||
swarmResources := genericResourcesFromGRPC(resources)
|
||||
for _, res := range swarmResources {
|
||||
if res.NamedResourceSpec != nil {
|
||||
return nil, fmt.Errorf("invalid generic-resource request `%s=%s`, Named Generic Resources is not supported for service create or update", res.NamedResourceSpec.Kind, res.NamedResourceSpec.Value)
|
||||
}
|
||||
}
|
||||
|
||||
return swarmResources, nil
|
||||
}
|
||||
|
||||
// genericResourcesFromGRPC converts a GRPC GenericResource to a GenericResource
|
||||
func genericResourcesFromGRPC(genericRes []*swarmapi.GenericResource) []swarm.GenericResource {
|
||||
var generic []swarm.GenericResource
|
||||
for _, res := range genericRes {
|
||||
var current swarm.GenericResource
|
||||
|
||||
switch r := res.Resource.(type) {
|
||||
case *swarmapi.GenericResource_DiscreteResourceSpec:
|
||||
current.DiscreteResourceSpec = &swarm.DiscreteGenericResource{
|
||||
Kind: r.DiscreteResourceSpec.Kind,
|
||||
Value: r.DiscreteResourceSpec.Value,
|
||||
}
|
||||
case *swarmapi.GenericResource_NamedResourceSpec:
|
||||
current.NamedResourceSpec = &swarm.NamedGenericResource{
|
||||
Kind: r.NamedResourceSpec.Kind,
|
||||
Value: r.NamedResourceSpec.Value,
|
||||
}
|
||||
}
|
||||
|
||||
generic = append(generic, current)
|
||||
}
|
||||
|
||||
return generic
|
||||
}
|
||||
|
||||
func buildGenericResourceMap(genericRes []swarm.GenericResource) (map[string]swarm.GenericResource, error) {
|
||||
m := make(map[string]swarm.GenericResource)
|
||||
|
||||
for _, res := range genericRes {
|
||||
if res.DiscreteResourceSpec == nil {
|
||||
return nil, fmt.Errorf("invalid generic-resource `%+v` for service task", res)
|
||||
}
|
||||
|
||||
_, ok := m[res.DiscreteResourceSpec.Kind]
|
||||
if ok {
|
||||
return nil, fmt.Errorf("duplicate generic-resource `%+v` for service task", res.DiscreteResourceSpec.Kind)
|
||||
}
|
||||
|
||||
m[res.DiscreteResourceSpec.Kind] = res
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func buildGenericResourceList(genericRes map[string]swarm.GenericResource) []swarm.GenericResource {
|
||||
var l []swarm.GenericResource
|
||||
|
||||
for _, res := range genericRes {
|
||||
l = append(l, res)
|
||||
}
|
||||
|
||||
return l
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestValidateSingleGenericResource(t *testing.T) {
|
||||
incorrect := []string{"foo", "fooo-bar"}
|
||||
correct := []string{"foo=bar", "bar=1", "foo=barbar"}
|
||||
|
||||
for _, v := range incorrect {
|
||||
_, err := ValidateSingleGenericResource(v)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
for _, v := range correct {
|
||||
_, err := ValidateSingleGenericResource(v)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
}
|
||||
@ -222,23 +222,30 @@ func (opts updateOptions) rollbackConfig(flags *pflag.FlagSet) *swarm.UpdateConf
|
||||
}
|
||||
|
||||
type resourceOptions struct {
|
||||
limitCPU opts.NanoCPUs
|
||||
limitMemBytes opts.MemBytes
|
||||
resCPU opts.NanoCPUs
|
||||
resMemBytes opts.MemBytes
|
||||
limitCPU opts.NanoCPUs
|
||||
limitMemBytes opts.MemBytes
|
||||
resCPU opts.NanoCPUs
|
||||
resMemBytes opts.MemBytes
|
||||
resGenericResources []string
|
||||
}
|
||||
|
||||
func (r *resourceOptions) ToResourceRequirements() *swarm.ResourceRequirements {
|
||||
func (r *resourceOptions) ToResourceRequirements() (*swarm.ResourceRequirements, error) {
|
||||
generic, err := ParseGenericResources(r.resGenericResources)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &swarm.ResourceRequirements{
|
||||
Limits: &swarm.Resources{
|
||||
NanoCPUs: r.limitCPU.Value(),
|
||||
MemoryBytes: r.limitMemBytes.Value(),
|
||||
},
|
||||
Reservations: &swarm.Resources{
|
||||
NanoCPUs: r.resCPU.Value(),
|
||||
MemoryBytes: r.resMemBytes.Value(),
|
||||
NanoCPUs: r.resCPU.Value(),
|
||||
MemoryBytes: r.resMemBytes.Value(),
|
||||
GenericResources: generic,
|
||||
},
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
type restartPolicyOptions struct {
|
||||
@ -505,6 +512,8 @@ type serviceOptions struct {
|
||||
healthcheck healthCheckOptions
|
||||
secrets opts.SecretOpt
|
||||
configs opts.ConfigOpt
|
||||
|
||||
isolation string
|
||||
}
|
||||
|
||||
func newServiceOptions() *serviceOptions {
|
||||
@ -586,6 +595,11 @@ func (options *serviceOptions) ToService(ctx context.Context, apiClient client.N
|
||||
return service, err
|
||||
}
|
||||
|
||||
resources, err := options.resources.ToResourceRequirements()
|
||||
if err != nil {
|
||||
return service, err
|
||||
}
|
||||
|
||||
service = swarm.ServiceSpec{
|
||||
Annotations: swarm.Annotations{
|
||||
Name: options.name,
|
||||
@ -614,9 +628,10 @@ func (options *serviceOptions) ToService(ctx context.Context, apiClient client.N
|
||||
Hosts: convertExtraHostsToSwarmHosts(options.hosts.GetAll()),
|
||||
StopGracePeriod: options.ToStopGracePeriod(flags),
|
||||
Healthcheck: healthConfig,
|
||||
Isolation: container.Isolation(options.isolation),
|
||||
},
|
||||
Networks: networks,
|
||||
Resources: options.resources.ToResourceRequirements(),
|
||||
Resources: resources,
|
||||
RestartPolicy: options.restartPolicy.ToRestartPolicy(flags),
|
||||
Placement: &swarm.Placement{
|
||||
Constraints: options.constraints.GetAll(),
|
||||
@ -784,6 +799,8 @@ func addServiceFlags(flags *pflag.FlagSet, opts *serviceOptions, defaultFlagValu
|
||||
|
||||
flags.StringVar(&opts.stopSignal, flagStopSignal, "", "Signal to stop the container")
|
||||
flags.SetAnnotation(flagStopSignal, "version", []string{"1.28"})
|
||||
flags.StringVar(&opts.isolation, flagIsolation, "", "Service container isolation mode")
|
||||
flags.SetAnnotation(flagIsolation, "version", []string{"1.35"})
|
||||
}
|
||||
|
||||
const (
|
||||
@ -813,6 +830,8 @@ const (
|
||||
flagEnvFile = "env-file"
|
||||
flagEnvRemove = "env-rm"
|
||||
flagEnvAdd = "env-add"
|
||||
flagGenericResourcesRemove = "generic-resource-rm"
|
||||
flagGenericResourcesAdd = "generic-resource-add"
|
||||
flagGroup = "group"
|
||||
flagGroupAdd = "group-add"
|
||||
flagGroupRemove = "group-rm"
|
||||
@ -879,4 +898,5 @@ const (
|
||||
flagConfig = "config"
|
||||
flagConfigAdd = "config-add"
|
||||
flagConfigRemove = "config-rm"
|
||||
flagIsolation = "isolation"
|
||||
)
|
||||
|
||||
@ -85,3 +85,41 @@ func TestHealthCheckOptionsToHealthConfigConflict(t *testing.T) {
|
||||
_, err := opt.toHealthConfig()
|
||||
assert.EqualError(t, err, "--no-healthcheck conflicts with --health-* options")
|
||||
}
|
||||
|
||||
func TestResourceOptionsToResourceRequirements(t *testing.T) {
|
||||
incorrectOptions := []resourceOptions{
|
||||
{
|
||||
resGenericResources: []string{"foo=bar", "foo=1"},
|
||||
},
|
||||
{
|
||||
resGenericResources: []string{"foo=bar", "foo=baz"},
|
||||
},
|
||||
{
|
||||
resGenericResources: []string{"foo=bar"},
|
||||
},
|
||||
{
|
||||
resGenericResources: []string{"foo=1", "foo=2"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, opt := range incorrectOptions {
|
||||
_, err := opt.ToResourceRequirements()
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
correctOptions := []resourceOptions{
|
||||
{
|
||||
resGenericResources: []string{"foo=1"},
|
||||
},
|
||||
{
|
||||
resGenericResources: []string{"foo=1", "bar=2"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, opt := range correctOptions {
|
||||
r, err := opt.ToResourceRequirements()
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, r.Reservations.GenericResources, len(opt.resGenericResources))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -95,6 +95,12 @@ func newUpdateCommand(dockerCli command.Cli) *cobra.Command {
|
||||
flags.Var(&options.hosts, flagHostAdd, "Add a custom host-to-IP mapping (host:ip)")
|
||||
flags.SetAnnotation(flagHostAdd, "version", []string{"1.25"})
|
||||
|
||||
// Add needs parsing, Remove only needs the key
|
||||
flags.Var(newListOptsVar(), flagGenericResourcesRemove, "Remove a Generic resource")
|
||||
flags.SetAnnotation(flagHostAdd, "version", []string{"1.32"})
|
||||
flags.Var(newListOptsVarWithValidator(ValidateSingleGenericResource), flagGenericResourcesAdd, "Add a Generic resource")
|
||||
flags.SetAnnotation(flagHostAdd, "version", []string{"1.32"})
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
@ -102,6 +108,10 @@ func newListOptsVar() *opts.ListOpts {
|
||||
return opts.NewListOptsRef(&[]string{}, nil)
|
||||
}
|
||||
|
||||
func newListOptsVarWithValidator(validator opts.ValidatorFctType) *opts.ListOpts {
|
||||
return opts.NewListOptsRef(&[]string{}, validator)
|
||||
}
|
||||
|
||||
// nolint: gocyclo
|
||||
func runUpdate(dockerCli command.Cli, flags *pflag.FlagSet, options *serviceOptions, serviceID string) error {
|
||||
apiClient := dockerCli.Client()
|
||||
@ -269,6 +279,14 @@ func updateService(ctx context.Context, apiClient client.NetworkAPIClient, flags
|
||||
}
|
||||
}
|
||||
|
||||
updateIsolation := func(flag string, field *container.Isolation) error {
|
||||
if flags.Changed(flag) {
|
||||
val, _ := flags.GetString(flag)
|
||||
*field = container.Isolation(val)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
cspec := spec.TaskTemplate.ContainerSpec
|
||||
task := &spec.TaskTemplate
|
||||
|
||||
@ -288,6 +306,9 @@ func updateService(ctx context.Context, apiClient client.NetworkAPIClient, flags
|
||||
updateString(flagWorkdir, &cspec.Dir)
|
||||
updateString(flagUser, &cspec.User)
|
||||
updateString(flagHostname, &cspec.Hostname)
|
||||
if err := updateIsolation(flagIsolation, &cspec.Isolation); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := updateMounts(flags, &cspec.Mounts); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -303,6 +324,14 @@ func updateService(ctx context.Context, apiClient client.NetworkAPIClient, flags
|
||||
updateInt64Value(flagReserveMemory, &task.Resources.Reservations.MemoryBytes)
|
||||
}
|
||||
|
||||
if err := addGenericResources(flags, task); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := removeGenericResources(flags, task); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
updateDurationOpt(flagStopGracePeriod, &cspec.StopGracePeriod)
|
||||
|
||||
if anyChanged(flags, flagRestartCondition, flagRestartDelay, flagRestartMaxAttempts, flagRestartWindow) {
|
||||
@ -459,6 +488,72 @@ func anyChanged(flags *pflag.FlagSet, fields ...string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func addGenericResources(flags *pflag.FlagSet, spec *swarm.TaskSpec) error {
|
||||
if !flags.Changed(flagGenericResourcesAdd) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if spec.Resources == nil {
|
||||
spec.Resources = &swarm.ResourceRequirements{}
|
||||
}
|
||||
|
||||
if spec.Resources.Reservations == nil {
|
||||
spec.Resources.Reservations = &swarm.Resources{}
|
||||
}
|
||||
|
||||
values := flags.Lookup(flagGenericResourcesAdd).Value.(*opts.ListOpts).GetAll()
|
||||
generic, err := ParseGenericResources(values)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m, err := buildGenericResourceMap(spec.Resources.Reservations.GenericResources)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, toAddRes := range generic {
|
||||
m[toAddRes.DiscreteResourceSpec.Kind] = toAddRes
|
||||
}
|
||||
|
||||
spec.Resources.Reservations.GenericResources = buildGenericResourceList(m)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func removeGenericResources(flags *pflag.FlagSet, spec *swarm.TaskSpec) error {
|
||||
// Can only be Discrete Resources
|
||||
if !flags.Changed(flagGenericResourcesRemove) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if spec.Resources == nil {
|
||||
spec.Resources = &swarm.ResourceRequirements{}
|
||||
}
|
||||
|
||||
if spec.Resources.Reservations == nil {
|
||||
spec.Resources.Reservations = &swarm.Resources{}
|
||||
}
|
||||
|
||||
values := flags.Lookup(flagGenericResourcesRemove).Value.(*opts.ListOpts).GetAll()
|
||||
|
||||
m, err := buildGenericResourceMap(spec.Resources.Reservations.GenericResources)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, toRemoveRes := range values {
|
||||
if _, ok := m[toRemoveRes]; !ok {
|
||||
return fmt.Errorf("could not find generic-resource `%s` to remove it", toRemoveRes)
|
||||
}
|
||||
|
||||
delete(m, toRemoveRes)
|
||||
}
|
||||
|
||||
spec.Resources.Reservations.GenericResources = buildGenericResourceList(m)
|
||||
return nil
|
||||
}
|
||||
|
||||
func updatePlacementConstraints(flags *pflag.FlagSet, placement *swarm.Placement) {
|
||||
if flags.Changed(flagConstraintAdd) {
|
||||
values := flags.Lookup(flagConstraintAdd).Value.(*opts.ListOpts).GetAll()
|
||||
|
||||
@ -518,3 +518,71 @@ func TestUpdateStopSignal(t *testing.T) {
|
||||
updateService(nil, nil, flags, spec)
|
||||
assert.Equal(t, "SIGWINCH", cspec.StopSignal)
|
||||
}
|
||||
|
||||
func TestUpdateIsolationValid(t *testing.T) {
|
||||
flags := newUpdateCommand(nil).Flags()
|
||||
err := flags.Set("isolation", "process")
|
||||
require.NoError(t, err)
|
||||
spec := swarm.ServiceSpec{
|
||||
TaskTemplate: swarm.TaskSpec{
|
||||
ContainerSpec: &swarm.ContainerSpec{},
|
||||
},
|
||||
}
|
||||
err = updateService(context.Background(), nil, flags, &spec)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, container.IsolationProcess, spec.TaskTemplate.ContainerSpec.Isolation)
|
||||
}
|
||||
|
||||
func TestUpdateIsolationInvalid(t *testing.T) {
|
||||
// validation depends on daemon os / version so validation should be done on the daemon side
|
||||
flags := newUpdateCommand(nil).Flags()
|
||||
err := flags.Set("isolation", "test")
|
||||
require.NoError(t, err)
|
||||
spec := swarm.ServiceSpec{
|
||||
TaskTemplate: swarm.TaskSpec{
|
||||
ContainerSpec: &swarm.ContainerSpec{},
|
||||
},
|
||||
}
|
||||
err = updateService(context.Background(), nil, flags, &spec)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, container.Isolation("test"), spec.TaskTemplate.ContainerSpec.Isolation)
|
||||
}
|
||||
|
||||
func TestAddGenericResources(t *testing.T) {
|
||||
task := &swarm.TaskSpec{}
|
||||
flags := newUpdateCommand(nil).Flags()
|
||||
|
||||
assert.Nil(t, addGenericResources(flags, task))
|
||||
|
||||
flags.Set(flagGenericResourcesAdd, "foo=1")
|
||||
assert.NoError(t, addGenericResources(flags, task))
|
||||
assert.Len(t, task.Resources.Reservations.GenericResources, 1)
|
||||
|
||||
// Checks that foo isn't added a 2nd time
|
||||
flags = newUpdateCommand(nil).Flags()
|
||||
flags.Set(flagGenericResourcesAdd, "bar=1")
|
||||
assert.NoError(t, addGenericResources(flags, task))
|
||||
assert.Len(t, task.Resources.Reservations.GenericResources, 2)
|
||||
}
|
||||
|
||||
func TestRemoveGenericResources(t *testing.T) {
|
||||
task := &swarm.TaskSpec{}
|
||||
flags := newUpdateCommand(nil).Flags()
|
||||
|
||||
assert.Nil(t, removeGenericResources(flags, task))
|
||||
|
||||
flags.Set(flagGenericResourcesRemove, "foo")
|
||||
assert.Error(t, removeGenericResources(flags, task))
|
||||
|
||||
flags = newUpdateCommand(nil).Flags()
|
||||
flags.Set(flagGenericResourcesAdd, "foo=1")
|
||||
addGenericResources(flags, task)
|
||||
flags = newUpdateCommand(nil).Flags()
|
||||
flags.Set(flagGenericResourcesAdd, "bar=1")
|
||||
addGenericResources(flags, task)
|
||||
|
||||
flags = newUpdateCommand(nil).Flags()
|
||||
flags.Set(flagGenericResourcesRemove, "foo")
|
||||
assert.NoError(t, removeGenericResources(flags, task))
|
||||
assert.Len(t, task.Resources.Reservations.GenericResources, 1)
|
||||
}
|
||||
|
||||
@ -1,34 +1,52 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"sort"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/templates"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
var versionTemplate = `Client:
|
||||
Version: {{.Client.Version}}
|
||||
API version: {{.Client.APIVersion}}{{if ne .Client.APIVersion .Client.DefaultAPIVersion}} (downgraded from {{.Client.DefaultAPIVersion}}){{end}}
|
||||
Go version: {{.Client.GoVersion}}
|
||||
Git commit: {{.Client.GitCommit}}
|
||||
Built: {{.Client.BuildTime}}
|
||||
OS/Arch: {{.Client.Os}}/{{.Client.Arch}}{{if .ServerOK}}
|
||||
var versionTemplate = `{{with .Client -}}
|
||||
Client:{{if ne .Platform.Name ""}} {{.Platform.Name}}{{end}}
|
||||
Version: {{.Version}}
|
||||
API version: {{.APIVersion}}{{if ne .APIVersion .DefaultAPIVersion}} (downgraded from {{.DefaultAPIVersion}}){{end}}
|
||||
Go version: {{.GoVersion}}
|
||||
Git commit: {{.GitCommit}}
|
||||
Built: {{.BuildTime}}
|
||||
OS/Arch: {{.Os}}/{{.Arch}}
|
||||
{{- end}}
|
||||
|
||||
Server:
|
||||
Version: {{.Server.Version}}
|
||||
API version: {{.Server.APIVersion}} (minimum version {{.Server.MinAPIVersion}})
|
||||
Go version: {{.Server.GoVersion}}
|
||||
Git commit: {{.Server.GitCommit}}
|
||||
Built: {{.Server.BuildTime}}
|
||||
OS/Arch: {{.Server.Os}}/{{.Server.Arch}}
|
||||
Experimental: {{.Server.Experimental}}{{end}}`
|
||||
{{- if .ServerOK}}{{with .Server}}
|
||||
|
||||
Server:{{if ne .Platform.Name ""}} {{.Platform.Name}}{{end}}
|
||||
{{- range $component := .Components}}
|
||||
{{$component.Name}}:
|
||||
{{- if eq $component.Name "Engine" }}
|
||||
Version: {{.Version}}
|
||||
API version: {{index .Details "ApiVersion"}} (minimum version {{index .Details "MinAPIVersion"}})
|
||||
Go version: {{index .Details "GoVersion"}}
|
||||
Git commit: {{index .Details "GitCommit"}}
|
||||
Built: {{index .Details "BuildTime"}}
|
||||
OS/Arch: {{index .Details "Os"}}/{{index .Details "Arch"}}
|
||||
Experimental: {{index .Details "Experimental"}}
|
||||
{{- else }}
|
||||
Version: {{$component.Version}}
|
||||
{{- $detailsOrder := getDetailsOrder $component}}
|
||||
{{- range $key := $detailsOrder}}
|
||||
{{$key}}: {{index $component.Details $key}}
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
{{- end}}{{end}}`
|
||||
|
||||
type versionOptions struct {
|
||||
format string
|
||||
@ -41,6 +59,8 @@ type versionInfo struct {
|
||||
}
|
||||
|
||||
type clientVersion struct {
|
||||
Platform struct{ Name string } `json:",omitempty"`
|
||||
|
||||
Version string
|
||||
APIVersion string `json:"ApiVersion"`
|
||||
DefaultAPIVersion string `json:"DefaultAPIVersion,omitempty"`
|
||||
@ -77,15 +97,27 @@ func NewVersionCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
func reformatDate(buildTime string) string {
|
||||
t, errTime := time.Parse(time.RFC3339Nano, buildTime)
|
||||
if errTime == nil {
|
||||
return t.Format(time.ANSIC)
|
||||
}
|
||||
return buildTime
|
||||
}
|
||||
|
||||
func runVersion(dockerCli *command.DockerCli, opts *versionOptions) error {
|
||||
ctx := context.Background()
|
||||
|
||||
templateFormat := versionTemplate
|
||||
tmpl := templates.New("version")
|
||||
if opts.format != "" {
|
||||
templateFormat = opts.format
|
||||
} else {
|
||||
tmpl = tmpl.Funcs(template.FuncMap{"getDetailsOrder": getDetailsOrder})
|
||||
}
|
||||
|
||||
tmpl, err := templates.Parse(templateFormat)
|
||||
var err error
|
||||
tmpl, err = tmpl.Parse(templateFormat)
|
||||
if err != nil {
|
||||
return cli.StatusError{StatusCode: 64,
|
||||
Status: "Template parsing error: " + err.Error()}
|
||||
@ -103,22 +135,41 @@ func runVersion(dockerCli *command.DockerCli, opts *versionOptions) error {
|
||||
Arch: runtime.GOARCH,
|
||||
},
|
||||
}
|
||||
|
||||
serverVersion, err := dockerCli.Client().ServerVersion(ctx)
|
||||
if err == nil {
|
||||
vd.Server = &serverVersion
|
||||
}
|
||||
vd.Client.Platform.Name = cli.PlatformName
|
||||
|
||||
// first we need to make BuildTime more human friendly
|
||||
t, errTime := time.Parse(time.RFC3339Nano, vd.Client.BuildTime)
|
||||
if errTime == nil {
|
||||
vd.Client.BuildTime = t.Format(time.ANSIC)
|
||||
}
|
||||
vd.Client.BuildTime = reformatDate(vd.Client.BuildTime)
|
||||
|
||||
if vd.ServerOK() {
|
||||
t, errTime = time.Parse(time.RFC3339Nano, vd.Server.BuildTime)
|
||||
if errTime == nil {
|
||||
vd.Server.BuildTime = t.Format(time.ANSIC)
|
||||
sv, err := dockerCli.Client().ServerVersion(ctx)
|
||||
if err == nil {
|
||||
vd.Server = &sv
|
||||
foundEngine := false
|
||||
for _, component := range sv.Components {
|
||||
if component.Name == "Engine" {
|
||||
foundEngine = true
|
||||
buildTime, ok := component.Details["BuildTime"]
|
||||
if ok {
|
||||
component.Details["BuildTime"] = reformatDate(buildTime)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !foundEngine {
|
||||
vd.Server.Components = append(vd.Server.Components, types.ComponentVersion{
|
||||
Name: "Engine",
|
||||
Version: sv.Version,
|
||||
Details: map[string]string{
|
||||
"ApiVersion": sv.APIVersion,
|
||||
"MinAPIVersion": sv.MinAPIVersion,
|
||||
"GitCommit": sv.GitCommit,
|
||||
"GoVersion": sv.GoVersion,
|
||||
"Os": sv.Os,
|
||||
"Arch": sv.Arch,
|
||||
"BuildTime": reformatDate(vd.Server.BuildTime),
|
||||
"Experimental": fmt.Sprintf("%t", sv.Experimental),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -128,3 +179,12 @@ func runVersion(dockerCli *command.DockerCli, opts *versionOptions) error {
|
||||
dockerCli.Out().Write([]byte{'\n'})
|
||||
return err
|
||||
}
|
||||
|
||||
func getDetailsOrder(v types.ComponentVersion) []string {
|
||||
out := make([]string, 0, len(v.Details))
|
||||
for k := range v.Details {
|
||||
out = append(out, k)
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
@ -192,7 +192,24 @@ func (e EmptyTargetsNotaryRepository) GetAllTargetMetadataByName(name string) ([
|
||||
}
|
||||
|
||||
func (e EmptyTargetsNotaryRepository) ListRoles() ([]client.RoleWithSignatures, error) {
|
||||
return []client.RoleWithSignatures{}, nil
|
||||
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 (e EmptyTargetsNotaryRepository) GetDelegationRoles() ([]data.Role, error) {
|
||||
|
||||
@ -20,6 +20,7 @@ func NewTrustCommand(dockerCli command.Cli) *cobra.Command {
|
||||
newSignCommand(dockerCli),
|
||||
newTrustKeyCommand(dockerCli),
|
||||
newTrustSignerCommand(dockerCli),
|
||||
newInspectCommand(dockerCli),
|
||||
)
|
||||
return cmd
|
||||
}
|
||||
|
||||
167
components/cli/cli/command/trust/common.go
Normal file
167
components/cli/cli/command/trust/common.go
Normal file
@ -0,0 +1,167 @@
|
||||
package trust
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/image"
|
||||
"github.com/docker/cli/cli/trust"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/theupdateframework/notary"
|
||||
"github.com/theupdateframework/notary/client"
|
||||
"github.com/theupdateframework/notary/tuf/data"
|
||||
)
|
||||
|
||||
// trustTagKey represents a unique signed tag and hex-encoded hash pair
|
||||
type trustTagKey struct {
|
||||
SignedTag string
|
||||
Digest 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].SignedTag < tagComparator[j].SignedTag
|
||||
}
|
||||
|
||||
func (tagComparator trustTagRowList) Swap(i, j int) {
|
||||
tagComparator[i], tagComparator[j] = tagComparator[j], tagComparator[i]
|
||||
}
|
||||
|
||||
// trustRepo represents consumable information about a trusted repository
|
||||
type trustRepo struct {
|
||||
Name string
|
||||
SignedTags trustTagRowList
|
||||
Signers []trustSigner
|
||||
AdminstrativeKeys []trustSigner
|
||||
}
|
||||
|
||||
// trustSigner represents a trusted signer in a trusted repository
|
||||
// a signer is defined by a name and list of trustKeys
|
||||
type trustSigner struct {
|
||||
Name string `json:",omitempty"`
|
||||
Keys []trustKey `json:",omitempty"`
|
||||
}
|
||||
|
||||
// trustKey contains information about trusted keys
|
||||
type trustKey struct {
|
||||
ID string `json:",omitempty"`
|
||||
}
|
||||
|
||||
// lookupTrustInfo returns processed signature and role information about a notary repository.
|
||||
// This information is to be pretty printed or serialized into a machine-readable format.
|
||||
func lookupTrustInfo(cli command.Cli, remote string) (trustTagRowList, []client.RoleWithSignatures, []data.Role, error) {
|
||||
ctx := context.Background()
|
||||
imgRefAndAuth, err := trust.GetImageReferencesAndAuth(ctx, image.AuthResolver(cli), remote)
|
||||
if err != nil {
|
||||
return trustTagRowList{}, []client.RoleWithSignatures{}, []data.Role{}, err
|
||||
}
|
||||
tag := imgRefAndAuth.Tag()
|
||||
notaryRepo, err := cli.NotaryClient(imgRefAndAuth, trust.ActionsPullOnly)
|
||||
if err != nil {
|
||||
return trustTagRowList{}, []client.RoleWithSignatures{}, []data.Role{}, trust.NotaryError(imgRefAndAuth.Reference().Name(), err)
|
||||
}
|
||||
|
||||
if err = clearChangeList(notaryRepo); err != nil {
|
||||
return trustTagRowList{}, []client.RoleWithSignatures{}, []data.Role{}, 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(remote, 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 trustTagRowList{}, []client.RoleWithSignatures{}, []data.Role{}, fmt.Errorf("No signatures or cannot access %s", remote)
|
||||
}
|
||||
}
|
||||
signatureRows := matchReleasedSignatures(allSignedTargets)
|
||||
|
||||
// get the administrative roles
|
||||
adminRolesWithSigs, err := notaryRepo.ListRoles()
|
||||
if err != nil {
|
||||
return trustTagRowList{}, []client.RoleWithSignatures{}, []data.Role{}, 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)
|
||||
}
|
||||
|
||||
return signatureRows, adminRolesWithSigs, delegationRoles, nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
83
components/cli/cli/command/trust/inspect.go
Normal file
83
components/cli/cli/command/trust/inspect.go
Normal file
@ -0,0 +1,83 @@
|
||||
package trust
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"sort"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/inspect"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/theupdateframework/notary/tuf/data"
|
||||
)
|
||||
|
||||
func newInspectCommand(dockerCli command.Cli) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "inspect IMAGE[:TAG] [IMAGE[:TAG]...]",
|
||||
Short: "Return low-level information about keys and signatures",
|
||||
Args: cli.RequiresMinArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runInspect(dockerCli, args)
|
||||
},
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runInspect(dockerCli command.Cli, remotes []string) error {
|
||||
getRefFunc := func(ref string) (interface{}, []byte, error) {
|
||||
i, err := getRepoTrustInfo(dockerCli, ref)
|
||||
return nil, i, err
|
||||
}
|
||||
return inspect.Inspect(dockerCli.Out(), remotes, "", getRefFunc)
|
||||
}
|
||||
|
||||
func getRepoTrustInfo(cli command.Cli, remote string) ([]byte, error) {
|
||||
signatureRows, adminRolesWithSigs, delegationRoles, err := lookupTrustInfo(cli, remote)
|
||||
if err != nil {
|
||||
return []byte{}, err
|
||||
}
|
||||
// process the signatures to include repo admin if signed by the base targets role
|
||||
for idx, sig := range signatureRows {
|
||||
if len(sig.Signers) == 0 {
|
||||
signatureRows[idx].Signers = append(sig.Signers, releasedRoleName)
|
||||
}
|
||||
}
|
||||
|
||||
signerList, adminList := []trustSigner{}, []trustSigner{}
|
||||
|
||||
signerRoleToKeyIDs := getDelegationRoleToKeyMap(delegationRoles)
|
||||
|
||||
for signerName, signerKeys := range signerRoleToKeyIDs {
|
||||
signerKeyList := []trustKey{}
|
||||
for _, keyID := range signerKeys {
|
||||
signerKeyList = append(signerKeyList, trustKey{ID: keyID})
|
||||
}
|
||||
signerList = append(signerList, trustSigner{signerName, signerKeyList})
|
||||
}
|
||||
sort.Slice(signerList, func(i, j int) bool { return signerList[i].Name > signerList[j].Name })
|
||||
|
||||
for _, adminRole := range adminRolesWithSigs {
|
||||
switch adminRole.Name {
|
||||
case data.CanonicalRootRole:
|
||||
rootKeys := []trustKey{}
|
||||
for _, keyID := range adminRole.KeyIDs {
|
||||
rootKeys = append(rootKeys, trustKey{ID: keyID})
|
||||
}
|
||||
adminList = append(adminList, trustSigner{"Root", rootKeys})
|
||||
case data.CanonicalTargetsRole:
|
||||
targetKeys := []trustKey{}
|
||||
for _, keyID := range adminRole.KeyIDs {
|
||||
targetKeys = append(targetKeys, trustKey{ID: keyID})
|
||||
}
|
||||
adminList = append(adminList, trustSigner{"Repository", targetKeys})
|
||||
}
|
||||
}
|
||||
sort.Slice(adminList, func(i, j int) bool { return adminList[i].Name > adminList[j].Name })
|
||||
|
||||
return json.Marshal(trustRepo{
|
||||
Name: remote,
|
||||
SignedTags: signatureRows,
|
||||
Signers: signerList,
|
||||
AdminstrativeKeys: adminList,
|
||||
})
|
||||
}
|
||||
135
components/cli/cli/command/trust/inspect_test.go
Normal file
135
components/cli/cli/command/trust/inspect_test.go
Normal file
@ -0,0 +1,135 @@
|
||||
package trust
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/cli/internal/test/testutil"
|
||||
"github.com/gotestyourself/gotestyourself/golden"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
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 := newInspectCommand(cli)
|
||||
cmd.SetArgs([]string{"nonexistent-reg-name.io/image"})
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
testutil.ErrorContains(t, cmd.Execute(), "No signatures or cannot access nonexistent-reg-name.io/image")
|
||||
|
||||
cli = test.NewFakeCli(&fakeClient{})
|
||||
cli.SetNotaryClient(getOfflineNotaryRepository)
|
||||
cmd = newInspectCommand(cli)
|
||||
cmd.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 := newInspectCommand(cli)
|
||||
cmd.SetArgs([]string{"reg/unsigned-img"})
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
testutil.ErrorContains(t, cmd.Execute(), "No signatures or cannot access reg/unsigned-img")
|
||||
golden.Assert(t, cli.OutBuffer().String(), "trust-inspect-uninitialized.golden")
|
||||
|
||||
cli = test.NewFakeCli(&fakeClient{})
|
||||
cli.SetNotaryClient(getUninitializedNotaryRepository)
|
||||
cmd = newInspectCommand(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")
|
||||
golden.Assert(t, cli.OutBuffer().String(), "trust-inspect-uninitialized.golden")
|
||||
}
|
||||
|
||||
func TestTrustInspectCommandEmptyNotaryRepo(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{})
|
||||
cli.SetNotaryClient(getEmptyTargetsNotaryRepository)
|
||||
cmd := newInspectCommand(cli)
|
||||
cmd.SetArgs([]string{"reg/img:unsigned-tag"})
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
assert.NoError(t, cmd.Execute())
|
||||
golden.Assert(t, cli.OutBuffer().String(), "trust-inspect-empty-repo.golden")
|
||||
}
|
||||
|
||||
func TestTrustInspectCommandFullRepoWithoutSigners(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{})
|
||||
cli.SetNotaryClient(getLoadedWithNoSignersNotaryRepository)
|
||||
cmd := newInspectCommand(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 := newInspectCommand(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 := newInspectCommand(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 TestTrustInspectCommandMultipleFullReposWithSigners(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{})
|
||||
cli.SetNotaryClient(getLoadedNotaryRepository)
|
||||
cmd := newInspectCommand(cli)
|
||||
cmd.SetArgs([]string{"signed-repo", "signed-repo"})
|
||||
assert.NoError(t, cmd.Execute())
|
||||
golden.Assert(t, cli.OutBuffer().String(), "trust-inspect-multiple-repos-with-signers.golden")
|
||||
}
|
||||
|
||||
func TestTrustInspectCommandUnsignedTagInSignedRepo(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{})
|
||||
cli.SetNotaryClient(getLoadedNotaryRepository)
|
||||
cmd := newInspectCommand(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")
|
||||
}
|
||||
@ -18,19 +18,29 @@ import (
|
||||
"github.com/theupdateframework/notary/tuf/data"
|
||||
)
|
||||
|
||||
type signOptions struct {
|
||||
local bool
|
||||
imageName string
|
||||
}
|
||||
|
||||
func newSignCommand(dockerCli command.Cli) *cobra.Command {
|
||||
options := signOptions{}
|
||||
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])
|
||||
options.imageName = args[0]
|
||||
return runSignImage(dockerCli, options)
|
||||
},
|
||||
}
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVar(&options.local, "local", false, "Sign a locally tagged image")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runSignImage(cli command.Cli, imageName string) error {
|
||||
func runSignImage(cli command.Cli, options signOptions) error {
|
||||
imageName := options.imageName
|
||||
ctx := context.Background()
|
||||
imgRefAndAuth, err := trust.GetImageReferencesAndAuth(ctx, image.AuthResolver(cli), imageName)
|
||||
if err != nil {
|
||||
@ -71,13 +81,15 @@ func runSignImage(cli command.Cli, imageName string) error {
|
||||
}
|
||||
requestPrivilege := command.RegistryAuthenticationPrivilegedFunc(cli, imgRefAndAuth.RepoInfo().Index, "push")
|
||||
target, err := createTarget(notaryRepo, imgRefAndAuth.Tag())
|
||||
if err != nil {
|
||||
if err != nil || options.local {
|
||||
switch err := err.(type) {
|
||||
case client.ErrNoSuchTarget, client.ErrRepositoryNotExist:
|
||||
// If the error is nil then the local flag is set
|
||||
case client.ErrNoSuchTarget, client.ErrRepositoryNotExist, nil:
|
||||
// Fail fast if the image doesn't exist locally
|
||||
if err := checkLocalImageExistence(ctx, cli, imageName); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(cli.Err(), "Signing and pushing trust data for local image %s, may overwrite remote trust data\n", imageName)
|
||||
return image.TrustedPush(ctx, cli, imgRefAndAuth.RepoInfo(), imgRefAndAuth.Reference(), *imgRefAndAuth.AuthConfig(), requestPrivilege)
|
||||
default:
|
||||
return err
|
||||
@ -164,7 +176,7 @@ func getExistingSignatureInfoForReleasedTag(notaryRepo client.Repository, tag st
|
||||
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)
|
||||
fmt.Fprintf(out, "Existing signatures for tag %s digest %s from:\n%s\n", existingSigInfo.SignedTag, existingSigInfo.Digest, joinedSigners)
|
||||
}
|
||||
|
||||
func initNotaryRepoWithSigners(notaryRepo client.Repository, newSigner data.RoleName) error {
|
||||
|
||||
@ -1,13 +1,12 @@
|
||||
package trust
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"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"
|
||||
@ -296,3 +295,13 @@ func TestSignCommandChangeListIsCleanedOnError(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, len(cl.List()), 0)
|
||||
}
|
||||
|
||||
func TestSignCommandLocalFlag(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{})
|
||||
cli.SetNotaryClient(getEmptyTargetsNotaryRepository)
|
||||
cmd := newSignCommand(cli)
|
||||
cmd.SetArgs([]string{"--local", "reg-name.io/image:red"})
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
testutil.ErrorContains(t, cmd.Execute(), "error during connect: Get /images/reg-name.io/image:red/json: unsupported protocol scheme")
|
||||
|
||||
}
|
||||
|
||||
25
components/cli/cli/command/trust/testdata/trust-inspect-empty-repo.golden
vendored
Normal file
25
components/cli/cli/command/trust/testdata/trust-inspect-empty-repo.golden
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
[
|
||||
{
|
||||
"Name": "reg/img:unsigned-tag",
|
||||
"SignedTags": [],
|
||||
"Signers": [],
|
||||
"AdminstrativeKeys": [
|
||||
{
|
||||
"Name": "Root",
|
||||
"Keys": [
|
||||
{
|
||||
"ID": "rootID"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "Repository",
|
||||
"Keys": [
|
||||
{
|
||||
"ID": "targetsID"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@ -1,6 +1,33 @@
|
||||
SIGNED TAG DIGEST SIGNERS
|
||||
green 677265656e2d646967657374 (Repo Admin)
|
||||
|
||||
Administrative keys for signed-repo:
|
||||
Repository Key: targetsID
|
||||
Root Key: rootID
|
||||
[
|
||||
{
|
||||
"Name": "signed-repo",
|
||||
"SignedTags": [
|
||||
{
|
||||
"SignedTag": "green",
|
||||
"Digest": "677265656e2d646967657374",
|
||||
"Signers": [
|
||||
"Repo Admin"
|
||||
]
|
||||
}
|
||||
],
|
||||
"Signers": [],
|
||||
"AdminstrativeKeys": [
|
||||
{
|
||||
"Name": "Root",
|
||||
"Keys": [
|
||||
{
|
||||
"ID": "rootID"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "Repository",
|
||||
"Keys": [
|
||||
{
|
||||
"ID": "targetsID"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@ -1,14 +1,65 @@
|
||||
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
|
||||
[
|
||||
{
|
||||
"Name": "signed-repo",
|
||||
"SignedTags": [
|
||||
{
|
||||
"SignedTag": "blue",
|
||||
"Digest": "626c75652d646967657374",
|
||||
"Signers": [
|
||||
"alice"
|
||||
]
|
||||
},
|
||||
{
|
||||
"SignedTag": "green",
|
||||
"Digest": "677265656e2d646967657374",
|
||||
"Signers": [
|
||||
"Repo Admin"
|
||||
]
|
||||
},
|
||||
{
|
||||
"SignedTag": "red",
|
||||
"Digest": "7265642d646967657374",
|
||||
"Signers": [
|
||||
"alice",
|
||||
"bob"
|
||||
]
|
||||
}
|
||||
],
|
||||
"Signers": [
|
||||
{
|
||||
"Name": "bob",
|
||||
"Keys": [
|
||||
{
|
||||
"ID": "B"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "alice",
|
||||
"Keys": [
|
||||
{
|
||||
"ID": "A"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"AdminstrativeKeys": [
|
||||
{
|
||||
"Name": "Root",
|
||||
"Keys": [
|
||||
{
|
||||
"ID": "rootID"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "Repository",
|
||||
"Keys": [
|
||||
{
|
||||
"ID": "targetsID"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
128
components/cli/cli/command/trust/testdata/trust-inspect-multiple-repos-with-signers.golden
vendored
Normal file
128
components/cli/cli/command/trust/testdata/trust-inspect-multiple-repos-with-signers.golden
vendored
Normal file
@ -0,0 +1,128 @@
|
||||
[
|
||||
{
|
||||
"Name": "signed-repo",
|
||||
"SignedTags": [
|
||||
{
|
||||
"SignedTag": "blue",
|
||||
"Digest": "626c75652d646967657374",
|
||||
"Signers": [
|
||||
"alice"
|
||||
]
|
||||
},
|
||||
{
|
||||
"SignedTag": "green",
|
||||
"Digest": "677265656e2d646967657374",
|
||||
"Signers": [
|
||||
"Repo Admin"
|
||||
]
|
||||
},
|
||||
{
|
||||
"SignedTag": "red",
|
||||
"Digest": "7265642d646967657374",
|
||||
"Signers": [
|
||||
"alice",
|
||||
"bob"
|
||||
]
|
||||
}
|
||||
],
|
||||
"Signers": [
|
||||
{
|
||||
"Name": "bob",
|
||||
"Keys": [
|
||||
{
|
||||
"ID": "B"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "alice",
|
||||
"Keys": [
|
||||
{
|
||||
"ID": "A"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"AdminstrativeKeys": [
|
||||
{
|
||||
"Name": "Root",
|
||||
"Keys": [
|
||||
{
|
||||
"ID": "rootID"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "Repository",
|
||||
"Keys": [
|
||||
{
|
||||
"ID": "targetsID"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "signed-repo",
|
||||
"SignedTags": [
|
||||
{
|
||||
"SignedTag": "blue",
|
||||
"Digest": "626c75652d646967657374",
|
||||
"Signers": [
|
||||
"alice"
|
||||
]
|
||||
},
|
||||
{
|
||||
"SignedTag": "green",
|
||||
"Digest": "677265656e2d646967657374",
|
||||
"Signers": [
|
||||
"Repo Admin"
|
||||
]
|
||||
},
|
||||
{
|
||||
"SignedTag": "red",
|
||||
"Digest": "7265642d646967657374",
|
||||
"Signers": [
|
||||
"alice",
|
||||
"bob"
|
||||
]
|
||||
}
|
||||
],
|
||||
"Signers": [
|
||||
{
|
||||
"Name": "bob",
|
||||
"Keys": [
|
||||
{
|
||||
"ID": "B"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "alice",
|
||||
"Keys": [
|
||||
{
|
||||
"ID": "A"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"AdminstrativeKeys": [
|
||||
{
|
||||
"Name": "Root",
|
||||
"Keys": [
|
||||
{
|
||||
"ID": "rootID"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "Repository",
|
||||
"Keys": [
|
||||
{
|
||||
"ID": "targetsID"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@ -1,6 +1,33 @@
|
||||
SIGNED TAG DIGEST SIGNERS
|
||||
green 677265656e2d646967657374 (Repo Admin)
|
||||
|
||||
Administrative keys for signed-repo:
|
||||
Repository Key: targetsID
|
||||
Root Key: rootID
|
||||
[
|
||||
{
|
||||
"Name": "signed-repo:green",
|
||||
"SignedTags": [
|
||||
{
|
||||
"SignedTag": "green",
|
||||
"Digest": "677265656e2d646967657374",
|
||||
"Signers": [
|
||||
"Repo Admin"
|
||||
]
|
||||
}
|
||||
],
|
||||
"Signers": [],
|
||||
"AdminstrativeKeys": [
|
||||
{
|
||||
"Name": "Root",
|
||||
"Keys": [
|
||||
{
|
||||
"ID": "rootID"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "Repository",
|
||||
"Keys": [
|
||||
{
|
||||
"ID": "targetsID"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
1
components/cli/cli/command/trust/testdata/trust-inspect-uninitialized.golden
vendored
Normal file
1
components/cli/cli/command/trust/testdata/trust-inspect-uninitialized.golden
vendored
Normal file
@ -0,0 +1 @@
|
||||
[]
|
||||
@ -1,13 +1,42 @@
|
||||
|
||||
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
|
||||
[
|
||||
{
|
||||
"Name": "signed-repo:unsigned",
|
||||
"SignedTags": [],
|
||||
"Signers": [
|
||||
{
|
||||
"Name": "bob",
|
||||
"Keys": [
|
||||
{
|
||||
"ID": "B"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "alice",
|
||||
"Keys": [
|
||||
{
|
||||
"ID": "A"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"AdminstrativeKeys": [
|
||||
{
|
||||
"Name": "Root",
|
||||
"Keys": [
|
||||
{
|
||||
"ID": "rootID"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "Repository",
|
||||
"Keys": [
|
||||
{
|
||||
"ID": "targetsID"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
6
components/cli/cli/command/trust/testdata/trust-view-full-repo-no-signers.golden
vendored
Normal file
6
components/cli/cli/command/trust/testdata/trust-view-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-view-full-repo-with-signers.golden
vendored
Normal file
14
components/cli/cli/command/trust/testdata/trust-view-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-view-one-tag-no-signers.golden
vendored
Normal file
6
components/cli/cli/command/trust/testdata/trust-view-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-view-unsigned-tag-with-signers.golden
vendored
Normal file
13
components/cli/cli/command/trust/testdata/trust-view-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
|
||||
@ -1,8 +1,6 @@
|
||||
package trust
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
@ -11,80 +9,28 @@ import (
|
||||
"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/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/theupdateframework/notary"
|
||||
"github.com/theupdateframework/notary/client"
|
||||
"github.com/theupdateframework/notary/tuf/data"
|
||||
)
|
||||
|
||||
// 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 viewTrustInfo(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)
|
||||
func viewTrustInfo(cli command.Cli, remote string) error {
|
||||
signatureRows, adminRolesWithSigs, delegationRoles, err := lookupTrustInfo(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
|
||||
@ -92,18 +38,6 @@ func lookupTrustInfo(cli command.Cli, remote string) error {
|
||||
} 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
|
||||
@ -117,7 +51,6 @@ func lookupTrustInfo(cli command.Cli, remote string) error {
|
||||
// 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
|
||||
}
|
||||
|
||||
@ -128,65 +61,6 @@ func printSortedAdminKeys(out io.Writer, adminRoles []client.RoleWithSignatures)
|
||||
}
|
||||
}
|
||||
|
||||
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{
|
||||
@ -201,8 +75,8 @@ func printSignatures(out io.Writer, signatureRows trustTagRowList) error {
|
||||
formattedSigners = append(formattedSigners, fmt.Sprintf("(%s)", releasedRoleName))
|
||||
}
|
||||
formattedTags = append(formattedTags, formatter.SignedTagInfo{
|
||||
Name: sigRow.TagName,
|
||||
Digest: sigRow.HashHex,
|
||||
Name: sigRow.SignedTag,
|
||||
Digest: sigRow.Digest,
|
||||
Signers: formattedSigners,
|
||||
})
|
||||
}
|
||||
|
||||
@ -20,7 +20,7 @@ type fakeClient struct {
|
||||
dockerClient.Client
|
||||
}
|
||||
|
||||
func TestTrustInspectCommandErrors(t *testing.T) {
|
||||
func TestTrustViewCommandErrors(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
args []string
|
||||
@ -55,7 +55,7 @@ func TestTrustInspectCommandErrors(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrustInspectCommandOfflineErrors(t *testing.T) {
|
||||
func TestTrustViewCommandOfflineErrors(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{})
|
||||
cli.SetNotaryClient(getOfflineNotaryRepository)
|
||||
cmd := newViewCommand(cli)
|
||||
@ -71,7 +71,7 @@ func TestTrustInspectCommandOfflineErrors(t *testing.T) {
|
||||
testutil.ErrorContains(t, cmd.Execute(), "No signatures or cannot access nonexistent-reg-name.io/image")
|
||||
}
|
||||
|
||||
func TestTrustInspectCommandUninitializedErrors(t *testing.T) {
|
||||
func TestTrustViewCommandUninitializedErrors(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{})
|
||||
cli.SetNotaryClient(getUninitializedNotaryRepository)
|
||||
cmd := newViewCommand(cli)
|
||||
@ -87,7 +87,7 @@ func TestTrustInspectCommandUninitializedErrors(t *testing.T) {
|
||||
testutil.ErrorContains(t, cmd.Execute(), "No signatures or cannot access reg/unsigned-img:tag")
|
||||
}
|
||||
|
||||
func TestTrustInspectCommandEmptyNotaryRepoErrors(t *testing.T) {
|
||||
func TestTrustViewCommandEmptyNotaryRepoErrors(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{})
|
||||
cli.SetNotaryClient(getEmptyTargetsNotaryRepository)
|
||||
cmd := newViewCommand(cli)
|
||||
@ -107,44 +107,44 @@ func TestTrustInspectCommandEmptyNotaryRepoErrors(t *testing.T) {
|
||||
assert.Contains(t, cli.OutBuffer().String(), "Administrative keys for reg/img:")
|
||||
}
|
||||
|
||||
func TestTrustInspectCommandFullRepoWithoutSigners(t *testing.T) {
|
||||
func TestTrustViewCommandFullRepoWithoutSigners(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")
|
||||
golden.Assert(t, cli.OutBuffer().String(), "trust-view-full-repo-no-signers.golden")
|
||||
}
|
||||
|
||||
func TestTrustInspectCommandOneTagWithoutSigners(t *testing.T) {
|
||||
func TestTrustViewCommandOneTagWithoutSigners(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")
|
||||
golden.Assert(t, cli.OutBuffer().String(), "trust-view-one-tag-no-signers.golden")
|
||||
}
|
||||
|
||||
func TestTrustInspectCommandFullRepoWithSigners(t *testing.T) {
|
||||
func TestTrustViewCommandFullRepoWithSigners(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")
|
||||
golden.Assert(t, cli.OutBuffer().String(), "trust-view-full-repo-with-signers.golden")
|
||||
}
|
||||
|
||||
func TestTrustInspectCommandUnsignedTagInSignedRepo(t *testing.T) {
|
||||
func TestTrustViewCommandUnsignedTagInSignedRepo(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")
|
||||
golden.Assert(t, cli.OutBuffer().String(), "trust-view-unsigned-tag-with-signers.golden")
|
||||
}
|
||||
|
||||
func TestNotaryRoleToSigner(t *testing.T) {
|
||||
@ -224,8 +224,8 @@ func TestMatchOneReleasedSingleSignature(t *testing.T) {
|
||||
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)
|
||||
assert.Equal(t, releasedTgt.Name, outputRow.SignedTag)
|
||||
assert.Equal(t, hex.EncodeToString(releasedTgt.Hashes[notary.SHA256]), outputRow.Digest)
|
||||
}
|
||||
|
||||
func TestMatchOneReleasedMultiSignature(t *testing.T) {
|
||||
@ -249,8 +249,8 @@ func TestMatchOneReleasedMultiSignature(t *testing.T) {
|
||||
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)
|
||||
assert.Equal(t, releasedTgt.Name, outputRow.SignedTag)
|
||||
assert.Equal(t, hex.EncodeToString(releasedTgt.Hashes[notary.SHA256]), outputRow.Digest)
|
||||
}
|
||||
|
||||
func TestMatchMultiReleasedMultiSignature(t *testing.T) {
|
||||
@ -288,18 +288,18 @@ func TestMatchMultiReleasedMultiSignature(t *testing.T) {
|
||||
// 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)
|
||||
assert.Equal(t, targetA.Name, outputTargetA.SignedTag)
|
||||
assert.Equal(t, hex.EncodeToString(targetA.Hashes[notary.SHA256]), outputTargetA.Digest)
|
||||
|
||||
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)
|
||||
assert.Equal(t, targetB.Name, outputTargetB.SignedTag)
|
||||
assert.Equal(t, hex.EncodeToString(targetB.Hashes[notary.SHA256]), outputTargetB.Digest)
|
||||
|
||||
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)
|
||||
assert.Equal(t, targetC.Name, outputTargetC.SignedTag)
|
||||
assert.Equal(t, hex.EncodeToString(targetC.Hashes[notary.SHA256]), outputTargetC.Digest)
|
||||
}
|
||||
|
||||
func TestMatchReleasedSignatureFromTargets(t *testing.T) {
|
||||
@ -313,8 +313,8 @@ func TestMatchReleasedSignatureFromTargets(t *testing.T) {
|
||||
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)
|
||||
assert.Equal(t, releasedTgt.Name, outputRow.SignedTag)
|
||||
assert.Equal(t, hex.EncodeToString(releasedTgt.Hashes[notary.SHA256]), outputRow.Digest)
|
||||
}
|
||||
|
||||
func TestGetSignerRolesWithKeyIDs(t *testing.T) {
|
||||
|
||||
@ -11,6 +11,7 @@ import (
|
||||
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/pkg/system"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
// CopyToFile writes the content of the reader to the specified file
|
||||
@ -117,3 +118,10 @@ func PruneFilters(dockerCli Cli, pruneFilters filters.Args) filters.Args {
|
||||
|
||||
return pruneFilters
|
||||
}
|
||||
|
||||
// AddPlatformFlag adds `platform` to a set of flags for API version 1.32 and later.
|
||||
func AddPlatformFlag(flags *pflag.FlagSet, target *string) {
|
||||
flags.StringVar(target, "platform", os.Getenv("DOCKER_DEFAULT_PLATFORM"), "Set platform if server is multi-platform capable")
|
||||
flags.SetAnnotation("platform", "version", []string{"1.32"})
|
||||
flags.SetAnnotation("platform", "experimental", nil)
|
||||
}
|
||||
|
||||
@ -101,18 +101,11 @@ func Secrets(namespace Namespace, secrets map[string]composetypes.SecretConfig)
|
||||
continue
|
||||
}
|
||||
|
||||
data, err := ioutil.ReadFile(secret.File)
|
||||
obj, err := fileObjectConfig(namespace, name, composetypes.FileObjectConfig(secret))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result = append(result, swarm.SecretSpec{
|
||||
Annotations: swarm.Annotations{
|
||||
Name: namespace.Scope(name),
|
||||
Labels: AddStackLabel(namespace, secret.Labels),
|
||||
},
|
||||
Data: data,
|
||||
})
|
||||
result = append(result, swarm.SecretSpec{Annotations: obj.Annotations, Data: obj.Data})
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
@ -125,18 +118,37 @@ func Configs(namespace Namespace, configs map[string]composetypes.ConfigObjConfi
|
||||
continue
|
||||
}
|
||||
|
||||
data, err := ioutil.ReadFile(config.File)
|
||||
obj, err := fileObjectConfig(namespace, name, composetypes.FileObjectConfig(config))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result = append(result, swarm.ConfigSpec{
|
||||
Annotations: swarm.Annotations{
|
||||
Name: namespace.Scope(name),
|
||||
Labels: AddStackLabel(namespace, config.Labels),
|
||||
},
|
||||
Data: data,
|
||||
})
|
||||
result = append(result, swarm.ConfigSpec{Annotations: obj.Annotations, Data: obj.Data})
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type swarmFileObject struct {
|
||||
Annotations swarm.Annotations
|
||||
Data []byte
|
||||
}
|
||||
|
||||
func fileObjectConfig(namespace Namespace, name string, obj composetypes.FileObjectConfig) (swarmFileObject, error) {
|
||||
data, err := ioutil.ReadFile(obj.File)
|
||||
if err != nil {
|
||||
return swarmFileObject{}, err
|
||||
}
|
||||
|
||||
if obj.Name != "" {
|
||||
name = obj.Name
|
||||
} else {
|
||||
name = namespace.Scope(name)
|
||||
}
|
||||
|
||||
return swarmFileObject{
|
||||
Annotations: swarm.Annotations{
|
||||
Name: name,
|
||||
Labels: AddStackLabel(namespace, obj.Labels),
|
||||
},
|
||||
Data: data,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@ -149,6 +149,7 @@ func Service(
|
||||
Configs: configs,
|
||||
ReadOnly: service.ReadOnly,
|
||||
Privileges: &privileges,
|
||||
Isolation: container.Isolation(service.Isolation),
|
||||
},
|
||||
LogDriver: logDriver,
|
||||
Resources: resources,
|
||||
@ -255,43 +256,24 @@ func convertServiceSecrets(
|
||||
secretSpecs map[string]composetypes.SecretConfig,
|
||||
) ([]*swarm.SecretReference, error) {
|
||||
refs := []*swarm.SecretReference{}
|
||||
for _, secret := range secrets {
|
||||
target := secret.Target
|
||||
if target == "" {
|
||||
target = secret.Source
|
||||
}
|
||||
|
||||
secretSpec, exists := secretSpecs[secret.Source]
|
||||
lookup := func(key string) (composetypes.FileObjectConfig, error) {
|
||||
secretSpec, exists := secretSpecs[key]
|
||||
if !exists {
|
||||
return nil, errors.Errorf("undefined secret %q", secret.Source)
|
||||
}
|
||||
|
||||
source := namespace.Scope(secret.Source)
|
||||
if secretSpec.External.External {
|
||||
source = secretSpec.External.Name
|
||||
}
|
||||
|
||||
uid := secret.UID
|
||||
gid := secret.GID
|
||||
if uid == "" {
|
||||
uid = "0"
|
||||
}
|
||||
if gid == "" {
|
||||
gid = "0"
|
||||
}
|
||||
mode := secret.Mode
|
||||
if mode == nil {
|
||||
mode = uint32Ptr(0444)
|
||||
return composetypes.FileObjectConfig{}, errors.Errorf("undefined secret %q", key)
|
||||
}
|
||||
return composetypes.FileObjectConfig(secretSpec), nil
|
||||
}
|
||||
for _, secret := range secrets {
|
||||
obj, err := convertFileObject(namespace, composetypes.FileReferenceConfig(secret), lookup)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
file := swarm.SecretReferenceFileTarget(obj.File)
|
||||
refs = append(refs, &swarm.SecretReference{
|
||||
File: &swarm.SecretReferenceFileTarget{
|
||||
Name: target,
|
||||
UID: uid,
|
||||
GID: gid,
|
||||
Mode: os.FileMode(*mode),
|
||||
},
|
||||
SecretName: source,
|
||||
File: &file,
|
||||
SecretName: obj.Name,
|
||||
})
|
||||
}
|
||||
|
||||
@ -312,43 +294,24 @@ func convertServiceConfigObjs(
|
||||
configSpecs map[string]composetypes.ConfigObjConfig,
|
||||
) ([]*swarm.ConfigReference, error) {
|
||||
refs := []*swarm.ConfigReference{}
|
||||
for _, config := range configs {
|
||||
target := config.Target
|
||||
if target == "" {
|
||||
target = config.Source
|
||||
}
|
||||
|
||||
configSpec, exists := configSpecs[config.Source]
|
||||
lookup := func(key string) (composetypes.FileObjectConfig, error) {
|
||||
configSpec, exists := configSpecs[key]
|
||||
if !exists {
|
||||
return nil, errors.Errorf("undefined config %q", config.Source)
|
||||
}
|
||||
|
||||
source := namespace.Scope(config.Source)
|
||||
if configSpec.External.External {
|
||||
source = configSpec.External.Name
|
||||
}
|
||||
|
||||
uid := config.UID
|
||||
gid := config.GID
|
||||
if uid == "" {
|
||||
uid = "0"
|
||||
}
|
||||
if gid == "" {
|
||||
gid = "0"
|
||||
}
|
||||
mode := config.Mode
|
||||
if mode == nil {
|
||||
mode = uint32Ptr(0444)
|
||||
return composetypes.FileObjectConfig{}, errors.Errorf("undefined config %q", key)
|
||||
}
|
||||
return composetypes.FileObjectConfig(configSpec), nil
|
||||
}
|
||||
for _, config := range configs {
|
||||
obj, err := convertFileObject(namespace, composetypes.FileReferenceConfig(config), lookup)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
file := swarm.ConfigReferenceFileTarget(obj.File)
|
||||
refs = append(refs, &swarm.ConfigReference{
|
||||
File: &swarm.ConfigReferenceFileTarget{
|
||||
Name: target,
|
||||
UID: uid,
|
||||
GID: gid,
|
||||
Mode: os.FileMode(*mode),
|
||||
},
|
||||
ConfigName: source,
|
||||
File: &file,
|
||||
ConfigName: obj.Name,
|
||||
})
|
||||
}
|
||||
|
||||
@ -361,6 +324,63 @@ func convertServiceConfigObjs(
|
||||
return confs, err
|
||||
}
|
||||
|
||||
type swarmReferenceTarget struct {
|
||||
Name string
|
||||
UID string
|
||||
GID string
|
||||
Mode os.FileMode
|
||||
}
|
||||
|
||||
type swarmReferenceObject struct {
|
||||
File swarmReferenceTarget
|
||||
ID string
|
||||
Name string
|
||||
}
|
||||
|
||||
func convertFileObject(
|
||||
namespace Namespace,
|
||||
config composetypes.FileReferenceConfig,
|
||||
lookup func(key string) (composetypes.FileObjectConfig, error),
|
||||
) (swarmReferenceObject, error) {
|
||||
target := config.Target
|
||||
if target == "" {
|
||||
target = config.Source
|
||||
}
|
||||
|
||||
obj, err := lookup(config.Source)
|
||||
if err != nil {
|
||||
return swarmReferenceObject{}, err
|
||||
}
|
||||
|
||||
source := namespace.Scope(config.Source)
|
||||
if obj.Name != "" {
|
||||
source = obj.Name
|
||||
}
|
||||
|
||||
uid := config.UID
|
||||
gid := config.GID
|
||||
if uid == "" {
|
||||
uid = "0"
|
||||
}
|
||||
if gid == "" {
|
||||
gid = "0"
|
||||
}
|
||||
mode := config.Mode
|
||||
if mode == nil {
|
||||
mode = uint32Ptr(0444)
|
||||
}
|
||||
|
||||
return swarmReferenceObject{
|
||||
File: swarmReferenceTarget{
|
||||
Name: target,
|
||||
UID: uid,
|
||||
GID: gid,
|
||||
Mode: os.FileMode(*mode),
|
||||
},
|
||||
Name: source,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func uint32Ptr(value uint32) *uint32 {
|
||||
return &value
|
||||
}
|
||||
@ -490,9 +510,25 @@ func convertResources(source composetypes.Resources) (*swarm.ResourceRequirement
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var generic []swarm.GenericResource
|
||||
for _, res := range source.Reservations.GenericResources {
|
||||
var r swarm.GenericResource
|
||||
|
||||
if res.DiscreteResourceSpec != nil {
|
||||
r.DiscreteResourceSpec = &swarm.DiscreteGenericResource{
|
||||
Kind: res.DiscreteResourceSpec.Kind,
|
||||
Value: res.DiscreteResourceSpec.Value,
|
||||
}
|
||||
}
|
||||
|
||||
generic = append(generic, r)
|
||||
}
|
||||
|
||||
resources.Reservations = &swarm.Resources{
|
||||
NanoCPUs: cpus,
|
||||
MemoryBytes: int64(source.Reservations.MemoryBytes),
|
||||
NanoCPUs: cpus,
|
||||
MemoryBytes: int64(source.Reservations.MemoryBytes),
|
||||
GenericResources: generic,
|
||||
}
|
||||
}
|
||||
return resources, nil
|
||||
|
||||
@ -1,15 +1,21 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
composetypes "github.com/docker/cli/cli/compose/types"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func TestConvertRestartPolicyFromNone(t *testing.T) {
|
||||
@ -372,3 +378,179 @@ func TestConvertUpdateConfigOrder(t *testing.T) {
|
||||
})
|
||||
assert.Equal(t, updateConfig.Order, "stop-first")
|
||||
}
|
||||
|
||||
func TestConvertFileObject(t *testing.T) {
|
||||
namespace := NewNamespace("testing")
|
||||
config := composetypes.FileReferenceConfig{
|
||||
Source: "source",
|
||||
Target: "target",
|
||||
UID: "user",
|
||||
GID: "group",
|
||||
Mode: uint32Ptr(0644),
|
||||
}
|
||||
swarmRef, err := convertFileObject(namespace, config, lookupConfig)
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := swarmReferenceObject{
|
||||
Name: "testing_source",
|
||||
File: swarmReferenceTarget{
|
||||
Name: config.Target,
|
||||
UID: config.UID,
|
||||
GID: config.GID,
|
||||
Mode: os.FileMode(0644),
|
||||
},
|
||||
}
|
||||
assert.Equal(t, expected, swarmRef)
|
||||
}
|
||||
|
||||
func lookupConfig(key string) (composetypes.FileObjectConfig, error) {
|
||||
if key != "source" {
|
||||
return composetypes.FileObjectConfig{}, errors.New("bad key")
|
||||
}
|
||||
return composetypes.FileObjectConfig{}, nil
|
||||
}
|
||||
|
||||
func TestConvertFileObjectDefaults(t *testing.T) {
|
||||
namespace := NewNamespace("testing")
|
||||
config := composetypes.FileReferenceConfig{Source: "source"}
|
||||
swarmRef, err := convertFileObject(namespace, config, lookupConfig)
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := swarmReferenceObject{
|
||||
Name: "testing_source",
|
||||
File: swarmReferenceTarget{
|
||||
Name: config.Source,
|
||||
UID: "0",
|
||||
GID: "0",
|
||||
Mode: os.FileMode(0444),
|
||||
},
|
||||
}
|
||||
assert.Equal(t, expected, swarmRef)
|
||||
}
|
||||
|
||||
func TestServiceConvertsIsolation(t *testing.T) {
|
||||
src := composetypes.ServiceConfig{
|
||||
Isolation: "hyperv",
|
||||
}
|
||||
result, err := Service("1.35", Namespace{name: "foo"}, src, nil, nil, nil, nil)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, container.IsolationHyperV, result.TaskTemplate.ContainerSpec.Isolation)
|
||||
}
|
||||
|
||||
func TestConvertServiceSecrets(t *testing.T) {
|
||||
namespace := Namespace{name: "foo"}
|
||||
secrets := []composetypes.ServiceSecretConfig{
|
||||
{Source: "foo_secret"},
|
||||
{Source: "bar_secret"},
|
||||
}
|
||||
secretSpecs := map[string]composetypes.SecretConfig{
|
||||
"foo_secret": {
|
||||
Name: "foo_secret",
|
||||
},
|
||||
"bar_secret": {
|
||||
Name: "bar_secret",
|
||||
},
|
||||
}
|
||||
client := &fakeClient{
|
||||
secretListFunc: func(opts types.SecretListOptions) ([]swarm.Secret, error) {
|
||||
assert.Contains(t, opts.Filters.Get("name"), "foo_secret")
|
||||
assert.Contains(t, opts.Filters.Get("name"), "bar_secret")
|
||||
return []swarm.Secret{
|
||||
{Spec: swarm.SecretSpec{Annotations: swarm.Annotations{Name: "foo_secret"}}},
|
||||
{Spec: swarm.SecretSpec{Annotations: swarm.Annotations{Name: "bar_secret"}}},
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
refs, err := convertServiceSecrets(client, namespace, secrets, secretSpecs)
|
||||
require.NoError(t, err)
|
||||
expected := []*swarm.SecretReference{
|
||||
{
|
||||
SecretName: "bar_secret",
|
||||
File: &swarm.SecretReferenceFileTarget{
|
||||
Name: "bar_secret",
|
||||
UID: "0",
|
||||
GID: "0",
|
||||
Mode: 0444,
|
||||
},
|
||||
},
|
||||
{
|
||||
SecretName: "foo_secret",
|
||||
File: &swarm.SecretReferenceFileTarget{
|
||||
Name: "foo_secret",
|
||||
UID: "0",
|
||||
GID: "0",
|
||||
Mode: 0444,
|
||||
},
|
||||
},
|
||||
}
|
||||
require.Equal(t, expected, refs)
|
||||
}
|
||||
|
||||
func TestConvertServiceConfigs(t *testing.T) {
|
||||
namespace := Namespace{name: "foo"}
|
||||
configs := []composetypes.ServiceConfigObjConfig{
|
||||
{Source: "foo_config"},
|
||||
{Source: "bar_config"},
|
||||
}
|
||||
configSpecs := map[string]composetypes.ConfigObjConfig{
|
||||
"foo_config": {
|
||||
Name: "foo_config",
|
||||
},
|
||||
"bar_config": {
|
||||
Name: "bar_config",
|
||||
},
|
||||
}
|
||||
client := &fakeClient{
|
||||
configListFunc: func(opts types.ConfigListOptions) ([]swarm.Config, error) {
|
||||
assert.Contains(t, opts.Filters.Get("name"), "foo_config")
|
||||
assert.Contains(t, opts.Filters.Get("name"), "bar_config")
|
||||
return []swarm.Config{
|
||||
{Spec: swarm.ConfigSpec{Annotations: swarm.Annotations{Name: "foo_config"}}},
|
||||
{Spec: swarm.ConfigSpec{Annotations: swarm.Annotations{Name: "bar_config"}}},
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
refs, err := convertServiceConfigObjs(client, namespace, configs, configSpecs)
|
||||
require.NoError(t, err)
|
||||
expected := []*swarm.ConfigReference{
|
||||
{
|
||||
ConfigName: "bar_config",
|
||||
File: &swarm.ConfigReferenceFileTarget{
|
||||
Name: "bar_config",
|
||||
UID: "0",
|
||||
GID: "0",
|
||||
Mode: 0444,
|
||||
},
|
||||
},
|
||||
{
|
||||
ConfigName: "foo_config",
|
||||
File: &swarm.ConfigReferenceFileTarget{
|
||||
Name: "foo_config",
|
||||
UID: "0",
|
||||
GID: "0",
|
||||
Mode: 0444,
|
||||
},
|
||||
},
|
||||
}
|
||||
require.Equal(t, expected, refs)
|
||||
}
|
||||
|
||||
type fakeClient struct {
|
||||
client.Client
|
||||
secretListFunc func(types.SecretListOptions) ([]swarm.Secret, error)
|
||||
configListFunc func(types.ConfigListOptions) ([]swarm.Config, error)
|
||||
}
|
||||
|
||||
func (c *fakeClient) SecretList(ctx context.Context, options types.SecretListOptions) ([]swarm.Secret, error) {
|
||||
if c.secretListFunc != nil {
|
||||
return c.secretListFunc(options)
|
||||
}
|
||||
return []swarm.Secret{}, nil
|
||||
}
|
||||
|
||||
func (c *fakeClient) ConfigList(ctx context.Context, options types.ConfigListOptions) ([]swarm.Config, error) {
|
||||
if c.configListFunc != nil {
|
||||
return c.configListFunc(options)
|
||||
}
|
||||
return []swarm.Config{}, nil
|
||||
}
|
||||
|
||||
@ -68,16 +68,15 @@ func convertVolumeToMount(
|
||||
result.VolumeOptions.NoCopy = volume.Volume.NoCopy
|
||||
}
|
||||
|
||||
// External named volumes
|
||||
if stackVolume.External.External {
|
||||
result.Source = stackVolume.External.Name
|
||||
return result, nil
|
||||
}
|
||||
|
||||
if stackVolume.Name != "" {
|
||||
result.Source = stackVolume.Name
|
||||
}
|
||||
|
||||
// External named volumes
|
||||
if stackVolume.External.External {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
result.VolumeOptions.Labels = AddStackLabel(namespace, stackVolume.Labels)
|
||||
if stackVolume.Driver != "" || stackVolume.DriverOpts != nil {
|
||||
result.VolumeOptions.DriverConfig = &mount.Driver{
|
||||
|
||||
@ -148,20 +148,16 @@ func TestConvertVolumeToMountNamedVolumeWithNameCustomizd(t *testing.T) {
|
||||
func TestConvertVolumeToMountNamedVolumeExternal(t *testing.T) {
|
||||
stackVolumes := volumes{
|
||||
"outside": composetypes.VolumeConfig{
|
||||
External: composetypes.External{
|
||||
External: true,
|
||||
Name: "special",
|
||||
},
|
||||
Name: "special",
|
||||
External: composetypes.External{External: true},
|
||||
},
|
||||
}
|
||||
namespace := NewNamespace("foo")
|
||||
expected := mount.Mount{
|
||||
Type: mount.TypeVolume,
|
||||
Source: "special",
|
||||
Target: "/foo",
|
||||
VolumeOptions: &mount.VolumeOptions{
|
||||
NoCopy: false,
|
||||
},
|
||||
Type: mount.TypeVolume,
|
||||
Source: "special",
|
||||
Target: "/foo",
|
||||
VolumeOptions: &mount.VolumeOptions{NoCopy: false},
|
||||
}
|
||||
config := composetypes.ServiceVolumeConfig{
|
||||
Type: "volume",
|
||||
@ -176,10 +172,8 @@ func TestConvertVolumeToMountNamedVolumeExternal(t *testing.T) {
|
||||
func TestConvertVolumeToMountNamedVolumeExternalNoCopy(t *testing.T) {
|
||||
stackVolumes := volumes{
|
||||
"outside": composetypes.VolumeConfig{
|
||||
External: composetypes.External{
|
||||
External: true,
|
||||
Name: "special",
|
||||
},
|
||||
Name: "special",
|
||||
External: composetypes.External{External: true},
|
||||
},
|
||||
}
|
||||
namespace := NewNamespace("foo")
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
version: "3.4"
|
||||
version: "3.5"
|
||||
|
||||
services:
|
||||
foo:
|
||||
@ -16,7 +16,6 @@ services:
|
||||
labels: [FOO=BAR]
|
||||
|
||||
|
||||
|
||||
cap_add:
|
||||
- ALL
|
||||
|
||||
@ -54,6 +53,13 @@ services:
|
||||
reservations:
|
||||
cpus: '0.0001'
|
||||
memory: 20M
|
||||
generic_resources:
|
||||
- discrete_resource_spec:
|
||||
kind: 'gpu'
|
||||
value: 2
|
||||
- discrete_resource_spec:
|
||||
kind: 'ssd'
|
||||
value: 1
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
delay: 5s
|
||||
|
||||
@ -12,6 +12,7 @@ import (
|
||||
"github.com/docker/cli/cli/compose/template"
|
||||
"github.com/docker/cli/cli/compose/types"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api/types/versions"
|
||||
"github.com/docker/go-connections/nat"
|
||||
units "github.com/docker/go-units"
|
||||
shellwords "github.com/mattn/go-shellwords"
|
||||
@ -49,6 +50,7 @@ func Load(configDetails types.ConfigDetails) (*types.Config, error) {
|
||||
}
|
||||
|
||||
configDict := getConfigDict(configDetails)
|
||||
configDetails.Version = schema.Version(configDict)
|
||||
|
||||
if err := validateForbidden(configDict); err != nil {
|
||||
return nil, err
|
||||
@ -60,7 +62,7 @@ func Load(configDetails types.ConfigDetails) (*types.Config, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := schema.Validate(configDict, schema.Version(configDict)); err != nil {
|
||||
if err := schema.Validate(configDict, configDetails.Version); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return loadSections(configDict, configDetails)
|
||||
@ -96,28 +98,28 @@ func loadSections(config map[string]interface{}, configDetails types.ConfigDetai
|
||||
{
|
||||
key: "networks",
|
||||
fnc: func(config map[string]interface{}) error {
|
||||
cfg.Networks, err = LoadNetworks(config)
|
||||
cfg.Networks, err = LoadNetworks(config, configDetails.Version)
|
||||
return err
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "volumes",
|
||||
fnc: func(config map[string]interface{}) error {
|
||||
cfg.Volumes, err = LoadVolumes(config)
|
||||
cfg.Volumes, err = LoadVolumes(config, configDetails.Version)
|
||||
return err
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "secrets",
|
||||
fnc: func(config map[string]interface{}) error {
|
||||
cfg.Secrets, err = LoadSecrets(config, configDetails.WorkingDir)
|
||||
cfg.Secrets, err = LoadSecrets(config, configDetails)
|
||||
return err
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "configs",
|
||||
fnc: func(config map[string]interface{}) error {
|
||||
cfg.Configs, err = LoadConfigObjs(config, configDetails.WorkingDir)
|
||||
cfg.Configs, err = LoadConfigObjs(config, configDetails)
|
||||
return err
|
||||
},
|
||||
},
|
||||
@ -423,17 +425,30 @@ func transformUlimits(data interface{}) (interface{}, error) {
|
||||
|
||||
// LoadNetworks produces a NetworkConfig map from a compose file Dict
|
||||
// the source Dict is not validated if directly used. Use Load() to enable validation
|
||||
func LoadNetworks(source map[string]interface{}) (map[string]types.NetworkConfig, error) {
|
||||
func LoadNetworks(source map[string]interface{}, version string) (map[string]types.NetworkConfig, error) {
|
||||
networks := make(map[string]types.NetworkConfig)
|
||||
err := transform(source, &networks)
|
||||
if err != nil {
|
||||
return networks, err
|
||||
}
|
||||
for name, network := range networks {
|
||||
if network.External.External && network.External.Name == "" {
|
||||
network.External.Name = name
|
||||
networks[name] = network
|
||||
if !network.External.External {
|
||||
continue
|
||||
}
|
||||
switch {
|
||||
case network.External.Name != "":
|
||||
if network.Name != "" {
|
||||
return nil, errors.Errorf("network %s: network.external.name and network.name conflict; only use network.name", name)
|
||||
}
|
||||
if versions.GreaterThanOrEqualTo(version, "3.5") {
|
||||
logrus.Warnf("network %s: network.external.name is deprecated in favor of network.name", name)
|
||||
}
|
||||
network.Name = network.External.Name
|
||||
network.External.Name = ""
|
||||
case network.Name == "":
|
||||
network.Name = name
|
||||
}
|
||||
networks[name] = network
|
||||
}
|
||||
return networks, nil
|
||||
}
|
||||
@ -446,76 +461,100 @@ func externalVolumeError(volume, key string) error {
|
||||
|
||||
// LoadVolumes produces a VolumeConfig map from a compose file Dict
|
||||
// the source Dict is not validated if directly used. Use Load() to enable validation
|
||||
func LoadVolumes(source map[string]interface{}) (map[string]types.VolumeConfig, error) {
|
||||
func LoadVolumes(source map[string]interface{}, version string) (map[string]types.VolumeConfig, error) {
|
||||
volumes := make(map[string]types.VolumeConfig)
|
||||
err := transform(source, &volumes)
|
||||
if err != nil {
|
||||
if err := transform(source, &volumes); err != nil {
|
||||
return volumes, err
|
||||
}
|
||||
for name, volume := range volumes {
|
||||
if volume.External.External {
|
||||
if volume.Driver != "" {
|
||||
return nil, externalVolumeError(name, "driver")
|
||||
}
|
||||
if len(volume.DriverOpts) > 0 {
|
||||
return nil, externalVolumeError(name, "driver_opts")
|
||||
}
|
||||
if len(volume.Labels) > 0 {
|
||||
return nil, externalVolumeError(name, "labels")
|
||||
}
|
||||
if volume.External.Name == "" {
|
||||
volume.External.Name = name
|
||||
volumes[name] = volume
|
||||
} else {
|
||||
logrus.Warnf("volume %s: volume.external.name is deprecated in favor of volume.name", name)
|
||||
|
||||
if volume.Name != "" {
|
||||
return nil, errors.Errorf("volume %s: volume.external.name and volume.name conflict; only use volume.name", name)
|
||||
}
|
||||
}
|
||||
for name, volume := range volumes {
|
||||
if !volume.External.External {
|
||||
continue
|
||||
}
|
||||
switch {
|
||||
case volume.Driver != "":
|
||||
return nil, externalVolumeError(name, "driver")
|
||||
case len(volume.DriverOpts) > 0:
|
||||
return nil, externalVolumeError(name, "driver_opts")
|
||||
case len(volume.Labels) > 0:
|
||||
return nil, externalVolumeError(name, "labels")
|
||||
case volume.External.Name != "":
|
||||
if volume.Name != "" {
|
||||
return nil, errors.Errorf("volume %s: volume.external.name and volume.name conflict; only use volume.name", name)
|
||||
}
|
||||
if versions.GreaterThanOrEqualTo(version, "3.4") {
|
||||
logrus.Warnf("volume %s: volume.external.name is deprecated in favor of volume.name", name)
|
||||
}
|
||||
volume.Name = volume.External.Name
|
||||
volume.External.Name = ""
|
||||
case volume.Name == "":
|
||||
volume.Name = name
|
||||
}
|
||||
volumes[name] = volume
|
||||
}
|
||||
return volumes, nil
|
||||
}
|
||||
|
||||
// LoadSecrets produces a SecretConfig map from a compose file Dict
|
||||
// the source Dict is not validated if directly used. Use Load() to enable validation
|
||||
func LoadSecrets(source map[string]interface{}, workingDir string) (map[string]types.SecretConfig, error) {
|
||||
func LoadSecrets(source map[string]interface{}, details types.ConfigDetails) (map[string]types.SecretConfig, error) {
|
||||
secrets := make(map[string]types.SecretConfig)
|
||||
if err := transform(source, &secrets); err != nil {
|
||||
return secrets, err
|
||||
}
|
||||
for name, secret := range secrets {
|
||||
if secret.External.External && secret.External.Name == "" {
|
||||
secret.External.Name = name
|
||||
secrets[name] = secret
|
||||
}
|
||||
if secret.File != "" {
|
||||
secret.File = absPath(workingDir, secret.File)
|
||||
obj, err := loadFileObjectConfig(name, "secret", types.FileObjectConfig(secret), details)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
secrets[name] = types.SecretConfig(obj)
|
||||
}
|
||||
return secrets, nil
|
||||
}
|
||||
|
||||
// LoadConfigObjs produces a ConfigObjConfig map from a compose file Dict
|
||||
// the source Dict is not validated if directly used. Use Load() to enable validation
|
||||
func LoadConfigObjs(source map[string]interface{}, workingDir string) (map[string]types.ConfigObjConfig, error) {
|
||||
func LoadConfigObjs(source map[string]interface{}, details types.ConfigDetails) (map[string]types.ConfigObjConfig, error) {
|
||||
configs := make(map[string]types.ConfigObjConfig)
|
||||
if err := transform(source, &configs); err != nil {
|
||||
return configs, err
|
||||
}
|
||||
for name, config := range configs {
|
||||
if config.External.External && config.External.Name == "" {
|
||||
config.External.Name = name
|
||||
configs[name] = config
|
||||
}
|
||||
if config.File != "" {
|
||||
config.File = absPath(workingDir, config.File)
|
||||
obj, err := loadFileObjectConfig(name, "config", types.FileObjectConfig(config), details)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
configs[name] = types.ConfigObjConfig(obj)
|
||||
}
|
||||
return configs, nil
|
||||
}
|
||||
|
||||
func loadFileObjectConfig(name string, objType string, obj types.FileObjectConfig, details types.ConfigDetails) (types.FileObjectConfig, error) {
|
||||
// if "external: true"
|
||||
if obj.External.External {
|
||||
// handle deprecated external.name
|
||||
if obj.External.Name != "" {
|
||||
if obj.Name != "" {
|
||||
return obj, errors.Errorf("%[1]s %[2]s: %[1]s.external.name and %[1]s.name conflict; only use %[1]s.name", objType, name)
|
||||
}
|
||||
if versions.GreaterThanOrEqualTo(details.Version, "3.5") {
|
||||
logrus.Warnf("%[1]s %[2]s: %[1]s.external.name is deprecated in favor of %[1]s.name", objType, name)
|
||||
}
|
||||
obj.Name = obj.External.Name
|
||||
obj.External.Name = ""
|
||||
} else {
|
||||
if obj.Name == "" {
|
||||
obj.Name = name
|
||||
}
|
||||
}
|
||||
// if not "external: true"
|
||||
} else {
|
||||
obj.File = absPath(details.WorkingDir, obj.File)
|
||||
}
|
||||
|
||||
return obj, nil
|
||||
}
|
||||
|
||||
func absPath(workingDir string, filePath string) string {
|
||||
if filepath.IsAbs(filePath) {
|
||||
return filePath
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package loader
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
@ -9,6 +10,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/docker/cli/cli/compose/types"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@ -624,17 +626,18 @@ networks:
|
||||
},
|
||||
},
|
||||
Configs: map[string]types.ConfigObjConfig{
|
||||
"appconfig": {External: types.External{External: true, Name: "appconfig"}},
|
||||
"appconfig": {External: types.External{External: true}, Name: "appconfig"},
|
||||
},
|
||||
Secrets: map[string]types.SecretConfig{
|
||||
"super": {External: types.External{External: true, Name: "super"}},
|
||||
"super": {External: types.External{External: true}, Name: "super"},
|
||||
},
|
||||
Volumes: map[string]types.VolumeConfig{
|
||||
"data": {External: types.External{External: true, Name: "data"}},
|
||||
"data": {External: types.External{External: true}, Name: "data"},
|
||||
},
|
||||
Networks: map[string]types.NetworkConfig{
|
||||
"front": {
|
||||
External: types.External{External: true, Name: "front"},
|
||||
External: types.External{External: true},
|
||||
Name: "front",
|
||||
Internal: true,
|
||||
Attachable: true,
|
||||
},
|
||||
@ -798,7 +801,7 @@ volumes:
|
||||
assert.Contains(t, err.Error(), "external_volume")
|
||||
}
|
||||
|
||||
func TestInvalidExternalNameAndNameCombination(t *testing.T) {
|
||||
func TestLoadVolumeInvalidExternalNameAndNameCombination(t *testing.T) {
|
||||
_, err := loadYAML(`
|
||||
version: "3.4"
|
||||
volumes:
|
||||
@ -877,6 +880,20 @@ func TestFullExample(t *testing.T) {
|
||||
Reservations: &types.Resource{
|
||||
NanoCPUs: "0.0001",
|
||||
MemoryBytes: 20 * 1024 * 1024,
|
||||
GenericResources: []types.GenericResource{
|
||||
{
|
||||
DiscreteResourceSpec: &types.DiscreteGenericResource{
|
||||
Kind: "gpu",
|
||||
Value: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
DiscreteResourceSpec: &types.DiscreteGenericResource{
|
||||
Kind: "ssd",
|
||||
Value: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
RestartPolicy: &types.RestartPolicy{
|
||||
@ -1156,17 +1173,13 @@ func TestFullExample(t *testing.T) {
|
||||
},
|
||||
|
||||
"external-network": {
|
||||
External: types.External{
|
||||
Name: "external-network",
|
||||
External: true,
|
||||
},
|
||||
Name: "external-network",
|
||||
External: types.External{External: true},
|
||||
},
|
||||
|
||||
"other-external-network": {
|
||||
External: types.External{
|
||||
Name: "my-cool-network",
|
||||
External: true,
|
||||
},
|
||||
Name: "my-cool-network",
|
||||
External: types.External{External: true},
|
||||
},
|
||||
}
|
||||
|
||||
@ -1190,23 +1203,16 @@ func TestFullExample(t *testing.T) {
|
||||
},
|
||||
},
|
||||
"external-volume": {
|
||||
External: types.External{
|
||||
Name: "external-volume",
|
||||
External: true,
|
||||
},
|
||||
Name: "external-volume",
|
||||
External: types.External{External: true},
|
||||
},
|
||||
"other-external-volume": {
|
||||
External: types.External{
|
||||
Name: "my-cool-volume",
|
||||
External: true,
|
||||
},
|
||||
Name: "my-cool-volume",
|
||||
External: types.External{External: true},
|
||||
},
|
||||
"external-volume3": {
|
||||
Name: "this-is-volume3",
|
||||
External: types.External{
|
||||
Name: "external-volume3",
|
||||
External: true,
|
||||
},
|
||||
Name: "this-is-volume3",
|
||||
External: types.External{External: true},
|
||||
},
|
||||
}
|
||||
|
||||
@ -1406,3 +1412,206 @@ services:
|
||||
require.Len(t, config.Services, 1)
|
||||
assert.Equal(t, expected, config.Services[0].ExtraHosts)
|
||||
}
|
||||
|
||||
func TestLoadVolumesWarnOnDeprecatedExternalNameVersion34(t *testing.T) {
|
||||
buf, cleanup := patchLogrus()
|
||||
defer cleanup()
|
||||
|
||||
source := map[string]interface{}{
|
||||
"foo": map[string]interface{}{
|
||||
"external": map[string]interface{}{
|
||||
"name": "oops",
|
||||
},
|
||||
},
|
||||
}
|
||||
volumes, err := LoadVolumes(source, "3.4")
|
||||
require.NoError(t, err)
|
||||
expected := map[string]types.VolumeConfig{
|
||||
"foo": {
|
||||
Name: "oops",
|
||||
External: types.External{External: true},
|
||||
},
|
||||
}
|
||||
assert.Equal(t, expected, volumes)
|
||||
assert.Contains(t, buf.String(), "volume.external.name is deprecated")
|
||||
|
||||
}
|
||||
|
||||
func patchLogrus() (*bytes.Buffer, func()) {
|
||||
buf := new(bytes.Buffer)
|
||||
out := logrus.StandardLogger().Out
|
||||
logrus.SetOutput(buf)
|
||||
return buf, func() { logrus.SetOutput(out) }
|
||||
}
|
||||
|
||||
func TestLoadVolumesWarnOnDeprecatedExternalNameVersion33(t *testing.T) {
|
||||
buf, cleanup := patchLogrus()
|
||||
defer cleanup()
|
||||
|
||||
source := map[string]interface{}{
|
||||
"foo": map[string]interface{}{
|
||||
"external": map[string]interface{}{
|
||||
"name": "oops",
|
||||
},
|
||||
},
|
||||
}
|
||||
volumes, err := LoadVolumes(source, "3.3")
|
||||
require.NoError(t, err)
|
||||
expected := map[string]types.VolumeConfig{
|
||||
"foo": {
|
||||
Name: "oops",
|
||||
External: types.External{External: true},
|
||||
},
|
||||
}
|
||||
assert.Equal(t, expected, volumes)
|
||||
assert.Equal(t, "", buf.String())
|
||||
}
|
||||
|
||||
func TestLoadV35(t *testing.T) {
|
||||
actual, err := loadYAML(`
|
||||
version: "3.5"
|
||||
services:
|
||||
foo:
|
||||
image: busybox
|
||||
isolation: process
|
||||
configs:
|
||||
foo:
|
||||
name: fooqux
|
||||
external: true
|
||||
bar:
|
||||
name: barqux
|
||||
file: ./example1.env
|
||||
secrets:
|
||||
foo:
|
||||
name: fooqux
|
||||
external: true
|
||||
bar:
|
||||
name: barqux
|
||||
file: ./full-example.yml
|
||||
`)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, actual.Services, 1)
|
||||
assert.Len(t, actual.Secrets, 2)
|
||||
assert.Len(t, actual.Configs, 2)
|
||||
assert.Equal(t, "process", actual.Services[0].Isolation)
|
||||
}
|
||||
|
||||
func TestLoadV35InvalidIsolation(t *testing.T) {
|
||||
// validation should be done only on the daemon side
|
||||
actual, err := loadYAML(`
|
||||
version: "3.5"
|
||||
services:
|
||||
foo:
|
||||
image: busybox
|
||||
isolation: invalid
|
||||
configs:
|
||||
super:
|
||||
external: true
|
||||
`)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, actual.Services, 1)
|
||||
assert.Equal(t, "invalid", actual.Services[0].Isolation)
|
||||
}
|
||||
|
||||
func TestLoadSecretInvalidExternalNameAndNameCombination(t *testing.T) {
|
||||
_, err := loadYAML(`
|
||||
version: "3.5"
|
||||
secrets:
|
||||
external_secret:
|
||||
name: user_specified_name
|
||||
external:
|
||||
name: external_name
|
||||
`)
|
||||
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "secret.external.name and secret.name conflict; only use secret.name")
|
||||
assert.Contains(t, err.Error(), "external_secret")
|
||||
}
|
||||
|
||||
func TestLoadSecretsWarnOnDeprecatedExternalNameVersion35(t *testing.T) {
|
||||
buf, cleanup := patchLogrus()
|
||||
defer cleanup()
|
||||
|
||||
source := map[string]interface{}{
|
||||
"foo": map[string]interface{}{
|
||||
"external": map[string]interface{}{
|
||||
"name": "oops",
|
||||
},
|
||||
},
|
||||
}
|
||||
details := types.ConfigDetails{
|
||||
Version: "3.5",
|
||||
}
|
||||
secrets, err := LoadSecrets(source, details)
|
||||
require.NoError(t, err)
|
||||
expected := map[string]types.SecretConfig{
|
||||
"foo": {
|
||||
Name: "oops",
|
||||
External: types.External{External: true},
|
||||
},
|
||||
}
|
||||
assert.Equal(t, expected, secrets)
|
||||
assert.Contains(t, buf.String(), "secret.external.name is deprecated")
|
||||
}
|
||||
|
||||
func TestLoadNetworksWarnOnDeprecatedExternalNameVersion35(t *testing.T) {
|
||||
buf, cleanup := patchLogrus()
|
||||
defer cleanup()
|
||||
|
||||
source := map[string]interface{}{
|
||||
"foo": map[string]interface{}{
|
||||
"external": map[string]interface{}{
|
||||
"name": "oops",
|
||||
},
|
||||
},
|
||||
}
|
||||
networks, err := LoadNetworks(source, "3.5")
|
||||
require.NoError(t, err)
|
||||
expected := map[string]types.NetworkConfig{
|
||||
"foo": {
|
||||
Name: "oops",
|
||||
External: types.External{External: true},
|
||||
},
|
||||
}
|
||||
assert.Equal(t, expected, networks)
|
||||
assert.Contains(t, buf.String(), "network.external.name is deprecated")
|
||||
|
||||
}
|
||||
|
||||
func TestLoadNetworksWarnOnDeprecatedExternalNameVersion34(t *testing.T) {
|
||||
buf, cleanup := patchLogrus()
|
||||
defer cleanup()
|
||||
|
||||
source := map[string]interface{}{
|
||||
"foo": map[string]interface{}{
|
||||
"external": map[string]interface{}{
|
||||
"name": "oops",
|
||||
},
|
||||
},
|
||||
}
|
||||
networks, err := LoadNetworks(source, "3.4")
|
||||
require.NoError(t, err)
|
||||
expected := map[string]types.NetworkConfig{
|
||||
"foo": {
|
||||
Name: "oops",
|
||||
External: types.External{External: true},
|
||||
},
|
||||
}
|
||||
assert.Equal(t, expected, networks)
|
||||
assert.Equal(t, "", buf.String())
|
||||
}
|
||||
|
||||
func TestLoadNetworkInvalidExternalNameAndNameCombination(t *testing.T) {
|
||||
_, err := loadYAML(`
|
||||
version: "3.5"
|
||||
networks:
|
||||
foo:
|
||||
name: user_specified_name
|
||||
external:
|
||||
name: external_name
|
||||
`)
|
||||
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "network.external.name and network.name conflict; only use network.name")
|
||||
assert.Contains(t, err.Error(), "foo")
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -262,7 +262,8 @@
|
||||
"nocopy": {"type": "boolean"}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
],
|
||||
"uniqueItems": true
|
||||
|
||||
@ -296,7 +296,8 @@
|
||||
"nocopy": {"type": "boolean"}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
],
|
||||
"uniqueItems": true
|
||||
|
||||
@ -299,7 +299,8 @@
|
||||
"nocopy": {"type": "boolean"}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
],
|
||||
"uniqueItems": true
|
||||
|
||||
573
components/cli/cli/compose/schema/data/config_schema_v3.5.json
Normal file
573
components/cli/cli/compose/schema/data/config_schema_v3.5.json
Normal file
@ -0,0 +1,573 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"id": "config_schema_v3.5.json",
|
||||
"type": "object",
|
||||
"required": ["version"],
|
||||
|
||||
"properties": {
|
||||
"version": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
"services": {
|
||||
"id": "#/properties/services",
|
||||
"type": "object",
|
||||
"patternProperties": {
|
||||
"^[a-zA-Z0-9._-]+$": {
|
||||
"$ref": "#/definitions/service"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
|
||||
"networks": {
|
||||
"id": "#/properties/networks",
|
||||
"type": "object",
|
||||
"patternProperties": {
|
||||
"^[a-zA-Z0-9._-]+$": {
|
||||
"$ref": "#/definitions/network"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"volumes": {
|
||||
"id": "#/properties/volumes",
|
||||
"type": "object",
|
||||
"patternProperties": {
|
||||
"^[a-zA-Z0-9._-]+$": {
|
||||
"$ref": "#/definitions/volume"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
|
||||
"secrets": {
|
||||
"id": "#/properties/secrets",
|
||||
"type": "object",
|
||||
"patternProperties": {
|
||||
"^[a-zA-Z0-9._-]+$": {
|
||||
"$ref": "#/definitions/secret"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
|
||||
"configs": {
|
||||
"id": "#/properties/configs",
|
||||
"type": "object",
|
||||
"patternProperties": {
|
||||
"^[a-zA-Z0-9._-]+$": {
|
||||
"$ref": "#/definitions/config"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
|
||||
"patternProperties": {"^x-": {}},
|
||||
"additionalProperties": false,
|
||||
|
||||
"definitions": {
|
||||
|
||||
"service": {
|
||||
"id": "#/definitions/service",
|
||||
"type": "object",
|
||||
|
||||
"properties": {
|
||||
"deploy": {"$ref": "#/definitions/deployment"},
|
||||
"build": {
|
||||
"oneOf": [
|
||||
{"type": "string"},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"context": {"type": "string"},
|
||||
"dockerfile": {"type": "string"},
|
||||
"args": {"$ref": "#/definitions/list_or_dict"},
|
||||
"labels": {"$ref": "#/definitions/list_or_dict"},
|
||||
"cache_from": {"$ref": "#/definitions/list_of_strings"},
|
||||
"network": {"type": "string"},
|
||||
"target": {"type": "string"},
|
||||
"shm_size": {"type": ["integer", "string"]}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"cap_add": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
|
||||
"cap_drop": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
|
||||
"cgroup_parent": {"type": "string"},
|
||||
"command": {
|
||||
"oneOf": [
|
||||
{"type": "string"},
|
||||
{"type": "array", "items": {"type": "string"}}
|
||||
]
|
||||
},
|
||||
"configs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"oneOf": [
|
||||
{"type": "string"},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"source": {"type": "string"},
|
||||
"target": {"type": "string"},
|
||||
"uid": {"type": "string"},
|
||||
"gid": {"type": "string"},
|
||||
"mode": {"type": "number"}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"container_name": {"type": "string"},
|
||||
"credential_spec": {"type": "object", "properties": {
|
||||
"file": {"type": "string"},
|
||||
"registry": {"type": "string"}
|
||||
}},
|
||||
"depends_on": {"$ref": "#/definitions/list_of_strings"},
|
||||
"devices": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
|
||||
"dns": {"$ref": "#/definitions/string_or_list"},
|
||||
"dns_search": {"$ref": "#/definitions/string_or_list"},
|
||||
"domainname": {"type": "string"},
|
||||
"entrypoint": {
|
||||
"oneOf": [
|
||||
{"type": "string"},
|
||||
{"type": "array", "items": {"type": "string"}}
|
||||
]
|
||||
},
|
||||
"env_file": {"$ref": "#/definitions/string_or_list"},
|
||||
"environment": {"$ref": "#/definitions/list_or_dict"},
|
||||
|
||||
"expose": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": ["string", "number"],
|
||||
"format": "expose"
|
||||
},
|
||||
"uniqueItems": true
|
||||
},
|
||||
|
||||
"external_links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
|
||||
"extra_hosts": {"$ref": "#/definitions/list_or_dict"},
|
||||
"healthcheck": {"$ref": "#/definitions/healthcheck"},
|
||||
"hostname": {"type": "string"},
|
||||
"image": {"type": "string"},
|
||||
"ipc": {"type": "string"},
|
||||
"isolation": {"type": "string"},
|
||||
"labels": {"$ref": "#/definitions/list_or_dict"},
|
||||
"links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
|
||||
|
||||
"logging": {
|
||||
"type": "object",
|
||||
|
||||
"properties": {
|
||||
"driver": {"type": "string"},
|
||||
"options": {
|
||||
"type": "object",
|
||||
"patternProperties": {
|
||||
"^.+$": {"type": ["string", "number", "null"]}
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
|
||||
"mac_address": {"type": "string"},
|
||||
"network_mode": {"type": "string"},
|
||||
|
||||
"networks": {
|
||||
"oneOf": [
|
||||
{"$ref": "#/definitions/list_of_strings"},
|
||||
{
|
||||
"type": "object",
|
||||
"patternProperties": {
|
||||
"^[a-zA-Z0-9._-]+$": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"aliases": {"$ref": "#/definitions/list_of_strings"},
|
||||
"ipv4_address": {"type": "string"},
|
||||
"ipv6_address": {"type": "string"}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{"type": "null"}
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"pid": {"type": ["string", "null"]},
|
||||
|
||||
"ports": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"oneOf": [
|
||||
{"type": "number", "format": "ports"},
|
||||
{"type": "string", "format": "ports"},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"mode": {"type": "string"},
|
||||
"target": {"type": "integer"},
|
||||
"published": {"type": "integer"},
|
||||
"protocol": {"type": "string"}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"uniqueItems": true
|
||||
},
|
||||
|
||||
"privileged": {"type": "boolean"},
|
||||
"read_only": {"type": "boolean"},
|
||||
"restart": {"type": "string"},
|
||||
"security_opt": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
|
||||
"shm_size": {"type": ["number", "string"]},
|
||||
"secrets": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"oneOf": [
|
||||
{"type": "string"},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"source": {"type": "string"},
|
||||
"target": {"type": "string"},
|
||||
"uid": {"type": "string"},
|
||||
"gid": {"type": "string"},
|
||||
"mode": {"type": "number"}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"sysctls": {"$ref": "#/definitions/list_or_dict"},
|
||||
"stdin_open": {"type": "boolean"},
|
||||
"stop_grace_period": {"type": "string", "format": "duration"},
|
||||
"stop_signal": {"type": "string"},
|
||||
"tmpfs": {"$ref": "#/definitions/string_or_list"},
|
||||
"tty": {"type": "boolean"},
|
||||
"ulimits": {
|
||||
"type": "object",
|
||||
"patternProperties": {
|
||||
"^[a-z]+$": {
|
||||
"oneOf": [
|
||||
{"type": "integer"},
|
||||
{
|
||||
"type":"object",
|
||||
"properties": {
|
||||
"hard": {"type": "integer"},
|
||||
"soft": {"type": "integer"}
|
||||
},
|
||||
"required": ["soft", "hard"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"user": {"type": "string"},
|
||||
"userns_mode": {"type": "string"},
|
||||
"volumes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"oneOf": [
|
||||
{"type": "string"},
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["type"],
|
||||
"properties": {
|
||||
"type": {"type": "string"},
|
||||
"source": {"type": "string"},
|
||||
"target": {"type": "string"},
|
||||
"read_only": {"type": "boolean"},
|
||||
"consistency": {"type": "string"},
|
||||
"bind": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"propagation": {"type": "string"}
|
||||
}
|
||||
},
|
||||
"volume": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"nocopy": {"type": "boolean"}
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
],
|
||||
"uniqueItems": true
|
||||
}
|
||||
},
|
||||
"working_dir": {"type": "string"}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
|
||||
"healthcheck": {
|
||||
"id": "#/definitions/healthcheck",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"disable": {"type": "boolean"},
|
||||
"interval": {"type": "string", "format": "duration"},
|
||||
"retries": {"type": "number"},
|
||||
"test": {
|
||||
"oneOf": [
|
||||
{"type": "string"},
|
||||
{"type": "array", "items": {"type": "string"}}
|
||||
]
|
||||
},
|
||||
"timeout": {"type": "string", "format": "duration"},
|
||||
"start_period": {"type": "string", "format": "duration"}
|
||||
}
|
||||
},
|
||||
"deployment": {
|
||||
"id": "#/definitions/deployment",
|
||||
"type": ["object", "null"],
|
||||
"properties": {
|
||||
"mode": {"type": "string"},
|
||||
"endpoint_mode": {"type": "string"},
|
||||
"replicas": {"type": "integer"},
|
||||
"labels": {"$ref": "#/definitions/list_or_dict"},
|
||||
"update_config": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"parallelism": {"type": "integer"},
|
||||
"delay": {"type": "string", "format": "duration"},
|
||||
"failure_action": {"type": "string"},
|
||||
"monitor": {"type": "string", "format": "duration"},
|
||||
"max_failure_ratio": {"type": "number"},
|
||||
"order": {"type": "string", "enum": [
|
||||
"start-first", "stop-first"
|
||||
]}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"resources": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"limits": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"cpus": {"type": "string"},
|
||||
"memory": {"type": "string"}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"reservations": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"cpus": {"type": "string"},
|
||||
"memory": {"type": "string"},
|
||||
"generic_resources": {"$ref": "#/definitions/generic_resources"}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"restart_policy": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"condition": {"type": "string"},
|
||||
"delay": {"type": "string", "format": "duration"},
|
||||
"max_attempts": {"type": "integer"},
|
||||
"window": {"type": "string", "format": "duration"}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"placement": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"constraints": {"type": "array", "items": {"type": "string"}},
|
||||
"preferences": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"spread": {"type": "string"}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
|
||||
"generic_resources": {
|
||||
"id": "#/definitions/generic_resources",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"discrete_resource_spec": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": {"type": "string"},
|
||||
"value": {"type": "number"}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
|
||||
"network": {
|
||||
"id": "#/definitions/network",
|
||||
"type": ["object", "null"],
|
||||
"properties": {
|
||||
"name": {"type": "string"},
|
||||
"driver": {"type": "string"},
|
||||
"driver_opts": {
|
||||
"type": "object",
|
||||
"patternProperties": {
|
||||
"^.+$": {"type": ["string", "number"]}
|
||||
}
|
||||
},
|
||||
"ipam": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"driver": {"type": "string"},
|
||||
"config": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"subnet": {"type": "string"}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"external": {
|
||||
"type": ["boolean", "object"],
|
||||
"properties": {
|
||||
"name": {"type": "string"}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"internal": {"type": "boolean"},
|
||||
"attachable": {"type": "boolean"},
|
||||
"labels": {"$ref": "#/definitions/list_or_dict"}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
|
||||
"volume": {
|
||||
"id": "#/definitions/volume",
|
||||
"type": ["object", "null"],
|
||||
"properties": {
|
||||
"name": {"type": "string"},
|
||||
"driver": {"type": "string"},
|
||||
"driver_opts": {
|
||||
"type": "object",
|
||||
"patternProperties": {
|
||||
"^.+$": {"type": ["string", "number"]}
|
||||
}
|
||||
},
|
||||
"external": {
|
||||
"type": ["boolean", "object"],
|
||||
"properties": {
|
||||
"name": {"type": "string"}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"labels": {"$ref": "#/definitions/list_or_dict"}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
|
||||
"secret": {
|
||||
"id": "#/definitions/secret",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string"},
|
||||
"file": {"type": "string"},
|
||||
"external": {
|
||||
"type": ["boolean", "object"],
|
||||
"properties": {
|
||||
"name": {"type": "string"}
|
||||
}
|
||||
},
|
||||
"labels": {"$ref": "#/definitions/list_or_dict"}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
|
||||
"config": {
|
||||
"id": "#/definitions/config",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string"},
|
||||
"file": {"type": "string"},
|
||||
"external": {
|
||||
"type": ["boolean", "object"],
|
||||
"properties": {
|
||||
"name": {"type": "string"}
|
||||
}
|
||||
},
|
||||
"labels": {"$ref": "#/definitions/list_or_dict"}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
|
||||
"string_or_list": {
|
||||
"oneOf": [
|
||||
{"type": "string"},
|
||||
{"$ref": "#/definitions/list_of_strings"}
|
||||
]
|
||||
},
|
||||
|
||||
"list_of_strings": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"uniqueItems": true
|
||||
},
|
||||
|
||||
"list_or_dict": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"patternProperties": {
|
||||
".+": {
|
||||
"type": ["string", "number", "null"]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{"type": "array", "items": {"type": "string"}, "uniqueItems": true}
|
||||
]
|
||||
},
|
||||
|
||||
"constraints": {
|
||||
"service": {
|
||||
"id": "#/definitions/constraints/service",
|
||||
"anyOf": [
|
||||
{"required": ["build"]},
|
||||
{"required": ["image"]}
|
||||
],
|
||||
"properties": {
|
||||
"build": {
|
||||
"required": ["context"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -46,6 +46,25 @@ func TestValidateAllowsXTopLevelFields(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestValidateSecretConfigNames(t *testing.T) {
|
||||
config := dict{
|
||||
"version": "3.5",
|
||||
"configs": dict{
|
||||
"bar": dict{
|
||||
"name": "foobar",
|
||||
},
|
||||
},
|
||||
"secrets": dict{
|
||||
"baz": dict{
|
||||
"name": "foobaz",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err := Validate(config, "3.5")
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestValidateInvalidVersion(t *testing.T) {
|
||||
config := dict{
|
||||
"version": "2.1",
|
||||
@ -84,3 +103,16 @@ func TestValidatePlacement(t *testing.T) {
|
||||
|
||||
assert.NoError(t, Validate(config, "3.3"))
|
||||
}
|
||||
|
||||
func TestValidateIsolation(t *testing.T) {
|
||||
config := dict{
|
||||
"version": "3.5",
|
||||
"services": dict{
|
||||
"foo": dict{
|
||||
"image": "busybox",
|
||||
"isolation": "some-isolation-value",
|
||||
},
|
||||
},
|
||||
}
|
||||
assert.NoError(t, Validate(config, "3.5"))
|
||||
}
|
||||
|
||||
@ -55,6 +55,7 @@ type ConfigFile struct {
|
||||
|
||||
// ConfigDetails are the details about a group of ConfigFiles
|
||||
type ConfigDetails struct {
|
||||
Version string
|
||||
WorkingDir string
|
||||
ConfigFiles []ConfigFile
|
||||
Environment map[string]string
|
||||
@ -125,6 +126,7 @@ type ServiceConfig struct {
|
||||
User string
|
||||
Volumes []ServiceVolumeConfig
|
||||
WorkingDir string `mapstructure:"working_dir"`
|
||||
Isolation string `mapstructure:"isolation"`
|
||||
}
|
||||
|
||||
// BuildConfig is a type for build
|
||||
@ -215,8 +217,24 @@ type Resources struct {
|
||||
// Resource is a resource to be limited or reserved
|
||||
type Resource struct {
|
||||
// TODO: types to convert from units and ratios
|
||||
NanoCPUs string `mapstructure:"cpus"`
|
||||
MemoryBytes UnitBytes `mapstructure:"memory"`
|
||||
NanoCPUs string `mapstructure:"cpus"`
|
||||
MemoryBytes UnitBytes `mapstructure:"memory"`
|
||||
GenericResources []GenericResource `mapstructure:"generic_resources"`
|
||||
}
|
||||
|
||||
// GenericResource represents a "user defined" resource which can
|
||||
// only be an integer (e.g: SSD=3) for a service
|
||||
type GenericResource struct {
|
||||
DiscreteResourceSpec *DiscreteGenericResource `mapstructure:"discrete_resource_spec"`
|
||||
}
|
||||
|
||||
// DiscreteGenericResource represents a "user defined" resource which is defined
|
||||
// as an integer
|
||||
// "Kind" is used to describe the Kind of a resource (e.g: "GPU", "FPGA", "SSD", ...)
|
||||
// Value is used to count the resource (SSD=5, HDD=3, ...)
|
||||
type DiscreteGenericResource struct {
|
||||
Kind string
|
||||
Value int64
|
||||
}
|
||||
|
||||
// UnitBytes is the bytes type
|
||||
@ -277,7 +295,8 @@ type ServiceVolumeVolume struct {
|
||||
NoCopy bool `mapstructure:"nocopy"`
|
||||
}
|
||||
|
||||
type fileReferenceConfig struct {
|
||||
// FileReferenceConfig for a reference to a swarm file object
|
||||
type FileReferenceConfig struct {
|
||||
Source string
|
||||
Target string
|
||||
UID string
|
||||
@ -286,10 +305,10 @@ type fileReferenceConfig struct {
|
||||
}
|
||||
|
||||
// ServiceConfigObjConfig is the config obj configuration for a service
|
||||
type ServiceConfigObjConfig fileReferenceConfig
|
||||
type ServiceConfigObjConfig FileReferenceConfig
|
||||
|
||||
// ServiceSecretConfig is the secret configuration for a service
|
||||
type ServiceSecretConfig fileReferenceConfig
|
||||
type ServiceSecretConfig FileReferenceConfig
|
||||
|
||||
// UlimitsConfig the ulimit configuration
|
||||
type UlimitsConfig struct {
|
||||
@ -300,6 +319,7 @@ type UlimitsConfig struct {
|
||||
|
||||
// NetworkConfig for a network
|
||||
type NetworkConfig struct {
|
||||
Name string
|
||||
Driver string
|
||||
DriverOpts map[string]string `mapstructure:"driver_opts"`
|
||||
Ipam IPAMConfig
|
||||
@ -343,14 +363,16 @@ type CredentialSpecConfig struct {
|
||||
Registry string
|
||||
}
|
||||
|
||||
type fileObjectConfig struct {
|
||||
// FileObjectConfig is a config type for a file used by a service
|
||||
type FileObjectConfig struct {
|
||||
Name string
|
||||
File string
|
||||
External External
|
||||
Labels Labels
|
||||
}
|
||||
|
||||
// SecretConfig for a secret
|
||||
type SecretConfig fileObjectConfig
|
||||
type SecretConfig FileObjectConfig
|
||||
|
||||
// ConfigObjConfig is the config for the swarm "Config" object
|
||||
type ConfigObjConfig fileObjectConfig
|
||||
type ConfigObjConfig FileObjectConfig
|
||||
|
||||
@ -177,7 +177,7 @@ func (configFile *ConfigFile) Save() error {
|
||||
return configFile.SaveToWriter(f)
|
||||
}
|
||||
|
||||
// ParseProxyConfig computes proxy configuration by retreiving the config for the provided host and
|
||||
// ParseProxyConfig computes proxy configuration by retrieving the config for the provided host and
|
||||
// then checking this against any environment variables provided to the container
|
||||
func (configFile *ConfigFile) ParseProxyConfig(host string, runOpts []string) map[string]*string {
|
||||
var cfgKey string
|
||||
|
||||
@ -127,7 +127,7 @@ func GetNotaryRepository(in io.Reader, out io.Writer, userAgent string, repoInfo
|
||||
}
|
||||
|
||||
// Skip configuration headers since request is not going to Docker daemon
|
||||
modifiers := registry.DockerHeaders(userAgent, http.Header{})
|
||||
modifiers := registry.Headers(userAgent, http.Header{})
|
||||
authTransport := transport.NewTransport(base, modifiers...)
|
||||
pingClient := &http.Client{
|
||||
Transport: authTransport,
|
||||
@ -299,7 +299,7 @@ type ImageRefAndAuth struct {
|
||||
}
|
||||
|
||||
// GetImageReferencesAndAuth retrieves the necessary reference and auth information for an image name
|
||||
// as a ImageRefAndAuth struct
|
||||
// as an ImageRefAndAuth struct
|
||||
func GetImageReferencesAndAuth(ctx context.Context, authResolver func(ctx context.Context, index *registrytypes.IndexInfo) types.AuthConfig, imgName string) (ImageRefAndAuth, error) {
|
||||
ref, err := reference.ParseNormalizedNamed(imgName)
|
||||
if err != nil {
|
||||
|
||||
@ -3,7 +3,8 @@ package cli
|
||||
// Default build-time variable.
|
||||
// These values are overriding via ldflags
|
||||
var (
|
||||
Version = "unknown-version"
|
||||
GitCommit = "unknown-commit"
|
||||
BuildTime = "unknown-buildtime"
|
||||
PlatformName = ""
|
||||
Version = "unknown-version"
|
||||
GitCommit = "unknown-commit"
|
||||
BuildTime = "unknown-buildtime"
|
||||
)
|
||||
|
||||
@ -1,29 +0,0 @@
|
||||
// +build !daemon
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func newDaemonCommand() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "daemon",
|
||||
Hidden: true,
|
||||
Args: cobra.ArbitraryArgs,
|
||||
DisableFlagParsing: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runDaemon()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func runDaemon() error {
|
||||
return fmt.Errorf(
|
||||
"`docker daemon` is not supported on %s. Please run `dockerd` directly",
|
||||
strings.Title(runtime.GOOS))
|
||||
}
|
||||
@ -1,17 +0,0 @@
|
||||
// +build !daemon
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDaemonCommand(t *testing.T) {
|
||||
cmd := newDaemonCommand()
|
||||
cmd.SetArgs([]string{"--version"})
|
||||
err := cmd.Execute()
|
||||
|
||||
assert.EqualError(t, err, "Please run `dockerd`")
|
||||
}
|
||||
@ -1,33 +0,0 @@
|
||||
// +build daemon
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func stubRun(cmd *cobra.Command, args []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestDaemonCommandHelp(t *testing.T) {
|
||||
cmd := newDaemonCommand()
|
||||
cmd.RunE = stubRun
|
||||
cmd.SetArgs([]string{"--help"})
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
err := cmd.Execute()
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestDaemonCommand(t *testing.T) {
|
||||
cmd := newDaemonCommand()
|
||||
cmd.RunE = stubRun
|
||||
cmd.SetArgs([]string{"--containerd", "/foo"})
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
err := cmd.Execute()
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
@ -1,79 +0,0 @@
|
||||
// +build daemon
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const daemonBinary = "dockerd"
|
||||
|
||||
func newDaemonCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "daemon",
|
||||
Hidden: true,
|
||||
Args: cobra.ArbitraryArgs,
|
||||
DisableFlagParsing: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runDaemon()
|
||||
},
|
||||
Deprecated: "and will be removed in Docker 17.12. Please run `dockerd` directly.",
|
||||
}
|
||||
cmd.SetHelpFunc(helpFunc)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// CmdDaemon execs dockerd with the same flags
|
||||
func runDaemon() error {
|
||||
// Use os.Args[1:] so that "global" args are passed to dockerd
|
||||
return execDaemon(stripDaemonArg(os.Args[1:]))
|
||||
}
|
||||
|
||||
func execDaemon(args []string) error {
|
||||
binaryPath, err := findDaemonBinary()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return syscall.Exec(
|
||||
binaryPath,
|
||||
append([]string{daemonBinary}, args...),
|
||||
os.Environ())
|
||||
}
|
||||
|
||||
func helpFunc(cmd *cobra.Command, args []string) {
|
||||
if err := execDaemon([]string{"--help"}); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%s\n", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// findDaemonBinary looks for the path to the dockerd binary starting with
|
||||
// the directory of the current executable (if one exists) and followed by $PATH
|
||||
func findDaemonBinary() (string, error) {
|
||||
execDirname := filepath.Dir(os.Args[0])
|
||||
if execDirname != "" {
|
||||
binaryPath := filepath.Join(execDirname, daemonBinary)
|
||||
if _, err := os.Stat(binaryPath); err == nil {
|
||||
return binaryPath, nil
|
||||
}
|
||||
}
|
||||
|
||||
return exec.LookPath(daemonBinary)
|
||||
}
|
||||
|
||||
// stripDaemonArg removes the `daemon` argument from the list
|
||||
func stripDaemonArg(args []string) []string {
|
||||
for i, arg := range args {
|
||||
if arg == "daemon" {
|
||||
return append(args[:i], args[i+1:]...)
|
||||
}
|
||||
}
|
||||
return args
|
||||
}
|
||||
@ -39,10 +39,6 @@ func newDockerCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
return command.ShowHelp(dockerCli.Err())(cmd, args)
|
||||
},
|
||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
// daemon command is special, we redirect directly to another binary
|
||||
if cmd.Name() == "daemon" {
|
||||
return nil
|
||||
}
|
||||
// flags must be the top-level command flags, not cmd.Flags()
|
||||
opts.Common.SetDefaultOptions(flags)
|
||||
dockerPreRun(opts)
|
||||
@ -64,7 +60,6 @@ func newDockerCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
setHelpFunc(dockerCli, cmd, flags, opts)
|
||||
|
||||
cmd.SetOutput(dockerCli.Out())
|
||||
cmd.AddCommand(newDaemonCommand())
|
||||
commands.AddCommands(cmd, dockerCli)
|
||||
|
||||
setValidateArgs(dockerCli, cmd, flags, opts)
|
||||
|
||||
@ -833,14 +833,14 @@ __docker_complete_log_options() {
|
||||
local common_options2="env env-regex labels"
|
||||
|
||||
# awslogs does not implement the $common_options2.
|
||||
local awslogs_options="$common_options1 awslogs-create-group awslogs-datetime-format awslogs-group awslogs-multiline-pattern awslogs-region awslogs-stream tag"
|
||||
local awslogs_options="$common_options1 awslogs-create-group awslogs-credentials-endpoint awslogs-datetime-format awslogs-group awslogs-multiline-pattern awslogs-region awslogs-stream tag"
|
||||
|
||||
local fluentd_options="$common_options1 $common_options2 fluentd-address fluentd-async-connect fluentd-buffer-limit fluentd-retry-wait fluentd-max-retries tag"
|
||||
local fluentd_options="$common_options1 $common_options2 fluentd-address fluentd-async-connect fluentd-buffer-limit fluentd-retry-wait fluentd-max-retries fluentd-sub-second-precision tag"
|
||||
local gcplogs_options="$common_options1 $common_options2 gcp-log-cmd gcp-meta-id gcp-meta-name gcp-meta-zone gcp-project"
|
||||
local gelf_options="$common_options1 $common_options2 gelf-address gelf-compression-level gelf-compression-type tag"
|
||||
local gelf_options="$common_options1 $common_options2 gelf-address gelf-compression-level gelf-compression-type gelf-tcp-max-reconnect gelf-tcp-reconnect-delay tag"
|
||||
local journald_options="$common_options1 $common_options2 tag"
|
||||
local json_file_options="$common_options1 $common_options2 max-file max-size"
|
||||
local logentries_options="$common_options1 $common_options2 logentries-token tag"
|
||||
local logentries_options="$common_options1 $common_options2 line-only logentries-token tag"
|
||||
local splunk_options="$common_options1 $common_options2 splunk-caname splunk-capath splunk-format splunk-gzip splunk-gzip-level splunk-index splunk-insecureskipverify splunk-source splunk-sourcetype splunk-token splunk-url splunk-verify-connection tag"
|
||||
local syslog_options="$common_options1 $common_options2 syslog-address syslog-facility syslog-format syslog-tls-ca-cert syslog-tls-cert syslog-tls-key syslog-tls-skip-verify tag"
|
||||
|
||||
@ -892,12 +892,21 @@ __docker_complete_log_driver_options() {
|
||||
COMPREPLY=( $( compgen -W "false true" -- "${cur##*=}" ) )
|
||||
return
|
||||
;;
|
||||
awslogs-credentials-endpoint)
|
||||
COMPREPLY=( $( compgen -W "/" -- "${cur##*=}" ) )
|
||||
__docker_nospace
|
||||
return
|
||||
;;
|
||||
fluentd-async-connect)
|
||||
COMPREPLY=( $( compgen -W "false true" -- "${cur##*=}" ) )
|
||||
return
|
||||
;;
|
||||
fluentd-sub-second-precision)
|
||||
COMPREPLY=( $( compgen -W "false true" -- "${cur##*=}" ) )
|
||||
return
|
||||
;;
|
||||
gelf-address)
|
||||
COMPREPLY=( $( compgen -W "udp" -S "://" -- "${cur##*=}" ) )
|
||||
COMPREPLY=( $( compgen -W "tcp udp" -S "://" -- "${cur##*=}" ) )
|
||||
__docker_nospace
|
||||
return
|
||||
;;
|
||||
@ -909,6 +918,10 @@ __docker_complete_log_driver_options() {
|
||||
COMPREPLY=( $( compgen -W "gzip none zlib" -- "${cur##*=}" ) )
|
||||
return
|
||||
;;
|
||||
line-only)
|
||||
COMPREPLY=( $( compgen -W "false true" -- "${cur##*=}" ) )
|
||||
return
|
||||
;;
|
||||
mode)
|
||||
COMPREPLY=( $( compgen -W "blocking non-blocking" -- "${cur##*=}" ) )
|
||||
return
|
||||
@ -1430,11 +1443,14 @@ _docker_container_exec() {
|
||||
__docker_complete_user_group
|
||||
return
|
||||
;;
|
||||
--workdir|-w)
|
||||
return
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$cur" in
|
||||
-*)
|
||||
COMPREPLY=( $( compgen -W "--detach -d --detach-keys --env -e --help --interactive -i --privileged -t --tty -u --user" -- "$cur" ) )
|
||||
COMPREPLY=( $( compgen -W "--detach -d --detach-keys --env -e --help --interactive -i --privileged -t --tty -u --user --workdir -w" -- "$cur" ) )
|
||||
;;
|
||||
*)
|
||||
__docker_complete_containers_running
|
||||
@ -1487,17 +1503,17 @@ _docker_container_kill() {
|
||||
|
||||
_docker_container_logs() {
|
||||
case "$prev" in
|
||||
--since|--tail)
|
||||
--since|--tail|--until)
|
||||
return
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$cur" in
|
||||
-*)
|
||||
COMPREPLY=( $( compgen -W "--details --follow -f --help --since --tail --timestamps -t" -- "$cur" ) )
|
||||
COMPREPLY=( $( compgen -W "--details --follow -f --help --since --tail --timestamps -t --until" -- "$cur" ) )
|
||||
;;
|
||||
*)
|
||||
local counter=$(__docker_pos_first_nonflag '--since|--tail')
|
||||
local counter=$(__docker_pos_first_nonflag '--since|--tail|--until')
|
||||
if [ "$cword" -eq "$counter" ]; then
|
||||
__docker_complete_containers_all
|
||||
fi
|
||||
@ -1763,6 +1779,9 @@ _docker_container_run_and_create() {
|
||||
--io-maxiops
|
||||
--isolation
|
||||
"
|
||||
__docker_daemon_is_experimental && options_with_args+="
|
||||
--platform
|
||||
"
|
||||
|
||||
local boolean_options="
|
||||
--disable-content-trust=false
|
||||
@ -2467,7 +2486,15 @@ _docker_image_build() {
|
||||
--quiet -q
|
||||
--rm
|
||||
"
|
||||
__docker_daemon_is_experimental && boolean_options+="--squash"
|
||||
if __docker_daemon_is_experimental ; then
|
||||
options_with_args+="
|
||||
--platform
|
||||
"
|
||||
boolean_options+="
|
||||
--squash
|
||||
--stream
|
||||
"
|
||||
fi
|
||||
|
||||
local all_options="$options_with_args $boolean_options"
|
||||
|
||||
@ -2679,12 +2706,21 @@ _docker_image_prune() {
|
||||
}
|
||||
|
||||
_docker_image_pull() {
|
||||
case "$prev" in
|
||||
--platform)
|
||||
return
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$cur" in
|
||||
-*)
|
||||
COMPREPLY=( $( compgen -W "--all-tags -a --disable-content-trust=false --help" -- "$cur" ) )
|
||||
local options="--all-tags -a --disable-content-trust=false --help"
|
||||
__docker_daemon_is_experimental && options+=" --platform"
|
||||
|
||||
COMPREPLY=( $( compgen -W "$options" -- "$cur" ) )
|
||||
;;
|
||||
*)
|
||||
local counter=$(__docker_pos_first_nonflag)
|
||||
local counter=$(__docker_pos_first_nonflag --platform)
|
||||
if [ "$cword" -eq "$counter" ]; then
|
||||
for arg in "${COMP_WORDS[@]}"; do
|
||||
case "$arg" in
|
||||
@ -3305,6 +3341,7 @@ _docker_service_update_and_create() {
|
||||
--health-start-period
|
||||
--health-timeout
|
||||
--hostname
|
||||
--isolation
|
||||
--label -l
|
||||
--limit-cpu
|
||||
--limit-memory
|
||||
@ -3493,6 +3530,10 @@ _docker_service_update_and_create() {
|
||||
__docker_nospace
|
||||
return
|
||||
;;
|
||||
--isolation)
|
||||
__docker_complete_isolation
|
||||
return
|
||||
;;
|
||||
--log-driver)
|
||||
__docker_complete_log_drivers
|
||||
return
|
||||
@ -4510,7 +4551,7 @@ _docker_system() {
|
||||
info
|
||||
prune
|
||||
"
|
||||
__docker_subcommands "$subcommands $aliases" && return
|
||||
__docker_subcommands "$subcommands" && return
|
||||
|
||||
case "$cur" in
|
||||
-*)
|
||||
@ -4665,6 +4706,68 @@ _docker_tag() {
|
||||
_docker_image_tag
|
||||
}
|
||||
|
||||
|
||||
_docker_trust() {
|
||||
local subcommands="
|
||||
revoke
|
||||
sign
|
||||
view
|
||||
"
|
||||
__docker_subcommands "$subcommands" && return
|
||||
|
||||
case "$cur" in
|
||||
-*)
|
||||
COMPREPLY=( $( compgen -W "--help" -- "$cur" ) )
|
||||
;;
|
||||
*)
|
||||
COMPREPLY=( $( compgen -W "$subcommands" -- "$cur" ) )
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
_docker_trust_revoke() {
|
||||
case "$cur" in
|
||||
-*)
|
||||
COMPREPLY=( $( compgen -W "--help --yes -y" -- "$cur" ) )
|
||||
;;
|
||||
*)
|
||||
local counter=$(__docker_pos_first_nonflag)
|
||||
if [ "$cword" -eq "$counter" ]; then
|
||||
__docker_complete_images
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
_docker_trust_sign() {
|
||||
case "$cur" in
|
||||
-*)
|
||||
COMPREPLY=( $( compgen -W "--help --local" -- "$cur" ) )
|
||||
;;
|
||||
*)
|
||||
local counter=$(__docker_pos_first_nonflag)
|
||||
if [ "$cword" -eq "$counter" ]; then
|
||||
__docker_complete_images
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
_docker_trust_view() {
|
||||
case "$cur" in
|
||||
-*)
|
||||
COMPREPLY=( $( compgen -W "--help" -- "$cur" ) )
|
||||
;;
|
||||
*)
|
||||
local counter=$(__docker_pos_first_nonflag)
|
||||
if [ "$cword" -eq "$counter" ]; then
|
||||
__docker_complete_images
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
|
||||
_docker_unpause() {
|
||||
_docker_container_unpause
|
||||
}
|
||||
@ -4892,6 +4995,7 @@ _docker() {
|
||||
local experimental_commands=(
|
||||
checkpoint
|
||||
deploy
|
||||
trust
|
||||
)
|
||||
|
||||
local commands=(${management_commands[*]} ${top_level_commands[*]})
|
||||
|
||||
@ -174,6 +174,7 @@ complete -c docker -A -f -n '__fish_seen_subcommand_from exec' -s d -l detach -d
|
||||
complete -c docker -A -f -n '__fish_seen_subcommand_from exec' -l help -d 'Print usage'
|
||||
complete -c docker -A -f -n '__fish_seen_subcommand_from exec' -s i -l interactive -d 'Keep STDIN open even if not attached'
|
||||
complete -c docker -A -f -n '__fish_seen_subcommand_from exec' -s t -l tty -d 'Allocate a pseudo-TTY'
|
||||
complete -c docker -A -f -n '__fish_seen_subcommand_from exec' -s w -l workdir -d 'Working directory inside the container'
|
||||
complete -c docker -A -f -n '__fish_seen_subcommand_from exec' -a '(__fish_print_docker_containers running)' -d "Container"
|
||||
|
||||
# export
|
||||
|
||||
@ -226,7 +226,7 @@ __docker_get_log_options() {
|
||||
common_options=("max-buffer-size" "mode")
|
||||
common_options2=("env" "env-regex" "labels")
|
||||
awslogs_options=($common_options "awslogs-create-group" "awslogs-datetime-format" "awslogs-group" "awslogs-multiline-pattern" "awslogs-region" "awslogs-stream" "tag")
|
||||
fluentd_options=($common_options $common_options2 "fluentd-address" "fluentd-async-connect" "fluentd-buffer-limit" "fluentd-retry-wait" "fluentd-max-retries" "tag")
|
||||
fluentd_options=($common_options $common_options2 "fluentd-address" "fluentd-async-connect" "fluentd-buffer-limit" "fluentd-retry-wait" "fluentd-max-retries" "fluentd-sub-second-precision" "tag")
|
||||
gcplogs_options=($common_options $common_options2 "gcp-log-cmd" "gcp-meta-id" "gcp-meta-name" "gcp-meta-zone" "gcp-project")
|
||||
gelf_options=($common_options $common_options2 "gelf-address" "gelf-compression-level" "gelf-compression-type" "tag")
|
||||
journald_options=($common_options $common_options2 "tag")
|
||||
@ -745,6 +745,7 @@ __docker_container_subcommand() {
|
||||
"($help)--privileged[Give extended Linux capabilities to the command]" \
|
||||
"($help -t --tty)"{-t,--tty}"[Allocate a pseudo-tty]" \
|
||||
"($help -u --user)"{-u=,--user=}"[Username or UID]:user:_users" \
|
||||
"($help -w --workdir)"{-w=,--workdir=}"[Working directory inside the container]:directory:_directories"
|
||||
"($help -):containers:__docker_complete_running_containers" \
|
||||
"($help -)*::command:->anycommand" && ret=0
|
||||
case $state in
|
||||
@ -1393,7 +1394,7 @@ __docker_nodes() {
|
||||
# Names
|
||||
if [[ $type = (names|all) ]]; then
|
||||
for line in $lines; do
|
||||
s="${line[${begin[NAME]},${end[NAME]}]%% ##}"
|
||||
s="${line[${begin[HOSTNAME]},${end[HOSTNAME]}]%% ##}"
|
||||
nodes=($nodes $s)
|
||||
done
|
||||
fi
|
||||
@ -1955,6 +1956,7 @@ __docker_service_subcommand() {
|
||||
"($help)--health-retries=[Consecutive failures needed to report unhealthy]:retries:(1 2 3 4 5)"
|
||||
"($help)--health-timeout=[Maximum time to allow one check to run]:time: "
|
||||
"($help)--hostname=[Service container hostname]:hostname: " \
|
||||
"($help)--isolation=[Service container isolation mode]:isolation:(default process hyperv)" \
|
||||
"($help)*--label=[Service labels]:label: "
|
||||
"($help)--limit-cpu=[Limit CPUs]:value: "
|
||||
"($help)--limit-memory=[Limit Memory]:value: "
|
||||
@ -2168,9 +2170,9 @@ __docker_stacks() {
|
||||
end[${header[$i,$((j-1))]}]=-1
|
||||
lines=(${lines[2,-1]})
|
||||
|
||||
# Service ID
|
||||
# Service NAME
|
||||
for line in $lines; do
|
||||
s="${line[${begin[ID]},${end[ID]}]%% ##}"
|
||||
s="${line[${begin[NAME]},${end[NAME]}]%% ##}"
|
||||
stacks=($stacks $s)
|
||||
done
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@ CROSS_IMAGE_NAME = docker-cli-cross$(IMAGE_TAG)
|
||||
VALIDATE_IMAGE_NAME = docker-cli-shell-validate$(IMAGE_TAG)
|
||||
MOUNTS = -v "$(CURDIR)":/go/src/github.com/docker/cli
|
||||
VERSION = $(shell cat VERSION)
|
||||
ENVVARS = -e VERSION=$(VERSION) -e GITCOMMIT
|
||||
ENVVARS = -e VERSION=$(VERSION) -e GITCOMMIT -e PLATFORM
|
||||
|
||||
# build docker image (dockerfiles/Dockerfile.build)
|
||||
.PHONY: build_docker_image
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
FROM dockercore/golang-cross@sha256:8a347b3692ba925dcef753f2de289e11774410c1bc752b9d525cb05477a7697b
|
||||
FROM dockercore/golang-cross@sha256:2e843a0e4d82b6bab34d2cb7abe26d1a6cda23226ecc3869100c8db553603f9b
|
||||
ENV DISABLE_WARN_OUTSIDE_CONTAINER=1
|
||||
WORKDIR /go/src/github.com/docker/cli
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
|
||||
FROM golang:1.8.4-alpine3.6
|
||||
FROM golang:1.9.2-alpine3.6
|
||||
|
||||
RUN apk add -U git make bash coreutils ca-certificates
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
FROM golang:1.8.4-alpine3.6
|
||||
FROM golang:1.9.2-alpine3.6
|
||||
|
||||
RUN apk add -U git
|
||||
|
||||
|
||||
@ -81,7 +81,7 @@ The `filter` param to filter the list of image by reference (name or name:tag) i
|
||||
### `docker daemon` subcommand
|
||||
**Deprecated In Release: [v1.13.0](https://github.com/docker/docker/releases/tag/v1.13.0)**
|
||||
|
||||
**Target For Removal In Release: v17.12**
|
||||
**Removed In Release: v17.12**
|
||||
|
||||
The daemon is moved to a separate binary (`dockerd`), and should be used instead.
|
||||
|
||||
|
||||
@ -126,7 +126,7 @@ Config provides the base accessible fields for working with V0 plugin format
|
||||
- **`propagatedMount`** *string*
|
||||
|
||||
path to be mounted as rshared, so that mounts under that path are visible to docker. This is useful for volume plugins.
|
||||
This path will be bind-mounted outisde of the plugin rootfs so it's contents
|
||||
This path will be bind-mounted outside of the plugin rootfs so it's contents
|
||||
are preserved on upgrade.
|
||||
|
||||
- **`env`** *PluginEnv array*
|
||||
|
||||
@ -65,7 +65,7 @@ The sections below provide an inexhaustive overview of available plugins.
|
||||
| [Convoy plugin](https://github.com/rancher/convoy) | A volume plugin for a variety of storage back-ends including device mapper and NFS. It's a simple standalone executable written in Go and provides the framework to support vendor-specific extensions such as snapshots, backups and restore. |
|
||||
| [DigitalOcean Block Storage plugin](https://github.com/omallo/docker-volume-plugin-dostorage) | Integrates DigitalOcean's [block storage solution](https://www.digitalocean.com/products/storage/) into the Docker ecosystem by automatically attaching a given block storage volume to a DigitalOcean droplet and making the contents of the volume available to Docker containers running on that droplet. |
|
||||
| [DRBD plugin](https://www.drbd.org/en/supported-projects/docker) | A volume plugin that provides highly available storage replicated by [DRBD](https://www.drbd.org). Data written to the docker volume is replicated in a cluster of DRBD nodes. |
|
||||
| [Flocker plugin](https://clusterhq.com/docker-plugin/) | A volume plugin that provides multi-host portable volumes for Docker, enabling you to run databases and other stateful containers and move them around across a cluster of machines. |
|
||||
| [Flocker plugin](https://github.com/ScatterHQ/flocker) | A volume plugin that provides multi-host portable volumes for Docker, enabling you to run databases and other stateful containers and move them around across a cluster of machines. |
|
||||
| [Fuxi Volume Plugin](https://github.com/openstack/fuxi) | A volume plugin that is developed as part of the OpenStack Kuryr project and implements the Docker volume plugin API by utilizing Cinder, the OpenStack block storage service. |
|
||||
| [gce-docker plugin](https://github.com/mcuadros/gce-docker) | A volume plugin able to attach, format and mount Google Compute [persistent-disks](https://cloud.google.com/compute/docs/disks/persistent-disks). |
|
||||
| [GlusterFS plugin](https://github.com/calavera/docker-volume-glusterfs) | A volume plugin that provides multi-host volumes management for Docker using GlusterFS. |
|
||||
|
||||
@ -199,7 +199,7 @@ Request the path to the volume with the given `volume_name`.
|
||||
|
||||
```json
|
||||
{
|
||||
"Mountpoin": "/path/to/directory/on/host",
|
||||
"Mountpoint": "/path/to/directory/on/host",
|
||||
"Err": ""
|
||||
}
|
||||
```
|
||||
|
||||
@ -765,7 +765,7 @@ type of documentation between the person who builds the image and the person who
|
||||
runs the container, about which ports are intended to be published. To actually
|
||||
publish the port when running the container, use the `-p` flag on `docker run`
|
||||
to publish and map one or more ports, or the `-P` flag to publish all exposed
|
||||
ports and map them to to high-order ports.
|
||||
ports and map them to high-order ports.
|
||||
|
||||
To set up port redirection on the host system, see [using the -P
|
||||
flag](run.md#expose-incoming-ports). The `docker network` command supports
|
||||
@ -823,16 +823,23 @@ change them using `docker run --env <key>=<value>`.
|
||||
|
||||
ADD has two forms:
|
||||
|
||||
- `ADD <src>... <dest>`
|
||||
- `ADD ["<src>",... "<dest>"]` (this form is required for paths containing
|
||||
- `ADD [--chown=<user>:<group>] <src>... <dest>`
|
||||
- `ADD [--chown=<user>:<group>] ["<src>",... "<dest>"]` (this form is required for paths containing
|
||||
whitespace)
|
||||
|
||||
> **Note**:
|
||||
> The `--chown` feature is only supported on Dockerfiles used to build Linux containers,
|
||||
> and will not work on Windows containers. Since user and group ownership concepts do
|
||||
> not translate between Linux and Windows, the use of `/etc/passwd` and `/etc/group` for
|
||||
> translating user and group names to IDs restricts this feature to only be viable for
|
||||
> for Linux OS-based containers.
|
||||
|
||||
The `ADD` instruction copies new files, directories or remote file URLs from `<src>`
|
||||
and adds them to the filesystem of the image at the path `<dest>`.
|
||||
|
||||
Multiple `<src>` resource may be specified but if they are files or
|
||||
directories then they must be relative to the source directory that is
|
||||
being built (the context of the build).
|
||||
Multiple `<src>` resources may be specified but if they are files or
|
||||
directories, their paths are interpreted as relative to the source of
|
||||
the context of the build.
|
||||
|
||||
Each `<src>` may contain wildcards and matching will be done using Go's
|
||||
[filepath.Match](http://golang.org/pkg/path/filepath#Match) rules. For example:
|
||||
@ -854,7 +861,26 @@ named `arr[0].txt`, use the following;
|
||||
ADD arr[[]0].txt /mydir/ # copy a file named "arr[0].txt" to /mydir/
|
||||
|
||||
|
||||
All new files and directories are created with a UID and GID of 0.
|
||||
All new files and directories are created with a UID and GID of 0, unless the
|
||||
optional `--chown` flag specifies a given username, groupname, or UID/GID
|
||||
combination to request specific ownership of the content added. The
|
||||
format of the `--chown` flag allows for either username and groupname strings
|
||||
or direct integer UID and GID in any combination. Providing a username without
|
||||
groupname or a UID without GID will use the same numeric UID as the GID. If a
|
||||
username or groupname is provided, the container's root filesystem
|
||||
`/etc/passwd` and `/etc/group` files will be used to perform the translation
|
||||
from name to integer UID or GID respectively. The following examples show
|
||||
valid definitions for the `--chown` flag:
|
||||
|
||||
ADD --chown=55:mygroup files* /somedir/
|
||||
ADD --chown=bin files* /somedir/
|
||||
ADD --chown=1 files* /somedir/
|
||||
ADD --chown=10:11 files* /somedir/
|
||||
|
||||
If the container root filesystem does not contain either `/etc/passwd` or
|
||||
`/etc/group` files and either user or group names are used in the `--chown`
|
||||
flag, the build will fail on the `ADD` operation. Using numeric IDs requires
|
||||
no lookup and will not depend on container root filesystem content.
|
||||
|
||||
In the case where `<src>` is a remote file URL, the destination will
|
||||
have permissions of 600. If the remote file being retrieved has an HTTP
|
||||
@ -944,15 +970,23 @@ guide](https://docs.docker.com/engine/userguide/eng-image/dockerfile_best-practi
|
||||
|
||||
COPY has two forms:
|
||||
|
||||
- `COPY <src>... <dest>`
|
||||
- `COPY ["<src>",... "<dest>"]` (this form is required for paths containing
|
||||
- `COPY [--chown=<user>:<group>] <src>... <dest>`
|
||||
- `COPY [--chown=<user>:<group>] ["<src>",... "<dest>"]` (this form is required for paths containing
|
||||
whitespace)
|
||||
|
||||
> **Note**:
|
||||
> The `--chown` feature is only supported on Dockerfiles used to build Linux containers,
|
||||
> and will not work on Windows containers. Since user and group ownership concepts do
|
||||
> not translate between Linux and Windows, the use of `/etc/passwd` and `/etc/group` for
|
||||
> translating user and group names to IDs restricts this feature to only be viable for
|
||||
> for Linux OS-based containers.
|
||||
|
||||
The `COPY` instruction copies new files or directories from `<src>`
|
||||
and adds them to the filesystem of the container at the path `<dest>`.
|
||||
|
||||
Multiple `<src>` resource may be specified but they must be relative
|
||||
to the source directory that is being built (the context of the build).
|
||||
Multiple `<src>` resources may be specified but the paths of files and
|
||||
directories will be interpreted as relative to the source of the context
|
||||
of the build.
|
||||
|
||||
Each `<src>` may contain wildcards and matching will be done using Go's
|
||||
[filepath.Match](http://golang.org/pkg/path/filepath#Match) rules. For example:
|
||||
@ -974,7 +1008,26 @@ named `arr[0].txt`, use the following;
|
||||
|
||||
COPY arr[[]0].txt /mydir/ # copy a file named "arr[0].txt" to /mydir/
|
||||
|
||||
All new files and directories are created with a UID and GID of 0.
|
||||
All new files and directories are created with a UID and GID of 0, unless the
|
||||
optional `--chown` flag specifies a given username, groupname, or UID/GID
|
||||
combination to request specific ownership of the copied content. The
|
||||
format of the `--chown` flag allows for either username and groupname strings
|
||||
or direct integer UID and GID in any combination. Providing a username without
|
||||
groupname or a UID without GID will use the same numeric UID as the GID. If a
|
||||
username or groupname is provided, the container's root filesystem
|
||||
`/etc/passwd` and `/etc/group` files will be used to perform the translation
|
||||
from name to integer UID or GID respectively. The following examples show
|
||||
valid definitions for the `--chown` flag:
|
||||
|
||||
COPY --chown=55:mygroup files* /somedir/
|
||||
COPY --chown=bin files* /somedir/
|
||||
COPY --chown=1 files* /somedir/
|
||||
COPY --chown=10:11 files* /somedir/
|
||||
|
||||
If the container root filesystem does not contain either `/etc/passwd` or
|
||||
`/etc/group` files and either user or group names are used in the `--chown`
|
||||
flag, the build will fail on the `COPY` operation. Using numeric IDs requires
|
||||
no lookup and will not depend on container root filesystem content.
|
||||
|
||||
> **Note**:
|
||||
> If you build using STDIN (`docker build - < somefile`), there is no
|
||||
@ -1284,7 +1337,7 @@ consider the following Dockerfile snippet:
|
||||
RUN echo "hello world" > /myvol/greeting
|
||||
VOLUME /myvol
|
||||
|
||||
This Dockerfile results in an image that causes `docker run`, to
|
||||
This Dockerfile results in an image that causes `docker run` to
|
||||
create a new mount point at `/myvol` and copy the `greeting` file
|
||||
into the newly created volume.
|
||||
|
||||
@ -1306,8 +1359,8 @@ Keep the following things in mind about volumes in the `Dockerfile`.
|
||||
|
||||
- **The host directory is declared at container run-time**: The host directory
|
||||
(the mountpoint) is, by its nature, host-dependent. This is to preserve image
|
||||
portability. since a given host directory can't be guaranteed to be available
|
||||
on all hosts.For this reason, you can't mount a host directory from
|
||||
portability, since a given host directory can't be guaranteed to be available
|
||||
on all hosts. For this reason, you can't mount a host directory from
|
||||
within the Dockerfile. The `VOLUME` instruction does not support specifying a `host-dir`
|
||||
parameter. You must specify the mountpoint when you create or run the container.
|
||||
|
||||
@ -1322,9 +1375,20 @@ group (or GID) to use when running the image and for any `RUN`, `CMD` and
|
||||
`ENTRYPOINT` instructions that follow it in the `Dockerfile`.
|
||||
|
||||
> **Warning**:
|
||||
> When the user does doesn't have a primary group then the image (or the next
|
||||
> When the user doesn't have a primary group then the image (or the next
|
||||
> instructions) will be run with the `root` group.
|
||||
|
||||
> On Windows, the user must be created first if it's not a built-in account.
|
||||
> This can be done with the `net user` command called as part of a Dockerfile.
|
||||
|
||||
```Dockerfile
|
||||
FROM microsoft/windowsservercore
|
||||
# Create Windows user in the container
|
||||
RUN net user /add patrick
|
||||
# Set it for subsequent commands
|
||||
USER patrick
|
||||
```
|
||||
|
||||
|
||||
## WORKDIR
|
||||
|
||||
|
||||
@ -512,7 +512,7 @@ The `--squash` option has a number of known limitations:
|
||||
layers in tact, and one for the squashed version.
|
||||
- While squashing layers may produce smaller images, it may have a negative
|
||||
impact on performance, as a single layer takes longer to extract, and
|
||||
downloading a single layer cannot be paralellized.
|
||||
downloading a single layer cannot be parallelized.
|
||||
- When attempting to squash an image that does not make changes to the
|
||||
filesystem (for example, the Dockerfile only contains `ENV` instructions),
|
||||
the squash step will fail (see [issue #33823](https://github.com/moby/moby/issues/33823)
|
||||
|
||||
@ -217,7 +217,7 @@ For the `devicemapper`, `btrfs`, `windowsfilter` and `zfs` graph drivers,
|
||||
user cannot pass a size less than the Default BaseFS Size.
|
||||
For the `overlay2` storage driver, the size option is only available if the
|
||||
backing fs is `xfs` and mounted with the `pquota` mount option.
|
||||
Under these conditions, user can pass any size less then the backing fs size.
|
||||
Under these conditions, user can pass any size less than the backing fs size.
|
||||
|
||||
### Specify isolation technology for container (--isolation)
|
||||
|
||||
|
||||
@ -72,6 +72,7 @@ Options:
|
||||
--max-concurrent-uploads int Set the max concurrent uploads for each push (default 5)
|
||||
--metrics-addr string Set default address and port to serve the metrics api on
|
||||
--mtu int Set the containers network MTU
|
||||
--node-generic-resources list Advertise user-defined resource
|
||||
--no-new-privileges Set no-new-privileges by default for new containers
|
||||
--oom-score-adjust int Set the oom_score_adj for the daemon (default -500)
|
||||
-p, --pidfile string Path to use for daemon PID file (default "/var/run/docker.pid")
|
||||
@ -393,7 +394,7 @@ $ sudo dockerd --storage-opt dm.thinp_autoextend_threshold=80
|
||||
|
||||
##### `dm.thinp_autoextend_percent`
|
||||
|
||||
Sets the value percentage value to increase the thin pool by when when `lvm`
|
||||
Sets the value percentage value to increase the thin pool by when `lvm`
|
||||
attempts to autoextend the available space [100 = disabled]
|
||||
|
||||
###### Example:
|
||||
@ -1237,6 +1238,23 @@ Please note that this feature is still marked as experimental as metrics and met
|
||||
names could change while this feature is still in experimental. Please provide
|
||||
feedback on what you would like to see collected in the API.
|
||||
|
||||
#### Node Generic Resources
|
||||
|
||||
The `--node-generic-resources` option takes a list of key-value
|
||||
pair (`key=value`) that allows you to advertise user defined resources
|
||||
in a swarm cluster.
|
||||
|
||||
The current expected use case is to advertise NVIDIA GPUs so that services
|
||||
requesting `NVIDIA-GPU=[0-16]` can land on a node that has enough GPUs for
|
||||
the task to run.
|
||||
|
||||
Example of usage:
|
||||
```json
|
||||
{
|
||||
"node-generic-resources": ["NVIDIA-GPU=UUID1", "NVIDIA-GPU=UUID2"]
|
||||
}
|
||||
```
|
||||
|
||||
### Daemon configuration file
|
||||
|
||||
The `--config-file` option allows you to set any configuration option
|
||||
@ -1325,6 +1343,7 @@ This is a full example of the allowed configuration options on Linux:
|
||||
"no-new-privileges": false,
|
||||
"default-runtime": "runc",
|
||||
"oom-score-adjust": -500,
|
||||
"node-generic-resources": ["NVIDIA-GPU=UUID1", "NVIDIA-GPU=UUID2"],
|
||||
"runtimes": {
|
||||
"cc-runtime": {
|
||||
"path": "/usr/bin/cc-runtime"
|
||||
|
||||
@ -29,6 +29,7 @@ Options:
|
||||
--privileged Give extended privileges to the command
|
||||
-t, --tty Allocate a pseudo-TTY
|
||||
-u, --user Username or UID (format: <name|uid>[:<group|gid>])
|
||||
-w, --workdir Working directory inside the container
|
||||
```
|
||||
|
||||
## Description
|
||||
@ -39,7 +40,7 @@ The command started using `docker exec` only runs while the container's primary
|
||||
process (`PID 1`) is running, and it is not restarted if the container is
|
||||
restarted.
|
||||
|
||||
COMMAND will run in the default directory of the container. It the
|
||||
COMMAND will run in the default directory of the container. If the
|
||||
underlying image has a custom directory specified with the WORKDIR directive
|
||||
in its Dockerfile, this will be used instead.
|
||||
|
||||
@ -86,6 +87,20 @@ This will create a new Bash session in the container `ubuntu_bash` with environm
|
||||
variable `$VAR` set to "1". Note that this environment variable will only be valid
|
||||
on the current Bash session.
|
||||
|
||||
By default `docker exec` command runs in the same working directory set when container was created.
|
||||
|
||||
```bash
|
||||
$ docker exec -it ubuntu_bash pwd
|
||||
/
|
||||
```
|
||||
|
||||
You can select working directory for the command to execute into
|
||||
|
||||
```bash
|
||||
$ docker exec -it -w /root ubuntu_bash pwd
|
||||
/root
|
||||
```
|
||||
|
||||
|
||||
### Try to run `docker exec` on a paused container
|
||||
|
||||
|
||||
@ -47,7 +47,7 @@ describes all the details of the format.
|
||||
|
||||
The `docker inspect` command matches any type of object by either ID or name.
|
||||
In some cases multiple type of objects (for example, a container and a volume)
|
||||
exist with the same name, making the result ambigious.
|
||||
exist with the same name, making the result ambiguous.
|
||||
|
||||
To restrict `docker inspect` to a specific type of object, use the `--type`
|
||||
option.
|
||||
|
||||
@ -25,6 +25,7 @@ Options:
|
||||
-f, --follow Follow log output
|
||||
--help Print usage
|
||||
--since string Show logs since timestamp (e.g. 2013-01-02T13:23:37) or relative (e.g. 42m for 42 minutes)
|
||||
--until string Show logs before timestamp (e.g. 2013-01-02T13:23:37) or relative (e.g. 42m for 42 minutes)
|
||||
--tail string Number of lines to show from the end of the logs (default "all")
|
||||
-t, --timestamps Show timestamps
|
||||
```
|
||||
@ -66,3 +67,19 @@ that have elapsed since January 1, 1970 (midnight UTC/GMT), not counting leap
|
||||
seconds (aka Unix epoch or Unix time), and the optional .nanoseconds field is a
|
||||
fraction of a second no more than nine digits long. You can combine the
|
||||
`--since` option with either or both of the `--follow` or `--tail` options.
|
||||
|
||||
## Examples
|
||||
|
||||
### Retrieve logs until a specific point in time
|
||||
|
||||
In order to retrieve logs before a specific point in time, run:
|
||||
|
||||
```bash
|
||||
$ docker run --name test -d busybox sh -c "while true; do $(echo date); sleep 1; done"
|
||||
$ date
|
||||
Tue 14 Nov 2017 16:40:00 CET
|
||||
$ docker logs -f --until=2s
|
||||
Tue 14 Nov 2017 16:40:00 CET
|
||||
Tue 14 Nov 2017 16:40:01 CET
|
||||
Tue 14 Nov 2017 16:40:02 CET
|
||||
```
|
||||
@ -204,7 +204,7 @@ to create an externally isolated `overlay` network, you can specify the
|
||||
You can create the network which will be used to provide the routing-mesh in the
|
||||
swarm cluster. You do so by specifying `--ingress` when creating the network. Only
|
||||
one ingress network can be created at the time. The network can be removed only
|
||||
if no services depend on it. Any option available when creating a overlay network
|
||||
if no services depend on it. Any option available when creating an overlay network
|
||||
is also available when creating the ingress network, besides the `--attachable` option.
|
||||
|
||||
```bash
|
||||
|
||||
@ -207,7 +207,7 @@ $ docker network inspect ingress
|
||||
details such as the service's VIP and port mappings. It also shows IPs of service tasks,
|
||||
and the IPs of the nodes where the tasks are running.
|
||||
|
||||
Following is an example output for a overlay network `ov1` that has one service `s1`
|
||||
Following is an example output for an overlay network `ov1` that has one service `s1`
|
||||
attached to. service `s1` in this case has three replicas.
|
||||
|
||||
```bash
|
||||
|
||||
@ -240,7 +240,7 @@ For the `devicemapper`, `btrfs`, `windowsfilter` and `zfs` graph drivers,
|
||||
user cannot pass a size less than the Default BaseFS Size.
|
||||
For the `overlay2` storage driver, the size option is only available if the
|
||||
backing fs is `xfs` and mounted with the `pquota` mount option.
|
||||
Under these conditions, user can pass any size less then the backing fs size.
|
||||
Under these conditions, user can pass any size less than the backing fs size.
|
||||
|
||||
### Mount tmpfs (--tmpfs)
|
||||
|
||||
|
||||
@ -33,6 +33,7 @@ Options:
|
||||
--entrypoint command Overwrite the default ENTRYPOINT of the image
|
||||
-e, --env list Set environment variables
|
||||
--env-file list Read in a file of environment variables
|
||||
--generic-resource list User defined resources request
|
||||
--group list Set one or more supplementary user groups for the container
|
||||
--health-cmd string Command to run to check health
|
||||
--health-interval duration Time between running the check (ms|s|m|h)
|
||||
@ -42,6 +43,7 @@ Options:
|
||||
--help Print usage
|
||||
--host list Set one or more custom host-to-IP mappings (host:ip)
|
||||
--hostname string Container hostname
|
||||
--isolation string Service container isolation mode
|
||||
-l, --label list Service labels
|
||||
--limit-cpu decimal Limit CPUs
|
||||
--limit-memory bytes Limit Memory
|
||||
@ -353,7 +355,7 @@ volumes in a service:
|
||||
<li><tt>default</tt>: Equivalent to <tt>consistent</tt>.</li>
|
||||
<li><tt>consistent</tt>: Full consistency. The container runtime and the host maintain an identical view of the mount at all times.</li>
|
||||
<li><tt>cached</tt>: The host's view of the mount is authoritative. There may be delays before updates made on the host are visible within a container.</li>
|
||||
<li><tt>delegated</tt>: The container runtime's view of the mount is authoritative. There may be delays before updates made in a container are are visible on the host.</li>
|
||||
<li><tt>delegated</tt>: The container runtime's view of the mount is authoritative. There may be delays before updates made in a container are visible on the host.</li>
|
||||
</ul>
|
||||
</p>
|
||||
</td>
|
||||
@ -588,27 +590,27 @@ follows:
|
||||
<tr>
|
||||
<td><tt>node.id</tt></td>
|
||||
<td>Node ID</td>
|
||||
<td><tt>node.id == 2ivku8v2gvtg4</tt></td>
|
||||
<td><tt>node.id==2ivku8v2gvtg4</tt></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><tt>node.hostname</tt></td>
|
||||
<td>Node hostname</td>
|
||||
<td><tt>node.hostname != node-2</tt></td>
|
||||
<td><tt>node.hostname!=node-2</tt></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><tt>node.role</tt></td>
|
||||
<td>Node role</td>
|
||||
<td><tt>node.role == manager</tt></td>
|
||||
<td><tt>node.role==manager</tt></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><tt>node.labels</tt></td>
|
||||
<td>user defined node labels</td>
|
||||
<td><tt>node.labels.security == high</tt></td>
|
||||
<td><tt>node.labels.security==high</tt></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><tt>engine.labels</tt></td>
|
||||
<td>Docker Engine's labels</td>
|
||||
<td><tt>engine.labels.operatingsystem == ubuntu 14.04</tt></td>
|
||||
<td><tt>engine.labels.operatingsystem==ubuntu 14.04</tt></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@ -735,52 +737,76 @@ Containers on the same network can access each other using
|
||||
### Publish service ports externally to the swarm (-p, --publish)
|
||||
|
||||
You can publish service ports to make them available externally to the swarm
|
||||
using the `--publish` flag:
|
||||
|
||||
```bash
|
||||
$ docker service create --publish <TARGET-PORT>:<SERVICE-PORT> nginx
|
||||
```
|
||||
|
||||
For example:
|
||||
using the `--publish` flag. The `--publish` flag can take two different styles
|
||||
of arguments. The short version is positional, and allows you to specify the
|
||||
target port and container port separated by a colon.
|
||||
|
||||
```bash
|
||||
$ docker service create --name my_web --replicas 3 --publish 8080:80 nginx
|
||||
```
|
||||
|
||||
When you publish a service port, the swarm routing mesh makes the service
|
||||
accessible at the target port on every node regardless if there is a task for
|
||||
the service running on the node. For more information refer to
|
||||
There is also a long format, which is easier to read and allows you to specify
|
||||
more options. The long format is preferred. You cannot specify the service's
|
||||
mode when using the short format. Here is an example of using the long format
|
||||
for the same service as above:
|
||||
|
||||
```bash
|
||||
$ docker service create --name my_web --replicas 3 --publish target=8080,port=80 nginx
|
||||
```
|
||||
|
||||
The options you can specify are:
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Option</th>
|
||||
<th>Short syntax</th>
|
||||
<th>Long syntax</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>target and container port </td>
|
||||
<td><tt></tt></td>
|
||||
<td><tt></tt></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>protocol</td>
|
||||
<td><tt>--publish 8080:80</tt></td>
|
||||
<td><tt>--publish target=8080,port=80</tt></td>
|
||||
<td><p>
|
||||
The container port to publish and the target port to bind it to on the
|
||||
routing mesh or directly on the node.
|
||||
</p></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>mode</td>
|
||||
<td>Not possible to set using short syntax.</td>
|
||||
<td><tt>--publish target=8080,port=80,mode=host</tt></td>
|
||||
<td><p>
|
||||
The mode to use for binding the port, either `ingress` or `host`. Defaults
|
||||
to `ingress` to use the routing mesh.
|
||||
</p></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>protocol</td>
|
||||
<td><tt>--publish 8080:80/tcp</tt></td>
|
||||
<td><tt>--publish target=8080,port=80,protocol=tcp</tt></td>
|
||||
<td><p>
|
||||
The protocol to use, either `tcp` or `udp`. Defaults to `tcp`. To bind a
|
||||
port for both protocols, specify the `-p` or `--publish` flag twice.
|
||||
</p></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
When you publish a service port using `ingres` mode, the swarm routing mesh
|
||||
makes the service accessible at the target port on every node regardless if
|
||||
there is a task for the service running on the node. If you use `host` mode,
|
||||
the port is only bound on nodes where the service is running, and a given port
|
||||
on a node can only be bound once. You can only set the publication mode using
|
||||
the long syntax. For more information refer to
|
||||
[Use swarm mode routing mesh](https://docs.docker.com/engine/swarm/ingress/).
|
||||
|
||||
### Publish a port for TCP only or UDP only
|
||||
|
||||
By default, when you publish a port, it is a TCP port. You can
|
||||
specifically publish a UDP port instead of or in addition to a TCP port. When
|
||||
you publish both TCP and UDP ports, Docker 1.12.2 and earlier require you to
|
||||
add the suffix `/tcp` for TCP ports. Otherwise it is optional.
|
||||
|
||||
#### TCP only
|
||||
|
||||
The following two commands are equivalent.
|
||||
|
||||
```bash
|
||||
$ docker service create --name dns-cache -p 53:53 dns-cache
|
||||
|
||||
$ docker service create --name dns-cache -p 53:53/tcp dns-cache
|
||||
```
|
||||
|
||||
#### TCP and UDP
|
||||
|
||||
```bash
|
||||
$ docker service create --name dns-cache -p 53:53/tcp -p 53:53/udp dns-cache
|
||||
```
|
||||
|
||||
#### UDP only
|
||||
|
||||
```bash
|
||||
$ docker service create --name dns-cache -p 53:53/udp dns-cache
|
||||
```
|
||||
|
||||
### Provide credential specs for managed service accounts (Windows only)
|
||||
|
||||
This option is only used for services using Windows containers. The
|
||||
@ -875,6 +901,33 @@ $ docker inspect --format="{{.Config.Hostname}}" 2e7a8a9c4da2-wo41w8hg8qanxwjwsg
|
||||
x3ti0erg11rjpg64m75kej2mz-hosttempl
|
||||
```
|
||||
|
||||
### Specify isolation mode (Windows)
|
||||
|
||||
By default, tasks scheduled on Windows nodes are run using the default isolation mode
|
||||
configured for this particular node. To force a specific isolation mode, you can use
|
||||
the `--isolation` flag:
|
||||
|
||||
```bash
|
||||
$ docker service create --name myservice --isolation=process microsoft/nanoserver
|
||||
```
|
||||
|
||||
Supported isolation modes on Windows are:
|
||||
- `default`: use default settings specified on the node running the task
|
||||
- `process`: use process isolation (Windows server only)
|
||||
- `hyperv`: use Hyper-V isolation
|
||||
|
||||
### Create services requesting Generic Resources
|
||||
|
||||
You can narrow the kind of nodes your task can land on through the using the
|
||||
`--generic-resource` flag (if the nodes advertise these resources):
|
||||
|
||||
```bash
|
||||
$ docker service create --name cuda \
|
||||
--generic-resource "NVIDIA-GPU=2" \
|
||||
--generic-resource "SSD=1" \
|
||||
nvidia/cuda
|
||||
```
|
||||
|
||||
## Related commands
|
||||
|
||||
* [service inspect](service_inspect.md)
|
||||
|
||||
@ -41,6 +41,8 @@ Options:
|
||||
--env-add list Add or update an environment variable
|
||||
--env-rm list Remove an environment variable
|
||||
--force Force update even if no changes require it
|
||||
--generic-resource-add list Add an additional generic resource to the service's resources requirements
|
||||
--generic-resource-rm list Remove a previously added generic resource to the service's resources requirements
|
||||
--group-add list Add an additional supplementary user group to the container
|
||||
--group-rm list Remove a previously added supplementary user group from the container
|
||||
--health-cmd string Command to run to check health
|
||||
@ -53,6 +55,7 @@ Options:
|
||||
--host-rm list Remove a custom host-to-IP mapping (host:ip)
|
||||
--hostname string Container hostname
|
||||
--image string Service image tag
|
||||
--isolation string Service container isolation mode
|
||||
--label-add list Add or update a service label
|
||||
--label-rm list Remove a label by its key
|
||||
--limit-cpu decimal Limit CPUs
|
||||
@ -174,6 +177,21 @@ $ docker service update --mount-rm /somewhere myservice
|
||||
myservice
|
||||
```
|
||||
|
||||
### Add or remove port mappings
|
||||
|
||||
Use the `--port-add` or `--port-rm` flags to add or remove port mappings to or
|
||||
from a service. You can use the short or long syntax discussed in the
|
||||
[docker service update](service_create/#attach-a-service-to-an-existing-network-network)
|
||||
reference.
|
||||
|
||||
The following example adds a port mapping to an existing service.
|
||||
|
||||
```bash
|
||||
$ docker service update \
|
||||
--port-add port=80,target=8080 \
|
||||
myservice
|
||||
```
|
||||
|
||||
### Roll back to the previous version of a service
|
||||
|
||||
Use the `--rollback` option to roll back to the previous version of the service.
|
||||
@ -258,6 +276,12 @@ $ docker service update \
|
||||
Some flags of `service update` support the use of templating.
|
||||
See [`service create`](./service_create.md#templating) for the reference.
|
||||
|
||||
|
||||
### Specify isolation mode (Windows)
|
||||
|
||||
`service update` supports the same `--isolation` flag as `service create`
|
||||
See [`service create`](./service_create.md) for the reference.
|
||||
|
||||
## Related commands
|
||||
|
||||
* [service create](service_create.md)
|
||||
|
||||
364
components/cli/docs/reference/commandline/trust_inspect.md
Normal file
364
components/cli/docs/reference/commandline/trust_inspect.md
Normal file
@ -0,0 +1,364 @@
|
||||
---
|
||||
title: "trust inspect"
|
||||
description: "The inspect command description and usage"
|
||||
keywords: "view, notary, trust"
|
||||
---
|
||||
|
||||
<!-- This file is maintained within the docker/cli GitHub
|
||||
repository at https://github.com/docker/cli/. Make all
|
||||
pull requests against that repo. If you see this file in
|
||||
another repository, consider it read-only there, as it will
|
||||
periodically be overwritten by the definitive file. Pull
|
||||
requests which include edits to this file in other repositories
|
||||
will be rejected.
|
||||
-->
|
||||
|
||||
# trust inspect
|
||||
|
||||
```markdown
|
||||
Usage: docker trust inspect IMAGE[:TAG] [IMAGE[:TAG]...]
|
||||
|
||||
Return low-level information about keys and signatures
|
||||
|
||||
```
|
||||
|
||||
## Description
|
||||
|
||||
`docker trust inspect` provides low-level JSON information on signed repositories.
|
||||
This includes all image tags that are signed, who signed them, and who can sign
|
||||
new tags.
|
||||
|
||||
`docker trust inspect` prints the trust information in a machine-readable format. Refer to
|
||||
[`docker trust view`](trust_view.md) for a human-friendly output.
|
||||
|
||||
`docker trust inspect` is currently experimental.
|
||||
|
||||
|
||||
## Examples
|
||||
|
||||
### Get low-level details about signatures for a single image tag
|
||||
|
||||
Use the `docker trust inspect` to get trust information about an image. The
|
||||
following example prints trust information for the `alpine:latest` image:
|
||||
|
||||
```bash
|
||||
$ docker trust inspect alpine:latest
|
||||
[
|
||||
{
|
||||
"Name": "alpine:latest",
|
||||
"SignedTags": [
|
||||
{
|
||||
"SignedTag": "latest",
|
||||
"Digest": "d6bfc3baf615dc9618209a8d607ba2a8103d9c8a405b3bd8741d88b4bef36478",
|
||||
"Signers": [
|
||||
"Repo Admin"
|
||||
]
|
||||
}
|
||||
],
|
||||
"Signers": [],
|
||||
"AdminstrativeKeys": [
|
||||
{
|
||||
"Name": "Repository",
|
||||
"Keys": [
|
||||
{
|
||||
"ID": "5a46c9aaa82ff150bb7305a2d17d0c521c2d784246807b2dc611f436a69041fd"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "Root",
|
||||
"Keys": [
|
||||
{
|
||||
"ID": "a2489bcac7a79aa67b19b96c4a3bf0c675ffdf00c6d2fabe1a5df1115e80adce"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
The `SignedTags` key will list the `SignedTag` name, its `Digest`, and the `Signers` responsible for the signature.
|
||||
|
||||
`AdministrativeKeys` will list the `Repository` and `Root` keys.
|
||||
|
||||
This format mirrors the output of `docker trust view`
|
||||
|
||||
If signers are set up for the repository via other `docker trust` commands, `docker trust inspect` includes a `Signers` key:
|
||||
|
||||
```bash
|
||||
$ docker trust inspect my-image:purple
|
||||
[
|
||||
{
|
||||
"Name": "my-image:purple",
|
||||
"SignedTags": [
|
||||
{
|
||||
"SignedTag": "purple",
|
||||
"Digest": "941d3dba358621ce3c41ef67b47cf80f701ff80cdf46b5cc86587eaebfe45557",
|
||||
"Signers": [
|
||||
"alice",
|
||||
"bob",
|
||||
"carol"
|
||||
]
|
||||
}
|
||||
],
|
||||
"Signers": [
|
||||
{
|
||||
"Name": "alice",
|
||||
"Keys": [
|
||||
{
|
||||
"ID": "04dd031411ed671ae1e12f47ddc8646d98f135090b01e54c3561e843084484a3"
|
||||
},
|
||||
{
|
||||
"ID": "6a11e4898a4014d400332ab0e096308c844584ff70943cdd1d6628d577f45fd8"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "bob",
|
||||
"Keys": [
|
||||
{
|
||||
"ID": "433e245c656ae9733cdcc504bfa560f90950104442c4528c9616daa45824ccba"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "carol",
|
||||
"Keys": [
|
||||
{
|
||||
"ID": "d32fa8b5ca08273a2880f455fcb318da3dc80aeae1a30610815140deef8f30d9"
|
||||
},
|
||||
{
|
||||
"ID": "9a8bbec6ba2af88a5fad6047d428d17e6d05dbdd03d15b4fc8a9a0e8049cd606"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"AdminstrativeKeys": [
|
||||
{
|
||||
"Name": "Repository",
|
||||
"Keys": [
|
||||
{
|
||||
"ID": "27df2c8187e7543345c2e0bf3a1262e0bc63a72754e9a7395eac3f747ec23a44"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "Root",
|
||||
"Keys": [
|
||||
{
|
||||
"ID": "40b66ccc8b176be8c7d365a17f3e046d1c3494e053dd57cfeacfe2e19c4f8e8f"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
If the image tag is unsigned or unavailable, `docker trust inspect` does not display any signed tags.
|
||||
|
||||
```bash
|
||||
$ docker trust inspect unsigned-img
|
||||
No signatures or cannot access unsigned-img
|
||||
```
|
||||
|
||||
However, if other tags are signed in the same image repository, `docker trust inspect` reports relevant key information:
|
||||
|
||||
```bash
|
||||
$ docker trust inspect alpine:unsigned
|
||||
[
|
||||
{
|
||||
"Name": "alpine:unsigned",
|
||||
"Signers": [],
|
||||
"AdminstrativeKeys": [
|
||||
{
|
||||
"Name": "Repository",
|
||||
"Keys": [
|
||||
{
|
||||
"ID": "5a46c9aaa82ff150bb7305a2d17d0c521c2d784246807b2dc611f436a69041fd"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "Root",
|
||||
"Keys": [
|
||||
{
|
||||
"ID": "a2489bcac7a79aa67b19b96c4a3bf0c675ffdf00c6d2fabe1a5df1115e80adce"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Get details about signatures for all image tags in a repository
|
||||
|
||||
If no tag is specified, `docker trust inspect` will report details for all signed tags in the repository:
|
||||
|
||||
```bash
|
||||
$ docker trust inspect alpine
|
||||
[
|
||||
{
|
||||
"Name": "alpine",
|
||||
"SignedTags": [
|
||||
{
|
||||
"SignedTag": "3.5",
|
||||
"Digest": "b007a354427e1880de9cdba533e8e57382b7f2853a68a478a17d447b302c219c",
|
||||
"Signers": [
|
||||
"Repo Admin"
|
||||
]
|
||||
},
|
||||
{
|
||||
"SignedTag": "3.6",
|
||||
"Digest": "d6bfc3baf615dc9618209a8d607ba2a8103d9c8a405b3bd8741d88b4bef36478",
|
||||
"Signers": [
|
||||
"Repo Admin"
|
||||
]
|
||||
},
|
||||
{
|
||||
"SignedTag": "edge",
|
||||
"Digest": "23e7d843e63a3eee29b6b8cfcd10e23dd1ef28f47251a985606a31040bf8e096",
|
||||
"Signers": [
|
||||
"Repo Admin"
|
||||
]
|
||||
},
|
||||
{
|
||||
"SignedTag": "latest",
|
||||
"Digest": "d6bfc3baf615dc9618209a8d607ba2a8103d9c8a405b3bd8741d88b4bef36478",
|
||||
"Signers": [
|
||||
"Repo Admin"
|
||||
]
|
||||
}
|
||||
],
|
||||
"Signers": [],
|
||||
"AdminstrativeKeys": [
|
||||
{
|
||||
"Name": "Repository",
|
||||
"Keys": [
|
||||
{
|
||||
"ID": "5a46c9aaa82ff150bb7305a2d17d0c521c2d784246807b2dc611f436a69041fd"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "Root",
|
||||
"Keys": [
|
||||
{
|
||||
"ID": "a2489bcac7a79aa67b19b96c4a3bf0c675ffdf00c6d2fabe1a5df1115e80adce"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
|
||||
### Get details about signatures for multiple images
|
||||
|
||||
`docker trust inspect` can take multiple repositories and images as arguments, and reports the results in an ordered list:
|
||||
|
||||
```bash
|
||||
$ docker trust inspect alpine notary
|
||||
[
|
||||
{
|
||||
"Name": "alpine",
|
||||
"SignedTags": [
|
||||
{
|
||||
"SignedTag": "3.5",
|
||||
"Digest": "b007a354427e1880de9cdba533e8e57382b7f2853a68a478a17d447b302c219c",
|
||||
"Signers": [
|
||||
"Repo Admin"
|
||||
]
|
||||
},
|
||||
{
|
||||
"SignedTag": "3.6",
|
||||
"Digest": "d6bfc3baf615dc9618209a8d607ba2a8103d9c8a405b3bd8741d88b4bef36478",
|
||||
"Signers": [
|
||||
"Repo Admin"
|
||||
]
|
||||
},
|
||||
{
|
||||
"SignedTag": "edge",
|
||||
"Digest": "23e7d843e63a3eee29b6b8cfcd10e23dd1ef28f47251a985606a31040bf8e096",
|
||||
"Signers": [
|
||||
"Repo Admin"
|
||||
]
|
||||
},
|
||||
{
|
||||
"SignedTag": "integ-test-base",
|
||||
"Digest": "3952dc48dcc4136ccdde37fbef7e250346538a55a0366e3fccc683336377e372",
|
||||
"Signers": [
|
||||
"Repo Admin"
|
||||
]
|
||||
},
|
||||
{
|
||||
"SignedTag": "latest",
|
||||
"Digest": "d6bfc3baf615dc9618209a8d607ba2a8103d9c8a405b3bd8741d88b4bef36478",
|
||||
"Signers": [
|
||||
"Repo Admin"
|
||||
]
|
||||
}
|
||||
],
|
||||
"Signers": [],
|
||||
"AdminstrativeKeys": [
|
||||
{
|
||||
"Name": "Repository",
|
||||
"Keys": [
|
||||
{
|
||||
"ID": "5a46c9aaa82ff150bb7305a2d17d0c521c2d784246807b2dc611f436a69041fd"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "Root",
|
||||
"Keys": [
|
||||
{
|
||||
"ID": "a2489bcac7a79aa67b19b96c4a3bf0c675ffdf00c6d2fabe1a5df1115e80adce"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "notary",
|
||||
"SignedTags": [
|
||||
{
|
||||
"SignedTag": "server",
|
||||
"Digest": "71f64ab718a3331dee103bc5afc6bc492914738ce37c2d2f127a8133714ecf5c",
|
||||
"Signers": [
|
||||
"Repo Admin"
|
||||
]
|
||||
},
|
||||
{
|
||||
"SignedTag": "signer",
|
||||
"Digest": "a6122d79b1e74f70b5dd933b18a6d1f99329a4728011079f06b245205f158fe8",
|
||||
"Signers": [
|
||||
"Repo Admin"
|
||||
]
|
||||
}
|
||||
],
|
||||
"Signers": [],
|
||||
"AdminstrativeKeys": [
|
||||
{
|
||||
"Name": "Root",
|
||||
"Keys": [
|
||||
{
|
||||
"ID": "8cdcdef5bd039f4ab5a029126951b5985eebf57cabdcdc4d21f5b3be8bb4ce92"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "Repository",
|
||||
"Keys": [
|
||||
{
|
||||
"ID": "85bfd031017722f950d480a721f845a2944db26a3dc084040a70f1b0d9bbb3df"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
@ -16,10 +16,14 @@ keywords: "sign, notary, trust"
|
||||
# trust sign
|
||||
|
||||
```markdown
|
||||
Usage: docker trust sign IMAGE:TAG
|
||||
Usage: docker trust sign [OPTIONS] IMAGE:TAG
|
||||
|
||||
Sign an image
|
||||
|
||||
Options:
|
||||
--help print usage
|
||||
--local force the signing of a local image
|
||||
|
||||
```
|
||||
|
||||
## Description
|
||||
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/e2e/internal/fixtures"
|
||||
shlex "github.com/flynn-archive/go-shlex"
|
||||
"github.com/gotestyourself/gotestyourself/golden"
|
||||
"github.com/gotestyourself/gotestyourself/icmd"
|
||||
@ -11,8 +12,6 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const alpineImage = "registry:5000/alpine:3.6"
|
||||
|
||||
func TestRunAttachedFromRemoteImageAndRemove(t *testing.T) {
|
||||
image := createRemoteImage(t)
|
||||
|
||||
@ -27,8 +26,8 @@ func TestRunAttachedFromRemoteImageAndRemove(t *testing.T) {
|
||||
// TODO: create this with registry API instead of engine API
|
||||
func createRemoteImage(t *testing.T) string {
|
||||
image := "registry:5000/alpine:test-run-pulls"
|
||||
icmd.RunCommand("docker", "pull", alpineImage).Assert(t, icmd.Success)
|
||||
icmd.RunCommand("docker", "tag", alpineImage, image).Assert(t, icmd.Success)
|
||||
icmd.RunCommand("docker", "pull", fixtures.AlpineImage).Assert(t, icmd.Success)
|
||||
icmd.RunCommand("docker", "tag", fixtures.AlpineImage, image).Assert(t, icmd.Success)
|
||||
icmd.RunCommand("docker", "push", image).Assert(t, icmd.Success)
|
||||
icmd.RunCommand("docker", "rmi", image).Assert(t, icmd.Success)
|
||||
return image
|
||||
|
||||
83
components/cli/e2e/image/build_test.go
Normal file
83
components/cli/e2e/image/build_test.go
Normal file
@ -0,0 +1,83 @@
|
||||
package image
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/e2e/internal/fixtures"
|
||||
"github.com/gotestyourself/gotestyourself/fs"
|
||||
"github.com/gotestyourself/gotestyourself/icmd"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func TestBuildFromContextDirectoryWithTag(t *testing.T) {
|
||||
dir := fs.NewDir(t, "test-build-context-dir",
|
||||
fs.WithFile("run", "echo running", fs.WithMode(0755)),
|
||||
fs.WithDir("data", fs.WithFile("one", "1111")),
|
||||
fs.WithFile("Dockerfile", fmt.Sprintf(`
|
||||
FROM %s
|
||||
COPY run /usr/bin/run
|
||||
RUN run
|
||||
COPY data /data
|
||||
`, fixtures.AlpineImage)))
|
||||
defer dir.Remove()
|
||||
|
||||
result := icmd.RunCmd(
|
||||
icmd.Command("docker", "build", "-t", "myimage", "."),
|
||||
withWorkingDir(dir))
|
||||
|
||||
result.Assert(t, icmd.Expected{Err: icmd.None})
|
||||
assertBuildOutput(t, result.Stdout(), map[int]lineCompare{
|
||||
0: prefix("Sending build context to Docker daemon"),
|
||||
1: equals("Step 1/4 : FROM\tregistry:5000/alpine:3.6"),
|
||||
3: equals("Step 2/4 : COPY\trun /usr/bin/run"),
|
||||
5: equals("Step 3/4 : RUN\t\trun"),
|
||||
7: equals("running"),
|
||||
8: prefix("Removing intermediate container "),
|
||||
10: equals("Step 4/4 : COPY\tdata /data"),
|
||||
12: prefix("Successfully built "),
|
||||
13: equals("Successfully tagged myimage:latest"),
|
||||
})
|
||||
}
|
||||
|
||||
func withWorkingDir(dir *fs.Dir) func(*icmd.Cmd) {
|
||||
return func(cmd *icmd.Cmd) {
|
||||
cmd.Dir = dir.Path()
|
||||
}
|
||||
}
|
||||
|
||||
func assertBuildOutput(t *testing.T, actual string, expectedLines map[int]lineCompare) {
|
||||
for i, line := range strings.Split(actual, "\n") {
|
||||
cmp, ok := expectedLines[i]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if err := cmp(line); err != nil {
|
||||
t.Errorf("line %d: %s", i, err)
|
||||
}
|
||||
}
|
||||
if t.Failed() {
|
||||
t.Log(actual)
|
||||
}
|
||||
}
|
||||
|
||||
type lineCompare func(string) error
|
||||
|
||||
func prefix(expected string) func(string) error {
|
||||
return func(actual string) error {
|
||||
if strings.HasPrefix(actual, expected) {
|
||||
return nil
|
||||
}
|
||||
return errors.Errorf("expected %s to start with %s", actual, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func equals(expected string) func(string) error {
|
||||
return func(actual string) error {
|
||||
if expected == actual {
|
||||
return nil
|
||||
}
|
||||
return errors.Errorf("got %s, expected %s", actual, expected)
|
||||
}
|
||||
}
|
||||
@ -2,23 +2,19 @@ package image
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/e2e/internal/fixtures"
|
||||
"github.com/gotestyourself/gotestyourself/golden"
|
||||
"github.com/gotestyourself/gotestyourself/icmd"
|
||||
)
|
||||
|
||||
const notaryURL = "https://notary-server:4443"
|
||||
const registryPrefix = "registry:5000"
|
||||
|
||||
const alpineImage = "registry:5000/alpine:3.6"
|
||||
const busyboxImage = "registry:5000/busybox:1.27.2"
|
||||
|
||||
func TestPullWithContentTrust(t *testing.T) {
|
||||
image := createMaskedTrustedRemoteImage(t, "trust", "latest")
|
||||
|
||||
result := icmd.RunCmd(icmd.Command("docker", "pull", image), withTrustNoPassphrase)
|
||||
result := icmd.RunCmd(icmd.Command("docker", "pull", image), fixtures.WithTrust, fixtures.WithNotary)
|
||||
result.Assert(t, icmd.Success)
|
||||
golden.Assert(t, result.Stderr(), "pull-with-content-trust-err.golden")
|
||||
golden.Assert(t, result.Stdout(), "pull-with-content-trust.golden")
|
||||
@ -34,39 +30,19 @@ func createMaskedTrustedRemoteImage(t *testing.T, repo, tag string) string {
|
||||
|
||||
func createTrustedRemoteImage(t *testing.T, repo, tag string) string {
|
||||
image := fmt.Sprintf("%s/%s:%s", registryPrefix, repo, tag)
|
||||
icmd.RunCommand("docker", "pull", alpineImage).Assert(t, icmd.Success)
|
||||
icmd.RunCommand("docker", "tag", alpineImage, image).Assert(t, icmd.Success)
|
||||
icmd.RunCommand("docker", "pull", fixtures.AlpineImage).Assert(t, icmd.Success)
|
||||
icmd.RunCommand("docker", "tag", fixtures.AlpineImage, image).Assert(t, icmd.Success)
|
||||
result := icmd.RunCmd(
|
||||
icmd.Command("docker", "push", image),
|
||||
withTrustAndPassphrase("root_password", "repo_password"))
|
||||
fixtures.WithPassphrase("root_password", "repo_password"), fixtures.WithTrust, fixtures.WithNotary)
|
||||
result.Assert(t, icmd.Success)
|
||||
icmd.RunCommand("docker", "rmi", image).Assert(t, icmd.Success)
|
||||
return image
|
||||
}
|
||||
|
||||
func createNamedUnsignedImageFromBusyBox(t *testing.T, image string) {
|
||||
icmd.RunCommand("docker", "pull", busyboxImage).Assert(t, icmd.Success)
|
||||
icmd.RunCommand("docker", "tag", busyboxImage, image).Assert(t, icmd.Success)
|
||||
icmd.RunCommand("docker", "pull", fixtures.BusyboxImage).Assert(t, icmd.Success)
|
||||
icmd.RunCommand("docker", "tag", fixtures.BusyboxImage, image).Assert(t, icmd.Success)
|
||||
icmd.RunCommand("docker", "push", image).Assert(t, icmd.Success)
|
||||
icmd.RunCommand("docker", "rmi", image).Assert(t, icmd.Success)
|
||||
}
|
||||
|
||||
func withTrustAndPassphrase(rootPwd, repositoryPwd string) func(cmd *icmd.Cmd) {
|
||||
return func(cmd *icmd.Cmd) {
|
||||
env := append(os.Environ(),
|
||||
"DOCKER_CONTENT_TRUST=1",
|
||||
"DOCKER_CONTENT_TRUST_SERVER="+notaryURL,
|
||||
"DOCKER_CONTENT_TRUST_ROOT_PASSPHRASE="+rootPwd,
|
||||
"DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE="+repositoryPwd,
|
||||
)
|
||||
cmd.Env = append(cmd.Env, env...)
|
||||
}
|
||||
}
|
||||
|
||||
func withTrustNoPassphrase(cmd *icmd.Cmd) {
|
||||
env := append(os.Environ(),
|
||||
"DOCKER_CONTENT_TRUST=1",
|
||||
"DOCKER_CONTENT_TRUST_SERVER="+notaryURL,
|
||||
)
|
||||
cmd.Env = append(cmd.Env, env...)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user