Compare commits
489 Commits
v17.12.0-c
...
v18.02.0-c
| Author | SHA1 | Date | |
|---|---|---|---|
| fc4de447b5 | |||
| b89c843589 | |||
| 58dddd3699 | |||
| 1c6c49f915 | |||
| f968a2cc55 | |||
| 80e6ea3db6 | |||
| d055e4b6d9 | |||
| bbc43ecc52 | |||
| 51e5449bfa | |||
| 65553aa3a5 | |||
| 7a876b5739 | |||
| 6f67a5601a | |||
| b97aef8f2a | |||
| f25c14edf2 | |||
| 8bc38e16b2 | |||
| 30849d8ad6 | |||
| 24653a880d | |||
| f30580f501 | |||
| d3c944e007 | |||
| ba58fe58e7 | |||
| 9651b006e6 | |||
| 97eb7b67a3 | |||
| fcc1ab290b | |||
| 9849fadfbe | |||
| 49bb088afe | |||
| 3fba076ef0 | |||
| 5e1d90afd7 | |||
| f903039853 | |||
| 1dcf6652c8 | |||
| 3b28f86d44 | |||
| 1a2098cecf | |||
| 9d7b9c23f5 | |||
| ffef3ad3d7 | |||
| ef4aa7ebe5 | |||
| 0851fa60de | |||
| 60e5b2e6f7 | |||
| 4d4d834cb8 | |||
| 6fc26d75dd | |||
| 65d771b644 | |||
| 3fe329a3ed | |||
| f46043cd6e | |||
| bb193d7289 | |||
| 91c8e6e25b | |||
| 4798645bfb | |||
| 800ee6ea8f | |||
| ae55d43f2e | |||
| 6c6e8038c9 | |||
| ad2946faa1 | |||
| 1530820600 | |||
| 02b6af2e96 | |||
| 544ec9eef9 | |||
| d67508be5d | |||
| dbb0b294c6 | |||
| bb0dcf69f8 | |||
| 74da78f854 | |||
| 2b99ce1d23 | |||
| 8c17001e4f | |||
| 5cec7a6e2f | |||
| bc95dc9fb3 | |||
| 50c6561a9e | |||
| 928478219f | |||
| e3d68b9fea | |||
| e9f8cfe726 | |||
| 739f4e49fc | |||
| 40f1f3c105 | |||
| 36e0e57cbe | |||
| c2b247fce6 | |||
| d093175f6e | |||
| 8adbcf25d6 | |||
| c9b8ded195 | |||
| fa4fb352fe | |||
| dbd159de4c | |||
| bde2b4704d | |||
| 946a37c1e4 | |||
| 2859d16079 | |||
| 30c97f4539 | |||
| 5e6be7b1c2 | |||
| 12ceea25e6 | |||
| 621663ea5d | |||
| 3cf8a0c442 | |||
| 9dd65d097b | |||
| 3c3132fc24 | |||
| 06609b9304 | |||
| c973e2ff26 | |||
| d00d4e32b9 | |||
| ed76e0e519 | |||
| 2cf544120b | |||
| fdf41db38c | |||
| ebd586c561 | |||
| 58e2db872f | |||
| 0b18a391f5 | |||
| 46bc3ffbc5 | |||
| ac28efa249 | |||
| e94c45a5c9 | |||
| 3ca8db9a46 | |||
| 676b242a5f | |||
| 40b95b8e94 | |||
| 9e8b44781f | |||
| b4c44961cf | |||
| 3fa889eb3f | |||
| 86ca16658d | |||
| 3ee93da859 | |||
| dc08aa82a2 | |||
| a654d0d2d6 | |||
| 852153685d | |||
| 33860da10b | |||
| ce1ad508f6 | |||
| 2096535fce | |||
| 8d6e00613b | |||
| c6678dac69 | |||
| 2138e8ddb5 | |||
| 3e6e651fda | |||
| 2b31b5d6e4 | |||
| 2012c45c5a | |||
| 33df5a9f66 | |||
| d782361adb | |||
| 28df99f4fa | |||
| 0986b8a32c | |||
| b602be47f6 | |||
| 1c449fb090 | |||
| 5a20e1240c | |||
| 0e51141e24 | |||
| 19e1833a23 | |||
| eaa41c5725 | |||
| dede819b22 | |||
| a5a13dc0e0 | |||
| 4a656e30d0 | |||
| 7da3044682 | |||
| d8a82d08f1 | |||
| 374fd77d92 | |||
| cd6c81a6fb | |||
| 837b878f9d | |||
| 01a3b0d1ac | |||
| 42cf4bc867 | |||
| a19065e951 | |||
| 35c47edb0e | |||
| 1c29e89e9b | |||
| fd6c5edde8 | |||
| e9c2f9fe77 | |||
| fcc2e6e315 | |||
| 2c0a4f1b1f | |||
| 96cd62f939 | |||
| 6480a738eb | |||
| fe856d6a3e | |||
| 596eb2d73f | |||
| 2d002babe5 | |||
| 9ec0657065 | |||
| a9d5589889 | |||
| c4ee004968 | |||
| 45b9f9df5c | |||
| 04ddd90677 | |||
| 69f6e83862 | |||
| 2802ca3c0e | |||
| d7084e8097 | |||
| 72b954bd11 | |||
| 3c68801e6a | |||
| de86612508 | |||
| d1c8c580ed | |||
| 28a714b165 | |||
| d0afa0d404 | |||
| de027de052 | |||
| 1240c39efc | |||
| e0f1df2210 | |||
| 03f0380721 | |||
| 02da0b9277 | |||
| 621388138c | |||
| 3b2e22c98e | |||
| 4ab8a85d30 | |||
| f9646377ed | |||
| 6303353f60 | |||
| 2c5689d728 | |||
| 4db79d6583 | |||
| c3d475f697 | |||
| 2c7eb8f0d8 | |||
| e19dd094e6 | |||
| 298483e61f | |||
| e1c32a9951 | |||
| 837919cf56 | |||
| 4f33349d30 | |||
| d6831a1fa6 | |||
| 2539b89332 | |||
| d4d0b5c268 | |||
| 952c29f8da | |||
| 5127f79a88 | |||
| 15165b99b6 | |||
| 4bf05acc16 | |||
| cfd3975d5f | |||
| 3e7d4bee2e | |||
| 59912e9e1d | |||
| 8bf6743e94 | |||
| a64e387daf | |||
| 54836718fd | |||
| 9251d7a17a | |||
| 5c2e02eb8f | |||
| c6bfcea4c8 | |||
| f408947b41 | |||
| eae3cf0a12 | |||
| a9d3d30077 | |||
| 1682b7c735 | |||
| dc5184e050 | |||
| b1b02a3b31 | |||
| bf3f938c50 | |||
| 77c9b11634 | |||
| 6252dbd48d | |||
| c31e485134 | |||
| 6b1b00bfce | |||
| 6611bbb002 | |||
| 2f79038e74 | |||
| 6094444b27 | |||
| 6979911095 | |||
| 940cce76d9 | |||
| 6f88b66c36 | |||
| cf94b87820 | |||
| 097704d16a | |||
| 9cffb84b7c | |||
| 2e2fe70626 | |||
| bcd054dac5 | |||
| 207f11f4b6 | |||
| ec52ac3e88 | |||
| 790deda568 | |||
| adaa8a0f77 | |||
| 88fc146546 | |||
| 58f87a5e9a | |||
| 844b65b181 | |||
| 7a08117e1e | |||
| 785a293997 | |||
| 1f027b4d1b | |||
| d25231dbba | |||
| 4cf72cbba1 | |||
| f81efe6736 | |||
| 476b1d747b | |||
| 143be37b02 | |||
| 5017f2840c | |||
| 27f91f80b9 | |||
| 5857ab9402 | |||
| f75b092795 | |||
| 7ee813acf5 | |||
| 414f8b2aa4 | |||
| ecf8d36483 | |||
| 5fe06b3e89 | |||
| 6169dab4e0 | |||
| d0f281be92 | |||
| df9a679f90 | |||
| e6a8744081 | |||
| 1bed718e52 | |||
| 0e6233fd86 | |||
| 9c4ef1072a | |||
| 233f49e086 | |||
| 656c1163c8 | |||
| 3887b15050 | |||
| 62d7ad0948 | |||
| e76ddf0d7b | |||
| ad4db68af2 | |||
| 40052b6ec6 | |||
| d9cc44462d | |||
| 0c06f43df3 | |||
| 1fbbb4c59b | |||
| c30ceea721 | |||
| 8322194c96 | |||
| c08c5639be | |||
| e4754a59e3 | |||
| fb52c2003b | |||
| 39c64668e4 | |||
| fa87c342b2 | |||
| 6f3ae5f1f6 | |||
| da47568e0d | |||
| e2368ae63b | |||
| 707c5a8bd6 | |||
| 9f589817fd | |||
| 169f9cc018 | |||
| 17341a3ddc | |||
| 01e7ddaec8 | |||
| 3e3e540e8d | |||
| 2bb414da6f | |||
| 9c63a8a5b6 | |||
| 257633843b | |||
| c2b1f7ca70 | |||
| bcb9583e82 | |||
| ce67d6e9b7 | |||
| 4360eb4024 | |||
| f8a7c9aa17 | |||
| 7cbef79c0e | |||
| 49ff2398e6 | |||
| 5965f4a425 | |||
| 3b99478053 | |||
| 42bcb27498 | |||
| 8e9edf70c7 | |||
| 5b7cd70f03 | |||
| 6635d18f37 | |||
| fa250e5c10 | |||
| ae19592ca8 | |||
| 6aa9598694 | |||
| 25c9e80a84 | |||
| 414d05d8c4 | |||
| cfc9623c01 | |||
| 4b57d7f1d3 | |||
| 31e8b1c077 | |||
| f1638ade2e | |||
| 8befd2d809 | |||
| 2a34102708 | |||
| dd0cbe7272 | |||
| e25f9d0953 | |||
| 9bf811bf85 | |||
| f63a46268d | |||
| f46e42c6ec | |||
| 0ba6b0ad15 | |||
| cb8fda044c | |||
| effda473ed | |||
| d7b5fca8a7 | |||
| d05eafef63 | |||
| 486a48d270 | |||
| c52c03e94f | |||
| d1cf6ac921 | |||
| 84f5ef8c85 | |||
| 40a74c51c6 | |||
| dcfd719f61 | |||
| f4626b5de8 | |||
| 1d466a29ee | |||
| 65271c3145 | |||
| b5273bceed | |||
| 507de4363e | |||
| 1e23b82bb7 | |||
| baa35d70b2 | |||
| cbbf94a252 | |||
| 1ebc428804 | |||
| 4dba81f210 | |||
| 4b3a19ed1e | |||
| c88231e80c | |||
| 0a947da27f | |||
| 4cbf52bf35 | |||
| ed8df8d90f | |||
| 2ac4f6cd3a | |||
| 2a46f74679 | |||
| 7c289486a2 | |||
| 751e6f56a0 | |||
| ee811e7a56 | |||
| b1a25d1e15 | |||
| 3e15d0b417 | |||
| 618f1e2388 | |||
| 7fa76a7822 | |||
| 87487ae8dd | |||
| b8be0fb3b8 | |||
| aa8a79c526 | |||
| 1f700eda21 | |||
| 02056dd212 | |||
| c61589a0f0 | |||
| d109b596b8 | |||
| 2af5d43021 | |||
| e0ed436e6c | |||
| e190404195 | |||
| e1e6adb17e | |||
| 2a820c0f5f | |||
| 88269f42ba | |||
| e5199a0f6e | |||
| 3b758ede4d | |||
| 847487f5da | |||
| 6b042c3e6d | |||
| 50de5d926a | |||
| f0d368cb33 | |||
| 3f7322c602 | |||
| 760baaebbb | |||
| b225d1b061 | |||
| 0acf31e126 | |||
| 92961327ba | |||
| f1ba211fd1 | |||
| 822fd111b3 | |||
| 50a3444995 | |||
| 04606e240e | |||
| 7a36094339 | |||
| 3e7449957b | |||
| 048a0b941a | |||
| c5f1cdf92c | |||
| df88bdc9c9 | |||
| fd08bae89c | |||
| ef4dfd2f67 | |||
| 7e2ee76b7e | |||
| 15c0c88784 | |||
| f3becf1d35 | |||
| 343f73cf3d | |||
| 435d4943db | |||
| 756e315027 | |||
| 264a31ea91 | |||
| 7b36860dee | |||
| ef43ef8bc9 | |||
| ebbba75d0a | |||
| f77854ac3c | |||
| a67c7bc0ee | |||
| e1fb11f9ea | |||
| fbc087df98 | |||
| b16d6e9f15 | |||
| c687cf1b38 | |||
| 72249a1f2e | |||
| 5e5a802f6b | |||
| 7ad7bb59be | |||
| ba6f88e84d | |||
| edf70bdb82 | |||
| 58f0e7706b | |||
| b845d8f122 | |||
| 600655d01d | |||
| eeeede3c5f | |||
| e653cda753 | |||
| 14b537bf91 | |||
| 5b55002724 | |||
| 3ac0d82798 | |||
| 57c4619d75 | |||
| a49939a820 | |||
| 5dfe871bd5 | |||
| 6f60e78f91 | |||
| feccd72a49 | |||
| 63a4263b5f | |||
| 0ef0e17180 | |||
| 5b033215e5 | |||
| 9626c25157 | |||
| f76cba5158 | |||
| 0834563329 | |||
| 05b6e11768 | |||
| 4a3429a762 | |||
| e5dd0c19d9 | |||
| 78fc682ae3 | |||
| 1fd0a9d87b | |||
| f1310bd820 | |||
| faf7b2e58e | |||
| 4ee5590aac | |||
| dc415e6d5e | |||
| 75b526540d | |||
| 0810853589 | |||
| ea904b1a94 | |||
| ee5a10619c | |||
| a85f0c817f | |||
| 467a8bb243 | |||
| 6959a6fac2 | |||
| aa0cf4fa09 | |||
| a4823e1268 | |||
| 74ffa93aa3 | |||
| 5e0f8059c9 | |||
| d81d63c977 | |||
| ec88074283 | |||
| 1aec0724c8 | |||
| c8a642f128 | |||
| 35175d99d5 | |||
| 5b1b2d3dcf | |||
| 209f0e3356 | |||
| 90cb59fabc | |||
| d586c2cf19 | |||
| 6c564b6785 | |||
| eae25b27ea | |||
| ef0d4a2bec | |||
| 1e22870b24 | |||
| 29bb8562cc | |||
| ee263ca445 | |||
| 099ae9b4b0 | |||
| 1305053a0b | |||
| 443795270f | |||
| 5b48c4106b | |||
| 6f58b7f1de | |||
| 16d5b20e33 | |||
| 6fdef05177 | |||
| 27e78af99a | |||
| 141bc0558d | |||
| cd7f41a503 | |||
| 04040622fb | |||
| 7b0f6565c5 | |||
| 376d2e9872 | |||
| 6edf75f3d5 | |||
| 782ef5e9db | |||
| fd25684fc8 | |||
| 94fbfde561 | |||
| c3b54b0b2b | |||
| 17ee87b9ed | |||
| 4ef94db1ce | |||
| 90f62259ac | |||
| 4343c7deaa | |||
| 43ace3ab68 | |||
| b8eda642a7 | |||
| 6a7f05996f | |||
| 090ce73a5d | |||
| 74dc65818c | |||
| 9b7822f4af | |||
| 1be15401b4 | |||
| a017ada68b | |||
| f1cfd038e2 | |||
| 25c006f1cc | |||
| d07549fe22 | |||
| 01d480da3c | |||
| 749a92c1a6 | |||
| e56d0513c6 | |||
| 3acd4904bf | |||
| 1a65d7d84f | |||
| 0ccfca8508 |
142
CHANGELOG.md
142
CHANGELOG.md
@ -1,111 +1,71 @@
|
||||
# Changelog
|
||||
|
||||
Items starting with `DEPRECATE` are important deprecation notices. For more
|
||||
information on the list of deprecated flags and APIs please have a look at
|
||||
https://docs.docker.com/engine/deprecated/ where target removal dates can also
|
||||
be found.
|
||||
For more information on the list of deprecated flags and APIs, have a look at
|
||||
https://docs.docker.com/engine/deprecated/ where you can find target removal dates.
|
||||
|
||||
**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)
|
||||
|
||||
## 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)
|
||||
## 18.02.0-ce (2018-02-07)
|
||||
|
||||
### Builder
|
||||
|
||||
- Fix build cache hash for broken symlink [moby/moby#34271](https://github.com/moby/moby/pull/34271)
|
||||
- Fix long stream sync [moby/moby#35404](https://github.com/moby/moby/pull/35404)
|
||||
- Fix dockerfile parser failing silently on long tokens [moby/moby#35429](https://github.com/moby/moby/pull/35429)
|
||||
- Gitutils: fix checking out submodules [moby/moby#35737](https://github.com/moby/moby/pull/35737)
|
||||
|
||||
### Client
|
||||
|
||||
* Remove secret/config duplication in cli/compose [docker/cli#671](https://github.com/docker/cli/pull/671)
|
||||
* Add `--local` flag to `docker trust sign` [docker/cli#575](https://github.com/docker/cli/pull/575)
|
||||
* Add `docker trust inspect` [docker/cli#694](https://github.com/docker/cli/pull/694)
|
||||
+ Add `name` field to secrets and configs to allow interpolation in Compose files [docker/cli#668](https://github.com/docker/cli/pull/668)
|
||||
+ Add `--isolation` for setting swarm service isolation mode [docker/cli#426](https://github.com/docker/cli/pull/426)
|
||||
* Remove deprecated "daemon" subcommand [docker/cli#689](https://github.com/docker/cli/pull/689)
|
||||
- Fix behaviour of `rmi -f` with unexpected errors [docker/cli#654](https://github.com/docker/cli/pull/654)
|
||||
* Integrated Generic resource in service create [docker/cli#429](https://github.com/docker/cli/pull/429)
|
||||
- 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)
|
||||
* Attach: Ensure attach exit code matches container's [docker/cli#696](https://github.com/docker/cli/pull/696)
|
||||
+ Added support for tmpfs-mode in compose file [docker/cli#808](https://github.com/docker/cli/pull/808)
|
||||
+ Adds a new compose file version 3.6 [docker/cli#808](https://github.com/docker/cli/pull/808)
|
||||
- Fix issue of filter in `docker ps` where `health=starting` returns nothing [moby/moby#35940](https://github.com/moby/moby/pull/35940)
|
||||
+ Improve presentation of published port ranges [docker/cli#581](https://github.com/docker/cli/pull/581)
|
||||
* Bump Go to 1.9.3 [docker/cli#827](https://github.com/docker/cli/pull/827)
|
||||
- Fix broken Kubernetes stack flags [docker/cli#831](https://github.com/docker/cli/pull/831)
|
||||
* Annotate "stack" commands to be "swarm" and "kubernetes" [docker/cli#804](https://github.com/docker/cli/pull/804)
|
||||
|
||||
### Documentation
|
||||
### Experimental
|
||||
|
||||
* Update API version history for 1.35 [moby/moby#35724](https://github.com/moby/moby/pull/35724)
|
||||
+ Add manifest command [docker/cli#138](https://github.com/docker/cli/pull/138)
|
||||
* LCOW remotefs - return error in Read() implementation [moby/moby#36051](https://github.com/moby/moby/pull/36051)
|
||||
+ LCOW: Coalesce daemon stores, allow dual LCOW and WCOW mode [moby/moby#34859](https://github.com/moby/moby/pull/34859)
|
||||
- LCOW: Fix OpenFile parameters [moby/moby#36043](https://github.com/moby/moby/pull/36043)
|
||||
* LCOW: Raise minimum requirement to Windows RS3 RTM build (16299) [moby/moby#36065](https://github.com/moby/moby/pull/36065)
|
||||
|
||||
### Logging
|
||||
|
||||
* Logentries driver line-only=true []byte output fix [moby/moby#35612](https://github.com/moby/moby/pull/35612)
|
||||
* Logentries line-only logopt fix to maintain backwards compatibility [moby/moby#35628](https://github.com/moby/moby/pull/35628)
|
||||
+ Add `--until` flag for docker logs [moby/moby#32914](https://github.com/moby/moby/pull/32914)
|
||||
+ Add gelf log driver plugin to Windows build [moby/moby#35073](https://github.com/moby/moby/pull/35073)
|
||||
* Set timeout on splunk batch send [moby/moby#35496](https://github.com/moby/moby/pull/35496)
|
||||
* Update Graylog2/go-gelf [moby/moby#35765](https://github.com/moby/moby/pull/35765)
|
||||
* Improve daemon config reload; log active configuration [moby/moby#36019](https://github.com/moby/moby/pull/36019)
|
||||
- Fixed error detection using IsErrNotFound and IsErrNotImplemented for the ContainerLogs method [moby/moby#36000](https://github.com/moby/moby/pull/36000)
|
||||
+ Add journald tag as SYSLOG_IDENTIFIER [moby/moby#35570](https://github.com/moby/moby/pull/35570)
|
||||
* Splunk: limit the reader size on error responses [moby/moby#35509](https://github.com/moby/moby/pull/35509)
|
||||
|
||||
|
||||
### Networking
|
||||
|
||||
* Move load balancer sandbox creation/deletion into libnetwork [moby/moby#35422](https://github.com/moby/moby/pull/35422)
|
||||
* Only chown network files within container metadata [moby/moby#34224](https://github.com/moby/moby/pull/34224)
|
||||
* Restore error type in FindNetwork [moby/moby#35634](https://github.com/moby/moby/pull/35634)
|
||||
- Fix consumes MIME type for NetworkConnect [moby/moby#35542](https://github.com/moby/moby/pull/35542)
|
||||
+ Added support for persisting Windows network driver specific options [moby/moby#35563](https://github.com/moby/moby/pull/35563)
|
||||
- Fix timeout on netlink sockets and watchmiss leak [moby/moby#35677](https://github.com/moby/moby/pull/35677)
|
||||
+ New daemon config for networking diagnosis [moby/moby#35677](https://github.com/moby/moby/pull/35677)
|
||||
- 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
|
||||
|
||||
* 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
|
||||
|
||||
+ 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)
|
||||
* Disable service on release network results in zero-downtime deployments with rolling upgrades [moby/moby#35960](https://github.com/moby/moby/pull/35960)
|
||||
- Fix services failing to start if multiple networks with the same name exist in different spaces [moby/moby#30897](https://github.com/moby/moby/pull/30897)
|
||||
- Fix duplicate networks being added with `docker service update --network-add` [docker/cli#780](https://github.com/docker/cli/pull/780)
|
||||
- Fixing ingress network when upgrading from 17.09 to 17.12. [moby/moby#36003](https://github.com/moby/moby/pull/36003)
|
||||
- Fix ndots configuration [docker/libnetwork#1995](https://github.com/docker/libnetwork/pull/1995)
|
||||
- Fix IPV6 networking being deconfigured if live-restore is enabled [docker/libnetwork#2043](https://github.com/docker/libnetwork/pull/2043)
|
||||
+ Add support for MX type DNS queries in the embedded DNS server [docker/libnetwork#2041](https://github.com/docker/libnetwork/pull/2041)
|
||||
|
||||
### Packaging
|
||||
|
||||
+ 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)
|
||||
+ Added packaging for Fedora 26, Fedora 27, and Centos 7 on aarch64 [docker/docker-ce-packaging#71](https://github.com/docker/docker-ce-packaging/pull/71)
|
||||
- Removed support for Ubuntu Zesty [docker/docker-ce-packaging#73](https://github.com/docker/docker-ce-packaging/pull/73)
|
||||
- Removed support for Fedora 25 [docker/docker-ce-packaging#72](https://github.com/docker/docker-ce-packaging/pull/72)
|
||||
|
||||
|
||||
### Runtime
|
||||
|
||||
- Fixes unexpected Docker Daemon shutdown based on pipe error [moby/moby#35968](https://github.com/moby/moby/pull/35968)
|
||||
- Fix some occurrences of hcsshim::ImportLayer failed in Win32: The system cannot find the path specified [moby/moby#35924](https://github.com/moby/moby/pull/35924)
|
||||
* Windows: increase the maximum layer size during build to 127GB [moby/moby#35925](https://github.com/moby/moby/pull/35925)
|
||||
- Fix Devicemapper: Error running DeleteDevice dm_task_run failed [moby/moby#35919](https://github.com/moby/moby/pull/35919)
|
||||
+ Introduce « exec_die » event [moby/moby#35744](https://github.com/moby/moby/pull/35744)
|
||||
* Update API to version 1.36 [moby/moby#35744](https://github.com/moby/moby/pull/35744)
|
||||
- Fix `docker update` not updating cpu quota, and cpu-period of a running container [moby/moby#36030](https://github.com/moby/moby/pull/36030)
|
||||
* Make container shm parent unbindable [moby/moby#35830](https://github.com/moby/moby/pull/35830)
|
||||
+ Make image (layer) downloads faster by using pigz [moby/moby#35697](https://github.com/moby/moby/pull/35697)
|
||||
+ Protect the daemon from volume plugins that are slow or deadlocked [moby/moby#35441](https://github.com/moby/moby/pull/35441)
|
||||
- Fix `DOCKER_RAMDISK` environment variable not being honoured [moby/moby#35957](https://github.com/moby/moby/pull/35957)
|
||||
* Bump containerd to 1.0.1 (9b55aab90508bd389d7654c4baf173a981477d55) [moby/moby#35986](https://github.com/moby/moby/pull/35986)
|
||||
* Update runc to fix hang during start and exec [moby/moby#36097](https://github.com/moby/moby/pull/36097)
|
||||
- Fix "--node-generic-resource" singular/plural [moby/moby#36125](https://github.com/moby/moby/pull/36125)
|
||||
|
||||
472
components/cli/.mailmap
Normal file
472
components/cli/.mailmap
Normal file
@ -0,0 +1,472 @@
|
||||
# Generate AUTHORS: scripts/docs/generate-authors.sh
|
||||
|
||||
# Tip for finding duplicates (besides scanning the output of AUTHORS for name
|
||||
# duplicates that aren't also email duplicates): scan the output of:
|
||||
# git log --format='%aE - %aN' | sort -uf
|
||||
#
|
||||
# For explanation on this file format: man git-shortlog
|
||||
|
||||
Aaron L. Xu <liker.xu@foxmail.com>
|
||||
Abhinandan Prativadi <abhi@docker.com>
|
||||
Adrien Gallouët <adrien@gallouet.fr> <angt@users.noreply.github.com>
|
||||
Ahmed Kamal <email.ahmedkamal@googlemail.com>
|
||||
Ahmet Alp Balkan <ahmetb@microsoft.com> <ahmetalpbalkan@gmail.com>
|
||||
AJ Bowen <aj@gandi.net>
|
||||
AJ Bowen <aj@gandi.net> <amy@gandi.net>
|
||||
Akihiro Matsushima <amatsusbit@gmail.com> <amatsus@users.noreply.github.com>
|
||||
Akihiro Suda <suda.akihiro@lab.ntt.co.jp> <suda.kyoto@gmail.com>
|
||||
Aleksa Sarai <asarai@suse.de>
|
||||
Aleksa Sarai <asarai@suse.de> <asarai@suse.com>
|
||||
Aleksa Sarai <asarai@suse.de> <cyphar@cyphar.com>
|
||||
Aleksandrs Fadins <aleks@s-ko.net>
|
||||
Alessandro Boch <aboch@tetrationanalytics.com> <aboch@docker.com>
|
||||
Alex Chen <alexchenunix@gmail.com> <root@localhost.localdomain>
|
||||
Alex Ellis <alexellis2@gmail.com>
|
||||
Alexander Larsson <alexl@redhat.com> <alexander.larsson@gmail.com>
|
||||
Alexander Morozov <lk4d4@docker.com>
|
||||
Alexander Morozov <lk4d4@docker.com> <lk4d4math@gmail.com>
|
||||
Alexandre Beslic <alexandre.beslic@gmail.com> <abronan@docker.com>
|
||||
Alicia Lauerman <alicia@eta.im> <allydevour@me.com>
|
||||
Allen Sun <allensun.shl@alibaba-inc.com> <allen.sun@daocloud.io>
|
||||
Allen Sun <allensun.shl@alibaba-inc.com> <shlallen1990@gmail.com>
|
||||
Andrew Weiss <andrew.weiss@docker.com> <andrew.weiss@microsoft.com>
|
||||
Andrew Weiss <andrew.weiss@docker.com> <andrew.weiss@outlook.com>
|
||||
André Martins <aanm90@gmail.com> <martins@noironetworks.com>
|
||||
Andy Rothfusz <github@developersupport.net> <github@metaliveblog.com>
|
||||
Andy Smith <github@anarkystic.com>
|
||||
Ankush Agarwal <ankushagarwal11@gmail.com> <ankushagarwal@users.noreply.github.com>
|
||||
Antonio Murdaca <antonio.murdaca@gmail.com> <amurdaca@redhat.com>
|
||||
Antonio Murdaca <antonio.murdaca@gmail.com> <me@runcom.ninja>
|
||||
Antonio Murdaca <antonio.murdaca@gmail.com> <runcom@linux.com>
|
||||
Antonio Murdaca <antonio.murdaca@gmail.com> <runcom@redhat.com>
|
||||
Antonio Murdaca <antonio.murdaca@gmail.com> <runcom@users.noreply.github.com>
|
||||
Anuj Bahuguna <anujbahuguna.dev@gmail.com>
|
||||
Anuj Bahuguna <anujbahuguna.dev@gmail.com> <abahuguna@fiberlink.com>
|
||||
Anusha Ragunathan <anusha.ragunathan@docker.com> <anusha@docker.com>
|
||||
Arnaud Porterie <arnaud.porterie@docker.com>
|
||||
Arnaud Porterie <arnaud.porterie@docker.com> <icecrime@gmail.com>
|
||||
Arthur Gautier <baloo@gandi.net> <superbaloo+registrations.github@superbaloo.net>
|
||||
Avi Miller <avi.miller@oracle.com> <avi.miller@gmail.com>
|
||||
Ben Bonnefoy <frenchben@docker.com>
|
||||
Ben Golub <ben.golub@dotcloud.com>
|
||||
Ben Toews <mastahyeti@gmail.com> <mastahyeti@users.noreply.github.com>
|
||||
Benoit Chesneau <bchesneau@gmail.com>
|
||||
Bhiraj Butala <abhiraj.butala@gmail.com>
|
||||
Bhumika Bayani <bhumikabayani@gmail.com>
|
||||
Bilal Amarni <bilal.amarni@gmail.com> <bamarni@users.noreply.github.com>
|
||||
Bill Wang <ozbillwang@gmail.com> <SydOps@users.noreply.github.com>
|
||||
Bin Liu <liubin0329@gmail.com>
|
||||
Bin Liu <liubin0329@gmail.com> <liubin0329@users.noreply.github.com>
|
||||
Bingshen Wang <bingshen.wbs@alibaba-inc.com>
|
||||
Boaz Shuster <ripcurld.github@gmail.com>
|
||||
Brandon Philips <brandon.philips@coreos.com> <brandon@ifup.co>
|
||||
Brandon Philips <brandon.philips@coreos.com> <brandon@ifup.org>
|
||||
Brent Salisbury <brent.salisbury@docker.com> <brent@docker.com>
|
||||
Brian Goff <cpuguy83@gmail.com>
|
||||
Brian Goff <cpuguy83@gmail.com> <bgoff@cpuguy83-mbp.home>
|
||||
Brian Goff <cpuguy83@gmail.com> <bgoff@cpuguy83-mbp.local>
|
||||
Chander Govindarajan <chandergovind@gmail.com>
|
||||
Chao Wang <wangchao.fnst@cn.fujitsu.com> <chaowang@localhost.localdomain>
|
||||
Charles Hooper <charles.hooper@dotcloud.com> <chooper@plumata.com>
|
||||
Chen Chao <cc272309126@gmail.com>
|
||||
Chen Chuanliang <chen.chuanliang@zte.com.cn>
|
||||
Chen Mingjie <chenmingjie0828@163.com>
|
||||
Chen Qiu <cheney-90@hotmail.com>
|
||||
Chen Qiu <cheney-90@hotmail.com> <21321229@zju.edu.cn>
|
||||
Chris Dias <cdias@microsoft.com>
|
||||
Chris McKinnel <chris.mckinnel@tangentlabs.co.uk>
|
||||
Christopher Biscardi <biscarch@sketcht.com>
|
||||
Christopher Latham <sudosurootdev@gmail.com>
|
||||
Chun Chen <ramichen@tencent.com> <chenchun.feed@gmail.com>
|
||||
Corbin Coleman <corbin.coleman@docker.com>
|
||||
Cristian Staretu <cristian.staretu@gmail.com>
|
||||
Cristian Staretu <cristian.staretu@gmail.com> <unclejack@users.noreply.github.com>
|
||||
Cristian Staretu <cristian.staretu@gmail.com> <unclejacksons@gmail.com>
|
||||
CUI Wei <ghostplant@qq.com> cuiwei13 <cuiwei13@pku.edu.cn>
|
||||
Daehyeok Mun <daehyeok@gmail.com>
|
||||
Daehyeok Mun <daehyeok@gmail.com> <daehyeok@daehyeok-ui-MacBook-Air.local>
|
||||
Daehyeok Mun <daehyeok@gmail.com> <daehyeok@daehyeokui-MacBook-Air.local>
|
||||
Dan Feldman <danf@jfrog.com>
|
||||
Daniel Dao <dqminh@cloudflare.com>
|
||||
Daniel Dao <dqminh@cloudflare.com> <dqminh89@gmail.com>
|
||||
Daniel Garcia <daniel@danielgarcia.info>
|
||||
Daniel Gasienica <daniel@gasienica.ch> <dgasienica@zynga.com>
|
||||
Daniel Goosen <daniel.goosen@surveysampling.com> <djgoosen@users.noreply.github.com>
|
||||
Daniel Grunwell <mwgrunny@gmail.com>
|
||||
Daniel J Walsh <dwalsh@redhat.com>
|
||||
Daniel Mizyrycki <daniel.mizyrycki@dotcloud.com> <daniel@dotcloud.com>
|
||||
Daniel Mizyrycki <daniel.mizyrycki@dotcloud.com> <mzdaniel@glidelink.net>
|
||||
Daniel Mizyrycki <daniel.mizyrycki@dotcloud.com> <root@vagrant-ubuntu-12.10.vagrantup.com>
|
||||
Daniel Nephin <dnephin@docker.com> <dnephin@gmail.com>
|
||||
Daniel Norberg <dano@spotify.com> <daniel.norberg@gmail.com>
|
||||
Daniel Watkins <daniel@daniel-watkins.co.uk>
|
||||
Danny Yates <danny@codeaholics.org> <Danny.Yates@mailonline.co.uk>
|
||||
Darren Shepherd <darren.s.shepherd@gmail.com> <darren@rancher.com>
|
||||
Dattatraya Kumbhar <dattatraya.kumbhar@gslab.com>
|
||||
Dave Goodchild <buddhamagnet@gmail.com>
|
||||
Dave Henderson <dhenderson@gmail.com> <Dave.Henderson@ca.ibm.com>
|
||||
Dave Tucker <dt@docker.com> <dave@dtucker.co.uk>
|
||||
David M. Karr <davidmichaelkarr@gmail.com>
|
||||
David Sheets <dsheets@docker.com> <sheets@alum.mit.edu>
|
||||
David Sissitka <me@dsissitka.com>
|
||||
David Williamson <david.williamson@docker.com> <davidwilliamson@users.noreply.github.com>
|
||||
Deshi Xiao <dxiao@redhat.com> <dsxiao@dataman-inc.com>
|
||||
Deshi Xiao <dxiao@redhat.com> <xiaods@gmail.com>
|
||||
Diego Siqueira <dieg0@live.com>
|
||||
Diogo Monica <diogo@docker.com> <diogo.monica@gmail.com>
|
||||
Dominik Honnef <dominik@honnef.co> <dominikh@fork-bomb.org>
|
||||
Doug Davis <dug@us.ibm.com> <duglin@users.noreply.github.com>
|
||||
Doug Tangren <d.tangren@gmail.com>
|
||||
Elan Ruusamäe <glen@pld-linux.org>
|
||||
Elan Ruusamäe <glen@pld-linux.org> <glen@delfi.ee>
|
||||
Eric G. Noriega <enoriega@vizuri.com> <egnoriega@users.noreply.github.com>
|
||||
Eric Hanchrow <ehanchrow@ine.com> <eric.hanchrow@gmail.com>
|
||||
Eric Rosenberg <ehaydenr@gmail.com> <ehaydenr@users.noreply.github.com>
|
||||
Erica Windisch <erica@windisch.us> <eric@windisch.us>
|
||||
Erica Windisch <erica@windisch.us> <ewindisch@docker.com>
|
||||
Erik Hollensbe <github@hollensbe.org> <erik+github@hollensbe.org>
|
||||
Erwin van der Koogh <info@erronis.nl>
|
||||
Euan Kemp <euan.kemp@coreos.com> <euank@amazon.com>
|
||||
Eugen Krizo <eugen.krizo@gmail.com>
|
||||
Evan Hazlett <ejhazlett@gmail.com> <ehazlett@users.noreply.github.com>
|
||||
Evelyn Xu <evelynhsu21@gmail.com>
|
||||
Evgeny Shmarnev <shmarnev@gmail.com>
|
||||
Faiz Khan <faizkhan00@gmail.com>
|
||||
Felix Hupfeld <felix@quobyte.com> <quofelix@users.noreply.github.com>
|
||||
Felix Ruess <felix.ruess@gmail.com> <felix.ruess@roboception.de>
|
||||
Feng Yan <fy2462@gmail.com>
|
||||
Fengtu Wang <wangfengtu@huawei.com> <wangfengtu@huawei.com>
|
||||
Francisco Carriedo <fcarriedo@gmail.com>
|
||||
Frank Rosquin <frank.rosquin+github@gmail.com> <frank.rosquin@gmail.com>
|
||||
Frederick F. Kautz IV <fkautz@redhat.com> <fkautz@alumni.cmu.edu>
|
||||
Gabriel Nicolas Avellaneda <avellaneda.gabriel@gmail.com>
|
||||
Gaetan de Villele <gdevillele@gmail.com>
|
||||
Gang Qiao <qiaohai8866@gmail.com> <1373319223@qq.com>
|
||||
George Kontridze <george@bugsnag.com>
|
||||
Gerwim Feiken <g.feiken@tfe.nl> <gerwim@gmail.com>
|
||||
Giampaolo Mancini <giampaolo@trampolineup.com>
|
||||
Gopikannan Venugopalsamy <gopikannan.venugopalsamy@gmail.com>
|
||||
Gou Rao <gou@portworx.com> <gourao@users.noreply.github.com>
|
||||
Greg Stephens <greg@udon.org>
|
||||
Guillaume J. Charmes <guillaume.charmes@docker.com> <charmes.guillaume@gmail.com>
|
||||
Guillaume J. Charmes <guillaume.charmes@docker.com> <guillaume.charmes@dotcloud.com>
|
||||
Guillaume J. Charmes <guillaume.charmes@docker.com> <guillaume@charmes.net>
|
||||
Guillaume J. Charmes <guillaume.charmes@docker.com> <guillaume@docker.com>
|
||||
Guillaume J. Charmes <guillaume.charmes@docker.com> <guillaume@dotcloud.com>
|
||||
Gurjeet Singh <gurjeet@singh.im> <singh.gurjeet@gmail.com>
|
||||
Gustav Sinder <gustav.sinder@gmail.com>
|
||||
Günther Jungbluth <gunther@gameslabs.net>
|
||||
Hakan Özler <hakan.ozler@kodcu.com>
|
||||
Hao Shu Wei <haosw@cn.ibm.com>
|
||||
Hao Shu Wei <haosw@cn.ibm.com> <haoshuwei1989@163.com>
|
||||
Harald Albers <github@albersweb.de> <albers@users.noreply.github.com>
|
||||
Harold Cooper <hrldcpr@gmail.com>
|
||||
Harry Zhang <harryz@hyper.sh> <harryzhang@zju.edu.cn>
|
||||
Harry Zhang <harryz@hyper.sh> <resouer@163.com>
|
||||
Harry Zhang <harryz@hyper.sh> <resouer@gmail.com>
|
||||
Harry Zhang <resouer@163.com>
|
||||
Harshal Patil <harshal.patil@in.ibm.com> <harche@users.noreply.github.com>
|
||||
Helen Xie <chenjg@harmonycloud.cn>
|
||||
Hollie Teal <hollie@docker.com>
|
||||
Hollie Teal <hollie@docker.com> <hollie.teal@docker.com>
|
||||
Hollie Teal <hollie@docker.com> <hollietealok@users.noreply.github.com>
|
||||
Hu Keping <hukeping@huawei.com>
|
||||
Huu Nguyen <huu@prismskylabs.com> <whoshuu@gmail.com>
|
||||
Hyzhou Zhy <hyzhou.zhy@alibaba-inc.com>
|
||||
Hyzhou Zhy <hyzhou.zhy@alibaba-inc.com> <1187766782@qq.com>
|
||||
Ilya Khlopotov <ilya.khlopotov@gmail.com>
|
||||
Jack Laxson <jackjrabbit@gmail.com>
|
||||
Jacob Atzen <jacob@jacobatzen.dk> <jatzen@gmail.com>
|
||||
Jacob Tomlinson <jacob@tom.linson.uk> <jacobtomlinson@users.noreply.github.com>
|
||||
Jaivish Kothari <janonymous.codevulture@gmail.com>
|
||||
Jamie Hannaford <jamie@limetree.org> <jamie.hannaford@rackspace.com>
|
||||
Jean-Baptiste Barth <jeanbaptiste.barth@gmail.com>
|
||||
Jean-Baptiste Dalido <jeanbaptiste@appgratis.com>
|
||||
Jean-Tiare Le Bigot <jt@yadutaf.fr> <admin@jtlebi.fr>
|
||||
Jeff Anderson <jeff@docker.com> <jefferya@programmerq.net>
|
||||
Jeff Nickoloff <jeff.nickoloff@gmail.com> <jeff@allingeek.com>
|
||||
Jeroen Franse <jeroenfranse@gmail.com>
|
||||
Jessica Frazelle <jessfraz@google.com>
|
||||
Jessica Frazelle <jessfraz@google.com> <acidburn@docker.com>
|
||||
Jessica Frazelle <jessfraz@google.com> <acidburn@google.com>
|
||||
Jessica Frazelle <jessfraz@google.com> <jess@docker.com>
|
||||
Jessica Frazelle <jessfraz@google.com> <jess@mesosphere.com>
|
||||
Jessica Frazelle <jessfraz@google.com> <jfrazelle@users.noreply.github.com>
|
||||
Jessica Frazelle <jessfraz@google.com> <me@jessfraz.com>
|
||||
Jessica Frazelle <jessfraz@google.com> <princess@docker.com>
|
||||
Jim Galasyn <jim.galasyn@docker.com>
|
||||
Jiuyue Ma <majiuyue@huawei.com>
|
||||
Joey Geiger <jgeiger@gmail.com>
|
||||
Joffrey F <joffrey@docker.com>
|
||||
Joffrey F <joffrey@docker.com> <f.joffrey@gmail.com>
|
||||
Joffrey F <joffrey@docker.com> <joffrey@dotcloud.com>
|
||||
Johan Euphrosine <proppy@google.com> <proppy@aminche.com>
|
||||
John Harris <john@johnharris.io>
|
||||
John Howard (VM) <John.Howard@microsoft.com>
|
||||
John Howard (VM) <John.Howard@microsoft.com> <jhoward@microsoft.com>
|
||||
John Howard (VM) <John.Howard@microsoft.com> <jhoward@ntdev.microsoft.com>
|
||||
John Howard (VM) <John.Howard@microsoft.com> <jhowardmsft@users.noreply.github.com>
|
||||
John Howard (VM) <John.Howard@microsoft.com> <john.howard@microsoft.com>
|
||||
John Stephens <johnstep@docker.com> <johnstep@users.noreply.github.com>
|
||||
Jordan Arentsen <blissdev@gmail.com>
|
||||
Jordan Jennings <jjn2009@gmail.com> <jjn2009@users.noreply.github.com>
|
||||
Jorit Kleine-Möllhoff <joppich@bricknet.de> <joppich@users.noreply.github.com>
|
||||
Jose Diaz-Gonzalez <jose@seatgeek.com> <josegonzalez@users.noreply.github.com>
|
||||
Josh Eveleth <joshe@opendns.com> <jeveleth@users.noreply.github.com>
|
||||
Josh Hawn <josh.hawn@docker.com> <jlhawn@berkeley.edu>
|
||||
Josh Horwitz <horwitz@addthis.com> <horwitzja@gmail.com>
|
||||
Josh Soref <jsoref@gmail.com> <jsoref@users.noreply.github.com>
|
||||
Josh Wilson <josh.wilson@fivestars.com> <jcwilson@users.noreply.github.com>
|
||||
Joyce Jang <mail@joycejang.com>
|
||||
Julien Bordellier <julienbordellier@gmail.com> <git@julienbordellier.com>
|
||||
Julien Bordellier <julienbordellier@gmail.com> <me@julienbordellier.com>
|
||||
Justin Cormack <justin.cormack@docker.com>
|
||||
Justin Cormack <justin.cormack@docker.com> <justin.cormack@unikernel.com>
|
||||
Justin Cormack <justin.cormack@docker.com> <justin@specialbusservice.com>
|
||||
Justin Simonelis <justin.p.simonelis@gmail.com> <justin.simonelis@PTS-JSIMON2.toronto.exclamation.com>
|
||||
Jérôme Petazzoni <jerome.petazzoni@docker.com> <jerome.petazzoni@dotcloud.com>
|
||||
Jérôme Petazzoni <jerome.petazzoni@docker.com> <jerome.petazzoni@gmail.com>
|
||||
Jérôme Petazzoni <jerome.petazzoni@docker.com> <jp@enix.org>
|
||||
K. Heller <pestophagous@gmail.com> <pestophagous@users.noreply.github.com>
|
||||
Kai Qiang Wu (Kennan) <wkq5325@gmail.com>
|
||||
Kai Qiang Wu (Kennan) <wkq5325@gmail.com> <wkqwu@cn.ibm.com>
|
||||
Kamil Domański <kamil@domanski.co>
|
||||
Kamjar Gerami <kami.gerami@gmail.com>
|
||||
Ken Cochrane <kencochrane@gmail.com> <KenCochrane@gmail.com>
|
||||
Ken Herner <kherner@progress.com> <chosenken@gmail.com>
|
||||
Kenfe-Mickaël Laventure <mickael.laventure@gmail.com>
|
||||
Kevin Feyrer <kevin.feyrer@btinternet.com> <kevinfeyrer@users.noreply.github.com>
|
||||
Kevin Kern <kaiwentan@harmonycloud.cn>
|
||||
Kevin Meredith <kevin.m.meredith@gmail.com>
|
||||
Kir Kolyshkin <kolyshkin@gmail.com>
|
||||
Kir Kolyshkin <kolyshkin@gmail.com> <kir@openvz.org>
|
||||
Kir Kolyshkin <kolyshkin@gmail.com> <kolyshkin@users.noreply.github.com>
|
||||
Konrad Kleine <konrad.wilhelm.kleine@gmail.com> <kwk@users.noreply.github.com>
|
||||
Konstantin Gribov <grossws@gmail.com>
|
||||
Konstantin Pelykh <kpelykh@zettaset.com>
|
||||
Kotaro Yoshimatsu <kotaro.yoshimatsu@gmail.com>
|
||||
Kunal Kushwaha <kushwaha_kunal_v7@lab.ntt.co.jp> <kunal.kushwaha@gmail.com>
|
||||
Lajos Papp <lajos.papp@sequenceiq.com> <lalyos@yahoo.com>
|
||||
Lei Jitang <leijitang@huawei.com>
|
||||
Lei Jitang <leijitang@huawei.com> <leijitang@gmail.com>
|
||||
Liang Mingqiang <mqliang.zju@gmail.com>
|
||||
Liang-Chi Hsieh <viirya@gmail.com>
|
||||
Liao Qingwei <liaoqingwei@huawei.com>
|
||||
Linus Heckemann <lheckemann@twig-world.com>
|
||||
Linus Heckemann <lheckemann@twig-world.com> <anonymouse2048@gmail.com>
|
||||
Lokesh Mandvekar <lsm5@fedoraproject.org> <lsm5@redhat.com>
|
||||
Lorenzo Fontana <lo@linux.com> <fontanalorenzo@me.com>
|
||||
Louis Opter <kalessin@kalessin.fr>
|
||||
Louis Opter <kalessin@kalessin.fr> <louis@dotcloud.com>
|
||||
Luca Favatella <luca.favatella@erlang-solutions.com> <lucafavatella@users.noreply.github.com>
|
||||
Luke Marsden <me@lukemarsden.net> <luke@digital-crocus.com>
|
||||
Lyn <energylyn@zju.edu.cn>
|
||||
Lynda O'Leary <lyndaoleary29@gmail.com>
|
||||
Lynda O'Leary <lyndaoleary29@gmail.com> <lyndaoleary@hotmail.com>
|
||||
Ma Müller <mueller-ma@users.noreply.github.com>
|
||||
Madhan Raj Mookkandy <MadhanRaj.Mookkandy@microsoft.com> <madhanm@microsoft.com>
|
||||
Madhu Venugopal <madhu@socketplane.io> <madhu@docker.com>
|
||||
Mageee <fangpuyi@foxmail.com> <21521230.zju.edu.cn>
|
||||
Mansi Nahar <mmn4185@rit.edu> <mansi.nahar@macbookpro-mansinahar.local>
|
||||
Mansi Nahar <mmn4185@rit.edu> <mansinahar@users.noreply.github.com>
|
||||
Marc Abramowitz <marc@marc-abramowitz.com> <msabramo@gmail.com>
|
||||
Marcelo Horacio Fortino <info@fortinux.com> <fortinux@users.noreply.github.com>
|
||||
Marcus Linke <marcus.linke@gmx.de>
|
||||
Marianna Tessel <mtesselh@gmail.com>
|
||||
Mark Oates <fl0yd@me.com>
|
||||
Markan Patel <mpatel678@gmail.com>
|
||||
Markus Kortlang <hyp3rdino@googlemail.com> <markus.kortlang@lhsystems.com>
|
||||
Martin Redmond <redmond.martin@gmail.com> <martin@tinychat.com>
|
||||
Martin Redmond <redmond.martin@gmail.com> <xgithub@redmond5.com>
|
||||
Mary Anthony <mary.anthony@docker.com> <mary@docker.com>
|
||||
Mary Anthony <mary.anthony@docker.com> <moxieandmore@gmail.com>
|
||||
Mary Anthony <mary.anthony@docker.com> moxiegirl <mary@docker.com>
|
||||
Matt Bentley <matt.bentley@docker.com> <mbentley@mbentley.net>
|
||||
Matt Schurenko <matt.schurenko@gmail.com>
|
||||
Matt Williams <mattyw@me.com>
|
||||
Matt Williams <mattyw@me.com> <gh@mattyw.net>
|
||||
Matthew Heon <mheon@redhat.com> <mheon@mheonlaptop.redhat.com>
|
||||
Matthew Mosesohn <raytrac3r@gmail.com>
|
||||
Matthew Mueller <mattmuelle@gmail.com>
|
||||
Matthias Kühnle <git.nivoc@neverbox.com> <kuehnle@online.de>
|
||||
Mauricio Garavaglia <mauricio@medallia.com> <mauriciogaravaglia@gmail.com>
|
||||
Michael Crosby <michael@docker.com> <crosby.michael@gmail.com>
|
||||
Michael Crosby <michael@docker.com> <crosbymichael@gmail.com>
|
||||
Michael Crosby <michael@docker.com> <michael@crosbymichael.com>
|
||||
Michael Hudson-Doyle <michael.hudson@canonical.com> <michael.hudson@linaro.org>
|
||||
Michael Huettermann <michael@huettermann.net>
|
||||
Michael Käufl <docker@c.michael-kaeufl.de> <michael-k@users.noreply.github.com>
|
||||
Michael Spetsiotis <michael_spets@hotmail.com>
|
||||
Michal Minář <miminar@redhat.com>
|
||||
Miguel Angel Alvarez Cabrerizo <doncicuto@gmail.com> <30386061+doncicuto@users.noreply.github.com>
|
||||
Miguel Angel Fernández <elmendalerenda@gmail.com>
|
||||
Mihai Borobocea <MihaiBorob@gmail.com> <MihaiBorobocea@gmail.com>
|
||||
Mike Casas <mkcsas0@gmail.com> <mikecasas@users.noreply.github.com>
|
||||
Mike Goelzer <mike.goelzer@docker.com> <mgoelzer@docker.com>
|
||||
Milind Chawre <milindchawre@gmail.com>
|
||||
Misty Stanley-Jones <misty@docker.com> <misty@apache.org>
|
||||
Mohit Soni <mosoni@ebay.com> <mohitsoni1989@gmail.com>
|
||||
Moorthy RS <rsmoorthy@gmail.com> <rsmoorthy@users.noreply.github.com>
|
||||
Moysés Borges <moysesb@gmail.com>
|
||||
Moysés Borges <moysesb@gmail.com> <moyses.furtado@wplex.com.br>
|
||||
Nace Oroz <orkica@gmail.com>
|
||||
Nathan LeClaire <nathan.leclaire@docker.com> <nathan.leclaire@gmail.com>
|
||||
Nathan LeClaire <nathan.leclaire@docker.com> <nathanleclaire@gmail.com>
|
||||
Neil Horman <nhorman@tuxdriver.com> <nhorman@hmswarspite.think-freely.org>
|
||||
Nick Russo <nicholasjamesrusso@gmail.com> <nicholasrusso@icloud.com>
|
||||
Nicolas Borboën <ponsfrilus@gmail.com> <ponsfrilus@users.noreply.github.com>
|
||||
Nigel Poulton <nigelpoulton@hotmail.com>
|
||||
Nik Nyby <nikolas@gnu.org> <nnyby@columbia.edu>
|
||||
Nolan Darilek <nolan@thewordnerd.info>
|
||||
O.S. Tezer <ostezer@gmail.com>
|
||||
O.S. Tezer <ostezer@gmail.com> <ostezer@users.noreply.github.com>
|
||||
Oh Jinkyun <tintypemolly@gmail.com> <tintypemolly@Ohui-MacBook-Pro.local>
|
||||
Ouyang Liduo <oyld0210@163.com>
|
||||
Patrick Stapleton <github@gdi2290.com>
|
||||
Paul Liljenberg <liljenberg.paul@gmail.com> <letters@paulnotcom.se>
|
||||
Pavel Tikhomirov <ptikhomirov@virtuozzo.com> <ptikhomirov@parallels.com>
|
||||
Pawel Konczalski <mail@konczalski.de>
|
||||
Peter Choi <phkchoi89@gmail.com> <reikani@Peters-MacBook-Pro.local>
|
||||
Peter Dave Hello <hsu@peterdavehello.org> <PeterDaveHello@users.noreply.github.com>
|
||||
Peter Jaffe <pjaffe@nevo.com>
|
||||
Peter Nagy <xificurC@gmail.com> <pnagy@gratex.com>
|
||||
Peter Waller <p@pwaller.net> <peter@scraperwiki.com>
|
||||
Phil Estes <estesp@linux.vnet.ibm.com> <estesp@gmail.com>
|
||||
Philip Alexander Etling <paetling@gmail.com>
|
||||
Philipp Gillé <philipp.gille@gmail.com> <philippgille@users.noreply.github.com>
|
||||
Qiang Huang <h.huangqiang@huawei.com>
|
||||
Qiang Huang <h.huangqiang@huawei.com> <qhuang@10.0.2.15>
|
||||
Ray Tsang <rayt@google.com> <saturnism@users.noreply.github.com>
|
||||
Renaud Gaubert <rgaubert@nvidia.com> <renaud.gaubert@gmail.com>
|
||||
Robert Terhaar <rterhaar@atlanticdynamic.com> <robbyt@users.noreply.github.com>
|
||||
Roberto G. Hashioka <roberto.hashioka@docker.com> <roberto_hashioka@hotmail.com>
|
||||
Roberto Muñoz Fernández <robertomf@gmail.com> <roberto.munoz.fernandez.contractor@bbva.com>
|
||||
Roman Dudin <katrmr@gmail.com> <decadent@users.noreply.github.com>
|
||||
Ross Boucher <rboucher@gmail.com>
|
||||
Runshen Zhu <runshen.zhu@gmail.com>
|
||||
Ryan Stelly <ryan.stelly@live.com>
|
||||
Sakeven Jiang <jc5930@sina.cn>
|
||||
Sandeep Bansal <sabansal@microsoft.com>
|
||||
Sandeep Bansal <sabansal@microsoft.com> <msabansal@microsoft.com>
|
||||
Sargun Dhillon <sargun@netflix.com> <sargun@sargun.me>
|
||||
Sean Lee <seanlee@tw.ibm.com> <scaleoutsean@users.noreply.github.com>
|
||||
Sebastiaan van Stijn <github@gone.nl> <sebastiaan@ws-key-sebas3.dpi1.dpi>
|
||||
Sebastiaan van Stijn <github@gone.nl> <thaJeztah@users.noreply.github.com>
|
||||
Shaun Kaasten <shaunk@gmail.com>
|
||||
Shawn Landden <shawn@churchofgit.com> <shawnlandden@gmail.com>
|
||||
Shengbo Song <thomassong@tencent.com>
|
||||
Shengbo Song <thomassong@tencent.com> <mymneo@163.com>
|
||||
Shih-Yuan Lee <fourdollars@gmail.com>
|
||||
Shishir Mahajan <shishir.mahajan@redhat.com> <smahajan@redhat.com>
|
||||
Shukui Yang <yangshukui@huawei.com>
|
||||
Shuwei Hao <haosw@cn.ibm.com>
|
||||
Shuwei Hao <haosw@cn.ibm.com> <haoshuwei24@gmail.com>
|
||||
Sidhartha Mani <sidharthamn@gmail.com>
|
||||
Sjoerd Langkemper <sjoerd-github@linuxonly.nl> <sjoerd@byte.nl>
|
||||
Solomon Hykes <solomon@docker.com> <s@docker.com>
|
||||
Solomon Hykes <solomon@docker.com> <solomon.hykes@dotcloud.com>
|
||||
Solomon Hykes <solomon@docker.com> <solomon@dotcloud.com>
|
||||
Soshi Katsuta <soshi.katsuta@gmail.com>
|
||||
Soshi Katsuta <soshi.katsuta@gmail.com> <katsuta_soshi@cyberagent.co.jp>
|
||||
Sridhar Ratnakumar <sridharr@activestate.com>
|
||||
Sridhar Ratnakumar <sridharr@activestate.com> <github@srid.name>
|
||||
Srini Brahmaroutu <srbrahma@us.ibm.com> <sbrahma@us.ibm.com>
|
||||
Srinivasan Srivatsan <srinivasan.srivatsan@hpe.com> <srinsriv@users.noreply.github.com>
|
||||
Stefan Berger <stefanb@linux.vnet.ibm.com>
|
||||
Stefan Berger <stefanb@linux.vnet.ibm.com> <stefanb@us.ibm.com>
|
||||
Stefan J. Wernli <swernli@microsoft.com> <swernli@ntdev.microsoft.com>
|
||||
Stefan S. <tronicum@user.github.com>
|
||||
Stephen Day <stephen.day@docker.com>
|
||||
Stephen Day <stephen.day@docker.com> <stevvooe@users.noreply.github.com>
|
||||
Steve Desmond <steve@vtsv.ca> <stevedesmond-ca@users.noreply.github.com>
|
||||
Sun Gengze <690388648@qq.com>
|
||||
Sun Jianbo <wonderflow.sun@gmail.com>
|
||||
Sun Jianbo <wonderflow.sun@gmail.com> <wonderflow@zju.edu.cn>
|
||||
Sven Dowideit <SvenDowideit@home.org.au>
|
||||
Sven Dowideit <SvenDowideit@home.org.au> <sven@t440s.home.gateway>
|
||||
Sven Dowideit <SvenDowideit@home.org.au> <SvenDowideit@docker.com>
|
||||
Sven Dowideit <SvenDowideit@home.org.au> <SvenDowideit@fosiki.com>
|
||||
Sven Dowideit <SvenDowideit@home.org.au> <SvenDowideit@home.org.au>
|
||||
Sven Dowideit <SvenDowideit@home.org.au> <SvenDowideit@users.noreply.github.com>
|
||||
Sven Dowideit <SvenDowideit@home.org.au> <¨SvenDowideit@home.org.au¨>
|
||||
Sylvain Bellemare <sylvain@ascribe.io>
|
||||
Sylvain Bellemare <sylvain@ascribe.io> <sylvain.bellemare@ezeep.com>
|
||||
Tangi Colin <tangicolin@gmail.com>
|
||||
Tejesh Mehta <tejesh.mehta@gmail.com> <tj@init.me>
|
||||
Thatcher Peskens <thatcher@docker.com>
|
||||
Thatcher Peskens <thatcher@docker.com> <thatcher@dotcloud.com>
|
||||
Thatcher Peskens <thatcher@docker.com> <thatcher@gmx.net>
|
||||
Thomas Gazagnaire <thomas@gazagnaire.org> <thomas@gazagnaire.com>
|
||||
Thomas Léveil <thomasleveil@gmail.com>
|
||||
Thomas Léveil <thomasleveil@gmail.com> <thomasleveil@users.noreply.github.com>
|
||||
Tibor Vass <teabee89@gmail.com> <tibor@docker.com>
|
||||
Tibor Vass <teabee89@gmail.com> <tiborvass@users.noreply.github.com>
|
||||
Tim Bart <tim@fewagainstmany.com>
|
||||
Tim Bosse <taim@bosboot.org> <maztaim@users.noreply.github.com>
|
||||
Tim Ruffles <oi@truffles.me.uk> <timruffles@googlemail.com>
|
||||
Tim Terhorst <mynamewastaken+git@gmail.com>
|
||||
Tim Zju <21651152@zju.edu.cn>
|
||||
Timothy Hobbs <timothyhobbs@seznam.cz>
|
||||
Toli Kuznets <toli@docker.com>
|
||||
Tom Barlow <tomwbarlow@gmail.com>
|
||||
Tom Sweeney <tsweeney@redhat.com>
|
||||
Tõnis Tiigi <tonistiigi@gmail.com>
|
||||
Trishna Guha <trishnaguha17@gmail.com>
|
||||
Tristan Carel <tristan@cogniteev.com>
|
||||
Tristan Carel <tristan@cogniteev.com> <tristan.carel@gmail.com>
|
||||
Umesh Yadav <umesh4257@gmail.com>
|
||||
Umesh Yadav <umesh4257@gmail.com> <dungeonmaster18@users.noreply.github.com>
|
||||
Victor Lyuboslavsky <victor@victoreda.com>
|
||||
Victor Vieux <victor.vieux@docker.com> <dev@vvieux.com>
|
||||
Victor Vieux <victor.vieux@docker.com> <victor.vieux@dotcloud.com>
|
||||
Victor Vieux <victor.vieux@docker.com> <victor@docker.com>
|
||||
Victor Vieux <victor.vieux@docker.com> <victor@dotcloud.com>
|
||||
Victor Vieux <victor.vieux@docker.com> <victorvieux@gmail.com>
|
||||
Victor Vieux <victor.vieux@docker.com> <vieux@docker.com>
|
||||
Viktor Vojnovski <viktor.vojnovski@amadeus.com> <vojnovski@gmail.com>
|
||||
Vincent Batts <vbatts@redhat.com> <vbatts@hashbangbash.com>
|
||||
Vincent Bernat <Vincent.Bernat@exoscale.ch> <bernat@luffy.cx>
|
||||
Vincent Bernat <Vincent.Bernat@exoscale.ch> <vincent@bernat.im>
|
||||
Vincent Demeester <vincent.demeester@docker.com> <vincent+github@demeester.fr>
|
||||
Vincent Demeester <vincent.demeester@docker.com> <vincent@demeester.fr>
|
||||
Vincent Demeester <vincent.demeester@docker.com> <vincent@sbr.pm>
|
||||
Vishnu Kannan <vishnuk@google.com>
|
||||
Vladimir Rutsky <altsysrq@gmail.com> <iamironbob@gmail.com>
|
||||
Walter Stanish <walter@pratyeka.org>
|
||||
Wang Guoliang <liangcszzu@163.com>
|
||||
Wang Jie <wangjie5@chinaskycloud.com>
|
||||
Wang Ping <present.wp@icloud.com>
|
||||
Wang Xing <hzwangxing@corp.netease.com> <root@localhost>
|
||||
Wang Yuexiao <wang.yuexiao@zte.com.cn>
|
||||
Wayne Chang <wayne@neverfear.org>
|
||||
Wayne Song <wsong@docker.com> <wsong@users.noreply.github.com>
|
||||
Wei Wu <wuwei4455@gmail.com> cizixs <cizixs@163.com>
|
||||
Wenjun Tang <tangwj2@lenovo.com> <dodia@163.com>
|
||||
Wewang Xiaorenfine <wang.xiaoren@zte.com.cn>
|
||||
Will Weaver <monkey@buildingbananas.com>
|
||||
Xianglin Gao <xlgao@zju.edu.cn>
|
||||
Xianlu Bird <xianlubird@gmail.com>
|
||||
Xiaoyu Zhang <zhang.xiaoyu33@zte.com.cn>
|
||||
Xuecong Liao <satorulogic@gmail.com>
|
||||
Yamasaki Masahide <masahide.y@gmail.com>
|
||||
Yao Zaiyong <yaozaiyong@hotmail.com>
|
||||
Yassine Tijani <yasstij11@gmail.com>
|
||||
Yazhong Liu <yorkiefixer@gmail.com>
|
||||
Yestin Sun <sunyi0804@gmail.com> <yestin.sun@polyera.com>
|
||||
Yi EungJun <eungjun.yi@navercorp.com> <semtlenori@gmail.com>
|
||||
Ying Li <ying.li@docker.com>
|
||||
Ying Li <ying.li@docker.com> <cyli@twistedmatrix.com>
|
||||
Yong Tang <yong.tang.github@outlook.com> <yongtang@users.noreply.github.com>
|
||||
Yosef Fertel <yfertel@gmail.com> <frosforever@users.noreply.github.com>
|
||||
Yu Changchun <yuchangchun1@huawei.com>
|
||||
Yu Chengxia <yuchengxia@huawei.com>
|
||||
Yu Peng <yu.peng36@zte.com.cn>
|
||||
Yu Peng <yu.peng36@zte.com.cn> <yupeng36@zte.com.cn>
|
||||
Zachary Jaffee <zjaffee@us.ibm.com> <zij@case.edu>
|
||||
Zachary Jaffee <zjaffee@us.ibm.com> <zjaffee@apache.org>
|
||||
ZhangHang <stevezhang2014@gmail.com>
|
||||
Zhenkun Bi <bi.zhenkun@zte.com.cn>
|
||||
Zhou Hao <zhouhao@cn.fujitsu.com>
|
||||
Zhu Kunjia <zhu.kunjia@zte.com.cn>
|
||||
Zou Yu <zouyu7@huawei.com>
|
||||
|
||||
627
components/cli/AUTHORS
Normal file
627
components/cli/AUTHORS
Normal file
@ -0,0 +1,627 @@
|
||||
# This file lists all individuals having contributed content to the repository.
|
||||
# For how it is generated, see `scripts/docs/generate-authors.sh`.
|
||||
|
||||
Aanand Prasad <aanand.prasad@gmail.com>
|
||||
Aaron L. Xu <liker.xu@foxmail.com>
|
||||
Aaron Lehmann <aaron.lehmann@docker.com>
|
||||
Aaron.L.Xu <likexu@harmonycloud.cn>
|
||||
Abdur Rehman <abdur_rehman@mentor.com>
|
||||
Abhinandan Prativadi <abhi@docker.com>
|
||||
Abin Shahab <ashahab@altiscale.com>
|
||||
Addam Hardy <addam.hardy@gmail.com>
|
||||
Adolfo Ochagavía <aochagavia92@gmail.com>
|
||||
Adrien Duermael <adrien@duermael.com>
|
||||
Adrien Folie <folie.adrien@gmail.com>
|
||||
Ahmet Alp Balkan <ahmetb@microsoft.com>
|
||||
Aidan Feldman <aidan.feldman@gmail.com>
|
||||
Aidan Hobson Sayers <aidanhs@cantab.net>
|
||||
AJ Bowen <aj@gandi.net>
|
||||
Akihiro Suda <suda.akihiro@lab.ntt.co.jp>
|
||||
Alan Thompson <cloojure@gmail.com>
|
||||
Albert Callarisa <shark234@gmail.com>
|
||||
Aleksa Sarai <asarai@suse.de>
|
||||
Alessandro Boch <aboch@tetrationanalytics.com>
|
||||
Alex Mavrogiannis <alex.mavrogiannis@docker.com>
|
||||
Alexander Boyd <alex@opengroove.org>
|
||||
Alexander Larsson <alexl@redhat.com>
|
||||
Alexander Morozov <lk4d4@docker.com>
|
||||
Alexandre González <agonzalezro@gmail.com>
|
||||
Alfred Landrum <alfred.landrum@docker.com>
|
||||
Alicia Lauerman <alicia@eta.im>
|
||||
Allen Sun <allensun.shl@alibaba-inc.com>
|
||||
Alvin Deng <alvin.q.deng@utexas.edu>
|
||||
Amen Belayneh <amenbelayneh@gmail.com>
|
||||
Amir Goldstein <amir73il@aquasec.com>
|
||||
Amit Krishnan <amit.krishnan@oracle.com>
|
||||
Amit Shukla <amit.shukla@docker.com>
|
||||
Amy Lindburg <amy.lindburg@docker.com>
|
||||
Andrea Luzzardi <aluzzardi@gmail.com>
|
||||
Andreas Köhler <andi5.py@gmx.net>
|
||||
Andrew France <andrew@avito.co.uk>
|
||||
Andrew Hsu <andrewhsu@docker.com>
|
||||
Andrew Macpherson <hopscotch23@gmail.com>
|
||||
Andrew McDonnell <bugs@andrewmcdonnell.net>
|
||||
Andrew Po <absourd.noise@gmail.com>
|
||||
Andrey Petrov <andrey.petrov@shazow.net>
|
||||
André Martins <aanm90@gmail.com>
|
||||
Andy Goldstein <agoldste@redhat.com>
|
||||
Andy Rothfusz <github@developersupport.net>
|
||||
Anil Madhavapeddy <anil@recoil.org>
|
||||
Ankush Agarwal <ankushagarwal11@gmail.com>
|
||||
Anton Polonskiy <anton.polonskiy@gmail.com>
|
||||
Antonio Murdaca <antonio.murdaca@gmail.com>
|
||||
Antonis Kalipetis <akalipetis@gmail.com>
|
||||
Anusha Ragunathan <anusha.ragunathan@docker.com>
|
||||
Arash Deshmeh <adeshmeh@ca.ibm.com>
|
||||
Arnaud Porterie <arnaud.porterie@docker.com>
|
||||
Ashwini Oruganti <ashwini.oruganti@gmail.com>
|
||||
Azat Khuyiyakhmetov <shadow_uz@mail.ru>
|
||||
Bardia Keyoumarsi <bkeyouma@ucsc.edu>
|
||||
Barnaby Gray <barnaby@pickle.me.uk>
|
||||
Bastiaan Bakker <bbakker@xebia.com>
|
||||
BastianHofmann <bastianhofmann@me.com>
|
||||
Ben Bonnefoy <frenchben@docker.com>
|
||||
Ben Firshman <ben@firshman.co.uk>
|
||||
Benjamin Boudreau <boudreau.benjamin@gmail.com>
|
||||
Bhumika Bayani <bhumikabayani@gmail.com>
|
||||
Bill Wang <ozbillwang@gmail.com>
|
||||
Bin Liu <liubin0329@gmail.com>
|
||||
Bingshen Wang <bingshen.wbs@alibaba-inc.com>
|
||||
Boaz Shuster <ripcurld.github@gmail.com>
|
||||
Boris Pruessmann <boris@pruessmann.org>
|
||||
Bradley Cicenas <bradley.cicenas@gmail.com>
|
||||
Brandon Philips <brandon.philips@coreos.com>
|
||||
Brent Salisbury <brent.salisbury@docker.com>
|
||||
Bret Fisher <bret@bretfisher.com>
|
||||
Brian (bex) Exelbierd <bexelbie@redhat.com>
|
||||
Brian Goff <cpuguy83@gmail.com>
|
||||
Bryan Bess <squarejaw@bsbess.com>
|
||||
Bryan Boreham <bjboreham@gmail.com>
|
||||
Bryan Murphy <bmurphy1976@gmail.com>
|
||||
bryfry <bryon.fryer@gmail.com>
|
||||
Cameron Spear <cameronspear@gmail.com>
|
||||
Cao Weiwei <cao.weiwei30@zte.com.cn>
|
||||
Carlo Mion <mion00@gmail.com>
|
||||
Carlos Alexandro Becker <caarlos0@gmail.com>
|
||||
Ce Gao <ce.gao@outlook.com>
|
||||
Cedric Davies <cedricda@microsoft.com>
|
||||
Cezar Sa Espinola <cezarsa@gmail.com>
|
||||
Chao Wang <wangchao.fnst@cn.fujitsu.com>
|
||||
Charles Chan <charleswhchan@users.noreply.github.com>
|
||||
Charles Law <claw@conduce.com>
|
||||
Charles Smith <charles.smith@docker.com>
|
||||
Charlie Drage <charlie@charliedrage.com>
|
||||
ChaYoung You <yousbe@gmail.com>
|
||||
Chen Chuanliang <chen.chuanliang@zte.com.cn>
|
||||
Chen Hanxiao <chenhanxiao@cn.fujitsu.com>
|
||||
Chen Mingjie <chenmingjie0828@163.com>
|
||||
Chen Qiu <cheney-90@hotmail.com>
|
||||
Chris Gavin <chris@chrisgavin.me>
|
||||
Chris Gibson <chris@chrisg.io>
|
||||
Chris McKinnel <chrismckinnel@gmail.com>
|
||||
Chris Snow <chsnow123@gmail.com>
|
||||
Chris Weyl <cweyl@alumni.drew.edu>
|
||||
Christian Persson <saser@live.se>
|
||||
Christian Stefanescu <st.chris@gmail.com>
|
||||
Christophe Robin <crobin@nekoo.com>
|
||||
Christophe Vidal <kriss@krizalys.com>
|
||||
Christopher Biscardi <biscarch@sketcht.com>
|
||||
Christopher Jones <tophj@linux.vnet.ibm.com>
|
||||
Chun Chen <ramichen@tencent.com>
|
||||
Clinton Kitson <clintonskitson@gmail.com>
|
||||
Coenraad Loubser <coenraad@wish.org.za>
|
||||
Colin Hebert <hebert.colin@gmail.com>
|
||||
Collin Guarino <collin.guarino@gmail.com>
|
||||
Colm Hally <colmhally@gmail.com>
|
||||
Corey Farrell <git@cfware.com>
|
||||
Cristian Staretu <cristian.staretu@gmail.com>
|
||||
Daehyeok Mun <daehyeok@gmail.com>
|
||||
Dafydd Crosby <dtcrsby@gmail.com>
|
||||
dalanlan <dalanlan925@gmail.com>
|
||||
Damien Nadé <github@livna.org>
|
||||
Dan Cotora <dan@bluevision.ro>
|
||||
Daniel Dao <dqminh@cloudflare.com>
|
||||
Daniel Farrell <dfarrell@redhat.com>
|
||||
Daniel Gasienica <daniel@gasienica.ch>
|
||||
Daniel Goosen <daniel.goosen@surveysampling.com>
|
||||
Daniel Hiltgen <daniel.hiltgen@docker.com>
|
||||
Daniel J Walsh <dwalsh@redhat.com>
|
||||
Daniel Nephin <dnephin@docker.com>
|
||||
Daniel Norberg <dano@spotify.com>
|
||||
Daniel Watkins <daniel@daniel-watkins.co.uk>
|
||||
Daniel Zhang <jmzwcn@gmail.com>
|
||||
Danny Berger <dpb587@gmail.com>
|
||||
Darren Shepherd <darren.s.shepherd@gmail.com>
|
||||
Darren Stahl <darst@microsoft.com>
|
||||
Dattatraya Kumbhar <dattatraya.kumbhar@gslab.com>
|
||||
Dave Goodchild <buddhamagnet@gmail.com>
|
||||
Dave Henderson <dhenderson@gmail.com>
|
||||
Dave Tucker <dt@docker.com>
|
||||
David Calavera <david.calavera@gmail.com>
|
||||
David Cramer <davcrame@cisco.com>
|
||||
David Dooling <dooling@gmail.com>
|
||||
David Gageot <david@gageot.net>
|
||||
David Lechner <david@lechnology.com>
|
||||
David Sheets <dsheets@docker.com>
|
||||
David Williamson <david.williamson@docker.com>
|
||||
David Xia <dxia@spotify.com>
|
||||
David Young <yangboh@cn.ibm.com>
|
||||
Deng Guangxing <dengguangxing@huawei.com>
|
||||
Denis Defreyne <denis@soundcloud.com>
|
||||
Denis Gladkikh <denis@gladkikh.email>
|
||||
Denis Ollier <larchunix@users.noreply.github.com>
|
||||
Dennis Docter <dennis@d23.nl>
|
||||
Derek McGowan <derek@mcgstyle.net>
|
||||
Deshi Xiao <dxiao@redhat.com>
|
||||
Dharmit Shah <shahdharmit@gmail.com>
|
||||
Dhawal Yogesh Bhanushali <dbhanushali@vmware.com>
|
||||
Dieter Reuter <dieter.reuter@me.com>
|
||||
Dima Stopel <dima@twistlock.com>
|
||||
Dimitry Andric <d.andric@activevideo.com>
|
||||
Ding Fei <dingfei@stars.org.cn>
|
||||
Diogo Monica <diogo@docker.com>
|
||||
Dmitry Gusev <dmitry.gusev@gmail.com>
|
||||
Dmitry Smirnov <onlyjob@member.fsf.org>
|
||||
Dmitry V. Krivenok <krivenok.dmitry@gmail.com>
|
||||
Don Kjer <don.kjer@gmail.com>
|
||||
Dong Chen <dongluo.chen@docker.com>
|
||||
Doug Davis <dug@us.ibm.com>
|
||||
Drew Erny <drew.erny@docker.com>
|
||||
Ed Costello <epc@epcostello.com>
|
||||
Eli Uriegas <eli.uriegas@docker.com>
|
||||
Eli Uriegas <seemethere101@gmail.com>
|
||||
Elias Faxö <elias.faxo@tre.se>
|
||||
Eric G. Noriega <enoriega@vizuri.com>
|
||||
Eric Rosenberg <ehaydenr@gmail.com>
|
||||
Eric Sage <eric.david.sage@gmail.com>
|
||||
Eric-Olivier Lamey <eo@lamey.me>
|
||||
Erica Windisch <erica@windisch.us>
|
||||
Erik Hollensbe <github@hollensbe.org>
|
||||
Erik St. Martin <alakriti@gmail.com>
|
||||
Eugene Yakubovich <eugene.yakubovich@coreos.com>
|
||||
Evan Allrich <evan@unguku.com>
|
||||
Evan Hazlett <ejhazlett@gmail.com>
|
||||
Evan Krall <krall@yelp.com>
|
||||
Evelyn Xu <evelynhsu21@gmail.com>
|
||||
Everett Toews <everett.toews@rackspace.com>
|
||||
Fabio Falci <fabiofalci@gmail.com>
|
||||
Fabrizio Soppelsa <fsoppelsa@mirantis.com>
|
||||
Felix Hupfeld <felix@quobyte.com>
|
||||
Felix Rabe <felix@rabe.io>
|
||||
Flavio Crisciani <flavio.crisciani@docker.com>
|
||||
Florian Klein <florian.klein@free.fr>
|
||||
Foysal Iqbal <foysal.iqbal.fb@gmail.com>
|
||||
Fred Lifton <fred.lifton@docker.com>
|
||||
Frederick F. Kautz IV <fkautz@redhat.com>
|
||||
Frederik Nordahl Jul Sabroe <frederikns@gmail.com>
|
||||
Frieder Bluemle <frieder.bluemle@gmail.com>
|
||||
Gabriel Nicolas Avellaneda <avellaneda.gabriel@gmail.com>
|
||||
Gaetan de Villele <gdevillele@gmail.com>
|
||||
Gang Qiao <qiaohai8866@gmail.com>
|
||||
Gary Schaetz <gary@schaetzkc.com>
|
||||
Genki Takiuchi <genki@s21g.com>
|
||||
George MacRorie <gmacr31@gmail.com>
|
||||
George Xie <georgexsh@gmail.com>
|
||||
Gianluca Borello <g.borello@gmail.com>
|
||||
Gildas Cuisinier <gildas.cuisinier@gcuisinier.net>
|
||||
Gou Rao <gou@portworx.com>
|
||||
Grant Reaber <grant.reaber@gmail.com>
|
||||
Greg Pflaum <gpflaum@users.noreply.github.com>
|
||||
Guilhem Lettron <guilhem+github@lettron.fr>
|
||||
Guillaume J. Charmes <guillaume.charmes@docker.com>
|
||||
gwx296173 <gaojing3@huawei.com>
|
||||
Günther Jungbluth <gunther@gameslabs.net>
|
||||
Hakan Özler <hakan.ozler@kodcu.com>
|
||||
Hao Zhang <21521210@zju.edu.cn>
|
||||
Harald Albers <github@albersweb.de>
|
||||
Harold Cooper <hrldcpr@gmail.com>
|
||||
Harry Zhang <harryz@hyper.sh>
|
||||
He Simei <hesimei@zju.edu.cn>
|
||||
Helen Xie <chenjg@harmonycloud.cn>
|
||||
Henning Sprang <henning.sprang@gmail.com>
|
||||
Hernan Garcia <hernandanielg@gmail.com>
|
||||
Hongbin Lu <hongbin034@gmail.com>
|
||||
Hu Keping <hukeping@huawei.com>
|
||||
Huayi Zhang <irachex@gmail.com>
|
||||
huqun <huqun@zju.edu.cn>
|
||||
Huu Nguyen <huu@prismskylabs.com>
|
||||
Hyzhou Zhy <hyzhou.zhy@alibaba-inc.com>
|
||||
Ian Campbell <ian.campbell@docker.com>
|
||||
Ian Philpot <ian.philpot@microsoft.com>
|
||||
Ignacio Capurro <icapurrofagian@gmail.com>
|
||||
Ilya Dmitrichenko <errordeveloper@gmail.com>
|
||||
Ilya Khlopotov <ilya.khlopotov@gmail.com>
|
||||
Ilya Sotkov <ilya@sotkov.com>
|
||||
Isabel Jimenez <contact.isabeljimenez@gmail.com>
|
||||
Ivan Grcic <igrcic@gmail.com>
|
||||
Jacob Atzen <jacob@jacobatzen.dk>
|
||||
Jacob Tomlinson <jacob@tom.linson.uk>
|
||||
Jaivish Kothari <janonymous.codevulture@gmail.com>
|
||||
Jake Sanders <jsand@google.com>
|
||||
James Nesbitt <james.nesbitt@wunderkraut.com>
|
||||
James Turnbull <james@lovedthanlost.net>
|
||||
Jamie Hannaford <jamie@limetree.org>
|
||||
Jan Koprowski <jan.koprowski@gmail.com>
|
||||
Jan Pazdziora <jpazdziora@redhat.com>
|
||||
Jan-Jaap Driessen <janjaapdriessen@gmail.com>
|
||||
Jana Radhakrishnan <mrjana@docker.com>
|
||||
Jared Hocutt <jaredh@netapp.com>
|
||||
Jasmine Hegman <jasmine@jhegman.com>
|
||||
Jason Heiss <jheiss@aput.net>
|
||||
Jason Plum <jplum@devonit.com>
|
||||
Jay Kamat <github@jgkamat.33mail.com>
|
||||
Jean-Pierre Huynh <jean-pierre.huynh@ounet.fr>
|
||||
Jean-Pierre Huynh <jp@moogsoft.com>
|
||||
Jeff Lindsay <progrium@gmail.com>
|
||||
Jeff Nickoloff <jeff.nickoloff@gmail.com>
|
||||
Jeff Silberman <jsilberm@gmail.com>
|
||||
Jeremy Chambers <jeremy@thehipbot.com>
|
||||
Jeremy Unruh <jeremybunruh@gmail.com>
|
||||
Jeremy Yallop <yallop@docker.com>
|
||||
Jeroen Franse <jeroenfranse@gmail.com>
|
||||
Jesse Adametz <jesseadametz@gmail.com>
|
||||
Jessica Frazelle <jessfraz@google.com>
|
||||
Jezeniel Zapanta <jpzapanta22@gmail.com>
|
||||
Jian Zhang <zhangjian.fnst@cn.fujitsu.com>
|
||||
Jie Luo <luo612@zju.edu.cn>
|
||||
Jilles Oldenbeuving <ojilles@gmail.com>
|
||||
Jim Galasyn <jim.galasyn@docker.com>
|
||||
Jimmy Leger <jimmy.leger@gmail.com>
|
||||
Jimmy Song <rootsongjc@gmail.com>
|
||||
jimmyxian <jimmyxian2004@yahoo.com.cn>
|
||||
Joao Fernandes <joao.fernandes@docker.com>
|
||||
Joe Doliner <jdoliner@pachyderm.io>
|
||||
Joe Gordon <joe.gordon0@gmail.com>
|
||||
Joel Handwell <joelhandwell@gmail.com>
|
||||
Joey Geiger <jgeiger@gmail.com>
|
||||
Joffrey F <joffrey@docker.com>
|
||||
Johan Euphrosine <proppy@google.com>
|
||||
Johannes 'fish' Ziemke <github@freigeist.org>
|
||||
John Feminella <jxf@jxf.me>
|
||||
John Harris <john@johnharris.io>
|
||||
John Howard (VM) <John.Howard@microsoft.com>
|
||||
John Laswell <john.n.laswell@gmail.com>
|
||||
John Maguire <jmaguire@duosecurity.com>
|
||||
John Mulhausen <john@docker.com>
|
||||
John Starks <jostarks@microsoft.com>
|
||||
John Stephens <johnstep@docker.com>
|
||||
John Tims <john.k.tims@gmail.com>
|
||||
John V. Martinez <jvmatl@gmail.com>
|
||||
John Willis <john.willis@docker.com>
|
||||
Jonathan Boulle <jonathanboulle@gmail.com>
|
||||
Jonathan Lee <jonjohn1232009@gmail.com>
|
||||
Jonathan Lomas <jonathan@floatinglomas.ca>
|
||||
Jonathan McCrohan <jmccrohan@gmail.com>
|
||||
Jonh Wendell <jonh.wendell@redhat.com>
|
||||
Jordan Jennings <jjn2009@gmail.com>
|
||||
Joseph Kern <jkern@semafour.net>
|
||||
Josh Bodah <jb3689@yahoo.com>
|
||||
Josh Chorlton <jchorlton@gmail.com>
|
||||
Josh Hawn <josh.hawn@docker.com>
|
||||
Josh Horwitz <horwitz@addthis.com>
|
||||
Josh Soref <jsoref@gmail.com>
|
||||
Julien Barbier <write0@gmail.com>
|
||||
Julien Kassar <github@kassisol.com>
|
||||
Julien Maitrehenry <julien.maitrehenry@me.com>
|
||||
Justas Brazauskas <brazauskasjustas@gmail.com>
|
||||
Justin Cormack <justin.cormack@docker.com>
|
||||
Justin Simonelis <justin.p.simonelis@gmail.com>
|
||||
Jyrki Puttonen <jyrkiput@gmail.com>
|
||||
Jérôme Petazzoni <jerome.petazzoni@docker.com>
|
||||
Jörg Thalheim <joerg@higgsboson.tk>
|
||||
Kai Blin <kai@samba.org>
|
||||
Kai Qiang Wu (Kennan) <wkq5325@gmail.com>
|
||||
Kara Alexandra <kalexandra@us.ibm.com>
|
||||
Kareem Khazem <karkhaz@karkhaz.com>
|
||||
Karthik Nayak <Karthik.188@gmail.com>
|
||||
Katie McLaughlin <katie@glasnt.com>
|
||||
Ke Xu <leonhartx.k@gmail.com>
|
||||
Kei Ohmura <ohmura.kei@gmail.com>
|
||||
Keith Hudgins <greenman@greenman.org>
|
||||
Ken Cochrane <kencochrane@gmail.com>
|
||||
Ken ICHIKAWA <ichikawa.ken@jp.fujitsu.com>
|
||||
Kenfe-Mickaël Laventure <mickael.laventure@gmail.com>
|
||||
Kevin Burke <kev@inburke.com>
|
||||
Kevin Feyrer <kevin.feyrer@btinternet.com>
|
||||
Kevin Kern <kaiwentan@harmonycloud.cn>
|
||||
Kevin Meredith <kevin.m.meredith@gmail.com>
|
||||
Kevin Richardson <kevin@kevinrichardson.co>
|
||||
khaled souf <khaled.souf@gmail.com>
|
||||
Kim Eik <kim@heldig.org>
|
||||
Kir Kolyshkin <kolyshkin@gmail.com>
|
||||
Kotaro Yoshimatsu <kotaro.yoshimatsu@gmail.com>
|
||||
Krasi Georgiev <krasi@vip-consult.solutions>
|
||||
Kris-Mikael Krister <krismikael@protonmail.com>
|
||||
Kun Zhang <zkazure@gmail.com>
|
||||
Kunal Kushwaha <kushwaha_kunal_v7@lab.ntt.co.jp>
|
||||
Kyle Spiers <kyle@spiers.me>
|
||||
Lachlan Cooper <lachlancooper@gmail.com>
|
||||
Lai Jiangshan <jiangshanlai@gmail.com>
|
||||
Lars Kellogg-Stedman <lars@redhat.com>
|
||||
Laura Frank <ljfrank@gmail.com>
|
||||
Laurent Erignoux <lerignoux@gmail.com>
|
||||
Lei Jitang <leijitang@huawei.com>
|
||||
Lennie <github@consolejunkie.net>
|
||||
Leo Gallucci <elgalu3@gmail.com>
|
||||
Lewis Daly <lewisdaly@me.com>
|
||||
Li Yi <denverdino@gmail.com>
|
||||
Li Yi <weiyuan.yl@alibaba-inc.com>
|
||||
Liang-Chi Hsieh <viirya@gmail.com>
|
||||
Lily Guo <lily.guo@docker.com>
|
||||
Lin Lu <doraalin@163.com>
|
||||
Linus Heckemann <lheckemann@twig-world.com>
|
||||
Liping Xue <lipingxue@gmail.com>
|
||||
Liron Levin <liron@twistlock.com>
|
||||
liwenqi <vikilwq@zju.edu.cn>
|
||||
lixiaobing10051267 <li.xiaobing1@zte.com.cn>
|
||||
Lloyd Dewolf <foolswisdom@gmail.com>
|
||||
Lorenzo Fontana <lo@linux.com>
|
||||
Louis Opter <kalessin@kalessin.fr>
|
||||
Luca Favatella <luca.favatella@erlang-solutions.com>
|
||||
Luca Marturana <lucamarturana@gmail.com>
|
||||
Lucas Chan <lucas-github@lucaschan.com>
|
||||
Luka Hartwig <mail@lukahartwig.de>
|
||||
Lukasz Zajaczkowski <Lukasz.Zajaczkowski@ts.fujitsu.com>
|
||||
Lénaïc Huard <lhuard@amadeus.com>
|
||||
Ma Shimiao <mashimiao.fnst@cn.fujitsu.com>
|
||||
Mabin <bin.ma@huawei.com>
|
||||
Madhav Puri <madhav.puri@gmail.com>
|
||||
Madhu Venugopal <madhu@socketplane.io>
|
||||
Malte Janduda <mail@janduda.net>
|
||||
Manjunath A Kumatagi <mkumatag@in.ibm.com>
|
||||
Mansi Nahar <mmn4185@rit.edu>
|
||||
mapk0y <mapk0y@gmail.com>
|
||||
Marc Bihlmaier <marc.bihlmaier@reddoxx.com>
|
||||
Marco Mariani <marco.mariani@alterway.fr>
|
||||
Marcus Martins <marcus@docker.com>
|
||||
Marianna Tessel <mtesselh@gmail.com>
|
||||
Marius Sturm <marius@graylog.com>
|
||||
Mark Oates <fl0yd@me.com>
|
||||
Martin Mosegaard Amdisen <martin.amdisen@praqma.com>
|
||||
Mary Anthony <mary.anthony@docker.com>
|
||||
Mason Malone <mason.malone@gmail.com>
|
||||
Matt Gucci <matt9ucci@gmail.com>
|
||||
Matt Robenolt <matt@ydekproductions.com>
|
||||
Matthew Heon <mheon@redhat.com>
|
||||
Matthieu Hauglustaine <matt.hauglustaine@gmail.com>
|
||||
Max Shytikov <mshytikov@gmail.com>
|
||||
Maxime Petazzoni <max@signalfuse.com>
|
||||
Mei ChunTao <mei.chuntao@zte.com.cn>
|
||||
Micah Zoltu <micah@newrelic.com>
|
||||
Michael A. Smith <michael@smith-li.com>
|
||||
Michael Bridgen <mikeb@squaremobius.net>
|
||||
Michael Crosby <michael@docker.com>
|
||||
Michael Friis <friism@gmail.com>
|
||||
Michael Irwin <mikesir87@gmail.com>
|
||||
Michael Käufl <docker@c.michael-kaeufl.de>
|
||||
Michael Prokop <github@michael-prokop.at>
|
||||
Michael Scharf <github@scharf.gr>
|
||||
Michael Spetsiotis <michael_spets@hotmail.com>
|
||||
Michael Steinert <mike.steinert@gmail.com>
|
||||
Michael West <mwest@mdsol.com>
|
||||
Michal Minář <miminar@redhat.com>
|
||||
Michał Czeraszkiewicz <czerasz@gmail.com>
|
||||
Miguel Angel Alvarez Cabrerizo <doncicuto@gmail.com>
|
||||
Mihai Borobocea <MihaiBorob@gmail.com>
|
||||
Mihuleacc Sergiu <mihuleac.sergiu@gmail.com>
|
||||
Mike Brown <brownwm@us.ibm.com>
|
||||
Mike Casas <mkcsas0@gmail.com>
|
||||
Mike Danese <mikedanese@google.com>
|
||||
Mike Dillon <mike@embody.org>
|
||||
Mike Goelzer <mike.goelzer@docker.com>
|
||||
Mike MacCana <mike.maccana@gmail.com>
|
||||
mikelinjie <294893458@qq.com>
|
||||
Mikhail Vasin <vasin@cloud-tv.ru>
|
||||
Milind Chawre <milindchawre@gmail.com>
|
||||
Misty Stanley-Jones <misty@docker.com>
|
||||
Mohammad Banikazemi <mb@us.ibm.com>
|
||||
Mohammed Aaqib Ansari <maaquib@gmail.com>
|
||||
Moorthy RS <rsmoorthy@gmail.com>
|
||||
Morgan Bauer <mbauer@us.ibm.com>
|
||||
Moysés Borges <moysesb@gmail.com>
|
||||
Mrunal Patel <mrunalp@gmail.com>
|
||||
Muthukumar R <muthur@gmail.com>
|
||||
Máximo Cuadros <mcuadros@gmail.com>
|
||||
Nace Oroz <orkica@gmail.com>
|
||||
Nahum Shalman <nshalman@omniti.com>
|
||||
Nalin Dahyabhai <nalin@redhat.com>
|
||||
Natalie Parker <nparker@omnifone.com>
|
||||
Nate Brennand <nate.brennand@clever.com>
|
||||
Nathan Hsieh <hsieh.nathan@gmail.com>
|
||||
Nathan LeClaire <nathan.leclaire@docker.com>
|
||||
Nathan McCauley <nathan.mccauley@docker.com>
|
||||
Neil Peterson <neilpeterson@outlook.com>
|
||||
Nicola Kabar <nicolaka@gmail.com>
|
||||
Nicolas Borboën <ponsfrilus@gmail.com>
|
||||
Nicolas De Loof <nicolas.deloof@gmail.com>
|
||||
Nikhil Chawla <chawlanikhil24@gmail.com>
|
||||
Nikolas Garofil <nikolas.garofil@uantwerpen.be>
|
||||
Nikolay Milovanov <nmil@itransformers.net>
|
||||
Nishant Totla <nishanttotla@gmail.com>
|
||||
NIWA Hideyuki <niwa.niwa@nifty.ne.jp>
|
||||
Noah Treuhaft <noah.treuhaft@docker.com>
|
||||
O.S. Tezer <ostezer@gmail.com>
|
||||
ohmystack <jun.jiang02@ele.me>
|
||||
Olle Jonsson <olle.jonsson@gmail.com>
|
||||
Otto Kekäläinen <otto@seravo.fi>
|
||||
Ovidio Mallo <ovidio.mallo@gmail.com>
|
||||
Pascal Borreli <pascal@borreli.com>
|
||||
Patrick Böänziger <patrick.baenziger@bsi-software.com>
|
||||
Patrick Hemmer <patrick.hemmer@gmail.com>
|
||||
Patrick Lang <plang@microsoft.com>
|
||||
Paul <paul9869@gmail.com>
|
||||
Paul Kehrer <paul.l.kehrer@gmail.com>
|
||||
Paul Lietar <paul@lietar.net>
|
||||
Paul Weaver <pauweave@cisco.com>
|
||||
Pavel Pospisil <pospispa@gmail.com>
|
||||
Paweł Szczekutowicz <pszczekutowicz@gmail.com>
|
||||
Peeyush Gupta <gpeeyush@linux.vnet.ibm.com>
|
||||
Peter Edge <peter.edge@gmail.com>
|
||||
Peter Jaffe <pjaffe@nevo.com>
|
||||
Peter Nagy <xificurC@gmail.com>
|
||||
Peter Salvatore <peter@psftw.com>
|
||||
Peter Waller <p@pwaller.net>
|
||||
Phil Estes <estesp@linux.vnet.ibm.com>
|
||||
Philip Alexander Etling <paetling@gmail.com>
|
||||
Philipp Gillé <philipp.gille@gmail.com>
|
||||
pidster <pid@pidster.com>
|
||||
pixelistik <pixelistik@users.noreply.github.com>
|
||||
Pratik Karki <prertik@outlook.com>
|
||||
Prayag Verma <prayag.verma@gmail.com>
|
||||
Pure White <daniel48@126.com>
|
||||
Qiang Huang <h.huangqiang@huawei.com>
|
||||
Qinglan Peng <qinglanpeng@zju.edu.cn>
|
||||
qudongfang <qudongfang@gmail.com>
|
||||
Raghavendra K T <raghavendra.kt@linux.vnet.ibm.com>
|
||||
Ray Tsang <rayt@google.com>
|
||||
Reficul <xuzhenglun@gmail.com>
|
||||
Remy Suen <remy.suen@gmail.com>
|
||||
Renaud Gaubert <rgaubert@nvidia.com>
|
||||
Ricardo N Feliciano <FelicianoTech@gmail.com>
|
||||
Rich Moyse <rich@moyse.us>
|
||||
Richard Mathie <richard.mathie@amey.co.uk>
|
||||
Richard Scothern <richard.scothern@gmail.com>
|
||||
Rick Wieman <git@rickw.nl>
|
||||
Ritesh H Shukla <sritesh@vmware.com>
|
||||
Riyaz Faizullabhoy <riyaz.faizullabhoy@docker.com>
|
||||
Robert Wallis <smilingrob@gmail.com>
|
||||
Robin Naundorf <r.naundorf@fh-muenster.de>
|
||||
Robin Speekenbrink <robin@kingsquare.nl>
|
||||
Rodolfo Ortiz <rodolfo.ortiz@definityfirst.com>
|
||||
Rogelio Canedo <rcanedo@mappy.priv>
|
||||
Roland Kammerer <roland.kammerer@linbit.com>
|
||||
Roman Dudin <katrmr@gmail.com>
|
||||
Rory Hunter <roryhunter2@gmail.com>
|
||||
Ross Boucher <rboucher@gmail.com>
|
||||
Rubens Figueiredo <r.figueiredo.52@gmail.com>
|
||||
Ryan Belgrave <rmb1993@gmail.com>
|
||||
Ryan Detzel <ryan.detzel@gmail.com>
|
||||
Ryan Stelly <ryan.stelly@live.com>
|
||||
Sainath Grandhi <sainath.grandhi@intel.com>
|
||||
Sakeven Jiang <jc5930@sina.cn>
|
||||
Sally O'Malley <somalley@redhat.com>
|
||||
Sam Neirinck <sam@samneirinck.com>
|
||||
Sambuddha Basu <sambuddhabasu1@gmail.com>
|
||||
Samuel Karp <skarp@amazon.com>
|
||||
Santhosh Manohar <santhosh@docker.com>
|
||||
Scott Collier <emailscottcollier@gmail.com>
|
||||
Sean Christopherson <sean.j.christopherson@intel.com>
|
||||
Sean Rodman <srodman7689@gmail.com>
|
||||
Sebastiaan van Stijn <github@gone.nl>
|
||||
Sergey Tryuber <Sergeant007@users.noreply.github.com>
|
||||
Serhat Gülçiçek <serhat25@gmail.com>
|
||||
Sevki Hasirci <s@sevki.org>
|
||||
Shaun Kaasten <shaunk@gmail.com>
|
||||
Sheng Yang <sheng@yasker.org>
|
||||
Shijiang Wei <mountkin@gmail.com>
|
||||
Shishir Mahajan <shishir.mahajan@redhat.com>
|
||||
Shoubhik Bose <sbose78@gmail.com>
|
||||
Shukui Yang <yangshukui@huawei.com>
|
||||
Sian Lerk Lau <kiawin@gmail.com>
|
||||
Sidhartha Mani <sidharthamn@gmail.com>
|
||||
sidharthamani <sid@rancher.com>
|
||||
Simei He <hesimei@zju.edu.cn>
|
||||
Simon Ferquel <simon.ferquel@docker.com>
|
||||
Sindhu S <sindhus@live.in>
|
||||
Slava Semushin <semushin@redhat.com>
|
||||
Solomon Hykes <solomon@docker.com>
|
||||
Song Gao <song@gao.io>
|
||||
Spencer Brown <spencer@spencerbrown.org>
|
||||
squeegels <1674195+squeegels@users.noreply.github.com>
|
||||
Srini Brahmaroutu <srbrahma@us.ibm.com>
|
||||
Stefan S. <tronicum@user.github.com>
|
||||
Stefan Scherer <scherer_stefan@icloud.com>
|
||||
Stefan Weil <sw@weilnetz.de>
|
||||
Stephen Day <stephen.day@docker.com>
|
||||
Stephen Rust <srust@blockbridge.com>
|
||||
Steve Durrheimer <s.durrheimer@gmail.com>
|
||||
Steven Burgess <steven.a.burgess@hotmail.com>
|
||||
Subhajit Ghosh <isubuz.g@gmail.com>
|
||||
Sun Jianbo <wonderflow.sun@gmail.com>
|
||||
Sven Dowideit <SvenDowideit@home.org.au>
|
||||
Sylvain Baubeau <sbaubeau@redhat.com>
|
||||
Sébastien HOUZÉ <cto@verylastroom.com>
|
||||
T K Sourabh <sourabhtk37@gmail.com>
|
||||
TAGOMORI Satoshi <tagomoris@gmail.com>
|
||||
Taylor Jones <monitorjbl@gmail.com>
|
||||
Thatcher Peskens <thatcher@docker.com>
|
||||
Thomas Gazagnaire <thomas@gazagnaire.org>
|
||||
Thomas Leonard <thomas.leonard@docker.com>
|
||||
Thomas Léveil <thomasleveil@gmail.com>
|
||||
Thomas Riccardi <riccardi@systran.fr>
|
||||
Thomas Swift <tgs242@gmail.com>
|
||||
Tianon Gravi <admwiggin@gmail.com>
|
||||
Tianyi Wang <capkurmagati@gmail.com>
|
||||
Tibor Vass <teabee89@gmail.com>
|
||||
Tim Dettrick <t.dettrick@uq.edu.au>
|
||||
Tim Hockin <thockin@google.com>
|
||||
Tim Smith <timbot@google.com>
|
||||
Tim Waugh <twaugh@redhat.com>
|
||||
Tim Wraight <tim.wraight@tangentlabs.co.uk>
|
||||
timfeirg <kkcocogogo@gmail.com>
|
||||
Timothy Hobbs <timothyhobbs@seznam.cz>
|
||||
Tobias Bradtke <webwurst@gmail.com>
|
||||
Tobias Gesellchen <tobias@gesellix.de>
|
||||
Todd Whiteman <todd.whiteman@joyent.com>
|
||||
Tom Denham <tom@tomdee.co.uk>
|
||||
Tom Fotherby <tom+github@peopleperhour.com>
|
||||
Tom X. Tobin <tomxtobin@tomxtobin.com>
|
||||
Tomas Tomecek <ttomecek@redhat.com>
|
||||
Tomasz Kopczynski <tomek@kopczynski.net.pl>
|
||||
Tomáš Hrčka <thrcka@redhat.com>
|
||||
Tony Abboud <tdabboud@hotmail.com>
|
||||
Tõnis Tiigi <tonistiigi@gmail.com>
|
||||
Trapier Marshall <trapier.marshall@docker.com>
|
||||
Travis Cline <travis.cline@gmail.com>
|
||||
Tristan Carel <tristan@cogniteev.com>
|
||||
Tycho Andersen <tycho@docker.com>
|
||||
Tycho Andersen <tycho@tycho.ws>
|
||||
uhayate <uhayate.gong@daocloud.io>
|
||||
Umesh Yadav <umesh4257@gmail.com>
|
||||
Valentin Lorentz <progval+git@progval.net>
|
||||
Veres Lajos <vlajos@gmail.com>
|
||||
Victor Vieux <victor.vieux@docker.com>
|
||||
Victoria Bialas <victoria.bialas@docker.com>
|
||||
Viktor Stanchev <me@viktorstanchev.com>
|
||||
Vincent Batts <vbatts@redhat.com>
|
||||
Vincent Bernat <Vincent.Bernat@exoscale.ch>
|
||||
Vincent Demeester <vincent.demeester@docker.com>
|
||||
Vincent Woo <me@vincentwoo.com>
|
||||
Vishnu Kannan <vishnuk@google.com>
|
||||
Vivek Goyal <vgoyal@redhat.com>
|
||||
Wang Jie <wangjie5@chinaskycloud.com>
|
||||
Wang Long <long.wanglong@huawei.com>
|
||||
Wang Ping <present.wp@icloud.com>
|
||||
Wang Xing <hzwangxing@corp.netease.com>
|
||||
Wang Yuexiao <wang.yuexiao@zte.com.cn>
|
||||
Wayne Song <wsong@docker.com>
|
||||
Wen Cheng Ma <wenchma@cn.ibm.com>
|
||||
Wenzhi Liang <wenzhi.liang@gmail.com>
|
||||
Wes Morgan <cap10morgan@gmail.com>
|
||||
Wewang Xiaorenfine <wang.xiaoren@zte.com.cn>
|
||||
William Henry <whenry@redhat.com>
|
||||
Xianglin Gao <xlgao@zju.edu.cn>
|
||||
Xinbo Weng <xihuanbo_0521@zju.edu.cn>
|
||||
Xuecong Liao <satorulogic@gmail.com>
|
||||
Yan Feng <yanfeng2@huawei.com>
|
||||
Yanqiang Miao <miao.yanqiang@zte.com.cn>
|
||||
Yassine Tijani <yasstij11@gmail.com>
|
||||
Yi EungJun <eungjun.yi@navercorp.com>
|
||||
Ying Li <ying.li@docker.com>
|
||||
Yong Tang <yong.tang.github@outlook.com>
|
||||
Yosef Fertel <yfertel@gmail.com>
|
||||
Yu Peng <yu.peng36@zte.com.cn>
|
||||
Yuan Sun <sunyuan3@huawei.com>
|
||||
Yunxiang Huang <hyxqshk@vip.qq.com>
|
||||
zebrilee <zebrilee@gmail.com>
|
||||
Zhang Kun <zkazure@gmail.com>
|
||||
Zhang Wei <zhangwei555@huawei.com>
|
||||
Zhang Wentao <zhangwentao234@huawei.com>
|
||||
ZhangHang <stevezhang2014@gmail.com>
|
||||
zhenghenghuo <zhenghenghuo@zju.edu.cn>
|
||||
Zhou Hao <zhouhao@cn.fujitsu.com>
|
||||
Zhu Guihua <zhugh.fnst@cn.fujitsu.com>
|
||||
Álex González <agonzalezro@gmail.com>
|
||||
Álvaro Lázaro <alvaro.lazaro.g@gmail.com>
|
||||
Átila Camurça Alves <camurca.home@gmail.com>
|
||||
徐俊杰 <paco.xu@daocloud.io>
|
||||
@ -54,6 +54,10 @@ vendor: vendor.conf ## check that vendor matches vendor.conf
|
||||
vndr 2> /dev/null
|
||||
scripts/validate/check-git-diff vendor
|
||||
|
||||
.PHONY: authors
|
||||
authors: ## generate AUTHORS file from git history
|
||||
scripts/docs/generate-authors.sh
|
||||
|
||||
.PHONY: manpages
|
||||
manpages: ## generate man pages from go source and markdown
|
||||
scripts/docs/generate-man.sh
|
||||
|
||||
@ -1 +1 @@
|
||||
17.12.0-ce
|
||||
18.02.0-ce
|
||||
|
||||
@ -5,16 +5,22 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/config"
|
||||
cliconfig "github.com/docker/cli/cli/config"
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
cliflags "github.com/docker/cli/cli/flags"
|
||||
manifeststore "github.com/docker/cli/cli/manifest/store"
|
||||
registryclient "github.com/docker/cli/cli/registry/client"
|
||||
"github.com/docker/cli/cli/trust"
|
||||
dopts "github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api"
|
||||
"github.com/docker/docker/api/types"
|
||||
registrytypes "github.com/docker/docker/api/types/registry"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/docker/go-connections/sockets"
|
||||
"github.com/docker/go-connections/tlsconfig"
|
||||
@ -42,24 +48,28 @@ type Cli interface {
|
||||
SetIn(in *InStream)
|
||||
ConfigFile() *configfile.ConfigFile
|
||||
ServerInfo() ServerInfo
|
||||
ClientInfo() ClientInfo
|
||||
NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (notaryclient.Repository, error)
|
||||
DefaultVersion() string
|
||||
ManifestStore() manifeststore.Store
|
||||
RegistryClient(bool) registryclient.RegistryClient
|
||||
}
|
||||
|
||||
// DockerCli is an instance the docker command line client.
|
||||
// Instances of the client can be returned from NewDockerCli.
|
||||
type DockerCli struct {
|
||||
configFile *configfile.ConfigFile
|
||||
in *InStream
|
||||
out *OutStream
|
||||
err io.Writer
|
||||
client client.APIClient
|
||||
defaultVersion string
|
||||
server ServerInfo
|
||||
configFile *configfile.ConfigFile
|
||||
in *InStream
|
||||
out *OutStream
|
||||
err io.Writer
|
||||
client client.APIClient
|
||||
serverInfo ServerInfo
|
||||
clientInfo ClientInfo
|
||||
}
|
||||
|
||||
// DefaultVersion returns api.defaultVersion or DOCKER_API_VERSION if specified.
|
||||
func (cli *DockerCli) DefaultVersion() string {
|
||||
return cli.defaultVersion
|
||||
return cli.clientInfo.DefaultVersion
|
||||
}
|
||||
|
||||
// Client returns the APIClient
|
||||
@ -104,7 +114,27 @@ func (cli *DockerCli) ConfigFile() *configfile.ConfigFile {
|
||||
// ServerInfo returns the server version details for the host this client is
|
||||
// connected to
|
||||
func (cli *DockerCli) ServerInfo() ServerInfo {
|
||||
return cli.server
|
||||
return cli.serverInfo
|
||||
}
|
||||
|
||||
// ClientInfo returns the client details for the cli
|
||||
func (cli *DockerCli) ClientInfo() ClientInfo {
|
||||
return cli.clientInfo
|
||||
}
|
||||
|
||||
// ManifestStore returns a store for local manifests
|
||||
func (cli *DockerCli) ManifestStore() manifeststore.Store {
|
||||
// TODO: support override default location from config file
|
||||
return manifeststore.NewStore(filepath.Join(config.Dir(), "manifests"))
|
||||
}
|
||||
|
||||
// RegistryClient returns a client for communicating with a Docker distribution
|
||||
// registry
|
||||
func (cli *DockerCli) RegistryClient(allowInsecure bool) registryclient.RegistryClient {
|
||||
resolver := func(ctx context.Context, index *registrytypes.IndexInfo) types.AuthConfig {
|
||||
return ResolveAuthConfig(ctx, cli, index)
|
||||
}
|
||||
return registryclient.NewRegistryClient(resolver, UserAgent(), allowInsecure)
|
||||
}
|
||||
|
||||
// Initialize the dockerCli runs initialization that must happen after command
|
||||
@ -125,17 +155,36 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hasExperimental, err := isEnabled(cli.configFile.Experimental)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Experimental field")
|
||||
}
|
||||
orchestrator := GetOrchestrator(hasExperimental, opts.Common.Orchestrator, cli.configFile.Orchestrator)
|
||||
cli.clientInfo = ClientInfo{
|
||||
DefaultVersion: cli.client.ClientVersion(),
|
||||
HasExperimental: hasExperimental,
|
||||
Orchestrator: orchestrator,
|
||||
}
|
||||
cli.initializeFromClient()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cli *DockerCli) initializeFromClient() {
|
||||
cli.defaultVersion = cli.client.ClientVersion()
|
||||
func isEnabled(value string) (bool, error) {
|
||||
switch value {
|
||||
case "enabled":
|
||||
return true, nil
|
||||
case "", "disabled":
|
||||
return false, nil
|
||||
default:
|
||||
return false, errors.Errorf("%q is not valid, should be either enabled or disabled", value)
|
||||
}
|
||||
}
|
||||
|
||||
func (cli *DockerCli) initializeFromClient() {
|
||||
ping, err := cli.client.Ping(context.Background())
|
||||
if err != nil {
|
||||
// Default to true if we fail to connect to daemon
|
||||
cli.server = ServerInfo{HasExperimental: true}
|
||||
cli.serverInfo = ServerInfo{HasExperimental: true}
|
||||
|
||||
if ping.APIVersion != "" {
|
||||
cli.client.NegotiateAPIVersionPing(ping)
|
||||
@ -143,7 +192,7 @@ func (cli *DockerCli) initializeFromClient() {
|
||||
return
|
||||
}
|
||||
|
||||
cli.server = ServerInfo{
|
||||
cli.serverInfo = ServerInfo{
|
||||
HasExperimental: ping.Experimental,
|
||||
OSType: ping.OSType,
|
||||
}
|
||||
@ -176,6 +225,18 @@ type ServerInfo struct {
|
||||
OSType string
|
||||
}
|
||||
|
||||
// ClientInfo stores details about the supported features of the client
|
||||
type ClientInfo struct {
|
||||
HasExperimental bool
|
||||
DefaultVersion string
|
||||
Orchestrator Orchestrator
|
||||
}
|
||||
|
||||
// HasKubernetes checks if kubernetes orchestrator is enabled
|
||||
func (c ClientInfo) HasKubernetes() bool {
|
||||
return c.HasExperimental && c.Orchestrator == OrchestratorKubernetes
|
||||
}
|
||||
|
||||
// NewDockerCli returns a DockerCli instance with IO output and error streams set by in, out and err.
|
||||
func NewDockerCli(in io.ReadCloser, out, err io.Writer) *DockerCli {
|
||||
return &DockerCli{in: NewInStream(in), out: NewOutStream(out), err: err}
|
||||
|
||||
@ -1,17 +1,18 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"crypto/x509"
|
||||
|
||||
cliconfig "github.com/docker/cli/cli/config"
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
"github.com/docker/cli/cli/flags"
|
||||
"github.com/docker/cli/internal/test/testutil"
|
||||
"github.com/docker/docker/api"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/gotestyourself/gotestyourself/fs"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@ -124,13 +125,155 @@ func TestInitializeFromClient(t *testing.T) {
|
||||
|
||||
cli := &DockerCli{client: apiclient}
|
||||
cli.initializeFromClient()
|
||||
assert.Equal(t, defaultVersion, cli.defaultVersion)
|
||||
assert.Equal(t, testcase.expectedServer, cli.server)
|
||||
assert.Equal(t, testcase.expectedServer, cli.serverInfo)
|
||||
assert.Equal(t, testcase.negotiated, apiclient.negotiated)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExperimentalCLI(t *testing.T) {
|
||||
defaultVersion := "v1.55"
|
||||
|
||||
var testcases = []struct {
|
||||
doc string
|
||||
configfile string
|
||||
expectedExperimentalCLI bool
|
||||
}{
|
||||
{
|
||||
doc: "default",
|
||||
configfile: `{}`,
|
||||
expectedExperimentalCLI: false,
|
||||
},
|
||||
{
|
||||
doc: "experimental",
|
||||
configfile: `{
|
||||
"experimental": "enabled"
|
||||
}`,
|
||||
expectedExperimentalCLI: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testcase := range testcases {
|
||||
t.Run(testcase.doc, func(t *testing.T) {
|
||||
dir := fs.NewDir(t, testcase.doc, fs.WithFile("config.json", testcase.configfile))
|
||||
defer dir.Remove()
|
||||
apiclient := &fakeClient{
|
||||
version: defaultVersion,
|
||||
}
|
||||
|
||||
cli := &DockerCli{client: apiclient, err: os.Stderr}
|
||||
cliconfig.SetDir(dir.Path())
|
||||
err := cli.Initialize(flags.NewClientOptions())
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testcase.expectedExperimentalCLI, cli.ClientInfo().HasExperimental)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrchestratorSwitch(t *testing.T) {
|
||||
defaultVersion := "v0.00"
|
||||
|
||||
var testcases = []struct {
|
||||
doc string
|
||||
configfile string
|
||||
envOrchestrator string
|
||||
flagOrchestrator string
|
||||
expectedOrchestrator string
|
||||
expectedKubernetes bool
|
||||
}{
|
||||
{
|
||||
doc: "default",
|
||||
configfile: `{
|
||||
"experimental": "enabled"
|
||||
}`,
|
||||
expectedOrchestrator: "swarm",
|
||||
expectedKubernetes: false,
|
||||
},
|
||||
{
|
||||
doc: "kubernetesIsExperimental",
|
||||
configfile: `{
|
||||
"experimental": "disabled",
|
||||
"orchestrator": "kubernetes"
|
||||
}`,
|
||||
envOrchestrator: "kubernetes",
|
||||
flagOrchestrator: "kubernetes",
|
||||
expectedOrchestrator: "swarm",
|
||||
expectedKubernetes: false,
|
||||
},
|
||||
{
|
||||
doc: "kubernetesConfigFile",
|
||||
configfile: `{
|
||||
"experimental": "enabled",
|
||||
"orchestrator": "kubernetes"
|
||||
}`,
|
||||
expectedOrchestrator: "kubernetes",
|
||||
expectedKubernetes: true,
|
||||
},
|
||||
{
|
||||
doc: "kubernetesEnv",
|
||||
configfile: `{
|
||||
"experimental": "enabled"
|
||||
}`,
|
||||
envOrchestrator: "kubernetes",
|
||||
expectedOrchestrator: "kubernetes",
|
||||
expectedKubernetes: true,
|
||||
},
|
||||
{
|
||||
doc: "kubernetesFlag",
|
||||
configfile: `{
|
||||
"experimental": "enabled"
|
||||
}`,
|
||||
flagOrchestrator: "kubernetes",
|
||||
expectedOrchestrator: "kubernetes",
|
||||
expectedKubernetes: true,
|
||||
},
|
||||
{
|
||||
doc: "envOverridesConfigFile",
|
||||
configfile: `{
|
||||
"experimental": "enabled",
|
||||
"orchestrator": "kubernetes"
|
||||
}`,
|
||||
envOrchestrator: "swarm",
|
||||
expectedOrchestrator: "swarm",
|
||||
expectedKubernetes: false,
|
||||
},
|
||||
{
|
||||
doc: "flagOverridesEnv",
|
||||
configfile: `{
|
||||
"experimental": "enabled"
|
||||
}`,
|
||||
envOrchestrator: "kubernetes",
|
||||
flagOrchestrator: "swarm",
|
||||
expectedOrchestrator: "swarm",
|
||||
expectedKubernetes: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testcase := range testcases {
|
||||
t.Run(testcase.doc, func(t *testing.T) {
|
||||
dir := fs.NewDir(t, testcase.doc, fs.WithFile("config.json", testcase.configfile))
|
||||
defer dir.Remove()
|
||||
apiclient := &fakeClient{
|
||||
version: defaultVersion,
|
||||
}
|
||||
if testcase.envOrchestrator != "" {
|
||||
defer patchEnvVariable(t, "DOCKER_ORCHESTRATOR", testcase.envOrchestrator)()
|
||||
}
|
||||
|
||||
cli := &DockerCli{client: apiclient, err: os.Stderr}
|
||||
cliconfig.SetDir(dir.Path())
|
||||
options := flags.NewClientOptions()
|
||||
if testcase.flagOrchestrator != "" {
|
||||
options.Common.Orchestrator = testcase.flagOrchestrator
|
||||
}
|
||||
err := cli.Initialize(options)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testcase.expectedKubernetes, cli.ClientInfo().HasKubernetes())
|
||||
assert.Equal(t, testcase.expectedOrchestrator, string(cli.ClientInfo().Orchestrator))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetClientWithPassword(t *testing.T) {
|
||||
expected := "password"
|
||||
|
||||
|
||||
@ -8,6 +8,7 @@ import (
|
||||
"github.com/docker/cli/cli/command/config"
|
||||
"github.com/docker/cli/cli/command/container"
|
||||
"github.com/docker/cli/cli/command/image"
|
||||
"github.com/docker/cli/cli/command/manifest"
|
||||
"github.com/docker/cli/cli/command/network"
|
||||
"github.com/docker/cli/cli/command/node"
|
||||
"github.com/docker/cli/cli/command/plugin"
|
||||
@ -39,12 +40,15 @@ func AddCommands(cmd *cobra.Command, dockerCli *command.DockerCli) {
|
||||
image.NewImageCommand(dockerCli),
|
||||
image.NewBuildCommand(dockerCli),
|
||||
|
||||
// node
|
||||
node.NewNodeCommand(dockerCli),
|
||||
// manifest
|
||||
manifest.NewManifestCommand(dockerCli),
|
||||
|
||||
// network
|
||||
network.NewNetworkCommand(dockerCli),
|
||||
|
||||
// node
|
||||
node.NewNodeCommand(dockerCli),
|
||||
|
||||
// plugin
|
||||
plugin.NewPluginCommand(dockerCli),
|
||||
|
||||
|
||||
@ -10,11 +10,14 @@ import (
|
||||
// NewConfigCommand returns a cobra command for `config` subcommands
|
||||
func NewConfigCommand(dockerCli command.Cli) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "config",
|
||||
Short: "Manage Docker configs",
|
||||
Args: cli.NoArgs,
|
||||
RunE: command.ShowHelp(dockerCli.Err()),
|
||||
Annotations: map[string]string{"version": "1.30"},
|
||||
Use: "config",
|
||||
Short: "Manage Docker configs",
|
||||
Args: cli.NoArgs,
|
||||
RunE: command.ShowHelp(dockerCli.Err()),
|
||||
Annotations: map[string]string{
|
||||
"version": "1.30",
|
||||
"swarm": "",
|
||||
},
|
||||
}
|
||||
cmd.AddCommand(
|
||||
newConfigListCommand(dockerCli),
|
||||
|
||||
@ -28,7 +28,7 @@ func newConfigCreateCommand(dockerCli command.Cli) *cobra.Command {
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "create [OPTIONS] CONFIG file|-",
|
||||
Short: "Create a configuration file from a file or STDIN as content",
|
||||
Short: "Create a config from a file or STDIN",
|
||||
Args: cli.ExactArgs(2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
createOpts.name = args[0]
|
||||
|
||||
@ -21,7 +21,7 @@ func newConfigInspectCommand(dockerCli command.Cli) *cobra.Command {
|
||||
opts := inspectOptions{}
|
||||
cmd := &cobra.Command{
|
||||
Use: "inspect [OPTIONS] CONFIG [CONFIG...]",
|
||||
Short: "Display detailed information on one or more configuration files",
|
||||
Short: "Display detailed information on one or more configs",
|
||||
Args: cli.RequiresMinArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.names = args
|
||||
|
||||
@ -19,7 +19,7 @@ func newConfigRemoveCommand(dockerCli command.Cli) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "rm CONFIG [CONFIG...]",
|
||||
Aliases: []string{"remove"},
|
||||
Short: "Remove one or more configuration files",
|
||||
Short: "Remove one or more configs",
|
||||
Args: cli.RequiresMinArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts := removeOptions{
|
||||
|
||||
@ -21,6 +21,8 @@ type fakeClient struct {
|
||||
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)
|
||||
waitFunc func(string) (<-chan container.ContainerWaitOKBody, <-chan error)
|
||||
Version string
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerInspect(_ context.Context, containerID string) (types.ContainerJSON, error) {
|
||||
@ -95,3 +97,14 @@ func (f *fakeClient) ContainerLogs(_ context.Context, container string, options
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ClientVersion() string {
|
||||
return f.Version
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerWait(_ context.Context, container string, _ container.WaitCondition) (<-chan container.ContainerWaitOKBody, <-chan error) {
|
||||
if f.waitFunc != nil {
|
||||
return f.waitFunc(container)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@ -37,7 +37,12 @@ func waitExitOrRemoved(ctx context.Context, dockerCli command.Cli, containerID s
|
||||
go func() {
|
||||
select {
|
||||
case result := <-resultC:
|
||||
statusC <- int(result.StatusCode)
|
||||
if result.Error != nil {
|
||||
logrus.Errorf("Error waiting for container: %v", result.Error.Message)
|
||||
statusC <- 125
|
||||
} else {
|
||||
statusC <- int(result.StatusCode)
|
||||
}
|
||||
case err := <-errC:
|
||||
logrus.Errorf("error waiting for container: %v", err)
|
||||
statusC <- 125
|
||||
|
||||
69
components/cli/cli/command/container/utils_test.go
Normal file
69
components/cli/cli/command/container/utils_test.go
Normal file
@ -0,0 +1,69 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/docker/api"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func waitFn(cid string) (<-chan container.ContainerWaitOKBody, <-chan error) {
|
||||
resC := make(chan container.ContainerWaitOKBody)
|
||||
errC := make(chan error, 1)
|
||||
var res container.ContainerWaitOKBody
|
||||
|
||||
go func() {
|
||||
switch {
|
||||
case strings.Contains(cid, "exit-code-42"):
|
||||
res.StatusCode = 42
|
||||
resC <- res
|
||||
case strings.Contains(cid, "non-existent"):
|
||||
err := errors.Errorf("No such container: %v", cid)
|
||||
errC <- err
|
||||
case strings.Contains(cid, "wait-error"):
|
||||
res.Error = &container.ContainerWaitOKBodyError{Message: "removal failed"}
|
||||
resC <- res
|
||||
default:
|
||||
// normal exit
|
||||
resC <- res
|
||||
}
|
||||
}()
|
||||
|
||||
return resC, errC
|
||||
}
|
||||
|
||||
func TestWaitExitOrRemoved(t *testing.T) {
|
||||
testcases := []struct {
|
||||
cid string
|
||||
exitCode int
|
||||
}{
|
||||
{
|
||||
cid: "normal-container",
|
||||
exitCode: 0,
|
||||
},
|
||||
{
|
||||
cid: "give-me-exit-code-42",
|
||||
exitCode: 42,
|
||||
},
|
||||
{
|
||||
cid: "i-want-a-wait-error",
|
||||
exitCode: 125,
|
||||
},
|
||||
{
|
||||
cid: "non-existent-container-id",
|
||||
exitCode: 125,
|
||||
},
|
||||
}
|
||||
|
||||
client := test.NewFakeCli(&fakeClient{waitFunc: waitFn, Version: api.DefaultVersion})
|
||||
for _, testcase := range testcases {
|
||||
statusC := waitExitOrRemoved(context.Background(), client, testcase.cid, true)
|
||||
exitCode := <-statusC
|
||||
assert.Equal(t, testcase.exitCode, exitCode)
|
||||
}
|
||||
}
|
||||
@ -2,6 +2,7 @@ package formatter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@ -520,19 +521,95 @@ func (c *serviceContext) Image() string {
|
||||
return image
|
||||
}
|
||||
|
||||
type portRange struct {
|
||||
pStart uint32
|
||||
pEnd uint32
|
||||
tStart uint32
|
||||
tEnd uint32
|
||||
protocol swarm.PortConfigProtocol
|
||||
}
|
||||
|
||||
func (pr portRange) String() string {
|
||||
var (
|
||||
pub string
|
||||
tgt string
|
||||
)
|
||||
|
||||
if pr.pEnd > pr.pStart {
|
||||
pub = fmt.Sprintf("%d-%d", pr.pStart, pr.pEnd)
|
||||
} else {
|
||||
pub = fmt.Sprintf("%d", pr.pStart)
|
||||
}
|
||||
if pr.tEnd > pr.tStart {
|
||||
tgt = fmt.Sprintf("%d-%d", pr.tStart, pr.tEnd)
|
||||
} else {
|
||||
tgt = fmt.Sprintf("%d", pr.tStart)
|
||||
}
|
||||
return fmt.Sprintf("*:%s->%s/%s", pub, tgt, pr.protocol)
|
||||
}
|
||||
|
||||
// Ports formats published ports on the ingress network for output.
|
||||
//
|
||||
// Where possible, ranges are grouped to produce a compact output:
|
||||
// - multiple ports mapped to a single port (80->80, 81->80); is formatted as *:80-81->80
|
||||
// - multiple consecutive ports on both sides; (80->80, 81->81) are formatted as: *:80-81->80-81
|
||||
//
|
||||
// The above should not be grouped together, i.e.:
|
||||
// - 80->80, 81->81, 82->80 should be presented as : *:80-81->80-81, *:82->80
|
||||
//
|
||||
// TODO improve:
|
||||
// - combine non-consecutive ports mapped to a single port (80->80, 81->80, 84->80, 86->80, 87->80); to be printed as *:80-81,84,86-87->80
|
||||
// - combine tcp and udp mappings if their port-mapping is exactly the same (*:80-81->80-81/tcp+udp instead of *:80-81->80-81/tcp, *:80-81->80-81/udp)
|
||||
func (c *serviceContext) Ports() string {
|
||||
if c.service.Endpoint.Ports == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
pr := portRange{}
|
||||
ports := []string{}
|
||||
for _, pConfig := range c.service.Endpoint.Ports {
|
||||
if pConfig.PublishMode == swarm.PortConfigPublishModeIngress {
|
||||
ports = append(ports, fmt.Sprintf("*:%d->%d/%s",
|
||||
pConfig.PublishedPort,
|
||||
pConfig.TargetPort,
|
||||
pConfig.Protocol,
|
||||
))
|
||||
|
||||
sort.Sort(byProtocolAndPublishedPort(c.service.Endpoint.Ports))
|
||||
|
||||
for _, p := range c.service.Endpoint.Ports {
|
||||
if p.PublishMode == swarm.PortConfigPublishModeIngress {
|
||||
prIsRange := pr.tEnd != pr.tStart
|
||||
tOverlaps := p.TargetPort <= pr.tEnd
|
||||
|
||||
// Start a new port-range if:
|
||||
// - the protocol is different from the current port-range
|
||||
// - published or target port are not consecutive to the current port-range
|
||||
// - the current port-range is a _range_, and the target port overlaps with the current range's target-ports
|
||||
if p.Protocol != pr.protocol || p.PublishedPort-pr.pEnd > 1 || p.TargetPort-pr.tEnd > 1 || prIsRange && tOverlaps {
|
||||
// start a new port-range, and print the previous port-range (if any)
|
||||
if pr.pStart > 0 {
|
||||
ports = append(ports, pr.String())
|
||||
}
|
||||
pr = portRange{
|
||||
pStart: p.PublishedPort,
|
||||
pEnd: p.PublishedPort,
|
||||
tStart: p.TargetPort,
|
||||
tEnd: p.TargetPort,
|
||||
protocol: p.Protocol,
|
||||
}
|
||||
continue
|
||||
}
|
||||
pr.pEnd = p.PublishedPort
|
||||
pr.tEnd = p.TargetPort
|
||||
}
|
||||
}
|
||||
return strings.Join(ports, ",")
|
||||
if pr.pStart > 0 {
|
||||
ports = append(ports, pr.String())
|
||||
}
|
||||
return strings.Join(ports, ", ")
|
||||
}
|
||||
|
||||
type byProtocolAndPublishedPort []swarm.PortConfig
|
||||
|
||||
func (a byProtocolAndPublishedPort) Len() int { return len(a) }
|
||||
func (a byProtocolAndPublishedPort) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a byProtocolAndPublishedPort) Less(i, j int) bool {
|
||||
if a[i].Protocol == a[j].Protocol {
|
||||
return a[i].PublishedPort < a[j].PublishedPort
|
||||
}
|
||||
return a[i].Protocol < a[j].Protocol
|
||||
}
|
||||
|
||||
@ -224,3 +224,124 @@ func TestServiceContextWriteJSONField(t *testing.T) {
|
||||
assert.Equal(t, services[i].Spec.Name, s, msg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceContext_Ports(t *testing.T) {
|
||||
c := serviceContext{
|
||||
service: swarm.Service{
|
||||
Endpoint: swarm.Endpoint{
|
||||
Ports: []swarm.PortConfig{
|
||||
{
|
||||
Protocol: "tcp",
|
||||
TargetPort: 80,
|
||||
PublishedPort: 81,
|
||||
PublishMode: "ingress",
|
||||
},
|
||||
{
|
||||
Protocol: "tcp",
|
||||
TargetPort: 80,
|
||||
PublishedPort: 80,
|
||||
PublishMode: "ingress",
|
||||
},
|
||||
{
|
||||
Protocol: "tcp",
|
||||
TargetPort: 95,
|
||||
PublishedPort: 95,
|
||||
PublishMode: "ingress",
|
||||
},
|
||||
{
|
||||
Protocol: "tcp",
|
||||
TargetPort: 90,
|
||||
PublishedPort: 90,
|
||||
PublishMode: "ingress",
|
||||
},
|
||||
{
|
||||
Protocol: "tcp",
|
||||
TargetPort: 91,
|
||||
PublishedPort: 91,
|
||||
PublishMode: "ingress",
|
||||
},
|
||||
{
|
||||
Protocol: "tcp",
|
||||
TargetPort: 92,
|
||||
PublishedPort: 92,
|
||||
PublishMode: "ingress",
|
||||
},
|
||||
{
|
||||
Protocol: "tcp",
|
||||
TargetPort: 93,
|
||||
PublishedPort: 93,
|
||||
PublishMode: "ingress",
|
||||
},
|
||||
{
|
||||
Protocol: "tcp",
|
||||
TargetPort: 94,
|
||||
PublishedPort: 94,
|
||||
PublishMode: "ingress",
|
||||
},
|
||||
{
|
||||
Protocol: "udp",
|
||||
TargetPort: 95,
|
||||
PublishedPort: 95,
|
||||
PublishMode: "ingress",
|
||||
},
|
||||
{
|
||||
Protocol: "udp",
|
||||
TargetPort: 90,
|
||||
PublishedPort: 90,
|
||||
PublishMode: "ingress",
|
||||
},
|
||||
{
|
||||
Protocol: "udp",
|
||||
TargetPort: 96,
|
||||
PublishedPort: 96,
|
||||
PublishMode: "ingress",
|
||||
},
|
||||
{
|
||||
Protocol: "udp",
|
||||
TargetPort: 91,
|
||||
PublishedPort: 91,
|
||||
PublishMode: "ingress",
|
||||
},
|
||||
{
|
||||
Protocol: "udp",
|
||||
TargetPort: 92,
|
||||
PublishedPort: 92,
|
||||
PublishMode: "ingress",
|
||||
},
|
||||
{
|
||||
Protocol: "udp",
|
||||
TargetPort: 93,
|
||||
PublishedPort: 93,
|
||||
PublishMode: "ingress",
|
||||
},
|
||||
{
|
||||
Protocol: "udp",
|
||||
TargetPort: 94,
|
||||
PublishedPort: 94,
|
||||
PublishMode: "ingress",
|
||||
},
|
||||
{
|
||||
Protocol: "tcp",
|
||||
TargetPort: 60,
|
||||
PublishedPort: 60,
|
||||
PublishMode: "ingress",
|
||||
},
|
||||
{
|
||||
Protocol: "tcp",
|
||||
TargetPort: 61,
|
||||
PublishedPort: 61,
|
||||
PublishMode: "ingress",
|
||||
},
|
||||
{
|
||||
Protocol: "tcp",
|
||||
TargetPort: 61,
|
||||
PublishedPort: 62,
|
||||
PublishMode: "ingress",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, "*:60-61->60-61/tcp, *:62->61/tcp, *:80-81->80/tcp, *:90-95->90-95/tcp, *:90-96->90-96/udp", c.Ports())
|
||||
}
|
||||
|
||||
@ -81,7 +81,7 @@ func runRemove(dockerCli command.Cli, opts removeOptions, images []string) error
|
||||
if !opts.force || fatalErr {
|
||||
return errors.New(msg)
|
||||
}
|
||||
fmt.Fprintf(dockerCli.Err(), msg)
|
||||
fmt.Fprintln(dockerCli.Err(), msg)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -98,7 +98,7 @@ func TestNewRemoveCommandSuccess(t *testing.T) {
|
||||
assert.Equal(t, true, options.Force)
|
||||
return []types.ImageDeleteResponseItem{}, notFound{"image1"}
|
||||
},
|
||||
expectedStderr: "Error: No such image: image1",
|
||||
expectedStderr: "Error: No such image: image1\n",
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
93
components/cli/cli/command/manifest/annotate.go
Normal file
93
components/cli/cli/command/manifest/annotate.go
Normal file
@ -0,0 +1,93 @@
|
||||
package manifest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/manifest/store"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type annotateOptions struct {
|
||||
target string // the target manifest list name (also transaction ID)
|
||||
image string // the manifest to annotate within the list
|
||||
variant string // an architecture variant
|
||||
os string
|
||||
arch string
|
||||
osFeatures []string
|
||||
}
|
||||
|
||||
// NewAnnotateCommand creates a new `docker manifest annotate` command
|
||||
func newAnnotateCommand(dockerCli command.Cli) *cobra.Command {
|
||||
var opts annotateOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "annotate [OPTIONS] MANIFEST_LIST MANIFEST",
|
||||
Short: "Add additional information to a local image manifest",
|
||||
Args: cli.ExactArgs(2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.target = args[0]
|
||||
opts.image = args[1]
|
||||
return runManifestAnnotate(dockerCli, opts)
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
|
||||
flags.StringVar(&opts.os, "os", "", "Set operating system")
|
||||
flags.StringVar(&opts.arch, "arch", "", "Set architecture")
|
||||
flags.StringSliceVar(&opts.osFeatures, "os-features", []string{}, "Set operating system feature")
|
||||
flags.StringVar(&opts.variant, "variant", "", "Set architecture variant")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runManifestAnnotate(dockerCli command.Cli, opts annotateOptions) error {
|
||||
targetRef, err := normalizeReference(opts.target)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "annotate: error parsing name for manifest list %s", opts.target)
|
||||
}
|
||||
imgRef, err := normalizeReference(opts.image)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "annotate: error parsing name for manifest %s", opts.image)
|
||||
}
|
||||
|
||||
manifestStore := dockerCli.ManifestStore()
|
||||
imageManifest, err := manifestStore.Get(targetRef, imgRef)
|
||||
switch {
|
||||
case store.IsNotFound(err):
|
||||
return fmt.Errorf("manifest for image %s does not exist in %s", opts.image, opts.target)
|
||||
case err != nil:
|
||||
return err
|
||||
}
|
||||
|
||||
// Update the mf
|
||||
if opts.os != "" {
|
||||
imageManifest.Platform.OS = opts.os
|
||||
}
|
||||
if opts.arch != "" {
|
||||
imageManifest.Platform.Architecture = opts.arch
|
||||
}
|
||||
for _, osFeature := range opts.osFeatures {
|
||||
imageManifest.Platform.OSFeatures = appendIfUnique(imageManifest.Platform.OSFeatures, osFeature)
|
||||
}
|
||||
if opts.variant != "" {
|
||||
imageManifest.Platform.Variant = opts.variant
|
||||
}
|
||||
|
||||
if !isValidOSArch(imageManifest.Platform.OS, imageManifest.Platform.Architecture) {
|
||||
return errors.Errorf("manifest entry for image has unsupported os/arch combination: %s/%s", opts.os, opts.arch)
|
||||
}
|
||||
return manifestStore.Save(targetRef, imgRef, imageManifest)
|
||||
}
|
||||
|
||||
func appendIfUnique(list []string, str string) []string {
|
||||
for _, s := range list {
|
||||
if s == str {
|
||||
return list
|
||||
}
|
||||
}
|
||||
return append(list, str)
|
||||
}
|
||||
78
components/cli/cli/command/manifest/annotate_test.go
Normal file
78
components/cli/cli/command/manifest/annotate_test.go
Normal file
@ -0,0 +1,78 @@
|
||||
package manifest
|
||||
|
||||
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"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestManifestAnnotateError(t *testing.T) {
|
||||
testCases := []struct {
|
||||
args []string
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
args: []string{"too-few-arguments"},
|
||||
expectedError: "requires exactly 2 arguments",
|
||||
},
|
||||
{
|
||||
args: []string{"th!si'sa/fa!ke/li$t/name", "example.com/alpine:3.0"},
|
||||
expectedError: "error parsing name for manifest list",
|
||||
},
|
||||
{
|
||||
args: []string{"example.com/list:v1", "th!si'sa/fa!ke/im@ge/nam32"},
|
||||
expectedError: "error parsing name for manifest",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
cli := test.NewFakeCli(nil)
|
||||
cmd := newAnnotateCommand(cli)
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
func TestManifestAnnotate(t *testing.T) {
|
||||
store, cleanup := newTempManifestStore(t)
|
||||
defer cleanup()
|
||||
|
||||
cli := test.NewFakeCli(nil)
|
||||
cli.SetManifestStore(store)
|
||||
namedRef := ref(t, "alpine:3.0")
|
||||
imageManifest := fullImageManifest(t, namedRef)
|
||||
err := store.Save(ref(t, "list:v1"), namedRef, imageManifest)
|
||||
require.NoError(t, err)
|
||||
|
||||
cmd := newAnnotateCommand(cli)
|
||||
cmd.SetArgs([]string{"example.com/list:v1", "example.com/fake:0.0"})
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
expectedError := "manifest for image example.com/fake:0.0 does not exist"
|
||||
testutil.ErrorContains(t, cmd.Execute(), expectedError)
|
||||
|
||||
cmd.SetArgs([]string{"example.com/list:v1", "example.com/alpine:3.0"})
|
||||
cmd.Flags().Set("os", "freebsd")
|
||||
cmd.Flags().Set("arch", "fake")
|
||||
cmd.Flags().Set("os-features", "feature1")
|
||||
cmd.Flags().Set("variant", "v7")
|
||||
expectedError = "manifest entry for image has unsupported os/arch combination"
|
||||
testutil.ErrorContains(t, cmd.Execute(), expectedError)
|
||||
|
||||
cmd.Flags().Set("arch", "arm")
|
||||
require.NoError(t, cmd.Execute())
|
||||
|
||||
cmd = newInspectCommand(cli)
|
||||
err = cmd.Flags().Set("verbose", "true")
|
||||
require.NoError(t, err)
|
||||
cmd.SetArgs([]string{"example.com/list:v1", "example.com/alpine:3.0"})
|
||||
require.NoError(t, cmd.Execute())
|
||||
actual := cli.OutBuffer()
|
||||
expected := golden.Get(t, "inspect-annotate.golden")
|
||||
assert.Equal(t, string(expected), actual.String())
|
||||
}
|
||||
46
components/cli/cli/command/manifest/client_test.go
Normal file
46
components/cli/cli/command/manifest/client_test.go
Normal file
@ -0,0 +1,46 @@
|
||||
package manifest
|
||||
|
||||
import (
|
||||
manifesttypes "github.com/docker/cli/cli/manifest/types"
|
||||
"github.com/docker/cli/cli/registry/client"
|
||||
"github.com/docker/distribution"
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/opencontainers/go-digest"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type fakeRegistryClient struct {
|
||||
client.RegistryClient
|
||||
getManifestFunc func(ctx context.Context, ref reference.Named) (manifesttypes.ImageManifest, error)
|
||||
getManifestListFunc func(ctx context.Context, ref reference.Named) ([]manifesttypes.ImageManifest, error)
|
||||
mountBlobFunc func(ctx context.Context, source reference.Canonical, target reference.Named) error
|
||||
putManifestFunc func(ctx context.Context, source reference.Named, mf distribution.Manifest) (digest.Digest, error)
|
||||
}
|
||||
|
||||
func (c *fakeRegistryClient) GetManifest(ctx context.Context, ref reference.Named) (manifesttypes.ImageManifest, error) {
|
||||
if c.getManifestFunc != nil {
|
||||
return c.getManifestFunc(ctx, ref)
|
||||
}
|
||||
return manifesttypes.ImageManifest{}, nil
|
||||
}
|
||||
|
||||
func (c *fakeRegistryClient) GetManifestList(ctx context.Context, ref reference.Named) ([]manifesttypes.ImageManifest, error) {
|
||||
if c.getManifestListFunc != nil {
|
||||
return c.getManifestListFunc(ctx, ref)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (c *fakeRegistryClient) MountBlob(ctx context.Context, source reference.Canonical, target reference.Named) error {
|
||||
if c.mountBlobFunc != nil {
|
||||
return c.mountBlobFunc(ctx, source, target)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *fakeRegistryClient) PutManifest(ctx context.Context, ref reference.Named, mf distribution.Manifest) (digest.Digest, error) {
|
||||
if c.putManifestFunc != nil {
|
||||
return c.putManifestFunc(ctx, ref, mf)
|
||||
}
|
||||
return digest.Digest(""), nil
|
||||
}
|
||||
45
components/cli/cli/command/manifest/cmd.go
Normal file
45
components/cli/cli/command/manifest/cmd.go
Normal file
@ -0,0 +1,45 @@
|
||||
package manifest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// NewManifestCommand returns a cobra command for `manifest` subcommands
|
||||
func NewManifestCommand(dockerCli command.Cli) *cobra.Command {
|
||||
// use dockerCli as command.Cli
|
||||
cmd := &cobra.Command{
|
||||
Use: "manifest COMMAND",
|
||||
Short: "Manage Docker image manifests and manifest lists",
|
||||
Long: manifestDescription,
|
||||
Args: cli.NoArgs,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString())
|
||||
},
|
||||
Annotations: map[string]string{"experimentalCLI": ""},
|
||||
}
|
||||
cmd.AddCommand(
|
||||
newCreateListCommand(dockerCli),
|
||||
newInspectCommand(dockerCli),
|
||||
newAnnotateCommand(dockerCli),
|
||||
newPushListCommand(dockerCli),
|
||||
)
|
||||
return cmd
|
||||
}
|
||||
|
||||
var manifestDescription = `
|
||||
The **docker manifest** command has subcommands for managing image manifests and
|
||||
manifest lists. A manifest list allows you to use one name to refer to the same image
|
||||
built for multiple architectures.
|
||||
|
||||
To see help for a subcommand, use:
|
||||
|
||||
docker manifest CMD --help
|
||||
|
||||
For full details on using docker manifest lists, see the registry v2 specification.
|
||||
|
||||
`
|
||||
82
components/cli/cli/command/manifest/create_list.go
Normal file
82
components/cli/cli/command/manifest/create_list.go
Normal file
@ -0,0 +1,82 @@
|
||||
package manifest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/manifest/store"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type createOpts struct {
|
||||
amend bool
|
||||
insecure bool
|
||||
}
|
||||
|
||||
func newCreateListCommand(dockerCli command.Cli) *cobra.Command {
|
||||
opts := createOpts{}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "create MANFEST_LIST MANIFEST [MANIFEST...]",
|
||||
Short: "Create a local manifest list for annotating and pushing to a registry",
|
||||
Args: cli.RequiresMinArgs(2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return createManifestList(dockerCli, args, opts)
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVar(&opts.insecure, "insecure", false, "allow communication with an insecure registry")
|
||||
flags.BoolVarP(&opts.amend, "amend", "a", false, "Amend an existing manifest list")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func createManifestList(dockerCli command.Cli, args []string, opts createOpts) error {
|
||||
newRef := args[0]
|
||||
targetRef, err := normalizeReference(newRef)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error parsing name for manifest list %s", newRef)
|
||||
}
|
||||
|
||||
_, err = registry.ParseRepositoryInfo(targetRef)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error parsing repository name for manifest list %s", newRef)
|
||||
}
|
||||
|
||||
manifestStore := dockerCli.ManifestStore()
|
||||
_, err = manifestStore.GetList(targetRef)
|
||||
switch {
|
||||
case store.IsNotFound(err):
|
||||
// New manifest list
|
||||
case err != nil:
|
||||
return err
|
||||
case !opts.amend:
|
||||
return errors.Errorf("refusing to amend an existing manifest list with no --amend flag")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
// Now create the local manifest list transaction by looking up the manifest schemas
|
||||
// for the constituent images:
|
||||
manifests := args[1:]
|
||||
for _, manifestRef := range manifests {
|
||||
namedRef, err := normalizeReference(manifestRef)
|
||||
if err != nil {
|
||||
// TODO: wrap error?
|
||||
return err
|
||||
}
|
||||
|
||||
manifest, err := getManifest(ctx, dockerCli, targetRef, namedRef, opts.insecure)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := manifestStore.Save(targetRef, namedRef, manifest); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(dockerCli.Out(), "Created manifest list %s\n", targetRef.String())
|
||||
return nil
|
||||
}
|
||||
117
components/cli/cli/command/manifest/create_test.go
Normal file
117
components/cli/cli/command/manifest/create_test.go
Normal file
@ -0,0 +1,117 @@
|
||||
package manifest
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
manifesttypes "github.com/docker/cli/cli/manifest/types"
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/cli/internal/test/testutil"
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/gotestyourself/gotestyourself/golden"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func TestManifestCreateErrors(t *testing.T) {
|
||||
testCases := []struct {
|
||||
args []string
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
args: []string{"too-few-arguments"},
|
||||
expectedError: "requires at least 2 arguments",
|
||||
},
|
||||
{
|
||||
args: []string{"th!si'sa/fa!ke/li$t/name", "example.com/alpine:3.0"},
|
||||
expectedError: "error parsing name for manifest list",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
cli := test.NewFakeCli(nil)
|
||||
cmd := newCreateListCommand(cli)
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
// create a manifest list, then overwrite it, and inspect to see if the old one is still there
|
||||
func TestManifestCreateAmend(t *testing.T) {
|
||||
store, cleanup := newTempManifestStore(t)
|
||||
defer cleanup()
|
||||
|
||||
cli := test.NewFakeCli(nil)
|
||||
cli.SetManifestStore(store)
|
||||
|
||||
namedRef := ref(t, "alpine:3.0")
|
||||
imageManifest := fullImageManifest(t, namedRef)
|
||||
err := store.Save(ref(t, "list:v1"), namedRef, imageManifest)
|
||||
require.NoError(t, err)
|
||||
namedRef = ref(t, "alpine:3.1")
|
||||
imageManifest = fullImageManifest(t, namedRef)
|
||||
err = store.Save(ref(t, "list:v1"), namedRef, imageManifest)
|
||||
require.NoError(t, err)
|
||||
|
||||
cmd := newCreateListCommand(cli)
|
||||
cmd.SetArgs([]string{"example.com/list:v1", "example.com/alpine:3.1"})
|
||||
cmd.Flags().Set("amend", "true")
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
err = cmd.Execute()
|
||||
require.NoError(t, err)
|
||||
|
||||
// make a new cli to clear the buffers
|
||||
cli = test.NewFakeCli(nil)
|
||||
cli.SetManifestStore(store)
|
||||
inspectCmd := newInspectCommand(cli)
|
||||
inspectCmd.SetArgs([]string{"example.com/list:v1"})
|
||||
require.NoError(t, inspectCmd.Execute())
|
||||
actual := cli.OutBuffer()
|
||||
expected := golden.Get(t, "inspect-manifest-list.golden")
|
||||
assert.Equal(t, string(expected), actual.String())
|
||||
}
|
||||
|
||||
// attempt to overwrite a saved manifest and get refused
|
||||
func TestManifestCreateRefuseAmend(t *testing.T) {
|
||||
store, cleanup := newTempManifestStore(t)
|
||||
defer cleanup()
|
||||
|
||||
cli := test.NewFakeCli(nil)
|
||||
cli.SetManifestStore(store)
|
||||
namedRef := ref(t, "alpine:3.0")
|
||||
imageManifest := fullImageManifest(t, namedRef)
|
||||
err := store.Save(ref(t, "list:v1"), namedRef, imageManifest)
|
||||
require.NoError(t, err)
|
||||
|
||||
cmd := newCreateListCommand(cli)
|
||||
cmd.SetArgs([]string{"example.com/list:v1", "example.com/alpine:3.0"})
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
err = cmd.Execute()
|
||||
assert.EqualError(t, err, "refusing to amend an existing manifest list with no --amend flag")
|
||||
}
|
||||
|
||||
// attempt to make a manifest list without valid images
|
||||
func TestManifestCreateNoManifest(t *testing.T) {
|
||||
store, cleanup := newTempManifestStore(t)
|
||||
defer cleanup()
|
||||
|
||||
cli := test.NewFakeCli(nil)
|
||||
cli.SetManifestStore(store)
|
||||
cli.SetRegistryClient(&fakeRegistryClient{
|
||||
getManifestFunc: func(_ context.Context, ref reference.Named) (manifesttypes.ImageManifest, error) {
|
||||
return manifesttypes.ImageManifest{}, errors.Errorf("No such image: %v", ref)
|
||||
},
|
||||
getManifestListFunc: func(ctx context.Context, ref reference.Named) ([]manifesttypes.ImageManifest, error) {
|
||||
return nil, errors.Errorf("No such manifest: %s", ref)
|
||||
},
|
||||
})
|
||||
|
||||
cmd := newCreateListCommand(cli)
|
||||
cmd.SetArgs([]string{"example.com/list:v1", "example.com/alpine:3.0"})
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
err := cmd.Execute()
|
||||
assert.EqualError(t, err, "No such image: example.com/alpine:3.0")
|
||||
}
|
||||
147
components/cli/cli/command/manifest/inspect.go
Normal file
147
components/cli/cli/command/manifest/inspect.go
Normal file
@ -0,0 +1,147 @@
|
||||
package manifest
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/manifest/types"
|
||||
"github.com/docker/distribution/manifest/manifestlist"
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type inspectOptions struct {
|
||||
ref string
|
||||
list string
|
||||
verbose bool
|
||||
insecure bool
|
||||
}
|
||||
|
||||
// NewInspectCommand creates a new `docker manifest inspect` command
|
||||
func newInspectCommand(dockerCli command.Cli) *cobra.Command {
|
||||
var opts inspectOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "inspect [OPTIONS] [MANIFEST_LIST] MANIFEST",
|
||||
Short: "Display an image manifest, or manifest list",
|
||||
Args: cli.RequiresRangeArgs(1, 2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
switch len(args) {
|
||||
case 1:
|
||||
opts.ref = args[0]
|
||||
case 2:
|
||||
opts.list = args[0]
|
||||
opts.ref = args[1]
|
||||
}
|
||||
return runInspect(dockerCli, opts)
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVar(&opts.insecure, "insecure", false, "allow communication with an insecure registry")
|
||||
flags.BoolVarP(&opts.verbose, "verbose", "v", false, "Output additional info including layers and platform")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runInspect(dockerCli command.Cli, opts inspectOptions) error {
|
||||
namedRef, err := normalizeReference(opts.ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If list reference is provided, display the local manifest in a list
|
||||
if opts.list != "" {
|
||||
listRef, err := normalizeReference(opts.list)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
imageManifest, err := dockerCli.ManifestStore().Get(listRef, namedRef)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return printManifest(dockerCli, imageManifest, opts)
|
||||
}
|
||||
|
||||
// Try a local manifest list first
|
||||
localManifestList, err := dockerCli.ManifestStore().GetList(namedRef)
|
||||
if err == nil {
|
||||
return printManifestList(dockerCli, namedRef, localManifestList, opts)
|
||||
}
|
||||
|
||||
// Next try a remote manifest
|
||||
ctx := context.Background()
|
||||
registryClient := dockerCli.RegistryClient(opts.insecure)
|
||||
imageManifest, err := registryClient.GetManifest(ctx, namedRef)
|
||||
if err == nil {
|
||||
return printManifest(dockerCli, imageManifest, opts)
|
||||
}
|
||||
|
||||
// Finally try a remote manifest list
|
||||
manifestList, err := registryClient.GetManifestList(ctx, namedRef)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return printManifestList(dockerCli, namedRef, manifestList, opts)
|
||||
}
|
||||
|
||||
func printManifest(dockerCli command.Cli, manifest types.ImageManifest, opts inspectOptions) error {
|
||||
buffer := new(bytes.Buffer)
|
||||
if !opts.verbose {
|
||||
_, raw, err := manifest.Payload()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := json.Indent(buffer, raw, "", "\t"); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintln(dockerCli.Out(), buffer.String())
|
||||
return nil
|
||||
}
|
||||
jsonBytes, err := json.MarshalIndent(manifest, "", "\t")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dockerCli.Out().Write(append(jsonBytes, '\n'))
|
||||
return nil
|
||||
}
|
||||
|
||||
func printManifestList(dockerCli command.Cli, namedRef reference.Named, list []types.ImageManifest, opts inspectOptions) error {
|
||||
if !opts.verbose {
|
||||
targetRepo, err := registry.ParseRepositoryInfo(namedRef)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
manifests := []manifestlist.ManifestDescriptor{}
|
||||
// More than one response. This is a manifest list.
|
||||
for _, img := range list {
|
||||
mfd, err := buildManifestDescriptor(targetRepo, img)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error assembling ManifestDescriptor")
|
||||
}
|
||||
manifests = append(manifests, mfd)
|
||||
}
|
||||
deserializedML, err := manifestlist.FromDescriptors(manifests)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
jsonBytes, err := deserializedML.MarshalJSON()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintln(dockerCli.Out(), string(jsonBytes))
|
||||
return nil
|
||||
}
|
||||
jsonBytes, err := json.MarshalIndent(list, "", "\t")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dockerCli.Out().Write(append(jsonBytes, '\n'))
|
||||
return nil
|
||||
}
|
||||
131
components/cli/cli/command/manifest/inspect_test.go
Normal file
131
components/cli/cli/command/manifest/inspect_test.go
Normal file
@ -0,0 +1,131 @@
|
||||
package manifest
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/manifest/store"
|
||||
"github.com/docker/cli/cli/manifest/types"
|
||||
manifesttypes "github.com/docker/cli/cli/manifest/types"
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/distribution"
|
||||
"github.com/docker/distribution/manifest/schema2"
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/gotestyourself/gotestyourself/golden"
|
||||
"github.com/opencontainers/go-digest"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func newTempManifestStore(t *testing.T) (store.Store, func()) {
|
||||
tmpdir, err := ioutil.TempDir("", "test-manifest-storage")
|
||||
require.NoError(t, err)
|
||||
|
||||
return store.NewStore(tmpdir), func() { os.RemoveAll(tmpdir) }
|
||||
}
|
||||
|
||||
func ref(t *testing.T, name string) reference.Named {
|
||||
named, err := reference.ParseNamed("example.com/" + name)
|
||||
require.NoError(t, err)
|
||||
return named
|
||||
}
|
||||
|
||||
func fullImageManifest(t *testing.T, ref reference.Named) types.ImageManifest {
|
||||
man, err := schema2.FromStruct(schema2.Manifest{
|
||||
Versioned: schema2.SchemaVersion,
|
||||
Config: distribution.Descriptor{
|
||||
Digest: "sha256:7328f6f8b41890597575cbaadc884e7386ae0acc53b747401ebce5cf0d624560",
|
||||
Size: 1520,
|
||||
MediaType: schema2.MediaTypeImageConfig,
|
||||
},
|
||||
Layers: []distribution.Descriptor{
|
||||
{
|
||||
MediaType: schema2.MediaTypeLayer,
|
||||
Size: 1990402,
|
||||
Digest: "sha256:88286f41530e93dffd4b964e1db22ce4939fffa4a4c665dab8591fbab03d4926",
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
// TODO: include image data for verbose inspect
|
||||
return types.NewImageManifest(ref, digest.Digest("sha256:7328f6f8b41890597575cbaadc884e7386ae0acc53b747401ebce5cf0d62abcd"), types.Image{OS: "linux", Architecture: "amd64"}, man)
|
||||
}
|
||||
|
||||
func TestInspectCommandLocalManifestNotFound(t *testing.T) {
|
||||
store, cleanup := newTempManifestStore(t)
|
||||
defer cleanup()
|
||||
|
||||
cli := test.NewFakeCli(nil)
|
||||
cli.SetManifestStore(store)
|
||||
|
||||
cmd := newInspectCommand(cli)
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
cmd.SetArgs([]string{"example.com/list:v1", "example.com/alpine:3.0"})
|
||||
err := cmd.Execute()
|
||||
assert.EqualError(t, err, "No such manifest: example.com/alpine:3.0")
|
||||
}
|
||||
|
||||
func TestInspectCommandNotFound(t *testing.T) {
|
||||
store, cleanup := newTempManifestStore(t)
|
||||
defer cleanup()
|
||||
|
||||
cli := test.NewFakeCli(nil)
|
||||
cli.SetManifestStore(store)
|
||||
cli.SetRegistryClient(&fakeRegistryClient{
|
||||
getManifestFunc: func(_ context.Context, _ reference.Named) (manifesttypes.ImageManifest, error) {
|
||||
return manifesttypes.ImageManifest{}, errors.New("missing")
|
||||
},
|
||||
getManifestListFunc: func(ctx context.Context, ref reference.Named) ([]manifesttypes.ImageManifest, error) {
|
||||
return nil, errors.Errorf("No such manifest: %s", ref)
|
||||
},
|
||||
})
|
||||
|
||||
cmd := newInspectCommand(cli)
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
cmd.SetArgs([]string{"example.com/alpine:3.0"})
|
||||
err := cmd.Execute()
|
||||
assert.EqualError(t, err, "No such manifest: example.com/alpine:3.0")
|
||||
}
|
||||
|
||||
func TestInspectCommandLocalManifest(t *testing.T) {
|
||||
store, cleanup := newTempManifestStore(t)
|
||||
defer cleanup()
|
||||
|
||||
cli := test.NewFakeCli(nil)
|
||||
cli.SetManifestStore(store)
|
||||
namedRef := ref(t, "alpine:3.0")
|
||||
imageManifest := fullImageManifest(t, namedRef)
|
||||
err := store.Save(ref(t, "list:v1"), namedRef, imageManifest)
|
||||
require.NoError(t, err)
|
||||
|
||||
cmd := newInspectCommand(cli)
|
||||
cmd.SetArgs([]string{"example.com/list:v1", "example.com/alpine:3.0"})
|
||||
require.NoError(t, cmd.Execute())
|
||||
actual := cli.OutBuffer()
|
||||
expected := golden.Get(t, "inspect-manifest.golden")
|
||||
assert.Equal(t, string(expected), actual.String())
|
||||
}
|
||||
|
||||
func TestInspectcommandRemoteManifest(t *testing.T) {
|
||||
store, cleanup := newTempManifestStore(t)
|
||||
defer cleanup()
|
||||
|
||||
cli := test.NewFakeCli(nil)
|
||||
cli.SetManifestStore(store)
|
||||
cli.SetRegistryClient(&fakeRegistryClient{
|
||||
getManifestFunc: func(_ context.Context, ref reference.Named) (manifesttypes.ImageManifest, error) {
|
||||
return fullImageManifest(t, ref), nil
|
||||
},
|
||||
})
|
||||
|
||||
cmd := newInspectCommand(cli)
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
cmd.SetArgs([]string{"example.com/alpine:3.0"})
|
||||
require.NoError(t, cmd.Execute())
|
||||
actual := cli.OutBuffer()
|
||||
expected := golden.Get(t, "inspect-manifest.golden")
|
||||
assert.Equal(t, string(expected), actual.String())
|
||||
}
|
||||
273
components/cli/cli/command/manifest/push.go
Normal file
273
components/cli/cli/command/manifest/push.go
Normal file
@ -0,0 +1,273 @@
|
||||
package manifest
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/manifest/types"
|
||||
registryclient "github.com/docker/cli/cli/registry/client"
|
||||
"github.com/docker/distribution/manifest/manifestlist"
|
||||
"github.com/docker/distribution/manifest/schema2"
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type pushOpts struct {
|
||||
insecure bool
|
||||
purge bool
|
||||
target string
|
||||
}
|
||||
|
||||
type mountRequest struct {
|
||||
ref reference.Named
|
||||
manifest types.ImageManifest
|
||||
}
|
||||
|
||||
type manifestBlob struct {
|
||||
canonical reference.Canonical
|
||||
os string
|
||||
}
|
||||
|
||||
type pushRequest struct {
|
||||
targetRef reference.Named
|
||||
list *manifestlist.DeserializedManifestList
|
||||
mountRequests []mountRequest
|
||||
manifestBlobs []manifestBlob
|
||||
insecure bool
|
||||
}
|
||||
|
||||
func newPushListCommand(dockerCli command.Cli) *cobra.Command {
|
||||
opts := pushOpts{}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "push [OPTIONS] MANIFEST_LIST",
|
||||
Short: "Push a manifest list to a repository",
|
||||
Args: cli.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.target = args[0]
|
||||
return runPush(dockerCli, opts)
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVarP(&opts.purge, "purge", "p", false, "Remove the local manifest list after push")
|
||||
flags.BoolVar(&opts.insecure, "insecure", false, "Allow push to an insecure registry")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runPush(dockerCli command.Cli, opts pushOpts) error {
|
||||
|
||||
targetRef, err := normalizeReference(opts.target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
manifests, err := dockerCli.ManifestStore().GetList(targetRef)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(manifests) == 0 {
|
||||
return errors.Errorf("%s not found", targetRef)
|
||||
}
|
||||
|
||||
pushRequest, err := buildPushRequest(manifests, targetRef, opts.insecure)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
if err := pushList(ctx, dockerCli, pushRequest); err != nil {
|
||||
return err
|
||||
}
|
||||
if opts.purge {
|
||||
return dockerCli.ManifestStore().Remove(targetRef)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildPushRequest(manifests []types.ImageManifest, targetRef reference.Named, insecure bool) (pushRequest, error) {
|
||||
req := pushRequest{targetRef: targetRef, insecure: insecure}
|
||||
|
||||
var err error
|
||||
req.list, err = buildManifestList(manifests, targetRef)
|
||||
if err != nil {
|
||||
return req, err
|
||||
}
|
||||
|
||||
targetRepo, err := registry.ParseRepositoryInfo(targetRef)
|
||||
if err != nil {
|
||||
return req, err
|
||||
}
|
||||
targetRepoName, err := registryclient.RepoNameForReference(targetRepo.Name)
|
||||
if err != nil {
|
||||
return req, err
|
||||
}
|
||||
|
||||
for _, imageManifest := range manifests {
|
||||
manifestRepoName, err := registryclient.RepoNameForReference(imageManifest.Ref)
|
||||
if err != nil {
|
||||
return req, err
|
||||
}
|
||||
|
||||
repoName, _ := reference.WithName(manifestRepoName)
|
||||
if repoName.Name() != targetRepoName {
|
||||
blobs, err := buildBlobRequestList(imageManifest, repoName)
|
||||
if err != nil {
|
||||
return req, err
|
||||
}
|
||||
req.manifestBlobs = append(req.manifestBlobs, blobs...)
|
||||
|
||||
manifestPush, err := buildPutManifestRequest(imageManifest, targetRef)
|
||||
if err != nil {
|
||||
return req, err
|
||||
}
|
||||
req.mountRequests = append(req.mountRequests, manifestPush)
|
||||
}
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func buildManifestList(manifests []types.ImageManifest, targetRef reference.Named) (*manifestlist.DeserializedManifestList, error) {
|
||||
targetRepoInfo, err := registry.ParseRepositoryInfo(targetRef)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
descriptors := []manifestlist.ManifestDescriptor{}
|
||||
for _, imageManifest := range manifests {
|
||||
if imageManifest.Platform.Architecture == "" || imageManifest.Platform.OS == "" {
|
||||
return nil, errors.Errorf(
|
||||
"manifest %s must have an OS and Architecture to be pushed to a registry", imageManifest.Ref)
|
||||
}
|
||||
descriptor, err := buildManifestDescriptor(targetRepoInfo, imageManifest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
descriptors = append(descriptors, descriptor)
|
||||
}
|
||||
|
||||
return manifestlist.FromDescriptors(descriptors)
|
||||
}
|
||||
|
||||
func buildManifestDescriptor(targetRepo *registry.RepositoryInfo, imageManifest types.ImageManifest) (manifestlist.ManifestDescriptor, error) {
|
||||
repoInfo, err := registry.ParseRepositoryInfo(imageManifest.Ref)
|
||||
if err != nil {
|
||||
return manifestlist.ManifestDescriptor{}, err
|
||||
}
|
||||
|
||||
manifestRepoHostname := reference.Domain(repoInfo.Name)
|
||||
targetRepoHostname := reference.Domain(targetRepo.Name)
|
||||
if manifestRepoHostname != targetRepoHostname {
|
||||
return manifestlist.ManifestDescriptor{}, errors.Errorf("cannot use source images from a different registry than the target image: %s != %s", manifestRepoHostname, targetRepoHostname)
|
||||
}
|
||||
|
||||
mediaType, raw, err := imageManifest.Payload()
|
||||
if err != nil {
|
||||
return manifestlist.ManifestDescriptor{}, err
|
||||
}
|
||||
|
||||
manifest := manifestlist.ManifestDescriptor{
|
||||
Platform: imageManifest.Platform,
|
||||
}
|
||||
manifest.Descriptor.Digest = imageManifest.Digest
|
||||
manifest.Size = int64(len(raw))
|
||||
manifest.MediaType = mediaType
|
||||
|
||||
if err = manifest.Descriptor.Digest.Validate(); err != nil {
|
||||
return manifestlist.ManifestDescriptor{}, errors.Wrapf(err,
|
||||
"digest parse of image %q failed", imageManifest.Ref)
|
||||
}
|
||||
|
||||
return manifest, nil
|
||||
}
|
||||
|
||||
func buildBlobRequestList(imageManifest types.ImageManifest, repoName reference.Named) ([]manifestBlob, error) {
|
||||
var blobReqs []manifestBlob
|
||||
|
||||
for _, blobDigest := range imageManifest.Blobs() {
|
||||
canonical, err := reference.WithDigest(repoName, blobDigest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
blobReqs = append(blobReqs, manifestBlob{canonical: canonical, os: imageManifest.Platform.OS})
|
||||
}
|
||||
return blobReqs, nil
|
||||
}
|
||||
|
||||
// nolint: interfacer
|
||||
func buildPutManifestRequest(imageManifest types.ImageManifest, targetRef reference.Named) (mountRequest, error) {
|
||||
refWithoutTag, err := reference.WithName(targetRef.Name())
|
||||
if err != nil {
|
||||
return mountRequest{}, err
|
||||
}
|
||||
mountRef, err := reference.WithDigest(refWithoutTag, imageManifest.Digest)
|
||||
if err != nil {
|
||||
return mountRequest{}, err
|
||||
}
|
||||
|
||||
// This indentation has to be added to ensure sha parity with the registry
|
||||
v2ManifestBytes, err := json.MarshalIndent(imageManifest.SchemaV2Manifest, "", " ")
|
||||
if err != nil {
|
||||
return mountRequest{}, err
|
||||
}
|
||||
// indent only the DeserializedManifest portion of this, in order to maintain parity with the registry
|
||||
// and not alter the sha
|
||||
var v2Manifest schema2.DeserializedManifest
|
||||
if err = v2Manifest.UnmarshalJSON(v2ManifestBytes); err != nil {
|
||||
return mountRequest{}, err
|
||||
}
|
||||
imageManifest.SchemaV2Manifest = &v2Manifest
|
||||
|
||||
return mountRequest{ref: mountRef, manifest: imageManifest}, err
|
||||
}
|
||||
|
||||
func pushList(ctx context.Context, dockerCli command.Cli, req pushRequest) error {
|
||||
rclient := dockerCli.RegistryClient(req.insecure)
|
||||
|
||||
if err := mountBlobs(ctx, rclient, req.targetRef, req.manifestBlobs); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := pushReferences(ctx, dockerCli.Out(), rclient, req.mountRequests); err != nil {
|
||||
return err
|
||||
}
|
||||
dgst, err := rclient.PutManifest(ctx, req.targetRef, req.list)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintln(dockerCli.Out(), dgst.String())
|
||||
return nil
|
||||
}
|
||||
|
||||
func pushReferences(ctx context.Context, out io.Writer, client registryclient.RegistryClient, mounts []mountRequest) error {
|
||||
for _, mount := range mounts {
|
||||
newDigest, err := client.PutManifest(ctx, mount.ref, mount.manifest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(out, "Pushed ref %s with digest: %s\n", mount.ref, newDigest)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func mountBlobs(ctx context.Context, client registryclient.RegistryClient, ref reference.Named, blobs []manifestBlob) error {
|
||||
for _, blob := range blobs {
|
||||
err := client.MountBlob(ctx, blob.canonical, ref)
|
||||
switch err.(type) {
|
||||
case nil:
|
||||
case registryclient.ErrBlobCreated:
|
||||
if blob.os != "windows" {
|
||||
return fmt.Errorf("error mounting %s to %s", blob.canonical, ref)
|
||||
}
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
73
components/cli/cli/command/manifest/push_test.go
Normal file
73
components/cli/cli/command/manifest/push_test.go
Normal file
@ -0,0 +1,73 @@
|
||||
package manifest
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
manifesttypes "github.com/docker/cli/cli/manifest/types"
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/cli/internal/test/testutil"
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func newFakeRegistryClient(t *testing.T) *fakeRegistryClient {
|
||||
require.NoError(t, nil)
|
||||
|
||||
return &fakeRegistryClient{
|
||||
getManifestFunc: func(_ context.Context, _ reference.Named) (manifesttypes.ImageManifest, error) {
|
||||
return manifesttypes.ImageManifest{}, errors.New("")
|
||||
},
|
||||
getManifestListFunc: func(_ context.Context, _ reference.Named) ([]manifesttypes.ImageManifest, error) {
|
||||
return nil, errors.Errorf("")
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestManifestPushErrors(t *testing.T) {
|
||||
testCases := []struct {
|
||||
args []string
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
args: []string{"one-arg", "extra-arg"},
|
||||
expectedError: "requires exactly 1 argument",
|
||||
},
|
||||
{
|
||||
args: []string{"th!si'sa/fa!ke/li$t/-name"},
|
||||
expectedError: "invalid reference format",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
cli := test.NewFakeCli(nil)
|
||||
cmd := newPushListCommand(cli)
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
// store a one-image manifest list and puah it
|
||||
func TestManifestPush(t *testing.T) {
|
||||
store, sCleanup := newTempManifestStore(t)
|
||||
defer sCleanup()
|
||||
|
||||
registry := newFakeRegistryClient(t)
|
||||
|
||||
cli := test.NewFakeCli(nil)
|
||||
cli.SetManifestStore(store)
|
||||
cli.SetRegistryClient(registry)
|
||||
|
||||
namedRef := ref(t, "alpine:3.0")
|
||||
imageManifest := fullImageManifest(t, namedRef)
|
||||
err := store.Save(ref(t, "list:v1"), namedRef, imageManifest)
|
||||
require.NoError(t, err)
|
||||
|
||||
cmd := newPushListCommand(cli)
|
||||
cmd.SetArgs([]string{"example.com/list:v1"})
|
||||
err = cmd.Execute()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
28
components/cli/cli/command/manifest/testdata/inspect-annotate.golden
vendored
Normal file
28
components/cli/cli/command/manifest/testdata/inspect-annotate.golden
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
{
|
||||
"Ref": "example.com/alpine:3.0",
|
||||
"Digest": "sha256:7328f6f8b41890597575cbaadc884e7386ae0acc53b747401ebce5cf0d62abcd",
|
||||
"SchemaV2Manifest": {
|
||||
"schemaVersion": 2,
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
|
||||
"config": {
|
||||
"mediaType": "application/vnd.docker.container.image.v1+json",
|
||||
"size": 1520,
|
||||
"digest": "sha256:7328f6f8b41890597575cbaadc884e7386ae0acc53b747401ebce5cf0d624560"
|
||||
},
|
||||
"layers": [
|
||||
{
|
||||
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
|
||||
"size": 1990402,
|
||||
"digest": "sha256:88286f41530e93dffd4b964e1db22ce4939fffa4a4c665dab8591fbab03d4926"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Platform": {
|
||||
"architecture": "arm",
|
||||
"os": "freebsd",
|
||||
"os.features": [
|
||||
"feature1"
|
||||
],
|
||||
"variant": "v7"
|
||||
}
|
||||
}
|
||||
24
components/cli/cli/command/manifest/testdata/inspect-manifest-list.golden
vendored
Normal file
24
components/cli/cli/command/manifest/testdata/inspect-manifest-list.golden
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"schemaVersion": 2,
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.list.v2+json",
|
||||
"manifests": [
|
||||
{
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
|
||||
"size": 428,
|
||||
"digest": "sha256:7328f6f8b41890597575cbaadc884e7386ae0acc53b747401ebce5cf0d62abcd",
|
||||
"platform": {
|
||||
"architecture": "amd64",
|
||||
"os": "linux"
|
||||
}
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
|
||||
"size": 428,
|
||||
"digest": "sha256:7328f6f8b41890597575cbaadc884e7386ae0acc53b747401ebce5cf0d62abcd",
|
||||
"platform": {
|
||||
"architecture": "amd64",
|
||||
"os": "linux"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
16
components/cli/cli/command/manifest/testdata/inspect-manifest.golden
vendored
Normal file
16
components/cli/cli/command/manifest/testdata/inspect-manifest.golden
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"schemaVersion": 2,
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
|
||||
"config": {
|
||||
"mediaType": "application/vnd.docker.container.image.v1+json",
|
||||
"size": 1520,
|
||||
"digest": "sha256:7328f6f8b41890597575cbaadc884e7386ae0acc53b747401ebce5cf0d624560"
|
||||
},
|
||||
"layers": [
|
||||
{
|
||||
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
|
||||
"size": 1990402,
|
||||
"digest": "sha256:88286f41530e93dffd4b964e1db22ce4939fffa4a4c665dab8591fbab03d4926"
|
||||
}
|
||||
]
|
||||
}
|
||||
79
components/cli/cli/command/manifest/util.go
Normal file
79
components/cli/cli/command/manifest/util.go
Normal file
@ -0,0 +1,79 @@
|
||||
package manifest
|
||||
|
||||
import (
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/manifest/store"
|
||||
"github.com/docker/cli/cli/manifest/types"
|
||||
"github.com/docker/distribution/reference"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type osArch struct {
|
||||
os string
|
||||
arch string
|
||||
}
|
||||
|
||||
// Remove any unsupported os/arch combo
|
||||
// list of valid os/arch values (see "Optional Environment Variables" section
|
||||
// of https://golang.org/doc/install/source
|
||||
// Added linux/s390x as we know System z support already exists
|
||||
var validOSArches = map[osArch]bool{
|
||||
{os: "darwin", arch: "386"}: true,
|
||||
{os: "darwin", arch: "amd64"}: true,
|
||||
{os: "darwin", arch: "arm"}: true,
|
||||
{os: "darwin", arch: "arm64"}: true,
|
||||
{os: "dragonfly", arch: "amd64"}: true,
|
||||
{os: "freebsd", arch: "386"}: true,
|
||||
{os: "freebsd", arch: "amd64"}: true,
|
||||
{os: "freebsd", arch: "arm"}: true,
|
||||
{os: "linux", arch: "386"}: true,
|
||||
{os: "linux", arch: "amd64"}: true,
|
||||
{os: "linux", arch: "arm"}: true,
|
||||
{os: "linux", arch: "arm64"}: true,
|
||||
{os: "linux", arch: "ppc64le"}: true,
|
||||
{os: "linux", arch: "mips64"}: true,
|
||||
{os: "linux", arch: "mips64le"}: true,
|
||||
{os: "linux", arch: "s390x"}: true,
|
||||
{os: "netbsd", arch: "386"}: true,
|
||||
{os: "netbsd", arch: "amd64"}: true,
|
||||
{os: "netbsd", arch: "arm"}: true,
|
||||
{os: "openbsd", arch: "386"}: true,
|
||||
{os: "openbsd", arch: "amd64"}: true,
|
||||
{os: "openbsd", arch: "arm"}: true,
|
||||
{os: "plan9", arch: "386"}: true,
|
||||
{os: "plan9", arch: "amd64"}: true,
|
||||
{os: "solaris", arch: "amd64"}: true,
|
||||
{os: "windows", arch: "386"}: true,
|
||||
{os: "windows", arch: "amd64"}: true,
|
||||
}
|
||||
|
||||
func isValidOSArch(os string, arch string) bool {
|
||||
// check for existence of this combo
|
||||
_, ok := validOSArches[osArch{os, arch}]
|
||||
return ok
|
||||
}
|
||||
|
||||
func normalizeReference(ref string) (reference.Named, error) {
|
||||
namedRef, err := reference.ParseNormalizedNamed(ref)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, isDigested := namedRef.(reference.Canonical); !isDigested {
|
||||
return reference.TagNameOnly(namedRef), nil
|
||||
}
|
||||
return namedRef, nil
|
||||
}
|
||||
|
||||
// getManifest from the local store, and fallback to the remote registry if it
|
||||
// doesn't exist locally
|
||||
func getManifest(ctx context.Context, dockerCli command.Cli, listRef, namedRef reference.Named, insecure bool) (types.ImageManifest, error) {
|
||||
data, err := dockerCli.ManifestStore().Get(listRef, namedRef)
|
||||
switch {
|
||||
case store.IsNotFound(err):
|
||||
return dockerCli.RegistryClient(insecure).GetManifest(ctx, namedRef)
|
||||
case err != nil:
|
||||
return types.ImageManifest{}, err
|
||||
default:
|
||||
return data, nil
|
||||
}
|
||||
}
|
||||
@ -14,11 +14,14 @@ import (
|
||||
// NewNodeCommand returns a cobra command for `node` subcommands
|
||||
func NewNodeCommand(dockerCli command.Cli) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "node",
|
||||
Short: "Manage Swarm nodes",
|
||||
Args: cli.NoArgs,
|
||||
RunE: command.ShowHelp(dockerCli.Err()),
|
||||
Annotations: map[string]string{"version": "1.24"},
|
||||
Use: "node",
|
||||
Short: "Manage Swarm nodes",
|
||||
Args: cli.NoArgs,
|
||||
RunE: command.ShowHelp(dockerCli.Err()),
|
||||
Annotations: map[string]string{
|
||||
"version": "1.24",
|
||||
"swarm": "",
|
||||
},
|
||||
}
|
||||
cmd.AddCommand(
|
||||
newDemoteCommand(dockerCli),
|
||||
|
||||
59
components/cli/cli/command/orchestrator.go
Normal file
59
components/cli/cli/command/orchestrator.go
Normal file
@ -0,0 +1,59 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Orchestrator type acts as an enum describing supported orchestrators.
|
||||
type Orchestrator string
|
||||
|
||||
const (
|
||||
// OrchestratorKubernetes orchestrator
|
||||
OrchestratorKubernetes = Orchestrator("kubernetes")
|
||||
// OrchestratorSwarm orchestrator
|
||||
OrchestratorSwarm = Orchestrator("swarm")
|
||||
orchestratorUnset = Orchestrator("unset")
|
||||
|
||||
defaultOrchestrator = OrchestratorSwarm
|
||||
envVarDockerOrchestrator = "DOCKER_ORCHESTRATOR"
|
||||
)
|
||||
|
||||
func normalize(flag string) Orchestrator {
|
||||
switch flag {
|
||||
case "kubernetes":
|
||||
return OrchestratorKubernetes
|
||||
case "swarm":
|
||||
return OrchestratorSwarm
|
||||
default:
|
||||
return orchestratorUnset
|
||||
}
|
||||
}
|
||||
|
||||
// GetOrchestrator checks DOCKER_ORCHESTRATOR environment variable and configuration file
|
||||
// orchestrator value and returns user defined Orchestrator.
|
||||
func GetOrchestrator(isExperimental bool, flagValue, value string) Orchestrator {
|
||||
// Non experimental CLI has kubernetes disabled
|
||||
if !isExperimental {
|
||||
return defaultOrchestrator
|
||||
}
|
||||
// Check flag
|
||||
if o := normalize(flagValue); o != orchestratorUnset {
|
||||
return o
|
||||
}
|
||||
// Check environment variable
|
||||
env := os.Getenv(envVarDockerOrchestrator)
|
||||
if o := normalize(env); o != orchestratorUnset {
|
||||
return o
|
||||
}
|
||||
// Check specified orchestrator
|
||||
if o := normalize(value); o != orchestratorUnset {
|
||||
return o
|
||||
}
|
||||
|
||||
if value != "" {
|
||||
fmt.Fprintf(os.Stderr, "Specified orchestrator %q is invalid. Please use either kubernetes or swarm\n", value)
|
||||
}
|
||||
// Nothing set, use default orchestrator
|
||||
return defaultOrchestrator
|
||||
}
|
||||
@ -10,14 +10,13 @@ import (
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/docker/docker/api/types"
|
||||
registrytypes "github.com/docker/docker/api/types/registry"
|
||||
"github.com/docker/docker/pkg/term"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
// ElectAuthServer returns the default registry to use (by asking the daemon)
|
||||
|
||||
@ -10,11 +10,14 @@ import (
|
||||
// NewSecretCommand returns a cobra command for `secret` subcommands
|
||||
func NewSecretCommand(dockerCli command.Cli) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "secret",
|
||||
Short: "Manage Docker secrets",
|
||||
Args: cli.NoArgs,
|
||||
RunE: command.ShowHelp(dockerCli.Err()),
|
||||
Annotations: map[string]string{"version": "1.25"},
|
||||
Use: "secret",
|
||||
Short: "Manage Docker secrets",
|
||||
Args: cli.NoArgs,
|
||||
RunE: command.ShowHelp(dockerCli.Err()),
|
||||
Annotations: map[string]string{
|
||||
"version": "1.25",
|
||||
"swarm": "",
|
||||
},
|
||||
}
|
||||
cmd.AddCommand(
|
||||
newSecretListCommand(dockerCli),
|
||||
|
||||
@ -16,6 +16,7 @@ type fakeClient struct {
|
||||
serviceListFunc func(context.Context, types.ServiceListOptions) ([]swarm.Service, error)
|
||||
taskListFunc func(context.Context, types.TaskListOptions) ([]swarm.Task, error)
|
||||
infoFunc func(ctx context.Context) (types.Info, error)
|
||||
networkInspectFunc func(ctx context.Context, networkID string, options types.NetworkInspectOptions) (types.NetworkResource, error)
|
||||
}
|
||||
|
||||
func (f *fakeClient) NodeList(ctx context.Context, options types.NodeListOptions) ([]swarm.Node, error) {
|
||||
@ -60,6 +61,13 @@ func (f *fakeClient) Info(ctx context.Context) (types.Info, error) {
|
||||
return f.infoFunc(ctx)
|
||||
}
|
||||
|
||||
func (f *fakeClient) NetworkInspect(ctx context.Context, networkID string, options types.NetworkInspectOptions) (types.NetworkResource, error) {
|
||||
if f.networkInspectFunc != nil {
|
||||
return f.networkInspectFunc(ctx, networkID, options)
|
||||
}
|
||||
return types.NetworkResource{}, nil
|
||||
}
|
||||
|
||||
func newService(id string, name string) swarm.Service {
|
||||
return swarm.Service{
|
||||
ID: id,
|
||||
|
||||
@ -10,11 +10,14 @@ import (
|
||||
// NewServiceCommand returns a cobra command for `service` subcommands
|
||||
func NewServiceCommand(dockerCli command.Cli) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "service",
|
||||
Short: "Manage services",
|
||||
Args: cli.NoArgs,
|
||||
RunE: command.ShowHelp(dockerCli.Err()),
|
||||
Annotations: map[string]string{"version": "1.24"},
|
||||
Use: "service",
|
||||
Short: "Manage services",
|
||||
Args: cli.NoArgs,
|
||||
RunE: command.ShowHelp(dockerCli.Err()),
|
||||
Annotations: map[string]string{
|
||||
"version": "1.24",
|
||||
"swarm": "",
|
||||
},
|
||||
}
|
||||
cmd.AddCommand(
|
||||
newCreateCommand(dockerCli),
|
||||
|
||||
@ -353,22 +353,21 @@ func (c *credentialSpecOpt) Value() *swarm.CredentialSpec {
|
||||
return c.value
|
||||
}
|
||||
|
||||
func convertNetworks(ctx context.Context, apiClient client.NetworkAPIClient, networks opts.NetworkOpt) ([]swarm.NetworkAttachmentConfig, error) {
|
||||
func resolveNetworkID(ctx context.Context, apiClient client.NetworkAPIClient, networkIDOrName string) (string, error) {
|
||||
nw, err := apiClient.NetworkInspect(ctx, networkIDOrName, types.NetworkInspectOptions{Scope: "swarm"})
|
||||
return nw.ID, err
|
||||
}
|
||||
|
||||
func convertNetworks(networks opts.NetworkOpt) []swarm.NetworkAttachmentConfig {
|
||||
var netAttach []swarm.NetworkAttachmentConfig
|
||||
for _, net := range networks.Value() {
|
||||
networkIDOrName := net.Target
|
||||
_, err := apiClient.NetworkInspect(ctx, networkIDOrName, types.NetworkInspectOptions{Scope: "swarm"})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
netAttach = append(netAttach, swarm.NetworkAttachmentConfig{
|
||||
Target: net.Target,
|
||||
Aliases: net.Aliases,
|
||||
DriverOpts: net.DriverOpts,
|
||||
})
|
||||
}
|
||||
sort.Sort(byNetworkTarget(netAttach))
|
||||
return netAttach, nil
|
||||
return netAttach
|
||||
}
|
||||
|
||||
type endpointOptions struct {
|
||||
@ -590,10 +589,15 @@ func (options *serviceOptions) ToService(ctx context.Context, apiClient client.N
|
||||
return service, err
|
||||
}
|
||||
|
||||
networks, err := convertNetworks(ctx, apiClient, options.networks)
|
||||
if err != nil {
|
||||
return service, err
|
||||
networks := convertNetworks(options.networks)
|
||||
for i, net := range networks {
|
||||
nwID, err := resolveNetworkID(ctx, apiClient, net.Target)
|
||||
if err != nil {
|
||||
return service, err
|
||||
}
|
||||
networks[i].Target = nwID
|
||||
}
|
||||
sort.Sort(byNetworkTarget(networks))
|
||||
|
||||
resources, err := options.resources.ToResourceRequirements()
|
||||
if err != nil {
|
||||
|
||||
@ -1,12 +1,17 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMemBytesString(t *testing.T) {
|
||||
@ -123,3 +128,37 @@ func TestResourceOptionsToResourceRequirements(t *testing.T) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestToServiceNetwork(t *testing.T) {
|
||||
nws := []types.NetworkResource{
|
||||
{Name: "aaa-network", ID: "id555"},
|
||||
{Name: "mmm-network", ID: "id999"},
|
||||
{Name: "zzz-network", ID: "id111"},
|
||||
}
|
||||
|
||||
client := &fakeClient{
|
||||
networkInspectFunc: func(ctx context.Context, networkID string, options types.NetworkInspectOptions) (types.NetworkResource, error) {
|
||||
for _, network := range nws {
|
||||
if network.ID == networkID || network.Name == networkID {
|
||||
return network, nil
|
||||
}
|
||||
}
|
||||
return types.NetworkResource{}, fmt.Errorf("network not found: %s", networkID)
|
||||
},
|
||||
}
|
||||
|
||||
nwo := opts.NetworkOpt{}
|
||||
nwo.Set("zzz-network")
|
||||
nwo.Set("mmm-network")
|
||||
nwo.Set("aaa-network")
|
||||
|
||||
o := newServiceOptions()
|
||||
o.mode = "replicated"
|
||||
o.networks = nwo
|
||||
|
||||
ctx := context.Background()
|
||||
flags := newCreateCommand(nil).Flags()
|
||||
service, err := o.ToService(ctx, client, flags)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []swarm.NetworkAttachmentConfig{{Target: "id111"}, {Target: "id555"}, {Target: "id999"}}, service.TaskTemplate.Networks)
|
||||
}
|
||||
|
||||
@ -1119,14 +1119,16 @@ func updateNetworks(ctx context.Context, apiClient client.NetworkAPIClient, flag
|
||||
|
||||
if flags.Changed(flagNetworkAdd) {
|
||||
values := flags.Lookup(flagNetworkAdd).Value.(*opts.NetworkOpt)
|
||||
networks, err := convertNetworks(ctx, apiClient, *values)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
networks := convertNetworks(*values)
|
||||
for _, network := range networks {
|
||||
if _, exists := existingNetworks[network.Target]; exists {
|
||||
nwID, err := resolveNetworkID(ctx, apiClient, network.Target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, exists := existingNetworks[nwID]; exists {
|
||||
return errors.Errorf("service is already attached to network %s", network.Target)
|
||||
}
|
||||
network.Target = nwID
|
||||
newNetworks = append(newNetworks, network)
|
||||
existingNetworks[network.Target] = struct{}{}
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sort"
|
||||
"testing"
|
||||
@ -586,3 +587,69 @@ func TestRemoveGenericResources(t *testing.T) {
|
||||
assert.NoError(t, removeGenericResources(flags, task))
|
||||
assert.Len(t, task.Resources.Reservations.GenericResources, 1)
|
||||
}
|
||||
|
||||
func TestUpdateNetworks(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
nws := []types.NetworkResource{
|
||||
{Name: "aaa-network", ID: "id555"},
|
||||
{Name: "mmm-network", ID: "id999"},
|
||||
{Name: "zzz-network", ID: "id111"},
|
||||
}
|
||||
|
||||
client := &fakeClient{
|
||||
networkInspectFunc: func(ctx context.Context, networkID string, options types.NetworkInspectOptions) (types.NetworkResource, error) {
|
||||
for _, network := range nws {
|
||||
if network.ID == networkID || network.Name == networkID {
|
||||
return network, nil
|
||||
}
|
||||
}
|
||||
return types.NetworkResource{}, fmt.Errorf("network not found: %s", networkID)
|
||||
},
|
||||
}
|
||||
|
||||
svc := swarm.ServiceSpec{
|
||||
TaskTemplate: swarm.TaskSpec{
|
||||
ContainerSpec: &swarm.ContainerSpec{},
|
||||
Networks: []swarm.NetworkAttachmentConfig{
|
||||
{Target: "id999"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
flags := newUpdateCommand(nil).Flags()
|
||||
err := flags.Set(flagNetworkAdd, "aaa-network")
|
||||
require.NoError(t, err)
|
||||
err = updateService(ctx, client, flags, &svc)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []swarm.NetworkAttachmentConfig{{Target: "id555"}, {Target: "id999"}}, svc.TaskTemplate.Networks)
|
||||
|
||||
flags = newUpdateCommand(nil).Flags()
|
||||
err = flags.Set(flagNetworkAdd, "aaa-network")
|
||||
require.NoError(t, err)
|
||||
err = updateService(ctx, client, flags, &svc)
|
||||
assert.EqualError(t, err, "service is already attached to network aaa-network")
|
||||
assert.Equal(t, []swarm.NetworkAttachmentConfig{{Target: "id555"}, {Target: "id999"}}, svc.TaskTemplate.Networks)
|
||||
|
||||
flags = newUpdateCommand(nil).Flags()
|
||||
err = flags.Set(flagNetworkAdd, "id555")
|
||||
require.NoError(t, err)
|
||||
err = updateService(ctx, client, flags, &svc)
|
||||
assert.EqualError(t, err, "service is already attached to network id555")
|
||||
assert.Equal(t, []swarm.NetworkAttachmentConfig{{Target: "id555"}, {Target: "id999"}}, svc.TaskTemplate.Networks)
|
||||
|
||||
flags = newUpdateCommand(nil).Flags()
|
||||
err = flags.Set(flagNetworkRemove, "id999")
|
||||
require.NoError(t, err)
|
||||
err = updateService(ctx, client, flags, &svc)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []swarm.NetworkAttachmentConfig{{Target: "id555"}}, svc.TaskTemplate.Networks)
|
||||
|
||||
flags = newUpdateCommand(nil).Flags()
|
||||
err = flags.Set(flagNetworkAdd, "mmm-network")
|
||||
require.NoError(t, err)
|
||||
err = flags.Set(flagNetworkRemove, "aaa-network")
|
||||
require.NoError(t, err)
|
||||
err = updateService(ctx, client, flags, &svc)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []swarm.NetworkAttachmentConfig{{Target: "id999"}}, svc.TaskTemplate.Networks)
|
||||
}
|
||||
|
||||
@ -9,19 +9,30 @@ import (
|
||||
// NewStackCommand returns a cobra command for `stack` subcommands
|
||||
func NewStackCommand(dockerCli command.Cli) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "stack",
|
||||
Short: "Manage Docker stacks",
|
||||
Args: cli.NoArgs,
|
||||
RunE: command.ShowHelp(dockerCli.Err()),
|
||||
Annotations: map[string]string{"version": "1.25"},
|
||||
Use: "stack",
|
||||
Short: "Manage Docker stacks",
|
||||
Args: cli.NoArgs,
|
||||
RunE: command.ShowHelp(dockerCli.Err()),
|
||||
Annotations: map[string]string{
|
||||
"kubernetes": "",
|
||||
"swarm": "",
|
||||
"version": "1.25",
|
||||
},
|
||||
}
|
||||
cmd.AddCommand(
|
||||
newDeployCommand(dockerCli),
|
||||
newListCommand(dockerCli),
|
||||
newPsCommand(dockerCli),
|
||||
newRemoveCommand(dockerCli),
|
||||
newServicesCommand(dockerCli),
|
||||
newPsCommand(dockerCli),
|
||||
)
|
||||
flags := cmd.PersistentFlags()
|
||||
flags.String("namespace", "default", "Kubernetes namespace to use")
|
||||
flags.SetAnnotation("namespace", "kubernetes", nil)
|
||||
flags.SetAnnotation("namespace", "experimentalCLI", nil)
|
||||
flags.String("kubeconfig", "", "Kubernetes config file")
|
||||
flags.SetAnnotation("kubeconfig", "kubernetes", nil)
|
||||
flags.SetAnnotation("kubeconfig", "experimentalCLI", nil)
|
||||
return cmd
|
||||
}
|
||||
|
||||
@ -30,6 +41,10 @@ func NewTopLevelDeployCommand(dockerCli command.Cli) *cobra.Command {
|
||||
cmd := newDeployCommand(dockerCli)
|
||||
// Remove the aliases at the top level
|
||||
cmd.Aliases = []string{}
|
||||
cmd.Annotations = map[string]string{"experimental": "", "version": "1.25"}
|
||||
cmd.Annotations = map[string]string{
|
||||
"experimental": "",
|
||||
"swarm": "",
|
||||
"version": "1.25",
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
@ -1,36 +1,16 @@
|
||||
package stack
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/compose/convert"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/api/types/versions"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/docker/cli/cli/command/stack/kubernetes"
|
||||
"github.com/docker/cli/cli/command/stack/options"
|
||||
"github.com/docker/cli/cli/command/stack/swarm"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultNetworkDriver = "overlay"
|
||||
resolveImageAlways = "always"
|
||||
resolveImageChanged = "changed"
|
||||
resolveImageNever = "never"
|
||||
)
|
||||
|
||||
type deployOptions struct {
|
||||
bundlefile string
|
||||
composefile string
|
||||
namespace string
|
||||
resolveImage string
|
||||
sendRegistryAuth bool
|
||||
prune bool
|
||||
}
|
||||
|
||||
func newDeployCommand(dockerCli command.Cli) *cobra.Command {
|
||||
var opts deployOptions
|
||||
var opts options.Deploy
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "deploy [OPTIONS] STACK",
|
||||
@ -38,85 +18,32 @@ func newDeployCommand(dockerCli command.Cli) *cobra.Command {
|
||||
Short: "Deploy a new stack or update an existing stack",
|
||||
Args: cli.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.namespace = args[0]
|
||||
return runDeploy(dockerCli, opts)
|
||||
opts.Namespace = args[0]
|
||||
if dockerCli.ClientInfo().HasKubernetes() {
|
||||
kli, err := kubernetes.WrapCli(dockerCli, cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return kubernetes.RunDeploy(kli, opts)
|
||||
}
|
||||
return swarm.RunDeploy(dockerCli, opts)
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
addBundlefileFlag(&opts.bundlefile, flags)
|
||||
addComposefileFlag(&opts.composefile, flags)
|
||||
addRegistryAuthFlag(&opts.sendRegistryAuth, flags)
|
||||
flags.BoolVar(&opts.prune, "prune", false, "Prune services that are no longer referenced")
|
||||
flags.StringVar(&opts.Bundlefile, "bundle-file", "", "Path to a Distributed Application Bundle file")
|
||||
flags.SetAnnotation("bundle-file", "experimental", nil)
|
||||
flags.SetAnnotation("bundle-file", "swarm", nil)
|
||||
flags.StringVarP(&opts.Composefile, "compose-file", "c", "", "Path to a Compose file")
|
||||
flags.SetAnnotation("compose-file", "version", []string{"1.25"})
|
||||
flags.BoolVar(&opts.SendRegistryAuth, "with-registry-auth", false, "Send registry authentication details to Swarm agents")
|
||||
flags.SetAnnotation("with-registry-auth", "swarm", nil)
|
||||
flags.BoolVar(&opts.Prune, "prune", false, "Prune services that are no longer referenced")
|
||||
flags.SetAnnotation("prune", "version", []string{"1.27"})
|
||||
flags.StringVar(&opts.resolveImage, "resolve-image", resolveImageAlways,
|
||||
`Query the registry to resolve image digest and supported platforms ("`+resolveImageAlways+`"|"`+resolveImageChanged+`"|"`+resolveImageNever+`")`)
|
||||
flags.SetAnnotation("prune", "swarm", nil)
|
||||
flags.StringVar(&opts.ResolveImage, "resolve-image", swarm.ResolveImageAlways,
|
||||
`Query the registry to resolve image digest and supported platforms ("`+swarm.ResolveImageAlways+`"|"`+swarm.ResolveImageChanged+`"|"`+swarm.ResolveImageNever+`")`)
|
||||
flags.SetAnnotation("resolve-image", "version", []string{"1.30"})
|
||||
flags.SetAnnotation("resolve-image", "swarm", nil)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runDeploy(dockerCli command.Cli, opts deployOptions) error {
|
||||
ctx := context.Background()
|
||||
|
||||
if err := validateResolveImageFlag(dockerCli, &opts); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch {
|
||||
case opts.bundlefile == "" && opts.composefile == "":
|
||||
return errors.Errorf("Please specify either a bundle file (with --bundle-file) or a Compose file (with --compose-file).")
|
||||
case opts.bundlefile != "" && opts.composefile != "":
|
||||
return errors.Errorf("You cannot specify both a bundle file and a Compose file.")
|
||||
case opts.bundlefile != "":
|
||||
return deployBundle(ctx, dockerCli, opts)
|
||||
default:
|
||||
return deployCompose(ctx, dockerCli, opts)
|
||||
}
|
||||
}
|
||||
|
||||
// validateResolveImageFlag validates the opts.resolveImage command line option
|
||||
// and also turns image resolution off if the version is older than 1.30
|
||||
func validateResolveImageFlag(dockerCli command.Cli, opts *deployOptions) error {
|
||||
if opts.resolveImage != resolveImageAlways && opts.resolveImage != resolveImageChanged && opts.resolveImage != resolveImageNever {
|
||||
return errors.Errorf("Invalid option %s for flag --resolve-image", opts.resolveImage)
|
||||
}
|
||||
// client side image resolution should not be done when the supported
|
||||
// server version is older than 1.30
|
||||
if versions.LessThan(dockerCli.Client().ClientVersion(), "1.30") {
|
||||
opts.resolveImage = resolveImageNever
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkDaemonIsSwarmManager does an Info API call to verify that the daemon is
|
||||
// a swarm manager. This is necessary because we must create networks before we
|
||||
// create services, but the API call for creating a network does not return a
|
||||
// proper status code when it can't create a network in the "global" scope.
|
||||
func checkDaemonIsSwarmManager(ctx context.Context, dockerCli command.Cli) error {
|
||||
info, err := dockerCli.Client().Info(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !info.Swarm.ControlAvailable {
|
||||
return errors.New("this node is not a swarm manager. Use \"docker swarm init\" or \"docker swarm join\" to connect this node to swarm and try again")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// pruneServices removes services that are no longer referenced in the source
|
||||
func pruneServices(ctx context.Context, dockerCli command.Cli, namespace convert.Namespace, services map[string]struct{}) {
|
||||
client := dockerCli.Client()
|
||||
|
||||
oldServices, err := getServices(ctx, client, namespace.Name())
|
||||
if err != nil {
|
||||
fmt.Fprintf(dockerCli.Err(), "Failed to list services: %s", err)
|
||||
}
|
||||
|
||||
pruneServices := []swarm.Service{}
|
||||
for _, service := range oldServices {
|
||||
if _, exists := services[namespace.Descope(service.Spec.Name)]; !exists {
|
||||
pruneServices = append(pruneServices, service)
|
||||
}
|
||||
}
|
||||
removeServices(ctx, dockerCli, pruneServices)
|
||||
}
|
||||
|
||||
32
components/cli/cli/command/stack/kubernetes/check.go
Normal file
32
components/cli/cli/command/stack/kubernetes/check.go
Normal file
@ -0,0 +1,32 @@
|
||||
package kubernetes
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
apiv1beta1 "github.com/docker/cli/kubernetes/compose/v1beta1"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
)
|
||||
|
||||
// APIPresent checks that an API is installed.
|
||||
func APIPresent(config *rest.Config) error {
|
||||
log.Debugf("check API present at %s", config.Host)
|
||||
clients, err := kubernetes.NewForConfig(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
groups, err := clients.Discovery().ServerGroups()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, group := range groups.Groups {
|
||||
if group.Name == apiv1beta1.SchemeGroupVersion.Group {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("could not find %s api. Install it on your cluster first", apiv1beta1.SchemeGroupVersion.Group)
|
||||
}
|
||||
76
components/cli/cli/command/stack/kubernetes/cli.go
Normal file
76
components/cli/cli/command/stack/kubernetes/cli.go
Normal file
@ -0,0 +1,76 @@
|
||||
package kubernetes
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
composev1beta1 "github.com/docker/cli/kubernetes/client/clientset_generated/clientset/typed/compose/v1beta1"
|
||||
"github.com/docker/docker/pkg/homedir"
|
||||
"github.com/spf13/cobra"
|
||||
restclient "k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
)
|
||||
|
||||
// KubeCli holds kubernetes specifics (client, namespace) with the command.Cli
|
||||
type KubeCli struct {
|
||||
command.Cli
|
||||
kubeConfig *restclient.Config
|
||||
kubeNamespace string
|
||||
}
|
||||
|
||||
// WrapCli wraps command.Cli with kubernetes specifics
|
||||
func WrapCli(dockerCli command.Cli, cmd *cobra.Command) (*KubeCli, error) {
|
||||
var err error
|
||||
cli := &KubeCli{
|
||||
Cli: dockerCli,
|
||||
kubeNamespace: "default",
|
||||
}
|
||||
if cmd.Flags().Changed("namespace") {
|
||||
cli.kubeNamespace, err = cmd.Flags().GetString("namespace")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
kubeConfig := ""
|
||||
if cmd.Flags().Changed("kubeconfig") {
|
||||
kubeConfig, err = cmd.Flags().GetString("kubeconfig")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if kubeConfig == "" {
|
||||
if config := os.Getenv("KUBECONFIG"); config != "" {
|
||||
kubeConfig = config
|
||||
} else {
|
||||
kubeConfig = filepath.Join(homedir.Get(), ".kube/config")
|
||||
}
|
||||
}
|
||||
|
||||
config, err := clientcmd.BuildConfigFromFlags("", kubeConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to load kubernetes configuration file '%s'", kubeConfig)
|
||||
}
|
||||
cli.kubeConfig = config
|
||||
|
||||
return cli, nil
|
||||
}
|
||||
|
||||
func (c *KubeCli) composeClient() (*Factory, error) {
|
||||
return NewFactory(c.kubeNamespace, c.kubeConfig)
|
||||
}
|
||||
|
||||
func (c *KubeCli) stacks() (composev1beta1.StackInterface, error) {
|
||||
err := APIPresent(c.kubeConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
clientSet, err := composev1beta1.NewForConfig(c.kubeConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return clientSet.Stacks(c.kubeNamespace), nil
|
||||
}
|
||||
71
components/cli/cli/command/stack/kubernetes/client.go
Normal file
71
components/cli/cli/command/stack/kubernetes/client.go
Normal file
@ -0,0 +1,71 @@
|
||||
package kubernetes
|
||||
|
||||
import (
|
||||
appsv1beta2 "k8s.io/client-go/kubernetes/typed/apps/v1beta2"
|
||||
typesappsv1beta2 "k8s.io/client-go/kubernetes/typed/apps/v1beta2"
|
||||
corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||
restclient "k8s.io/client-go/rest"
|
||||
)
|
||||
|
||||
// Factory is the kubernetes client factory
|
||||
type Factory struct {
|
||||
namespace string
|
||||
config *restclient.Config
|
||||
coreClientSet *corev1.CoreV1Client
|
||||
appsClientSet *appsv1beta2.AppsV1beta2Client
|
||||
}
|
||||
|
||||
// NewFactory creates a kubernetes client factory
|
||||
func NewFactory(namespace string, config *restclient.Config) (*Factory, error) {
|
||||
coreClientSet, err := corev1.NewForConfig(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
appsClientSet, err := appsv1beta2.NewForConfig(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Factory{
|
||||
namespace: namespace,
|
||||
config: config,
|
||||
coreClientSet: coreClientSet,
|
||||
appsClientSet: appsClientSet,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ConfigMaps returns a client for kubernetes's config maps
|
||||
func (s *Factory) ConfigMaps() corev1.ConfigMapInterface {
|
||||
return s.coreClientSet.ConfigMaps(s.namespace)
|
||||
}
|
||||
|
||||
// Secrets returns a client for kubernetes's secrets
|
||||
func (s *Factory) Secrets() corev1.SecretInterface {
|
||||
return s.coreClientSet.Secrets(s.namespace)
|
||||
}
|
||||
|
||||
// Services returns a client for kubernetes's secrets
|
||||
func (s *Factory) Services() corev1.ServiceInterface {
|
||||
return s.coreClientSet.Services(s.namespace)
|
||||
}
|
||||
|
||||
// Pods returns a client for kubernetes's pods
|
||||
func (s *Factory) Pods() corev1.PodInterface {
|
||||
return s.coreClientSet.Pods(s.namespace)
|
||||
}
|
||||
|
||||
// Nodes returns a client for kubernetes's nodes
|
||||
func (s *Factory) Nodes() corev1.NodeInterface {
|
||||
return s.coreClientSet.Nodes()
|
||||
}
|
||||
|
||||
// ReplicationControllers returns a client for kubernetes replication controllers
|
||||
func (s *Factory) ReplicationControllers() corev1.ReplicationControllerInterface {
|
||||
return s.coreClientSet.ReplicationControllers(s.namespace)
|
||||
}
|
||||
|
||||
// ReplicaSets return a client for kubernetes replace sets
|
||||
func (s *Factory) ReplicaSets() typesappsv1beta2.ReplicaSetInterface {
|
||||
return s.appsClientSet.ReplicaSets(s.namespace)
|
||||
}
|
||||
54
components/cli/cli/command/stack/kubernetes/collision.go
Normal file
54
components/cli/cli/command/stack/kubernetes/collision.go
Normal file
@ -0,0 +1,54 @@
|
||||
package kubernetes
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
composetypes "github.com/docker/cli/cli/compose/types"
|
||||
apiv1beta1 "github.com/docker/cli/kubernetes/compose/v1beta1"
|
||||
"github.com/docker/cli/kubernetes/labels"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||
)
|
||||
|
||||
// IsColliding verify that services defined in the stack collides with already deployed services
|
||||
func IsColliding(services corev1.ServiceInterface, stack *apiv1beta1.Stack, cfg *composetypes.Config) error {
|
||||
stackObjects := getServices(cfg)
|
||||
|
||||
for _, srv := range stackObjects {
|
||||
if err := verify(services, stack.Name, srv); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// verify checks wether the service is already present in kubernetes.
|
||||
// If we find the service by name but it doesn't have our label or it has a different value
|
||||
// than the stack name for the label, we fail (i.e. it will collide)
|
||||
func verify(services corev1.ServiceInterface, stackName string, service string) error {
|
||||
svc, err := services.Get(service, metav1.GetOptions{})
|
||||
if err == nil {
|
||||
if key, ok := svc.ObjectMeta.Labels[labels.ForStackName]; ok {
|
||||
if key != stackName {
|
||||
return fmt.Errorf("service %s already present in stack named %s", service, key)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("service %s already present in the cluster", service)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getServices(cfg *composetypes.Config) []string {
|
||||
services := make([]string, len(cfg.Services))
|
||||
for i := range cfg.Services {
|
||||
services[i] = cfg.Services[i].Name
|
||||
}
|
||||
sort.Strings(services)
|
||||
return services
|
||||
}
|
||||
174
components/cli/cli/command/stack/kubernetes/conversion.go
Normal file
174
components/cli/cli/command/stack/kubernetes/conversion.go
Normal file
@ -0,0 +1,174 @@
|
||||
package kubernetes
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/docker/cli/kubernetes/labels"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
appsv1beta2 "k8s.io/api/apps/v1beta2"
|
||||
apiv1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||
)
|
||||
|
||||
// Pod conversion
|
||||
func podToTask(pod apiv1.Pod) swarm.Task {
|
||||
var startTime time.Time
|
||||
if pod.Status.StartTime != nil {
|
||||
startTime = (*pod.Status.StartTime).Time
|
||||
}
|
||||
task := swarm.Task{
|
||||
ID: string(pod.UID),
|
||||
NodeID: pod.Spec.NodeName,
|
||||
Spec: swarm.TaskSpec{
|
||||
ContainerSpec: &swarm.ContainerSpec{
|
||||
Image: getContainerImage(pod.Spec.Containers),
|
||||
},
|
||||
},
|
||||
DesiredState: podPhaseToState(pod.Status.Phase),
|
||||
Status: swarm.TaskStatus{
|
||||
State: podPhaseToState(pod.Status.Phase),
|
||||
Timestamp: startTime,
|
||||
PortStatus: swarm.PortStatus{
|
||||
Ports: getPorts(pod.Spec.Containers),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return task
|
||||
}
|
||||
|
||||
func podPhaseToState(phase apiv1.PodPhase) swarm.TaskState {
|
||||
switch phase {
|
||||
case apiv1.PodPending:
|
||||
return swarm.TaskStatePending
|
||||
case apiv1.PodRunning:
|
||||
return swarm.TaskStateRunning
|
||||
case apiv1.PodSucceeded:
|
||||
return swarm.TaskStateComplete
|
||||
case apiv1.PodFailed:
|
||||
return swarm.TaskStateFailed
|
||||
default:
|
||||
return swarm.TaskState("unknown")
|
||||
}
|
||||
}
|
||||
|
||||
func toSwarmProtocol(protocol apiv1.Protocol) swarm.PortConfigProtocol {
|
||||
switch protocol {
|
||||
case apiv1.ProtocolTCP:
|
||||
return swarm.PortConfigProtocolTCP
|
||||
case apiv1.ProtocolUDP:
|
||||
return swarm.PortConfigProtocolUDP
|
||||
}
|
||||
return swarm.PortConfigProtocol("unknown")
|
||||
}
|
||||
|
||||
func fetchPods(namespace string, pods corev1.PodInterface) ([]apiv1.Pod, error) {
|
||||
labelSelector := labels.SelectorForStack(namespace)
|
||||
podsList, err := pods.List(metav1.ListOptions{LabelSelector: labelSelector})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return podsList.Items, nil
|
||||
}
|
||||
|
||||
func getContainerImage(containers []apiv1.Container) string {
|
||||
if len(containers) == 0 {
|
||||
return ""
|
||||
}
|
||||
return containers[0].Image
|
||||
}
|
||||
|
||||
func getPorts(containers []apiv1.Container) []swarm.PortConfig {
|
||||
if len(containers) == 0 || len(containers[0].Ports) == 0 {
|
||||
return nil
|
||||
}
|
||||
ports := make([]swarm.PortConfig, len(containers[0].Ports))
|
||||
for i, port := range containers[0].Ports {
|
||||
ports[i] = swarm.PortConfig{
|
||||
PublishedPort: uint32(port.HostPort),
|
||||
TargetPort: uint32(port.ContainerPort),
|
||||
Protocol: toSwarmProtocol(port.Protocol),
|
||||
}
|
||||
}
|
||||
return ports
|
||||
}
|
||||
|
||||
type tasksBySlot []swarm.Task
|
||||
|
||||
func (t tasksBySlot) Len() int {
|
||||
return len(t)
|
||||
}
|
||||
|
||||
func (t tasksBySlot) Swap(i, j int) {
|
||||
t[i], t[j] = t[j], t[i]
|
||||
}
|
||||
|
||||
func (t tasksBySlot) Less(i, j int) bool {
|
||||
// Sort by slot.
|
||||
if t[i].Slot != t[j].Slot {
|
||||
return t[i].Slot < t[j].Slot
|
||||
}
|
||||
|
||||
// If same slot, sort by most recent.
|
||||
return t[j].Meta.CreatedAt.Before(t[i].CreatedAt)
|
||||
}
|
||||
|
||||
// Replicas conversion
|
||||
func replicasToServices(replicas *appsv1beta2.ReplicaSetList, services *apiv1.ServiceList) ([]swarm.Service, map[string]formatter.ServiceListInfo, error) {
|
||||
result := make([]swarm.Service, len(replicas.Items))
|
||||
infos := make(map[string]formatter.ServiceListInfo, len(replicas.Items))
|
||||
for i, r := range replicas.Items {
|
||||
service, ok := findService(services, r.Labels[labels.ForServiceName])
|
||||
if !ok {
|
||||
return nil, nil, fmt.Errorf("could not find service '%s'", r.Labels[labels.ForServiceName])
|
||||
}
|
||||
stack, ok := service.Labels[labels.ForStackName]
|
||||
if ok {
|
||||
stack += "_"
|
||||
}
|
||||
uid := string(service.UID)
|
||||
s := swarm.Service{
|
||||
ID: uid,
|
||||
Spec: swarm.ServiceSpec{
|
||||
Annotations: swarm.Annotations{
|
||||
Name: stack + service.Name,
|
||||
},
|
||||
TaskTemplate: swarm.TaskSpec{
|
||||
ContainerSpec: &swarm.ContainerSpec{
|
||||
Image: getContainerImage(r.Spec.Template.Spec.Containers),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
if service.Spec.Type == apiv1.ServiceTypeLoadBalancer {
|
||||
configs := make([]swarm.PortConfig, len(service.Spec.Ports))
|
||||
for i, p := range service.Spec.Ports {
|
||||
configs[i] = swarm.PortConfig{
|
||||
PublishMode: swarm.PortConfigPublishModeIngress,
|
||||
PublishedPort: uint32(p.Port),
|
||||
TargetPort: uint32(p.TargetPort.IntValue()),
|
||||
Protocol: toSwarmProtocol(p.Protocol),
|
||||
}
|
||||
}
|
||||
s.Endpoint = swarm.Endpoint{Ports: configs}
|
||||
}
|
||||
result[i] = s
|
||||
infos[uid] = formatter.ServiceListInfo{
|
||||
Mode: "replicated",
|
||||
Replicas: fmt.Sprintf("%d/%d", r.Status.AvailableReplicas, r.Status.Replicas),
|
||||
}
|
||||
}
|
||||
return result, infos, nil
|
||||
}
|
||||
|
||||
func findService(services *apiv1.ServiceList, name string) (apiv1.Service, bool) {
|
||||
for _, s := range services.Items {
|
||||
if s.Name == name {
|
||||
return s, true
|
||||
}
|
||||
}
|
||||
return apiv1.Service{}, false
|
||||
}
|
||||
41
components/cli/cli/command/stack/kubernetes/convert.go
Normal file
41
components/cli/cli/command/stack/kubernetes/convert.go
Normal file
@ -0,0 +1,41 @@
|
||||
package kubernetes
|
||||
|
||||
import (
|
||||
"github.com/docker/cli/kubernetes/labels"
|
||||
apiv1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// toConfigMap converts a Compose Config to a Kube ConfigMap.
|
||||
func toConfigMap(stackName, name, key string, content []byte) *apiv1.ConfigMap {
|
||||
return &apiv1.ConfigMap{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "ConfigMap",
|
||||
APIVersion: "v1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Labels: map[string]string{
|
||||
labels.ForStackName: stackName,
|
||||
},
|
||||
},
|
||||
Data: map[string]string{
|
||||
key: string(content),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// toSecret converts a Compose Secret to a Kube Secret.
|
||||
func toSecret(stackName, name, key string, content []byte) *apiv1.Secret {
|
||||
return &apiv1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Labels: map[string]string{
|
||||
labels.ForStackName: stackName,
|
||||
},
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
key: content,
|
||||
},
|
||||
}
|
||||
}
|
||||
134
components/cli/cli/command/stack/kubernetes/deploy.go
Normal file
134
components/cli/cli/command/stack/kubernetes/deploy.go
Normal file
@ -0,0 +1,134 @@
|
||||
package kubernetes
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"path"
|
||||
|
||||
"github.com/docker/cli/cli/command/stack/options"
|
||||
composeTypes "github.com/docker/cli/cli/compose/types"
|
||||
"github.com/pkg/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||
)
|
||||
|
||||
// RunDeploy is the kubernetes implementation of docker stack deploy
|
||||
func RunDeploy(dockerCli *KubeCli, opts options.Deploy) error {
|
||||
cmdOut := dockerCli.Out()
|
||||
// Check arguments
|
||||
if opts.Composefile == "" {
|
||||
return errors.Errorf("Please specify a Compose file (with --compose-file).")
|
||||
}
|
||||
// Initialize clients
|
||||
stacks, err := dockerCli.stacks()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
composeClient, err := dockerCli.composeClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
configMaps := composeClient.ConfigMaps()
|
||||
secrets := composeClient.Secrets()
|
||||
services := composeClient.Services()
|
||||
pods := composeClient.Pods()
|
||||
watcher := DeployWatcher{
|
||||
Pods: pods,
|
||||
}
|
||||
|
||||
// Parse the compose file
|
||||
stack, cfg, err := LoadStack(opts.Namespace, opts.Composefile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// FIXME(vdemeester) handle warnings server-side
|
||||
if err = IsColliding(services, stack, cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = createFileBasedConfigMaps(stack.Name, cfg.Configs, configMaps); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = createFileBasedSecrets(stack.Name, cfg.Secrets, secrets); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if in, err := stacks.Get(stack.Name, metav1.GetOptions{}); err == nil {
|
||||
in.Spec = stack.Spec
|
||||
|
||||
if _, err = stacks.Update(in); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Stack %s was updated\n", stack.Name)
|
||||
} else {
|
||||
if _, err = stacks.Create(stack); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintf(cmdOut, "Stack %s was created\n", stack.Name)
|
||||
}
|
||||
|
||||
fmt.Fprintln(cmdOut, "Waiting for the stack to be stable and running...")
|
||||
|
||||
<-watcher.Watch(stack, serviceNames(cfg))
|
||||
|
||||
fmt.Fprintf(cmdOut, "Stack %s is stable and running\n\n", stack.Name)
|
||||
// TODO: fmt.Fprintf(cmdOut, "Read the logs with:\n $ %s stack logs %s\n", filepath.Base(os.Args[0]), stack.Name)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// createFileBasedConfigMaps creates a Kubernetes ConfigMap for each Compose global file-based config.
|
||||
func createFileBasedConfigMaps(stackName string, globalConfigs map[string]composeTypes.ConfigObjConfig, configMaps corev1.ConfigMapInterface) error {
|
||||
for name, config := range globalConfigs {
|
||||
if config.File == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
fileName := path.Base(config.File)
|
||||
content, err := ioutil.ReadFile(config.File)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := configMaps.Create(toConfigMap(stackName, name, fileName, content)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func serviceNames(cfg *composeTypes.Config) []string {
|
||||
names := []string{}
|
||||
|
||||
for _, service := range cfg.Services {
|
||||
names = append(names, service.Name)
|
||||
}
|
||||
|
||||
return names
|
||||
}
|
||||
|
||||
// createFileBasedSecrets creates a Kubernetes Secret for each Compose global file-based secret.
|
||||
func createFileBasedSecrets(stackName string, globalSecrets map[string]composeTypes.SecretConfig, secrets corev1.SecretInterface) error {
|
||||
for name, secret := range globalSecrets {
|
||||
if secret.File == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
fileName := path.Base(secret.File)
|
||||
content, err := ioutil.ReadFile(secret.File)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := secrets.Create(toSecret(stackName, name, fileName, content)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
74
components/cli/cli/command/stack/kubernetes/list.go
Normal file
74
components/cli/cli/command/stack/kubernetes/list.go
Normal file
@ -0,0 +1,74 @@
|
||||
package kubernetes
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/docker/cli/cli/command/stack/options"
|
||||
"github.com/docker/cli/cli/compose/loader"
|
||||
composetypes "github.com/docker/cli/cli/compose/types"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"vbom.ml/util/sortorder"
|
||||
)
|
||||
|
||||
// RunList is the kubernetes implementation of docker stack ls
|
||||
func RunList(dockerCli *KubeCli, opts options.List) error {
|
||||
stacks, err := getStacks(dockerCli)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
format := opts.Format
|
||||
if len(format) == 0 {
|
||||
format = formatter.TableFormatKey
|
||||
}
|
||||
stackCtx := formatter.Context{
|
||||
Output: dockerCli.Out(),
|
||||
Format: formatter.NewStackFormat(format),
|
||||
}
|
||||
sort.Sort(byName(stacks))
|
||||
return formatter.StackWrite(stackCtx, stacks)
|
||||
}
|
||||
|
||||
type byName []*formatter.Stack
|
||||
|
||||
func (n byName) Len() int { return len(n) }
|
||||
func (n byName) Swap(i, j int) { n[i], n[j] = n[j], n[i] }
|
||||
func (n byName) Less(i, j int) bool { return sortorder.NaturalLess(n[i].Name, n[j].Name) }
|
||||
|
||||
func getStacks(kubeCli *KubeCli) ([]*formatter.Stack, error) {
|
||||
stackSvc, err := kubeCli.stacks()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stacks, err := stackSvc.List(metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var formattedStacks []*formatter.Stack
|
||||
for _, stack := range stacks.Items {
|
||||
cfg, err := loadStack(stack.Spec.ComposeFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
formattedStacks = append(formattedStacks, &formatter.Stack{
|
||||
Name: stack.Name,
|
||||
Services: len(getServices(cfg)),
|
||||
})
|
||||
}
|
||||
return formattedStacks, nil
|
||||
}
|
||||
|
||||
func loadStack(composefile string) (*composetypes.Config, error) {
|
||||
parsed, err := loader.ParseYAML([]byte(composefile))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return loader.Load(composetypes.ConfigDetails{
|
||||
ConfigFiles: []composetypes.ConfigFile{
|
||||
{
|
||||
Config: parsed,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
169
components/cli/cli/command/stack/kubernetes/loader.go
Normal file
169
components/cli/cli/command/stack/kubernetes/loader.go
Normal file
@ -0,0 +1,169 @@
|
||||
package kubernetes
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli/compose/loader"
|
||||
"github.com/docker/cli/cli/compose/template"
|
||||
composetypes "github.com/docker/cli/cli/compose/types"
|
||||
apiv1beta1 "github.com/docker/cli/kubernetes/compose/v1beta1"
|
||||
"github.com/pkg/errors"
|
||||
yaml "gopkg.in/yaml.v2"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// LoadStack loads a stack from a Compose file, with a given name.
|
||||
// FIXME(vdemeester) remove this and use cli/compose/loader for both swarm and kubernetes
|
||||
func LoadStack(name, composeFile string) (*apiv1beta1.Stack, *composetypes.Config, error) {
|
||||
if composeFile == "" {
|
||||
return nil, nil, errors.New("compose-file must be set")
|
||||
}
|
||||
|
||||
workingDir, err := os.Getwd()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
composePath := composeFile
|
||||
if !strings.HasPrefix(composePath, "/") {
|
||||
composePath = filepath.Join(workingDir, composeFile)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(composePath); os.IsNotExist(err) {
|
||||
return nil, nil, errors.Errorf("no compose file found in %s", filepath.Dir(composePath))
|
||||
}
|
||||
|
||||
binary, err := ioutil.ReadFile(composePath)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(err, "cannot read compose file")
|
||||
}
|
||||
|
||||
env := env(workingDir)
|
||||
return load(name, binary, workingDir, env)
|
||||
}
|
||||
|
||||
func load(name string, binary []byte, workingDir string, env map[string]string) (*apiv1beta1.Stack, *composetypes.Config, error) {
|
||||
processed, err := template.Substitute(string(binary), func(key string) (string, bool) { return env[key], true })
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(err, "cannot load compose file")
|
||||
}
|
||||
|
||||
parsed, err := loader.ParseYAML([]byte(processed))
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrapf(err, "cannot load compose file")
|
||||
}
|
||||
|
||||
cfg, err := loader.Load(composetypes.ConfigDetails{
|
||||
WorkingDir: workingDir,
|
||||
ConfigFiles: []composetypes.ConfigFile{
|
||||
{
|
||||
Config: parsed,
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrapf(err, "cannot load compose file")
|
||||
}
|
||||
|
||||
result, err := processEnvFiles(processed, parsed, cfg)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrapf(err, "cannot load compose file")
|
||||
}
|
||||
|
||||
return &apiv1beta1.Stack{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
},
|
||||
Spec: apiv1beta1.StackSpec{
|
||||
ComposeFile: result,
|
||||
},
|
||||
}, cfg, nil
|
||||
}
|
||||
|
||||
type iMap = map[string]interface{}
|
||||
|
||||
func processEnvFiles(input string, parsed map[string]interface{}, config *composetypes.Config) (string, error) {
|
||||
changed := false
|
||||
|
||||
for _, svc := range config.Services {
|
||||
if len(svc.EnvFile) == 0 {
|
||||
continue
|
||||
}
|
||||
// Load() processed the env_file for us, we just need to inject back into
|
||||
// the intermediate representation
|
||||
env := iMap{}
|
||||
for k, v := range svc.Environment {
|
||||
env[k] = v
|
||||
}
|
||||
parsed["services"].(iMap)[svc.Name].(iMap)["environment"] = env
|
||||
delete(parsed["services"].(iMap)[svc.Name].(iMap), "env_file")
|
||||
changed = true
|
||||
}
|
||||
if !changed {
|
||||
return input, nil
|
||||
}
|
||||
res, err := yaml.Marshal(parsed)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(res), nil
|
||||
}
|
||||
|
||||
func env(workingDir string) map[string]string {
|
||||
// Apply .env file first
|
||||
config := readEnvFile(filepath.Join(workingDir, ".env"))
|
||||
|
||||
// Apply env variables
|
||||
for k, v := range envToMap(os.Environ()) {
|
||||
config[k] = v
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
func readEnvFile(path string) map[string]string {
|
||||
config := map[string]string{}
|
||||
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return config // Ignore
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if strings.HasPrefix(strings.TrimSpace(line), "#") {
|
||||
continue
|
||||
}
|
||||
|
||||
parts := strings.SplitN(line, "=", 2)
|
||||
if len(parts) == 2 {
|
||||
key := parts[0]
|
||||
value := parts[1]
|
||||
|
||||
config[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
func envToMap(env []string) map[string]string {
|
||||
config := map[string]string{}
|
||||
|
||||
for _, value := range env {
|
||||
parts := strings.SplitN(value, "=", 2)
|
||||
|
||||
key := parts[0]
|
||||
value := parts[1]
|
||||
|
||||
config[key] = value
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
34
components/cli/cli/command/stack/kubernetes/loader_test.go
Normal file
34
components/cli/cli/command/stack/kubernetes/loader_test.go
Normal file
@ -0,0 +1,34 @@
|
||||
package kubernetes
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPlaceholders(t *testing.T) {
|
||||
env := map[string]string{
|
||||
"TAG": "_latest_",
|
||||
"K1": "V1",
|
||||
"K2": "V2",
|
||||
}
|
||||
|
||||
prefix := "version: '3'\nvolumes:\n data:\n external:\n name: "
|
||||
var tests = []struct {
|
||||
input string
|
||||
expectedOutput string
|
||||
}{
|
||||
{prefix + "BEFORE${TAG}AFTER", prefix + "BEFORE_latest_AFTER"},
|
||||
{prefix + "BEFORE${K1}${K2}AFTER", prefix + "BEFOREV1V2AFTER"},
|
||||
{prefix + "BEFORE$TAG AFTER", prefix + "BEFORE_latest_ AFTER"},
|
||||
{prefix + "BEFORE$$TAG AFTER", prefix + "BEFORE$TAG AFTER"},
|
||||
{prefix + "BEFORE $UNKNOWN AFTER", prefix + "BEFORE AFTER"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
output, _, err := load("stack", []byte(test.input), ".", env)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, test.expectedOutput, output.Spec.ComposeFile)
|
||||
}
|
||||
}
|
||||
106
components/cli/cli/command/stack/kubernetes/ps.go
Normal file
106
components/cli/cli/command/stack/kubernetes/ps.go
Normal file
@ -0,0 +1,106 @@
|
||||
package kubernetes
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/docker/cli/cli/command/stack/options"
|
||||
"github.com/docker/cli/cli/command/task"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
apiv1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||
"k8s.io/kubernetes/pkg/api"
|
||||
)
|
||||
|
||||
// RunPS is the kubernetes implementation of docker stack ps
|
||||
func RunPS(dockerCli *KubeCli, options options.PS) error {
|
||||
namespace := options.Namespace
|
||||
|
||||
// Initialize clients
|
||||
client, err := dockerCli.composeClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stacks, err := dockerCli.stacks()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
podsClient := client.Pods()
|
||||
|
||||
// Fetch pods
|
||||
if _, err := stacks.Get(namespace, metav1.GetOptions{}); err != nil {
|
||||
return fmt.Errorf("nothing found in stack: %s", namespace)
|
||||
}
|
||||
|
||||
pods, err := fetchPods(namespace, podsClient)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(pods) == 0 {
|
||||
return fmt.Errorf("nothing found in stack: %s", namespace)
|
||||
}
|
||||
|
||||
format := options.Format
|
||||
if len(format) == 0 {
|
||||
format = task.DefaultFormat(dockerCli.ConfigFile(), options.Quiet)
|
||||
}
|
||||
nodeResolver := makeNodeResolver(options.NoResolve, client.Nodes())
|
||||
|
||||
tasks := make([]swarm.Task, len(pods))
|
||||
for i, pod := range pods {
|
||||
tasks[i] = podToTask(pod)
|
||||
}
|
||||
return print(dockerCli, namespace, tasks, pods, nodeResolver, !options.NoTrunc, options.Quiet, format)
|
||||
}
|
||||
|
||||
type idResolver func(name string) (string, error)
|
||||
|
||||
func print(dockerCli command.Cli, namespace string, tasks []swarm.Task, pods []apiv1.Pod, nodeResolver idResolver, trunc, quiet bool, format string) error {
|
||||
sort.Stable(tasksBySlot(tasks))
|
||||
|
||||
names := map[string]string{}
|
||||
nodes := map[string]string{}
|
||||
|
||||
tasksCtx := formatter.Context{
|
||||
Output: dockerCli.Out(),
|
||||
Format: formatter.NewTaskFormat(format, quiet),
|
||||
Trunc: trunc,
|
||||
}
|
||||
|
||||
for i, task := range tasks {
|
||||
nodeValue, err := nodeResolver(pods[i].Spec.NodeName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
names[task.ID] = fmt.Sprintf("%s_%s", namespace, pods[i].Name)
|
||||
nodes[task.ID] = nodeValue
|
||||
}
|
||||
|
||||
return formatter.TaskWrite(tasksCtx, tasks, names, nodes)
|
||||
}
|
||||
|
||||
func makeNodeResolver(noResolve bool, nodes corev1.NodeInterface) func(string) (string, error) {
|
||||
// Here we have a name and we need to resolve its identifier. To mimic swarm behavior
|
||||
// we need to resolve the id when noresolve is set, otherwise we return the name.
|
||||
if noResolve {
|
||||
return func(name string) (string, error) {
|
||||
n, err := nodes.List(metav1.ListOptions{
|
||||
FieldSelector: fields.OneTermEqualSelector(api.ObjectNameField, name).String(),
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(n.Items) != 1 {
|
||||
return "", fmt.Errorf("could not find node '%s'", name)
|
||||
}
|
||||
return string(n.Items[0].UID), nil
|
||||
}
|
||||
}
|
||||
return func(name string) (string, error) { return name, nil }
|
||||
}
|
||||
25
components/cli/cli/command/stack/kubernetes/remove.go
Normal file
25
components/cli/cli/command/stack/kubernetes/remove.go
Normal file
@ -0,0 +1,25 @@
|
||||
package kubernetes
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/cli/cli/command/stack/options"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// RunRemove is the kubernetes implementation of docker stack remove
|
||||
func RunRemove(dockerCli *KubeCli, opts options.Remove) error {
|
||||
stacks, err := dockerCli.stacks()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, stack := range opts.Namespaces {
|
||||
fmt.Fprintf(dockerCli.Out(), "Removing stack: %s\n", stack)
|
||||
err := stacks.Delete(stack, &metav1.DeleteOptions{})
|
||||
if err != nil {
|
||||
fmt.Fprintf(dockerCli.Out(), "Failed to remove stack %s: %s\n", stack, err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
64
components/cli/cli/command/stack/kubernetes/services.go
Normal file
64
components/cli/cli/command/stack/kubernetes/services.go
Normal file
@ -0,0 +1,64 @@
|
||||
package kubernetes
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/docker/cli/cli/command/stack/options"
|
||||
"github.com/docker/cli/kubernetes/labels"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// RunServices is the kubernetes implementation of docker stack services
|
||||
func RunServices(dockerCli *KubeCli, opts options.Services) error {
|
||||
// Initialize clients
|
||||
client, err := dockerCli.composeClient()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
stacks, err := dockerCli.stacks()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
replicas := client.ReplicaSets()
|
||||
|
||||
if _, err := stacks.Get(opts.Namespace, metav1.GetOptions{}); err != nil {
|
||||
fmt.Fprintf(dockerCli.Err(), "Nothing found in stack: %s\n", opts.Namespace)
|
||||
return nil
|
||||
}
|
||||
|
||||
replicasList, err := replicas.List(metav1.ListOptions{LabelSelector: labels.SelectorForStack(opts.Namespace)})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
servicesList, err := client.Services().List(metav1.ListOptions{LabelSelector: labels.SelectorForStack(opts.Namespace)})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Convert Replicas sets and kubernetes services to swam services and formatter informations
|
||||
services, info, err := replicasToServices(replicasList, servicesList)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if opts.Quiet {
|
||||
info = map[string]formatter.ServiceListInfo{}
|
||||
}
|
||||
|
||||
format := opts.Format
|
||||
if len(format) == 0 {
|
||||
if len(dockerCli.ConfigFile().ServicesFormat) > 0 && !opts.Quiet {
|
||||
format = dockerCli.ConfigFile().ServicesFormat
|
||||
} else {
|
||||
format = formatter.TableFormatKey
|
||||
}
|
||||
}
|
||||
|
||||
servicesCtx := formatter.Context{
|
||||
Output: dockerCli.Out(),
|
||||
Format: formatter.NewServiceListFormat(format, opts.Quiet),
|
||||
}
|
||||
return formatter.ServiceListWrite(servicesCtx, services, info)
|
||||
}
|
||||
117
components/cli/cli/command/stack/kubernetes/watcher.go
Normal file
117
components/cli/cli/command/stack/kubernetes/watcher.go
Normal file
@ -0,0 +1,117 @@
|
||||
package kubernetes
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
apiv1beta1 "github.com/docker/cli/kubernetes/compose/v1beta1"
|
||||
"github.com/docker/cli/kubernetes/labels"
|
||||
apiv1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||
)
|
||||
|
||||
// DeployWatcher watches a stack deployement
|
||||
type DeployWatcher struct {
|
||||
Pods corev1.PodInterface
|
||||
}
|
||||
|
||||
// Watch watches a stuck deployement and return a chan that will holds the state of the stack
|
||||
func (w DeployWatcher) Watch(stack *apiv1beta1.Stack, serviceNames []string) chan bool {
|
||||
stop := make(chan bool)
|
||||
|
||||
go w.waitForPods(stack.Name, serviceNames, stop)
|
||||
|
||||
return stop
|
||||
}
|
||||
|
||||
func (w DeployWatcher) waitForPods(stackName string, serviceNames []string, stop chan bool) {
|
||||
starts := map[string]int32{}
|
||||
|
||||
for {
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
list, err := w.Pods.List(metav1.ListOptions{
|
||||
LabelSelector: labels.SelectorForStack(stackName),
|
||||
IncludeUninitialized: true,
|
||||
})
|
||||
if err != nil {
|
||||
stop <- true
|
||||
return
|
||||
}
|
||||
|
||||
for i := range list.Items {
|
||||
pod := list.Items[i]
|
||||
if pod.Status.Phase != apiv1.PodRunning {
|
||||
continue
|
||||
}
|
||||
|
||||
startCount := startCount(pod)
|
||||
serviceName := pod.Labels[labels.ForServiceName]
|
||||
if startCount != starts[serviceName] {
|
||||
if startCount == 1 {
|
||||
fmt.Printf(" - Service %s has one container running\n", serviceName)
|
||||
} else {
|
||||
fmt.Printf(" - Service %s was restarted %d %s\n", serviceName, startCount-1, timeTimes(startCount-1))
|
||||
}
|
||||
|
||||
starts[serviceName] = startCount
|
||||
}
|
||||
}
|
||||
|
||||
if allReady(list.Items, serviceNames) {
|
||||
stop <- true
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func startCount(pod apiv1.Pod) int32 {
|
||||
restart := int32(0)
|
||||
|
||||
for _, status := range pod.Status.ContainerStatuses {
|
||||
restart += status.RestartCount
|
||||
}
|
||||
|
||||
return 1 + restart
|
||||
}
|
||||
|
||||
func allReady(pods []apiv1.Pod, serviceNames []string) bool {
|
||||
serviceUp := map[string]bool{}
|
||||
|
||||
for _, pod := range pods {
|
||||
if time.Since(pod.GetCreationTimestamp().Time) < 10*time.Second {
|
||||
return false
|
||||
}
|
||||
|
||||
ready := false
|
||||
for _, cond := range pod.Status.Conditions {
|
||||
if cond.Type == apiv1.PodReady && cond.Status == apiv1.ConditionTrue {
|
||||
ready = true
|
||||
}
|
||||
}
|
||||
|
||||
if !ready {
|
||||
return false
|
||||
}
|
||||
|
||||
serviceName := pod.Labels[labels.ForServiceName]
|
||||
serviceUp[serviceName] = true
|
||||
}
|
||||
|
||||
for _, serviceName := range serviceNames {
|
||||
if !serviceUp[serviceName] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func timeTimes(n int32) string {
|
||||
if n == 1 {
|
||||
return "time"
|
||||
}
|
||||
|
||||
return "times"
|
||||
}
|
||||
@ -1,26 +1,16 @@
|
||||
package stack
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/docker/cli/cli/compose/convert"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/docker/cli/cli/command/stack/kubernetes"
|
||||
"github.com/docker/cli/cli/command/stack/options"
|
||||
"github.com/docker/cli/cli/command/stack/swarm"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/net/context"
|
||||
"vbom.ml/util/sortorder"
|
||||
)
|
||||
|
||||
type listOptions struct {
|
||||
format string
|
||||
}
|
||||
|
||||
func newListCommand(dockerCli command.Cli) *cobra.Command {
|
||||
opts := listOptions{}
|
||||
opts := options.List{}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "ls",
|
||||
@ -28,69 +18,18 @@ func newListCommand(dockerCli command.Cli) *cobra.Command {
|
||||
Short: "List stacks",
|
||||
Args: cli.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runList(dockerCli, opts)
|
||||
if dockerCli.ClientInfo().HasKubernetes() {
|
||||
kli, err := kubernetes.WrapCli(dockerCli, cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return kubernetes.RunList(kli, opts)
|
||||
}
|
||||
return swarm.RunList(dockerCli, opts)
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.StringVar(&opts.format, "format", "", "Pretty-print stacks using a Go template")
|
||||
flags.StringVar(&opts.Format, "format", "", "Pretty-print stacks using a Go template")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runList(dockerCli command.Cli, opts listOptions) error {
|
||||
client := dockerCli.Client()
|
||||
ctx := context.Background()
|
||||
|
||||
stacks, err := getStacks(ctx, client)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
format := opts.format
|
||||
if len(format) == 0 {
|
||||
format = formatter.TableFormatKey
|
||||
}
|
||||
stackCtx := formatter.Context{
|
||||
Output: dockerCli.Out(),
|
||||
Format: formatter.NewStackFormat(format),
|
||||
}
|
||||
sort.Sort(byName(stacks))
|
||||
return formatter.StackWrite(stackCtx, stacks)
|
||||
}
|
||||
|
||||
type byName []*formatter.Stack
|
||||
|
||||
func (n byName) Len() int { return len(n) }
|
||||
func (n byName) Swap(i, j int) { n[i], n[j] = n[j], n[i] }
|
||||
func (n byName) Less(i, j int) bool { return sortorder.NaturalLess(n[i].Name, n[j].Name) }
|
||||
|
||||
func getStacks(ctx context.Context, apiclient client.APIClient) ([]*formatter.Stack, error) {
|
||||
services, err := apiclient.ServiceList(
|
||||
ctx,
|
||||
types.ServiceListOptions{Filters: getAllStacksFilter()})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m := make(map[string]*formatter.Stack)
|
||||
for _, service := range services {
|
||||
labels := service.Spec.Labels
|
||||
name, ok := labels[convert.LabelNamespace]
|
||||
if !ok {
|
||||
return nil, errors.Errorf("cannot get label %s for service %s",
|
||||
convert.LabelNamespace, service.ID)
|
||||
}
|
||||
ztack, ok := m[name]
|
||||
if !ok {
|
||||
m[name] = &formatter.Stack{
|
||||
Name: name,
|
||||
Services: 1,
|
||||
}
|
||||
} else {
|
||||
ztack.Services++
|
||||
}
|
||||
}
|
||||
var stacks []*formatter.Stack
|
||||
for _, stack := range m {
|
||||
stacks = append(stacks, stack)
|
||||
}
|
||||
return stacks, nil
|
||||
}
|
||||
|
||||
41
components/cli/cli/command/stack/options/opts.go
Normal file
41
components/cli/cli/command/stack/options/opts.go
Normal file
@ -0,0 +1,41 @@
|
||||
package options
|
||||
|
||||
import "github.com/docker/cli/opts"
|
||||
|
||||
// Deploy holds docker stack deploy options
|
||||
type Deploy struct {
|
||||
Bundlefile string
|
||||
Composefile string
|
||||
Namespace string
|
||||
ResolveImage string
|
||||
SendRegistryAuth bool
|
||||
Prune bool
|
||||
}
|
||||
|
||||
// List holds docker stack ls options
|
||||
type List struct {
|
||||
Format string
|
||||
}
|
||||
|
||||
// PS holds docker stack ps options
|
||||
type PS struct {
|
||||
Filter opts.FilterOpt
|
||||
NoTrunc bool
|
||||
Namespace string
|
||||
NoResolve bool
|
||||
Quiet bool
|
||||
Format string
|
||||
}
|
||||
|
||||
// Remove holds docker stack remove options
|
||||
type Remove struct {
|
||||
Namespaces []string
|
||||
}
|
||||
|
||||
// Services holds docker stack services options
|
||||
type Services struct {
|
||||
Quiet bool
|
||||
Format string
|
||||
Filter opts.FilterOpt
|
||||
Namespace string
|
||||
}
|
||||
@ -1,51 +0,0 @@
|
||||
package stack
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/docker/cli/cli/command/bundlefile"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
func addComposefileFlag(opt *string, flags *pflag.FlagSet) {
|
||||
flags.StringVarP(opt, "compose-file", "c", "", "Path to a Compose file")
|
||||
flags.SetAnnotation("compose-file", "version", []string{"1.25"})
|
||||
}
|
||||
|
||||
func addBundlefileFlag(opt *string, flags *pflag.FlagSet) {
|
||||
flags.StringVar(opt, "bundle-file", "", "Path to a Distributed Application Bundle file")
|
||||
flags.SetAnnotation("bundle-file", "experimental", nil)
|
||||
}
|
||||
|
||||
func addRegistryAuthFlag(opt *bool, flags *pflag.FlagSet) {
|
||||
flags.BoolVar(opt, "with-registry-auth", false, "Send registry authentication details to Swarm agents")
|
||||
}
|
||||
|
||||
func loadBundlefile(stderr io.Writer, namespace string, path string) (*bundlefile.Bundlefile, error) {
|
||||
defaultPath := fmt.Sprintf("%s.dab", namespace)
|
||||
|
||||
if path == "" {
|
||||
path = defaultPath
|
||||
}
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
return nil, errors.Errorf(
|
||||
"Bundle %s not found. Specify the path with --file",
|
||||
path)
|
||||
}
|
||||
|
||||
fmt.Fprintf(stderr, "Loading bundle from %s\n", path)
|
||||
reader, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
bundle, err := bundlefile.LoadFile(reader)
|
||||
if err != nil {
|
||||
return nil, errors.Errorf("Error reading %s: %v\n", path, err)
|
||||
}
|
||||
return bundle, err
|
||||
}
|
||||
@ -1,69 +1,41 @@
|
||||
package stack
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/idresolver"
|
||||
"github.com/docker/cli/cli/command/task"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/cli/cli/command/stack/kubernetes"
|
||||
"github.com/docker/cli/cli/command/stack/options"
|
||||
"github.com/docker/cli/cli/command/stack/swarm"
|
||||
cliopts "github.com/docker/cli/opts"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type psOptions struct {
|
||||
filter opts.FilterOpt
|
||||
noTrunc bool
|
||||
namespace string
|
||||
noResolve bool
|
||||
quiet bool
|
||||
format string
|
||||
}
|
||||
|
||||
func newPsCommand(dockerCli command.Cli) *cobra.Command {
|
||||
options := psOptions{filter: opts.NewFilterOpt()}
|
||||
opts := options.PS{Filter: cliopts.NewFilterOpt()}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "ps [OPTIONS] STACK",
|
||||
Short: "List the tasks in the stack",
|
||||
Args: cli.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
options.namespace = args[0]
|
||||
return runPS(dockerCli, options)
|
||||
opts.Namespace = args[0]
|
||||
if dockerCli.ClientInfo().HasKubernetes() {
|
||||
kli, err := kubernetes.WrapCli(dockerCli, cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return kubernetes.RunPS(kli, opts)
|
||||
}
|
||||
return swarm.RunPS(dockerCli, opts)
|
||||
},
|
||||
}
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVar(&options.noTrunc, "no-trunc", false, "Do not truncate output")
|
||||
flags.BoolVar(&options.noResolve, "no-resolve", false, "Do not map IDs to Names")
|
||||
flags.VarP(&options.filter, "filter", "f", "Filter output based on conditions provided")
|
||||
flags.BoolVarP(&options.quiet, "quiet", "q", false, "Only display task IDs")
|
||||
flags.StringVar(&options.format, "format", "", "Pretty-print tasks using a Go template")
|
||||
flags.BoolVar(&opts.NoTrunc, "no-trunc", false, "Do not truncate output")
|
||||
flags.BoolVar(&opts.NoResolve, "no-resolve", false, "Do not map IDs to Names")
|
||||
flags.VarP(&opts.Filter, "filter", "f", "Filter output based on conditions provided")
|
||||
flags.SetAnnotation("filter", "swarm", nil)
|
||||
flags.BoolVarP(&opts.Quiet, "quiet", "q", false, "Only display task IDs")
|
||||
flags.StringVar(&opts.Format, "format", "", "Pretty-print tasks using a Go template")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runPS(dockerCli command.Cli, options psOptions) error {
|
||||
namespace := options.namespace
|
||||
client := dockerCli.Client()
|
||||
ctx := context.Background()
|
||||
|
||||
filter := getStackFilterFromOpt(options.namespace, options.filter)
|
||||
|
||||
tasks, err := client.TaskList(ctx, types.TaskListOptions{Filters: filter})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(tasks) == 0 {
|
||||
return fmt.Errorf("nothing found in stack: %s", namespace)
|
||||
}
|
||||
|
||||
format := options.format
|
||||
if len(format) == 0 {
|
||||
format = task.DefaultFormat(dockerCli.ConfigFile(), options.quiet)
|
||||
}
|
||||
|
||||
return task.Print(ctx, dockerCli, tasks, idresolver.New(client, options.noResolve), !options.noTrunc, options.quiet, format)
|
||||
}
|
||||
|
||||
@ -1,26 +1,16 @@
|
||||
package stack
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/api/types/versions"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/docker/cli/cli/command/stack/kubernetes"
|
||||
"github.com/docker/cli/cli/command/stack/options"
|
||||
"github.com/docker/cli/cli/command/stack/swarm"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type removeOptions struct {
|
||||
namespaces []string
|
||||
}
|
||||
|
||||
func newRemoveCommand(dockerCli command.Cli) *cobra.Command {
|
||||
var opts removeOptions
|
||||
var opts options.Remove
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "rm STACK [STACK...]",
|
||||
@ -28,134 +18,16 @@ func newRemoveCommand(dockerCli command.Cli) *cobra.Command {
|
||||
Short: "Remove one or more stacks",
|
||||
Args: cli.RequiresMinArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.namespaces = args
|
||||
return runRemove(dockerCli, opts)
|
||||
opts.Namespaces = args
|
||||
if dockerCli.ClientInfo().HasKubernetes() {
|
||||
kli, err := kubernetes.WrapCli(dockerCli, cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return kubernetes.RunRemove(kli, opts)
|
||||
}
|
||||
return swarm.RunRemove(dockerCli, opts)
|
||||
},
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runRemove(dockerCli command.Cli, opts removeOptions) error {
|
||||
namespaces := opts.namespaces
|
||||
client := dockerCli.Client()
|
||||
ctx := context.Background()
|
||||
|
||||
var errs []string
|
||||
for _, namespace := range namespaces {
|
||||
services, err := getServices(ctx, client, namespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
networks, err := getStackNetworks(ctx, client, namespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var secrets []swarm.Secret
|
||||
if versions.GreaterThanOrEqualTo(client.ClientVersion(), "1.25") {
|
||||
secrets, err = getStackSecrets(ctx, client, namespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var configs []swarm.Config
|
||||
if versions.GreaterThanOrEqualTo(client.ClientVersion(), "1.30") {
|
||||
configs, err = getStackConfigs(ctx, client, namespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(services)+len(networks)+len(secrets)+len(configs) == 0 {
|
||||
fmt.Fprintf(dockerCli.Err(), "Nothing found in stack: %s\n", namespace)
|
||||
continue
|
||||
}
|
||||
|
||||
hasError := removeServices(ctx, dockerCli, services)
|
||||
hasError = removeSecrets(ctx, dockerCli, secrets) || hasError
|
||||
hasError = removeConfigs(ctx, dockerCli, configs) || hasError
|
||||
hasError = removeNetworks(ctx, dockerCli, networks) || hasError
|
||||
|
||||
if hasError {
|
||||
errs = append(errs, fmt.Sprintf("Failed to remove some resources from stack: %s", namespace))
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return errors.Errorf(strings.Join(errs, "\n"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func sortServiceByName(services []swarm.Service) func(i, j int) bool {
|
||||
return func(i, j int) bool {
|
||||
return services[i].Spec.Name < services[j].Spec.Name
|
||||
}
|
||||
}
|
||||
|
||||
func removeServices(
|
||||
ctx context.Context,
|
||||
dockerCli command.Cli,
|
||||
services []swarm.Service,
|
||||
) bool {
|
||||
var hasError bool
|
||||
sort.Slice(services, sortServiceByName(services))
|
||||
for _, service := range services {
|
||||
fmt.Fprintf(dockerCli.Out(), "Removing service %s\n", service.Spec.Name)
|
||||
if err := dockerCli.Client().ServiceRemove(ctx, service.ID); err != nil {
|
||||
hasError = true
|
||||
fmt.Fprintf(dockerCli.Err(), "Failed to remove service %s: %s", service.ID, err)
|
||||
}
|
||||
}
|
||||
return hasError
|
||||
}
|
||||
|
||||
func removeNetworks(
|
||||
ctx context.Context,
|
||||
dockerCli command.Cli,
|
||||
networks []types.NetworkResource,
|
||||
) bool {
|
||||
var hasError bool
|
||||
for _, network := range networks {
|
||||
fmt.Fprintf(dockerCli.Out(), "Removing network %s\n", network.Name)
|
||||
if err := dockerCli.Client().NetworkRemove(ctx, network.ID); err != nil {
|
||||
hasError = true
|
||||
fmt.Fprintf(dockerCli.Err(), "Failed to remove network %s: %s", network.ID, err)
|
||||
}
|
||||
}
|
||||
return hasError
|
||||
}
|
||||
|
||||
func removeSecrets(
|
||||
ctx context.Context,
|
||||
dockerCli command.Cli,
|
||||
secrets []swarm.Secret,
|
||||
) bool {
|
||||
var hasError bool
|
||||
for _, secret := range secrets {
|
||||
fmt.Fprintf(dockerCli.Out(), "Removing secret %s\n", secret.Spec.Name)
|
||||
if err := dockerCli.Client().SecretRemove(ctx, secret.ID); err != nil {
|
||||
hasError = true
|
||||
fmt.Fprintf(dockerCli.Err(), "Failed to remove secret %s: %s", secret.ID, err)
|
||||
}
|
||||
}
|
||||
return hasError
|
||||
}
|
||||
|
||||
func removeConfigs(
|
||||
ctx context.Context,
|
||||
dockerCli command.Cli,
|
||||
configs []swarm.Config,
|
||||
) bool {
|
||||
var hasError bool
|
||||
for _, config := range configs {
|
||||
fmt.Fprintf(dockerCli.Out(), "Removing config %s\n", config.Spec.Name)
|
||||
if err := dockerCli.Client().ConfigRemove(ctx, config.ID); err != nil {
|
||||
hasError = true
|
||||
fmt.Fprintf(dockerCli.Err(), "Failed to remove config %s: %s", config.ID, err)
|
||||
}
|
||||
}
|
||||
return hasError
|
||||
}
|
||||
|
||||
@ -1,94 +1,39 @@
|
||||
package stack
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/docker/cli/cli/command/service"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/cli/cli/command/stack/kubernetes"
|
||||
"github.com/docker/cli/cli/command/stack/options"
|
||||
"github.com/docker/cli/cli/command/stack/swarm"
|
||||
cliopts "github.com/docker/cli/opts"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type servicesOptions struct {
|
||||
quiet bool
|
||||
format string
|
||||
filter opts.FilterOpt
|
||||
namespace string
|
||||
}
|
||||
|
||||
func newServicesCommand(dockerCli command.Cli) *cobra.Command {
|
||||
options := servicesOptions{filter: opts.NewFilterOpt()}
|
||||
opts := options.Services{Filter: cliopts.NewFilterOpt()}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "services [OPTIONS] STACK",
|
||||
Short: "List the services in the stack",
|
||||
Args: cli.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
options.namespace = args[0]
|
||||
return runServices(dockerCli, options)
|
||||
opts.Namespace = args[0]
|
||||
if dockerCli.ClientInfo().HasKubernetes() {
|
||||
kli, err := kubernetes.WrapCli(dockerCli, cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return kubernetes.RunServices(kli, opts)
|
||||
}
|
||||
return swarm.RunServices(dockerCli, opts)
|
||||
},
|
||||
}
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVarP(&options.quiet, "quiet", "q", false, "Only display IDs")
|
||||
flags.StringVar(&options.format, "format", "", "Pretty-print services using a Go template")
|
||||
flags.VarP(&options.filter, "filter", "f", "Filter output based on conditions provided")
|
||||
flags.BoolVarP(&opts.Quiet, "quiet", "q", false, "Only display IDs")
|
||||
flags.StringVar(&opts.Format, "format", "", "Pretty-print services using a Go template")
|
||||
flags.VarP(&opts.Filter, "filter", "f", "Filter output based on conditions provided")
|
||||
flags.SetAnnotation("filter", "swarm", nil)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runServices(dockerCli command.Cli, options servicesOptions) error {
|
||||
ctx := context.Background()
|
||||
client := dockerCli.Client()
|
||||
|
||||
filter := getStackFilterFromOpt(options.namespace, options.filter)
|
||||
services, err := client.ServiceList(ctx, types.ServiceListOptions{Filters: filter})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// if no services in this stack, print message and exit 0
|
||||
if len(services) == 0 {
|
||||
fmt.Fprintf(dockerCli.Err(), "Nothing found in stack: %s\n", options.namespace)
|
||||
return nil
|
||||
}
|
||||
|
||||
info := map[string]formatter.ServiceListInfo{}
|
||||
if !options.quiet {
|
||||
taskFilter := filters.NewArgs()
|
||||
for _, service := range services {
|
||||
taskFilter.Add("service", service.ID)
|
||||
}
|
||||
|
||||
tasks, err := client.TaskList(ctx, types.TaskListOptions{Filters: taskFilter})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
nodes, err := client.NodeList(ctx, types.NodeListOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
info = service.GetServicesStatus(services, nodes, tasks)
|
||||
}
|
||||
|
||||
format := options.format
|
||||
if len(format) == 0 {
|
||||
if len(dockerCli.ConfigFile().ServicesFormat) > 0 && !options.quiet {
|
||||
format = dockerCli.ConfigFile().ServicesFormat
|
||||
} else {
|
||||
format = formatter.TableFormatKey
|
||||
}
|
||||
}
|
||||
|
||||
servicesCtx := formatter.Context{
|
||||
Output: dockerCli.Out(),
|
||||
Format: formatter.NewServiceListFormat(format, options.quiet),
|
||||
}
|
||||
return formatter.ServiceListWrite(servicesCtx, services, info)
|
||||
}
|
||||
|
||||
239
components/cli/cli/command/stack/swarm/client_test.go
Normal file
239
components/cli/cli/command/stack/swarm/client_test.go
Normal file
@ -0,0 +1,239 @@
|
||||
package swarm
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli/compose/convert"
|
||||
"github.com/docker/docker/api"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/client"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type fakeClient struct {
|
||||
client.Client
|
||||
|
||||
version string
|
||||
|
||||
services []string
|
||||
networks []string
|
||||
secrets []string
|
||||
configs []string
|
||||
|
||||
removedServices []string
|
||||
removedNetworks []string
|
||||
removedSecrets []string
|
||||
removedConfigs []string
|
||||
|
||||
serviceListFunc func(options types.ServiceListOptions) ([]swarm.Service, error)
|
||||
networkListFunc func(options types.NetworkListOptions) ([]types.NetworkResource, error)
|
||||
secretListFunc func(options types.SecretListOptions) ([]swarm.Secret, error)
|
||||
configListFunc func(options types.ConfigListOptions) ([]swarm.Config, error)
|
||||
nodeListFunc func(options types.NodeListOptions) ([]swarm.Node, error)
|
||||
taskListFunc func(options types.TaskListOptions) ([]swarm.Task, error)
|
||||
nodeInspectWithRaw func(ref string) (swarm.Node, []byte, error)
|
||||
|
||||
serviceUpdateFunc func(serviceID string, version swarm.Version, service swarm.ServiceSpec, options types.ServiceUpdateOptions) (types.ServiceUpdateResponse, error)
|
||||
|
||||
serviceRemoveFunc func(serviceID string) error
|
||||
networkRemoveFunc func(networkID string) error
|
||||
secretRemoveFunc func(secretID string) error
|
||||
configRemoveFunc func(configID string) error
|
||||
}
|
||||
|
||||
func (cli *fakeClient) ServerVersion(ctx context.Context) (types.Version, error) {
|
||||
return types.Version{
|
||||
Version: "docker-dev",
|
||||
APIVersion: api.DefaultVersion,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (cli *fakeClient) ClientVersion() string {
|
||||
return cli.version
|
||||
}
|
||||
|
||||
func (cli *fakeClient) ServiceList(ctx context.Context, options types.ServiceListOptions) ([]swarm.Service, error) {
|
||||
if cli.serviceListFunc != nil {
|
||||
return cli.serviceListFunc(options)
|
||||
}
|
||||
|
||||
namespace := namespaceFromFilters(options.Filters)
|
||||
servicesList := []swarm.Service{}
|
||||
for _, name := range cli.services {
|
||||
if belongToNamespace(name, namespace) {
|
||||
servicesList = append(servicesList, serviceFromName(name))
|
||||
}
|
||||
}
|
||||
return servicesList, nil
|
||||
}
|
||||
|
||||
func (cli *fakeClient) NetworkList(ctx context.Context, options types.NetworkListOptions) ([]types.NetworkResource, error) {
|
||||
if cli.networkListFunc != nil {
|
||||
return cli.networkListFunc(options)
|
||||
}
|
||||
|
||||
namespace := namespaceFromFilters(options.Filters)
|
||||
networksList := []types.NetworkResource{}
|
||||
for _, name := range cli.networks {
|
||||
if belongToNamespace(name, namespace) {
|
||||
networksList = append(networksList, networkFromName(name))
|
||||
}
|
||||
}
|
||||
return networksList, nil
|
||||
}
|
||||
|
||||
func (cli *fakeClient) SecretList(ctx context.Context, options types.SecretListOptions) ([]swarm.Secret, error) {
|
||||
if cli.secretListFunc != nil {
|
||||
return cli.secretListFunc(options)
|
||||
}
|
||||
|
||||
namespace := namespaceFromFilters(options.Filters)
|
||||
secretsList := []swarm.Secret{}
|
||||
for _, name := range cli.secrets {
|
||||
if belongToNamespace(name, namespace) {
|
||||
secretsList = append(secretsList, secretFromName(name))
|
||||
}
|
||||
}
|
||||
return secretsList, nil
|
||||
}
|
||||
|
||||
func (cli *fakeClient) ConfigList(ctx context.Context, options types.ConfigListOptions) ([]swarm.Config, error) {
|
||||
if cli.configListFunc != nil {
|
||||
return cli.configListFunc(options)
|
||||
}
|
||||
|
||||
namespace := namespaceFromFilters(options.Filters)
|
||||
configsList := []swarm.Config{}
|
||||
for _, name := range cli.configs {
|
||||
if belongToNamespace(name, namespace) {
|
||||
configsList = append(configsList, configFromName(name))
|
||||
}
|
||||
}
|
||||
return configsList, nil
|
||||
}
|
||||
|
||||
func (cli *fakeClient) TaskList(ctx context.Context, options types.TaskListOptions) ([]swarm.Task, error) {
|
||||
if cli.taskListFunc != nil {
|
||||
return cli.taskListFunc(options)
|
||||
}
|
||||
return []swarm.Task{}, nil
|
||||
}
|
||||
|
||||
func (cli *fakeClient) NodeList(ctx context.Context, options types.NodeListOptions) ([]swarm.Node, error) {
|
||||
if cli.nodeListFunc != nil {
|
||||
return cli.nodeListFunc(options)
|
||||
}
|
||||
return []swarm.Node{}, nil
|
||||
}
|
||||
|
||||
func (cli *fakeClient) NodeInspectWithRaw(ctx context.Context, ref string) (swarm.Node, []byte, error) {
|
||||
if cli.nodeInspectWithRaw != nil {
|
||||
return cli.nodeInspectWithRaw(ref)
|
||||
}
|
||||
return swarm.Node{}, nil, nil
|
||||
}
|
||||
|
||||
func (cli *fakeClient) ServiceUpdate(ctx context.Context, serviceID string, version swarm.Version, service swarm.ServiceSpec, options types.ServiceUpdateOptions) (types.ServiceUpdateResponse, error) {
|
||||
if cli.serviceUpdateFunc != nil {
|
||||
return cli.serviceUpdateFunc(serviceID, version, service, options)
|
||||
}
|
||||
|
||||
return types.ServiceUpdateResponse{}, nil
|
||||
}
|
||||
|
||||
func (cli *fakeClient) ServiceRemove(ctx context.Context, serviceID string) error {
|
||||
if cli.serviceRemoveFunc != nil {
|
||||
return cli.serviceRemoveFunc(serviceID)
|
||||
}
|
||||
|
||||
cli.removedServices = append(cli.removedServices, serviceID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cli *fakeClient) NetworkRemove(ctx context.Context, networkID string) error {
|
||||
if cli.networkRemoveFunc != nil {
|
||||
return cli.networkRemoveFunc(networkID)
|
||||
}
|
||||
|
||||
cli.removedNetworks = append(cli.removedNetworks, networkID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cli *fakeClient) SecretRemove(ctx context.Context, secretID string) error {
|
||||
if cli.secretRemoveFunc != nil {
|
||||
return cli.secretRemoveFunc(secretID)
|
||||
}
|
||||
|
||||
cli.removedSecrets = append(cli.removedSecrets, secretID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cli *fakeClient) ConfigRemove(ctx context.Context, configID string) error {
|
||||
if cli.configRemoveFunc != nil {
|
||||
return cli.configRemoveFunc(configID)
|
||||
}
|
||||
|
||||
cli.removedConfigs = append(cli.removedConfigs, configID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func serviceFromName(name string) swarm.Service {
|
||||
return swarm.Service{
|
||||
ID: "ID-" + name,
|
||||
Spec: swarm.ServiceSpec{
|
||||
Annotations: swarm.Annotations{Name: name},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func networkFromName(name string) types.NetworkResource {
|
||||
return types.NetworkResource{
|
||||
ID: "ID-" + name,
|
||||
Name: name,
|
||||
}
|
||||
}
|
||||
|
||||
func secretFromName(name string) swarm.Secret {
|
||||
return swarm.Secret{
|
||||
ID: "ID-" + name,
|
||||
Spec: swarm.SecretSpec{
|
||||
Annotations: swarm.Annotations{Name: name},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func configFromName(name string) swarm.Config {
|
||||
return swarm.Config{
|
||||
ID: "ID-" + name,
|
||||
Spec: swarm.ConfigSpec{
|
||||
Annotations: swarm.Annotations{Name: name},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func namespaceFromFilters(filters filters.Args) string {
|
||||
label := filters.Get("label")[0]
|
||||
return strings.TrimPrefix(label, convert.LabelNamespace+"=")
|
||||
}
|
||||
|
||||
func belongToNamespace(id, namespace string) bool {
|
||||
return strings.HasPrefix(id, namespace+"_")
|
||||
}
|
||||
|
||||
func objectName(namespace, name string) string {
|
||||
return namespace + "_" + name
|
||||
}
|
||||
|
||||
func objectID(name string) string {
|
||||
return "ID-" + name
|
||||
}
|
||||
|
||||
func buildObjectIDs(objectNames []string) []string {
|
||||
IDs := make([]string, len(objectNames))
|
||||
for i, name := range objectNames {
|
||||
IDs[i] = objectID(name)
|
||||
}
|
||||
return IDs
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
package stack
|
||||
package swarm
|
||||
|
||||
import (
|
||||
"github.com/docker/cli/cli/compose/convert"
|
||||
88
components/cli/cli/command/stack/swarm/deploy.go
Normal file
88
components/cli/cli/command/stack/swarm/deploy.go
Normal file
@ -0,0 +1,88 @@
|
||||
package swarm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/stack/options"
|
||||
"github.com/docker/cli/cli/compose/convert"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/api/types/versions"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
// Resolve image constants
|
||||
const (
|
||||
defaultNetworkDriver = "overlay"
|
||||
ResolveImageAlways = "always"
|
||||
ResolveImageChanged = "changed"
|
||||
ResolveImageNever = "never"
|
||||
)
|
||||
|
||||
// RunDeploy is the swarm implementation of docker stack deploy
|
||||
func RunDeploy(dockerCli command.Cli, opts options.Deploy) error {
|
||||
ctx := context.Background()
|
||||
|
||||
if err := validateResolveImageFlag(dockerCli, &opts); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch {
|
||||
case opts.Bundlefile == "" && opts.Composefile == "":
|
||||
return errors.Errorf("Please specify either a bundle file (with --bundle-file) or a Compose file (with --compose-file).")
|
||||
case opts.Bundlefile != "" && opts.Composefile != "":
|
||||
return errors.Errorf("You cannot specify both a bundle file and a Compose file.")
|
||||
case opts.Bundlefile != "":
|
||||
return deployBundle(ctx, dockerCli, opts)
|
||||
default:
|
||||
return deployCompose(ctx, dockerCli, opts)
|
||||
}
|
||||
}
|
||||
|
||||
// validateResolveImageFlag validates the opts.resolveImage command line option
|
||||
// and also turns image resolution off if the version is older than 1.30
|
||||
func validateResolveImageFlag(dockerCli command.Cli, opts *options.Deploy) error {
|
||||
if opts.ResolveImage != ResolveImageAlways && opts.ResolveImage != ResolveImageChanged && opts.ResolveImage != ResolveImageNever {
|
||||
return errors.Errorf("Invalid option %s for flag --resolve-image", opts.ResolveImage)
|
||||
}
|
||||
// client side image resolution should not be done when the supported
|
||||
// server version is older than 1.30
|
||||
if versions.LessThan(dockerCli.Client().ClientVersion(), "1.30") {
|
||||
opts.ResolveImage = ResolveImageNever
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkDaemonIsSwarmManager does an Info API call to verify that the daemon is
|
||||
// a swarm manager. This is necessary because we must create networks before we
|
||||
// create services, but the API call for creating a network does not return a
|
||||
// proper status code when it can't create a network in the "global" scope.
|
||||
func checkDaemonIsSwarmManager(ctx context.Context, dockerCli command.Cli) error {
|
||||
info, err := dockerCli.Client().Info(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !info.Swarm.ControlAvailable {
|
||||
return errors.New("this node is not a swarm manager. Use \"docker swarm init\" or \"docker swarm join\" to connect this node to swarm and try again")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// pruneServices removes services that are no longer referenced in the source
|
||||
func pruneServices(ctx context.Context, dockerCli command.Cli, namespace convert.Namespace, services map[string]struct{}) {
|
||||
client := dockerCli.Client()
|
||||
|
||||
oldServices, err := getServices(ctx, client, namespace.Name())
|
||||
if err != nil {
|
||||
fmt.Fprintf(dockerCli.Err(), "Failed to list services: %s", err)
|
||||
}
|
||||
|
||||
pruneServices := []swarm.Service{}
|
||||
for _, service := range oldServices {
|
||||
if _, exists := services[namespace.Descope(service.Spec.Name)]; !exists {
|
||||
pruneServices = append(pruneServices, service)
|
||||
}
|
||||
}
|
||||
removeServices(ctx, dockerCli, pruneServices)
|
||||
}
|
||||
@ -1,16 +1,23 @@
|
||||
package stack
|
||||
package swarm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/bundlefile"
|
||||
"github.com/docker/cli/cli/command/stack/options"
|
||||
"github.com/docker/cli/cli/compose/convert"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func deployBundle(ctx context.Context, dockerCli command.Cli, opts deployOptions) error {
|
||||
bundle, err := loadBundlefile(dockerCli.Err(), opts.namespace, opts.bundlefile)
|
||||
func deployBundle(ctx context.Context, dockerCli command.Cli, opts options.Deploy) error {
|
||||
bundle, err := loadBundlefile(dockerCli.Err(), opts.Namespace, opts.Bundlefile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -19,9 +26,9 @@ func deployBundle(ctx context.Context, dockerCli command.Cli, opts deployOptions
|
||||
return err
|
||||
}
|
||||
|
||||
namespace := convert.NewNamespace(opts.namespace)
|
||||
namespace := convert.NewNamespace(opts.Namespace)
|
||||
|
||||
if opts.prune {
|
||||
if opts.Prune {
|
||||
services := map[string]struct{}{}
|
||||
for service := range bundle.Services {
|
||||
services[service] = struct{}{}
|
||||
@ -87,5 +94,31 @@ func deployBundle(ctx context.Context, dockerCli command.Cli, opts deployOptions
|
||||
if err := createNetworks(ctx, dockerCli, namespace, networks); err != nil {
|
||||
return err
|
||||
}
|
||||
return deployServices(ctx, dockerCli, services, namespace, opts.sendRegistryAuth, opts.resolveImage)
|
||||
return deployServices(ctx, dockerCli, services, namespace, opts.SendRegistryAuth, opts.ResolveImage)
|
||||
}
|
||||
|
||||
func loadBundlefile(stderr io.Writer, namespace string, path string) (*bundlefile.Bundlefile, error) {
|
||||
defaultPath := fmt.Sprintf("%s.dab", namespace)
|
||||
|
||||
if path == "" {
|
||||
path = defaultPath
|
||||
}
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
return nil, errors.Errorf(
|
||||
"Bundle %s not found. Specify the path with --file",
|
||||
path)
|
||||
}
|
||||
|
||||
fmt.Fprintf(stderr, "Loading bundle from %s\n", path)
|
||||
reader, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
bundle, err := bundlefile.LoadFile(reader)
|
||||
if err != nil {
|
||||
return nil, errors.Errorf("Error reading %s: %v\n", path, err)
|
||||
}
|
||||
return bundle, err
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
package stack
|
||||
package swarm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@ -1,4 +1,4 @@
|
||||
package stack
|
||||
package swarm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@ -10,6 +10,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/stack/options"
|
||||
"github.com/docker/cli/cli/compose/convert"
|
||||
"github.com/docker/cli/cli/compose/loader"
|
||||
composetypes "github.com/docker/cli/cli/compose/types"
|
||||
@ -22,8 +23,8 @@ import (
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func deployCompose(ctx context.Context, dockerCli command.Cli, opts deployOptions) error {
|
||||
configDetails, err := getConfigDetails(opts.composefile, dockerCli.In())
|
||||
func deployCompose(ctx context.Context, dockerCli command.Cli, opts options.Deploy) error {
|
||||
configDetails, err := getConfigDetails(opts.Composefile, dockerCli.In())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -54,9 +55,9 @@ func deployCompose(ctx context.Context, dockerCli command.Cli, opts deployOption
|
||||
return err
|
||||
}
|
||||
|
||||
namespace := convert.NewNamespace(opts.namespace)
|
||||
namespace := convert.NewNamespace(opts.Namespace)
|
||||
|
||||
if opts.prune {
|
||||
if opts.Prune {
|
||||
services := map[string]struct{}{}
|
||||
for _, service := range config.Services {
|
||||
services[service.Name] = struct{}{}
|
||||
@ -93,7 +94,7 @@ func deployCompose(ctx context.Context, dockerCli command.Cli, opts deployOption
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return deployServices(ctx, dockerCli, services, namespace, opts.sendRegistryAuth, opts.resolveImage)
|
||||
return deployServices(ctx, dockerCli, services, namespace, opts.SendRegistryAuth, opts.ResolveImage)
|
||||
}
|
||||
|
||||
func getServicesDeclaredNetworks(serviceConfigs []composetypes.ServiceConfig) map[string]struct{} {
|
||||
@ -248,13 +249,13 @@ func createConfigs(
|
||||
case err == nil:
|
||||
// config already exists, then we update that
|
||||
if err := client.ConfigUpdate(ctx, config.ID, config.Meta.Version, configSpec); err != nil {
|
||||
errors.Wrapf(err, "failed to update config %s", configSpec.Name)
|
||||
return errors.Wrapf(err, "failed to update config %s", configSpec.Name)
|
||||
}
|
||||
case apiclient.IsErrNotFound(err):
|
||||
// config does not exist, then we create a new one.
|
||||
fmt.Fprintf(dockerCli.Out(), "Creating config %s\n", configSpec.Name)
|
||||
if _, err := client.ConfigCreate(ctx, configSpec); err != nil {
|
||||
errors.Wrapf(err, "failed to create config %s", configSpec.Name)
|
||||
return errors.Wrapf(err, "failed to create config %s", configSpec.Name)
|
||||
}
|
||||
default:
|
||||
return err
|
||||
@ -339,7 +340,7 @@ func deployServices(
|
||||
updateOpts := types.ServiceUpdateOptions{EncodedRegistryAuth: encodedAuth}
|
||||
|
||||
switch {
|
||||
case resolveImage == resolveImageAlways || (resolveImage == resolveImageChanged && image != service.Spec.Labels[convert.LabelImage]):
|
||||
case resolveImage == ResolveImageAlways || (resolveImage == ResolveImageChanged && image != service.Spec.Labels[convert.LabelImage]):
|
||||
// image should be updated by the server using QueryRegistry
|
||||
updateOpts.QueryRegistry = true
|
||||
case image == service.Spec.Labels[convert.LabelImage]:
|
||||
@ -369,7 +370,7 @@ func deployServices(
|
||||
createOpts := types.ServiceCreateOptions{EncodedRegistryAuth: encodedAuth}
|
||||
|
||||
// query registry if flag disabling it was not set
|
||||
if resolveImage == resolveImageAlways || resolveImage == resolveImageChanged {
|
||||
if resolveImage == ResolveImageAlways || resolveImage == ResolveImageChanged {
|
||||
createOpts.QueryRegistry = true
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package stack
|
||||
package swarm
|
||||
|
||||
import (
|
||||
"os"
|
||||
@ -1,4 +1,4 @@
|
||||
package stack
|
||||
package swarm
|
||||
|
||||
import (
|
||||
"testing"
|
||||
@ -92,7 +92,7 @@ func TestServiceUpdateResolveImageChanged(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
err := deployServices(ctx, client, spec, namespace, false, resolveImageChanged)
|
||||
err := deployServices(ctx, client, spec, namespace, false, ResolveImageChanged)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testcase.expectedQueryRegistry, receivedOptions.QueryRegistry)
|
||||
assert.Equal(t, testcase.expectedImage, receivedService.TaskTemplate.ContainerSpec.Image)
|
||||
74
components/cli/cli/command/stack/swarm/list.go
Normal file
74
components/cli/cli/command/stack/swarm/list.go
Normal file
@ -0,0 +1,74 @@
|
||||
package swarm
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/docker/cli/cli/command/stack/options"
|
||||
"github.com/docker/cli/cli/compose/convert"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/net/context"
|
||||
"vbom.ml/util/sortorder"
|
||||
)
|
||||
|
||||
// RunList is the swarm implementation of docker stack ls
|
||||
func RunList(dockerCli command.Cli, opts options.List) error {
|
||||
client := dockerCli.Client()
|
||||
ctx := context.Background()
|
||||
|
||||
stacks, err := getStacks(ctx, client)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
format := opts.Format
|
||||
if len(format) == 0 {
|
||||
format = formatter.TableFormatKey
|
||||
}
|
||||
stackCtx := formatter.Context{
|
||||
Output: dockerCli.Out(),
|
||||
Format: formatter.NewStackFormat(format),
|
||||
}
|
||||
sort.Sort(byName(stacks))
|
||||
return formatter.StackWrite(stackCtx, stacks)
|
||||
}
|
||||
|
||||
type byName []*formatter.Stack
|
||||
|
||||
func (n byName) Len() int { return len(n) }
|
||||
func (n byName) Swap(i, j int) { n[i], n[j] = n[j], n[i] }
|
||||
func (n byName) Less(i, j int) bool { return sortorder.NaturalLess(n[i].Name, n[j].Name) }
|
||||
|
||||
func getStacks(ctx context.Context, apiclient client.APIClient) ([]*formatter.Stack, error) {
|
||||
services, err := apiclient.ServiceList(
|
||||
ctx,
|
||||
types.ServiceListOptions{Filters: getAllStacksFilter()})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m := make(map[string]*formatter.Stack)
|
||||
for _, service := range services {
|
||||
labels := service.Spec.Labels
|
||||
name, ok := labels[convert.LabelNamespace]
|
||||
if !ok {
|
||||
return nil, errors.Errorf("cannot get label %s for service %s",
|
||||
convert.LabelNamespace, service.ID)
|
||||
}
|
||||
ztack, ok := m[name]
|
||||
if !ok {
|
||||
m[name] = &formatter.Stack{
|
||||
Name: name,
|
||||
Services: 1,
|
||||
}
|
||||
} else {
|
||||
ztack.Services++
|
||||
}
|
||||
}
|
||||
var stacks []*formatter.Stack
|
||||
for _, stack := range m {
|
||||
stacks = append(stacks, stack)
|
||||
}
|
||||
return stacks, nil
|
||||
}
|
||||
37
components/cli/cli/command/stack/swarm/ps.go
Normal file
37
components/cli/cli/command/stack/swarm/ps.go
Normal file
@ -0,0 +1,37 @@
|
||||
package swarm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/idresolver"
|
||||
"github.com/docker/cli/cli/command/stack/options"
|
||||
"github.com/docker/cli/cli/command/task"
|
||||
"github.com/docker/docker/api/types"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
// RunPS is the swarm implementation of docker stack ps
|
||||
func RunPS(dockerCli command.Cli, opts options.PS) error {
|
||||
namespace := opts.Namespace
|
||||
client := dockerCli.Client()
|
||||
ctx := context.Background()
|
||||
|
||||
filter := getStackFilterFromOpt(opts.Namespace, opts.Filter)
|
||||
|
||||
tasks, err := client.TaskList(ctx, types.TaskListOptions{Filters: filter})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(tasks) == 0 {
|
||||
return fmt.Errorf("nothing found in stack: %s", namespace)
|
||||
}
|
||||
|
||||
format := opts.Format
|
||||
if len(format) == 0 {
|
||||
format = task.DefaultFormat(dockerCli.ConfigFile(), opts.Quiet)
|
||||
}
|
||||
|
||||
return task.Print(ctx, dockerCli, tasks, idresolver.New(client, opts.NoResolve), !opts.NoTrunc, opts.Quiet, format)
|
||||
}
|
||||
141
components/cli/cli/command/stack/swarm/remove.go
Normal file
141
components/cli/cli/command/stack/swarm/remove.go
Normal file
@ -0,0 +1,141 @@
|
||||
package swarm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/stack/options"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/api/types/versions"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
// RunRemove is the swarm implementation of docker stack remove
|
||||
func RunRemove(dockerCli command.Cli, opts options.Remove) error {
|
||||
namespaces := opts.Namespaces
|
||||
client := dockerCli.Client()
|
||||
ctx := context.Background()
|
||||
|
||||
var errs []string
|
||||
for _, namespace := range namespaces {
|
||||
services, err := getServices(ctx, client, namespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
networks, err := getStackNetworks(ctx, client, namespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var secrets []swarm.Secret
|
||||
if versions.GreaterThanOrEqualTo(client.ClientVersion(), "1.25") {
|
||||
secrets, err = getStackSecrets(ctx, client, namespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var configs []swarm.Config
|
||||
if versions.GreaterThanOrEqualTo(client.ClientVersion(), "1.30") {
|
||||
configs, err = getStackConfigs(ctx, client, namespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(services)+len(networks)+len(secrets)+len(configs) == 0 {
|
||||
fmt.Fprintf(dockerCli.Err(), "Nothing found in stack: %s\n", namespace)
|
||||
continue
|
||||
}
|
||||
|
||||
hasError := removeServices(ctx, dockerCli, services)
|
||||
hasError = removeSecrets(ctx, dockerCli, secrets) || hasError
|
||||
hasError = removeConfigs(ctx, dockerCli, configs) || hasError
|
||||
hasError = removeNetworks(ctx, dockerCli, networks) || hasError
|
||||
|
||||
if hasError {
|
||||
errs = append(errs, fmt.Sprintf("Failed to remove some resources from stack: %s", namespace))
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return errors.Errorf(strings.Join(errs, "\n"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func sortServiceByName(services []swarm.Service) func(i, j int) bool {
|
||||
return func(i, j int) bool {
|
||||
return services[i].Spec.Name < services[j].Spec.Name
|
||||
}
|
||||
}
|
||||
|
||||
func removeServices(
|
||||
ctx context.Context,
|
||||
dockerCli command.Cli,
|
||||
services []swarm.Service,
|
||||
) bool {
|
||||
var hasError bool
|
||||
sort.Slice(services, sortServiceByName(services))
|
||||
for _, service := range services {
|
||||
fmt.Fprintf(dockerCli.Out(), "Removing service %s\n", service.Spec.Name)
|
||||
if err := dockerCli.Client().ServiceRemove(ctx, service.ID); err != nil {
|
||||
hasError = true
|
||||
fmt.Fprintf(dockerCli.Err(), "Failed to remove service %s: %s", service.ID, err)
|
||||
}
|
||||
}
|
||||
return hasError
|
||||
}
|
||||
|
||||
func removeNetworks(
|
||||
ctx context.Context,
|
||||
dockerCli command.Cli,
|
||||
networks []types.NetworkResource,
|
||||
) bool {
|
||||
var hasError bool
|
||||
for _, network := range networks {
|
||||
fmt.Fprintf(dockerCli.Out(), "Removing network %s\n", network.Name)
|
||||
if err := dockerCli.Client().NetworkRemove(ctx, network.ID); err != nil {
|
||||
hasError = true
|
||||
fmt.Fprintf(dockerCli.Err(), "Failed to remove network %s: %s", network.ID, err)
|
||||
}
|
||||
}
|
||||
return hasError
|
||||
}
|
||||
|
||||
func removeSecrets(
|
||||
ctx context.Context,
|
||||
dockerCli command.Cli,
|
||||
secrets []swarm.Secret,
|
||||
) bool {
|
||||
var hasError bool
|
||||
for _, secret := range secrets {
|
||||
fmt.Fprintf(dockerCli.Out(), "Removing secret %s\n", secret.Spec.Name)
|
||||
if err := dockerCli.Client().SecretRemove(ctx, secret.ID); err != nil {
|
||||
hasError = true
|
||||
fmt.Fprintf(dockerCli.Err(), "Failed to remove secret %s: %s", secret.ID, err)
|
||||
}
|
||||
}
|
||||
return hasError
|
||||
}
|
||||
|
||||
func removeConfigs(
|
||||
ctx context.Context,
|
||||
dockerCli command.Cli,
|
||||
configs []swarm.Config,
|
||||
) bool {
|
||||
var hasError bool
|
||||
for _, config := range configs {
|
||||
fmt.Fprintf(dockerCli.Out(), "Removing config %s\n", config.Spec.Name)
|
||||
if err := dockerCli.Client().ConfigRemove(ctx, config.ID); err != nil {
|
||||
hasError = true
|
||||
fmt.Fprintf(dockerCli.Err(), "Failed to remove config %s: %s", config.ID, err)
|
||||
}
|
||||
}
|
||||
return hasError
|
||||
}
|
||||
66
components/cli/cli/command/stack/swarm/services.go
Normal file
66
components/cli/cli/command/stack/swarm/services.go
Normal file
@ -0,0 +1,66 @@
|
||||
package swarm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/docker/cli/cli/command/service"
|
||||
"github.com/docker/cli/cli/command/stack/options"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
// RunServices is the swarm implementation of docker stack services
|
||||
func RunServices(dockerCli command.Cli, opts options.Services) error {
|
||||
ctx := context.Background()
|
||||
client := dockerCli.Client()
|
||||
|
||||
filter := getStackFilterFromOpt(opts.Namespace, opts.Filter)
|
||||
services, err := client.ServiceList(ctx, types.ServiceListOptions{Filters: filter})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// if no services in this stack, print message and exit 0
|
||||
if len(services) == 0 {
|
||||
fmt.Fprintf(dockerCli.Err(), "Nothing found in stack: %s\n", opts.Namespace)
|
||||
return nil
|
||||
}
|
||||
|
||||
info := map[string]formatter.ServiceListInfo{}
|
||||
if !opts.Quiet {
|
||||
taskFilter := filters.NewArgs()
|
||||
for _, service := range services {
|
||||
taskFilter.Add("service", service.ID)
|
||||
}
|
||||
|
||||
tasks, err := client.TaskList(ctx, types.TaskListOptions{Filters: taskFilter})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
nodes, err := client.NodeList(ctx, types.NodeListOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
info = service.GetServicesStatus(services, nodes, tasks)
|
||||
}
|
||||
|
||||
format := opts.Format
|
||||
if len(format) == 0 {
|
||||
if len(dockerCli.ConfigFile().ServicesFormat) > 0 && !opts.Quiet {
|
||||
format = dockerCli.ConfigFile().ServicesFormat
|
||||
} else {
|
||||
format = formatter.TableFormatKey
|
||||
}
|
||||
}
|
||||
|
||||
servicesCtx := formatter.Context{
|
||||
Output: dockerCli.Out(),
|
||||
Format: formatter.NewServiceListFormat(format, opts.Quiet),
|
||||
}
|
||||
return formatter.ServiceListWrite(servicesCtx, services, info)
|
||||
}
|
||||
@ -10,11 +10,14 @@ import (
|
||||
// NewSwarmCommand returns a cobra command for `swarm` subcommands
|
||||
func NewSwarmCommand(dockerCli command.Cli) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "swarm",
|
||||
Short: "Manage Swarm",
|
||||
Args: cli.NoArgs,
|
||||
RunE: command.ShowHelp(dockerCli.Err()),
|
||||
Annotations: map[string]string{"version": "1.24"},
|
||||
Use: "swarm",
|
||||
Short: "Manage Swarm",
|
||||
Args: cli.NoArgs,
|
||||
RunE: command.ShowHelp(dockerCli.Err()),
|
||||
Annotations: map[string]string{
|
||||
"version": "1.24",
|
||||
"swarm": "",
|
||||
},
|
||||
}
|
||||
cmd.AddCommand(
|
||||
newInitCommand(dockerCli),
|
||||
|
||||
@ -1,13 +1,20 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/client"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type fakeClient struct {
|
||||
client.Client
|
||||
|
||||
version string
|
||||
version string
|
||||
serverVersion func(ctx context.Context) (types.Version, error)
|
||||
}
|
||||
|
||||
func (cli *fakeClient) ServerVersion(ctx context.Context) (types.Version, error) {
|
||||
return cli.serverVersion(ctx)
|
||||
}
|
||||
|
||||
func (cli *fakeClient) ClientVersion() string {
|
||||
|
||||
@ -23,6 +23,8 @@ Client:{{if ne .Platform.Name ""}} {{.Platform.Name}}{{end}}
|
||||
Git commit: {{.GitCommit}}
|
||||
Built: {{.BuildTime}}
|
||||
OS/Arch: {{.Os}}/{{.Arch}}
|
||||
Experimental: {{.Experimental}}
|
||||
Orchestrator: {{.Orchestrator}}
|
||||
{{- end}}
|
||||
|
||||
{{- if .ServerOK}}{{with .Server}}
|
||||
@ -69,6 +71,8 @@ type clientVersion struct {
|
||||
Os string
|
||||
Arch string
|
||||
BuildTime string `json:",omitempty"`
|
||||
Experimental bool
|
||||
Orchestrator string `json:",omitempty"`
|
||||
}
|
||||
|
||||
// ServerOK returns true when the client could connect to the docker server
|
||||
@ -78,7 +82,7 @@ func (v versionInfo) ServerOK() bool {
|
||||
}
|
||||
|
||||
// NewVersionCommand creates a new cobra.Command for `docker version`
|
||||
func NewVersionCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
func NewVersionCommand(dockerCli command.Cli) *cobra.Command {
|
||||
var opts versionOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
@ -105,9 +109,7 @@ func reformatDate(buildTime string) string {
|
||||
return buildTime
|
||||
}
|
||||
|
||||
func runVersion(dockerCli *command.DockerCli, opts *versionOptions) error {
|
||||
ctx := context.Background()
|
||||
|
||||
func runVersion(dockerCli command.Cli, opts *versionOptions) error {
|
||||
templateFormat := versionTemplate
|
||||
tmpl := templates.New("version")
|
||||
if opts.format != "" {
|
||||
@ -125,22 +127,21 @@ func runVersion(dockerCli *command.DockerCli, opts *versionOptions) error {
|
||||
|
||||
vd := versionInfo{
|
||||
Client: clientVersion{
|
||||
Platform: struct{ Name string }{cli.PlatformName},
|
||||
Version: cli.Version,
|
||||
APIVersion: dockerCli.Client().ClientVersion(),
|
||||
DefaultAPIVersion: dockerCli.DefaultVersion(),
|
||||
GoVersion: runtime.Version(),
|
||||
GitCommit: cli.GitCommit,
|
||||
BuildTime: cli.BuildTime,
|
||||
BuildTime: reformatDate(cli.BuildTime),
|
||||
Os: runtime.GOOS,
|
||||
Arch: runtime.GOARCH,
|
||||
Experimental: dockerCli.ClientInfo().HasExperimental,
|
||||
Orchestrator: string(dockerCli.ClientInfo().Orchestrator),
|
||||
},
|
||||
}
|
||||
vd.Client.Platform.Name = cli.PlatformName
|
||||
|
||||
// first we need to make BuildTime more human friendly
|
||||
vd.Client.BuildTime = reformatDate(vd.Client.BuildTime)
|
||||
|
||||
sv, err := dockerCli.Client().ServerVersion(ctx)
|
||||
sv, err := dockerCli.Client().ServerVersion(context.Background())
|
||||
if err == nil {
|
||||
vd.Server = &sv
|
||||
foundEngine := false
|
||||
|
||||
47
components/cli/cli/command/system/version_test.go
Normal file
47
components/cli/cli/command/system/version_test.go
Normal file
@ -0,0 +1,47 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/docker/api"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func TestVersionWithoutServer(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
serverVersion: func(ctx context.Context) (types.Version, error) {
|
||||
return types.Version{}, fmt.Errorf("no server")
|
||||
},
|
||||
})
|
||||
cmd := NewVersionCommand(cli)
|
||||
cmd.SetOutput(cli.Err())
|
||||
assert.Error(t, cmd.Execute())
|
||||
assert.Contains(t, cleanTabs(cli.OutBuffer().String()), "Client:")
|
||||
assert.NotContains(t, cleanTabs(cli.OutBuffer().String()), "Server:")
|
||||
}
|
||||
|
||||
func fakeServerVersion(ctx context.Context) (types.Version, error) {
|
||||
return types.Version{
|
||||
Version: "docker-dev",
|
||||
APIVersion: api.DefaultVersion,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func TestVersionWithOrchestrator(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{serverVersion: fakeServerVersion})
|
||||
cli.SetClientInfo(func() command.ClientInfo { return command.ClientInfo{Orchestrator: "swarm"} })
|
||||
cmd := NewVersionCommand(cli)
|
||||
assert.NoError(t, cmd.Execute())
|
||||
assert.Contains(t, cleanTabs(cli.OutBuffer().String()), "Orchestrator: swarm")
|
||||
}
|
||||
|
||||
func cleanTabs(line string) string {
|
||||
return strings.Join(strings.Fields(line), " ")
|
||||
}
|
||||
@ -9,10 +9,11 @@ import (
|
||||
// NewTrustCommand returns a cobra command for `trust` subcommands
|
||||
func NewTrustCommand(dockerCli command.Cli) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "trust",
|
||||
Short: "Manage trust on Docker images (experimental)",
|
||||
Args: cli.NoArgs,
|
||||
RunE: command.ShowHelp(dockerCli.Err()),
|
||||
Use: "trust",
|
||||
Short: "Manage trust on Docker images (experimental)",
|
||||
Args: cli.NoArgs,
|
||||
RunE: command.ShowHelp(dockerCli.Err()),
|
||||
Annotations: map[string]string{"experimentalCLI": ""},
|
||||
}
|
||||
cmd.AddCommand(
|
||||
newViewCommand(dockerCli),
|
||||
|
||||
@ -243,8 +243,5 @@ func addStagedSigner(notaryRepo client.Repository, newSigner data.RoleName, sign
|
||||
if err := notaryRepo.AddDelegationRoleAndKeys(trust.ReleasesRole, signerKeys); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := notaryRepo.AddDelegationPaths(trust.ReleasesRole, []string{""}); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return notaryRepo.AddDelegationPaths(trust.ReleasesRole, []string{""})
|
||||
}
|
||||
|
||||
@ -131,8 +131,5 @@ func removeSingleSigner(cli command.Cli, repoName, signerName string, forceYes b
|
||||
if err = notaryRepo.RemoveDelegationRole(signerDelegation); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = notaryRepo.Publish(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return notaryRepo.Publish()
|
||||
}
|
||||
|
||||
@ -22,43 +22,37 @@ func Volumes(serviceVolumes []composetypes.ServiceVolumeConfig, stackVolumes vol
|
||||
return mounts, nil
|
||||
}
|
||||
|
||||
func convertVolumeToMount(
|
||||
func createMountFromVolume(volume composetypes.ServiceVolumeConfig) mount.Mount {
|
||||
return mount.Mount{
|
||||
Type: mount.Type(volume.Type),
|
||||
Target: volume.Target,
|
||||
ReadOnly: volume.ReadOnly,
|
||||
Source: volume.Source,
|
||||
Consistency: mount.Consistency(volume.Consistency),
|
||||
}
|
||||
}
|
||||
|
||||
func handleVolumeToMount(
|
||||
volume composetypes.ServiceVolumeConfig,
|
||||
stackVolumes volumes,
|
||||
namespace Namespace,
|
||||
) (mount.Mount, error) {
|
||||
result := mount.Mount{
|
||||
Type: mount.Type(volume.Type),
|
||||
Source: volume.Source,
|
||||
Target: volume.Target,
|
||||
ReadOnly: volume.ReadOnly,
|
||||
Consistency: mount.Consistency(volume.Consistency),
|
||||
}
|
||||
result := createMountFromVolume(volume)
|
||||
|
||||
if volume.Tmpfs != nil {
|
||||
return mount.Mount{}, errors.New("tmpfs options are incompatible with type volume")
|
||||
}
|
||||
if volume.Bind != nil {
|
||||
return mount.Mount{}, errors.New("bind options are incompatible with type volume")
|
||||
}
|
||||
// Anonymous volumes
|
||||
if volume.Source == "" {
|
||||
return result, nil
|
||||
}
|
||||
if volume.Type == "volume" && volume.Bind != nil {
|
||||
return result, errors.New("bind options are incompatible with type volume")
|
||||
}
|
||||
if volume.Type == "bind" && volume.Volume != nil {
|
||||
return result, errors.New("volume options are incompatible with type bind")
|
||||
}
|
||||
|
||||
if volume.Bind != nil {
|
||||
result.BindOptions = &mount.BindOptions{
|
||||
Propagation: mount.Propagation(volume.Bind.Propagation),
|
||||
}
|
||||
}
|
||||
// Binds volumes
|
||||
if volume.Type == "bind" {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
stackVolume, exists := stackVolumes[volume.Source]
|
||||
if !exists {
|
||||
return result, errors.Errorf("undefined volume %q", volume.Source)
|
||||
return mount.Mount{}, errors.Errorf("undefined volume %q", volume.Source)
|
||||
}
|
||||
|
||||
result.Source = namespace.Scope(volume.Source)
|
||||
@ -85,6 +79,62 @@ func convertVolumeToMount(
|
||||
}
|
||||
}
|
||||
|
||||
// Named volumes
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func handleBindToMount(volume composetypes.ServiceVolumeConfig) (mount.Mount, error) {
|
||||
result := createMountFromVolume(volume)
|
||||
|
||||
if volume.Source == "" {
|
||||
return mount.Mount{}, errors.New("invalid bind source, source cannot be empty")
|
||||
}
|
||||
if volume.Volume != nil {
|
||||
return mount.Mount{}, errors.New("volume options are incompatible with type bind")
|
||||
}
|
||||
if volume.Tmpfs != nil {
|
||||
return mount.Mount{}, errors.New("tmpfs options are incompatible with type bind")
|
||||
}
|
||||
if volume.Bind != nil {
|
||||
result.BindOptions = &mount.BindOptions{
|
||||
Propagation: mount.Propagation(volume.Bind.Propagation),
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func handleTmpfsToMount(volume composetypes.ServiceVolumeConfig) (mount.Mount, error) {
|
||||
result := createMountFromVolume(volume)
|
||||
|
||||
if volume.Source != "" {
|
||||
return mount.Mount{}, errors.New("invalid tmpfs source, source must be empty")
|
||||
}
|
||||
if volume.Bind != nil {
|
||||
return mount.Mount{}, errors.New("bind options are incompatible with type tmpfs")
|
||||
}
|
||||
if volume.Volume != nil {
|
||||
return mount.Mount{}, errors.New("volume options are incompatible with type tmpfs")
|
||||
}
|
||||
if volume.Tmpfs != nil {
|
||||
result.TmpfsOptions = &mount.TmpfsOptions{
|
||||
SizeBytes: volume.Tmpfs.Size,
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func convertVolumeToMount(
|
||||
volume composetypes.ServiceVolumeConfig,
|
||||
stackVolumes volumes,
|
||||
namespace Namespace,
|
||||
) (mount.Mount, error) {
|
||||
|
||||
switch volume.Type {
|
||||
case "volume", "":
|
||||
return handleVolumeToMount(volume, stackVolumes, namespace)
|
||||
case "bind":
|
||||
return handleBindToMount(volume)
|
||||
case "tmpfs":
|
||||
return handleTmpfsToMount(volume)
|
||||
}
|
||||
return mount.Mount{}, errors.New("volume type must be volume, bind, or tmpfs")
|
||||
}
|
||||
|
||||
@ -22,7 +22,28 @@ func TestConvertVolumeToMountAnonymousVolume(t *testing.T) {
|
||||
assert.Equal(t, expected, mount)
|
||||
}
|
||||
|
||||
func TestConvertVolumeToMountConflictingOptionsBind(t *testing.T) {
|
||||
func TestConvertVolumeToMountAnonymousBind(t *testing.T) {
|
||||
config := composetypes.ServiceVolumeConfig{
|
||||
Type: "bind",
|
||||
Target: "/foo/bar",
|
||||
Bind: &composetypes.ServiceVolumeBind{
|
||||
Propagation: "slave",
|
||||
},
|
||||
}
|
||||
_, err := convertVolumeToMount(config, volumes{}, NewNamespace("foo"))
|
||||
assert.EqualError(t, err, "invalid bind source, source cannot be empty")
|
||||
}
|
||||
|
||||
func TestConvertVolumeToMountUnapprovedType(t *testing.T) {
|
||||
config := composetypes.ServiceVolumeConfig{
|
||||
Type: "foo",
|
||||
Target: "/foo/bar",
|
||||
}
|
||||
_, err := convertVolumeToMount(config, volumes{}, NewNamespace("foo"))
|
||||
assert.EqualError(t, err, "volume type must be volume, bind, or tmpfs")
|
||||
}
|
||||
|
||||
func TestConvertVolumeToMountConflictingOptionsBindInVolume(t *testing.T) {
|
||||
namespace := NewNamespace("foo")
|
||||
|
||||
config := composetypes.ServiceVolumeConfig{
|
||||
@ -37,7 +58,22 @@ func TestConvertVolumeToMountConflictingOptionsBind(t *testing.T) {
|
||||
assert.EqualError(t, err, "bind options are incompatible with type volume")
|
||||
}
|
||||
|
||||
func TestConvertVolumeToMountConflictingOptionsVolume(t *testing.T) {
|
||||
func TestConvertVolumeToMountConflictingOptionsTmpfsInVolume(t *testing.T) {
|
||||
namespace := NewNamespace("foo")
|
||||
|
||||
config := composetypes.ServiceVolumeConfig{
|
||||
Type: "volume",
|
||||
Source: "foo",
|
||||
Target: "/target",
|
||||
Tmpfs: &composetypes.ServiceVolumeTmpfs{
|
||||
Size: 1000,
|
||||
},
|
||||
}
|
||||
_, err := convertVolumeToMount(config, volumes{}, namespace)
|
||||
assert.EqualError(t, err, "tmpfs options are incompatible with type volume")
|
||||
}
|
||||
|
||||
func TestConvertVolumeToMountConflictingOptionsVolumeInBind(t *testing.T) {
|
||||
namespace := NewNamespace("foo")
|
||||
|
||||
config := composetypes.ServiceVolumeConfig{
|
||||
@ -52,6 +88,49 @@ func TestConvertVolumeToMountConflictingOptionsVolume(t *testing.T) {
|
||||
assert.EqualError(t, err, "volume options are incompatible with type bind")
|
||||
}
|
||||
|
||||
func TestConvertVolumeToMountConflictingOptionsTmpfsInBind(t *testing.T) {
|
||||
namespace := NewNamespace("foo")
|
||||
|
||||
config := composetypes.ServiceVolumeConfig{
|
||||
Type: "bind",
|
||||
Source: "/foo",
|
||||
Target: "/target",
|
||||
Tmpfs: &composetypes.ServiceVolumeTmpfs{
|
||||
Size: 1000,
|
||||
},
|
||||
}
|
||||
_, err := convertVolumeToMount(config, volumes{}, namespace)
|
||||
assert.EqualError(t, err, "tmpfs options are incompatible with type bind")
|
||||
}
|
||||
|
||||
func TestConvertVolumeToMountConflictingOptionsBindInTmpfs(t *testing.T) {
|
||||
namespace := NewNamespace("foo")
|
||||
|
||||
config := composetypes.ServiceVolumeConfig{
|
||||
Type: "tmpfs",
|
||||
Target: "/target",
|
||||
Bind: &composetypes.ServiceVolumeBind{
|
||||
Propagation: "slave",
|
||||
},
|
||||
}
|
||||
_, err := convertVolumeToMount(config, volumes{}, namespace)
|
||||
assert.EqualError(t, err, "bind options are incompatible with type tmpfs")
|
||||
}
|
||||
|
||||
func TestConvertVolumeToMountConflictingOptionsVolumeInTmpfs(t *testing.T) {
|
||||
namespace := NewNamespace("foo")
|
||||
|
||||
config := composetypes.ServiceVolumeConfig{
|
||||
Type: "tmpfs",
|
||||
Target: "/target",
|
||||
Volume: &composetypes.ServiceVolumeVolume{
|
||||
NoCopy: true,
|
||||
},
|
||||
}
|
||||
_, err := convertVolumeToMount(config, volumes{}, namespace)
|
||||
assert.EqualError(t, err, "volume options are incompatible with type tmpfs")
|
||||
}
|
||||
|
||||
func TestConvertVolumeToMountNamedVolume(t *testing.T) {
|
||||
stackVolumes := volumes{
|
||||
"normal": composetypes.VolumeConfig{
|
||||
@ -231,3 +310,35 @@ func TestConvertVolumeToMountVolumeDoesNotExist(t *testing.T) {
|
||||
_, err := convertVolumeToMount(config, volumes{}, namespace)
|
||||
assert.EqualError(t, err, "undefined volume \"unknown\"")
|
||||
}
|
||||
|
||||
func TestConvertTmpfsToMountVolume(t *testing.T) {
|
||||
config := composetypes.ServiceVolumeConfig{
|
||||
Type: "tmpfs",
|
||||
Target: "/foo/bar",
|
||||
Tmpfs: &composetypes.ServiceVolumeTmpfs{
|
||||
Size: 1000,
|
||||
},
|
||||
}
|
||||
expected := mount.Mount{
|
||||
Type: mount.TypeTmpfs,
|
||||
Target: "/foo/bar",
|
||||
TmpfsOptions: &mount.TmpfsOptions{SizeBytes: 1000},
|
||||
}
|
||||
mount, err := convertVolumeToMount(config, volumes{}, NewNamespace("foo"))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expected, mount)
|
||||
}
|
||||
|
||||
func TestConvertTmpfsToMountVolumeWithSource(t *testing.T) {
|
||||
config := composetypes.ServiceVolumeConfig{
|
||||
Type: "tmpfs",
|
||||
Source: "/bar",
|
||||
Target: "/foo/bar",
|
||||
Tmpfs: &composetypes.ServiceVolumeTmpfs{
|
||||
Size: 1000,
|
||||
},
|
||||
}
|
||||
|
||||
_, err := convertVolumeToMount(config, volumes{}, NewNamespace("foo"))
|
||||
assert.EqualError(t, err, "invalid tmpfs source, source must be empty")
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
version: "3.5"
|
||||
version: "3.6"
|
||||
|
||||
services:
|
||||
foo:
|
||||
@ -10,7 +10,7 @@ services:
|
||||
foo: bar
|
||||
target: foo
|
||||
network: foo
|
||||
cache_from:
|
||||
cache_from:
|
||||
- foo
|
||||
- bar
|
||||
labels: [FOO=BAR]
|
||||
@ -249,6 +249,10 @@ services:
|
||||
source: ./opt
|
||||
target: /opt
|
||||
consistency: cached
|
||||
- type: tmpfs
|
||||
target: /opt
|
||||
tmpfs:
|
||||
size: 10000
|
||||
|
||||
working_dir: /code
|
||||
|
||||
|
||||
@ -657,6 +657,7 @@ services:
|
||||
context: ./web
|
||||
links:
|
||||
- bar
|
||||
pid: host
|
||||
db:
|
||||
image: db
|
||||
build:
|
||||
@ -670,7 +671,7 @@ services:
|
||||
require.NoError(t, err)
|
||||
|
||||
unsupported := GetUnsupportedProperties(configDetails)
|
||||
assert.Equal(t, []string{"build", "links"}, unsupported)
|
||||
assert.Equal(t, []string{"build", "links", "pid"}, unsupported)
|
||||
}
|
||||
|
||||
func TestBuildProperties(t *testing.T) {
|
||||
@ -1148,6 +1149,9 @@ func TestFullExample(t *testing.T) {
|
||||
{Source: homeDir + "/configs", Target: "/etc/configs/", Type: "bind", ReadOnly: true},
|
||||
{Source: "datavolume", Target: "/var/lib/mysql", Type: "volume"},
|
||||
{Source: workingDir + "/opt", Target: "/opt", Consistency: "cached", Type: "bind"},
|
||||
{Target: "/opt", Type: "tmpfs", Tmpfs: &types.ServiceVolumeTmpfs{
|
||||
Size: int64(10000),
|
||||
}},
|
||||
},
|
||||
WorkingDir: "/code",
|
||||
}
|
||||
@ -1219,6 +1223,106 @@ func TestFullExample(t *testing.T) {
|
||||
assert.Equal(t, expectedVolumeConfig, config.Volumes)
|
||||
}
|
||||
|
||||
func TestLoadTmpfsVolume(t *testing.T) {
|
||||
config, err := loadYAML(`
|
||||
version: "3.6"
|
||||
services:
|
||||
tmpfs:
|
||||
image: nginx:latest
|
||||
volumes:
|
||||
- type: tmpfs
|
||||
target: /app
|
||||
tmpfs:
|
||||
size: 10000
|
||||
`)
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := types.ServiceVolumeConfig{
|
||||
Target: "/app",
|
||||
Type: "tmpfs",
|
||||
Tmpfs: &types.ServiceVolumeTmpfs{
|
||||
Size: int64(10000),
|
||||
},
|
||||
}
|
||||
|
||||
require.Len(t, config.Services, 1)
|
||||
assert.Len(t, config.Services[0].Volumes, 1)
|
||||
assert.Equal(t, expected, config.Services[0].Volumes[0])
|
||||
}
|
||||
|
||||
func TestLoadTmpfsVolumeAdditionalPropertyNotAllowed(t *testing.T) {
|
||||
_, err := loadYAML(`
|
||||
version: "3.5"
|
||||
services:
|
||||
tmpfs:
|
||||
image: nginx:latest
|
||||
volumes:
|
||||
- type: tmpfs
|
||||
target: /app
|
||||
tmpfs:
|
||||
size: 10000
|
||||
`)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "services.tmpfs.volumes.0 Additional property tmpfs is not allowed")
|
||||
}
|
||||
|
||||
func TestLoadTmpfsVolumeSizeCanBeZero(t *testing.T) {
|
||||
config, err := loadYAML(`
|
||||
version: "3.6"
|
||||
services:
|
||||
tmpfs:
|
||||
image: nginx:latest
|
||||
volumes:
|
||||
- type: tmpfs
|
||||
target: /app
|
||||
tmpfs:
|
||||
size: 0
|
||||
`)
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := types.ServiceVolumeConfig{
|
||||
Target: "/app",
|
||||
Type: "tmpfs",
|
||||
Tmpfs: &types.ServiceVolumeTmpfs{},
|
||||
}
|
||||
|
||||
require.Len(t, config.Services, 1)
|
||||
assert.Len(t, config.Services[0].Volumes, 1)
|
||||
assert.Equal(t, expected, config.Services[0].Volumes[0])
|
||||
}
|
||||
|
||||
func TestLoadTmpfsVolumeSizeMustBeGTEQZero(t *testing.T) {
|
||||
_, err := loadYAML(`
|
||||
version: "3.6"
|
||||
services:
|
||||
tmpfs:
|
||||
image: nginx:latest
|
||||
volumes:
|
||||
- type: tmpfs
|
||||
target: /app
|
||||
tmpfs:
|
||||
size: -1
|
||||
`)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "services.tmpfs.volumes.0.tmpfs.size Must be greater than or equal to 0")
|
||||
}
|
||||
|
||||
func TestLoadTmpfsVolumeSizeMustBeInteger(t *testing.T) {
|
||||
_, err := loadYAML(`
|
||||
version: "3.6"
|
||||
services:
|
||||
tmpfs:
|
||||
image: nginx:latest
|
||||
volumes:
|
||||
- type: tmpfs
|
||||
target: /app
|
||||
tmpfs:
|
||||
size: 0.0001
|
||||
`)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "services.tmpfs.volumes.0.tmpfs.size must be a integer")
|
||||
}
|
||||
|
||||
func serviceSort(services []types.ServiceConfig) []types.ServiceConfig {
|
||||
sort.Sort(servicesByName(services))
|
||||
return services
|
||||
|
||||
File diff suppressed because one or more lines are too long
582
components/cli/cli/compose/schema/data/config_schema_v3.6.json
Normal file
582
components/cli/cli/compose/schema/data/config_schema_v3.6.json
Normal file
@ -0,0 +1,582 @@
|
||||
{
|
||||
"$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"}
|
||||
}
|
||||
},
|
||||
"tmpfs": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"size": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -17,12 +17,12 @@ var UnsupportedProperties = []string{
|
||||
"links",
|
||||
"mac_address",
|
||||
"network_mode",
|
||||
"pid",
|
||||
"privileged",
|
||||
"restart",
|
||||
"security_opt",
|
||||
"shm_size",
|
||||
"sysctls",
|
||||
"tmpfs",
|
||||
"ulimits",
|
||||
"userns_mode",
|
||||
}
|
||||
@ -283,6 +283,7 @@ type ServiceVolumeConfig struct {
|
||||
Consistency string
|
||||
Bind *ServiceVolumeBind
|
||||
Volume *ServiceVolumeVolume
|
||||
Tmpfs *ServiceVolumeTmpfs
|
||||
}
|
||||
|
||||
// ServiceVolumeBind are options for a service volume of type bind
|
||||
@ -295,6 +296,11 @@ type ServiceVolumeVolume struct {
|
||||
NoCopy bool `mapstructure:"nocopy"`
|
||||
}
|
||||
|
||||
// ServiceVolumeTmpfs are options for a service volume of type tmpfs
|
||||
type ServiceVolumeTmpfs struct {
|
||||
Size int64
|
||||
}
|
||||
|
||||
// FileReferenceConfig for a reference to a swarm file object
|
||||
type FileReferenceConfig struct {
|
||||
Source string
|
||||
|
||||
@ -44,6 +44,8 @@ type ConfigFile struct {
|
||||
NodesFormat string `json:"nodesFormat,omitempty"`
|
||||
PruneFilters []string `json:"pruneFilters,omitempty"`
|
||||
Proxies map[string]ProxyConfig `json:"proxies,omitempty"`
|
||||
Experimental string `json:"experimental,omitempty"`
|
||||
Orchestrator string `json:"orchestrator,omitempty"`
|
||||
}
|
||||
|
||||
// ProxyConfig contains proxy configuration settings
|
||||
|
||||
@ -30,12 +30,13 @@ var (
|
||||
|
||||
// CommonOptions are options common to both the client and the daemon.
|
||||
type CommonOptions struct {
|
||||
Debug bool
|
||||
Hosts []string
|
||||
LogLevel string
|
||||
TLS bool
|
||||
TLSVerify bool
|
||||
TLSOptions *tlsconfig.Options
|
||||
Debug bool
|
||||
Hosts []string
|
||||
Orchestrator string
|
||||
LogLevel string
|
||||
TLS bool
|
||||
TLSVerify bool
|
||||
TLSOptions *tlsconfig.Options
|
||||
}
|
||||
|
||||
// NewCommonOptions returns a new CommonOptions
|
||||
@ -53,6 +54,8 @@ func (commonOpts *CommonOptions) InstallFlags(flags *pflag.FlagSet) {
|
||||
flags.StringVarP(&commonOpts.LogLevel, "log-level", "l", "info", `Set the logging level ("debug"|"info"|"warn"|"error"|"fatal")`)
|
||||
flags.BoolVar(&commonOpts.TLS, "tls", false, "Use TLS; implied by --tlsverify")
|
||||
flags.BoolVar(&commonOpts.TLSVerify, FlagTLSVerify, dockerTLSVerify, "Use TLS and verify the remote")
|
||||
flags.StringVar(&commonOpts.Orchestrator, "orchestrator", "", "Which orchestrator to use with the docker cli (swarm|kubernetes) (default swarm) (experimental)")
|
||||
flags.SetAnnotation("orchestrator", "experimentalCLI", nil)
|
||||
|
||||
// TODO use flag flags.String("identity"}, "i", "", "Path to libtrust key file")
|
||||
|
||||
|
||||
147
components/cli/cli/manifest/store/store.go
Normal file
147
components/cli/cli/manifest/store/store.go
Normal file
@ -0,0 +1,147 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli/manifest/types"
|
||||
"github.com/docker/distribution/reference"
|
||||
)
|
||||
|
||||
// Store manages local storage of image distribution manifests
|
||||
type Store interface {
|
||||
Remove(listRef reference.Reference) error
|
||||
Get(listRef reference.Reference, manifest reference.Reference) (types.ImageManifest, error)
|
||||
GetList(listRef reference.Reference) ([]types.ImageManifest, error)
|
||||
Save(listRef reference.Reference, manifest reference.Reference, image types.ImageManifest) error
|
||||
}
|
||||
|
||||
// fsStore manages manifest files stored on the local filesystem
|
||||
type fsStore struct {
|
||||
root string
|
||||
}
|
||||
|
||||
// NewStore returns a new store for a local file path
|
||||
func NewStore(root string) Store {
|
||||
return &fsStore{root: root}
|
||||
}
|
||||
|
||||
// Remove a manifest list from local storage
|
||||
func (s *fsStore) Remove(listRef reference.Reference) error {
|
||||
path := filepath.Join(s.root, makeFilesafeName(listRef.String()))
|
||||
return os.RemoveAll(path)
|
||||
}
|
||||
|
||||
// Get returns the local manifest
|
||||
func (s *fsStore) Get(listRef reference.Reference, manifest reference.Reference) (types.ImageManifest, error) {
|
||||
filename := manifestToFilename(s.root, listRef.String(), manifest.String())
|
||||
return s.getFromFilename(manifest, filename)
|
||||
}
|
||||
|
||||
func (s *fsStore) getFromFilename(ref reference.Reference, filename string) (types.ImageManifest, error) {
|
||||
bytes, err := ioutil.ReadFile(filename)
|
||||
switch {
|
||||
case os.IsNotExist(err):
|
||||
return types.ImageManifest{}, newNotFoundError(ref.String())
|
||||
case err != nil:
|
||||
return types.ImageManifest{}, err
|
||||
}
|
||||
var manifestInfo types.ImageManifest
|
||||
return manifestInfo, json.Unmarshal(bytes, &manifestInfo)
|
||||
}
|
||||
|
||||
// GetList returns all the local manifests for a transaction
|
||||
func (s *fsStore) GetList(listRef reference.Reference) ([]types.ImageManifest, error) {
|
||||
filenames, err := s.listManifests(listRef.String())
|
||||
switch {
|
||||
case err != nil:
|
||||
return nil, err
|
||||
case filenames == nil:
|
||||
return nil, newNotFoundError(listRef.String())
|
||||
}
|
||||
|
||||
manifests := []types.ImageManifest{}
|
||||
for _, filename := range filenames {
|
||||
filename = filepath.Join(s.root, makeFilesafeName(listRef.String()), filename)
|
||||
manifest, err := s.getFromFilename(listRef, filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
manifests = append(manifests, manifest)
|
||||
}
|
||||
return manifests, nil
|
||||
}
|
||||
|
||||
// listManifests stored in a transaction
|
||||
func (s *fsStore) listManifests(transaction string) ([]string, error) {
|
||||
transactionDir := filepath.Join(s.root, makeFilesafeName(transaction))
|
||||
fileInfos, err := ioutil.ReadDir(transactionDir)
|
||||
switch {
|
||||
case os.IsNotExist(err):
|
||||
return nil, nil
|
||||
case err != nil:
|
||||
return nil, err
|
||||
}
|
||||
|
||||
filenames := []string{}
|
||||
for _, info := range fileInfos {
|
||||
filenames = append(filenames, info.Name())
|
||||
}
|
||||
return filenames, nil
|
||||
}
|
||||
|
||||
// Save a manifest as part of a local manifest list
|
||||
func (s *fsStore) Save(listRef reference.Reference, manifest reference.Reference, image types.ImageManifest) error {
|
||||
if err := s.createManifestListDirectory(listRef.String()); err != nil {
|
||||
return err
|
||||
}
|
||||
filename := manifestToFilename(s.root, listRef.String(), manifest.String())
|
||||
bytes, err := json.Marshal(image)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ioutil.WriteFile(filename, bytes, 0644)
|
||||
}
|
||||
|
||||
func (s *fsStore) createManifestListDirectory(transaction string) error {
|
||||
path := filepath.Join(s.root, makeFilesafeName(transaction))
|
||||
return os.MkdirAll(path, 0755)
|
||||
}
|
||||
|
||||
func manifestToFilename(root, manifestList, manifest string) string {
|
||||
return filepath.Join(root, makeFilesafeName(manifestList), makeFilesafeName(manifest))
|
||||
}
|
||||
|
||||
func makeFilesafeName(ref string) string {
|
||||
fileName := strings.Replace(ref, ":", "-", -1)
|
||||
return strings.Replace(fileName, "/", "_", -1)
|
||||
}
|
||||
|
||||
type notFoundError struct {
|
||||
object string
|
||||
}
|
||||
|
||||
func newNotFoundError(ref string) *notFoundError {
|
||||
return ¬FoundError{object: ref}
|
||||
}
|
||||
|
||||
func (n *notFoundError) Error() string {
|
||||
return fmt.Sprintf("No such manifest: %s", n.object)
|
||||
}
|
||||
|
||||
// NotFound interface
|
||||
func (n *notFoundError) NotFound() {}
|
||||
|
||||
// IsNotFound returns true if the error is a not found error
|
||||
func IsNotFound(err error) bool {
|
||||
_, ok := err.(notFound)
|
||||
return ok
|
||||
}
|
||||
|
||||
type notFound interface {
|
||||
NotFound()
|
||||
}
|
||||
131
components/cli/cli/manifest/store/store_test.go
Normal file
131
components/cli/cli/manifest/store/store_test.go
Normal file
@ -0,0 +1,131 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/manifest/types"
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type fakeRef struct {
|
||||
name string
|
||||
}
|
||||
|
||||
func (f fakeRef) String() string {
|
||||
return f.name
|
||||
}
|
||||
|
||||
func (f fakeRef) Name() string {
|
||||
return f.name
|
||||
}
|
||||
|
||||
func ref(name string) fakeRef {
|
||||
return fakeRef{name: name}
|
||||
}
|
||||
|
||||
func sref(t *testing.T, name string) *types.SerializableNamed {
|
||||
named, err := reference.ParseNamed("example.com/" + name)
|
||||
require.NoError(t, err)
|
||||
return &types.SerializableNamed{Named: named}
|
||||
}
|
||||
|
||||
func newTestStore(t *testing.T) (Store, func()) {
|
||||
tmpdir, err := ioutil.TempDir("", "manifest-store-test")
|
||||
require.NoError(t, err)
|
||||
|
||||
return NewStore(tmpdir), func() { os.RemoveAll(tmpdir) }
|
||||
}
|
||||
|
||||
func getFiles(t *testing.T, store Store) []os.FileInfo {
|
||||
infos, err := ioutil.ReadDir(store.(*fsStore).root)
|
||||
require.NoError(t, err)
|
||||
return infos
|
||||
}
|
||||
|
||||
func TestStoreRemove(t *testing.T) {
|
||||
store, cleanup := newTestStore(t)
|
||||
defer cleanup()
|
||||
|
||||
listRef := ref("list")
|
||||
data := types.ImageManifest{Ref: sref(t, "abcdef")}
|
||||
require.NoError(t, store.Save(listRef, ref("manifest"), data))
|
||||
require.Len(t, getFiles(t, store), 1)
|
||||
|
||||
assert.NoError(t, store.Remove(listRef))
|
||||
assert.Len(t, getFiles(t, store), 0)
|
||||
}
|
||||
|
||||
func TestStoreSaveAndGet(t *testing.T) {
|
||||
store, cleanup := newTestStore(t)
|
||||
defer cleanup()
|
||||
|
||||
listRef := ref("list")
|
||||
data := types.ImageManifest{Ref: sref(t, "abcdef")}
|
||||
err := store.Save(listRef, ref("exists"), data)
|
||||
require.NoError(t, err)
|
||||
|
||||
var testcases = []struct {
|
||||
listRef reference.Reference
|
||||
manifestRef reference.Reference
|
||||
expected types.ImageManifest
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
listRef: listRef,
|
||||
manifestRef: ref("exists"),
|
||||
expected: data,
|
||||
},
|
||||
{
|
||||
listRef: listRef,
|
||||
manifestRef: ref("exist:does-not"),
|
||||
expectedErr: "No such manifest: exist:does-not",
|
||||
},
|
||||
{
|
||||
listRef: ref("list:does-not-exist"),
|
||||
manifestRef: ref("manifest:does-not-exist"),
|
||||
expectedErr: "No such manifest: manifest:does-not-exist",
|
||||
},
|
||||
}
|
||||
|
||||
for _, testcase := range testcases {
|
||||
actual, err := store.Get(testcase.listRef, testcase.manifestRef)
|
||||
if testcase.expectedErr != "" {
|
||||
assert.EqualError(t, err, testcase.expectedErr)
|
||||
assert.True(t, IsNotFound(err))
|
||||
continue
|
||||
}
|
||||
if !assert.NoError(t, err, testcase.manifestRef.String()) {
|
||||
continue
|
||||
}
|
||||
assert.Equal(t, testcase.expected, actual, testcase.manifestRef.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestStoreGetList(t *testing.T) {
|
||||
store, cleanup := newTestStore(t)
|
||||
defer cleanup()
|
||||
|
||||
listRef := ref("list")
|
||||
first := types.ImageManifest{Ref: sref(t, "first")}
|
||||
require.NoError(t, store.Save(listRef, ref("first"), first))
|
||||
second := types.ImageManifest{Ref: sref(t, "second")}
|
||||
require.NoError(t, store.Save(listRef, ref("exists"), second))
|
||||
|
||||
list, err := store.GetList(listRef)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, list, 2)
|
||||
}
|
||||
|
||||
func TestStoreGetListDoesNotExist(t *testing.T) {
|
||||
store, cleanup := newTestStore(t)
|
||||
defer cleanup()
|
||||
|
||||
listRef := ref("list")
|
||||
_, err := store.GetList(listRef)
|
||||
assert.EqualError(t, err, "No such manifest: list")
|
||||
assert.True(t, IsNotFound(err))
|
||||
}
|
||||
107
components/cli/cli/manifest/types/types.go
Normal file
107
components/cli/cli/manifest/types/types.go
Normal file
@ -0,0 +1,107 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/docker/distribution"
|
||||
"github.com/docker/distribution/manifest/manifestlist"
|
||||
"github.com/docker/distribution/manifest/schema2"
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/opencontainers/go-digest"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// ImageManifest contains info to output for a manifest object.
|
||||
type ImageManifest struct {
|
||||
Ref *SerializableNamed
|
||||
Digest digest.Digest
|
||||
SchemaV2Manifest *schema2.DeserializedManifest `json:",omitempty"`
|
||||
Platform manifestlist.PlatformSpec
|
||||
}
|
||||
|
||||
// Blobs returns the digests for all the blobs referenced by this manifest
|
||||
func (i ImageManifest) Blobs() []digest.Digest {
|
||||
digests := []digest.Digest{}
|
||||
for _, descriptor := range i.SchemaV2Manifest.References() {
|
||||
digests = append(digests, descriptor.Digest)
|
||||
}
|
||||
return digests
|
||||
}
|
||||
|
||||
// Payload returns the media type and bytes for the manifest
|
||||
func (i ImageManifest) Payload() (string, []byte, error) {
|
||||
switch {
|
||||
case i.SchemaV2Manifest != nil:
|
||||
return i.SchemaV2Manifest.Payload()
|
||||
default:
|
||||
return "", nil, errors.Errorf("%s has no payload", i.Ref)
|
||||
}
|
||||
}
|
||||
|
||||
// References implements the distribution.Manifest interface. It delegates to
|
||||
// the underlying manifest.
|
||||
func (i ImageManifest) References() []distribution.Descriptor {
|
||||
switch {
|
||||
case i.SchemaV2Manifest != nil:
|
||||
return i.SchemaV2Manifest.References()
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// NewImageManifest returns a new ImageManifest object. The values for Platform
|
||||
// are initialized from those in the image
|
||||
func NewImageManifest(ref reference.Named, digest digest.Digest, img Image, manifest *schema2.DeserializedManifest) ImageManifest {
|
||||
platform := manifestlist.PlatformSpec{
|
||||
OS: img.OS,
|
||||
Architecture: img.Architecture,
|
||||
OSVersion: img.OSVersion,
|
||||
OSFeatures: img.OSFeatures,
|
||||
}
|
||||
return ImageManifest{
|
||||
Ref: &SerializableNamed{Named: ref},
|
||||
Digest: digest,
|
||||
SchemaV2Manifest: manifest,
|
||||
Platform: platform,
|
||||
}
|
||||
}
|
||||
|
||||
// SerializableNamed is a reference.Named that can be serialzied and deserialized
|
||||
// from JSON
|
||||
type SerializableNamed struct {
|
||||
reference.Named
|
||||
}
|
||||
|
||||
// UnmarshalJSON loads the Named reference from JSON bytes
|
||||
func (s *SerializableNamed) UnmarshalJSON(b []byte) error {
|
||||
var raw string
|
||||
if err := json.Unmarshal(b, &raw); err != nil {
|
||||
return errors.Wrapf(err, "invalid named reference bytes: %s", b)
|
||||
}
|
||||
var err error
|
||||
s.Named, err = reference.ParseNamed(raw)
|
||||
return err
|
||||
}
|
||||
|
||||
// MarshalJSON returns the JSON bytes representation
|
||||
func (s *SerializableNamed) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(s.String())
|
||||
}
|
||||
|
||||
// Image is the minimal set of fields required to set default platform settings
|
||||
// on a manifest.
|
||||
type Image struct {
|
||||
Architecture string `json:"architecture,omitempty"`
|
||||
OS string `json:"os,omitempty"`
|
||||
OSVersion string `json:"os.version,omitempty"`
|
||||
OSFeatures []string `json:"os.features,omitempty"`
|
||||
}
|
||||
|
||||
// NewImageFromJSON creates an Image configuration from json.
|
||||
func NewImageFromJSON(src []byte) (*Image, error) {
|
||||
img := &Image{}
|
||||
if err := json.Unmarshal(src, img); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return img, nil
|
||||
}
|
||||
183
components/cli/cli/registry/client/client.go
Normal file
183
components/cli/cli/registry/client/client.go
Normal file
@ -0,0 +1,183 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
manifesttypes "github.com/docker/cli/cli/manifest/types"
|
||||
"github.com/docker/distribution"
|
||||
"github.com/docker/distribution/reference"
|
||||
distributionclient "github.com/docker/distribution/registry/client"
|
||||
"github.com/docker/docker/api/types"
|
||||
registrytypes "github.com/docker/docker/api/types/registry"
|
||||
"github.com/opencontainers/go-digest"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
// RegistryClient is a client used to communicate with a Docker distribution
|
||||
// registry
|
||||
type RegistryClient interface {
|
||||
GetManifest(ctx context.Context, ref reference.Named) (manifesttypes.ImageManifest, error)
|
||||
GetManifestList(ctx context.Context, ref reference.Named) ([]manifesttypes.ImageManifest, error)
|
||||
MountBlob(ctx context.Context, source reference.Canonical, target reference.Named) error
|
||||
PutManifest(ctx context.Context, ref reference.Named, manifest distribution.Manifest) (digest.Digest, error)
|
||||
}
|
||||
|
||||
// NewRegistryClient returns a new RegistryClient with a resolver
|
||||
func NewRegistryClient(resolver AuthConfigResolver, userAgent string, insecure bool) RegistryClient {
|
||||
return &client{
|
||||
authConfigResolver: resolver,
|
||||
insecureRegistry: insecure,
|
||||
userAgent: userAgent,
|
||||
}
|
||||
}
|
||||
|
||||
// AuthConfigResolver returns Auth Configuration for an index
|
||||
type AuthConfigResolver func(ctx context.Context, index *registrytypes.IndexInfo) types.AuthConfig
|
||||
|
||||
// PutManifestOptions is the data sent to push a manifest
|
||||
type PutManifestOptions struct {
|
||||
MediaType string
|
||||
Payload []byte
|
||||
}
|
||||
|
||||
type client struct {
|
||||
authConfigResolver AuthConfigResolver
|
||||
insecureRegistry bool
|
||||
userAgent string
|
||||
}
|
||||
|
||||
// ErrBlobCreated returned when a blob mount request was created
|
||||
type ErrBlobCreated struct {
|
||||
From reference.Named
|
||||
Target reference.Named
|
||||
}
|
||||
|
||||
func (err ErrBlobCreated) Error() string {
|
||||
return fmt.Sprintf("blob mounted from: %v to: %v",
|
||||
err.From, err.Target)
|
||||
}
|
||||
|
||||
// ErrHTTPProto returned if attempting to use TLS with a non-TLS registry
|
||||
type ErrHTTPProto struct {
|
||||
OrigErr string
|
||||
}
|
||||
|
||||
func (err ErrHTTPProto) Error() string {
|
||||
return err.OrigErr
|
||||
}
|
||||
|
||||
var _ RegistryClient = &client{}
|
||||
|
||||
// MountBlob into the registry, so it can be referenced by a manifest
|
||||
func (c *client) MountBlob(ctx context.Context, sourceRef reference.Canonical, targetRef reference.Named) error {
|
||||
repoEndpoint, err := newDefaultRepositoryEndpoint(targetRef, c.insecureRegistry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
repo, err := c.getRepositoryForReference(ctx, targetRef, repoEndpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
lu, err := repo.Blobs(ctx).Create(ctx, distributionclient.WithMountFrom(sourceRef))
|
||||
switch err.(type) {
|
||||
case distribution.ErrBlobMounted:
|
||||
logrus.Debugf("mount of blob %s succeeded", sourceRef)
|
||||
return nil
|
||||
case nil:
|
||||
default:
|
||||
return errors.Wrapf(err, "failed to mount blob %s to %s", sourceRef, targetRef)
|
||||
}
|
||||
lu.Cancel(ctx)
|
||||
logrus.Debugf("mount of blob %s created", sourceRef)
|
||||
return ErrBlobCreated{From: sourceRef, Target: targetRef}
|
||||
}
|
||||
|
||||
// PutManifest sends the manifest to a registry and returns the new digest
|
||||
func (c *client) PutManifest(ctx context.Context, ref reference.Named, manifest distribution.Manifest) (digest.Digest, error) {
|
||||
repoEndpoint, err := newDefaultRepositoryEndpoint(ref, c.insecureRegistry)
|
||||
if err != nil {
|
||||
return digest.Digest(""), err
|
||||
}
|
||||
|
||||
repo, err := c.getRepositoryForReference(ctx, ref, repoEndpoint)
|
||||
if err != nil {
|
||||
return digest.Digest(""), err
|
||||
}
|
||||
|
||||
manifestService, err := repo.Manifests(ctx)
|
||||
if err != nil {
|
||||
return digest.Digest(""), err
|
||||
}
|
||||
|
||||
_, opts, err := getManifestOptionsFromReference(ref)
|
||||
if err != nil {
|
||||
return digest.Digest(""), err
|
||||
}
|
||||
|
||||
dgst, err := manifestService.Put(ctx, manifest, opts...)
|
||||
return dgst, errors.Wrapf(err, "failed to put manifest %s", ref)
|
||||
}
|
||||
|
||||
func (c *client) getRepositoryForReference(ctx context.Context, ref reference.Named, repoEndpoint repositoryEndpoint) (distribution.Repository, error) {
|
||||
httpTransport, err := c.getHTTPTransportForRepoEndpoint(ctx, repoEndpoint)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "server gave HTTP response to HTTPS client") {
|
||||
return nil, ErrHTTPProto{OrigErr: err.Error()}
|
||||
}
|
||||
}
|
||||
repoName, err := reference.WithName(repoEndpoint.Name())
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to parse repo name from %s", ref)
|
||||
}
|
||||
return distributionclient.NewRepository(ctx, repoName, repoEndpoint.BaseURL(), httpTransport)
|
||||
}
|
||||
|
||||
func (c *client) getHTTPTransportForRepoEndpoint(ctx context.Context, repoEndpoint repositoryEndpoint) (http.RoundTripper, error) {
|
||||
httpTransport, err := getHTTPTransport(
|
||||
c.authConfigResolver(ctx, repoEndpoint.info.Index),
|
||||
repoEndpoint.endpoint,
|
||||
repoEndpoint.Name(),
|
||||
c.userAgent)
|
||||
return httpTransport, errors.Wrap(err, "failed to configure transport")
|
||||
}
|
||||
|
||||
// GetManifest returns an ImageManifest for the reference
|
||||
func (c *client) GetManifest(ctx context.Context, ref reference.Named) (manifesttypes.ImageManifest, error) {
|
||||
var result manifesttypes.ImageManifest
|
||||
fetch := func(ctx context.Context, repo distribution.Repository, ref reference.Named) (bool, error) {
|
||||
var err error
|
||||
result, err = fetchManifest(ctx, repo, ref)
|
||||
return result.Ref != nil, err
|
||||
}
|
||||
|
||||
err := c.iterateEndpoints(ctx, ref, fetch)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// GetManifestList returns a list of ImageManifest for the reference
|
||||
func (c *client) GetManifestList(ctx context.Context, ref reference.Named) ([]manifesttypes.ImageManifest, error) {
|
||||
result := []manifesttypes.ImageManifest{}
|
||||
fetch := func(ctx context.Context, repo distribution.Repository, ref reference.Named) (bool, error) {
|
||||
var err error
|
||||
result, err = fetchList(ctx, repo, ref)
|
||||
return len(result) > 0, err
|
||||
}
|
||||
|
||||
err := c.iterateEndpoints(ctx, ref, fetch)
|
||||
return result, err
|
||||
}
|
||||
|
||||
func getManifestOptionsFromReference(ref reference.Named) (digest.Digest, []distribution.ManifestServiceOption, error) {
|
||||
if tagged, isTagged := ref.(reference.NamedTagged); isTagged {
|
||||
tag := tagged.Tag()
|
||||
return "", []distribution.ManifestServiceOption{distribution.WithTag(tag)}, nil
|
||||
}
|
||||
if digested, isDigested := ref.(reference.Canonical); isDigested {
|
||||
return digested.Digest(), []distribution.ManifestServiceOption{}, nil
|
||||
}
|
||||
return "", nil, errors.Errorf("%s no tag or digest", ref)
|
||||
}
|
||||
133
components/cli/cli/registry/client/endpoint.go
Normal file
133
components/cli/cli/registry/client/endpoint.go
Normal file
@ -0,0 +1,133 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/docker/distribution/registry/client/auth"
|
||||
"github.com/docker/distribution/registry/client/transport"
|
||||
authtypes "github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type repositoryEndpoint struct {
|
||||
info *registry.RepositoryInfo
|
||||
endpoint registry.APIEndpoint
|
||||
}
|
||||
|
||||
// Name returns the repository name
|
||||
func (r repositoryEndpoint) Name() string {
|
||||
repoName := r.info.Name.Name()
|
||||
// If endpoint does not support CanonicalName, use the RemoteName instead
|
||||
if r.endpoint.TrimHostname {
|
||||
repoName = reference.Path(r.info.Name)
|
||||
}
|
||||
return repoName
|
||||
}
|
||||
|
||||
// BaseURL returns the endpoint url
|
||||
func (r repositoryEndpoint) BaseURL() string {
|
||||
return r.endpoint.URL.String()
|
||||
}
|
||||
|
||||
func newDefaultRepositoryEndpoint(ref reference.Named, insecure bool) (repositoryEndpoint, error) {
|
||||
repoInfo, err := registry.ParseRepositoryInfo(ref)
|
||||
if err != nil {
|
||||
return repositoryEndpoint{}, err
|
||||
}
|
||||
endpoint, err := getDefaultEndpointFromRepoInfo(repoInfo)
|
||||
if err != nil {
|
||||
return repositoryEndpoint{}, err
|
||||
}
|
||||
if insecure {
|
||||
endpoint.TLSConfig.InsecureSkipVerify = true
|
||||
}
|
||||
return repositoryEndpoint{info: repoInfo, endpoint: endpoint}, nil
|
||||
}
|
||||
|
||||
func getDefaultEndpointFromRepoInfo(repoInfo *registry.RepositoryInfo) (registry.APIEndpoint, error) {
|
||||
var err error
|
||||
|
||||
options := registry.ServiceOptions{}
|
||||
registryService, err := registry.NewService(options)
|
||||
if err != nil {
|
||||
return registry.APIEndpoint{}, err
|
||||
}
|
||||
endpoints, err := registryService.LookupPushEndpoints(reference.Domain(repoInfo.Name))
|
||||
if err != nil {
|
||||
return registry.APIEndpoint{}, err
|
||||
}
|
||||
// Default to the highest priority endpoint to return
|
||||
endpoint := endpoints[0]
|
||||
if !repoInfo.Index.Secure {
|
||||
for _, ep := range endpoints {
|
||||
if ep.URL.Scheme == "http" {
|
||||
endpoint = ep
|
||||
}
|
||||
}
|
||||
}
|
||||
return endpoint, nil
|
||||
}
|
||||
|
||||
// getHTTPTransport builds a transport for use in communicating with a registry
|
||||
func getHTTPTransport(authConfig authtypes.AuthConfig, endpoint registry.APIEndpoint, repoName string, userAgent string) (http.RoundTripper, error) {
|
||||
// get the http transport, this will be used in a client to upload manifest
|
||||
base := &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
Dial: (&net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
DualStack: true,
|
||||
}).Dial,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
TLSClientConfig: endpoint.TLSConfig,
|
||||
DisableKeepAlives: true,
|
||||
}
|
||||
|
||||
modifiers := registry.Headers(userAgent, http.Header{})
|
||||
authTransport := transport.NewTransport(base, modifiers...)
|
||||
challengeManager, confirmedV2, err := registry.PingV2Registry(endpoint.URL, authTransport)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error pinging v2 registry")
|
||||
}
|
||||
if !confirmedV2 {
|
||||
return nil, fmt.Errorf("unsupported registry version")
|
||||
}
|
||||
if authConfig.RegistryToken != "" {
|
||||
passThruTokenHandler := &existingTokenHandler{token: authConfig.RegistryToken}
|
||||
modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, passThruTokenHandler))
|
||||
} else {
|
||||
creds := registry.NewStaticCredentialStore(&authConfig)
|
||||
tokenHandler := auth.NewTokenHandler(authTransport, creds, repoName, "*")
|
||||
basicHandler := auth.NewBasicHandler(creds)
|
||||
modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler))
|
||||
}
|
||||
return transport.NewTransport(base, modifiers...), nil
|
||||
}
|
||||
|
||||
// RepoNameForReference returns the repository name from a reference
|
||||
func RepoNameForReference(ref reference.Named) (string, error) {
|
||||
// insecure is fine since this only returns the name
|
||||
repo, err := newDefaultRepositoryEndpoint(ref, false)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return repo.Name(), nil
|
||||
}
|
||||
|
||||
type existingTokenHandler struct {
|
||||
token string
|
||||
}
|
||||
|
||||
func (th *existingTokenHandler) AuthorizeRequest(req *http.Request, params map[string]string) error {
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", th.token))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (th *existingTokenHandler) Scheme() string {
|
||||
return "bearer"
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user