Compare commits
504 Commits
v17.11.0-c
...
v17.12.0-c
| Author | SHA1 | Date | |
|---|---|---|---|
| c97c6d62c2 | |||
| 2861174d81 | |||
| 8673a4245b | |||
| 5dee703312 | |||
| 6a2c058cd8 | |||
| 6819d0ec05 | |||
| 740d71bf6a | |||
| b6799f808c | |||
| f8b1976d4c | |||
| aad5f42ada | |||
| 59c59ce2f5 | |||
| 65b3c804b5 | |||
| fcbcbec6b1 | |||
| ee3330bc06 | |||
| ddc114ea2b | |||
| 52f2c25c69 | |||
| 7f829d4736 | |||
| 332e30b02e | |||
| 0a4c60553a | |||
| b827146463 | |||
| 2802af349a | |||
| 0ba5b1beec | |||
| e4e75fe503 | |||
| b63dd43fb3 | |||
| c01b9b9177 | |||
| 03845511e3 | |||
| 13ce294474 | |||
| 80c8033ede | |||
| 5a77b12973 | |||
| df19150f28 | |||
| 3083961ff0 | |||
| b18ab30d0e | |||
| c69b9c5296 | |||
| 6eb0519584 | |||
| a6dd5c1211 | |||
| 0b9f58dd38 | |||
| d191b0309f | |||
| a8731dffeb | |||
| a77efd829e | |||
| 7b688ab54a | |||
| 228adfcd40 | |||
| 02f4c4289a | |||
| f4f23ddb6b | |||
| ae0095d51f | |||
| 0d895d041d | |||
| 29cb745e30 | |||
| 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 |
128
CHANGELOG.md
128
CHANGELOG.md
@ -5,75 +5,107 @@ 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.11.0-ce (2017-11-20)
|
||||
**IMPORTANT**: You must stop all containers and plugins **BEFORE** upgrading to Docker CE 17.12.
|
||||
See related PR: [moby/moby#35812](https://github.com/moby/moby/pull/35812)
|
||||
|
||||
IMPORTANT: Docker CE 17.11 is the first Docker release based on
|
||||
[containerd 1.0 beta](https://github.com/containerd/containerd/releases/tag/v1.0.0-beta.2).
|
||||
Docker CE 17.11 and later won't recognize containers started with
|
||||
previous Docker versions. If using
|
||||
[Live Restore](https://docs.docker.com/engine/admin/live-restore/#enable-the-live-restore-option),
|
||||
you must stop all containers before upgrading to Docker CE 17.11.
|
||||
If you don't, any containers started by Docker versions that predate
|
||||
17.11 won't be recognized by Docker after the upgrade and will keep
|
||||
running, un-managed, on the system.
|
||||
## 17.12.0-ce (2017-12-27)
|
||||
|
||||
## Known Issues
|
||||
* AWS logs batch size calculation [moby/moby#35726](https://github.com/moby/moby/pull/35726)
|
||||
* Health check no longer uses the container's working directory [moby/moby#35843](https://github.com/moby/moby/issues/35843)
|
||||
* Errors not returned from client in stack deploy configs [moby/moby#757](https://github.com/docker/cli/pull/757)
|
||||
* Daemon aborts when project quota fails [moby/moby#35827](https://github.com/moby/moby/pull/35827)
|
||||
* Docker cannot use memory limit when using systemd options [moby/moby#35123](https://github.com/moby/moby/issues/35123)
|
||||
|
||||
### Builder
|
||||
|
||||
* Test & Fix build with rm/force-rm matrix [moby/moby#35139](https://github.com/moby/moby/pull/35139)
|
||||
- Fix build with `--stream` with a large context [moby/moby#35404](https://github.com/moby/moby/pull/35404)
|
||||
- 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
|
||||
|
||||
* Hide help flag from help output [docker/cli#645](https://github.com/docker/cli/pull/645)
|
||||
* Support parsing of named pipes for compose volumes [docker/cli#560](https://github.com/docker/cli/pull/560)
|
||||
* [Compose] Cast values to expected type after interpolating values [docker/cli#601](https://github.com/docker/cli/pull/601)
|
||||
+ Add output for "secrets" and "configs" on `docker stack deploy` [docker/cli#593](https://github.com/docker/cli/pull/593)
|
||||
- Fix flag description for `--host-add` [docker/cli#648](https://github.com/docker/cli/pull/648)
|
||||
* Do not truncate ID on docker service ps --quiet [docker/cli#579](https://github.com/docker/cli/pull/579)
|
||||
* 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)
|
||||
- Fix external networks in stacks [docker/cli#743](https://github.com/docker/cli/pull/743)
|
||||
* Remove support for referencing images by image shortid [docker/cli#753](https://github.com/docker/cli/pull/753) and [moby/moby#35790](https://github.com/moby/moby/pull/35790)
|
||||
* Use commit-sha instead of tag for containerd [moby/moby#35770](https://github.com/moby/moby/pull/35770)
|
||||
|
||||
### Deprecation
|
||||
### Documentation
|
||||
|
||||
* Update bash completion and deprecation for synchronous service updates [docker/cli#610](https://github.com/docker/cli/pull/610)
|
||||
* Update API version history for 1.35 [moby/moby#35724](https://github.com/moby/moby/pull/35724)
|
||||
|
||||
### Logging
|
||||
|
||||
* copy to log driver's bufsize, fixes #34887 [moby/moby#34888](https://github.com/moby/moby/pull/34888)
|
||||
+ Add TCP support for GELF log driver [moby/moby#34758](https://github.com/moby/moby/pull/34758)
|
||||
+ Add credentials endpoint option for awslogs driver [moby/moby#35055](https://github.com/moby/moby/pull/35055)
|
||||
* 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)
|
||||
* Update Graylog2/go-gelf [moby/moby#35765](https://github.com/moby/moby/pull/35765)
|
||||
|
||||
### Networking
|
||||
|
||||
- Fix network name masking network ID on delete [moby/moby#34509](https://github.com/moby/moby/pull/34509)
|
||||
- Fix returned error code for network creation from 500 to 409 [moby/moby#35030](https://github.com/moby/moby/pull/35030)
|
||||
- Fix tasks fail with error "Unable to complete atomic operation, key modified" [docker/libnetwork#2004](https://github.com/docker/libnetwork/pull/2004)
|
||||
* 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)
|
||||
- Clean up node management logic [docker/libnetwork#2036](https://github.com/docker/libnetwork/pull/2036)
|
||||
- Allocate VIPs when endpoints are restored [docker/swarmkit#2474](https://github.com/docker/swarmkit/pull/2474)
|
||||
|
||||
### Runtime
|
||||
|
||||
* Switch to Containerd 1.0 client [moby/moby#34895](https://github.com/moby/moby/pull/34895)
|
||||
* Increase container default shutdown timeout on Windows [moby/moby#35184](https://github.com/moby/moby/pull/35184)
|
||||
* LCOW: API: Add `platform` to /images/create and /build [moby/moby#34642](https://github.com/moby/moby/pull/34642)
|
||||
* Stop filtering Windows manifest lists by version [moby/moby#35117](https://github.com/moby/moby/pull/35117)
|
||||
* Use windows console mode constants from Azure/go-ansiterm [moby/moby#35056](https://github.com/moby/moby/pull/35056)
|
||||
* Windows Daemon should respect DOCKER_TMPDIR [moby/moby#35077](https://github.com/moby/moby/pull/35077)
|
||||
* Windows: Fix startup logging [moby/moby#35253](https://github.com/moby/moby/pull/35253)
|
||||
+ Add support for Windows version filtering on pull [moby/moby#35090](https://github.com/moby/moby/pull/35090)
|
||||
- Fixes LCOW after containerd 1.0 introduced regressions [moby/moby#35320](https://github.com/moby/moby/pull/35320)
|
||||
* ContainerWait on remove: don't stuck on rm fail [moby/moby#34999](https://github.com/moby/moby/pull/34999)
|
||||
* oci: obey CL_UNPRIVILEGED for user namespaced daemon [moby/moby#35205](https://github.com/moby/moby/pull/35205)
|
||||
* Don't abort when setting may_detach_mounts [moby/moby#35172](https://github.com/moby/moby/pull/35172)
|
||||
- Fix panic on get container pid when live restore containers [moby/moby#35157](https://github.com/moby/moby/pull/35157)
|
||||
- Mask `/proc/scsi` path for containers to prevent removal of devices (CVE-2017-16539) [moby/moby#35399](https://github.com/moby/moby/pull/35399)
|
||||
* Update to github.com/vbatts/tar-split@v0.10.2 (CVE-2017-14992) [moby/moby#35424](https://github.com/moby/moby/pull/35424)
|
||||
- Container: protect health monitor channel [moby/moby#35482](https://github.com/moby/moby/pull/35482)
|
||||
- Libcontainerd: fix leaking container/exec state [moby/moby#35484](https://github.com/moby/moby/pull/35484)
|
||||
* 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)
|
||||
* Disallow using legacy (v1) registries [moby/moby#35751](https://github.com/moby/moby/pull/35751) and [docker/cli#747](https://github.com/docker/cli/pull/747)
|
||||
- Windows: Fix case insensitive filename matching against builder cache [moby/moby#35793](https://github.com/moby/moby/pull/35793)
|
||||
- Fix race conditions around process handling and error checks [moby/moby#35809](https://github.com/moby/moby/pull/35809)
|
||||
* Ensure containers are stopped on daemon startup [moby/moby#35805](https://github.com/moby/moby/pull/35805)
|
||||
* Follow containerd namespace conventions [moby/moby#35812](https://github.com/moby/moby/pull/35812)
|
||||
|
||||
### Swarm Mode
|
||||
|
||||
* Modifying integration test due to new ipam options in swarmkit [moby/moby#35103](https://github.com/moby/moby/pull/35103)
|
||||
- Fix deadlock on getting swarm info [moby/moby#35388](https://github.com/moby/moby/pull/35388)
|
||||
+ Expand the scope of the `Err` field in `TaskStatus` to also cover non-terminal errors that block the task from progressing [docker/swarmkit#2287](https://github.com/docker/swarmkit/pull/2287)
|
||||
+ Added support for swarm service isolation mode [moby/moby#34424](https://github.com/moby/moby/pull/34424)
|
||||
- Fix task clean up for tasks that are complete [docker/swarmkit#2477](https://github.com/docker/swarmkit/pull/2477)
|
||||
|
||||
### Packaging
|
||||
|
||||
+ Build packages for Debian 10 (Buster) [docker/docker-ce-packaging#50](https://github.com/docker/docker-ce-packaging/pull/50)
|
||||
+ Build packages for Ubuntu 17.10 (Artful) [docker/docker-ce-packaging#55](https://github.com/docker/docker-ce-packaging/pull/55)
|
||||
+ Add Packaging for Fedora 27 [docker/docker-ce-packaging#59](https://github.com/docker/docker-ce-packaging/pull/59)
|
||||
* Change 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)
|
||||
* Pass Version to engine static builds [docker/docker-ce-packaging#70](https://github.com/docker/docker-ce-packaging/pull/70)
|
||||
+ Added support for aarch64 on Debian (stretch/jessie) and Ubuntu Zesty or newer [docker/docker-ce-packaging#35](https://github.com/docker/docker-ce-packaging/pull/35)
|
||||
|
||||
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-ce
|
||||
17.12.0-ce
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -62,7 +62,7 @@ func Networks(namespace Namespace, networks networkMap, servicesNetworks map[str
|
||||
for internalName := range servicesNetworks {
|
||||
network := networks[internalName]
|
||||
if network.External.External {
|
||||
externalNetworks = append(externalNetworks, network.External.Name)
|
||||
externalNetworks = append(externalNetworks, network.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -55,10 +55,8 @@ func TestNetworks(t *testing.T) {
|
||||
},
|
||||
},
|
||||
"outside": composetypes.NetworkConfig{
|
||||
External: composetypes.External{
|
||||
External: true,
|
||||
Name: "special",
|
||||
},
|
||||
External: composetypes.External{External: true},
|
||||
Name: "special",
|
||||
},
|
||||
"attachablenet": composetypes.NetworkConfig{
|
||||
Driver: "overlay",
|
||||
|
||||
@ -149,6 +149,7 @@ func Service(
|
||||
Configs: configs,
|
||||
ReadOnly: service.ReadOnly,
|
||||
Privileges: &privileges,
|
||||
Isolation: container.Isolation(service.Isolation),
|
||||
},
|
||||
LogDriver: logDriver,
|
||||
Resources: resources,
|
||||
@ -229,7 +230,7 @@ func convertServiceNetworks(
|
||||
}
|
||||
target := namespace.Scope(networkName)
|
||||
if networkConfig.External.External {
|
||||
target = networkConfig.External.Name
|
||||
target = networkConfig.Name
|
||||
}
|
||||
netAttachConfig := swarm.NetworkAttachmentConfig{
|
||||
Target: target,
|
||||
@ -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) {
|
||||
@ -213,10 +219,8 @@ func TestConvertServiceNetworksOnlyDefault(t *testing.T) {
|
||||
func TestConvertServiceNetworks(t *testing.T) {
|
||||
networkConfigs := networkMap{
|
||||
"front": composetypes.NetworkConfig{
|
||||
External: composetypes.External{
|
||||
External: true,
|
||||
Name: "fronttier",
|
||||
},
|
||||
External: composetypes.External{External: true},
|
||||
Name: "fronttier",
|
||||
},
|
||||
"back": composetypes.NetworkConfig{},
|
||||
}
|
||||
@ -253,10 +257,8 @@ func TestConvertServiceNetworks(t *testing.T) {
|
||||
func TestConvertServiceNetworksCustomDefault(t *testing.T) {
|
||||
networkConfigs := networkMap{
|
||||
"default": composetypes.NetworkConfig{
|
||||
External: composetypes.External{
|
||||
External: true,
|
||||
Name: "custom",
|
||||
},
|
||||
External: composetypes.External{External: true},
|
||||
Name: "custom",
|
||||
},
|
||||
}
|
||||
networks := map[string]*composetypes.ServiceNetworkConfig{}
|
||||
@ -372,3 +374,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
|
||||
@ -2145,7 +2164,6 @@ _docker_create() {
|
||||
_docker_daemon() {
|
||||
local boolean_options="
|
||||
$global_boolean_options
|
||||
--disable-legacy-registry
|
||||
--experimental
|
||||
--help
|
||||
--icc=false
|
||||
@ -2202,6 +2220,7 @@ _docker_daemon() {
|
||||
--metrics-addr
|
||||
--mtu
|
||||
--network-control-plane-mtu
|
||||
--node-generic-resource
|
||||
--oom-score-adjust
|
||||
--pidfile -p
|
||||
--registry-mirror
|
||||
@ -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
|
||||
@ -2830,6 +2866,7 @@ _docker_inspect() {
|
||||
$(__docker_services)
|
||||
$(__docker_volumes)
|
||||
" -- "$cur" ) )
|
||||
__ltrim_colon_completions "$cur"
|
||||
;;
|
||||
container)
|
||||
__docker_complete_containers_all
|
||||
@ -3305,6 +3342,7 @@ _docker_service_update_and_create() {
|
||||
--health-start-period
|
||||
--health-timeout
|
||||
--hostname
|
||||
--isolation
|
||||
--label -l
|
||||
--limit-cpu
|
||||
--limit-memory
|
||||
@ -3359,6 +3397,7 @@ _docker_service_update_and_create() {
|
||||
--dns-option
|
||||
--dns-search
|
||||
--env-file
|
||||
--generic-resource
|
||||
--group
|
||||
--host
|
||||
--mode
|
||||
@ -3420,6 +3459,8 @@ _docker_service_update_and_create() {
|
||||
--dns-rm
|
||||
--dns-search-add
|
||||
--dns-search-rm
|
||||
--generic-resource-add
|
||||
--generic-resource-rm
|
||||
--group-add
|
||||
--group-rm
|
||||
--host-add
|
||||
@ -3493,6 +3534,10 @@ _docker_service_update_and_create() {
|
||||
__docker_nospace
|
||||
return
|
||||
;;
|
||||
--isolation)
|
||||
__docker_complete_isolation
|
||||
return
|
||||
;;
|
||||
--log-driver)
|
||||
__docker_complete_log_drivers
|
||||
return
|
||||
@ -4510,7 +4555,7 @@ _docker_system() {
|
||||
info
|
||||
prune
|
||||
"
|
||||
__docker_subcommands "$subcommands $aliases" && return
|
||||
__docker_subcommands "$subcommands" && return
|
||||
|
||||
case "$cur" in
|
||||
-*)
|
||||
@ -4665,6 +4710,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 +4999,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
|
||||
|
||||
@ -2631,7 +2633,6 @@ __docker_subcommand() {
|
||||
"($help)--default-gateway-v6[Container default gateway IPv6 address]:IPv6 address: " \
|
||||
"($help)--default-shm-size=[Default shm size for containers]:size:" \
|
||||
"($help)*--default-ulimit=[Default ulimits for containers]:ulimit: " \
|
||||
"($help)--disable-legacy-registry[Disable contacting legacy registries (default true)]" \
|
||||
"($help)*--dns=[DNS server to use]:DNS: " \
|
||||
"($help)*--dns-opt=[DNS options to use]:DNS option: " \
|
||||
"($help)*--dns-search=[DNS search domains to use]:DNS search: " \
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -74,24 +74,28 @@ The `filter` param to filter the list of image by reference (name or name:tag) i
|
||||
### `repository:shortid` image references
|
||||
**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**
|
||||
|
||||
`repository:shortid` syntax for referencing images is very little used, collides with tag references can be confused with digest references.
|
||||
The `repository:shortid` syntax for referencing images is very little used,
|
||||
collides with tag references, and can be confused with digest references.
|
||||
|
||||
Support for the `repository:shortid` notation to reference images was removed
|
||||
in Docker 17.12.
|
||||
|
||||
### `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.
|
||||
|
||||
### Duplicate keys with conflicting values in engine labels
|
||||
**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**
|
||||
|
||||
Duplicate keys with conflicting values have been deprecated. A warning is displayed
|
||||
in the output, and an error will be returned in the future.
|
||||
When setting duplicate keys with conflicting values, an error will be produced, and the daemon
|
||||
will fail to start.
|
||||
|
||||
### `MAINTAINER` in Dockerfile
|
||||
**Deprecated In Release: [v1.13.0](https://github.com/docker/docker/releases/tag/v1.13.0)**
|
||||
@ -110,12 +114,16 @@ future Engine versions. Instead of just requesting, for example, the URL
|
||||
### Backing filesystem without `d_type` support for overlay/overlay2
|
||||
**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 overlay and overlay2 storage driver does not work as expected if the backing
|
||||
filesystem does not support `d_type`. For example, XFS does not support `d_type`
|
||||
if it is formatted with the `ftype=0` option.
|
||||
|
||||
Starting with Docker 17.12, new installations will not support running overlay2 on
|
||||
a backing filesystem without `d_type` support. For existing installations that upgrade
|
||||
to 17.12, a warning will be printed.
|
||||
|
||||
Please also refer to [#27358](https://github.com/docker/docker/issues/27358) for
|
||||
further information.
|
||||
|
||||
@ -292,7 +300,7 @@ of the `--changes` flag that allows to pass `Dockerfile` commands.
|
||||
|
||||
**Disabled By Default In Release: v17.06**
|
||||
|
||||
**Target For Removal In Release: v17.12**
|
||||
**Removed In Release: v17.12**
|
||||
|
||||
Version 1.8.3 added a flag (`--disable-legacy-registry=false`) which prevents the
|
||||
docker daemon from `pull`, `push`, and `login` operations against v1
|
||||
@ -303,6 +311,21 @@ Support for the v1 protocol to the public registry was removed in 1.13. Any
|
||||
mirror configurations using v1 should be updated to use a
|
||||
[v2 registry mirror](https://docs.docker.com/registry/recipes/mirror/).
|
||||
|
||||
Starting with Docker 17.12, support for V1 registries has been removed, and the
|
||||
`--disable-legacy-registry` flag can no longer be used, and `dockerd` will fail to
|
||||
start when set.
|
||||
|
||||
### `--disable-legacy-registry` override daemon option
|
||||
|
||||
**Disabled In Release: v17.12**
|
||||
|
||||
**Target For Removal In Release: v18.03**
|
||||
|
||||
The `--disable-legacy-registry` flag was disabled in Docker 17.12 and will print
|
||||
an error when used. For this error to be printed, the flag itself is still present,
|
||||
but hidden. The flag will be removed in Docker 18.03.
|
||||
|
||||
|
||||
### Docker Content Trust ENV passphrase variables name change
|
||||
**Deprecated In Release: [v1.9.0](https://github.com/docker/docker/releases/tag/v1.9.0)**
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -42,7 +42,6 @@ Options:
|
||||
--default-gateway-v6 ip Container default gateway IPv6 address
|
||||
--default-runtime string Default OCI runtime for containers (default "runc")
|
||||
--default-ulimit ulimit Default ulimits for containers (default [])
|
||||
--disable-legacy-registry Disable contacting legacy registries (default true)
|
||||
--dns list DNS server to use (default [])
|
||||
--dns-opt list DNS options to use (default [])
|
||||
--dns-search list DNS search domains to use (default [])
|
||||
@ -72,6 +71,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")
|
||||
@ -262,7 +262,7 @@ snapshots. For each devicemapper graph location – typically
|
||||
`/var/lib/docker/devicemapper` – a thin pool is created based on two block
|
||||
devices, one for data and one for metadata. By default, these block devices
|
||||
are created automatically by using loopback mounts of automatically created
|
||||
sparse files. Refer to [Storage driver options](#storage-driver-options) below
|
||||
sparse files. Refer to [Devicemapper options](#devicemapper-options) below
|
||||
for a way how to customize this setup.
|
||||
[~jpetazzo/Resizing Docker containers with the Device Mapper plugin](http://jpetazzo.github.io/2014/01/29/docker-device-mapper-resize/)
|
||||
article explains how to tune your existing setup without the use of options.
|
||||
@ -274,7 +274,7 @@ does not share executable memory between devices. Use
|
||||
The `zfs` driver is probably not as fast as `btrfs` but has a longer track record
|
||||
on stability. Thanks to `Single Copy ARC` shared blocks between clones will be
|
||||
cached only once. Use `dockerd -s zfs`. To select a different zfs filesystem
|
||||
set `zfs.fsname` option as described in [Storage driver options](#storage-driver-options).
|
||||
set `zfs.fsname` option as described in [ZFS options](#zfs-options).
|
||||
|
||||
The `overlay` is a very fast union filesystem. It is now merged in the main
|
||||
Linux kernel as of [3.18.0](https://lkml.org/lkml/2014/10/26/137). `overlay`
|
||||
@ -393,7 +393,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:
|
||||
@ -1053,18 +1053,14 @@ system's list of trusted CAs instead of enabling `--insecure-registry`.
|
||||
|
||||
#### Legacy Registries
|
||||
|
||||
Operations against registries supporting only the legacy v1 protocol are
|
||||
disabled by default. Specifically, the daemon will not attempt `push`,
|
||||
`pull` and `login` to v1 registries. The exception to this is `search`
|
||||
which can still be performed on v1 registries.
|
||||
Starting with Docker 17.12, operations against registries supporting only the
|
||||
legacy v1 protocol are no longer supported. Specifically, the daemon will not
|
||||
attempt `push`, `pull` and `login` to v1 registries. The exception to this is
|
||||
`search` which can still be performed on v1 registries.
|
||||
|
||||
Add `"disable-legacy-registry":false` to the [daemon configuration
|
||||
file](#daemon-configuration-file), or set the
|
||||
`--disable-legacy-registry=false` flag, if you need to interact with
|
||||
registries that have not yet migrated to the v2 protocol.
|
||||
The `disable-legacy-registry` configuration option has been removed and, when
|
||||
used, will produce an error on daemon startup.
|
||||
|
||||
Interaction v1 registries will no longer be supported in Docker v17.12,
|
||||
and the `disable-legacy-registry` configuration option will be removed.
|
||||
|
||||
### Running a Docker daemon behind an HTTPS_PROXY
|
||||
|
||||
@ -1237,6 +1233,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
|
||||
@ -1321,10 +1334,10 @@ This is a full example of the allowed configuration options on Linux:
|
||||
"registry-mirrors": [],
|
||||
"seccomp-profile": "",
|
||||
"insecure-registries": [],
|
||||
"disable-legacy-registry": false,
|
||||
"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"
|
||||
@ -1389,8 +1402,7 @@ This is a full example of the allowed configuration options on Windows:
|
||||
"raw-logs": false,
|
||||
"allow-nondistributable-artifacts": [],
|
||||
"registry-mirrors": [],
|
||||
"insecure-registries": [],
|
||||
"disable-legacy-registry": false
|
||||
"insecure-registries": []
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -588,11 +588,11 @@ Use Docker's `--restart` to specify a container's *restart policy*. A restart
|
||||
policy controls whether the Docker daemon restarts a container after exit.
|
||||
Docker supports the following restart policies:
|
||||
|
||||
| Policy | Result |
|
||||
|:----------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `no` | Do not automatically restart the container when it exits. This is the default. |
|
||||
| `failure` | Restart only if the container exits with a non-zero exit status. Optionally, limit the number of restart retries the Docker daemon attempts. |
|
||||
| `always` | Always restart the container regardless of the exit status. When you specify always, the Docker daemon will try to restart the container indefinitely. The container will also always start on daemon startup, regardless of the current state of the container. |
|
||||
| Policy | Result |
|
||||
|:---------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `no` | Do not automatically restart the container when it exits. This is the default. |
|
||||
| `on-failure[:max-retries]` | Restart only if the container exits with a non-zero exit status. Optionally, limit the number of restart retries the Docker daemon attempts. |
|
||||
| `always` | Always restart the container regardless of the exit status. When you specify always, the Docker daemon will try to restart the container indefinitely. The container will also always start on daemon startup, regardless of the current state of the container. |
|
||||
|
||||
```bash
|
||||
$ docker run --restart=always redis
|
||||
|
||||
@ -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
|
||||
published port and target 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 published=8080,target=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>published and target 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 published=8080,target=80</tt></td>
|
||||
<td><p>
|
||||
The port to publish the service to on the routing mesh or directly on
|
||||
the node, and the target port on the container.
|
||||
</p></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>mode</td>
|
||||
<td>Not possible to set using short syntax.</td>
|
||||
<td><tt>--publish published=8080,target=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 published=8080,target=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 published 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 published service ports
|
||||
|
||||
Use the `--publish-add` or `--publish-rm` flags to add or remove a published
|
||||
port for a service. You can use the short or long syntax discussed in the
|
||||
[docker service create](service_create/#attach-a-service-to-an-existing-network-network)
|
||||
reference.
|
||||
|
||||
The following example adds a published service port to an existing service.
|
||||
|
||||
```bash
|
||||
$ docker service update \
|
||||
--publish-add published=8080,target=80 \
|
||||
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)
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user