Compare commits
763 Commits
28.x
...
v29.0.0-rc
| Author | SHA1 | Date | |
|---|---|---|---|
| b5bac44972 | |||
| ef16d82301 | |||
| 73aade19c1 | |||
| 0f589b35c5 | |||
| 6fa5900339 | |||
| 5836040ec9 | |||
| f6feef8fe2 | |||
| 631f32ee9d | |||
| c41815f17a | |||
| 5d599e9322 | |||
| cdaae144e9 | |||
| 7fb94dae5b | |||
| 50598d21c9 | |||
| cf9e1778d3 | |||
| c98d9647d3 | |||
| f8e871344f | |||
| 173808f8b6 | |||
| 8444c911bd | |||
| 96bc39b36d | |||
| 918ec8c48a | |||
| 7f86de9319 | |||
| 7bdb4df07d | |||
| 9e7937746c | |||
| 8e2943c6c5 | |||
| 513ee76bac | |||
| 8767904ae8 | |||
| b8b4f54a89 | |||
| 03fb1df2dd | |||
| 58e3de8781 | |||
| 65496c5557 | |||
| 3434869388 | |||
| 9dde80abd8 | |||
| e636a2a069 | |||
| af255accaa | |||
| fcac1d5b2a | |||
| 053aa376ea | |||
| 606f1c65d0 | |||
| 83319f09f7 | |||
| 6ed16a2cc1 | |||
| 10281ff5a5 | |||
| 5007c96b0d | |||
| c467ebafd8 | |||
| 689152a804 | |||
| e809e65ba6 | |||
| e01ce69ff9 | |||
| 292001a451 | |||
| 0b1c7bc0f1 | |||
| d309027d58 | |||
| 15b422b317 | |||
| 832fc66ca7 | |||
| dad1d367c8 | |||
| e50c94f21a | |||
| 93cd6793b3 | |||
| f594a7f09b | |||
| 4b498addce | |||
| 715467c9d8 | |||
| dafbfc2f7e | |||
| 4a608069a7 | |||
| 153f7f10c9 | |||
| aef2ef8c77 | |||
| 965a0e3518 | |||
| 4afbd6146b | |||
| 056e314645 | |||
| 64805c2959 | |||
| e9a941001c | |||
| f74cd147bb | |||
| 4f7c07cfc2 | |||
| aeb78091a0 | |||
| 7b4cde6967 | |||
| 563f5fe335 | |||
| 171a9b70b2 | |||
| 5ba4c17d78 | |||
| d252afa6b0 | |||
| bc2aa4eed1 | |||
| 24e95b8682 | |||
| c844f92b58 | |||
| 07a79d68a6 | |||
| 2bcf047f90 | |||
| 0f2f9e9c41 | |||
| 6e1ff0bec1 | |||
| c52fa073cd | |||
| f24bb4bc76 | |||
| c599e9064e | |||
| 68ec5bfe20 | |||
| 130a3f5f87 | |||
| e6d150be16 | |||
| ecea0c01b3 | |||
| 2027349052 | |||
| bb31a8006e | |||
| 8ecdfed2af | |||
| 714c82a014 | |||
| aec24ba92c | |||
| 193db8ec41 | |||
| 5ad9fbdef7 | |||
| 7abc65bc06 | |||
| c2817e2d59 | |||
| f68d8f1f24 | |||
| 830e1d60ab | |||
| 9def7748a5 | |||
| f8b1b8d165 | |||
| 01febbc3bb | |||
| 59d228cd98 | |||
| 5ad91456c7 | |||
| 4c73cefc15 | |||
| b8f2b7c678 | |||
| f046bd371a | |||
| d36f16e224 | |||
| a3e954551d | |||
| f81816ef88 | |||
| 9e646f6d92 | |||
| 64be664e85 | |||
| 2c539a6530 | |||
| 6ddff81bee | |||
| 85ac71a3fe | |||
| 41432adc36 | |||
| 17d6a92954 | |||
| 94788a3b63 | |||
| af34b8471a | |||
| c8014ec509 | |||
| 413ee120de | |||
| 7923f440ed | |||
| b1ea4fe9d9 | |||
| 5483b10e94 | |||
| 9f941a49c1 | |||
| e598ea0176 | |||
| d17643a675 | |||
| 3754fe3c8a | |||
| 30ec4c09b3 | |||
| 6d351158cc | |||
| 6222292566 | |||
| c06c08531a | |||
| 74e3520724 | |||
| 886e041790 | |||
| 9dea52c193 | |||
| 780c427550 | |||
| ca6f899a58 | |||
| 395152ce88 | |||
| f5a7a3c72e | |||
| 394ab41696 | |||
| 91d8c0bf62 | |||
| 311a97a210 | |||
| b375006c3e | |||
| b0201c8531 | |||
| 2b4fd0d750 | |||
| 63c5254201 | |||
| 1b90a53be2 | |||
| c361deb85d | |||
| adfcb88896 | |||
| 9c79528489 | |||
| 9874437110 | |||
| 39d9a0cd51 | |||
| 9d97363f8f | |||
| 21e768adb7 | |||
| cdcf267264 | |||
| f8932e916b | |||
| e9664b9f34 | |||
| a61ecaf3c7 | |||
| 7e8b893952 | |||
| 5a4758f513 | |||
| 1b467f909c | |||
| 3c78ac2aad | |||
| 9f02d9643d | |||
| f3fb7728c7 | |||
| 0dec83f572 | |||
| e58a6ace45 | |||
| a6946d0fbf | |||
| c3317b0a43 | |||
| 75f3c08257 | |||
| aaed38f5d5 | |||
| fdf9b43ca8 | |||
| 95735d1631 | |||
| 013c41c602 | |||
| 43b03ef2c5 | |||
| 9a6313ed3b | |||
| 20e5dc9469 | |||
| 4287f1d887 | |||
| 746d7cb39e | |||
| 6855d70c52 | |||
| 1a80524834 | |||
| d719b416ab | |||
| 008b2df526 | |||
| 96732f858a | |||
| 1b085a2b63 | |||
| b0cb6406ff | |||
| 782deffe83 | |||
| e25843bfb6 | |||
| 592afa8c73 | |||
| 6d1c037640 | |||
| 9cba658e9a | |||
| 57314d42a2 | |||
| e922dbefca | |||
| 286658d7ff | |||
| 90959c40bd | |||
| f369c5bcf2 | |||
| 24bfedf3f8 | |||
| ef3c19a80b | |||
| 11d40488dd | |||
| 1bae6aafa8 | |||
| 1ace9aec34 | |||
| 734328eef9 | |||
| 9c88b315ef | |||
| 04bfe7dc78 | |||
| b611f288ee | |||
| 40cdfc0d81 | |||
| 0270b2d6f7 | |||
| 903e9b3426 | |||
| a44144e1db | |||
| ed7908e4ed | |||
| 048e931b42 | |||
| ec912e5524 | |||
| 8fbb70ae56 | |||
| f3687d8a8b | |||
| 9b2f831452 | |||
| 2711800430 | |||
| 84520653d8 | |||
| c8600e1cea | |||
| 4a4043cdb6 | |||
| 7cc801d93d | |||
| 7afda4c6c5 | |||
| 550d40f7bc | |||
| 5710de6d9a | |||
| 93bb8a7a0a | |||
| b1d45285ba | |||
| 4c802a1548 | |||
| 635a718209 | |||
| 133279fb0d | |||
| daa15c3bfa | |||
| 83e40c39b4 | |||
| 71f46056c9 | |||
| a53e83a3d4 | |||
| 083e5ce872 | |||
| 3cf005ec91 | |||
| c5cbb3e648 | |||
| e241f53ebc | |||
| 0f08b55bce | |||
| 3c244d1099 | |||
| 233322637a | |||
| 5d8fb335d4 | |||
| f4a433f841 | |||
| a02902eb78 | |||
| 437f1260fd | |||
| 4373ce5f8b | |||
| 9b79e48646 | |||
| b9314938b7 | |||
| b8cda96d11 | |||
| 82281087e3 | |||
| c3ceba2548 | |||
| e78f16961d | |||
| d391d0fa4a | |||
| 2f6abcf3c2 | |||
| ea8212ab55 | |||
| 3785198c6e | |||
| 531f7e121d | |||
| 810be9fbe9 | |||
| d18af47d0f | |||
| 47b1715d6f | |||
| 78c54646c3 | |||
| 467fcfe4bd | |||
| 8325214ccc | |||
| 924dd4710b | |||
| d67dad3fb4 | |||
| 1336f51e6a | |||
| c5467b5556 | |||
| 8b23c2bcb5 | |||
| 22dda0fc2b | |||
| 69972b682b | |||
| 8bb5595f28 | |||
| 5245e20866 | |||
| c0e37dda14 | |||
| b774e75931 | |||
| 9ba1314d3a | |||
| c5150177bf | |||
| 573e0bddef | |||
| b057ab6d98 | |||
| 0e4934d36c | |||
| cd583313ee | |||
| 5c8817b1b2 | |||
| bf78331f0c | |||
| 0c3bb6c0a4 | |||
| 179dc0228c | |||
| f10041c724 | |||
| 62d25205e9 | |||
| fb3f2da50e | |||
| 5df02441ca | |||
| 935df8a78f | |||
| 183337db9a | |||
| d62d370c23 | |||
| 26bb688ed0 | |||
| 73677146f4 | |||
| c24b62f19c | |||
| be97096566 | |||
| c4a87de3ec | |||
| 4df42ef1d9 | |||
| b55fed5ef6 | |||
| 9fb049c8b6 | |||
| 35f5a4313b | |||
| 589f56c345 | |||
| fa5b741c9b | |||
| af65ee4584 | |||
| 7d85d8fbea | |||
| 4ceef7d328 | |||
| 3bbb633e5b | |||
| fef8773bae | |||
| aba5962ffb | |||
| 1f12d795db | |||
| e7d14d905e | |||
| b0b0e457f0 | |||
| 44e66a97a9 | |||
| 27b316fc0d | |||
| f64b8a332d | |||
| 06ed23d5fe | |||
| 5bf3c6793d | |||
| 5bce5e17af | |||
| 0640306406 | |||
| ab7018b590 | |||
| 2351f5b915 | |||
| 153bd95158 | |||
| ce72a5c28b | |||
| d54c7f9e63 | |||
| 6ec32660e9 | |||
| 9e52a2817c | |||
| 18cdc25bb4 | |||
| 6fa7d18320 | |||
| 89874983c8 | |||
| 88f68273fd | |||
| a2c886251c | |||
| 9a6cbbc586 | |||
| 70915196cb | |||
| 9477941c20 | |||
| 6647e229be | |||
| 0adaf6be3b | |||
| ba21666654 | |||
| 321100e38b | |||
| 5dd52a9efa | |||
| 2827d037ba | |||
| 95eeafa551 | |||
| 81ea282e00 | |||
| 0155c264ae | |||
| 77205e782a | |||
| 580c3aa218 | |||
| f8d33f4602 | |||
| 2066dbcfe8 | |||
| c3be589c16 | |||
| 2a05951680 | |||
| 72f79333e5 | |||
| 6163c03b11 | |||
| 467305fcea | |||
| 65a6c35d90 | |||
| c4df0d17bb | |||
| b3cd9d48fe | |||
| 581cb2b70a | |||
| 6b86aac02e | |||
| 8b01d8e74c | |||
| 047ea37054 | |||
| 4f7f42df0e | |||
| 5a23ff9b17 | |||
| 8f25f4fb24 | |||
| d16c560664 | |||
| 036d3a6bab | |||
| f0e5a0d654 | |||
| ad6ab189a6 | |||
| 30774ed1f2 | |||
| 306b7445a1 | |||
| ef0a67551a | |||
| a7df96501f | |||
| 5a2f87f6f6 | |||
| b8507d71e8 | |||
| cdf705ce66 | |||
| 05220c5f19 | |||
| 9e331b55d6 | |||
| f40caed86c | |||
| 3d87aa441f | |||
| 4d9017d789 | |||
| c558c30056 | |||
| 823c6a75b3 | |||
| 7dfa471387 | |||
| 10072c3548 | |||
| 323fbc485e | |||
| 701b678104 | |||
| 7118f1fb4b | |||
| 212deb412d | |||
| 481e792773 | |||
| 6bc7ed8b65 | |||
| a2198ecd14 | |||
| f2c8b9dfd3 | |||
| cfd7e543fc | |||
| 7d7a7aac4d | |||
| 250e4a564c | |||
| fda64ebc64 | |||
| a5f4ba08d9 | |||
| 09cd4ea26c | |||
| a6826de3e2 | |||
| 5bcb60aaa6 | |||
| 1beb3d4d5b | |||
| 0395cdbd71 | |||
| aab947de8f | |||
| 5ab12e6262 | |||
| 104b07647f | |||
| 27734fdf4d | |||
| dcc3d25dc2 | |||
| c36e67d7b6 | |||
| 4f944e245b | |||
| 7ac3e0e0bf | |||
| 0e7d422e5f | |||
| 7cb8147e77 | |||
| 35a41c39a4 | |||
| abe4aa7893 | |||
| 08f876d6e8 | |||
| 0f875ba9ad | |||
| d9cafa759f | |||
| ba2c1c94ab | |||
| 7ad113ccc2 | |||
| 9216f04eb6 | |||
| 9fd71c8347 | |||
| 045ac0b159 | |||
| 03da6ad2d1 | |||
| 1df8feb2e4 | |||
| c7cbac58b3 | |||
| e8e4588f64 | |||
| d317bc30be | |||
| 832a3754e5 | |||
| 4d4533abaa | |||
| 7032f5922e | |||
| 2966159873 | |||
| 65e7ece518 | |||
| 5bb8ab4e6f | |||
| 8969b57500 | |||
| c6f4573153 | |||
| 04bcae3a8c | |||
| c592932f47 | |||
| 264080d2fc | |||
| e5a0fb09a3 | |||
| 7b172fcf53 | |||
| d223ec3b56 | |||
| 206a8da307 | |||
| f969adf63f | |||
| e416418e70 | |||
| efd6e7b6e0 | |||
| f72ec26693 | |||
| 6de2cdd1af | |||
| e308036440 | |||
| 12d30bb50c | |||
| 863b5633f3 | |||
| aa39a7e7be | |||
| 1a433cdbdb | |||
| 3d2bd97a82 | |||
| 70033b78d4 | |||
| 8cb8056efa | |||
| 7589722e93 | |||
| e06e758d5f | |||
| 58bb45c37f | |||
| 95c9b1b13b | |||
| c3ee82fdc3 | |||
| 9f453d3fea | |||
| f3088e37a0 | |||
| 83371c2014 | |||
| bf47419852 | |||
| 123ef81f7d | |||
| e626f778ec | |||
| d861b78a8a | |||
| 15cf4fa912 | |||
| e3903a1ac8 | |||
| b0d1d94711 | |||
| 8c0440a653 | |||
| 40e605a3b2 | |||
| 873609d790 | |||
| 570a17b3bc | |||
| 4405c0bd50 | |||
| 942a6c4c76 | |||
| 56cab16779 | |||
| 76c405cfdb | |||
| e650803f09 | |||
| 4d93a6486e | |||
| d071c29d4a | |||
| 8d99b45d4a | |||
| bd8e3e4440 | |||
| c6b7268932 | |||
| 2ce94e4fff | |||
| bf39340294 | |||
| b2b7187244 | |||
| 7dfcb06587 | |||
| d4588c711c | |||
| bf9173f383 | |||
| 630fe430ff | |||
| 3b0edc794c | |||
| 89316e18fc | |||
| 7a50955006 | |||
| 9961e39d40 | |||
| 88178eda32 | |||
| cd859b33b4 | |||
| 1d34432676 | |||
| e00762ed7d | |||
| 6b58c4d545 | |||
| 642adae0c0 | |||
| 02fda07211 | |||
| ab3fcf9f9b | |||
| 78a8856c14 | |||
| 4643b42e1d | |||
| 86e2a06f1b | |||
| 4286883b95 | |||
| 2d3b0b33b4 | |||
| c3170f1c81 | |||
| 73d88f514b | |||
| 9ca4ae9e70 | |||
| 11a60e871e | |||
| 57ef7eed46 | |||
| 9b9d103b29 | |||
| cfb8cb91f2 | |||
| 1d571d178d | |||
| 2e3d021912 | |||
| 13010ba673 | |||
| 5c76f7f2d8 | |||
| c2a042e0ed | |||
| e66a1456d3 | |||
| fcb260df1b | |||
| 6e5ce4df55 | |||
| ae1727c41e | |||
| 38595fecb6 | |||
| 4c3fa4ac3c | |||
| 6ad1d4617a | |||
| 2d979220cc | |||
| cce29da061 | |||
| 3265cead1d | |||
| 1b9d0762a5 | |||
| 6dcf9ac843 | |||
| 72f76f2720 | |||
| f9777d2517 | |||
| 5934553198 | |||
| a056cc6164 | |||
| 15f3e910d1 | |||
| 0c07d81a03 | |||
| e0af501e30 | |||
| 5f5a5e1297 | |||
| ee05a71513 | |||
| 6f0c66c152 | |||
| 5f6dfb00d8 | |||
| c06074b22f | |||
| 4ead8784d0 | |||
| 69854c4e08 | |||
| 877a6ef29f | |||
| 2bcf433605 | |||
| f14eeeb361 | |||
| 5ee2906e78 | |||
| 7eab668982 | |||
| ccc1c65859 | |||
| 842f8beb00 | |||
| d0ac0acff0 | |||
| 187a942a88 | |||
| 5a38118956 | |||
| 6bd8a4b2b5 | |||
| 5a99022556 | |||
| bf13010df8 | |||
| 53b6fddced | |||
| 4cd9833d7c | |||
| 4bff12f476 | |||
| 1456b53e4e | |||
| 6d9b06d227 | |||
| e8876edcc2 | |||
| eb5b03a8a3 | |||
| b71a055a11 | |||
| c5ea9079af | |||
| f2af519f2e | |||
| bd0546ad5b | |||
| 27a7947535 | |||
| 53183396d7 | |||
| 70f1147394 | |||
| a8f11a2fa2 | |||
| c612e141b5 | |||
| 9b7ee0e201 | |||
| 3b677449d8 | |||
| d38317c781 | |||
| 2dd462cc36 | |||
| 4c89455378 | |||
| adbe04b5fc | |||
| 097cc9ca64 | |||
| e069ded4c3 | |||
| 44eba133d6 | |||
| 3529651fa7 | |||
| 8324b17f9a | |||
| d16defd9e2 | |||
| 3035b6685b | |||
| 6769f62746 | |||
| ef38d81fdb | |||
| 5052a39915 | |||
| 4beddd3e25 | |||
| 7026e68a71 | |||
| c0fbbe05ca | |||
| 8c22927978 | |||
| c1cc6b61a3 | |||
| 3f5b1bdd32 | |||
| 845870e669 | |||
| 8683664b29 | |||
| d3c23a223c | |||
| 081add2fc5 | |||
| 8972e53ad0 | |||
| f2c64c123f | |||
| 25f95877b5 | |||
| 14ed619736 | |||
| 7dd9c20cac | |||
| 39829affbe | |||
| a93ed48d06 | |||
| f1ceb8c55d | |||
| abfe4d4629 | |||
| 68fc942fd2 | |||
| f9431e3b35 | |||
| 22cc0e90ae | |||
| de54347518 | |||
| b01d359cc9 | |||
| 2abcbf842f | |||
| fcfaa8daeb | |||
| a629a840a8 | |||
| 513ceeec0a | |||
| 5876b2941c | |||
| 50963accec | |||
| d789bac04a | |||
| 71460215d3 | |||
| 1cc698c68f | |||
| 549d39a89f | |||
| 54367b3283 | |||
| 057f3128b6 | |||
| dfbac70efa | |||
| 3b6a556533 | |||
| bf8cb43025 | |||
| a888c4091c | |||
| 02d578b637 | |||
| 21e8bbc8a2 | |||
| f86ad2ea4c | |||
| a4bf9e78e5 | |||
| a1ea79444b | |||
| 066710ba7b | |||
| b8df4abeb5 | |||
| 3f0ccd1b71 | |||
| 6176a7686e | |||
| 66aca29f7d | |||
| f937e62c89 | |||
| bf16dd1251 | |||
| 2199a05e08 | |||
| 149503a32c | |||
| 5c3577ff9f | |||
| 14203bbc77 | |||
| b6d7ac34be | |||
| 83e507377a | |||
| a0ae6e6a5a | |||
| 86b5b528a6 | |||
| f0030712e9 | |||
| 89d8c8a2a7 | |||
| e2a4e429bc | |||
| fdc4397906 | |||
| d63cae6f1c | |||
| 4bd6b6897f | |||
| a1035b0796 | |||
| 7ab3e7e774 | |||
| c6f935eba5 | |||
| 2f87a11e96 | |||
| ef7fd8bb67 | |||
| 3046019d3b | |||
| 1eeb0cc3e1 | |||
| 9257cc7f68 | |||
| f214f860b6 | |||
| f907c7a4b0 | |||
| cd277a5815 | |||
| c297770d2d | |||
| 219cfc8b7d | |||
| 2607ba8062 | |||
| 5322affc9f | |||
| dc41365b56 | |||
| dad2e67860 | |||
| 7cf245d2f7 | |||
| e0b351b3d9 | |||
| 7716219e17 | |||
| f6b90bc253 | |||
| 636a4cf2dc | |||
| 20181d4363 | |||
| d29d719c42 | |||
| fa169b6933 | |||
| 1ca6c946d5 | |||
| c2586d68cf | |||
| a87bde0068 | |||
| df9950aa06 | |||
| 323ef1997f | |||
| 80be02c72b | |||
| e504faf6da | |||
| 03ff54b8ba | |||
| 644dc16b16 | |||
| 2a2748a94c | |||
| 7609dde8d0 | |||
| 71bc8ab3ea | |||
| 6cf2c023f8 | |||
| 73604b8c36 | |||
| e2cab2c64c | |||
| 74042f5ffa | |||
| 8c317ad3fd | |||
| 64f33cd463 | |||
| a3bea24086 | |||
| b05aa464a6 | |||
| e34616574f | |||
| 8d2ccc128a | |||
| b5a939268b | |||
| 260f1dbebb | |||
| e95d133612 | |||
| 3dec3879c8 | |||
| fdc90caeee | |||
| 0db7b9f774 | |||
| 239b727834 | |||
| 907507e22a | |||
| d8089e7d1b | |||
| 29263e865b | |||
| 6bcc9ce730 | |||
| 46b8679315 | |||
| ea4c161067 | |||
| 2b56b66b10 | |||
| 0ba4362d69 | |||
| 8993f54fc3 | |||
| 38b99adc10 | |||
| 3b25977f82 | |||
| 342f8bca25 | |||
| 7422403164 | |||
| 09a3c93f96 | |||
| a10a1e619b | |||
| 75f791d904 | |||
| 8d3c0fb6dc | |||
| 45f09a1504 | |||
| ce639151e0 | |||
| 52c62bd13b | |||
| 8f865184a6 | |||
| ee957e144b | |||
| 6291744fa4 | |||
| 60b326f814 | |||
| aa6ad06304 | |||
| 66713384c3 | |||
| 5c21ec520e | |||
| 212213e81e | |||
| bcd6c45731 | |||
| 876fc1dac4 | |||
| 3bfb30acd7 | |||
| 3f4cc89f64 | |||
| a12090d787 | |||
| f1b0ef127d | |||
| 26a11366a7 | |||
| 9e39630a05 | |||
| 6d2a901118 | |||
| 389ada7188 | |||
| 613477b489 | |||
| a4c8c72411 | |||
| 5896d383ca | |||
| ea850377cd | |||
| 2d0d4ce4af | |||
| a0d9b0cf0d | |||
| b3d0327781 | |||
| 70aef9f502 | |||
| 80dd489f21 | |||
| 04e2a24a9e | |||
| 84c375f430 | |||
| 71672ece9c | |||
| db857b5d9c | |||
| 242f176825 | |||
| 6ea4877cff | |||
| 7bc503344a | |||
| e2cc22d076 | |||
| e9831d75e2 | |||
| 9450481b7e | |||
| a6cc6cd878 | |||
| e907d54fe6 |
6
.github/workflows/build.yml
vendored
6
.github/workflows/build.yml
vendored
@ -35,7 +35,7 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
-
|
||||
name: Create matrix
|
||||
id: platforms
|
||||
@ -88,7 +88,7 @@ jobs:
|
||||
fi
|
||||
-
|
||||
name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: ${{ env.ARTIFACT_NAME }}
|
||||
path: /tmp/out/*
|
||||
@ -143,7 +143,7 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
-
|
||||
name: Create matrix
|
||||
id: platforms
|
||||
|
||||
12
.github/workflows/codeql.yml
vendored
12
.github/workflows/codeql.yml
vendored
@ -46,7 +46,7 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 2
|
||||
# CodeQL 2.16.4's auto-build added support for multi-module repositories,
|
||||
@ -61,19 +61,19 @@ jobs:
|
||||
ln -s vendor.sum go.sum
|
||||
-
|
||||
name: Update Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.24.5"
|
||||
go-version: "1.25.3"
|
||||
-
|
||||
name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
uses: github/codeql-action/init@v4
|
||||
with:
|
||||
languages: go
|
||||
-
|
||||
name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
uses: github/codeql-action/autobuild@v4
|
||||
-
|
||||
name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
uses: github/codeql-action/analyze@v4
|
||||
with:
|
||||
category: "/language:go"
|
||||
|
||||
6
.github/workflows/e2e.yml
vendored
6
.github/workflows/e2e.yml
vendored
@ -37,14 +37,14 @@ jobs:
|
||||
- alpine
|
||||
- debian
|
||||
engine-version:
|
||||
- 29-rc # latest rc
|
||||
- 28 # latest
|
||||
- 27 # latest - 1
|
||||
- 26 # github actions default
|
||||
- 23 # mirantis lts
|
||||
- 25 # mirantis lts
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
-
|
||||
name: Update daemon.json
|
||||
run: |
|
||||
|
||||
11
.github/workflows/test.yml
vendored
11
.github/workflows/test.yml
vendored
@ -53,20 +53,21 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os:
|
||||
- macos-13 # macOS 13 on Intel
|
||||
- macos-14 # macOS 14 on arm64 (Apple Silicon M1)
|
||||
- macos-14 # macOS 14 on arm64 (Apple Silicon M1)
|
||||
- macos-15-intel # macOS 15 on Intel
|
||||
- macos-15 # macOS 15 on arm64 (Apple Silicon M1)
|
||||
# - windows-2022 # FIXME: some tests are failing on the Windows runner, as well as on Appveyor since June 24, 2018: https://ci.appveyor.com/project/docker/cli/history
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
path: ${{ env.GOPATH }}/src/github.com/docker/cli
|
||||
-
|
||||
name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.24.5"
|
||||
go-version: "1.25.3"
|
||||
-
|
||||
name: Test
|
||||
run: |
|
||||
|
||||
11
.github/workflows/validate-pr.yml
vendored
11
.github/workflows/validate-pr.yml
vendored
@ -11,18 +11,23 @@ permissions:
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, edited, labeled, unlabeled]
|
||||
types: [opened, edited, labeled, unlabeled, synchronize]
|
||||
|
||||
jobs:
|
||||
check-area-label:
|
||||
check-labels:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 120 # guardrails timeout for the whole job
|
||||
steps:
|
||||
- name: Missing `area/` label
|
||||
if: contains(join(github.event.pull_request.labels.*.name, ','), 'impact/') && !contains(join(github.event.pull_request.labels.*.name, ','), 'area/')
|
||||
if: always() && contains(join(github.event.pull_request.labels.*.name, ','), 'impact/') && !contains(join(github.event.pull_request.labels.*.name, ','), 'area/')
|
||||
run: |
|
||||
echo "::error::Every PR with an 'impact/*' label should also have an 'area/*' label"
|
||||
exit 1
|
||||
- name: Missing `kind/` label
|
||||
if: always() && contains(join(github.event.pull_request.labels.*.name, ','), 'impact/') && !contains(join(github.event.pull_request.labels.*.name, ','), 'kind/')
|
||||
run: |
|
||||
echo "::error::Every PR with an 'impact/*' label should also have a 'kind/*' label"
|
||||
exit 1
|
||||
- name: OK
|
||||
run: exit 0
|
||||
|
||||
|
||||
4
.github/workflows/validate.yml
vendored
4
.github/workflows/validate.yml
vendored
@ -48,7 +48,7 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
-
|
||||
name: Generate
|
||||
shell: 'script --return --quiet --command "bash {0}"'
|
||||
@ -74,7 +74,7 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
-
|
||||
name: Run
|
||||
shell: 'script --return --quiet --command "bash {0}"'
|
||||
|
||||
@ -5,7 +5,7 @@ run:
|
||||
# which causes it to fallback to go1.17 semantics.
|
||||
#
|
||||
# TODO(thaJeztah): update "usetesting" settings to enable go1.24 features once our minimum version is go1.24
|
||||
go: "1.24.5"
|
||||
go: "1.25.3"
|
||||
|
||||
timeout: 5m
|
||||
|
||||
@ -86,6 +86,8 @@ linters:
|
||||
desc: Use github.com/moby/sys/userns instead.
|
||||
- pkg: "github.com/containerd/containerd/platforms"
|
||||
desc: The containerd platforms package was migrated to a separate module. Use github.com/containerd/platforms instead.
|
||||
- pkg: "github.com/docker/docker/errdefs"
|
||||
desc: Use github.com/containerd/errdefs instead.
|
||||
- pkg: "github.com/docker/docker/pkg/system"
|
||||
desc: This package should not be used unless strictly necessary.
|
||||
- pkg: "github.com/docker/distribution/uuid"
|
||||
@ -124,10 +126,9 @@ linters:
|
||||
no-unaliased: true
|
||||
|
||||
alias:
|
||||
# Enforce alias to prevent it accidentally being used instead of our
|
||||
# own errdefs package (or vice-versa).
|
||||
- pkg: github.com/containerd/errdefs
|
||||
alias: cerrdefs
|
||||
# Should no longer be aliased, because we no longer allow moby/docker errdefs.
|
||||
- pkg: "github.com/docker/docker/errdefs"
|
||||
alias: ""
|
||||
- pkg: github.com/opencontainers/image-spec/specs-go/v1
|
||||
alias: ocispec
|
||||
# Enforce that gotest.tools/v3/assert/cmp is always aliased as "is"
|
||||
@ -153,6 +154,7 @@ linters:
|
||||
arguments: [200]
|
||||
- name: unused-receiver # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unused-receiver
|
||||
- name: use-any # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#use-any
|
||||
- name: use-errors-new # https://github.com/mgechev/revive/blob/HEAD/RULES_DESCRIPTIONS.md#use-errors-new
|
||||
|
||||
usetesting:
|
||||
os-chdir: false # FIXME(thaJeztah): Disable `os.Chdir()` detections; should be automatically disabled on Go < 1.24; see https://github.com/docker/cli/pull/5835#issuecomment-2665302478
|
||||
@ -221,6 +223,14 @@ linters:
|
||||
linters:
|
||||
- staticcheck
|
||||
|
||||
# Ignore deprecation linting for cli/command/stack/*.
|
||||
#
|
||||
# FIXME(thaJeztah): remove exception once these functions are un-exported or internal; see https://github.com/docker/cli/pull/6389
|
||||
- text: '^(SA1019): '
|
||||
path: "cli/command/stack"
|
||||
linters:
|
||||
- staticcheck
|
||||
|
||||
# Log a warning if an exclusion rule is unused.
|
||||
# Default: false
|
||||
warn-unused: true
|
||||
|
||||
11
.mailmap
11
.mailmap
@ -64,11 +64,14 @@ Arko Dasgupta <arko@tetrate.io> <arko.dasgupta@docker.com>
|
||||
Arko Dasgupta <arko@tetrate.io> <arkodg@users.noreply.github.com>
|
||||
Arnaud Porterie <icecrime@gmail.com>
|
||||
Arnaud Porterie <icecrime@gmail.com> <arnaud.porterie@docker.com>
|
||||
Arthur Flageul <arthur.flageul@gmail.com>
|
||||
Arthur Flageul <arthur.flageul@gmail.com> <arthur.flageul@docker.com>
|
||||
Arthur Gautier <baloo@gandi.net> <superbaloo+registrations.github@superbaloo.net>
|
||||
Arthur Peka <arthur.peka@outlook.com> <arthrp@users.noreply.github.com>
|
||||
Austin Vazquez <austin.vazquez.dev@gmail.com>
|
||||
Austin Vazquez <austin.vazquez.dev@gmail.com> <55906459+austinvazquez@users.noreply.github.com>
|
||||
Austin Vazquez <austin.vazquez.dev@gmail.com> <macedonv@amazon.com>
|
||||
Austin Vazquez <austin.vazquez@docker.com>
|
||||
Austin Vazquez <austin.vazquez@docker.com> <55906459+austinvazquez@users.noreply.github.com>
|
||||
Austin Vazquez <austin.vazquez@docker.com> <austin.vazquez.dev@gmail.com>
|
||||
Austin Vazquez <austin.vazquez@docker.com> <macedonv@amazon.com>
|
||||
Avi Miller <avi.miller@oracle.com> <avi.miller@gmail.com>
|
||||
Ben Bonnefoy <frenchben@docker.com>
|
||||
Ben Golub <ben.golub@dotcloud.com>
|
||||
@ -150,6 +153,8 @@ Dave Henderson <dhenderson@gmail.com> <Dave.Henderson@ca.ibm.com>
|
||||
Dave Tucker <dt@docker.com> <dave@dtucker.co.uk>
|
||||
David Alvarez <david.alvarez@flyeralarm.com>
|
||||
David Alvarez <david.alvarez@flyeralarm.com> <busilezas@gmail.com>
|
||||
David Dooling <david.dooling@docker.com>
|
||||
David Dooling <david.dooling@docker.com> <dooling@gmail.com>
|
||||
David Karlsson <david.karlsson@docker.com>
|
||||
David Karlsson <david.karlsson@docker.com> <35727626+dvdksn@users.noreply.github.com>
|
||||
David M. Karr <davidmichaelkarr@gmail.com>
|
||||
|
||||
12
AUTHORS
12
AUTHORS
@ -63,6 +63,7 @@ Andreas Köhler <andi5.py@gmx.net>
|
||||
Andres G. Aragoneses <knocte@gmail.com>
|
||||
Andres Leon Rangel <aleon1220@gmail.com>
|
||||
Andrew France <andrew@avito.co.uk>
|
||||
Andrew He <he.andrew.mail@gmail.com>
|
||||
Andrew Hsu <andrewhsu@docker.com>
|
||||
Andrew Macpherson <hopscotch23@gmail.com>
|
||||
Andrew McDonnell <bugs@andrewmcdonnell.net>
|
||||
@ -86,11 +87,12 @@ Archimedes Trajano <developer@trajano.net>
|
||||
Arko Dasgupta <arko@tetrate.io>
|
||||
Arnaud Porterie <icecrime@gmail.com>
|
||||
Arnaud Rebillout <elboulangero@gmail.com>
|
||||
Arthur Flageul <arthur.flageul@gmail.com>
|
||||
Arthur Peka <arthur.peka@outlook.com>
|
||||
Ashly Mathew <ashly.mathew@sap.com>
|
||||
Ashwini Oruganti <ashwini.oruganti@gmail.com>
|
||||
Aslam Ahemad <aslamahemad@gmail.com>
|
||||
Austin Vazquez <austin.vazquez.dev@gmail.com>
|
||||
Austin Vazquez <austin.vazquez@docker.com>
|
||||
Azat Khuyiyakhmetov <shadow_uz@mail.ru>
|
||||
Bardia Keyoumarsi <bkeyouma@ucsc.edu>
|
||||
Barnaby Gray <barnaby@pickle.me.uk>
|
||||
@ -135,10 +137,12 @@ Cao Weiwei <cao.weiwei30@zte.com.cn>
|
||||
Carlo Mion <mion00@gmail.com>
|
||||
Carlos Alexandro Becker <caarlos0@gmail.com>
|
||||
Carlos de Paula <me@carlosedp.com>
|
||||
carsontham <carsontham@outlook.com>
|
||||
Carston Schilds <Carston.Schilds@visier.com>
|
||||
Casey Korver <casey@korver.dev>
|
||||
Ce Gao <ce.gao@outlook.com>
|
||||
Cedric Davies <cedricda@microsoft.com>
|
||||
Cesar Talledo <cesar.talledo@docker.com>
|
||||
Cezar Sa Espinola <cezarsa@gmail.com>
|
||||
Chad Faragher <wyckster@hotmail.com>
|
||||
Chao Wang <wangchao.fnst@cn.fujitsu.com>
|
||||
@ -220,7 +224,7 @@ David Alvarez <david.alvarez@flyeralarm.com>
|
||||
David Beitey <david@davidjb.com>
|
||||
David Calavera <david.calavera@gmail.com>
|
||||
David Cramer <davcrame@cisco.com>
|
||||
David Dooling <dooling@gmail.com>
|
||||
David Dooling <david.dooling@docker.com>
|
||||
David Gageot <david@gageot.net>
|
||||
David Karlsson <david.karlsson@docker.com>
|
||||
David le Blanc <systemmonkey42@users.noreply.github.com>
|
||||
@ -265,6 +269,7 @@ Eli Uriegas <eli.uriegas@docker.com>
|
||||
Eli Uriegas <seemethere101@gmail.com>
|
||||
Elias Faxö <elias.faxo@tre.se>
|
||||
Elliot Luo <956941328@qq.com>
|
||||
Eng Zer Jun <engzerjun@gmail.com>
|
||||
Eric Bode <eric.bode@foundries.io>
|
||||
Eric Curtin <ericcurtin17@gmail.com>
|
||||
Eric Engestrom <eric@engestrom.ch>
|
||||
@ -345,6 +350,7 @@ Henning Sprang <henning.sprang@gmail.com>
|
||||
Henry N <henrynmail-github@yahoo.de>
|
||||
Hernan Garcia <hernandanielg@gmail.com>
|
||||
Hongbin Lu <hongbin034@gmail.com>
|
||||
Hossein Abbasi <16090309+hsnabszhdn@users.noreply.github.com>
|
||||
Hu Keping <hukeping@huawei.com>
|
||||
Huayi Zhang <irachex@gmail.com>
|
||||
Hugo Chastel <Hugo-C@users.noreply.github.com>
|
||||
@ -595,6 +601,7 @@ Michael Prokop <github@michael-prokop.at>
|
||||
Michael Scharf <github@scharf.gr>
|
||||
Michael Spetsiotis <michael_spets@hotmail.com>
|
||||
Michael Steinert <mike.steinert@gmail.com>
|
||||
Michael Tews <michael@tews.dev>
|
||||
Michael West <mwest@mdsol.com>
|
||||
Michal Minář <miminar@redhat.com>
|
||||
Michał Czeraszkiewicz <czerasz@gmail.com>
|
||||
@ -896,6 +903,7 @@ Wenlong Zhang <zhangwenlong@loongson.cn>
|
||||
Wenzhi Liang <wenzhi.liang@gmail.com>
|
||||
Wes Morgan <cap10morgan@gmail.com>
|
||||
Wewang Xiaorenfine <wang.xiaoren@zte.com.cn>
|
||||
Will Wang <willww64@gmail.com>
|
||||
William Henry <whenry@redhat.com>
|
||||
Xianglin Gao <xlgao@zju.edu.cn>
|
||||
Xiaodong Liu <liuxiaodong@loongson.cn>
|
||||
|
||||
26
Dockerfile
26
Dockerfile
@ -5,33 +5,39 @@ ARG BASE_VARIANT=alpine
|
||||
# ALPINE_VERSION sets the version of the alpine base image to use, including for the golang image.
|
||||
# It must be a supported tag in the docker.io/library/alpine image repository
|
||||
# that's also available as alpine image variant for the Golang version used.
|
||||
ARG ALPINE_VERSION=3.21
|
||||
ARG ALPINE_VERSION=3.22
|
||||
ARG BASE_DEBIAN_DISTRO=bookworm
|
||||
|
||||
ARG GO_VERSION=1.24.5
|
||||
ARG XX_VERSION=1.6.1
|
||||
ARG GOVERSIONINFO_VERSION=v1.4.1
|
||||
ARG GO_VERSION=1.25.3
|
||||
|
||||
# XX_VERSION specifies the version of the xx utility to use.
|
||||
# It must be a valid tag in the docker.io/tonistiigi/xx image repository.
|
||||
ARG XX_VERSION=1.7.0
|
||||
|
||||
# GOVERSIONINFO_VERSION is the version of GoVersionInfo to install.
|
||||
# It must be a valid tag from https://github.com/josephspurrier/goversioninfo
|
||||
ARG GOVERSIONINFO_VERSION=v1.5.0
|
||||
|
||||
# GOTESTSUM_VERSION sets the version of gotestsum to install in the dev container.
|
||||
# It must be a valid tag in the https://github.com/gotestyourself/gotestsum repository.
|
||||
ARG GOTESTSUM_VERSION=v1.12.3
|
||||
ARG GOTESTSUM_VERSION=v1.13.0
|
||||
|
||||
# BUILDX_VERSION sets the version of buildx to use for the e2e tests.
|
||||
# It must be a tag in the docker.io/docker/buildx-bin image repository
|
||||
# on Docker Hub.
|
||||
ARG BUILDX_VERSION=0.25.0
|
||||
ARG BUILDX_VERSION=0.29.1
|
||||
|
||||
# COMPOSE_VERSION is the version of compose to install in the dev container.
|
||||
# It must be a tag in the docker.io/docker/compose-bin image repository
|
||||
# on Docker Hub.
|
||||
ARG COMPOSE_VERSION=v2.38.2
|
||||
ARG COMPOSE_VERSION=v2.40.0
|
||||
|
||||
FROM --platform=$BUILDPLATFORM tonistiigi/xx:${XX_VERSION} AS xx
|
||||
|
||||
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS build-base-alpine
|
||||
ENV GOTOOLCHAIN=local
|
||||
COPY --link --from=xx / /
|
||||
RUN apk add --no-cache bash clang lld llvm file git
|
||||
RUN apk add --no-cache bash clang lld llvm file git git-daemon
|
||||
WORKDIR /go/src/github.com/docker/cli
|
||||
|
||||
FROM build-base-alpine AS build-alpine
|
||||
@ -116,10 +122,6 @@ FROM docker/buildx-bin:${BUILDX_VERSION} AS buildx
|
||||
FROM docker/compose-bin:${COMPOSE_VERSION} AS compose
|
||||
|
||||
FROM e2e-base-${BASE_VARIANT} AS e2e
|
||||
ARG NOTARY_VERSION=v0.6.1
|
||||
ADD --chmod=0755 https://github.com/theupdateframework/notary/releases/download/${NOTARY_VERSION}/notary-Linux-amd64 /usr/local/bin/notary
|
||||
COPY --link e2e/testdata/notary/root-ca.cert /usr/share/ca-certificates/notary.cert
|
||||
RUN echo 'notary.cert' >> /etc/ca-certificates.conf && update-ca-certificates
|
||||
COPY --link --from=gotestsum /out/gotestsum /usr/bin/gotestsum
|
||||
COPY --link --from=build /out ./build/
|
||||
COPY --link --from=build-plugins /out ./build/
|
||||
|
||||
@ -8,6 +8,7 @@ import (
|
||||
"github.com/docker/cli/cli-plugins/metadata"
|
||||
"github.com/docker/cli/cli-plugins/plugin"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@ -25,7 +26,7 @@ func main() {
|
||||
Short: "Print the API version of the server",
|
||||
RunE: func(_ *cobra.Command, _ []string) error {
|
||||
apiClient := dockerCLI.Client()
|
||||
ping, err := apiClient.Ping(context.Background())
|
||||
ping, err := apiClient.Ping(context.Background(), client.PingOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -1,30 +0,0 @@
|
||||
package manager
|
||||
|
||||
import "github.com/docker/cli/cli-plugins/metadata"
|
||||
|
||||
const (
|
||||
// CommandAnnotationPlugin is added to every stub command added by
|
||||
// AddPluginCommandStubs with the value "true" and so can be
|
||||
// used to distinguish plugin stubs from regular commands.
|
||||
CommandAnnotationPlugin = metadata.CommandAnnotationPlugin
|
||||
|
||||
// CommandAnnotationPluginVendor is added to every stub command
|
||||
// added by AddPluginCommandStubs and contains the vendor of
|
||||
// that plugin.
|
||||
CommandAnnotationPluginVendor = metadata.CommandAnnotationPluginVendor
|
||||
|
||||
// CommandAnnotationPluginVersion is added to every stub command
|
||||
// added by AddPluginCommandStubs and contains the version of
|
||||
// that plugin.
|
||||
CommandAnnotationPluginVersion = metadata.CommandAnnotationPluginVersion
|
||||
|
||||
// CommandAnnotationPluginInvalid is added to any stub command
|
||||
// added by AddPluginCommandStubs for an invalid command (that
|
||||
// is, one which failed it's candidate test) and contains the
|
||||
// reason for the failure.
|
||||
CommandAnnotationPluginInvalid = metadata.CommandAnnotationPluginInvalid
|
||||
|
||||
// CommandAnnotationPluginCommandPath is added to overwrite the
|
||||
// command path for a plugin invocation.
|
||||
CommandAnnotationPluginCommandPath = metadata.CommandAnnotationPluginCommandPath
|
||||
)
|
||||
@ -6,12 +6,6 @@ import (
|
||||
"github.com/docker/cli/cli-plugins/metadata"
|
||||
)
|
||||
|
||||
// Candidate represents a possible plugin candidate, for mocking purposes
|
||||
type Candidate interface {
|
||||
Path() string
|
||||
Metadata() ([]byte, error)
|
||||
}
|
||||
|
||||
type candidate struct {
|
||||
path string
|
||||
}
|
||||
|
||||
@ -32,14 +32,12 @@ func (c *fakeCandidate) Metadata() ([]byte, error) {
|
||||
func TestValidateCandidate(t *testing.T) {
|
||||
const (
|
||||
goodPluginName = metadata.NamePrefix + "goodplugin"
|
||||
builtinName = metadata.NamePrefix + "builtin"
|
||||
builtinAlias = metadata.NamePrefix + "alias"
|
||||
|
||||
builtinName = metadata.NamePrefix + "builtin"
|
||||
builtinAlias = metadata.NamePrefix + "alias"
|
||||
|
||||
badPrefixPath = "/usr/local/libexec/cli-plugins/wobble"
|
||||
badNamePath = "/usr/local/libexec/cli-plugins/docker-123456"
|
||||
goodPluginPath = "/usr/local/libexec/cli-plugins/" + goodPluginName
|
||||
metaExperimental = `{"SchemaVersion": "0.1.0", "Vendor": "e2e-testing", "Experimental": true}`
|
||||
badPrefixPath = "/usr/local/libexec/cli-plugins/wobble"
|
||||
badNamePath = "/usr/local/libexec/cli-plugins/docker-123456"
|
||||
goodPluginPath = "/usr/local/libexec/cli-plugins/" + goodPluginName
|
||||
)
|
||||
|
||||
fakeroot := &cobra.Command{Use: "docker"}
|
||||
@ -51,31 +49,103 @@ func TestValidateCandidate(t *testing.T) {
|
||||
})
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
c *fakeCandidate
|
||||
name string
|
||||
plugin *fakeCandidate
|
||||
|
||||
// Either err or invalid may be non-empty, but not both (both can be empty for a good plugin).
|
||||
err string
|
||||
invalid string
|
||||
expVer string
|
||||
}{
|
||||
/* Each failing one of the tests */
|
||||
{name: "empty path", c: &fakeCandidate{path: ""}, err: "plugin candidate path cannot be empty"},
|
||||
{name: "bad prefix", c: &fakeCandidate{path: badPrefixPath}, err: fmt.Sprintf("does not have %q prefix", metadata.NamePrefix)},
|
||||
{name: "bad path", c: &fakeCandidate{path: badNamePath}, invalid: "did not match"},
|
||||
{name: "builtin command", c: &fakeCandidate{path: builtinName}, invalid: `plugin "builtin" duplicates builtin command`},
|
||||
{name: "builtin alias", c: &fakeCandidate{path: builtinAlias}, invalid: `plugin "alias" duplicates an alias of builtin command "builtin"`},
|
||||
{name: "fetch failure", c: &fakeCandidate{path: goodPluginPath, exec: false}, invalid: fmt.Sprintf("failed to fetch metadata: faked a failure to exec %q", goodPluginPath)},
|
||||
{name: "metadata not json", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `xyzzy`}, invalid: "invalid character"},
|
||||
{name: "empty schemaversion", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{}`}, invalid: `plugin SchemaVersion "" is not valid`},
|
||||
{name: "invalid schemaversion", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "xyzzy"}`}, invalid: `plugin SchemaVersion "xyzzy" is not valid`},
|
||||
{name: "no vendor", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0"}`}, invalid: "plugin metadata does not define a vendor"},
|
||||
{name: "empty vendor", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0", "Vendor": ""}`}, invalid: "plugin metadata does not define a vendor"},
|
||||
// This one should work
|
||||
{name: "valid", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0", "Vendor": "e2e-testing"}`}},
|
||||
{name: "experimental + allowing experimental", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: metaExperimental}},
|
||||
// Invalid cases.
|
||||
{
|
||||
name: "empty path",
|
||||
plugin: &fakeCandidate{path: ""},
|
||||
err: "plugin candidate path cannot be empty",
|
||||
},
|
||||
{
|
||||
name: "bad prefix",
|
||||
plugin: &fakeCandidate{path: badPrefixPath},
|
||||
err: fmt.Sprintf("does not have %q prefix", metadata.NamePrefix),
|
||||
},
|
||||
{
|
||||
name: "bad path",
|
||||
plugin: &fakeCandidate{path: badNamePath},
|
||||
invalid: "did not match",
|
||||
},
|
||||
{
|
||||
name: "builtin command",
|
||||
plugin: &fakeCandidate{path: builtinName},
|
||||
invalid: `plugin "builtin" duplicates builtin command`,
|
||||
},
|
||||
{
|
||||
name: "builtin alias",
|
||||
plugin: &fakeCandidate{path: builtinAlias},
|
||||
invalid: `plugin "alias" duplicates an alias of builtin command "builtin"`,
|
||||
},
|
||||
{
|
||||
name: "fetch failure",
|
||||
plugin: &fakeCandidate{path: goodPluginPath, exec: false},
|
||||
invalid: fmt.Sprintf("failed to fetch metadata: faked a failure to exec %q", goodPluginPath),
|
||||
},
|
||||
{
|
||||
name: "metadata not json",
|
||||
plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `xyzzy`},
|
||||
invalid: "invalid character",
|
||||
},
|
||||
{
|
||||
name: "empty schemaversion",
|
||||
plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{}`},
|
||||
invalid: `plugin SchemaVersion version cannot be empty`,
|
||||
},
|
||||
{
|
||||
name: "invalid schemaversion",
|
||||
plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "xyzzy"}`},
|
||||
invalid: `plugin SchemaVersion "xyzzy" has wrong format: must be <major>.<minor>.<patch>`,
|
||||
},
|
||||
{
|
||||
name: "invalid schemaversion major",
|
||||
plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "2.0.0"}`},
|
||||
invalid: `plugin SchemaVersion "2.0.0" is not supported: must be lower than 2.0.0`,
|
||||
},
|
||||
{
|
||||
name: "no vendor",
|
||||
plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0"}`},
|
||||
invalid: "plugin metadata does not define a vendor",
|
||||
},
|
||||
{
|
||||
name: "empty vendor",
|
||||
plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0", "Vendor": ""}`},
|
||||
invalid: "plugin metadata does not define a vendor",
|
||||
},
|
||||
|
||||
// Valid cases.
|
||||
{
|
||||
name: "valid",
|
||||
plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0", "Vendor": "e2e-testing"}`},
|
||||
expVer: "0.1.0",
|
||||
},
|
||||
{
|
||||
// Including the deprecated "experimental" field should not break processing.
|
||||
name: "with legacy experimental",
|
||||
plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0", "Vendor": "e2e-testing", "Experimental": true}`},
|
||||
expVer: "0.1.0",
|
||||
},
|
||||
{
|
||||
// note that this may not be supported by older CLIs
|
||||
name: "new minor schema version",
|
||||
plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.2.0", "Vendor": "e2e-testing"}`},
|
||||
expVer: "0.2.0",
|
||||
},
|
||||
{
|
||||
// note that this may not be supported by older CLIs
|
||||
name: "new major schema version",
|
||||
plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "1.0.0", "Vendor": "e2e-testing"}`},
|
||||
expVer: "1.0.0",
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
p, err := newPlugin(tc.c, fakeroot.Commands())
|
||||
p, err := newPlugin(tc.plugin, fakeroot.Commands())
|
||||
switch {
|
||||
case tc.err != "":
|
||||
assert.ErrorContains(t, err, tc.err)
|
||||
@ -86,7 +156,7 @@ func TestValidateCandidate(t *testing.T) {
|
||||
default:
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, metadata.NamePrefix+p.Name, goodPluginName)
|
||||
assert.Equal(t, p.SchemaVersion, "0.1.0")
|
||||
assert.Equal(t, p.SchemaVersion, tc.expVer)
|
||||
assert.Equal(t, p.Vendor, "e2e-testing")
|
||||
}
|
||||
})
|
||||
|
||||
@ -23,11 +23,6 @@ func (e *pluginError) Error() string {
|
||||
return e.cause.Error()
|
||||
}
|
||||
|
||||
// Cause satisfies the errors.causer interface for pluginError.
|
||||
func (e *pluginError) Cause() error {
|
||||
return e.cause
|
||||
}
|
||||
|
||||
// Unwrap provides compatibility for Go 1.13 error chains.
|
||||
func (e *pluginError) Unwrap() error {
|
||||
return e.cause
|
||||
@ -41,14 +36,11 @@ func (e *pluginError) MarshalText() (text []byte, err error) {
|
||||
// wrapAsPluginError wraps an error in a pluginError with an
|
||||
// additional message.
|
||||
func wrapAsPluginError(err error, msg string) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
return &pluginError{cause: fmt.Errorf("%s: %w", msg, err)}
|
||||
}
|
||||
|
||||
// NewPluginError creates a new pluginError, analogous to
|
||||
// newPluginError creates a new pluginError, analogous to
|
||||
// errors.Errorf.
|
||||
func NewPluginError(msg string, args ...any) error {
|
||||
func newPluginError(msg string, args ...any) error {
|
||||
return &pluginError{cause: fmt.Errorf(msg, args...)}
|
||||
}
|
||||
|
||||
@ -10,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
func TestPluginError(t *testing.T) {
|
||||
err := NewPluginError("new error")
|
||||
err := newPluginError("new error")
|
||||
assert.Check(t, is.Error(err, "new error"))
|
||||
|
||||
inner := errors.New("testing")
|
||||
@ -21,4 +21,7 @@ func TestPluginError(t *testing.T) {
|
||||
actual, err := json.Marshal(err)
|
||||
assert.Check(t, err)
|
||||
assert.Check(t, is.Equal(`"wrapping: testing"`, string(actual)))
|
||||
|
||||
err = wrapAsPluginError(nil, "wrapping")
|
||||
assert.Check(t, is.Error(err, "wrapping: %!w(<nil>)"))
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ package manager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
@ -9,28 +10,16 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/containerd/errdefs"
|
||||
"github.com/docker/cli/cli-plugins/metadata"
|
||||
"github.com/docker/cli/cli/config"
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
"github.com/docker/cli/cli/debug"
|
||||
"github.com/fvbommel/sortorder"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
const (
|
||||
// ReexecEnvvar is the name of an ennvar which is set to the command
|
||||
// used to originally invoke the docker CLI when executing a
|
||||
// plugin. Assuming $PATH and $CWD remain unchanged this should allow
|
||||
// the plugin to re-execute the original CLI.
|
||||
ReexecEnvvar = metadata.ReexecEnvvar
|
||||
|
||||
// ResourceAttributesEnvvar is the name of the envvar that includes additional
|
||||
// resource attributes for OTEL.
|
||||
//
|
||||
// Deprecated: The "OTEL_RESOURCE_ATTRIBUTES" env-var is part of the OpenTelemetry specification; users should define their own const for this. This const will be removed in the next release.
|
||||
ResourceAttributesEnvvar = "OTEL_RESOURCE_ATTRIBUTES"
|
||||
)
|
||||
|
||||
// errPluginNotFound is the error returned when a plugin could not be found.
|
||||
type errPluginNotFound string
|
||||
|
||||
@ -40,17 +29,6 @@ func (e errPluginNotFound) Error() string {
|
||||
return "Error: No such CLI plugin: " + string(e)
|
||||
}
|
||||
|
||||
type notFound interface{ NotFound() }
|
||||
|
||||
// IsNotFound is true if the given error is due to a plugin not being found.
|
||||
func IsNotFound(err error) bool {
|
||||
if e, ok := err.(*pluginError); ok {
|
||||
err = e.Cause()
|
||||
}
|
||||
_, ok := err.(notFound)
|
||||
return ok
|
||||
}
|
||||
|
||||
// getPluginDirs returns the platform-specific locations to search for plugins
|
||||
// in order of preference.
|
||||
//
|
||||
@ -81,9 +59,17 @@ func addPluginCandidatesFromDir(res map[string][]string, d string) {
|
||||
return
|
||||
}
|
||||
for _, dentry := range dentries {
|
||||
switch dentry.Type() & os.ModeType { //nolint:exhaustive,nolintlint // no need to include all possible file-modes in this list
|
||||
case 0, os.ModeSymlink:
|
||||
// Regular file or symlink, keep going
|
||||
switch mode := dentry.Type() & os.ModeType; mode { //nolint:exhaustive,nolintlint // no need to include all possible file-modes in this list
|
||||
case os.ModeSymlink:
|
||||
if !debug.IsEnabled() {
|
||||
// Skip broken symlinks unless debug is enabled. With debug
|
||||
// enabled, this will print a warning in "docker info".
|
||||
if _, err := os.Stat(filepath.Join(d, dentry.Name())); errors.Is(err, os.ErrNotExist) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
case 0:
|
||||
// Regular file, keep going
|
||||
default:
|
||||
// Something else, ignore.
|
||||
continue
|
||||
@ -127,7 +113,7 @@ func getPlugin(name string, pluginDirs []string, rootcmd *cobra.Command) (*Plugi
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !IsNotFound(p.Err) {
|
||||
if !errdefs.IsNotFound(p.Err) {
|
||||
p.ShadowedPaths = paths[1:]
|
||||
}
|
||||
return &p, nil
|
||||
@ -164,7 +150,7 @@ func ListPlugins(dockerCli config.Provider, rootcmd *cobra.Command) ([]Plugin, e
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !IsNotFound(p.Err) {
|
||||
if !errdefs.IsNotFound(p.Err) {
|
||||
p.ShadowedPaths = paths[1:]
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
@ -185,15 +171,15 @@ func ListPlugins(dockerCli config.Provider, rootcmd *cobra.Command) ([]Plugin, e
|
||||
return plugins, nil
|
||||
}
|
||||
|
||||
// PluginRunCommand returns an "os/exec".Cmd which when .Run() will execute the named plugin.
|
||||
// PluginRunCommand returns an [os/exec.Cmd] which when [os/exec.Cmd.Run] will execute the named plugin.
|
||||
// The rootcmd argument is referenced to determine the set of builtin commands in order to detect conficts.
|
||||
// The error returned satisfies the IsNotFound() predicate if no plugin was found or if the first candidate plugin was invalid somehow.
|
||||
// The error returned satisfies the [errdefs.IsNotFound] predicate if no plugin was found or if the first candidate plugin was invalid somehow.
|
||||
func PluginRunCommand(dockerCli config.Provider, name string, rootcmd *cobra.Command) (*exec.Cmd, error) {
|
||||
// This uses the full original args, not the args which may
|
||||
// have been provided by cobra to our caller. This is because
|
||||
// they lack e.g. global options which we must propagate here.
|
||||
args := os.Args[1:]
|
||||
if !pluginNameRe.MatchString(name) {
|
||||
if !isValidPluginName(name) {
|
||||
// We treat this as "not found" so that callers will
|
||||
// fallback to their "invalid" command path.
|
||||
return nil, errPluginNotFound(name)
|
||||
|
||||
@ -5,6 +5,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/containerd/errdefs"
|
||||
"github.com/docker/cli/cli/config"
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
"github.com/docker/cli/internal/test"
|
||||
@ -37,7 +38,7 @@ func TestListPluginCandidates(t *testing.T) {
|
||||
"plugins3-target", // Will be referenced as a symlink from below
|
||||
fs.WithFile("docker-plugin1", ""),
|
||||
fs.WithDir("ignored3"),
|
||||
fs.WithSymlink("docker-brokensymlink", "broken"), // A broken symlink is still a candidate (but would fail tests later)
|
||||
fs.WithSymlink("docker-brokensymlink", "broken"), // A broken symlink is ignored
|
||||
fs.WithFile("non-plugin-symlinked", ""), // This shouldn't appear, but ...
|
||||
fs.WithSymlink("docker-symlinked", "non-plugin-symlinked"), // ... this link to it should.
|
||||
),
|
||||
@ -71,9 +72,6 @@ func TestListPluginCandidates(t *testing.T) {
|
||||
"hardlink2": {
|
||||
dir.Join("plugins2", "docker-hardlink2"),
|
||||
},
|
||||
"brokensymlink": {
|
||||
dir.Join("plugins3", "docker-brokensymlink"),
|
||||
},
|
||||
"symlinked": {
|
||||
dir.Join("plugins3", "docker-symlinked"),
|
||||
},
|
||||
@ -131,7 +129,7 @@ echo '{"SchemaVersion":"0.1.0"}'`, fs.WithMode(0o777)),
|
||||
|
||||
_, err = GetPlugin("ccc", cli, &cobra.Command{})
|
||||
assert.Error(t, err, "Error: No such CLI plugin: ccc")
|
||||
assert.Assert(t, IsNotFound(err))
|
||||
assert.Assert(t, errdefs.IsNotFound(err))
|
||||
}
|
||||
|
||||
func TestListPluginsIsSorted(t *testing.T) {
|
||||
@ -166,8 +164,8 @@ func TestErrPluginNotFound(t *testing.T) {
|
||||
var err error = errPluginNotFound("test")
|
||||
err.(errPluginNotFound).NotFound()
|
||||
assert.Error(t, err, "Error: No such CLI plugin: test")
|
||||
assert.Assert(t, IsNotFound(err))
|
||||
assert.Assert(t, !IsNotFound(nil))
|
||||
assert.Assert(t, errdefs.IsNotFound(err))
|
||||
assert.Assert(t, !errdefs.IsNotFound(nil))
|
||||
}
|
||||
|
||||
func TestGetPluginDirs(t *testing.T) {
|
||||
|
||||
@ -1,23 +0,0 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"github.com/docker/cli/cli-plugins/metadata"
|
||||
)
|
||||
|
||||
const (
|
||||
// NamePrefix is the prefix required on all plugin binary names
|
||||
NamePrefix = metadata.NamePrefix
|
||||
|
||||
// MetadataSubcommandName is the name of the plugin subcommand
|
||||
// which must be supported by every plugin and returns the
|
||||
// plugin metadata.
|
||||
MetadataSubcommandName = metadata.MetadataSubcommandName
|
||||
|
||||
// HookSubcommandName is the name of the plugin subcommand
|
||||
// which must be implemented by plugins declaring support
|
||||
// for hooks in their metadata.
|
||||
HookSubcommandName = metadata.HookSubcommandName
|
||||
)
|
||||
|
||||
// Metadata provided by the plugin.
|
||||
type Metadata = metadata.Metadata
|
||||
@ -2,21 +2,20 @@ package manager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli-plugins/metadata"
|
||||
"github.com/docker/cli/internal/lazyregexp"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var pluginNameRe = lazyregexp.New("^[a-z][a-z0-9]*$")
|
||||
|
||||
// Plugin represents a potential plugin with all it's metadata.
|
||||
type Plugin struct {
|
||||
metadata.Metadata
|
||||
@ -31,12 +30,34 @@ type Plugin struct {
|
||||
ShadowedPaths []string `json:",omitempty"`
|
||||
}
|
||||
|
||||
// MarshalJSON implements [json.Marshaler] to handle marshaling the
|
||||
// [Plugin.Err] field (Go doesn't marshal errors by default).
|
||||
func (p *Plugin) MarshalJSON() ([]byte, error) {
|
||||
type Alias Plugin // avoid recursion
|
||||
|
||||
cp := *p // shallow copy to avoid mutating original
|
||||
|
||||
if cp.Err != nil {
|
||||
if _, ok := cp.Err.(encoding.TextMarshaler); !ok {
|
||||
cp.Err = &pluginError{cp.Err}
|
||||
}
|
||||
}
|
||||
|
||||
return json.Marshal((*Alias)(&cp))
|
||||
}
|
||||
|
||||
// pluginCandidate represents a possible plugin candidate, for mocking purposes.
|
||||
type pluginCandidate interface {
|
||||
Path() string
|
||||
Metadata() ([]byte, error)
|
||||
}
|
||||
|
||||
// newPlugin determines if the given candidate is valid and returns a
|
||||
// Plugin. If the candidate fails one of the tests then `Plugin.Err`
|
||||
// is set, and is always a `pluginError`, but the `Plugin` is still
|
||||
// returned with no error. An error is only returned due to a
|
||||
// non-recoverable error.
|
||||
func newPlugin(c Candidate, cmds []*cobra.Command) (Plugin, error) {
|
||||
func newPlugin(c pluginCandidate, cmds []*cobra.Command) (Plugin, error) {
|
||||
path := c.Path()
|
||||
if path == "" {
|
||||
return Plugin{}, errors.New("plugin candidate path cannot be empty")
|
||||
@ -62,8 +83,8 @@ func newPlugin(c Candidate, cmds []*cobra.Command) (Plugin, error) {
|
||||
}
|
||||
|
||||
// Now apply the candidate tests, so these update p.Err.
|
||||
if !pluginNameRe.MatchString(p.Name) {
|
||||
p.Err = NewPluginError("plugin candidate %q did not match %q", p.Name, pluginNameRe.String())
|
||||
if !isValidPluginName(p.Name) {
|
||||
p.Err = newPluginError("plugin candidate %q did not match %q", p.Name, pluginNameFormat)
|
||||
return p, nil
|
||||
}
|
||||
|
||||
@ -75,11 +96,11 @@ func newPlugin(c Candidate, cmds []*cobra.Command) (Plugin, error) {
|
||||
continue
|
||||
}
|
||||
if cmd.Name() == p.Name {
|
||||
p.Err = NewPluginError("plugin %q duplicates builtin command", p.Name)
|
||||
p.Err = newPluginError("plugin %q duplicates builtin command", p.Name)
|
||||
return p, nil
|
||||
}
|
||||
if cmd.HasAlias(p.Name) {
|
||||
p.Err = NewPluginError("plugin %q duplicates an alias of builtin command %q", p.Name, cmd.Name())
|
||||
p.Err = newPluginError("plugin %q duplicates an alias of builtin command %q", p.Name, cmd.Name())
|
||||
return p, nil
|
||||
}
|
||||
}
|
||||
@ -95,17 +116,42 @@ func newPlugin(c Candidate, cmds []*cobra.Command) (Plugin, error) {
|
||||
p.Err = wrapAsPluginError(err, "invalid metadata")
|
||||
return p, nil
|
||||
}
|
||||
if p.Metadata.SchemaVersion != "0.1.0" {
|
||||
p.Err = NewPluginError("plugin SchemaVersion %q is not valid, must be 0.1.0", p.Metadata.SchemaVersion)
|
||||
if err := validateSchemaVersion(p.Metadata.SchemaVersion); err != nil {
|
||||
p.Err = &pluginError{cause: err}
|
||||
return p, nil
|
||||
}
|
||||
if p.Metadata.Vendor == "" {
|
||||
p.Err = NewPluginError("plugin metadata does not define a vendor")
|
||||
p.Err = newPluginError("plugin metadata does not define a vendor")
|
||||
return p, nil
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// validateSchemaVersion validates if the plugin's schemaVersion is supported.
|
||||
//
|
||||
// The current schema-version is "0.1.0", but we don't want to break compatibility
|
||||
// until v2.0.0 of the schema version. Check for the major version to be < 2.0.0.
|
||||
//
|
||||
// Note that CLI versions before 28.4.1 may not support these versions as they were
|
||||
// hard-coded to only accept "0.1.0".
|
||||
func validateSchemaVersion(version string) error {
|
||||
if version == "0.1.0" {
|
||||
return nil
|
||||
}
|
||||
if version == "" {
|
||||
return errors.New("plugin SchemaVersion version cannot be empty")
|
||||
}
|
||||
major, _, ok := strings.Cut(version, ".")
|
||||
majorVersion, err := strconv.Atoi(major)
|
||||
if !ok || err != nil {
|
||||
return fmt.Errorf("plugin SchemaVersion %q has wrong format: must be <major>.<minor>.<patch>", version)
|
||||
}
|
||||
if majorVersion > 1 {
|
||||
return fmt.Errorf("plugin SchemaVersion %q is not supported: must be lower than 2.0.0", version)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RunHook executes the plugin's hooks command
|
||||
// and returns its unprocessed output.
|
||||
func (p *Plugin) RunHook(ctx context.Context, hookData HookPluginData) ([]byte, error) {
|
||||
@ -124,3 +170,26 @@ func (p *Plugin) RunHook(ctx context.Context, hookData HookPluginData) ([]byte,
|
||||
|
||||
return hookCmdOutput, nil
|
||||
}
|
||||
|
||||
// pluginNameFormat is used as part of errors for invalid plugin-names.
|
||||
// We should consider making this less technical ("must start with "a-z",
|
||||
// and only consist of lowercase alphanumeric characters").
|
||||
const pluginNameFormat = `^[a-z][a-z0-9]*$`
|
||||
|
||||
func isValidPluginName(s string) bool {
|
||||
if len(s) == 0 {
|
||||
return false
|
||||
}
|
||||
// first character must be a-z
|
||||
if c := s[0]; c < 'a' || c > 'z' {
|
||||
return false
|
||||
}
|
||||
// followed by a-z or 0-9
|
||||
for i := 1; i < len(s); i++ {
|
||||
c := s[i]
|
||||
if (c < 'a' || c > 'z') && (c < '0' || c > '9') {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
43
cli-plugins/manager/plugin_test.go
Normal file
43
cli-plugins/manager/plugin_test.go
Normal file
@ -0,0 +1,43 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
|
||||
func TestPluginMarshal(t *testing.T) {
|
||||
const jsonWithError = `{"Name":"some-plugin","Err":"something went wrong"}`
|
||||
const jsonNoError = `{"Name":"some-plugin"}`
|
||||
|
||||
tests := []struct {
|
||||
doc string
|
||||
error error
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
doc: "no error",
|
||||
expected: jsonNoError,
|
||||
},
|
||||
{
|
||||
doc: "regular error",
|
||||
error: errors.New("something went wrong"),
|
||||
expected: jsonWithError,
|
||||
},
|
||||
{
|
||||
doc: "custom error",
|
||||
error: newPluginError("something went wrong"),
|
||||
expected: jsonWithError,
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.doc, func(t *testing.T) {
|
||||
actual, err := json.Marshal(&Plugin{Name: "some-plugin", Err: tc.error})
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, is.Equal(string(actual), tc.expected))
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -14,7 +14,7 @@ import (
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/connhelper"
|
||||
"github.com/docker/cli/cli/debug"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/spf13/cobra"
|
||||
"go.opentelemetry.io/otel"
|
||||
)
|
||||
@ -80,19 +80,23 @@ func RunPlugin(dockerCli *command.DockerCli, plugin *cobra.Command, meta metadat
|
||||
return cmd.Execute()
|
||||
}
|
||||
|
||||
// Run is the top-level entry point to the CLI plugin framework. It should be called from your plugin's `main()` function.
|
||||
func Run(makeCmd func(command.Cli) *cobra.Command, meta metadata.Metadata) {
|
||||
// Run is the top-level entry point to the CLI plugin framework. It should
|
||||
// be called from the plugin's "main()" function. It initializes a new
|
||||
// [command.DockerCli] instance with the given options before calling
|
||||
// makeCmd to construct the plugin command, then invokes the plugin command
|
||||
// using [RunPlugin].
|
||||
func Run(makeCmd func(command.Cli) *cobra.Command, meta metadata.Metadata, ops ...command.CLIOption) {
|
||||
otel.SetErrorHandler(debug.OTELErrorHandler)
|
||||
|
||||
dockerCli, err := command.NewDockerCli()
|
||||
dockerCLI, err := command.NewDockerCli(ops...)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
_, _ = fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
plugin := makeCmd(dockerCli)
|
||||
plugin := makeCmd(dockerCLI)
|
||||
|
||||
if err := RunPlugin(dockerCli, plugin, meta); err != nil {
|
||||
if err := RunPlugin(dockerCLI, plugin, meta); err != nil {
|
||||
var stErr cli.StatusError
|
||||
if errors.As(err, &stErr) {
|
||||
// StatusError should only be used for errors, and all errors should
|
||||
@ -100,10 +104,10 @@ func Run(makeCmd func(command.Cli) *cobra.Command, meta metadata.Metadata) {
|
||||
if stErr.StatusCode == 0 { // FIXME(thaJeztah): this should never be used with a zero status-code. Check if we do this anywhere.
|
||||
stErr.StatusCode = 1
|
||||
}
|
||||
_, _ = fmt.Fprintln(dockerCli.Err(), stErr)
|
||||
_, _ = fmt.Fprintln(dockerCLI.Err(), stErr)
|
||||
os.Exit(stErr.StatusCode)
|
||||
}
|
||||
_, _ = fmt.Fprintln(dockerCli.Err(), err)
|
||||
_, _ = fmt.Fprintln(dockerCLI.Err(), err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@ -135,7 +139,7 @@ func withPluginClientConn(name string) command.CLIOption {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
apiClient, err := client.NewClientWithOpts(client.WithDialContext(helper.Dialer))
|
||||
apiClient, err := client.New(client.WithDialContext(helper.Dialer))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -164,6 +168,11 @@ func newPluginCommand(dockerCli *command.DockerCli, plugin *cobra.Command, meta
|
||||
DisableDescriptions: os.Getenv("DOCKER_CLI_DISABLE_COMPLETION_DESCRIPTION") != "",
|
||||
},
|
||||
}
|
||||
|
||||
// Disable file-completion by default. Most commands and flags should not
|
||||
// complete with filenames.
|
||||
cmd.CompletionOptions.SetDefaultShellCompDirective(cobra.ShellCompDirectiveNoFileComp)
|
||||
|
||||
opts, _ := cli.SetupPluginRootCommand(cmd)
|
||||
|
||||
cmd.SetIn(dockerCli.In())
|
||||
@ -175,11 +184,24 @@ func newPluginCommand(dockerCli *command.DockerCli, plugin *cobra.Command, meta
|
||||
newMetadataSubcommand(plugin, meta),
|
||||
)
|
||||
|
||||
cli.DisableFlagsInUseLine(cmd)
|
||||
visitAll(cmd,
|
||||
// prevent adding "[flags]" to the end of the usage line.
|
||||
func(c *cobra.Command) { c.DisableFlagsInUseLine = true },
|
||||
)
|
||||
|
||||
return cli.NewTopLevelCommand(cmd, dockerCli, opts, cmd.Flags())
|
||||
}
|
||||
|
||||
// visitAll traverses all commands from the root.
|
||||
func visitAll(root *cobra.Command, fns ...func(*cobra.Command)) {
|
||||
for _, cmd := range root.Commands() {
|
||||
visitAll(cmd, fns...)
|
||||
}
|
||||
for _, fn := range fns {
|
||||
fn(root)
|
||||
}
|
||||
}
|
||||
|
||||
func newMetadataSubcommand(plugin *cobra.Command, meta metadata.Metadata) *cobra.Command {
|
||||
if meta.ShortDescription == "" {
|
||||
meta.ShortDescription = plugin.Short
|
||||
|
||||
28
cli-plugins/plugin/plugin_test.go
Normal file
28
cli-plugins/plugin/plugin_test.go
Normal file
@ -0,0 +1,28 @@
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func TestVisitAll(t *testing.T) {
|
||||
root := &cobra.Command{Use: "root"}
|
||||
sub1 := &cobra.Command{Use: "sub1"}
|
||||
sub1sub1 := &cobra.Command{Use: "sub1sub1"}
|
||||
sub1sub2 := &cobra.Command{Use: "sub1sub2"}
|
||||
sub2 := &cobra.Command{Use: "sub2"}
|
||||
|
||||
root.AddCommand(sub1, sub2)
|
||||
sub1.AddCommand(sub1sub1, sub1sub2)
|
||||
|
||||
var visited []string
|
||||
visitAll(root, func(ccmd *cobra.Command) {
|
||||
visited = append(visited, ccmd.Name())
|
||||
})
|
||||
expected := []string{"sub1sub1", "sub1sub2", "sub1", "sub2", "root"}
|
||||
if !slices.Equal(expected, visited) {
|
||||
t.Errorf("expected %#v, got %#v", expected, visited)
|
||||
}
|
||||
}
|
||||
32
cli/cobra.go
32
cli/cobra.go
@ -12,7 +12,6 @@ import (
|
||||
"github.com/fvbommel/sortorder"
|
||||
"github.com/moby/term"
|
||||
"github.com/morikuni/aec"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
@ -167,35 +166,6 @@ func (tcmd *TopLevelCommand) Initialize(ops ...command.CLIOption) error {
|
||||
return tcmd.dockerCli.Initialize(tcmd.opts, ops...)
|
||||
}
|
||||
|
||||
// VisitAll will traverse all commands from the root.
|
||||
// This is different from the VisitAll of cobra.Command where only parents
|
||||
// are checked.
|
||||
func VisitAll(root *cobra.Command, fn func(*cobra.Command)) {
|
||||
for _, cmd := range root.Commands() {
|
||||
VisitAll(cmd, fn)
|
||||
}
|
||||
fn(root)
|
||||
}
|
||||
|
||||
// DisableFlagsInUseLine sets the DisableFlagsInUseLine flag on all
|
||||
// commands within the tree rooted at cmd.
|
||||
func DisableFlagsInUseLine(cmd *cobra.Command) {
|
||||
VisitAll(cmd, func(ccmd *cobra.Command) {
|
||||
// do not add a `[flags]` to the end of the usage line.
|
||||
ccmd.DisableFlagsInUseLine = true
|
||||
})
|
||||
}
|
||||
|
||||
// HasCompletionArg returns true if a cobra completion arg request is found.
|
||||
func HasCompletionArg(args []string) bool {
|
||||
for _, arg := range args {
|
||||
if arg == cobra.ShellCompRequestCmd || arg == cobra.ShellCompNoDescRequestCmd {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var helpCommand = &cobra.Command{
|
||||
Use: "help [command]",
|
||||
Short: "Help about the command",
|
||||
@ -204,7 +174,7 @@ var helpCommand = &cobra.Command{
|
||||
RunE: func(c *cobra.Command, args []string) error {
|
||||
cmd, args, e := c.Root().Find(args)
|
||||
if cmd == nil || e != nil || len(args) > 0 {
|
||||
return errors.Errorf("unknown help topic: %v", strings.Join(args, " "))
|
||||
return fmt.Errorf("unknown help topic: %v", strings.Join(args, " "))
|
||||
}
|
||||
helpFunc := cmd.HelpFunc()
|
||||
helpFunc(cmd, args)
|
||||
|
||||
@ -10,28 +10,6 @@ import (
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
|
||||
func TestVisitAll(t *testing.T) {
|
||||
root := &cobra.Command{Use: "root"}
|
||||
sub1 := &cobra.Command{Use: "sub1"}
|
||||
sub1sub1 := &cobra.Command{Use: "sub1sub1"}
|
||||
sub1sub2 := &cobra.Command{Use: "sub1sub2"}
|
||||
sub2 := &cobra.Command{Use: "sub2"}
|
||||
|
||||
root.AddCommand(sub1, sub2)
|
||||
sub1.AddCommand(sub1sub1, sub1sub2)
|
||||
|
||||
// Take the opportunity to test DisableFlagsInUseLine too
|
||||
DisableFlagsInUseLine(root)
|
||||
|
||||
var visited []string
|
||||
VisitAll(root, func(ccmd *cobra.Command) {
|
||||
visited = append(visited, ccmd.Name())
|
||||
assert.Assert(t, ccmd.DisableFlagsInUseLine, "DisableFlagsInUseLine not set on %q", ccmd.Name())
|
||||
})
|
||||
expected := []string{"sub1sub1", "sub1sub2", "sub1", "sub2", "root"}
|
||||
assert.DeepEqual(t, expected, visited)
|
||||
}
|
||||
|
||||
func TestVendorAndVersion(t *testing.T) {
|
||||
// Non plugin.
|
||||
assert.Equal(t, vendorAndVersion(&cobra.Command{Use: "test"}), "")
|
||||
|
||||
@ -3,18 +3,17 @@ package builder
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/docker/docker/api/types/build"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/moby/moby/client"
|
||||
)
|
||||
|
||||
type fakeClient struct {
|
||||
client.Client
|
||||
builderPruneFunc func(ctx context.Context, opts build.CachePruneOptions) (*build.CachePruneReport, error)
|
||||
builderPruneFunc func(ctx context.Context, opts client.BuildCachePruneOptions) (client.BuildCachePruneResult, error)
|
||||
}
|
||||
|
||||
func (c *fakeClient) BuildCachePrune(ctx context.Context, opts build.CachePruneOptions) (*build.CachePruneReport, error) {
|
||||
func (c *fakeClient) BuildCachePrune(ctx context.Context, opts client.BuildCachePruneOptions) (client.BuildCachePruneResult, error) {
|
||||
if c.builderPruneFunc != nil {
|
||||
return c.builderPruneFunc(ctx, opts)
|
||||
}
|
||||
return nil, nil
|
||||
return client.BuildCachePruneResult{}, nil
|
||||
}
|
||||
|
||||
@ -6,29 +6,41 @@ import (
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/image"
|
||||
"github.com/docker/cli/internal/commands"
|
||||
)
|
||||
|
||||
// NewBuilderCommand returns a cobra command for `builder` subcommands
|
||||
func NewBuilderCommand(dockerCli command.Cli) *cobra.Command {
|
||||
func init() {
|
||||
commands.Register(newBuilderCommand)
|
||||
commands.Register(func(c command.Cli) *cobra.Command {
|
||||
return newBakeStubCommand(c)
|
||||
})
|
||||
}
|
||||
|
||||
// newBuilderCommand returns a cobra command for `builder` subcommands
|
||||
func newBuilderCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "builder",
|
||||
Short: "Manage builds",
|
||||
Args: cli.NoArgs,
|
||||
RunE: command.ShowHelp(dockerCli.Err()),
|
||||
RunE: command.ShowHelp(dockerCLI.Err()),
|
||||
Annotations: map[string]string{"version": "1.31"},
|
||||
|
||||
DisableFlagsInUseLine: true,
|
||||
}
|
||||
cmd.AddCommand(
|
||||
NewPruneCommand(dockerCli),
|
||||
image.NewBuildCommand(dockerCli),
|
||||
newPruneCommand(dockerCLI),
|
||||
// we should have a mechanism for registering sub-commands in the cli/internal/commands.Register function.
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
image.NewBuildCommand(dockerCLI),
|
||||
)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// NewBakeStubCommand returns a cobra command "stub" for the "bake" subcommand.
|
||||
// newBakeStubCommand returns a cobra command "stub" for the "bake" subcommand.
|
||||
// This command is a placeholder / stub that is dynamically replaced by an
|
||||
// alias for "docker buildx bake" if BuildKit is enabled (and the buildx plugin
|
||||
// installed).
|
||||
func NewBakeStubCommand(dockerCLI command.Streams) *cobra.Command {
|
||||
func newBakeStubCommand(dockerCLI command.Streams) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "bake [OPTIONS] [TARGET...]",
|
||||
Short: "Build from a file",
|
||||
@ -40,5 +52,6 @@ func NewBakeStubCommand(dockerCLI command.Streams) *cobra.Command {
|
||||
"aliases": "docker buildx bake",
|
||||
"version": "1.31",
|
||||
},
|
||||
DisableFlagsInUseLine: true,
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,23 +8,30 @@ import (
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/docker/cli/cli/command/system/pruner"
|
||||
"github.com/docker/cli/internal/prompt"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api/types/build"
|
||||
"github.com/docker/go-units"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type pruneOptions struct {
|
||||
force bool
|
||||
all bool
|
||||
filter opts.FilterOpt
|
||||
keepStorage opts.MemBytes
|
||||
func init() {
|
||||
// Register the prune command to run as part of "docker system prune"
|
||||
if err := pruner.Register(pruner.TypeBuildCache, pruneFn); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// NewPruneCommand returns a new cobra prune command for images
|
||||
func NewPruneCommand(dockerCli command.Cli) *cobra.Command {
|
||||
type pruneOptions struct {
|
||||
force bool
|
||||
all bool
|
||||
filter opts.FilterOpt
|
||||
reservedSpace opts.MemBytes
|
||||
}
|
||||
|
||||
// newPruneCommand returns a new cobra prune command for images
|
||||
func newPruneCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
options := pruneOptions{filter: opts.NewFilterOpt()}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
@ -32,25 +39,26 @@ func NewPruneCommand(dockerCli command.Cli) *cobra.Command {
|
||||
Short: "Remove build cache",
|
||||
Args: cli.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
spaceReclaimed, output, err := runPrune(cmd.Context(), dockerCli, options)
|
||||
spaceReclaimed, output, err := runPrune(cmd.Context(), dockerCLI, options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if output != "" {
|
||||
fmt.Fprintln(dockerCli.Out(), output)
|
||||
_, _ = fmt.Fprintln(dockerCLI.Out(), output)
|
||||
}
|
||||
fmt.Fprintln(dockerCli.Out(), "Total reclaimed space:", units.HumanSize(float64(spaceReclaimed)))
|
||||
_, _ = fmt.Fprintln(dockerCLI.Out(), "Total reclaimed space:", units.HumanSize(float64(spaceReclaimed)))
|
||||
return nil
|
||||
},
|
||||
Annotations: map[string]string{"version": "1.39"},
|
||||
ValidArgsFunction: completion.NoComplete,
|
||||
Annotations: map[string]string{"version": "1.39"},
|
||||
ValidArgsFunction: cobra.NoFileCompletions,
|
||||
DisableFlagsInUseLine: true,
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVarP(&options.force, "force", "f", false, "Do not prompt for confirmation")
|
||||
flags.BoolVarP(&options.all, "all", "a", false, "Remove all unused build cache, not just dangling ones")
|
||||
flags.Var(&options.filter, "filter", `Provide filter values (e.g. "until=24h")`)
|
||||
flags.Var(&options.keepStorage, "keep-storage", "Amount of disk space to keep for cache")
|
||||
flags.Var(&options.reservedSpace, "keep-storage", "Amount of disk space to keep for cache")
|
||||
|
||||
return cmd
|
||||
}
|
||||
@ -61,8 +69,7 @@ const (
|
||||
)
|
||||
|
||||
func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions) (spaceReclaimed uint64, output string, err error) {
|
||||
pruneFilters := options.filter.Value()
|
||||
pruneFilters = command.PruneFilters(dockerCli, pruneFilters)
|
||||
pruneFilters := command.PruneFilters(dockerCli, options.filter.Value())
|
||||
|
||||
warning := normalWarning
|
||||
if options.all {
|
||||
@ -78,15 +85,15 @@ func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions)
|
||||
}
|
||||
}
|
||||
|
||||
report, err := dockerCli.Client().BuildCachePrune(ctx, build.CachePruneOptions{
|
||||
All: options.all,
|
||||
KeepStorage: options.keepStorage.Value(), // FIXME(thaJeztah): rewrite to use new options; see https://github.com/moby/moby/pull/48720
|
||||
Filters: pruneFilters,
|
||||
resp, err := dockerCli.Client().BuildCachePrune(ctx, client.BuildCachePruneOptions{
|
||||
All: options.all,
|
||||
ReservedSpace: options.reservedSpace.Value(),
|
||||
Filters: pruneFilters,
|
||||
})
|
||||
if err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
|
||||
report := resp.Report
|
||||
if len(report.CachesDeleted) > 0 {
|
||||
var sb strings.Builder
|
||||
sb.WriteString("Deleted build cache objects:\n")
|
||||
@ -104,7 +111,22 @@ type cancelledErr struct{ error }
|
||||
|
||||
func (cancelledErr) Cancelled() {}
|
||||
|
||||
// CachePrune executes a prune command for build cache
|
||||
func CachePrune(ctx context.Context, dockerCli command.Cli, all bool, filter opts.FilterOpt) (uint64, string, error) {
|
||||
return runPrune(ctx, dockerCli, pruneOptions{force: true, all: all, filter: filter})
|
||||
// pruneFn prunes the build cache for use in "docker system prune" and
|
||||
// returns the amount of space reclaimed and a detailed output string.
|
||||
func pruneFn(ctx context.Context, dockerCLI command.Cli, options pruner.PruneOptions) (uint64, string, error) {
|
||||
if !options.Confirmed {
|
||||
// Dry-run: perform validation and produce confirmation before pruning.
|
||||
var confirmMsg string
|
||||
if options.All {
|
||||
confirmMsg = "all build cache"
|
||||
} else {
|
||||
confirmMsg = "unused build cache"
|
||||
}
|
||||
return 0, confirmMsg, cancelledErr{errors.New("builder prune has been cancelled")}
|
||||
}
|
||||
return runPrune(ctx, dockerCLI, pruneOptions{
|
||||
force: true,
|
||||
all: options.All,
|
||||
filter: options.Filter,
|
||||
})
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/docker/api/types/build"
|
||||
"github.com/moby/moby/client"
|
||||
)
|
||||
|
||||
func TestBuilderPromptTermination(t *testing.T) {
|
||||
@ -15,11 +15,11 @@ func TestBuilderPromptTermination(t *testing.T) {
|
||||
t.Cleanup(cancel)
|
||||
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
builderPruneFunc: func(ctx context.Context, opts build.CachePruneOptions) (*build.CachePruneReport, error) {
|
||||
return nil, errors.New("fakeClient builderPruneFunc should not be called")
|
||||
builderPruneFunc: func(ctx context.Context, opts client.BuildCachePruneOptions) (client.BuildCachePruneResult, error) {
|
||||
return client.BuildCachePruneResult{}, errors.New("fakeClient builderPruneFunc should not be called")
|
||||
},
|
||||
})
|
||||
cmd := NewPruneCommand(cli)
|
||||
cmd := newPruneCommand(cli)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
test.TerminatePrompt(ctx, t, cmd, cli)
|
||||
|
||||
@ -3,34 +3,33 @@ package checkpoint
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/docker/docker/api/types/checkpoint"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/moby/moby/client"
|
||||
)
|
||||
|
||||
type fakeClient struct {
|
||||
client.Client
|
||||
checkpointCreateFunc func(container string, options checkpoint.CreateOptions) error
|
||||
checkpointDeleteFunc func(container string, options checkpoint.DeleteOptions) error
|
||||
checkpointListFunc func(container string, options checkpoint.ListOptions) ([]checkpoint.Summary, error)
|
||||
checkpointCreateFunc func(container string, options client.CheckpointCreateOptions) error
|
||||
checkpointDeleteFunc func(container string, options client.CheckpointDeleteOptions) error
|
||||
checkpointListFunc func(container string, options client.CheckpointListOptions) (client.CheckpointListResult, error)
|
||||
}
|
||||
|
||||
func (cli *fakeClient) CheckpointCreate(_ context.Context, container string, options checkpoint.CreateOptions) error {
|
||||
func (cli *fakeClient) CheckpointCreate(_ context.Context, container string, options client.CheckpointCreateOptions) error {
|
||||
if cli.checkpointCreateFunc != nil {
|
||||
return cli.checkpointCreateFunc(container, options)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cli *fakeClient) CheckpointDelete(_ context.Context, container string, options checkpoint.DeleteOptions) error {
|
||||
func (cli *fakeClient) CheckpointDelete(_ context.Context, container string, options client.CheckpointDeleteOptions) error {
|
||||
if cli.checkpointDeleteFunc != nil {
|
||||
return cli.checkpointDeleteFunc(container, options)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cli *fakeClient) CheckpointList(_ context.Context, container string, options checkpoint.ListOptions) ([]checkpoint.Summary, error) {
|
||||
func (cli *fakeClient) CheckpointList(_ context.Context, container string, options client.CheckpointListOptions) (client.CheckpointListResult, error) {
|
||||
if cli.checkpointListFunc != nil {
|
||||
return cli.checkpointListFunc(container, options)
|
||||
}
|
||||
return []checkpoint.Summary{}, nil
|
||||
return client.CheckpointListResult{}, nil
|
||||
}
|
||||
|
||||
@ -3,26 +3,32 @@ package checkpoint
|
||||
import (
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/internal/commands"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// NewCheckpointCommand returns the `checkpoint` subcommand (only in experimental)
|
||||
func NewCheckpointCommand(dockerCli command.Cli) *cobra.Command {
|
||||
func init() {
|
||||
commands.Register(newCheckpointCommand)
|
||||
}
|
||||
|
||||
// newCheckpointCommand returns the `checkpoint` subcommand (only in experimental)
|
||||
func newCheckpointCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "checkpoint",
|
||||
Short: "Manage checkpoints",
|
||||
Args: cli.NoArgs,
|
||||
RunE: command.ShowHelp(dockerCli.Err()),
|
||||
RunE: command.ShowHelp(dockerCLI.Err()),
|
||||
Annotations: map[string]string{
|
||||
"experimental": "",
|
||||
"ostype": "linux",
|
||||
"version": "1.25",
|
||||
},
|
||||
DisableFlagsInUseLine: true,
|
||||
}
|
||||
cmd.AddCommand(
|
||||
newCreateCommand(dockerCli),
|
||||
newListCommand(dockerCli),
|
||||
newRemoveCommand(dockerCli),
|
||||
newCreateCommand(dockerCLI),
|
||||
newListCommand(dockerCLI),
|
||||
newRemoveCommand(dockerCLI),
|
||||
)
|
||||
return cmd
|
||||
}
|
||||
|
||||
@ -6,8 +6,7 @@ import (
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/docker/docker/api/types/checkpoint"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@ -18,7 +17,7 @@ type createOptions struct {
|
||||
leaveRunning bool
|
||||
}
|
||||
|
||||
func newCreateCommand(dockerCli command.Cli) *cobra.Command {
|
||||
func newCreateCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
var opts createOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
@ -28,9 +27,10 @@ func newCreateCommand(dockerCli command.Cli) *cobra.Command {
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.container = args[0]
|
||||
opts.checkpoint = args[1]
|
||||
return runCreate(cmd.Context(), dockerCli, opts)
|
||||
return runCreate(cmd.Context(), dockerCLI, opts)
|
||||
},
|
||||
ValidArgsFunction: completion.NoComplete,
|
||||
ValidArgsFunction: cobra.NoFileCompletions,
|
||||
DisableFlagsInUseLine: true,
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
@ -41,7 +41,7 @@ func newCreateCommand(dockerCli command.Cli) *cobra.Command {
|
||||
}
|
||||
|
||||
func runCreate(ctx context.Context, dockerCLI command.Cli, opts createOptions) error {
|
||||
err := dockerCLI.Client().CheckpointCreate(ctx, opts.container, checkpoint.CreateOptions{
|
||||
err := dockerCLI.Client().CheckpointCreate(ctx, opts.container, client.CheckpointCreateOptions{
|
||||
CheckpointID: opts.checkpoint,
|
||||
CheckpointDir: opts.checkpointDir,
|
||||
Exit: !opts.leaveRunning,
|
||||
|
||||
@ -8,7 +8,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/docker/api/types/checkpoint"
|
||||
"github.com/moby/moby/client"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
@ -16,7 +16,7 @@ import (
|
||||
func TestCheckpointCreateErrors(t *testing.T) {
|
||||
testCases := []struct {
|
||||
args []string
|
||||
checkpointCreateFunc func(container string, options checkpoint.CreateOptions) error
|
||||
checkpointCreateFunc func(container string, options client.CheckpointCreateOptions) error
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
@ -29,7 +29,7 @@ func TestCheckpointCreateErrors(t *testing.T) {
|
||||
},
|
||||
{
|
||||
args: []string{"foo", "bar"},
|
||||
checkpointCreateFunc: func(container string, options checkpoint.CreateOptions) error {
|
||||
checkpointCreateFunc: func(container string, options client.CheckpointCreateOptions) error {
|
||||
return errors.New("error creating checkpoint for container foo")
|
||||
},
|
||||
expectedError: "error creating checkpoint for container foo",
|
||||
@ -59,9 +59,9 @@ func TestCheckpointCreateWithOptions(t *testing.T) {
|
||||
leaveRunning := strconv.FormatBool(tc)
|
||||
t.Run("leave-running="+leaveRunning, func(t *testing.T) {
|
||||
var actualContainerName string
|
||||
var actualOptions checkpoint.CreateOptions
|
||||
var actualOptions client.CheckpointCreateOptions
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
checkpointCreateFunc: func(container string, options checkpoint.CreateOptions) error {
|
||||
checkpointCreateFunc: func(container string, options client.CheckpointCreateOptions) error {
|
||||
actualContainerName = container
|
||||
actualOptions = options
|
||||
return nil
|
||||
@ -75,7 +75,7 @@ func TestCheckpointCreateWithOptions(t *testing.T) {
|
||||
assert.Check(t, cmd.Flags().Set("checkpoint-dir", checkpointDir))
|
||||
assert.NilError(t, cmd.Execute())
|
||||
assert.Check(t, is.Equal(actualContainerName, containerName))
|
||||
expected := checkpoint.CreateOptions{
|
||||
expected := client.CheckpointCreateOptions{
|
||||
CheckpointID: checkpointName,
|
||||
CheckpointDir: checkpointDir,
|
||||
Exit: !tc,
|
||||
|
||||
@ -2,7 +2,7 @@ package checkpoint
|
||||
|
||||
import (
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/docker/docker/api/types/checkpoint"
|
||||
"github.com/moby/moby/api/types/checkpoint"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -10,25 +10,31 @@ const (
|
||||
checkpointNameHeader = "CHECKPOINT NAME"
|
||||
)
|
||||
|
||||
// NewFormat returns a format for use with a checkpoint Context
|
||||
func NewFormat(source string) formatter.Format {
|
||||
// newFormat returns a format for use with a checkpointContext.
|
||||
func newFormat(source string) formatter.Format {
|
||||
if source == formatter.TableFormatKey {
|
||||
return defaultCheckpointFormat
|
||||
}
|
||||
return formatter.Format(source)
|
||||
}
|
||||
|
||||
// FormatWrite writes formatted checkpoints using the Context
|
||||
func FormatWrite(ctx formatter.Context, checkpoints []checkpoint.Summary) error {
|
||||
render := func(format func(subContext formatter.SubContext) error) error {
|
||||
// formatWrite writes formatted checkpoints using the Context
|
||||
func formatWrite(fmtCtx formatter.Context, checkpoints []checkpoint.Summary) error {
|
||||
cpContext := &checkpointContext{
|
||||
HeaderContext: formatter.HeaderContext{
|
||||
Header: formatter.SubHeaderContext{
|
||||
"Name": checkpointNameHeader,
|
||||
},
|
||||
},
|
||||
}
|
||||
return fmtCtx.Write(cpContext, func(format func(subContext formatter.SubContext) error) error {
|
||||
for _, cp := range checkpoints {
|
||||
if err := format(&checkpointContext{c: cp}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return ctx.Write(newCheckpointContext(), render)
|
||||
})
|
||||
}
|
||||
|
||||
type checkpointContext struct {
|
||||
@ -36,14 +42,6 @@ type checkpointContext struct {
|
||||
c checkpoint.Summary
|
||||
}
|
||||
|
||||
func newCheckpointContext() *checkpointContext {
|
||||
cpCtx := checkpointContext{}
|
||||
cpCtx.Header = formatter.SubHeaderContext{
|
||||
"Name": checkpointNameHeader,
|
||||
}
|
||||
return &cpCtx
|
||||
}
|
||||
|
||||
func (c *checkpointContext) MarshalJSON() ([]byte, error) {
|
||||
return formatter.MarshalJSON(c)
|
||||
}
|
||||
|
||||
@ -5,7 +5,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/docker/docker/api/types/checkpoint"
|
||||
"github.com/moby/moby/api/types/checkpoint"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
@ -15,7 +15,7 @@ func TestCheckpointContextFormatWrite(t *testing.T) {
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
formatter.Context{Format: NewFormat(defaultCheckpointFormat)},
|
||||
formatter.Context{Format: newFormat(defaultCheckpointFormat)},
|
||||
`CHECKPOINT NAME
|
||||
checkpoint-1
|
||||
checkpoint-2
|
||||
@ -23,14 +23,14 @@ checkpoint-3
|
||||
`,
|
||||
},
|
||||
{
|
||||
formatter.Context{Format: NewFormat("{{.Name}}")},
|
||||
formatter.Context{Format: newFormat("{{.Name}}")},
|
||||
`checkpoint-1
|
||||
checkpoint-2
|
||||
checkpoint-3
|
||||
`,
|
||||
},
|
||||
{
|
||||
formatter.Context{Format: NewFormat("{{.Name}}:")},
|
||||
formatter.Context{Format: newFormat("{{.Name}}:")},
|
||||
`checkpoint-1:
|
||||
checkpoint-2:
|
||||
checkpoint-3:
|
||||
@ -41,7 +41,7 @@ checkpoint-3:
|
||||
for _, testcase := range cases {
|
||||
out := bytes.NewBufferString("")
|
||||
testcase.context.Output = out
|
||||
err := FormatWrite(testcase.context, []checkpoint.Summary{
|
||||
err := formatWrite(testcase.context, []checkpoint.Summary{
|
||||
{Name: "checkpoint-1"},
|
||||
{Name: "checkpoint-2"},
|
||||
{Name: "checkpoint-3"},
|
||||
|
||||
@ -7,7 +7,7 @@ import (
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/docker/docker/api/types/checkpoint"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@ -15,7 +15,7 @@ type listOptions struct {
|
||||
checkpointDir string
|
||||
}
|
||||
|
||||
func newListCommand(dockerCli command.Cli) *cobra.Command {
|
||||
func newListCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
var opts listOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
@ -24,9 +24,10 @@ func newListCommand(dockerCli command.Cli) *cobra.Command {
|
||||
Short: "List checkpoints for a container",
|
||||
Args: cli.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runList(cmd.Context(), dockerCli, args[0], opts)
|
||||
return runList(cmd.Context(), dockerCLI, args[0], opts)
|
||||
},
|
||||
ValidArgsFunction: completion.ContainerNames(dockerCli, false),
|
||||
ValidArgsFunction: completion.ContainerNames(dockerCLI, false),
|
||||
DisableFlagsInUseLine: true,
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
@ -35,8 +36,8 @@ func newListCommand(dockerCli command.Cli) *cobra.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runList(ctx context.Context, dockerCli command.Cli, container string, opts listOptions) error {
|
||||
checkpoints, err := dockerCli.Client().CheckpointList(ctx, container, checkpoint.ListOptions{
|
||||
func runList(ctx context.Context, dockerCLI command.Cli, container string, opts listOptions) error {
|
||||
checkpoints, err := dockerCLI.Client().CheckpointList(ctx, container, client.CheckpointListOptions{
|
||||
CheckpointDir: opts.checkpointDir,
|
||||
})
|
||||
if err != nil {
|
||||
@ -44,8 +45,8 @@ func runList(ctx context.Context, dockerCli command.Cli, container string, opts
|
||||
}
|
||||
|
||||
cpCtx := formatter.Context{
|
||||
Output: dockerCli.Out(),
|
||||
Format: NewFormat(formatter.TableFormatKey),
|
||||
Output: dockerCLI.Out(),
|
||||
Format: newFormat(formatter.TableFormatKey),
|
||||
}
|
||||
return FormatWrite(cpCtx, checkpoints)
|
||||
return formatWrite(cpCtx, checkpoints.Checkpoints)
|
||||
}
|
||||
|
||||
@ -6,7 +6,8 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/docker/api/types/checkpoint"
|
||||
"github.com/moby/moby/api/types/checkpoint"
|
||||
"github.com/moby/moby/client"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
"gotest.tools/v3/golden"
|
||||
@ -15,7 +16,7 @@ import (
|
||||
func TestCheckpointListErrors(t *testing.T) {
|
||||
testCases := []struct {
|
||||
args []string
|
||||
checkpointListFunc func(container string, options checkpoint.ListOptions) ([]checkpoint.Summary, error)
|
||||
checkpointListFunc func(container string, options client.CheckpointListOptions) (client.CheckpointListResult, error)
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
@ -28,8 +29,8 @@ func TestCheckpointListErrors(t *testing.T) {
|
||||
},
|
||||
{
|
||||
args: []string{"foo"},
|
||||
checkpointListFunc: func(container string, options checkpoint.ListOptions) ([]checkpoint.Summary, error) {
|
||||
return []checkpoint.Summary{}, errors.New("error getting checkpoints for container foo")
|
||||
checkpointListFunc: func(container string, options client.CheckpointListOptions) (client.CheckpointListResult, error) {
|
||||
return client.CheckpointListResult{}, errors.New("error getting checkpoints for container foo")
|
||||
},
|
||||
expectedError: "error getting checkpoints for container foo",
|
||||
},
|
||||
@ -50,11 +51,13 @@ func TestCheckpointListErrors(t *testing.T) {
|
||||
func TestCheckpointListWithOptions(t *testing.T) {
|
||||
var containerID, checkpointDir string
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
checkpointListFunc: func(container string, options checkpoint.ListOptions) ([]checkpoint.Summary, error) {
|
||||
checkpointListFunc: func(container string, options client.CheckpointListOptions) (client.CheckpointListResult, error) {
|
||||
containerID = container
|
||||
checkpointDir = options.CheckpointDir
|
||||
return []checkpoint.Summary{
|
||||
{Name: "checkpoint-foo"},
|
||||
return client.CheckpointListResult{
|
||||
Checkpoints: []checkpoint.Summary{
|
||||
{Name: "checkpoint-foo"},
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
|
||||
@ -5,7 +5,7 @@ import (
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/docker/api/types/checkpoint"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@ -13,7 +13,7 @@ type removeOptions struct {
|
||||
checkpointDir string
|
||||
}
|
||||
|
||||
func newRemoveCommand(dockerCli command.Cli) *cobra.Command {
|
||||
func newRemoveCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
var opts removeOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
@ -22,8 +22,9 @@ func newRemoveCommand(dockerCli command.Cli) *cobra.Command {
|
||||
Short: "Remove a checkpoint",
|
||||
Args: cli.ExactArgs(2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runRemove(cmd.Context(), dockerCli, args[0], args[1], opts)
|
||||
return runRemove(cmd.Context(), dockerCLI, args[0], args[1], opts)
|
||||
},
|
||||
DisableFlagsInUseLine: true,
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
@ -33,7 +34,7 @@ func newRemoveCommand(dockerCli command.Cli) *cobra.Command {
|
||||
}
|
||||
|
||||
func runRemove(ctx context.Context, dockerCli command.Cli, container string, checkpointID string, opts removeOptions) error {
|
||||
return dockerCli.Client().CheckpointDelete(ctx, container, checkpoint.DeleteOptions{
|
||||
return dockerCli.Client().CheckpointDelete(ctx, container, client.CheckpointDeleteOptions{
|
||||
CheckpointID: checkpointID,
|
||||
CheckpointDir: opts.checkpointDir,
|
||||
})
|
||||
|
||||
@ -6,7 +6,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/docker/api/types/checkpoint"
|
||||
"github.com/moby/moby/client"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
@ -14,7 +14,7 @@ import (
|
||||
func TestCheckpointRemoveErrors(t *testing.T) {
|
||||
testCases := []struct {
|
||||
args []string
|
||||
checkpointDeleteFunc func(container string, options checkpoint.DeleteOptions) error
|
||||
checkpointDeleteFunc func(container string, options client.CheckpointDeleteOptions) error
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
@ -27,7 +27,7 @@ func TestCheckpointRemoveErrors(t *testing.T) {
|
||||
},
|
||||
{
|
||||
args: []string{"foo", "bar"},
|
||||
checkpointDeleteFunc: func(container string, options checkpoint.DeleteOptions) error {
|
||||
checkpointDeleteFunc: func(container string, options client.CheckpointDeleteOptions) error {
|
||||
return errors.New("error deleting checkpoint")
|
||||
},
|
||||
expectedError: "error deleting checkpoint",
|
||||
@ -49,7 +49,7 @@ func TestCheckpointRemoveErrors(t *testing.T) {
|
||||
func TestCheckpointRemoveWithOptions(t *testing.T) {
|
||||
var containerID, checkpointID, checkpointDir string
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
checkpointDeleteFunc: func(container string, options checkpoint.DeleteOptions) error {
|
||||
checkpointDeleteFunc: func(container string, options client.CheckpointDeleteOptions) error {
|
||||
containerID = container
|
||||
checkpointID = options.CheckpointID
|
||||
checkpointDir = options.CheckpointDir
|
||||
|
||||
@ -5,6 +5,7 @@ package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
@ -23,11 +24,8 @@ import (
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/docker/cli/cli/version"
|
||||
dopts "github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api"
|
||||
"github.com/docker/docker/api/types/build"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/moby/moby/api/types/build"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@ -45,12 +43,9 @@ type Cli interface {
|
||||
Client() client.APIClient
|
||||
Streams
|
||||
SetIn(in *streams.In)
|
||||
Apply(ops ...CLIOption) error
|
||||
config.Provider
|
||||
ServerInfo() ServerInfo
|
||||
DefaultVersion() string
|
||||
CurrentVersion() string
|
||||
ContentTrustEnabled() bool
|
||||
BuildKitEnabled() (bool, error)
|
||||
ContextStore() store.Store
|
||||
CurrentContext() string
|
||||
@ -70,7 +65,6 @@ type DockerCli struct {
|
||||
err *streams.Out
|
||||
client client.APIClient
|
||||
serverInfo ServerInfo
|
||||
contentTrust bool
|
||||
contextStore store.Store
|
||||
currentContext string
|
||||
init sync.Once
|
||||
@ -78,6 +72,7 @@ type DockerCli struct {
|
||||
dockerEndpoint docker.Endpoint
|
||||
contextStoreConfig *store.Config
|
||||
initTimeout time.Duration
|
||||
userAgent string
|
||||
res telemetryResource
|
||||
|
||||
// baseCtx is the base context used for internal operations. In the future
|
||||
@ -88,17 +83,12 @@ type DockerCli struct {
|
||||
enableGlobalMeter, enableGlobalTracer bool
|
||||
}
|
||||
|
||||
// DefaultVersion returns [api.DefaultVersion].
|
||||
func (*DockerCli) DefaultVersion() string {
|
||||
return api.DefaultVersion
|
||||
}
|
||||
|
||||
// CurrentVersion returns the API version currently negotiated, or the default
|
||||
// version otherwise.
|
||||
func (cli *DockerCli) CurrentVersion() string {
|
||||
_ = cli.initialize()
|
||||
if cli.client == nil {
|
||||
return api.DefaultVersion
|
||||
return client.MaxAPIVersion
|
||||
}
|
||||
return cli.client.ClientVersion()
|
||||
}
|
||||
@ -157,19 +147,13 @@ func (cli *DockerCli) ServerInfo() ServerInfo {
|
||||
return cli.serverInfo
|
||||
}
|
||||
|
||||
// ContentTrustEnabled returns whether content trust has been enabled by an
|
||||
// environment variable.
|
||||
func (cli *DockerCli) ContentTrustEnabled() bool {
|
||||
return cli.contentTrust
|
||||
}
|
||||
|
||||
// BuildKitEnabled returns buildkit is enabled or not.
|
||||
func (cli *DockerCli) BuildKitEnabled() (bool, error) {
|
||||
// use DOCKER_BUILDKIT env var value if set and not empty
|
||||
if v := os.Getenv("DOCKER_BUILDKIT"); v != "" {
|
||||
enabled, err := strconv.ParseBool(v)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "DOCKER_BUILDKIT environment variable expects boolean value")
|
||||
return false, fmt.Errorf("DOCKER_BUILDKIT environment variable expects boolean value: %w", err)
|
||||
}
|
||||
return enabled, nil
|
||||
}
|
||||
@ -269,7 +253,7 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...CLIOption)
|
||||
cli.contextStore = &ContextStoreWithDefault{
|
||||
Store: store.New(config.ContextStoreDir(), *cli.contextStoreConfig),
|
||||
Resolver: func() (*DefaultContext, error) {
|
||||
return ResolveDefaultContext(cli.options, *cli.contextStoreConfig)
|
||||
return resolveDefaultContext(cli.options, *cli.contextStoreConfig)
|
||||
},
|
||||
}
|
||||
|
||||
@ -282,6 +266,17 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...CLIOption)
|
||||
}
|
||||
filterResourceAttributesEnvvar()
|
||||
|
||||
// early return if GODEBUG is already set or the docker context is
|
||||
// the default context, i.e. is a virtual context where we won't override
|
||||
// any GODEBUG values.
|
||||
if v := os.Getenv("GODEBUG"); cli.currentContext == DefaultContextName || v != "" {
|
||||
return nil
|
||||
}
|
||||
meta, err := cli.contextStore.GetMetadata(cli.currentContext)
|
||||
if err == nil {
|
||||
setGoDebug(meta)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -295,17 +290,17 @@ func NewAPIClientFromFlags(opts *cliflags.ClientOptions, configFile *configfile.
|
||||
contextStore := &ContextStoreWithDefault{
|
||||
Store: store.New(config.ContextStoreDir(), storeConfig),
|
||||
Resolver: func() (*DefaultContext, error) {
|
||||
return ResolveDefaultContext(opts, storeConfig)
|
||||
return resolveDefaultContext(opts, storeConfig)
|
||||
},
|
||||
}
|
||||
endpoint, err := resolveDockerEndpoint(contextStore, resolveContextName(opts, configFile))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "unable to resolve docker endpoint")
|
||||
return nil, fmt.Errorf("unable to resolve docker endpoint: %w", err)
|
||||
}
|
||||
return newAPIClientFromEndpoint(endpoint, configFile)
|
||||
return newAPIClientFromEndpoint(endpoint, configFile, client.WithUserAgent(UserAgent()))
|
||||
}
|
||||
|
||||
func newAPIClientFromEndpoint(ep docker.Endpoint, configFile *configfile.ConfigFile) (client.APIClient, error) {
|
||||
func newAPIClientFromEndpoint(ep docker.Endpoint, configFile *configfile.ConfigFile, extraOpts ...client.Opt) (client.APIClient, error) {
|
||||
opts, err := ep.ClientOpts()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -313,8 +308,15 @@ func newAPIClientFromEndpoint(ep docker.Endpoint, configFile *configfile.ConfigF
|
||||
if len(configFile.HTTPHeaders) > 0 {
|
||||
opts = append(opts, client.WithHTTPHeaders(configFile.HTTPHeaders))
|
||||
}
|
||||
opts = append(opts, withCustomHeadersFromEnv(), client.WithUserAgent(UserAgent()))
|
||||
return client.NewClientWithOpts(opts...)
|
||||
withCustomHeaders, err := withCustomHeadersFromEnv()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if withCustomHeaders != nil {
|
||||
opts = append(opts, withCustomHeaders)
|
||||
}
|
||||
opts = append(opts, extraOpts...)
|
||||
return client.New(opts...)
|
||||
}
|
||||
|
||||
func resolveDockerEndpoint(s store.Reader, contextName string) (docker.Endpoint, error) {
|
||||
@ -375,24 +377,21 @@ func (cli *DockerCli) initializeFromClient() {
|
||||
ctx, cancel := context.WithTimeout(cli.baseCtx, cli.getInitTimeout())
|
||||
defer cancel()
|
||||
|
||||
ping, err := cli.client.Ping(ctx)
|
||||
ping, err := cli.client.Ping(ctx, client.PingOptions{
|
||||
NegotiateAPIVersion: true,
|
||||
ForceNegotiate: true,
|
||||
})
|
||||
if err != nil {
|
||||
// Default to true if we fail to connect to daemon
|
||||
cli.serverInfo = ServerInfo{HasExperimental: true}
|
||||
|
||||
if ping.APIVersion != "" {
|
||||
cli.client.NegotiateAPIVersionPing(ping)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
cli.serverInfo = ServerInfo{
|
||||
HasExperimental: ping.Experimental,
|
||||
OSType: ping.OSType,
|
||||
BuildkitVersion: ping.BuilderVersion,
|
||||
SwarmStatus: ping.SwarmStatus,
|
||||
}
|
||||
cli.client.NegotiateAPIVersionPing(ping)
|
||||
}
|
||||
|
||||
// ContextStore returns the ContextStore
|
||||
@ -475,15 +474,67 @@ func (cli *DockerCli) getDockerEndPoint() (ep docker.Endpoint, err error) {
|
||||
return resolveDockerEndpoint(cli.contextStore, cn)
|
||||
}
|
||||
|
||||
// setGoDebug is an escape hatch that sets the GODEBUG environment
|
||||
// variable value using docker context metadata.
|
||||
//
|
||||
// {
|
||||
// "Name": "my-context",
|
||||
// "Metadata": { "GODEBUG": "x509negativeserial=1" }
|
||||
// }
|
||||
//
|
||||
// WARNING: Setting x509negativeserial=1 allows Go's x509 library to accept
|
||||
// X.509 certificates with negative serial numbers.
|
||||
// This behavior is deprecated and non-compliant with current security
|
||||
// standards (RFC 5280). Accepting negative serial numbers can introduce
|
||||
// serious security vulnerabilities, including the risk of certificate
|
||||
// collision or bypass attacks.
|
||||
// This option should only be used for legacy compatibility and never in
|
||||
// production environments.
|
||||
// Use at your own risk.
|
||||
func setGoDebug(meta store.Metadata) {
|
||||
fieldName := "GODEBUG"
|
||||
godebugEnv := os.Getenv(fieldName)
|
||||
// early return if GODEBUG is already set. We don't want to override what
|
||||
// the user already sets.
|
||||
if godebugEnv != "" {
|
||||
return
|
||||
}
|
||||
|
||||
var cfg any
|
||||
var ok bool
|
||||
switch m := meta.Metadata.(type) {
|
||||
case DockerContext:
|
||||
cfg, ok = m.AdditionalFields[fieldName]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
case map[string]any:
|
||||
cfg, ok = m[fieldName]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
v, ok := cfg.(string)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// set the GODEBUG environment variable with whatever was in the context
|
||||
_ = os.Setenv(fieldName, v)
|
||||
}
|
||||
|
||||
func (cli *DockerCli) initialize() error {
|
||||
cli.init.Do(func() {
|
||||
cli.dockerEndpoint, cli.initErr = cli.getDockerEndPoint()
|
||||
if cli.initErr != nil {
|
||||
cli.initErr = errors.Wrap(cli.initErr, "unable to resolve docker endpoint")
|
||||
cli.initErr = fmt.Errorf("unable to resolve docker endpoint: %w", cli.initErr)
|
||||
return
|
||||
}
|
||||
if cli.client == nil {
|
||||
if cli.client, cli.initErr = newAPIClientFromEndpoint(cli.dockerEndpoint, cli.configFile); cli.initErr != nil {
|
||||
ops := []client.Opt{client.WithUserAgent(cli.userAgent)}
|
||||
if cli.client, cli.initErr = newAPIClientFromEndpoint(cli.dockerEndpoint, cli.configFile, ops...); cli.initErr != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
@ -495,16 +546,6 @@ func (cli *DockerCli) initialize() error {
|
||||
return cli.initErr
|
||||
}
|
||||
|
||||
// Apply all the operation on the cli
|
||||
func (cli *DockerCli) Apply(ops ...CLIOption) error {
|
||||
for _, op := range ops {
|
||||
if err := op(cli); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ServerInfo stores details about the supported features and platform of the
|
||||
// server
|
||||
type ServerInfo struct {
|
||||
@ -519,7 +560,7 @@ type ServerInfo struct {
|
||||
// in the ping response, or if an error occurred, in which case the client
|
||||
// should use other ways to get the current swarm status, such as the /swarm
|
||||
// endpoint.
|
||||
SwarmStatus *swarm.Status
|
||||
SwarmStatus *client.SwarmStatus
|
||||
}
|
||||
|
||||
// NewDockerCli returns a DockerCli instance with all operators applied on it.
|
||||
@ -527,15 +568,17 @@ type ServerInfo struct {
|
||||
// environment.
|
||||
func NewDockerCli(ops ...CLIOption) (*DockerCli, error) {
|
||||
defaultOps := []CLIOption{
|
||||
WithContentTrustFromEnv(),
|
||||
WithDefaultContextStoreConfig(),
|
||||
WithStandardStreams(),
|
||||
WithUserAgent(UserAgent()),
|
||||
}
|
||||
ops = append(defaultOps, ops...)
|
||||
|
||||
cli := &DockerCli{baseCtx: context.Background()}
|
||||
if err := cli.Apply(ops...); err != nil {
|
||||
return nil, err
|
||||
for _, op := range ops {
|
||||
if err := op(cli); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return cli, nil
|
||||
}
|
||||
@ -547,11 +590,11 @@ func getServerHost(hosts []string, defaultToTLS bool) (string, error) {
|
||||
case 1:
|
||||
return dopts.ParseHost(defaultToTLS, hosts[0])
|
||||
default:
|
||||
return "", errors.New("Specify only one -H")
|
||||
return "", errors.New("specify only one -H")
|
||||
}
|
||||
}
|
||||
|
||||
// UserAgent returns the user agent string used for making API requests
|
||||
// UserAgent returns the default user agent string used for making API requests.
|
||||
func UserAgent() string {
|
||||
return "Docker-Client/" + version.Version + " (" + runtime.GOOS + ")"
|
||||
}
|
||||
|
||||
@ -3,16 +3,16 @@ package command
|
||||
import (
|
||||
"context"
|
||||
"encoding/csv"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/moby/term"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// CLIOption is a functional argument to apply options to a [DockerCli]. These
|
||||
@ -75,28 +75,6 @@ func WithErrorStream(err io.Writer) CLIOption {
|
||||
}
|
||||
}
|
||||
|
||||
// WithContentTrustFromEnv enables content trust on a cli from environment variable DOCKER_CONTENT_TRUST value.
|
||||
func WithContentTrustFromEnv() CLIOption {
|
||||
return func(cli *DockerCli) error {
|
||||
cli.contentTrust = false
|
||||
if e := os.Getenv("DOCKER_CONTENT_TRUST"); e != "" {
|
||||
if t, err := strconv.ParseBool(e); t || err != nil {
|
||||
// treat any other value as true
|
||||
cli.contentTrust = true
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithContentTrust enables content trust on a cli.
|
||||
func WithContentTrust(enabled bool) CLIOption {
|
||||
return func(cli *DockerCli) error {
|
||||
cli.contentTrust = enabled
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithDefaultContextStoreConfig configures the cli to use the default context store configuration.
|
||||
func WithDefaultContextStoreConfig() CLIOption {
|
||||
return func(cli *DockerCli) error {
|
||||
@ -180,61 +158,70 @@ const envOverrideHTTPHeaders = "DOCKER_CUSTOM_HEADERS"
|
||||
// override headers with the same name).
|
||||
//
|
||||
// TODO(thaJeztah): this is a client Option, and should be moved to the client. It is non-exported for that reason.
|
||||
func withCustomHeadersFromEnv() client.Opt {
|
||||
return func(apiClient *client.Client) error {
|
||||
value := os.Getenv(envOverrideHTTPHeaders)
|
||||
if value == "" {
|
||||
return nil
|
||||
}
|
||||
csvReader := csv.NewReader(strings.NewReader(value))
|
||||
fields, err := csvReader.Read()
|
||||
if err != nil {
|
||||
return invalidParameter(errors.Errorf(
|
||||
"failed to parse custom headers from %s environment variable: value must be formatted as comma-separated key=value pairs",
|
||||
envOverrideHTTPHeaders,
|
||||
func withCustomHeadersFromEnv() (client.Opt, error) {
|
||||
value := os.Getenv(envOverrideHTTPHeaders)
|
||||
if value == "" {
|
||||
return nil, nil
|
||||
}
|
||||
csvReader := csv.NewReader(strings.NewReader(value))
|
||||
fields, err := csvReader.Read()
|
||||
if err != nil {
|
||||
return nil, invalidParameter(fmt.Errorf(
|
||||
"failed to parse custom headers from %s environment variable: value must be formatted as comma-separated key=value pairs",
|
||||
envOverrideHTTPHeaders,
|
||||
))
|
||||
}
|
||||
if len(fields) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
env := map[string]string{}
|
||||
for _, kv := range fields {
|
||||
k, v, hasValue := strings.Cut(kv, "=")
|
||||
|
||||
// Only strip whitespace in keys; preserve whitespace in values.
|
||||
k = strings.TrimSpace(k)
|
||||
|
||||
if k == "" {
|
||||
return nil, invalidParameter(fmt.Errorf(
|
||||
`failed to set custom headers from %s environment variable: value contains a key=value pair with an empty key: '%s'`,
|
||||
envOverrideHTTPHeaders, kv,
|
||||
))
|
||||
}
|
||||
if len(fields) == 0 {
|
||||
return nil
|
||||
|
||||
// We don't currently allow empty key=value pairs, and produce an error.
|
||||
// This is something we could allow in future (e.g. to read value
|
||||
// from an environment variable with the same name). In the meantime,
|
||||
// produce an error to prevent users from depending on this.
|
||||
if !hasValue {
|
||||
return nil, invalidParameter(fmt.Errorf(
|
||||
`failed to set custom headers from %s environment variable: missing "=" in key=value pair: '%s'`,
|
||||
envOverrideHTTPHeaders, kv,
|
||||
))
|
||||
}
|
||||
|
||||
env := map[string]string{}
|
||||
for _, kv := range fields {
|
||||
k, v, hasValue := strings.Cut(kv, "=")
|
||||
env[http.CanonicalHeaderKey(k)] = v
|
||||
}
|
||||
|
||||
// Only strip whitespace in keys; preserve whitespace in values.
|
||||
k = strings.TrimSpace(k)
|
||||
if len(env) == 0 {
|
||||
// We should probably not hit this case, as we don't skip values
|
||||
// (only return errors), but we don't want to discard existing
|
||||
// headers with an empty set.
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if k == "" {
|
||||
return invalidParameter(errors.Errorf(
|
||||
`failed to set custom headers from %s environment variable: value contains a key=value pair with an empty key: '%s'`,
|
||||
envOverrideHTTPHeaders, kv,
|
||||
))
|
||||
}
|
||||
// TODO(thaJeztah): add a client.WithExtraHTTPHeaders() function to allow these headers to be _added_ to existing ones, instead of _replacing_
|
||||
// see https://github.com/docker/cli/pull/5098#issuecomment-2147403871 (when updating, also update the WARNING in the function and env-var GoDoc)
|
||||
return client.WithHTTPHeaders(env), nil
|
||||
}
|
||||
|
||||
// We don't currently allow empty key=value pairs, and produce an error.
|
||||
// This is something we could allow in future (e.g. to read value
|
||||
// from an environment variable with the same name). In the meantime,
|
||||
// produce an error to prevent users from depending on this.
|
||||
if !hasValue {
|
||||
return invalidParameter(errors.Errorf(
|
||||
`failed to set custom headers from %s environment variable: missing "=" in key=value pair: '%s'`,
|
||||
envOverrideHTTPHeaders, kv,
|
||||
))
|
||||
}
|
||||
|
||||
env[http.CanonicalHeaderKey(k)] = v
|
||||
// WithUserAgent configures the User-Agent string for cli HTTP requests.
|
||||
func WithUserAgent(userAgent string) CLIOption {
|
||||
return func(cli *DockerCli) error {
|
||||
if userAgent == "" {
|
||||
return errors.New("user agent cannot be blank")
|
||||
}
|
||||
|
||||
if len(env) == 0 {
|
||||
// We should probably not hit this case, as we don't skip values
|
||||
// (only return errors), but we don't want to discard existing
|
||||
// headers with an empty set.
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO(thaJeztah): add a client.WithExtraHTTPHeaders() function to allow these headers to be _added_ to existing ones, instead of _replacing_
|
||||
// see https://github.com/docker/cli/pull/5098#issuecomment-2147403871 (when updating, also update the WARNING in the function and env-var GoDoc)
|
||||
return client.WithHTTPHeaders(env)(apiClient)
|
||||
cli.userAgent = userAgent
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,28 +0,0 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func contentTrustEnabled(t *testing.T) bool {
|
||||
t.Helper()
|
||||
var cli DockerCli
|
||||
assert.NilError(t, WithContentTrustFromEnv()(&cli))
|
||||
return cli.contentTrust
|
||||
}
|
||||
|
||||
// NB: Do not t.Parallel() this test -- it messes with the process environment.
|
||||
func TestWithContentTrustFromEnv(t *testing.T) {
|
||||
const envvar = "DOCKER_CONTENT_TRUST"
|
||||
t.Setenv(envvar, "true")
|
||||
assert.Check(t, contentTrustEnabled(t))
|
||||
t.Setenv(envvar, "false")
|
||||
assert.Check(t, !contentTrustEnabled(t))
|
||||
t.Setenv(envvar, "invalid")
|
||||
assert.Check(t, contentTrustEnabled(t))
|
||||
os.Unsetenv(envvar)
|
||||
assert.Check(t, !contentTrustEnabled(t))
|
||||
}
|
||||
@ -18,10 +18,9 @@ import (
|
||||
|
||||
"github.com/docker/cli/cli/config"
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
"github.com/docker/cli/cli/context/store"
|
||||
"github.com/docker/cli/cli/flags"
|
||||
"github.com/docker/docker/api"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/moby/moby/client"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
@ -34,7 +33,7 @@ func TestNewAPIClientFromFlags(t *testing.T) {
|
||||
apiClient, err := NewAPIClientFromFlags(opts, &configfile.ConfigFile{})
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, apiClient.DaemonHost(), host)
|
||||
assert.Equal(t, apiClient.ClientVersion(), api.DefaultVersion)
|
||||
assert.Equal(t, apiClient.ClientVersion(), client.MaxAPIVersion)
|
||||
}
|
||||
|
||||
func TestNewAPIClientFromFlagsForDefaultSchema(t *testing.T) {
|
||||
@ -47,7 +46,7 @@ func TestNewAPIClientFromFlagsForDefaultSchema(t *testing.T) {
|
||||
apiClient, err := NewAPIClientFromFlags(opts, &configfile.ConfigFile{})
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, apiClient.DaemonHost(), slug+host)
|
||||
assert.Equal(t, apiClient.ClientVersion(), api.DefaultVersion)
|
||||
assert.Equal(t, apiClient.ClientVersion(), client.MaxAPIVersion)
|
||||
}
|
||||
|
||||
func TestNewAPIClientFromFlagsWithCustomHeaders(t *testing.T) {
|
||||
@ -71,7 +70,7 @@ func TestNewAPIClientFromFlagsWithCustomHeaders(t *testing.T) {
|
||||
apiClient, err := NewAPIClientFromFlags(opts, configFile)
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, apiClient.DaemonHost(), host)
|
||||
assert.Equal(t, apiClient.ClientVersion(), api.DefaultVersion)
|
||||
assert.Equal(t, apiClient.ClientVersion(), client.MaxAPIVersion)
|
||||
|
||||
// verify User-Agent is not appended to the configfile. see https://github.com/docker/cli/pull/2756
|
||||
assert.DeepEqual(t, configFile.HTTPHeaders, map[string]string{"My-Header": "Custom-Value"})
|
||||
@ -80,7 +79,7 @@ func TestNewAPIClientFromFlagsWithCustomHeaders(t *testing.T) {
|
||||
"My-Header": "Custom-Value",
|
||||
"User-Agent": UserAgent(),
|
||||
}
|
||||
_, err = apiClient.Ping(context.Background())
|
||||
_, err = apiClient.Ping(context.TODO(), client.PingOptions{})
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, received, expectedHeaders)
|
||||
}
|
||||
@ -106,7 +105,7 @@ func TestNewAPIClientFromFlagsWithCustomHeadersFromEnv(t *testing.T) {
|
||||
apiClient, err := NewAPIClientFromFlags(opts, configFile)
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, apiClient.DaemonHost(), host)
|
||||
assert.Equal(t, apiClient.ClientVersion(), api.DefaultVersion)
|
||||
assert.Equal(t, apiClient.ClientVersion(), client.MaxAPIVersion)
|
||||
|
||||
expectedHeaders := http.Header{
|
||||
"One": []string{"one-value"},
|
||||
@ -115,7 +114,7 @@ func TestNewAPIClientFromFlagsWithCustomHeadersFromEnv(t *testing.T) {
|
||||
"Four": []string{"four-value-override"},
|
||||
"User-Agent": []string{UserAgent()},
|
||||
}
|
||||
_, err = apiClient.Ping(context.Background())
|
||||
_, err = apiClient.Ping(context.TODO(), client.PingOptions{})
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, received, expectedHeaders)
|
||||
}
|
||||
@ -135,51 +134,55 @@ func TestNewAPIClientFromFlagsWithAPIVersionFromEnv(t *testing.T) {
|
||||
|
||||
type fakeClient struct {
|
||||
client.Client
|
||||
pingFunc func() (types.Ping, error)
|
||||
pingFunc func() (client.PingResult, error)
|
||||
version string
|
||||
negotiated bool
|
||||
}
|
||||
|
||||
func (c *fakeClient) Ping(_ context.Context) (types.Ping, error) {
|
||||
return c.pingFunc()
|
||||
func (c *fakeClient) Ping(_ context.Context, options client.PingOptions) (client.PingResult, error) {
|
||||
res, err := c.pingFunc()
|
||||
if options.NegotiateAPIVersion {
|
||||
if res.APIVersion != "" {
|
||||
if c.negotiated || options.ForceNegotiate {
|
||||
c.negotiated = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (c *fakeClient) ClientVersion() string {
|
||||
return c.version
|
||||
}
|
||||
|
||||
func (c *fakeClient) NegotiateAPIVersionPing(types.Ping) {
|
||||
c.negotiated = true
|
||||
}
|
||||
|
||||
func TestInitializeFromClient(t *testing.T) {
|
||||
const defaultVersion = "v1.55"
|
||||
|
||||
testcases := []struct {
|
||||
doc string
|
||||
pingFunc func() (types.Ping, error)
|
||||
pingFunc func() (client.PingResult, error)
|
||||
expectedServer ServerInfo
|
||||
negotiated bool
|
||||
}{
|
||||
{
|
||||
doc: "successful ping",
|
||||
pingFunc: func() (types.Ping, error) {
|
||||
return types.Ping{Experimental: true, OSType: "linux", APIVersion: "v1.30"}, nil
|
||||
pingFunc: func() (client.PingResult, error) {
|
||||
return client.PingResult{Experimental: true, OSType: "linux", APIVersion: "v1.44"}, nil
|
||||
},
|
||||
expectedServer: ServerInfo{HasExperimental: true, OSType: "linux"},
|
||||
negotiated: true,
|
||||
},
|
||||
{
|
||||
doc: "failed ping, no API version",
|
||||
pingFunc: func() (types.Ping, error) {
|
||||
return types.Ping{}, errors.New("failed")
|
||||
pingFunc: func() (client.PingResult, error) {
|
||||
return client.PingResult{}, errors.New("failed")
|
||||
},
|
||||
expectedServer: ServerInfo{HasExperimental: true},
|
||||
},
|
||||
{
|
||||
doc: "failed ping, with API version",
|
||||
pingFunc: func() (types.Ping, error) {
|
||||
return types.Ping{APIVersion: "v1.33"}, errors.New("failed")
|
||||
pingFunc: func() (client.PingResult, error) {
|
||||
return client.PingResult{APIVersion: "v1.44"}, errors.New("failed")
|
||||
},
|
||||
expectedServer: ServerInfo{HasExperimental: true},
|
||||
negotiated: true,
|
||||
@ -188,16 +191,16 @@ func TestInitializeFromClient(t *testing.T) {
|
||||
|
||||
for _, tc := range testcases {
|
||||
t.Run(tc.doc, func(t *testing.T) {
|
||||
apiclient := &fakeClient{
|
||||
apiClient := &fakeClient{
|
||||
pingFunc: tc.pingFunc,
|
||||
version: defaultVersion,
|
||||
}
|
||||
|
||||
cli := &DockerCli{client: apiclient}
|
||||
cli := &DockerCli{client: apiClient}
|
||||
err := cli.Initialize(flags.NewClientOptions())
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, cli.ServerInfo(), tc.expectedServer)
|
||||
assert.Equal(t, apiclient.negotiated, tc.negotiated)
|
||||
assert.Equal(t, apiClient.negotiated, tc.negotiated)
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -211,7 +214,7 @@ func TestInitializeFromClientHangs(t *testing.T) {
|
||||
assert.NilError(t, err)
|
||||
|
||||
receiveReqCh := make(chan bool)
|
||||
timeoutCtx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
timeoutCtx, cancel := context.WithTimeout(context.TODO(), time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Simulate a server that hangs on connections.
|
||||
@ -255,8 +258,14 @@ func TestInitializeFromClientHangs(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNewDockerCliAndOperators(t *testing.T) {
|
||||
// Test default operations and also overriding default ones
|
||||
cli, err := NewDockerCli(WithInputStream(io.NopCloser(strings.NewReader("some input"))))
|
||||
outbuf := bytes.NewBuffer(nil)
|
||||
errbuf := bytes.NewBuffer(nil)
|
||||
|
||||
cli, err := NewDockerCli(
|
||||
WithInputStream(io.NopCloser(strings.NewReader("some input"))),
|
||||
WithOutputStream(outbuf),
|
||||
WithErrorStream(errbuf),
|
||||
)
|
||||
assert.NilError(t, err)
|
||||
// Check streams are initialized
|
||||
assert.Check(t, cli.In() != nil)
|
||||
@ -266,19 +275,6 @@ func TestNewDockerCliAndOperators(t *testing.T) {
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, string(inputStream), "some input")
|
||||
|
||||
// Apply can modify a dockerCli after construction
|
||||
outbuf := bytes.NewBuffer(nil)
|
||||
errbuf := bytes.NewBuffer(nil)
|
||||
err = cli.Apply(
|
||||
WithInputStream(io.NopCloser(strings.NewReader("input"))),
|
||||
WithOutputStream(outbuf),
|
||||
WithErrorStream(errbuf),
|
||||
)
|
||||
assert.NilError(t, err)
|
||||
// Check input stream
|
||||
inputStream, err = io.ReadAll(cli.In())
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, string(inputStream), "input")
|
||||
// Check output stream
|
||||
_, err = fmt.Fprint(cli.Out(), "output")
|
||||
assert.NilError(t, err)
|
||||
@ -296,9 +292,9 @@ func TestNewDockerCliAndOperators(t *testing.T) {
|
||||
func TestInitializeShouldAlwaysCreateTheContextStore(t *testing.T) {
|
||||
cli, err := NewDockerCli()
|
||||
assert.NilError(t, err)
|
||||
assert.NilError(t, cli.Initialize(flags.NewClientOptions(), WithInitializeClient(func(cli *DockerCli) (client.APIClient, error) {
|
||||
return client.NewClientWithOpts()
|
||||
})))
|
||||
apiClient, err := client.New()
|
||||
assert.NilError(t, err)
|
||||
assert.NilError(t, cli.Initialize(flags.NewClientOptions(), WithAPIClient(apiClient)))
|
||||
assert.Check(t, cli.ContextStore() != nil)
|
||||
}
|
||||
|
||||
@ -353,3 +349,46 @@ func TestHooksEnabled(t *testing.T) {
|
||||
assert.Check(t, !cli.HooksEnabled())
|
||||
})
|
||||
}
|
||||
|
||||
func TestSetGoDebug(t *testing.T) {
|
||||
t.Run("GODEBUG already set", func(t *testing.T) {
|
||||
t.Setenv("GODEBUG", "val1,val2")
|
||||
meta := store.Metadata{}
|
||||
setGoDebug(meta)
|
||||
assert.Equal(t, "val1,val2", os.Getenv("GODEBUG"))
|
||||
})
|
||||
t.Run("GODEBUG in context metadata can set env", func(t *testing.T) {
|
||||
meta := store.Metadata{
|
||||
Metadata: DockerContext{
|
||||
AdditionalFields: map[string]any{
|
||||
"GODEBUG": "val1,val2=1",
|
||||
},
|
||||
},
|
||||
}
|
||||
setGoDebug(meta)
|
||||
assert.Equal(t, "val1,val2=1", os.Getenv("GODEBUG"))
|
||||
})
|
||||
}
|
||||
|
||||
func TestNewDockerCliWithCustomUserAgent(t *testing.T) {
|
||||
var received string
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
received = r.UserAgent()
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer ts.Close()
|
||||
host := strings.Replace(ts.URL, "http://", "tcp://", 1)
|
||||
opts := &flags.ClientOptions{Hosts: []string{host}}
|
||||
|
||||
cli, err := NewDockerCli(
|
||||
WithUserAgent("fake-agent/0.0.1"),
|
||||
)
|
||||
assert.NilError(t, err)
|
||||
cli.currentContext = DefaultContextName
|
||||
cli.options = opts
|
||||
cli.configFile = &configfile.ConfigFile{}
|
||||
|
||||
_, err = cli.Client().Ping(context.TODO(), client.PingOptions{})
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, received, "fake-agent/0.0.1")
|
||||
}
|
||||
|
||||
@ -1,110 +1,31 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/builder"
|
||||
"github.com/docker/cli/cli/command/checkpoint"
|
||||
"github.com/docker/cli/cli/command/config"
|
||||
"github.com/docker/cli/cli/command/container"
|
||||
"github.com/docker/cli/cli/command/context"
|
||||
"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"
|
||||
"github.com/docker/cli/cli/command/registry"
|
||||
"github.com/docker/cli/cli/command/secret"
|
||||
"github.com/docker/cli/cli/command/service"
|
||||
"github.com/docker/cli/cli/command/stack"
|
||||
"github.com/docker/cli/cli/command/swarm"
|
||||
"github.com/docker/cli/cli/command/system"
|
||||
"github.com/docker/cli/cli/command/trust"
|
||||
"github.com/docker/cli/cli/command/volume"
|
||||
_ "github.com/docker/cli/cli/command/builder"
|
||||
_ "github.com/docker/cli/cli/command/checkpoint"
|
||||
_ "github.com/docker/cli/cli/command/config"
|
||||
_ "github.com/docker/cli/cli/command/container"
|
||||
_ "github.com/docker/cli/cli/command/context"
|
||||
_ "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"
|
||||
_ "github.com/docker/cli/cli/command/registry"
|
||||
_ "github.com/docker/cli/cli/command/secret"
|
||||
_ "github.com/docker/cli/cli/command/service"
|
||||
_ "github.com/docker/cli/cli/command/stack"
|
||||
_ "github.com/docker/cli/cli/command/swarm"
|
||||
_ "github.com/docker/cli/cli/command/system"
|
||||
_ "github.com/docker/cli/cli/command/trust"
|
||||
_ "github.com/docker/cli/cli/command/volume"
|
||||
"github.com/docker/cli/internal/commands"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// AddCommands adds all the commands from cli/command to the root command
|
||||
func AddCommands(cmd *cobra.Command, dockerCli command.Cli) {
|
||||
cmd.AddCommand(
|
||||
// commonly used shorthands
|
||||
container.NewRunCommand(dockerCli),
|
||||
container.NewExecCommand(dockerCli),
|
||||
container.NewPsCommand(dockerCli),
|
||||
image.NewBuildCommand(dockerCli),
|
||||
image.NewPullCommand(dockerCli),
|
||||
image.NewPushCommand(dockerCli),
|
||||
image.NewImagesCommand(dockerCli),
|
||||
registry.NewLoginCommand(dockerCli),
|
||||
registry.NewLogoutCommand(dockerCli),
|
||||
registry.NewSearchCommand(dockerCli),
|
||||
system.NewVersionCommand(dockerCli),
|
||||
system.NewInfoCommand(dockerCli),
|
||||
|
||||
// management commands
|
||||
builder.NewBakeStubCommand(dockerCli),
|
||||
builder.NewBuilderCommand(dockerCli),
|
||||
checkpoint.NewCheckpointCommand(dockerCli),
|
||||
container.NewContainerCommand(dockerCli),
|
||||
context.NewContextCommand(dockerCli),
|
||||
image.NewImageCommand(dockerCli),
|
||||
manifest.NewManifestCommand(dockerCli),
|
||||
network.NewNetworkCommand(dockerCli),
|
||||
plugin.NewPluginCommand(dockerCli),
|
||||
system.NewSystemCommand(dockerCli),
|
||||
trust.NewTrustCommand(dockerCli),
|
||||
volume.NewVolumeCommand(dockerCli),
|
||||
|
||||
// orchestration (swarm) commands
|
||||
config.NewConfigCommand(dockerCli),
|
||||
node.NewNodeCommand(dockerCli),
|
||||
secret.NewSecretCommand(dockerCli),
|
||||
service.NewServiceCommand(dockerCli),
|
||||
stack.NewStackCommand(dockerCli),
|
||||
swarm.NewSwarmCommand(dockerCli),
|
||||
|
||||
// legacy commands may be hidden
|
||||
hide(container.NewAttachCommand(dockerCli)),
|
||||
hide(container.NewCommitCommand(dockerCli)),
|
||||
hide(container.NewCopyCommand(dockerCli)),
|
||||
hide(container.NewCreateCommand(dockerCli)),
|
||||
hide(container.NewDiffCommand(dockerCli)),
|
||||
hide(container.NewExportCommand(dockerCli)),
|
||||
hide(container.NewKillCommand(dockerCli)),
|
||||
hide(container.NewLogsCommand(dockerCli)),
|
||||
hide(container.NewPauseCommand(dockerCli)),
|
||||
hide(container.NewPortCommand(dockerCli)),
|
||||
hide(container.NewRenameCommand(dockerCli)),
|
||||
hide(container.NewRestartCommand(dockerCli)),
|
||||
hide(container.NewRmCommand(dockerCli)),
|
||||
hide(container.NewStartCommand(dockerCli)),
|
||||
hide(container.NewStatsCommand(dockerCli)),
|
||||
hide(container.NewStopCommand(dockerCli)),
|
||||
hide(container.NewTopCommand(dockerCli)),
|
||||
hide(container.NewUnpauseCommand(dockerCli)),
|
||||
hide(container.NewUpdateCommand(dockerCli)),
|
||||
hide(container.NewWaitCommand(dockerCli)),
|
||||
hide(image.NewHistoryCommand(dockerCli)),
|
||||
hide(image.NewImportCommand(dockerCli)),
|
||||
hide(image.NewLoadCommand(dockerCli)),
|
||||
hide(image.NewRemoveCommand(dockerCli)),
|
||||
hide(image.NewSaveCommand(dockerCli)),
|
||||
hide(image.NewTagCommand(dockerCli)),
|
||||
hide(system.NewEventsCommand(dockerCli)),
|
||||
hide(system.NewInspectCommand(dockerCli)),
|
||||
)
|
||||
}
|
||||
|
||||
func hide(cmd *cobra.Command) *cobra.Command {
|
||||
// If the environment variable with name "DOCKER_HIDE_LEGACY_COMMANDS" is not empty,
|
||||
// these legacy commands (such as `docker ps`, `docker exec`, etc)
|
||||
// will not be shown in output console.
|
||||
if os.Getenv("DOCKER_HIDE_LEGACY_COMMANDS") == "" {
|
||||
return cmd
|
||||
func AddCommands(cmd *cobra.Command, dockerCLI command.Cli) {
|
||||
for _, c := range commands.Commands() {
|
||||
cmd.AddCommand(c(dockerCLI))
|
||||
}
|
||||
cmdCopy := *cmd
|
||||
cmdCopy.Hidden = true
|
||||
cmdCopy.Aliases = []string{}
|
||||
return &cmdCopy
|
||||
}
|
||||
|
||||
@ -4,21 +4,14 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/distribution/reference"
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/docker/api/types/volume"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// ValidArgsFn a function to be used by cobra command as `ValidArgsFunction` to offer command line completion.
|
||||
//
|
||||
// Deprecated: use [cobra.CompletionFunc].
|
||||
type ValidArgsFn = cobra.CompletionFunc
|
||||
|
||||
// APIClientProvider provides a method to get an [client.APIClient], initializing
|
||||
// APIClientProvider provides a method to get a [client.APIClient], initializing
|
||||
// it if needed.
|
||||
//
|
||||
// It's a smaller interface than [command.Cli], and used in situations where an
|
||||
@ -34,24 +27,57 @@ func ImageNames(dockerCLI APIClientProvider, limit int) cobra.CompletionFunc {
|
||||
if limit > 0 && len(args) >= limit {
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
list, err := dockerCLI.Client().ImageList(cmd.Context(), image.ListOptions{})
|
||||
res, err := dockerCLI.Client().ImageList(cmd.Context(), client.ImageListOptions{})
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveError
|
||||
}
|
||||
var names []string
|
||||
for _, img := range list {
|
||||
for _, img := range res.Items {
|
||||
names = append(names, img.RepoTags...)
|
||||
}
|
||||
return names, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
}
|
||||
|
||||
// ImageNamesWithBase offers completion for images present within the local store,
|
||||
// including both full image names with tags and base image names (repository names only)
|
||||
// when multiple tags exist for the same base name
|
||||
func ImageNamesWithBase(dockerCLI APIClientProvider, limit int) cobra.CompletionFunc {
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
if limit > 0 && len(args) >= limit {
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
res, err := dockerCLI.Client().ImageList(cmd.Context(), client.ImageListOptions{})
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveError
|
||||
}
|
||||
var names []string
|
||||
baseNameCounts := make(map[string]int)
|
||||
for _, img := range res.Items {
|
||||
names = append(names, img.RepoTags...)
|
||||
for _, tag := range img.RepoTags {
|
||||
ref, err := reference.ParseNormalizedNamed(tag)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
baseNameCounts[reference.FamiliarName(ref)]++
|
||||
}
|
||||
}
|
||||
for baseName, count := range baseNameCounts {
|
||||
if count > 1 {
|
||||
names = append(names, baseName)
|
||||
}
|
||||
}
|
||||
return names, cobra.ShellCompDirectiveNoSpace | cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
}
|
||||
|
||||
// ContainerNames offers completion for container names and IDs
|
||||
// By default, only names are returned.
|
||||
// Set DOCKER_COMPLETION_SHOW_CONTAINER_IDS=yes to also complete IDs.
|
||||
func ContainerNames(dockerCLI APIClientProvider, all bool, filters ...func(container.Summary) bool) cobra.CompletionFunc {
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
list, err := dockerCLI.Client().ContainerList(cmd.Context(), container.ListOptions{
|
||||
res, err := dockerCLI.Client().ContainerList(cmd.Context(), client.ContainerListOptions{
|
||||
All: all,
|
||||
})
|
||||
if err != nil {
|
||||
@ -61,7 +87,7 @@ func ContainerNames(dockerCLI APIClientProvider, all bool, filters ...func(conta
|
||||
showContainerIDs := os.Getenv("DOCKER_COMPLETION_SHOW_CONTAINER_IDS") == "yes"
|
||||
|
||||
var names []string
|
||||
for _, ctr := range list {
|
||||
for _, ctr := range res.Items {
|
||||
skip := false
|
||||
for _, fn := range filters {
|
||||
if fn != nil && !fn(ctr) {
|
||||
@ -84,12 +110,12 @@ func ContainerNames(dockerCLI APIClientProvider, all bool, filters ...func(conta
|
||||
// VolumeNames offers completion for volumes
|
||||
func VolumeNames(dockerCLI APIClientProvider) cobra.CompletionFunc {
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
list, err := dockerCLI.Client().VolumeList(cmd.Context(), volume.ListOptions{})
|
||||
res, err := dockerCLI.Client().VolumeList(cmd.Context(), client.VolumeListOptions{})
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveError
|
||||
}
|
||||
var names []string
|
||||
for _, vol := range list.Volumes {
|
||||
for _, vol := range res.Items {
|
||||
names = append(names, vol.Name)
|
||||
}
|
||||
return names, cobra.ShellCompDirectiveNoFileComp
|
||||
@ -99,12 +125,12 @@ func VolumeNames(dockerCLI APIClientProvider) cobra.CompletionFunc {
|
||||
// NetworkNames offers completion for networks
|
||||
func NetworkNames(dockerCLI APIClientProvider) cobra.CompletionFunc {
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
list, err := dockerCLI.Client().NetworkList(cmd.Context(), network.ListOptions{})
|
||||
res, err := dockerCLI.Client().NetworkList(cmd.Context(), client.NetworkListOptions{})
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveError
|
||||
}
|
||||
var names []string
|
||||
for _, nw := range list {
|
||||
for _, nw := range res.Items {
|
||||
names = append(names, nw.Name)
|
||||
}
|
||||
return names, cobra.ShellCompDirectiveNoFileComp
|
||||
@ -124,14 +150,16 @@ func NetworkNames(dockerCLI APIClientProvider) cobra.CompletionFunc {
|
||||
// export MY_VAR=hello
|
||||
// docker run --rm --env MY_VAR alpine printenv MY_VAR
|
||||
// hello
|
||||
func EnvVarNames(_ *cobra.Command, _ []string, _ string) (names []string, _ cobra.ShellCompDirective) {
|
||||
envs := os.Environ()
|
||||
names = make([]string, 0, len(envs))
|
||||
for _, env := range envs {
|
||||
name, _, _ := strings.Cut(env, "=")
|
||||
names = append(names, name)
|
||||
func EnvVarNames() cobra.CompletionFunc {
|
||||
return func(_ *cobra.Command, _ []string, _ string) (names []string, _ cobra.ShellCompDirective) {
|
||||
envs := os.Environ()
|
||||
names = make([]string, 0, len(envs))
|
||||
for _, env := range envs {
|
||||
name, _, _ := strings.Cut(env, "=")
|
||||
names = append(names, name)
|
||||
}
|
||||
return names, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
return names, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
|
||||
// FromList offers completion for the given list of options.
|
||||
@ -142,13 +170,10 @@ func FromList(options ...string) cobra.CompletionFunc {
|
||||
// FileNames is a convenience function to use [cobra.ShellCompDirectiveDefault],
|
||||
// which indicates to let the shell perform its default behavior after
|
||||
// completions have been provided.
|
||||
func FileNames(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
|
||||
return nil, cobra.ShellCompDirectiveDefault
|
||||
}
|
||||
|
||||
// NoComplete is used for commands where there's no relevant completion
|
||||
func NoComplete(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
func FileNames() cobra.CompletionFunc {
|
||||
return func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
|
||||
return nil, cobra.ShellCompDirectiveDefault
|
||||
}
|
||||
}
|
||||
|
||||
var commonPlatforms = []string{
|
||||
@ -188,6 +213,8 @@ var commonPlatforms = []string{
|
||||
// - we currently exclude architectures that may have unofficial builds,
|
||||
// but don't have wide adoption (and no support), such as loong64, mipsXXX,
|
||||
// ppc64 (non-le) to prevent confusion.
|
||||
func Platforms(_ *cobra.Command, _ []string, _ string) (platforms []string, _ cobra.ShellCompDirective) {
|
||||
return commonPlatforms, cobra.ShellCompDirectiveNoFileComp
|
||||
func Platforms() cobra.CompletionFunc {
|
||||
return func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
|
||||
return commonPlatforms, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,13 +6,11 @@ import (
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/docker/api/types/volume"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/api/types/image"
|
||||
"github.com/moby/moby/api/types/network"
|
||||
"github.com/moby/moby/api/types/volume"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/spf13/cobra"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
@ -30,38 +28,38 @@ func (c fakeCLI) Client() client.APIClient {
|
||||
|
||||
type fakeClient struct {
|
||||
client.Client
|
||||
containerListFunc func(options container.ListOptions) ([]container.Summary, error)
|
||||
imageListFunc func(options image.ListOptions) ([]image.Summary, error)
|
||||
networkListFunc func(ctx context.Context, options network.ListOptions) ([]network.Summary, error)
|
||||
volumeListFunc func(filter filters.Args) (volume.ListResponse, error)
|
||||
containerListFunc func(context.Context, client.ContainerListOptions) (client.ContainerListResult, error)
|
||||
imageListFunc func(context.Context, client.ImageListOptions) (client.ImageListResult, error)
|
||||
networkListFunc func(context.Context, client.NetworkListOptions) (client.NetworkListResult, error)
|
||||
volumeListFunc func(context.Context, client.VolumeListOptions) (client.VolumeListResult, error)
|
||||
}
|
||||
|
||||
func (c *fakeClient) ContainerList(_ context.Context, options container.ListOptions) ([]container.Summary, error) {
|
||||
func (c *fakeClient) ContainerList(ctx context.Context, options client.ContainerListOptions) (client.ContainerListResult, error) {
|
||||
if c.containerListFunc != nil {
|
||||
return c.containerListFunc(options)
|
||||
return c.containerListFunc(ctx, options)
|
||||
}
|
||||
return []container.Summary{}, nil
|
||||
return client.ContainerListResult{}, nil
|
||||
}
|
||||
|
||||
func (c *fakeClient) ImageList(_ context.Context, options image.ListOptions) ([]image.Summary, error) {
|
||||
func (c *fakeClient) ImageList(ctx context.Context, options client.ImageListOptions) (client.ImageListResult, error) {
|
||||
if c.imageListFunc != nil {
|
||||
return c.imageListFunc(options)
|
||||
return c.imageListFunc(ctx, options)
|
||||
}
|
||||
return []image.Summary{}, nil
|
||||
return client.ImageListResult{}, nil
|
||||
}
|
||||
|
||||
func (c *fakeClient) NetworkList(ctx context.Context, options network.ListOptions) ([]network.Summary, error) {
|
||||
func (c *fakeClient) NetworkList(ctx context.Context, options client.NetworkListOptions) (client.NetworkListResult, error) {
|
||||
if c.networkListFunc != nil {
|
||||
return c.networkListFunc(ctx, options)
|
||||
}
|
||||
return []network.Inspect{}, nil
|
||||
return client.NetworkListResult{}, nil
|
||||
}
|
||||
|
||||
func (c *fakeClient) VolumeList(_ context.Context, options volume.ListOptions) (volume.ListResponse, error) {
|
||||
func (c *fakeClient) VolumeList(ctx context.Context, options client.VolumeListOptions) (client.VolumeListResult, error) {
|
||||
if c.volumeListFunc != nil {
|
||||
return c.volumeListFunc(options.Filters)
|
||||
return c.volumeListFunc(ctx, options)
|
||||
}
|
||||
return volume.ListResponse{}, nil
|
||||
return client.VolumeListResult{}, nil
|
||||
}
|
||||
|
||||
func TestCompleteContainerNames(t *testing.T) {
|
||||
@ -71,7 +69,7 @@ func TestCompleteContainerNames(t *testing.T) {
|
||||
filters []func(container.Summary) bool
|
||||
containers []container.Summary
|
||||
expOut []string
|
||||
expOpts container.ListOptions
|
||||
expOpts client.ContainerListOptions
|
||||
expDirective cobra.ShellCompDirective
|
||||
}{
|
||||
{
|
||||
@ -87,7 +85,7 @@ func TestCompleteContainerNames(t *testing.T) {
|
||||
{ID: "id-a", State: container.StateExited, Names: []string{"/container-a"}},
|
||||
},
|
||||
expOut: []string{"container-c", "container-c/link-b", "container-b", "container-a"},
|
||||
expOpts: container.ListOptions{All: true},
|
||||
expOpts: client.ContainerListOptions{All: true},
|
||||
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
||||
},
|
||||
{
|
||||
@ -100,7 +98,7 @@ func TestCompleteContainerNames(t *testing.T) {
|
||||
{ID: "id-a", State: container.StateExited, Names: []string{"/container-a"}},
|
||||
},
|
||||
expOut: []string{"id-c", "container-c", "container-c/link-b", "id-b", "container-b", "id-a", "container-a"},
|
||||
expOpts: container.ListOptions{All: true},
|
||||
expOpts: client.ContainerListOptions{All: true},
|
||||
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
||||
},
|
||||
{
|
||||
@ -124,7 +122,7 @@ func TestCompleteContainerNames(t *testing.T) {
|
||||
{ID: "id-a", State: container.StateExited, Names: []string{"/container-a"}},
|
||||
},
|
||||
expOut: []string{"container-b"},
|
||||
expOpts: container.ListOptions{All: true},
|
||||
expOpts: client.ContainerListOptions{All: true},
|
||||
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
||||
},
|
||||
{
|
||||
@ -140,7 +138,7 @@ func TestCompleteContainerNames(t *testing.T) {
|
||||
{ID: "id-a", State: container.StateCreated, Names: []string{"/container-a"}},
|
||||
},
|
||||
expOut: []string{"container-a"},
|
||||
expOpts: container.ListOptions{All: true},
|
||||
expOpts: client.ContainerListOptions{All: true},
|
||||
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
||||
},
|
||||
{
|
||||
@ -155,12 +153,12 @@ func TestCompleteContainerNames(t *testing.T) {
|
||||
t.Setenv("DOCKER_COMPLETION_SHOW_CONTAINER_IDS", "yes")
|
||||
}
|
||||
comp := ContainerNames(fakeCLI{&fakeClient{
|
||||
containerListFunc: func(opts container.ListOptions) ([]container.Summary, error) {
|
||||
assert.Check(t, is.DeepEqual(opts, tc.expOpts, cmpopts.IgnoreUnexported(container.ListOptions{}, filters.Args{})))
|
||||
containerListFunc: func(_ context.Context, opts client.ContainerListOptions) (client.ContainerListResult, error) {
|
||||
assert.Check(t, is.DeepEqual(opts, tc.expOpts))
|
||||
if tc.expDirective == cobra.ShellCompDirectiveError {
|
||||
return nil, errors.New("some error occurred")
|
||||
return client.ContainerListResult{}, errors.New("some error occurred")
|
||||
}
|
||||
return tc.containers, nil
|
||||
return client.ContainerListResult{Items: tc.containers}, nil
|
||||
},
|
||||
}}, tc.showAll, tc.filters...)
|
||||
|
||||
@ -176,7 +174,7 @@ func TestCompleteEnvVarNames(t *testing.T) {
|
||||
"ENV_A": "hello-a",
|
||||
"ENV_B": "hello-b",
|
||||
})
|
||||
values, directives := EnvVarNames(nil, nil, "")
|
||||
values, directives := EnvVarNames()(nil, nil, "")
|
||||
assert.Check(t, is.Equal(directives&cobra.ShellCompDirectiveNoFileComp, cobra.ShellCompDirectiveNoFileComp), "Should not perform file completion")
|
||||
|
||||
sort.Strings(values)
|
||||
@ -185,7 +183,7 @@ func TestCompleteEnvVarNames(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCompleteFileNames(t *testing.T) {
|
||||
values, directives := FileNames(nil, nil, "")
|
||||
values, directives := FileNames()(nil, nil, "")
|
||||
assert.Check(t, is.Equal(directives, cobra.ShellCompDirectiveDefault))
|
||||
assert.Check(t, is.Len(values, 0))
|
||||
}
|
||||
@ -228,11 +226,11 @@ func TestCompleteImageNames(t *testing.T) {
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.doc, func(t *testing.T) {
|
||||
comp := ImageNames(fakeCLI{&fakeClient{
|
||||
imageListFunc: func(options image.ListOptions) ([]image.Summary, error) {
|
||||
imageListFunc: func(context.Context, client.ImageListOptions) (client.ImageListResult, error) {
|
||||
if tc.expDirective == cobra.ShellCompDirectiveError {
|
||||
return nil, errors.New("some error occurred")
|
||||
return client.ImageListResult{}, errors.New("some error occurred")
|
||||
}
|
||||
return tc.images, nil
|
||||
return client.ImageListResult{Items: tc.images}, nil
|
||||
},
|
||||
}}, -1)
|
||||
|
||||
@ -257,9 +255,24 @@ func TestCompleteNetworkNames(t *testing.T) {
|
||||
{
|
||||
doc: "with results",
|
||||
networks: []network.Summary{
|
||||
{ID: "nw-c", Name: "network-c"},
|
||||
{ID: "nw-b", Name: "network-b"},
|
||||
{ID: "nw-a", Name: "network-a"},
|
||||
{
|
||||
Network: network.Network{
|
||||
ID: "nw-c",
|
||||
Name: "network-c",
|
||||
},
|
||||
},
|
||||
{
|
||||
Network: network.Network{
|
||||
ID: "nw-b",
|
||||
Name: "network-b",
|
||||
},
|
||||
},
|
||||
{
|
||||
Network: network.Network{
|
||||
ID: "nw-a",
|
||||
Name: "network-a",
|
||||
},
|
||||
},
|
||||
},
|
||||
expOut: []string{"network-c", "network-b", "network-a"},
|
||||
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
||||
@ -273,11 +286,11 @@ func TestCompleteNetworkNames(t *testing.T) {
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.doc, func(t *testing.T) {
|
||||
comp := NetworkNames(fakeCLI{&fakeClient{
|
||||
networkListFunc: func(ctx context.Context, options network.ListOptions) ([]network.Summary, error) {
|
||||
networkListFunc: func(context.Context, client.NetworkListOptions) (client.NetworkListResult, error) {
|
||||
if tc.expDirective == cobra.ShellCompDirectiveError {
|
||||
return nil, errors.New("some error occurred")
|
||||
return client.NetworkListResult{}, errors.New("some error occurred")
|
||||
}
|
||||
return tc.networks, nil
|
||||
return client.NetworkListResult{Items: tc.networks}, nil
|
||||
},
|
||||
}})
|
||||
|
||||
@ -288,14 +301,8 @@ func TestCompleteNetworkNames(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompleteNoComplete(t *testing.T) {
|
||||
values, directives := NoComplete(nil, nil, "")
|
||||
assert.Check(t, is.Equal(directives, cobra.ShellCompDirectiveNoFileComp))
|
||||
assert.Check(t, is.Len(values, 0))
|
||||
}
|
||||
|
||||
func TestCompletePlatforms(t *testing.T) {
|
||||
values, directives := Platforms(nil, nil, "")
|
||||
values, directives := Platforms()(nil, nil, "")
|
||||
assert.Check(t, is.Equal(directives&cobra.ShellCompDirectiveNoFileComp, cobra.ShellCompDirectiveNoFileComp), "Should not perform file completion")
|
||||
assert.Check(t, is.DeepEqual(values, commonPlatforms))
|
||||
}
|
||||
@ -303,7 +310,7 @@ func TestCompletePlatforms(t *testing.T) {
|
||||
func TestCompleteVolumeNames(t *testing.T) {
|
||||
tests := []struct {
|
||||
doc string
|
||||
volumes []*volume.Volume
|
||||
volumes []volume.Volume
|
||||
expOut []string
|
||||
expDirective cobra.ShellCompDirective
|
||||
}{
|
||||
@ -313,7 +320,7 @@ func TestCompleteVolumeNames(t *testing.T) {
|
||||
},
|
||||
{
|
||||
doc: "with results",
|
||||
volumes: []*volume.Volume{
|
||||
volumes: []volume.Volume{
|
||||
{Name: "volume-c"},
|
||||
{Name: "volume-b"},
|
||||
{Name: "volume-a"},
|
||||
@ -330,11 +337,11 @@ func TestCompleteVolumeNames(t *testing.T) {
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.doc, func(t *testing.T) {
|
||||
comp := VolumeNames(fakeCLI{&fakeClient{
|
||||
volumeListFunc: func(filter filters.Args) (volume.ListResponse, error) {
|
||||
volumeListFunc: func(context.Context, client.VolumeListOptions) (client.VolumeListResult, error) {
|
||||
if tc.expDirective == cobra.ShellCompDirectiveError {
|
||||
return volume.ListResponse{}, errors.New("some error occurred")
|
||||
return client.VolumeListResult{}, errors.New("some error occurred")
|
||||
}
|
||||
return volume.ListResponse{Volumes: tc.volumes}, nil
|
||||
return client.VolumeListResult{Items: tc.volumes}, nil
|
||||
},
|
||||
}})
|
||||
|
||||
|
||||
@ -3,42 +3,41 @@ package config
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/moby/moby/client"
|
||||
)
|
||||
|
||||
type fakeClient struct {
|
||||
client.Client
|
||||
configCreateFunc func(context.Context, swarm.ConfigSpec) (swarm.ConfigCreateResponse, error)
|
||||
configInspectFunc func(context.Context, string) (swarm.Config, []byte, error)
|
||||
configListFunc func(context.Context, swarm.ConfigListOptions) ([]swarm.Config, error)
|
||||
configRemoveFunc func(string) error
|
||||
configCreateFunc func(context.Context, client.ConfigCreateOptions) (client.ConfigCreateResult, error)
|
||||
configInspectFunc func(context.Context, string, client.ConfigInspectOptions) (client.ConfigInspectResult, error)
|
||||
configListFunc func(context.Context, client.ConfigListOptions) (client.ConfigListResult, error)
|
||||
configRemoveFunc func(context.Context, string, client.ConfigRemoveOptions) (client.ConfigRemoveResult, error)
|
||||
}
|
||||
|
||||
func (c *fakeClient) ConfigCreate(ctx context.Context, spec swarm.ConfigSpec) (swarm.ConfigCreateResponse, error) {
|
||||
func (c *fakeClient) ConfigCreate(ctx context.Context, options client.ConfigCreateOptions) (client.ConfigCreateResult, error) {
|
||||
if c.configCreateFunc != nil {
|
||||
return c.configCreateFunc(ctx, spec)
|
||||
return c.configCreateFunc(ctx, options)
|
||||
}
|
||||
return swarm.ConfigCreateResponse{}, nil
|
||||
return client.ConfigCreateResult{}, nil
|
||||
}
|
||||
|
||||
func (c *fakeClient) ConfigInspectWithRaw(ctx context.Context, id string) (swarm.Config, []byte, error) {
|
||||
func (c *fakeClient) ConfigInspect(ctx context.Context, id string, options client.ConfigInspectOptions) (client.ConfigInspectResult, error) {
|
||||
if c.configInspectFunc != nil {
|
||||
return c.configInspectFunc(ctx, id)
|
||||
return c.configInspectFunc(ctx, id, options)
|
||||
}
|
||||
return swarm.Config{}, nil, nil
|
||||
return client.ConfigInspectResult{}, nil
|
||||
}
|
||||
|
||||
func (c *fakeClient) ConfigList(ctx context.Context, options swarm.ConfigListOptions) ([]swarm.Config, error) {
|
||||
func (c *fakeClient) ConfigList(ctx context.Context, options client.ConfigListOptions) (client.ConfigListResult, error) {
|
||||
if c.configListFunc != nil {
|
||||
return c.configListFunc(ctx, options)
|
||||
}
|
||||
return []swarm.Config{}, nil
|
||||
return client.ConfigListResult{}, nil
|
||||
}
|
||||
|
||||
func (c *fakeClient) ConfigRemove(_ context.Context, name string) error {
|
||||
func (c *fakeClient) ConfigRemove(ctx context.Context, name string, options client.ConfigRemoveOptions) (client.ConfigRemoveResult, error) {
|
||||
if c.configRemoveFunc != nil {
|
||||
return c.configRemoveFunc(name)
|
||||
return c.configRemoveFunc(ctx, name, options)
|
||||
}
|
||||
return nil
|
||||
return client.ConfigRemoveResult{}, nil
|
||||
}
|
||||
|
||||
@ -4,27 +4,33 @@ import (
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/cli/internal/commands"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// NewConfigCommand returns a cobra command for `config` subcommands
|
||||
func NewConfigCommand(dockerCli command.Cli) *cobra.Command {
|
||||
func init() {
|
||||
commands.Register(newConfigCommand)
|
||||
}
|
||||
|
||||
// newConfigCommand returns a cobra command for `config` subcommands
|
||||
func newConfigCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "config",
|
||||
Short: "Manage Swarm configs",
|
||||
Args: cli.NoArgs,
|
||||
RunE: command.ShowHelp(dockerCli.Err()),
|
||||
RunE: command.ShowHelp(dockerCLI.Err()),
|
||||
Annotations: map[string]string{
|
||||
"version": "1.30",
|
||||
"swarm": "manager",
|
||||
},
|
||||
DisableFlagsInUseLine: true,
|
||||
}
|
||||
cmd.AddCommand(
|
||||
newConfigListCommand(dockerCli),
|
||||
newConfigCreateCommand(dockerCli),
|
||||
newConfigInspectCommand(dockerCli),
|
||||
newConfigRemoveCommand(dockerCli),
|
||||
newConfigListCommand(dockerCLI),
|
||||
newConfigCreateCommand(dockerCLI),
|
||||
newConfigInspectCommand(dockerCLI),
|
||||
newConfigRemoveCommand(dockerCLI),
|
||||
)
|
||||
return cmd
|
||||
}
|
||||
@ -32,12 +38,12 @@ func NewConfigCommand(dockerCli command.Cli) *cobra.Command {
|
||||
// completeNames offers completion for swarm configs
|
||||
func completeNames(dockerCLI completion.APIClientProvider) cobra.CompletionFunc {
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
list, err := dockerCLI.Client().ConfigList(cmd.Context(), swarm.ConfigListOptions{})
|
||||
res, err := dockerCLI.Client().ConfigList(cmd.Context(), client.ConfigListOptions{})
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveError
|
||||
}
|
||||
var names []string
|
||||
for _, config := range list {
|
||||
for _, config := range res.Items {
|
||||
names = append(names, config.ID)
|
||||
}
|
||||
return names, cobra.ShellCompDirectiveNoFileComp
|
||||
|
||||
@ -2,30 +2,30 @@ package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/moby/moby/api/types/swarm"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/moby/sys/sequential"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// CreateOptions specifies some options that are used when creating a config.
|
||||
type CreateOptions struct {
|
||||
Name string
|
||||
TemplateDriver string
|
||||
File string
|
||||
Labels opts.ListOpts
|
||||
// createOptions specifies some options that are used when creating a config.
|
||||
type createOptions struct {
|
||||
name string
|
||||
templateDriver string
|
||||
file string
|
||||
labels opts.ListOpts
|
||||
}
|
||||
|
||||
func newConfigCreateCommand(dockerCli command.Cli) *cobra.Command {
|
||||
createOpts := CreateOptions{
|
||||
Labels: opts.NewListOpts(opts.ValidateLabel),
|
||||
func newConfigCreateCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
createOpts := createOptions{
|
||||
labels: opts.NewListOpts(opts.ValidateLabel),
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
@ -33,42 +33,61 @@ func newConfigCreateCommand(dockerCli command.Cli) *cobra.Command {
|
||||
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]
|
||||
createOpts.File = args[1]
|
||||
return RunConfigCreate(cmd.Context(), dockerCli, createOpts)
|
||||
createOpts.name = args[0]
|
||||
createOpts.file = args[1]
|
||||
return runCreate(cmd.Context(), dockerCLI, createOpts)
|
||||
},
|
||||
ValidArgsFunction: completion.NoComplete,
|
||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
switch len(args) {
|
||||
case 0:
|
||||
// No completion for the first argument, which is the name for
|
||||
// the new config, but if a non-empty name is given, we return
|
||||
// it as completion to allow "tab"-ing to the next completion.
|
||||
return []string{toComplete}, cobra.ShellCompDirectiveNoFileComp
|
||||
case 1:
|
||||
// Second argument is either "-" or a file to load.
|
||||
//
|
||||
// TODO(thaJeztah): provide completion for "-".
|
||||
return nil, cobra.ShellCompDirectiveNoSpace | cobra.ShellCompDirectiveDefault
|
||||
default:
|
||||
// Command only accepts two arguments.
|
||||
return nil, cobra.ShellCompDirectiveNoSpace | cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
},
|
||||
DisableFlagsInUseLine: true,
|
||||
}
|
||||
flags := cmd.Flags()
|
||||
flags.VarP(&createOpts.Labels, "label", "l", "Config labels")
|
||||
flags.StringVar(&createOpts.TemplateDriver, "template-driver", "", "Template driver")
|
||||
flags.SetAnnotation("template-driver", "version", []string{"1.37"})
|
||||
flags.VarP(&createOpts.labels, "label", "l", "Config labels")
|
||||
flags.StringVar(&createOpts.templateDriver, "template-driver", "", "Template driver")
|
||||
_ = flags.SetAnnotation("template-driver", "version", []string{"1.37"})
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// RunConfigCreate creates a config with the given options.
|
||||
func RunConfigCreate(ctx context.Context, dockerCLI command.Cli, options CreateOptions) error {
|
||||
// runCreate creates a config with the given options.
|
||||
func runCreate(ctx context.Context, dockerCLI command.Cli, options createOptions) error {
|
||||
apiClient := dockerCLI.Client()
|
||||
|
||||
configData, err := readConfigData(dockerCLI.In(), options.File)
|
||||
configData, err := readConfigData(dockerCLI.In(), options.file)
|
||||
if err != nil {
|
||||
return errors.Errorf("Error reading content from %q: %v", options.File, err)
|
||||
return fmt.Errorf("error reading content from %q: %v", options.file, err)
|
||||
}
|
||||
|
||||
spec := swarm.ConfigSpec{
|
||||
Annotations: swarm.Annotations{
|
||||
Name: options.Name,
|
||||
Labels: opts.ConvertKVStringsToMap(options.Labels.GetSlice()),
|
||||
Name: options.name,
|
||||
Labels: opts.ConvertKVStringsToMap(options.labels.GetSlice()),
|
||||
},
|
||||
Data: configData,
|
||||
}
|
||||
if options.TemplateDriver != "" {
|
||||
if options.templateDriver != "" {
|
||||
spec.Templating = &swarm.Driver{
|
||||
Name: options.TemplateDriver,
|
||||
Name: options.templateDriver,
|
||||
}
|
||||
}
|
||||
r, err := apiClient.ConfigCreate(ctx, spec)
|
||||
r, err := apiClient.ConfigCreate(ctx, client.ConfigCreateOptions{
|
||||
Spec: spec,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -12,7 +12,8 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/moby/moby/api/types/swarm"
|
||||
"github.com/moby/moby/client"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
"gotest.tools/v3/golden"
|
||||
@ -23,7 +24,7 @@ const configDataFile = "config-create-with-name.golden"
|
||||
func TestConfigCreateErrors(t *testing.T) {
|
||||
testCases := []struct {
|
||||
args []string
|
||||
configCreateFunc func(context.Context, swarm.ConfigSpec) (swarm.ConfigCreateResponse, error)
|
||||
configCreateFunc func(context.Context, client.ConfigCreateOptions) (client.ConfigCreateResult, error)
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
@ -36,8 +37,8 @@ func TestConfigCreateErrors(t *testing.T) {
|
||||
},
|
||||
{
|
||||
args: []string{"name", filepath.Join("testdata", configDataFile)},
|
||||
configCreateFunc: func(_ context.Context, configSpec swarm.ConfigSpec) (swarm.ConfigCreateResponse, error) {
|
||||
return swarm.ConfigCreateResponse{}, errors.New("error creating config")
|
||||
configCreateFunc: func(_ context.Context, options client.ConfigCreateOptions) (client.ConfigCreateResult, error) {
|
||||
return client.ConfigCreateResult{}, errors.New("error creating config")
|
||||
},
|
||||
expectedError: "error creating config",
|
||||
},
|
||||
@ -61,15 +62,15 @@ func TestConfigCreateWithName(t *testing.T) {
|
||||
const name = "config-with-name"
|
||||
var actual []byte
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
configCreateFunc: func(_ context.Context, spec swarm.ConfigSpec) (swarm.ConfigCreateResponse, error) {
|
||||
if spec.Name != name {
|
||||
return swarm.ConfigCreateResponse{}, fmt.Errorf("expected name %q, got %q", name, spec.Name)
|
||||
configCreateFunc: func(_ context.Context, options client.ConfigCreateOptions) (client.ConfigCreateResult, error) {
|
||||
if options.Spec.Name != name {
|
||||
return client.ConfigCreateResult{}, fmt.Errorf("expected name %q, got %q", name, options.Spec.Name)
|
||||
}
|
||||
|
||||
actual = spec.Data
|
||||
actual = options.Spec.Data
|
||||
|
||||
return swarm.ConfigCreateResponse{
|
||||
ID: "ID-" + spec.Name,
|
||||
return client.ConfigCreateResult{
|
||||
ID: "ID-" + options.Spec.Name,
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
@ -100,13 +101,13 @@ func TestConfigCreateWithLabels(t *testing.T) {
|
||||
}
|
||||
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
configCreateFunc: func(_ context.Context, spec swarm.ConfigSpec) (swarm.ConfigCreateResponse, error) {
|
||||
if !reflect.DeepEqual(spec, expected) {
|
||||
return swarm.ConfigCreateResponse{}, fmt.Errorf("expected %+v, got %+v", expected, spec)
|
||||
configCreateFunc: func(_ context.Context, options client.ConfigCreateOptions) (client.ConfigCreateResult, error) {
|
||||
if !reflect.DeepEqual(options.Spec, expected) {
|
||||
return client.ConfigCreateResult{}, fmt.Errorf("expected %+v, got %+v", expected, options.Spec)
|
||||
}
|
||||
|
||||
return swarm.ConfigCreateResponse{
|
||||
ID: "ID-" + spec.Name,
|
||||
return client.ConfigCreateResult{
|
||||
ID: "ID-" + options.Spec.Name,
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
@ -126,17 +127,17 @@ func TestConfigCreateWithTemplatingDriver(t *testing.T) {
|
||||
const name = "config-with-template-driver"
|
||||
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
configCreateFunc: func(_ context.Context, spec swarm.ConfigSpec) (swarm.ConfigCreateResponse, error) {
|
||||
if spec.Name != name {
|
||||
return swarm.ConfigCreateResponse{}, fmt.Errorf("expected name %q, got %q", name, spec.Name)
|
||||
configCreateFunc: func(_ context.Context, options client.ConfigCreateOptions) (client.ConfigCreateResult, error) {
|
||||
if options.Spec.Name != name {
|
||||
return client.ConfigCreateResult{}, fmt.Errorf("expected name %q, got %q", name, options.Spec.Name)
|
||||
}
|
||||
|
||||
if spec.Templating.Name != expectedDriver.Name {
|
||||
return swarm.ConfigCreateResponse{}, fmt.Errorf("expected driver %v, got %v", expectedDriver, spec.Labels)
|
||||
if options.Spec.Templating.Name != expectedDriver.Name {
|
||||
return client.ConfigCreateResult{}, fmt.Errorf("expected driver %v, got %v", expectedDriver, options.Spec.Labels)
|
||||
}
|
||||
|
||||
return swarm.ConfigCreateResponse{
|
||||
ID: "ID-" + spec.Name,
|
||||
return client.ConfigCreateResult{
|
||||
ID: "ID-" + options.Spec.Name,
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
|
||||
@ -7,8 +7,9 @@ import (
|
||||
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/docker/cli/cli/command/inspect"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/go-units"
|
||||
"github.com/moby/moby/api/types/swarm"
|
||||
"github.com/moby/moby/client"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -29,8 +30,8 @@ Data:
|
||||
{{.Data}}`
|
||||
)
|
||||
|
||||
// NewFormat returns a Format for rendering using a config Context
|
||||
func NewFormat(source string, quiet bool) formatter.Format {
|
||||
// newFormat returns a Format for rendering using a configContext.
|
||||
func newFormat(source string, quiet bool) formatter.Format {
|
||||
switch source {
|
||||
case formatter.PrettyFormatKey:
|
||||
return configInspectPrettyTemplate
|
||||
@ -43,31 +44,28 @@ func NewFormat(source string, quiet bool) formatter.Format {
|
||||
return formatter.Format(source)
|
||||
}
|
||||
|
||||
// FormatWrite writes the context
|
||||
func FormatWrite(ctx formatter.Context, configs []swarm.Config) error {
|
||||
render := func(format func(subContext formatter.SubContext) error) error {
|
||||
for _, config := range configs {
|
||||
// formatWrite writes the context
|
||||
func formatWrite(fmtCtx formatter.Context, configs client.ConfigListResult) error {
|
||||
cCtx := &configContext{
|
||||
HeaderContext: formatter.HeaderContext{
|
||||
Header: formatter.SubHeaderContext{
|
||||
"ID": configIDHeader,
|
||||
"Name": formatter.NameHeader,
|
||||
"CreatedAt": configCreatedHeader,
|
||||
"UpdatedAt": configUpdatedHeader,
|
||||
"Labels": formatter.LabelsHeader,
|
||||
},
|
||||
},
|
||||
}
|
||||
return fmtCtx.Write(cCtx, func(format func(subContext formatter.SubContext) error) error {
|
||||
for _, config := range configs.Items {
|
||||
configCtx := &configContext{c: config}
|
||||
if err := format(configCtx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return ctx.Write(newConfigContext(), render)
|
||||
}
|
||||
|
||||
func newConfigContext() *configContext {
|
||||
cCtx := &configContext{}
|
||||
|
||||
cCtx.Header = formatter.SubHeaderContext{
|
||||
"ID": configIDHeader,
|
||||
"Name": formatter.NameHeader,
|
||||
"CreatedAt": configCreatedHeader,
|
||||
"UpdatedAt": configUpdatedHeader,
|
||||
"Labels": formatter.LabelsHeader,
|
||||
}
|
||||
return cCtx
|
||||
})
|
||||
}
|
||||
|
||||
type configContext struct {
|
||||
@ -114,12 +112,12 @@ func (c *configContext) Label(name string) string {
|
||||
return c.c.Spec.Annotations.Labels[name]
|
||||
}
|
||||
|
||||
// InspectFormatWrite renders the context for a list of configs
|
||||
func InspectFormatWrite(ctx formatter.Context, refs []string, getRef inspect.GetRefFunc) error {
|
||||
if ctx.Format != configInspectPrettyTemplate {
|
||||
return inspect.Inspect(ctx.Output, refs, string(ctx.Format), getRef)
|
||||
// inspectFormatWrite renders the context for a list of configs
|
||||
func inspectFormatWrite(fmtCtx formatter.Context, refs []string, getRef inspect.GetRefFunc) error {
|
||||
if fmtCtx.Format != configInspectPrettyTemplate {
|
||||
return inspect.Inspect(fmtCtx.Output, refs, string(fmtCtx.Format), getRef)
|
||||
}
|
||||
render := func(format func(subContext formatter.SubContext) error) error {
|
||||
return fmtCtx.Write(&configInspectContext{}, func(format func(subContext formatter.SubContext) error) error {
|
||||
for _, ref := range refs {
|
||||
configI, _, err := getRef(ref)
|
||||
if err != nil {
|
||||
@ -134,8 +132,7 @@ func InspectFormatWrite(ctx formatter.Context, refs []string, getRef inspect.Get
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return ctx.Write(&configInspectContext{}, render)
|
||||
})
|
||||
}
|
||||
|
||||
type configInspectContext struct {
|
||||
|
||||
@ -6,7 +6,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/moby/moby/api/types/swarm"
|
||||
"github.com/moby/moby/client"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
@ -27,44 +28,46 @@ func TestConfigContextFormatWrite(t *testing.T) {
|
||||
},
|
||||
// Table format
|
||||
{
|
||||
formatter.Context{Format: NewFormat("table", false)},
|
||||
formatter.Context{Format: newFormat("table", false)},
|
||||
`ID NAME CREATED UPDATED
|
||||
1 passwords Less than a second ago Less than a second ago
|
||||
2 id_rsa Less than a second ago Less than a second ago
|
||||
`,
|
||||
},
|
||||
{
|
||||
formatter.Context{Format: NewFormat("table {{.Name}}", true)},
|
||||
formatter.Context{Format: newFormat("table {{.Name}}", true)},
|
||||
`NAME
|
||||
passwords
|
||||
id_rsa
|
||||
`,
|
||||
},
|
||||
{
|
||||
formatter.Context{Format: NewFormat("{{.ID}}-{{.Name}}", false)},
|
||||
formatter.Context{Format: newFormat("{{.ID}}-{{.Name}}", false)},
|
||||
`1-passwords
|
||||
2-id_rsa
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
configs := []swarm.Config{
|
||||
{
|
||||
ID: "1",
|
||||
Meta: swarm.Meta{CreatedAt: time.Now(), UpdatedAt: time.Now()},
|
||||
Spec: swarm.ConfigSpec{Annotations: swarm.Annotations{Name: "passwords"}},
|
||||
},
|
||||
{
|
||||
ID: "2",
|
||||
Meta: swarm.Meta{CreatedAt: time.Now(), UpdatedAt: time.Now()},
|
||||
Spec: swarm.ConfigSpec{Annotations: swarm.Annotations{Name: "id_rsa"}},
|
||||
res := client.ConfigListResult{
|
||||
Items: []swarm.Config{
|
||||
{
|
||||
ID: "1",
|
||||
Meta: swarm.Meta{CreatedAt: time.Now(), UpdatedAt: time.Now()},
|
||||
Spec: swarm.ConfigSpec{Annotations: swarm.Annotations{Name: "passwords"}},
|
||||
},
|
||||
{
|
||||
ID: "2",
|
||||
Meta: swarm.Meta{CreatedAt: time.Now(), UpdatedAt: time.Now()},
|
||||
Spec: swarm.ConfigSpec{Annotations: swarm.Annotations{Name: "id_rsa"}},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(string(tc.context.Format), func(t *testing.T) {
|
||||
var out bytes.Buffer
|
||||
tc.context.Output = &out
|
||||
if err := FormatWrite(tc.context, configs); err != nil {
|
||||
if err := formatWrite(tc.context, res); err != nil {
|
||||
assert.ErrorContains(t, err, tc.expected)
|
||||
} else {
|
||||
assert.Equal(t, out.String(), tc.expected)
|
||||
|
||||
@ -12,61 +12,61 @@ import (
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
flagsHelper "github.com/docker/cli/cli/flags"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// InspectOptions contains options for the docker config inspect command.
|
||||
type InspectOptions struct {
|
||||
Names []string
|
||||
Format string
|
||||
Pretty bool
|
||||
// inspectOptions contains options for the docker config inspect command.
|
||||
type inspectOptions struct {
|
||||
names []string
|
||||
format string
|
||||
pretty bool
|
||||
}
|
||||
|
||||
func newConfigInspectCommand(dockerCli command.Cli) *cobra.Command {
|
||||
opts := InspectOptions{}
|
||||
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 configs",
|
||||
Args: cli.RequiresMinArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.Names = args
|
||||
return RunConfigInspect(cmd.Context(), dockerCli, opts)
|
||||
},
|
||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
return completeNames(dockerCli)(cmd, args, toComplete)
|
||||
opts.names = args
|
||||
return runInspect(cmd.Context(), dockerCLI, opts)
|
||||
},
|
||||
ValidArgsFunction: completeNames(dockerCLI),
|
||||
DisableFlagsInUseLine: true,
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(&opts.Format, "format", "f", "", flagsHelper.InspectFormatHelp)
|
||||
cmd.Flags().BoolVar(&opts.Pretty, "pretty", false, "Print the information in a human friendly format")
|
||||
cmd.Flags().StringVarP(&opts.format, "format", "f", "", flagsHelper.InspectFormatHelp)
|
||||
cmd.Flags().BoolVar(&opts.pretty, "pretty", false, "Print the information in a human friendly format")
|
||||
return cmd
|
||||
}
|
||||
|
||||
// RunConfigInspect inspects the given Swarm config.
|
||||
func RunConfigInspect(ctx context.Context, dockerCLI command.Cli, opts InspectOptions) error {
|
||||
// runInspect inspects the given Swarm config.
|
||||
func runInspect(ctx context.Context, dockerCLI command.Cli, opts inspectOptions) error {
|
||||
apiClient := dockerCLI.Client()
|
||||
|
||||
if opts.Pretty {
|
||||
opts.Format = "pretty"
|
||||
if opts.pretty {
|
||||
opts.format = "pretty"
|
||||
}
|
||||
|
||||
getRef := func(id string) (any, []byte, error) {
|
||||
return apiClient.ConfigInspectWithRaw(ctx, id)
|
||||
res, err := apiClient.ConfigInspect(ctx, id, client.ConfigInspectOptions{})
|
||||
return res.Config, res.Raw, err
|
||||
}
|
||||
f := opts.Format
|
||||
|
||||
// check if the user is trying to apply a template to the pretty format, which
|
||||
// is not supported
|
||||
if strings.HasPrefix(f, "pretty") && f != "pretty" {
|
||||
if strings.HasPrefix(opts.format, "pretty") && opts.format != "pretty" {
|
||||
return errors.New("cannot supply extra formatting options to the pretty template")
|
||||
}
|
||||
|
||||
configCtx := formatter.Context{
|
||||
Output: dockerCLI.Out(),
|
||||
Format: NewFormat(f, false),
|
||||
Format: newFormat(opts.format, false),
|
||||
}
|
||||
|
||||
if err := InspectFormatWrite(configCtx, opts.Names, getRef); err != nil {
|
||||
if err := inspectFormatWrite(configCtx, opts.names, getRef); err != nil {
|
||||
return cli.StatusError{StatusCode: 1, Status: err.Error()}
|
||||
}
|
||||
return nil
|
||||
|
||||
@ -10,7 +10,7 @@ import (
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/cli/internal/test/builders"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/moby/moby/client"
|
||||
"gotest.tools/v3/assert"
|
||||
"gotest.tools/v3/golden"
|
||||
)
|
||||
@ -19,7 +19,7 @@ func TestConfigInspectErrors(t *testing.T) {
|
||||
testCases := []struct {
|
||||
args []string
|
||||
flags map[string]string
|
||||
configInspectFunc func(_ context.Context, configID string) (swarm.Config, []byte, error)
|
||||
configInspectFunc func(_ context.Context, configID string, _ client.ConfigInspectOptions) (client.ConfigInspectResult, error)
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
@ -27,8 +27,8 @@ func TestConfigInspectErrors(t *testing.T) {
|
||||
},
|
||||
{
|
||||
args: []string{"foo"},
|
||||
configInspectFunc: func(_ context.Context, configID string) (swarm.Config, []byte, error) {
|
||||
return swarm.Config{}, nil, errors.New("error while inspecting the config")
|
||||
configInspectFunc: func(context.Context, string, client.ConfigInspectOptions) (client.ConfigInspectResult, error) {
|
||||
return client.ConfigInspectResult{}, errors.New("error while inspecting the config")
|
||||
},
|
||||
expectedError: "error while inspecting the config",
|
||||
},
|
||||
@ -41,11 +41,13 @@ func TestConfigInspectErrors(t *testing.T) {
|
||||
},
|
||||
{
|
||||
args: []string{"foo", "bar"},
|
||||
configInspectFunc: func(_ context.Context, configID string) (swarm.Config, []byte, error) {
|
||||
configInspectFunc: func(_ context.Context, configID string, _ client.ConfigInspectOptions) (client.ConfigInspectResult, error) {
|
||||
if configID == "foo" {
|
||||
return *builders.Config(builders.ConfigName("foo")), nil, nil
|
||||
return client.ConfigInspectResult{
|
||||
Config: *builders.Config(builders.ConfigName("foo")),
|
||||
}, nil
|
||||
}
|
||||
return swarm.Config{}, nil, errors.New("error while inspecting the config")
|
||||
return client.ConfigInspectResult{}, errors.New("error while inspecting the config")
|
||||
},
|
||||
expectedError: "error while inspecting the config",
|
||||
},
|
||||
@ -70,25 +72,34 @@ func TestConfigInspectWithoutFormat(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
args []string
|
||||
configInspectFunc func(_ context.Context, configID string) (swarm.Config, []byte, error)
|
||||
configInspectFunc func(_ context.Context, configID string, _ client.ConfigInspectOptions) (client.ConfigInspectResult, error)
|
||||
}{
|
||||
{
|
||||
name: "single-config",
|
||||
args: []string{"foo"},
|
||||
configInspectFunc: func(_ context.Context, name string) (swarm.Config, []byte, error) {
|
||||
configInspectFunc: func(_ context.Context, name string, _ client.ConfigInspectOptions) (client.ConfigInspectResult, error) {
|
||||
if name != "foo" {
|
||||
return swarm.Config{}, nil, fmt.Errorf("invalid name, expected %s, got %s", "foo", name)
|
||||
return client.ConfigInspectResult{}, fmt.Errorf("invalid name, expected %s, got %s", "foo", name)
|
||||
}
|
||||
return *builders.Config(builders.ConfigID("ID-foo"), builders.ConfigName("foo")), nil, nil
|
||||
return client.ConfigInspectResult{
|
||||
Config: *builders.Config(
|
||||
builders.ConfigID("ID-foo"),
|
||||
builders.ConfigName("foo"),
|
||||
),
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple-configs-with-labels",
|
||||
args: []string{"foo", "bar"},
|
||||
configInspectFunc: func(_ context.Context, name string) (swarm.Config, []byte, error) {
|
||||
return *builders.Config(builders.ConfigID("ID-"+name), builders.ConfigName(name), builders.ConfigLabels(map[string]string{
|
||||
"label1": "label-foo",
|
||||
})), nil, nil
|
||||
configInspectFunc: func(_ context.Context, name string, _ client.ConfigInspectOptions) (client.ConfigInspectResult, error) {
|
||||
return client.ConfigInspectResult{
|
||||
Config: *builders.Config(
|
||||
builders.ConfigID("ID-"+name),
|
||||
builders.ConfigName(name),
|
||||
builders.ConfigLabels(map[string]string{"label1": "label-foo"}),
|
||||
),
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -102,16 +113,19 @@ func TestConfigInspectWithoutFormat(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestConfigInspectWithFormat(t *testing.T) {
|
||||
configInspectFunc := func(_ context.Context, name string) (swarm.Config, []byte, error) {
|
||||
return *builders.Config(builders.ConfigName("foo"), builders.ConfigLabels(map[string]string{
|
||||
"label1": "label-foo",
|
||||
})), nil, nil
|
||||
configInspectFunc := func(_ context.Context, name string, _ client.ConfigInspectOptions) (client.ConfigInspectResult, error) {
|
||||
return client.ConfigInspectResult{
|
||||
Config: *builders.Config(
|
||||
builders.ConfigName("foo"),
|
||||
builders.ConfigLabels(map[string]string{"label1": "label-foo"}),
|
||||
),
|
||||
}, nil
|
||||
}
|
||||
testCases := []struct {
|
||||
name string
|
||||
format string
|
||||
args []string
|
||||
configInspectFunc func(_ context.Context, name string) (swarm.Config, []byte, error)
|
||||
configInspectFunc func(_ context.Context, name string, _ client.ConfigInspectOptions) (client.ConfigInspectResult, error)
|
||||
}{
|
||||
{
|
||||
name: "simple-template",
|
||||
@ -141,21 +155,23 @@ func TestConfigInspectWithFormat(t *testing.T) {
|
||||
func TestConfigInspectPretty(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
configInspectFunc func(context.Context, string) (swarm.Config, []byte, error)
|
||||
configInspectFunc func(context.Context, string, client.ConfigInspectOptions) (client.ConfigInspectResult, error)
|
||||
}{
|
||||
{
|
||||
name: "simple",
|
||||
configInspectFunc: func(_ context.Context, id string) (swarm.Config, []byte, error) {
|
||||
return *builders.Config(
|
||||
builders.ConfigLabels(map[string]string{
|
||||
"lbl1": "value1",
|
||||
}),
|
||||
builders.ConfigID("configID"),
|
||||
builders.ConfigName("configName"),
|
||||
builders.ConfigCreatedAt(time.Time{}),
|
||||
builders.ConfigUpdatedAt(time.Time{}),
|
||||
builders.ConfigData([]byte("payload here")),
|
||||
), []byte{}, nil
|
||||
configInspectFunc: func(_ context.Context, id string, _ client.ConfigInspectOptions) (client.ConfigInspectResult, error) {
|
||||
return client.ConfigInspectResult{
|
||||
Config: *builders.Config(
|
||||
builders.ConfigLabels(map[string]string{
|
||||
"lbl1": "value1",
|
||||
}),
|
||||
builders.ConfigID("configID"),
|
||||
builders.ConfigName("configName"),
|
||||
builders.ConfigCreatedAt(time.Time{}),
|
||||
builders.ConfigUpdatedAt(time.Time{}),
|
||||
builders.ConfigData([]byte("payload here")),
|
||||
),
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -6,24 +6,23 @@ import (
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
flagsHelper "github.com/docker/cli/cli/flags"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/fvbommel/sortorder"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// ListOptions contains options for the docker config ls command.
|
||||
type ListOptions struct {
|
||||
Quiet bool
|
||||
Format string
|
||||
Filter opts.FilterOpt
|
||||
// listOptions contains options for the docker config ls command.
|
||||
type listOptions struct {
|
||||
quiet bool
|
||||
format string
|
||||
filter opts.FilterOpt
|
||||
}
|
||||
|
||||
func newConfigListCommand(dockerCli command.Cli) *cobra.Command {
|
||||
listOpts := ListOptions{Filter: opts.NewFilterOpt()}
|
||||
func newConfigListCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
listOpts := listOptions{filter: opts.NewFilterOpt()}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "ls [OPTIONS]",
|
||||
@ -31,44 +30,45 @@ func newConfigListCommand(dockerCli command.Cli) *cobra.Command {
|
||||
Short: "List configs",
|
||||
Args: cli.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return RunConfigList(cmd.Context(), dockerCli, listOpts)
|
||||
return runList(cmd.Context(), dockerCLI, listOpts)
|
||||
},
|
||||
ValidArgsFunction: completion.NoComplete,
|
||||
ValidArgsFunction: cobra.NoFileCompletions,
|
||||
DisableFlagsInUseLine: true,
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVarP(&listOpts.Quiet, "quiet", "q", false, "Only display IDs")
|
||||
flags.StringVar(&listOpts.Format, "format", "", flagsHelper.FormatHelp)
|
||||
flags.VarP(&listOpts.Filter, "filter", "f", "Filter output based on conditions provided")
|
||||
flags.BoolVarP(&listOpts.quiet, "quiet", "q", false, "Only display IDs")
|
||||
flags.StringVar(&listOpts.format, "format", "", flagsHelper.FormatHelp)
|
||||
flags.VarP(&listOpts.filter, "filter", "f", "Filter output based on conditions provided")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// RunConfigList lists Swarm configs.
|
||||
func RunConfigList(ctx context.Context, dockerCLI command.Cli, options ListOptions) error {
|
||||
// runList lists Swarm configs.
|
||||
func runList(ctx context.Context, dockerCLI command.Cli, options listOptions) error {
|
||||
apiClient := dockerCLI.Client()
|
||||
|
||||
configs, err := apiClient.ConfigList(ctx, swarm.ConfigListOptions{Filters: options.Filter.Value()})
|
||||
res, err := apiClient.ConfigList(ctx, client.ConfigListOptions{Filters: options.filter.Value()})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
format := options.Format
|
||||
format := options.format
|
||||
if len(format) == 0 {
|
||||
if len(dockerCLI.ConfigFile().ConfigFormat) > 0 && !options.Quiet {
|
||||
if len(dockerCLI.ConfigFile().ConfigFormat) > 0 && !options.quiet {
|
||||
format = dockerCLI.ConfigFile().ConfigFormat
|
||||
} else {
|
||||
format = formatter.TableFormatKey
|
||||
}
|
||||
}
|
||||
|
||||
sort.Slice(configs, func(i, j int) bool {
|
||||
return sortorder.NaturalLess(configs[i].Spec.Name, configs[j].Spec.Name)
|
||||
sort.Slice(res.Items, func(i, j int) bool {
|
||||
return sortorder.NaturalLess(res.Items[i].Spec.Name, res.Items[j].Spec.Name)
|
||||
})
|
||||
|
||||
configCtx := formatter.Context{
|
||||
Output: dockerCLI.Out(),
|
||||
Format: NewFormat(format, options.Quiet),
|
||||
Format: newFormat(format, options.quiet),
|
||||
}
|
||||
return FormatWrite(configCtx, configs)
|
||||
return formatWrite(configCtx, res)
|
||||
}
|
||||
|
||||
@ -10,16 +10,16 @@ import (
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/cli/internal/test/builders"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/moby/moby/api/types/swarm"
|
||||
"github.com/moby/moby/client"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
"gotest.tools/v3/golden"
|
||||
)
|
||||
|
||||
func TestConfigListErrors(t *testing.T) {
|
||||
testCases := []struct {
|
||||
args []string
|
||||
configListFunc func(context.Context, swarm.ConfigListOptions) ([]swarm.Config, error)
|
||||
configListFunc func(context.Context, client.ConfigListOptions) (client.ConfigListResult, error)
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
@ -27,8 +27,8 @@ func TestConfigListErrors(t *testing.T) {
|
||||
expectedError: "accepts no argument",
|
||||
},
|
||||
{
|
||||
configListFunc: func(_ context.Context, options swarm.ConfigListOptions) ([]swarm.Config, error) {
|
||||
return []swarm.Config{}, errors.New("error listing configs")
|
||||
configListFunc: func(_ context.Context, options client.ConfigListOptions) (client.ConfigListResult, error) {
|
||||
return client.ConfigListResult{}, errors.New("error listing configs")
|
||||
},
|
||||
expectedError: "error listing configs",
|
||||
},
|
||||
@ -48,26 +48,28 @@ func TestConfigListErrors(t *testing.T) {
|
||||
|
||||
func TestConfigList(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
configListFunc: func(_ context.Context, options swarm.ConfigListOptions) ([]swarm.Config, error) {
|
||||
return []swarm.Config{
|
||||
*builders.Config(builders.ConfigID("ID-1-foo"),
|
||||
builders.ConfigName("1-foo"),
|
||||
builders.ConfigVersion(swarm.Version{Index: 10}),
|
||||
builders.ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
|
||||
builders.ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
|
||||
),
|
||||
*builders.Config(builders.ConfigID("ID-10-foo"),
|
||||
builders.ConfigName("10-foo"),
|
||||
builders.ConfigVersion(swarm.Version{Index: 11}),
|
||||
builders.ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
|
||||
builders.ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
|
||||
),
|
||||
*builders.Config(builders.ConfigID("ID-2-foo"),
|
||||
builders.ConfigName("2-foo"),
|
||||
builders.ConfigVersion(swarm.Version{Index: 11}),
|
||||
builders.ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
|
||||
builders.ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
|
||||
),
|
||||
configListFunc: func(_ context.Context, options client.ConfigListOptions) (client.ConfigListResult, error) {
|
||||
return client.ConfigListResult{
|
||||
Items: []swarm.Config{
|
||||
*builders.Config(builders.ConfigID("ID-1-foo"),
|
||||
builders.ConfigName("1-foo"),
|
||||
builders.ConfigVersion(swarm.Version{Index: 10}),
|
||||
builders.ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
|
||||
builders.ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
|
||||
),
|
||||
*builders.Config(builders.ConfigID("ID-10-foo"),
|
||||
builders.ConfigName("10-foo"),
|
||||
builders.ConfigVersion(swarm.Version{Index: 11}),
|
||||
builders.ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
|
||||
builders.ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
|
||||
),
|
||||
*builders.Config(builders.ConfigID("ID-2-foo"),
|
||||
builders.ConfigName("2-foo"),
|
||||
builders.ConfigVersion(swarm.Version{Index: 11}),
|
||||
builders.ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
|
||||
builders.ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
|
||||
),
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
@ -78,12 +80,14 @@ func TestConfigList(t *testing.T) {
|
||||
|
||||
func TestConfigListWithQuietOption(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
configListFunc: func(_ context.Context, options swarm.ConfigListOptions) ([]swarm.Config, error) {
|
||||
return []swarm.Config{
|
||||
*builders.Config(builders.ConfigID("ID-foo"), builders.ConfigName("foo")),
|
||||
*builders.Config(builders.ConfigID("ID-bar"), builders.ConfigName("bar"), builders.ConfigLabels(map[string]string{
|
||||
"label": "label-bar",
|
||||
})),
|
||||
configListFunc: func(_ context.Context, options client.ConfigListOptions) (client.ConfigListResult, error) {
|
||||
return client.ConfigListResult{
|
||||
Items: []swarm.Config{
|
||||
*builders.Config(builders.ConfigID("ID-foo"), builders.ConfigName("foo")),
|
||||
*builders.Config(builders.ConfigID("ID-bar"), builders.ConfigName("bar"), builders.ConfigLabels(map[string]string{
|
||||
"label": "label-bar",
|
||||
})),
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
@ -95,12 +99,14 @@ func TestConfigListWithQuietOption(t *testing.T) {
|
||||
|
||||
func TestConfigListWithConfigFormat(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
configListFunc: func(_ context.Context, options swarm.ConfigListOptions) ([]swarm.Config, error) {
|
||||
return []swarm.Config{
|
||||
*builders.Config(builders.ConfigID("ID-foo"), builders.ConfigName("foo")),
|
||||
*builders.Config(builders.ConfigID("ID-bar"), builders.ConfigName("bar"), builders.ConfigLabels(map[string]string{
|
||||
"label": "label-bar",
|
||||
})),
|
||||
configListFunc: func(_ context.Context, options client.ConfigListOptions) (client.ConfigListResult, error) {
|
||||
return client.ConfigListResult{
|
||||
Items: []swarm.Config{
|
||||
*builders.Config(builders.ConfigID("ID-foo"), builders.ConfigName("foo")),
|
||||
*builders.Config(builders.ConfigID("ID-bar"), builders.ConfigName("bar"), builders.ConfigLabels(map[string]string{
|
||||
"label": "label-bar",
|
||||
})),
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
@ -114,12 +120,14 @@ func TestConfigListWithConfigFormat(t *testing.T) {
|
||||
|
||||
func TestConfigListWithFormat(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
configListFunc: func(_ context.Context, options swarm.ConfigListOptions) ([]swarm.Config, error) {
|
||||
return []swarm.Config{
|
||||
*builders.Config(builders.ConfigID("ID-foo"), builders.ConfigName("foo")),
|
||||
*builders.Config(builders.ConfigID("ID-bar"), builders.ConfigName("bar"), builders.ConfigLabels(map[string]string{
|
||||
"label": "label-bar",
|
||||
})),
|
||||
configListFunc: func(_ context.Context, options client.ConfigListOptions) (client.ConfigListResult, error) {
|
||||
return client.ConfigListResult{
|
||||
Items: []swarm.Config{
|
||||
*builders.Config(builders.ConfigID("ID-foo"), builders.ConfigName("foo")),
|
||||
*builders.Config(builders.ConfigID("ID-bar"), builders.ConfigName("bar"), builders.ConfigLabels(map[string]string{
|
||||
"label": "label-bar",
|
||||
})),
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
@ -131,22 +139,24 @@ func TestConfigListWithFormat(t *testing.T) {
|
||||
|
||||
func TestConfigListWithFilter(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
configListFunc: func(_ context.Context, options swarm.ConfigListOptions) ([]swarm.Config, error) {
|
||||
assert.Check(t, is.Equal("foo", options.Filters.Get("name")[0]))
|
||||
assert.Check(t, is.Equal("lbl1=Label-bar", options.Filters.Get("label")[0]))
|
||||
return []swarm.Config{
|
||||
*builders.Config(builders.ConfigID("ID-foo"),
|
||||
builders.ConfigName("foo"),
|
||||
builders.ConfigVersion(swarm.Version{Index: 10}),
|
||||
builders.ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
|
||||
builders.ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
|
||||
),
|
||||
*builders.Config(builders.ConfigID("ID-bar"),
|
||||
builders.ConfigName("bar"),
|
||||
builders.ConfigVersion(swarm.Version{Index: 11}),
|
||||
builders.ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
|
||||
builders.ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
|
||||
),
|
||||
configListFunc: func(_ context.Context, options client.ConfigListOptions) (client.ConfigListResult, error) {
|
||||
assert.Check(t, options.Filters["name"]["foo"])
|
||||
assert.Check(t, options.Filters["label"]["lbl1=Label-bar"])
|
||||
return client.ConfigListResult{
|
||||
Items: []swarm.Config{
|
||||
*builders.Config(builders.ConfigID("ID-foo"),
|
||||
builders.ConfigName("foo"),
|
||||
builders.ConfigVersion(swarm.Version{Index: 10}),
|
||||
builders.ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
|
||||
builders.ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
|
||||
),
|
||||
*builders.Config(builders.ConfigID("ID-bar"),
|
||||
builders.ConfigName("bar"),
|
||||
builders.ConfigVersion(swarm.Version{Index: 11}),
|
||||
builders.ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
|
||||
builders.ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
|
||||
),
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
|
||||
@ -7,39 +7,31 @@ import (
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// RemoveOptions contains options for the docker config rm command.
|
||||
type RemoveOptions struct {
|
||||
Names []string
|
||||
}
|
||||
|
||||
func newConfigRemoveCommand(dockerCli command.Cli) *cobra.Command {
|
||||
func newConfigRemoveCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "rm CONFIG [CONFIG...]",
|
||||
Aliases: []string{"remove"},
|
||||
Short: "Remove one or more configs",
|
||||
Args: cli.RequiresMinArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts := RemoveOptions{
|
||||
Names: args,
|
||||
}
|
||||
return RunConfigRemove(cmd.Context(), dockerCli, opts)
|
||||
},
|
||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
return completeNames(dockerCli)(cmd, args, toComplete)
|
||||
return runRemove(cmd.Context(), dockerCLI, args)
|
||||
},
|
||||
ValidArgsFunction: completeNames(dockerCLI),
|
||||
DisableFlagsInUseLine: true,
|
||||
}
|
||||
}
|
||||
|
||||
// RunConfigRemove removes the given Swarm configs.
|
||||
func RunConfigRemove(ctx context.Context, dockerCLI command.Cli, opts RemoveOptions) error {
|
||||
// runRemove removes the given Swarm configs.
|
||||
func runRemove(ctx context.Context, dockerCLI command.Cli, names []string) error {
|
||||
apiClient := dockerCLI.Client()
|
||||
|
||||
var errs []error
|
||||
for _, name := range opts.Names {
|
||||
if err := apiClient.ConfigRemove(ctx, name); err != nil {
|
||||
for _, name := range names {
|
||||
if _, err := apiClient.ConfigRemove(ctx, name, client.ConfigRemoveOptions{}); err != nil {
|
||||
errs = append(errs, err)
|
||||
continue
|
||||
}
|
||||
|
||||
@ -1,12 +1,14 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/moby/moby/client"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
@ -14,7 +16,7 @@ import (
|
||||
func TestConfigRemoveErrors(t *testing.T) {
|
||||
testCases := []struct {
|
||||
args []string
|
||||
configRemoveFunc func(string) error
|
||||
configRemoveFunc func(context.Context, string, client.ConfigRemoveOptions) (client.ConfigRemoveResult, error)
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
@ -23,8 +25,8 @@ func TestConfigRemoveErrors(t *testing.T) {
|
||||
},
|
||||
{
|
||||
args: []string{"foo"},
|
||||
configRemoveFunc: func(name string) error {
|
||||
return errors.New("error removing config")
|
||||
configRemoveFunc: func(ctx context.Context, name string, options client.ConfigRemoveOptions) (client.ConfigRemoveResult, error) {
|
||||
return client.ConfigRemoveResult{}, errors.New("error removing config")
|
||||
},
|
||||
expectedError: "error removing config",
|
||||
},
|
||||
@ -46,9 +48,9 @@ func TestConfigRemoveWithName(t *testing.T) {
|
||||
names := []string{"foo", "bar"}
|
||||
var removedConfigs []string
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
configRemoveFunc: func(name string) error {
|
||||
configRemoveFunc: func(_ context.Context, name string, _ client.ConfigRemoveOptions) (client.ConfigRemoveResult, error) {
|
||||
removedConfigs = append(removedConfigs, name)
|
||||
return nil
|
||||
return client.ConfigRemoveResult{}, nil
|
||||
},
|
||||
})
|
||||
cmd := newConfigRemoveCommand(cli)
|
||||
@ -63,12 +65,12 @@ func TestConfigRemoveContinueAfterError(t *testing.T) {
|
||||
var removedConfigs []string
|
||||
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
configRemoveFunc: func(name string) error {
|
||||
configRemoveFunc: func(_ context.Context, name string, _ client.ConfigRemoveOptions) (client.ConfigRemoveResult, error) {
|
||||
removedConfigs = append(removedConfigs, name)
|
||||
if name == "foo" {
|
||||
return errors.New("error removing config: " + name)
|
||||
return client.ConfigRemoveResult{}, errors.New("error removing config: " + name)
|
||||
}
|
||||
return nil
|
||||
return client.ConfigRemoveResult{}, nil
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@ -2,15 +2,15 @@ package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/moby/sys/signal"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@ -23,25 +23,25 @@ type AttachOptions struct {
|
||||
}
|
||||
|
||||
func inspectContainerAndCheckState(ctx context.Context, apiClient client.APIClient, args string) (*container.InspectResponse, error) {
|
||||
c, err := apiClient.ContainerInspect(ctx, args)
|
||||
c, err := apiClient.ContainerInspect(ctx, args, client.ContainerInspectOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !c.State.Running {
|
||||
return nil, errors.New("You cannot attach to a stopped container, start it first")
|
||||
if !c.Container.State.Running {
|
||||
return nil, errors.New("cannot attach to a stopped container, start it first")
|
||||
}
|
||||
if c.State.Paused {
|
||||
return nil, errors.New("You cannot attach to a paused container, unpause it first")
|
||||
if c.Container.State.Paused {
|
||||
return nil, errors.New("cannot attach to a paused container, unpause it first")
|
||||
}
|
||||
if c.State.Restarting {
|
||||
return nil, errors.New("You cannot attach to a restarting container, wait until it is running")
|
||||
if c.Container.State.Restarting {
|
||||
return nil, errors.New("cannot attach to a restarting container, wait until it is running")
|
||||
}
|
||||
|
||||
return &c, nil
|
||||
return &c.Container, nil
|
||||
}
|
||||
|
||||
// NewAttachCommand creates a new cobra.Command for `docker attach`
|
||||
func NewAttachCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
// newAttachCommand creates a new cobra.Command for `docker attach`
|
||||
func newAttachCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
var opts AttachOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
@ -58,6 +58,7 @@ func NewAttachCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
ValidArgsFunction: completion.ContainerNames(dockerCLI, false, func(ctr container.Summary) bool {
|
||||
return ctr.State != container.StatePaused
|
||||
}),
|
||||
DisableFlagsInUseLine: true,
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
@ -73,7 +74,7 @@ func RunAttach(ctx context.Context, dockerCLI command.Cli, containerID string, o
|
||||
|
||||
// request channel to wait for client
|
||||
waitCtx := context.WithoutCancel(ctx)
|
||||
resultC, errC := apiClient.ContainerWait(waitCtx, containerID, "")
|
||||
waitRes := apiClient.ContainerWait(waitCtx, containerID, client.ContainerWaitOptions{})
|
||||
|
||||
c, err := inspectContainerAndCheckState(ctx, apiClient, containerID)
|
||||
if err != nil {
|
||||
@ -89,7 +90,7 @@ func RunAttach(ctx context.Context, dockerCLI command.Cli, containerID string, o
|
||||
detachKeys = opts.DetachKeys
|
||||
}
|
||||
|
||||
options := container.AttachOptions{
|
||||
options := client.ContainerAttachOptions{
|
||||
Stream: true,
|
||||
Stdin: !opts.NoStdin && c.Config.OpenStdin,
|
||||
Stdout: true,
|
||||
@ -113,11 +114,11 @@ func RunAttach(ctx context.Context, dockerCLI command.Cli, containerID string, o
|
||||
defer signal.StopCatch(sigc)
|
||||
}
|
||||
|
||||
resp, errAttach := apiClient.ContainerAttach(ctx, containerID, options)
|
||||
if errAttach != nil {
|
||||
return errAttach
|
||||
res, err := apiClient.ContainerAttach(ctx, containerID, options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Close()
|
||||
defer res.HijackedResponse.Close()
|
||||
|
||||
// If use docker attach command to attach to a stop container, it will return
|
||||
// "You cannot attach to a stopped container" error, it's ok, but when
|
||||
@ -141,7 +142,7 @@ func RunAttach(ctx context.Context, dockerCLI command.Cli, containerID string, o
|
||||
inputStream: in,
|
||||
outputStream: dockerCLI.Out(),
|
||||
errorStream: dockerCLI.Err(),
|
||||
resp: resp,
|
||||
resp: res.HijackedResponse,
|
||||
tty: c.Config.Tty,
|
||||
detachKeys: options.DetachKeys,
|
||||
}
|
||||
@ -151,19 +152,19 @@ func RunAttach(ctx context.Context, dockerCLI command.Cli, containerID string, o
|
||||
return err
|
||||
}
|
||||
|
||||
return getExitStatus(errC, resultC)
|
||||
return getExitStatus(waitRes)
|
||||
}
|
||||
|
||||
func getExitStatus(errC <-chan error, resultC <-chan container.WaitResponse) error {
|
||||
func getExitStatus(waitRes client.ContainerWaitResult) error {
|
||||
select {
|
||||
case result := <-resultC:
|
||||
case result := <-waitRes.Result:
|
||||
if result.Error != nil {
|
||||
return errors.New(result.Error.Message)
|
||||
}
|
||||
if result.StatusCode != 0 {
|
||||
return cli.StatusError{StatusCode: int(result.StatusCode)}
|
||||
}
|
||||
case err := <-errC:
|
||||
case err := <-waitRes.Error:
|
||||
return err
|
||||
}
|
||||
|
||||
@ -176,7 +177,7 @@ func resizeTTY(ctx context.Context, dockerCli command.Cli, containerID string) {
|
||||
// terminal, the only way to get the shell prompt to display for attaches 2+ is to artificially
|
||||
// resize it, then go back to normal. Without this, every attach after the first will
|
||||
// require the user to manually resize or hit enter.
|
||||
resizeTtyTo(ctx, dockerCli.Client(), containerID, height+1, width+1, false)
|
||||
resizeTTYTo(ctx, dockerCli.Client(), containerID, height+1, width+1, false)
|
||||
|
||||
// After the above resizing occurs, the call to MonitorTtySize below will handle resetting back
|
||||
// to the actual size.
|
||||
|
||||
@ -7,7 +7,8 @@ import (
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/client"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
@ -16,23 +17,23 @@ func TestNewAttachCommandErrors(t *testing.T) {
|
||||
name string
|
||||
args []string
|
||||
expectedError string
|
||||
containerInspectFunc func(img string) (container.InspectResponse, error)
|
||||
containerInspectFunc func(img string) (client.ContainerInspectResult, error)
|
||||
}{
|
||||
{
|
||||
name: "client-error",
|
||||
args: []string{"5cb5bb5e4a3b"},
|
||||
expectedError: "something went wrong",
|
||||
containerInspectFunc: func(containerID string) (container.InspectResponse, error) {
|
||||
return container.InspectResponse{}, errors.New("something went wrong")
|
||||
containerInspectFunc: func(containerID string) (client.ContainerInspectResult, error) {
|
||||
return client.ContainerInspectResult{}, errors.New("something went wrong")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "client-stopped",
|
||||
args: []string{"5cb5bb5e4a3b"},
|
||||
expectedError: "You cannot attach to a stopped container",
|
||||
containerInspectFunc: func(containerID string) (container.InspectResponse, error) {
|
||||
return container.InspectResponse{
|
||||
ContainerJSONBase: &container.ContainerJSONBase{
|
||||
expectedError: "cannot attach to a stopped container",
|
||||
containerInspectFunc: func(containerID string) (client.ContainerInspectResult, error) {
|
||||
return client.ContainerInspectResult{
|
||||
Container: container.InspectResponse{
|
||||
State: &container.State{
|
||||
Running: false,
|
||||
},
|
||||
@ -43,10 +44,10 @@ func TestNewAttachCommandErrors(t *testing.T) {
|
||||
{
|
||||
name: "client-paused",
|
||||
args: []string{"5cb5bb5e4a3b"},
|
||||
expectedError: "You cannot attach to a paused container",
|
||||
containerInspectFunc: func(containerID string) (container.InspectResponse, error) {
|
||||
return container.InspectResponse{
|
||||
ContainerJSONBase: &container.ContainerJSONBase{
|
||||
expectedError: "cannot attach to a paused container",
|
||||
containerInspectFunc: func(containerID string) (client.ContainerInspectResult, error) {
|
||||
return client.ContainerInspectResult{
|
||||
Container: container.InspectResponse{
|
||||
State: &container.State{
|
||||
Running: true,
|
||||
Paused: true,
|
||||
@ -58,10 +59,10 @@ func TestNewAttachCommandErrors(t *testing.T) {
|
||||
{
|
||||
name: "client-restarting",
|
||||
args: []string{"5cb5bb5e4a3b"},
|
||||
expectedError: "You cannot attach to a restarting container",
|
||||
containerInspectFunc: func(containerID string) (container.InspectResponse, error) {
|
||||
return container.InspectResponse{
|
||||
ContainerJSONBase: &container.ContainerJSONBase{
|
||||
expectedError: "cannot attach to a restarting container",
|
||||
containerInspectFunc: func(containerID string) (client.ContainerInspectResult, error) {
|
||||
return client.ContainerInspectResult{
|
||||
Container: container.InspectResponse{
|
||||
State: &container.State{
|
||||
Running: true,
|
||||
Paused: false,
|
||||
@ -74,7 +75,7 @@ func TestNewAttachCommandErrors(t *testing.T) {
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cmd := NewAttachCommand(test.NewFakeCli(&fakeClient{inspectFunc: tc.containerInspectFunc}))
|
||||
cmd := newAttachCommand(test.NewFakeCli(&fakeClient{inspectFunc: tc.containerInspectFunc}))
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
@ -124,7 +125,10 @@ func TestGetExitStatus(t *testing.T) {
|
||||
resultC <- *testcase.result
|
||||
}
|
||||
|
||||
err := getExitStatus(errC, resultC)
|
||||
err := getExitStatus(client.ContainerWaitResult{
|
||||
Result: resultC,
|
||||
Error: errC,
|
||||
})
|
||||
|
||||
if testcase.expectedError == nil {
|
||||
assert.NilError(t, err)
|
||||
|
||||
@ -3,232 +3,247 @@ package container
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"reflect"
|
||||
"strings"
|
||||
"unsafe"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/docker/api/types/system"
|
||||
"github.com/docker/docker/client"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/moby/moby/client"
|
||||
)
|
||||
|
||||
func mockContainerExportResult(content string) client.ContainerExportResult {
|
||||
out := client.ContainerExportResult{}
|
||||
|
||||
// Set unexported field "rc"
|
||||
v := reflect.ValueOf(&out).Elem()
|
||||
f := v.FieldByName("rc")
|
||||
r := io.NopCloser(strings.NewReader(content))
|
||||
reflect.NewAt(f.Type(), unsafe.Pointer(f.UnsafeAddr())).Elem().Set(reflect.ValueOf(r))
|
||||
return out
|
||||
}
|
||||
|
||||
func mockContainerLogsResult(content string) client.ContainerLogsResult {
|
||||
out := client.ContainerLogsResult{}
|
||||
|
||||
// Set unexported field "rc"
|
||||
v := reflect.ValueOf(&out).Elem()
|
||||
f := v.FieldByName("rc")
|
||||
r := io.NopCloser(strings.NewReader(content))
|
||||
reflect.NewAt(f.Type(), unsafe.Pointer(f.UnsafeAddr())).Elem().Set(reflect.ValueOf(r))
|
||||
return out
|
||||
}
|
||||
|
||||
type fakeStreamResult struct {
|
||||
io.ReadCloser
|
||||
client.ImagePushResponse // same interface as [client.ImagePushResponse]
|
||||
}
|
||||
|
||||
func (e fakeStreamResult) Read(p []byte) (int, error) { return e.ReadCloser.Read(p) }
|
||||
func (e fakeStreamResult) Close() error { return e.ReadCloser.Close() }
|
||||
|
||||
type fakeClient struct {
|
||||
client.Client
|
||||
inspectFunc func(string) (container.InspectResponse, error)
|
||||
execInspectFunc func(execID string) (container.ExecInspect, error)
|
||||
execCreateFunc func(containerID string, options container.ExecOptions) (container.ExecCreateResponse, error)
|
||||
createContainerFunc func(config *container.Config,
|
||||
hostConfig *container.HostConfig,
|
||||
networkingConfig *network.NetworkingConfig,
|
||||
platform *ocispec.Platform,
|
||||
containerName string) (container.CreateResponse, error)
|
||||
containerStartFunc func(containerID string, options container.StartOptions) error
|
||||
imageCreateFunc func(ctx context.Context, parentReference string, options image.CreateOptions) (io.ReadCloser, error)
|
||||
infoFunc func() (system.Info, error)
|
||||
containerStatPathFunc func(containerID, path string) (container.PathStat, error)
|
||||
containerCopyFromFunc func(containerID, srcPath string) (io.ReadCloser, container.PathStat, error)
|
||||
logFunc func(string, container.LogsOptions) (io.ReadCloser, error)
|
||||
waitFunc func(string) (<-chan container.WaitResponse, <-chan error)
|
||||
containerListFunc func(container.ListOptions) ([]container.Summary, error)
|
||||
containerExportFunc func(string) (io.ReadCloser, error)
|
||||
containerExecResizeFunc func(id string, options container.ResizeOptions) error
|
||||
containerRemoveFunc func(ctx context.Context, containerID string, options container.RemoveOptions) error
|
||||
containerRestartFunc func(ctx context.Context, containerID string, options container.StopOptions) error
|
||||
containerStopFunc func(ctx context.Context, containerID string, options container.StopOptions) error
|
||||
containerKillFunc func(ctx context.Context, containerID, signal string) error
|
||||
containerPruneFunc func(ctx context.Context, pruneFilters filters.Args) (container.PruneReport, error)
|
||||
containerAttachFunc func(ctx context.Context, containerID string, options container.AttachOptions) (types.HijackedResponse, error)
|
||||
containerDiffFunc func(ctx context.Context, containerID string) ([]container.FilesystemChange, error)
|
||||
inspectFunc func(string) (client.ContainerInspectResult, error)
|
||||
execInspectFunc func(execID string) (client.ExecInspectResult, error)
|
||||
execCreateFunc func(containerID string, options client.ExecCreateOptions) (client.ExecCreateResult, error)
|
||||
createContainerFunc func(options client.ContainerCreateOptions) (client.ContainerCreateResult, error)
|
||||
containerStartFunc func(containerID string, options client.ContainerStartOptions) (client.ContainerStartResult, error)
|
||||
imagePullFunc func(ctx context.Context, parentReference string, options client.ImagePullOptions) (client.ImagePullResponse, error)
|
||||
infoFunc func() (client.SystemInfoResult, error)
|
||||
containerStatPathFunc func(containerID, path string) (client.ContainerStatPathResult, error)
|
||||
containerCopyFromFunc func(containerID, srcPath string) (client.CopyFromContainerResult, error)
|
||||
logFunc func(string, client.ContainerLogsOptions) (client.ContainerLogsResult, error)
|
||||
waitFunc func(string) client.ContainerWaitResult
|
||||
containerListFunc func(client.ContainerListOptions) (client.ContainerListResult, error)
|
||||
containerExportFunc func(string) (client.ContainerExportResult, error)
|
||||
containerExecResizeFunc func(id string, options client.ExecResizeOptions) (client.ExecResizeResult, error)
|
||||
containerRemoveFunc func(ctx context.Context, containerID string, options client.ContainerRemoveOptions) (client.ContainerRemoveResult, error)
|
||||
containerRestartFunc func(ctx context.Context, containerID string, options client.ContainerRestartOptions) (client.ContainerRestartResult, error)
|
||||
containerStopFunc func(ctx context.Context, containerID string, options client.ContainerStopOptions) (client.ContainerStopResult, error)
|
||||
containerKillFunc func(ctx context.Context, containerID string, options client.ContainerKillOptions) (client.ContainerKillResult, error)
|
||||
containerPruneFunc func(ctx context.Context, options client.ContainerPruneOptions) (client.ContainerPruneResult, error)
|
||||
containerAttachFunc func(ctx context.Context, containerID string, options client.ContainerAttachOptions) (client.ContainerAttachResult, error)
|
||||
containerDiffFunc func(ctx context.Context, containerID string) (client.ContainerDiffResult, error)
|
||||
containerRenameFunc func(ctx context.Context, oldName, newName string) error
|
||||
containerCommitFunc func(ctx context.Context, container string, options container.CommitOptions) (container.CommitResponse, error)
|
||||
containerPauseFunc func(ctx context.Context, container string) error
|
||||
containerCommitFunc func(ctx context.Context, container string, options client.ContainerCommitOptions) (client.ContainerCommitResult, error)
|
||||
containerPauseFunc func(ctx context.Context, container string, options client.ContainerPauseOptions) (client.ContainerPauseResult, error)
|
||||
Version string
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerList(_ context.Context, options container.ListOptions) ([]container.Summary, error) {
|
||||
func (f *fakeClient) ContainerList(_ context.Context, options client.ContainerListOptions) (client.ContainerListResult, error) {
|
||||
if f.containerListFunc != nil {
|
||||
return f.containerListFunc(options)
|
||||
}
|
||||
return []container.Summary{}, nil
|
||||
return client.ContainerListResult{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerInspect(_ context.Context, containerID string) (container.InspectResponse, error) {
|
||||
func (f *fakeClient) ContainerInspect(_ context.Context, containerID string, _ client.ContainerInspectOptions) (client.ContainerInspectResult, error) {
|
||||
if f.inspectFunc != nil {
|
||||
return f.inspectFunc(containerID)
|
||||
}
|
||||
return container.InspectResponse{}, nil
|
||||
return client.ContainerInspectResult{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerExecCreate(_ context.Context, containerID string, config container.ExecOptions) (container.ExecCreateResponse, error) {
|
||||
func (f *fakeClient) ExecCreate(_ context.Context, containerID string, config client.ExecCreateOptions) (client.ExecCreateResult, error) {
|
||||
if f.execCreateFunc != nil {
|
||||
return f.execCreateFunc(containerID, config)
|
||||
}
|
||||
return container.ExecCreateResponse{}, nil
|
||||
return client.ExecCreateResult{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerExecInspect(_ context.Context, execID string) (container.ExecInspect, error) {
|
||||
func (f *fakeClient) ExecInspect(_ context.Context, execID string, _ client.ExecInspectOptions) (client.ExecInspectResult, error) {
|
||||
if f.execInspectFunc != nil {
|
||||
return f.execInspectFunc(execID)
|
||||
}
|
||||
return container.ExecInspect{}, nil
|
||||
return client.ExecInspectResult{}, nil
|
||||
}
|
||||
|
||||
func (*fakeClient) ContainerExecStart(context.Context, string, container.ExecStartOptions) error {
|
||||
return nil
|
||||
func (*fakeClient) ExecStart(context.Context, string, client.ExecStartOptions) (client.ExecStartResult, error) {
|
||||
return client.ExecStartResult{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerCreate(
|
||||
_ context.Context,
|
||||
config *container.Config,
|
||||
hostConfig *container.HostConfig,
|
||||
networkingConfig *network.NetworkingConfig,
|
||||
platform *ocispec.Platform,
|
||||
containerName string,
|
||||
) (container.CreateResponse, error) {
|
||||
func (f *fakeClient) ContainerCreate(_ context.Context, options client.ContainerCreateOptions) (client.ContainerCreateResult, error) {
|
||||
if f.createContainerFunc != nil {
|
||||
return f.createContainerFunc(config, hostConfig, networkingConfig, platform, containerName)
|
||||
return f.createContainerFunc(options)
|
||||
}
|
||||
return container.CreateResponse{}, nil
|
||||
return client.ContainerCreateResult{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerRemove(ctx context.Context, containerID string, options container.RemoveOptions) error {
|
||||
func (f *fakeClient) ContainerRemove(ctx context.Context, containerID string, options client.ContainerRemoveOptions) (client.ContainerRemoveResult, error) {
|
||||
if f.containerRemoveFunc != nil {
|
||||
return f.containerRemoveFunc(ctx, containerID, options)
|
||||
}
|
||||
return nil
|
||||
return client.ContainerRemoveResult{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ImageCreate(ctx context.Context, parentReference string, options image.CreateOptions) (io.ReadCloser, error) {
|
||||
if f.imageCreateFunc != nil {
|
||||
return f.imageCreateFunc(ctx, parentReference, options)
|
||||
func (f *fakeClient) ImagePull(ctx context.Context, parentReference string, options client.ImagePullOptions) (client.ImagePullResponse, error) {
|
||||
if f.imagePullFunc != nil {
|
||||
return f.imagePullFunc(ctx, parentReference, options)
|
||||
}
|
||||
return nil, nil
|
||||
return fakeStreamResult{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) Info(_ context.Context) (system.Info, error) {
|
||||
func (f *fakeClient) Info(context.Context, client.InfoOptions) (client.SystemInfoResult, error) {
|
||||
if f.infoFunc != nil {
|
||||
return f.infoFunc()
|
||||
}
|
||||
return system.Info{}, nil
|
||||
return client.SystemInfoResult{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerStatPath(_ context.Context, containerID, path string) (container.PathStat, error) {
|
||||
func (f *fakeClient) ContainerStatPath(_ context.Context, containerID string, options client.ContainerStatPathOptions) (client.ContainerStatPathResult, error) {
|
||||
if f.containerStatPathFunc != nil {
|
||||
return f.containerStatPathFunc(containerID, path)
|
||||
return f.containerStatPathFunc(containerID, options.Path)
|
||||
}
|
||||
return container.PathStat{}, nil
|
||||
return client.ContainerStatPathResult{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) CopyFromContainer(_ context.Context, containerID, srcPath string) (io.ReadCloser, container.PathStat, error) {
|
||||
func (f *fakeClient) CopyFromContainer(_ context.Context, containerID string, options client.CopyFromContainerOptions) (client.CopyFromContainerResult, error) {
|
||||
if f.containerCopyFromFunc != nil {
|
||||
return f.containerCopyFromFunc(containerID, srcPath)
|
||||
return f.containerCopyFromFunc(containerID, options.SourcePath)
|
||||
}
|
||||
return nil, container.PathStat{}, nil
|
||||
return client.CopyFromContainerResult{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerLogs(_ context.Context, containerID string, options container.LogsOptions) (io.ReadCloser, error) {
|
||||
func (f *fakeClient) ContainerLogs(_ context.Context, containerID string, options client.ContainerLogsOptions) (client.ContainerLogsResult, error) {
|
||||
if f.logFunc != nil {
|
||||
return f.logFunc(containerID, options)
|
||||
}
|
||||
return nil, nil
|
||||
return client.ContainerLogsResult{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ClientVersion() string {
|
||||
return f.Version
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerWait(_ context.Context, containerID string, _ container.WaitCondition) (<-chan container.WaitResponse, <-chan error) {
|
||||
func (f *fakeClient) ContainerWait(_ context.Context, containerID string, _ client.ContainerWaitOptions) client.ContainerWaitResult {
|
||||
if f.waitFunc != nil {
|
||||
return f.waitFunc(containerID)
|
||||
}
|
||||
return nil, nil
|
||||
return client.ContainerWaitResult{}
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerStart(_ context.Context, containerID string, options container.StartOptions) error {
|
||||
func (f *fakeClient) ContainerStart(_ context.Context, containerID string, options client.ContainerStartOptions) (client.ContainerStartResult, error) {
|
||||
if f.containerStartFunc != nil {
|
||||
return f.containerStartFunc(containerID, options)
|
||||
}
|
||||
return nil
|
||||
return client.ContainerStartResult{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerExport(_ context.Context, containerID string) (io.ReadCloser, error) {
|
||||
func (f *fakeClient) ContainerExport(_ context.Context, containerID string, _ client.ContainerExportOptions) (client.ContainerExportResult, error) {
|
||||
if f.containerExportFunc != nil {
|
||||
return f.containerExportFunc(containerID)
|
||||
}
|
||||
return nil, nil
|
||||
return client.ContainerExportResult{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerExecResize(_ context.Context, id string, options container.ResizeOptions) error {
|
||||
func (f *fakeClient) ExecResize(_ context.Context, id string, options client.ExecResizeOptions) (client.ExecResizeResult, error) {
|
||||
if f.containerExecResizeFunc != nil {
|
||||
return f.containerExecResizeFunc(id, options)
|
||||
}
|
||||
return nil
|
||||
return client.ExecResizeResult{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerKill(ctx context.Context, containerID, signal string) error {
|
||||
func (f *fakeClient) ContainerKill(ctx context.Context, containerID string, options client.ContainerKillOptions) (client.ContainerKillResult, error) {
|
||||
if f.containerKillFunc != nil {
|
||||
return f.containerKillFunc(ctx, containerID, signal)
|
||||
return f.containerKillFunc(ctx, containerID, options)
|
||||
}
|
||||
return nil
|
||||
return client.ContainerKillResult{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainersPrune(ctx context.Context, pruneFilters filters.Args) (container.PruneReport, error) {
|
||||
func (f *fakeClient) ContainersPrune(ctx context.Context, options client.ContainerPruneOptions) (client.ContainerPruneResult, error) {
|
||||
if f.containerPruneFunc != nil {
|
||||
return f.containerPruneFunc(ctx, pruneFilters)
|
||||
return f.containerPruneFunc(ctx, options)
|
||||
}
|
||||
return container.PruneReport{}, nil
|
||||
return client.ContainerPruneResult{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerRestart(ctx context.Context, containerID string, options container.StopOptions) error {
|
||||
func (f *fakeClient) ContainerRestart(ctx context.Context, containerID string, options client.ContainerRestartOptions) (client.ContainerRestartResult, error) {
|
||||
if f.containerRestartFunc != nil {
|
||||
return f.containerRestartFunc(ctx, containerID, options)
|
||||
}
|
||||
return nil
|
||||
return client.ContainerRestartResult{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerStop(ctx context.Context, containerID string, options container.StopOptions) error {
|
||||
func (f *fakeClient) ContainerStop(ctx context.Context, containerID string, options client.ContainerStopOptions) (client.ContainerStopResult, error) {
|
||||
if f.containerStopFunc != nil {
|
||||
return f.containerStopFunc(ctx, containerID, options)
|
||||
}
|
||||
return nil
|
||||
return client.ContainerStopResult{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerAttach(ctx context.Context, containerID string, options container.AttachOptions) (types.HijackedResponse, error) {
|
||||
func (f *fakeClient) ContainerAttach(ctx context.Context, containerID string, options client.ContainerAttachOptions) (client.ContainerAttachResult, error) {
|
||||
if f.containerAttachFunc != nil {
|
||||
return f.containerAttachFunc(ctx, containerID, options)
|
||||
}
|
||||
return types.HijackedResponse{}, nil
|
||||
return client.ContainerAttachResult{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerDiff(ctx context.Context, containerID string) ([]container.FilesystemChange, error) {
|
||||
func (f *fakeClient) ContainerDiff(ctx context.Context, containerID string, _ client.ContainerDiffOptions) (client.ContainerDiffResult, error) {
|
||||
if f.containerDiffFunc != nil {
|
||||
return f.containerDiffFunc(ctx, containerID)
|
||||
}
|
||||
|
||||
return []container.FilesystemChange{}, nil
|
||||
return client.ContainerDiffResult{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerRename(ctx context.Context, oldName, newName string) error {
|
||||
func (f *fakeClient) ContainerRename(ctx context.Context, oldName string, options client.ContainerRenameOptions) (client.ContainerRenameResult, error) {
|
||||
if f.containerRenameFunc != nil {
|
||||
return f.containerRenameFunc(ctx, oldName, newName)
|
||||
return client.ContainerRenameResult{}, f.containerRenameFunc(ctx, oldName, options.NewName)
|
||||
}
|
||||
|
||||
return nil
|
||||
return client.ContainerRenameResult{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerCommit(ctx context.Context, containerID string, options container.CommitOptions) (container.CommitResponse, error) {
|
||||
func (f *fakeClient) ContainerCommit(ctx context.Context, containerID string, options client.ContainerCommitOptions) (client.ContainerCommitResult, error) {
|
||||
if f.containerCommitFunc != nil {
|
||||
return f.containerCommitFunc(ctx, containerID, options)
|
||||
}
|
||||
return container.CommitResponse{}, nil
|
||||
return client.ContainerCommitResult{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerPause(ctx context.Context, containerID string) error {
|
||||
func (f *fakeClient) ContainerPause(ctx context.Context, containerID string, options client.ContainerPauseOptions) (client.ContainerPauseResult, error) {
|
||||
if f.containerPauseFunc != nil {
|
||||
return f.containerPauseFunc(ctx, containerID)
|
||||
return f.containerPauseFunc(ctx, containerID, options)
|
||||
}
|
||||
|
||||
return nil
|
||||
return client.ContainerPauseResult{}, nil
|
||||
}
|
||||
|
||||
@ -3,43 +3,73 @@ package container
|
||||
import (
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/internal/commands"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// NewContainerCommand returns a cobra command for `container` subcommands
|
||||
func NewContainerCommand(dockerCli command.Cli) *cobra.Command {
|
||||
func init() {
|
||||
commands.Register(newRunCommand)
|
||||
commands.Register(newExecCommand)
|
||||
commands.Register(newPsCommand)
|
||||
commands.Register(newContainerCommand)
|
||||
commands.RegisterLegacy(newAttachCommand)
|
||||
commands.RegisterLegacy(newCommitCommand)
|
||||
commands.RegisterLegacy(newCopyCommand)
|
||||
commands.RegisterLegacy(newCreateCommand)
|
||||
commands.RegisterLegacy(newDiffCommand)
|
||||
commands.RegisterLegacy(newExportCommand)
|
||||
commands.RegisterLegacy(newKillCommand)
|
||||
commands.RegisterLegacy(newLogsCommand)
|
||||
commands.RegisterLegacy(newPauseCommand)
|
||||
commands.RegisterLegacy(newPortCommand)
|
||||
commands.RegisterLegacy(newRenameCommand)
|
||||
commands.RegisterLegacy(newRestartCommand)
|
||||
commands.RegisterLegacy(newRmCommand)
|
||||
commands.RegisterLegacy(newStartCommand)
|
||||
commands.RegisterLegacy(newStatsCommand)
|
||||
commands.RegisterLegacy(newStopCommand)
|
||||
commands.RegisterLegacy(newTopCommand)
|
||||
commands.RegisterLegacy(newUnpauseCommand)
|
||||
commands.RegisterLegacy(newUpdateCommand)
|
||||
commands.RegisterLegacy(newWaitCommand)
|
||||
}
|
||||
|
||||
// newContainerCommand returns a cobra command for `container` subcommands
|
||||
func newContainerCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "container",
|
||||
Short: "Manage containers",
|
||||
Args: cli.NoArgs,
|
||||
RunE: command.ShowHelp(dockerCli.Err()),
|
||||
RunE: command.ShowHelp(dockerCLI.Err()),
|
||||
|
||||
DisableFlagsInUseLine: true,
|
||||
}
|
||||
cmd.AddCommand(
|
||||
NewAttachCommand(dockerCli),
|
||||
NewCommitCommand(dockerCli),
|
||||
NewCopyCommand(dockerCli),
|
||||
NewCreateCommand(dockerCli),
|
||||
NewDiffCommand(dockerCli),
|
||||
NewExecCommand(dockerCli),
|
||||
NewExportCommand(dockerCli),
|
||||
NewKillCommand(dockerCli),
|
||||
NewLogsCommand(dockerCli),
|
||||
NewPauseCommand(dockerCli),
|
||||
NewPortCommand(dockerCli),
|
||||
NewRenameCommand(dockerCli),
|
||||
NewRestartCommand(dockerCli),
|
||||
newRemoveCommand(dockerCli),
|
||||
NewRunCommand(dockerCli),
|
||||
NewStartCommand(dockerCli),
|
||||
NewStatsCommand(dockerCli),
|
||||
NewStopCommand(dockerCli),
|
||||
NewTopCommand(dockerCli),
|
||||
NewUnpauseCommand(dockerCli),
|
||||
NewUpdateCommand(dockerCli),
|
||||
NewWaitCommand(dockerCli),
|
||||
newListCommand(dockerCli),
|
||||
newInspectCommand(dockerCli),
|
||||
NewPruneCommand(dockerCli),
|
||||
newAttachCommand(dockerCLI),
|
||||
newCommitCommand(dockerCLI),
|
||||
newCopyCommand(dockerCLI),
|
||||
newCreateCommand(dockerCLI),
|
||||
newDiffCommand(dockerCLI),
|
||||
newExecCommand(dockerCLI),
|
||||
newExportCommand(dockerCLI),
|
||||
newKillCommand(dockerCLI),
|
||||
newLogsCommand(dockerCLI),
|
||||
newPauseCommand(dockerCLI),
|
||||
newPortCommand(dockerCLI),
|
||||
newRenameCommand(dockerCLI),
|
||||
newRestartCommand(dockerCLI),
|
||||
newRemoveCommand(dockerCLI),
|
||||
newRunCommand(dockerCLI),
|
||||
newStartCommand(dockerCLI),
|
||||
newStatsCommand(dockerCLI),
|
||||
newStopCommand(dockerCLI),
|
||||
newTopCommand(dockerCLI),
|
||||
newUnpauseCommand(dockerCLI),
|
||||
newUpdateCommand(dockerCLI),
|
||||
newWaitCommand(dockerCLI),
|
||||
newListCommand(dockerCLI),
|
||||
newInspectCommand(dockerCLI),
|
||||
newPruneCommand(dockerCLI),
|
||||
)
|
||||
return cmd
|
||||
}
|
||||
|
||||
@ -2,13 +2,14 @@ package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@ -17,13 +18,14 @@ type commitOptions struct {
|
||||
reference string
|
||||
|
||||
pause bool
|
||||
noPause bool
|
||||
comment string
|
||||
author string
|
||||
changes opts.ListOpts
|
||||
}
|
||||
|
||||
// NewCommitCommand creates a new cobra.Command for `docker commit`
|
||||
func NewCommitCommand(dockerCli command.Cli) *cobra.Command {
|
||||
// newCommitCommand creates a new cobra.Command for `docker commit`
|
||||
func newCommitCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
var options commitOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
@ -35,18 +37,29 @@ func NewCommitCommand(dockerCli command.Cli) *cobra.Command {
|
||||
if len(args) > 1 {
|
||||
options.reference = args[1]
|
||||
}
|
||||
return runCommit(cmd.Context(), dockerCli, &options)
|
||||
if cmd.Flag("pause").Changed {
|
||||
if cmd.Flag("no-pause").Changed {
|
||||
return errors.New("conflicting options: --no-pause and --pause cannot be used together")
|
||||
}
|
||||
options.noPause = !options.pause
|
||||
}
|
||||
return runCommit(cmd.Context(), dockerCLI, &options)
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"aliases": "docker container commit, docker commit",
|
||||
},
|
||||
ValidArgsFunction: completion.ContainerNames(dockerCli, false),
|
||||
ValidArgsFunction: completion.ContainerNames(dockerCLI, false),
|
||||
DisableFlagsInUseLine: true,
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.SetInterspersed(false)
|
||||
|
||||
flags.BoolVarP(&options.pause, "pause", "p", true, "Pause container during commit")
|
||||
// TODO(thaJeztah): Deprecated: the --pause flag was deprecated in v29 and can be removed in v30.
|
||||
flags.BoolVarP(&options.pause, "pause", "p", true, "Pause container during commit (deprecated: use --no-pause instead)")
|
||||
_ = flags.MarkDeprecated("pause", "and enabled by default. Use --no-pause to disable pausing during commit.")
|
||||
|
||||
flags.BoolVar(&options.noPause, "no-pause", false, "Disable pausing container during commit")
|
||||
flags.StringVarP(&options.comment, "message", "m", "", "Commit message")
|
||||
flags.StringVarP(&options.author, "author", "a", "", `Author (e.g., "John Hannibal Smith <hannibal@a-team.com>")`)
|
||||
|
||||
@ -57,17 +70,17 @@ func NewCommitCommand(dockerCli command.Cli) *cobra.Command {
|
||||
}
|
||||
|
||||
func runCommit(ctx context.Context, dockerCli command.Cli, options *commitOptions) error {
|
||||
response, err := dockerCli.Client().ContainerCommit(ctx, options.container, container.CommitOptions{
|
||||
response, err := dockerCli.Client().ContainerCommit(ctx, options.container, client.ContainerCommitOptions{
|
||||
Reference: options.reference,
|
||||
Comment: options.comment,
|
||||
Author: options.author,
|
||||
Changes: options.changes.GetSlice(),
|
||||
Pause: options.pause,
|
||||
NoPause: options.noPause,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintln(dockerCli.Out(), response.ID)
|
||||
_, _ = fmt.Fprintln(dockerCli.Out(), response.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -7,36 +7,32 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/moby/moby/client"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
|
||||
func TestRunCommit(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
containerCommitFunc: func(
|
||||
ctx context.Context,
|
||||
ctr string,
|
||||
options container.CommitOptions,
|
||||
) (container.CommitResponse, error) {
|
||||
containerCommitFunc: func(ctx context.Context, ctr string, options client.ContainerCommitOptions) (client.ContainerCommitResult, error) {
|
||||
assert.Check(t, is.Equal(options.Author, "Author Name <author@name.com>"))
|
||||
assert.Check(t, is.DeepEqual(options.Changes, []string{"EXPOSE 80"}))
|
||||
assert.Check(t, is.Equal(options.Comment, "commit message"))
|
||||
assert.Check(t, is.Equal(options.Pause, false))
|
||||
assert.Check(t, is.Equal(options.NoPause, true))
|
||||
assert.Check(t, is.Equal(ctr, "container-id"))
|
||||
|
||||
return container.CommitResponse{ID: "image-id"}, nil
|
||||
return client.ContainerCommitResult{ID: "image-id"}, nil
|
||||
},
|
||||
})
|
||||
|
||||
cmd := NewCommitCommand(cli)
|
||||
cmd := newCommitCommand(cli)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetArgs(
|
||||
[]string{
|
||||
"--author", "Author Name <author@name.com>",
|
||||
"--change", "EXPOSE 80",
|
||||
"--message", "commit message",
|
||||
"--pause=false",
|
||||
"--no-pause",
|
||||
"container-id",
|
||||
},
|
||||
)
|
||||
@ -51,16 +47,12 @@ func TestRunCommitClientError(t *testing.T) {
|
||||
clientError := errors.New("client error")
|
||||
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
containerCommitFunc: func(
|
||||
ctx context.Context,
|
||||
ctr string,
|
||||
options container.CommitOptions,
|
||||
) (container.CommitResponse, error) {
|
||||
return container.CommitResponse{}, clientError
|
||||
containerCommitFunc: func(ctx context.Context, ctr string, options client.ContainerCommitOptions) (client.ContainerCommitResult, error) {
|
||||
return client.ContainerCommitResult{}, clientError
|
||||
},
|
||||
})
|
||||
|
||||
cmd := NewCommitCommand(cli)
|
||||
cmd := newCommitCommand(cli)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
cmd.SetArgs([]string{"container-id"})
|
||||
|
||||
@ -8,7 +8,8 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/moby/sys/capability"
|
||||
"github.com/moby/sys/signal"
|
||||
"github.com/spf13/cobra"
|
||||
@ -122,15 +123,15 @@ func addCompletions(cmd *cobra.Command, dockerCLI completion.APIClientProvider)
|
||||
_ = cmd.RegisterFlagCompletionFunc("cap-add", completeLinuxCapabilityNames)
|
||||
_ = cmd.RegisterFlagCompletionFunc("cap-drop", completeLinuxCapabilityNames)
|
||||
_ = cmd.RegisterFlagCompletionFunc("cgroupns", completeCgroupns())
|
||||
_ = cmd.RegisterFlagCompletionFunc("env", completion.EnvVarNames)
|
||||
_ = cmd.RegisterFlagCompletionFunc("env-file", completion.FileNames)
|
||||
_ = cmd.RegisterFlagCompletionFunc("env", completion.EnvVarNames())
|
||||
_ = cmd.RegisterFlagCompletionFunc("env-file", completion.FileNames())
|
||||
_ = cmd.RegisterFlagCompletionFunc("ipc", completeIpc(dockerCLI))
|
||||
_ = cmd.RegisterFlagCompletionFunc("link", completeLink(dockerCLI))
|
||||
_ = cmd.RegisterFlagCompletionFunc("log-driver", completeLogDriver(dockerCLI))
|
||||
_ = cmd.RegisterFlagCompletionFunc("log-opt", completeLogOpt)
|
||||
_ = cmd.RegisterFlagCompletionFunc("network", completion.NetworkNames(dockerCLI))
|
||||
_ = cmd.RegisterFlagCompletionFunc("pid", completePid(dockerCLI))
|
||||
_ = cmd.RegisterFlagCompletionFunc("platform", completion.Platforms)
|
||||
_ = cmd.RegisterFlagCompletionFunc("platform", completion.Platforms())
|
||||
_ = cmd.RegisterFlagCompletionFunc("pull", completion.FromList(PullImageAlways, PullImageMissing, PullImageNever))
|
||||
_ = cmd.RegisterFlagCompletionFunc("restart", completeRestartPolicies)
|
||||
_ = cmd.RegisterFlagCompletionFunc("security-opt", completeSecurityOpt)
|
||||
@ -186,11 +187,11 @@ func completeLink(dockerCLI completion.APIClientProvider) cobra.CompletionFunc {
|
||||
// of the build-in log drivers.
|
||||
func completeLogDriver(dockerCLI completion.APIClientProvider) cobra.CompletionFunc {
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
info, err := dockerCLI.Client().Info(cmd.Context())
|
||||
res, err := dockerCLI.Client().Info(cmd.Context(), client.InfoOptions{})
|
||||
if err != nil {
|
||||
return builtInLogDrivers(), cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
drivers := info.Plugins.Log
|
||||
drivers := res.Info.Plugins.Log
|
||||
return drivers, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
}
|
||||
@ -279,12 +280,12 @@ func completeUlimit(_ *cobra.Command, _ []string, _ string) ([]string, cobra.She
|
||||
// completeVolumeDriver contacts the API to get the built-in and installed volume drivers.
|
||||
func completeVolumeDriver(dockerCLI completion.APIClientProvider) cobra.CompletionFunc {
|
||||
return func(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
|
||||
info, err := dockerCLI.Client().Info(cmd.Context())
|
||||
res, err := dockerCLI.Client().Info(cmd.Context(), client.InfoOptions{})
|
||||
if err != nil {
|
||||
// fallback: the built-in drivers
|
||||
return []string{"local"}, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
drivers := info.Plugins.Volume
|
||||
drivers := res.Info.Plugins.Volume
|
||||
return drivers, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,7 +6,8 @@ import (
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/cli/internal/test/builders"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/moby/sys/signal"
|
||||
"github.com/spf13/cobra"
|
||||
"gotest.tools/v3/assert"
|
||||
@ -26,7 +27,7 @@ func TestCompleteLinuxCapabilityNames(t *testing.T) {
|
||||
|
||||
func TestCompletePid(t *testing.T) {
|
||||
tests := []struct {
|
||||
containerListFunc func(container.ListOptions) ([]container.Summary, error)
|
||||
containerListFunc func(client.ContainerListOptions) (client.ContainerListResult, error)
|
||||
toComplete string
|
||||
expectedCompletions []string
|
||||
expectedDirective cobra.ShellCompDirective
|
||||
@ -42,10 +43,12 @@ func TestCompletePid(t *testing.T) {
|
||||
expectedDirective: cobra.ShellCompDirectiveNoSpace,
|
||||
},
|
||||
{
|
||||
containerListFunc: func(container.ListOptions) ([]container.Summary, error) {
|
||||
return []container.Summary{
|
||||
*builders.Container("c1"),
|
||||
*builders.Container("c2"),
|
||||
containerListFunc: func(client.ContainerListOptions) (client.ContainerListResult, error) {
|
||||
return client.ContainerListResult{
|
||||
Items: []container.Summary{
|
||||
*builders.Container("c1"),
|
||||
*builders.Container("c2"),
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
toComplete: "container:",
|
||||
@ -59,7 +62,7 @@ func TestCompletePid(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
containerListFunc: tc.containerListFunc,
|
||||
})
|
||||
completions, directive := completePid(cli)(NewRunCommand(cli), nil, tc.toComplete)
|
||||
completions, directive := completePid(cli)(newRunCommand(cli), nil, tc.toComplete)
|
||||
assert.Check(t, is.DeepEqual(completions, tc.expectedCompletions))
|
||||
assert.Check(t, is.Equal(directive, tc.expectedDirective))
|
||||
})
|
||||
|
||||
@ -3,6 +3,7 @@ package container
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
@ -15,11 +16,10 @@ import (
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/go-units"
|
||||
"github.com/moby/go-archive"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/morikuni/aec"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@ -121,8 +121,8 @@ func copyProgress(ctx context.Context, dst io.Writer, header string, total *int6
|
||||
return restore, done
|
||||
}
|
||||
|
||||
// NewCopyCommand creates a new `docker cp` command
|
||||
func NewCopyCommand(dockerCli command.Cli) *cobra.Command {
|
||||
// newCopyCommand creates a new `docker cp` command
|
||||
func newCopyCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
var opts copyOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
@ -147,13 +147,14 @@ container source to stdout.`,
|
||||
opts.destination = args[1]
|
||||
if !cmd.Flag("quiet").Changed {
|
||||
// User did not specify "quiet" flag; suppress output if no terminal is attached
|
||||
opts.quiet = !dockerCli.Out().IsTerminal()
|
||||
opts.quiet = !dockerCLI.Out().IsTerminal()
|
||||
}
|
||||
return runCopy(cmd.Context(), dockerCli, opts)
|
||||
return runCopy(cmd.Context(), dockerCLI, opts)
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"aliases": "docker container cp, docker cp",
|
||||
},
|
||||
DisableFlagsInUseLine: true,
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
@ -229,11 +230,13 @@ func copyFromContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cp
|
||||
// if client requests to follow symbol link, then must decide target file to be copied
|
||||
var rebaseName string
|
||||
if copyConfig.followLink {
|
||||
srcStat, err := apiClient.ContainerStatPath(ctx, copyConfig.container, srcPath)
|
||||
src, err := apiClient.ContainerStatPath(ctx, copyConfig.container, client.ContainerStatPathOptions{
|
||||
Path: srcPath,
|
||||
})
|
||||
|
||||
// If the destination is a symbolic link, we should follow it.
|
||||
if err == nil && srcStat.Mode&os.ModeSymlink != 0 {
|
||||
linkTarget := srcStat.LinkTarget
|
||||
if err == nil && src.Stat.Mode&os.ModeSymlink != 0 {
|
||||
linkTarget := src.Stat.LinkTarget
|
||||
if !isAbs(linkTarget) {
|
||||
// Join with the parent directory.
|
||||
srcParent, _ := archive.SplitPathDirEntry(srcPath)
|
||||
@ -248,11 +251,14 @@ func copyFromContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cp
|
||||
ctx, cancel := signal.NotifyContext(ctx, os.Interrupt)
|
||||
defer cancel()
|
||||
|
||||
content, stat, err := apiClient.CopyFromContainer(ctx, copyConfig.container, srcPath)
|
||||
cpRes, err := apiClient.CopyFromContainer(ctx, copyConfig.container, client.CopyFromContainerOptions{
|
||||
SourcePath: srcPath,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer content.Close()
|
||||
content := cpRes.Content
|
||||
defer func() { _ = content.Close() }()
|
||||
|
||||
if dstPath == "-" {
|
||||
_, err = io.Copy(dockerCLI.Out(), content)
|
||||
@ -262,7 +268,7 @@ func copyFromContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cp
|
||||
srcInfo := archive.CopyInfo{
|
||||
Path: srcPath,
|
||||
Exists: true,
|
||||
IsDir: stat.Mode.IsDir(),
|
||||
IsDir: cpRes.Stat.Mode.IsDir(),
|
||||
RebaseName: rebaseName,
|
||||
}
|
||||
|
||||
@ -298,50 +304,50 @@ func copyFromContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cp
|
||||
// about both the source and destination. The API is a simple tar
|
||||
// archive/extract API but we can use the stat info header about the
|
||||
// destination to be more informed about exactly what the destination is.
|
||||
func copyToContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cpConfig) (err error) {
|
||||
func copyToContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cpConfig) error {
|
||||
srcPath := copyConfig.sourcePath
|
||||
dstPath := copyConfig.destPath
|
||||
|
||||
if srcPath != "-" {
|
||||
// Get an absolute source path.
|
||||
srcPath, err = resolveLocalPath(srcPath)
|
||||
p, err := resolveLocalPath(srcPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
srcPath = p
|
||||
}
|
||||
|
||||
apiClient := dockerCLI.Client()
|
||||
// Prepare destination copy info by stat-ing the container path.
|
||||
dstInfo := archive.CopyInfo{Path: dstPath}
|
||||
dstStat, err := apiClient.ContainerStatPath(ctx, copyConfig.container, dstPath)
|
||||
if dst, err := apiClient.ContainerStatPath(ctx, copyConfig.container, client.ContainerStatPathOptions{Path: dstPath}); err == nil {
|
||||
// If the destination is a symbolic link, we should evaluate it.
|
||||
if dst.Stat.Mode&os.ModeSymlink != 0 {
|
||||
linkTarget := dst.Stat.LinkTarget
|
||||
if !isAbs(linkTarget) {
|
||||
// Join with the parent directory.
|
||||
dstParent, _ := archive.SplitPathDirEntry(dstPath)
|
||||
linkTarget = filepath.Join(dstParent, linkTarget)
|
||||
}
|
||||
|
||||
// If the destination is a symbolic link, we should evaluate it.
|
||||
if err == nil && dstStat.Mode&os.ModeSymlink != 0 {
|
||||
linkTarget := dstStat.LinkTarget
|
||||
if !isAbs(linkTarget) {
|
||||
// Join with the parent directory.
|
||||
dstParent, _ := archive.SplitPathDirEntry(dstPath)
|
||||
linkTarget = filepath.Join(dstParent, linkTarget)
|
||||
dstInfo.Path = linkTarget
|
||||
dst, err = apiClient.ContainerStatPath(ctx, copyConfig.container, client.ContainerStatPathOptions{Path: linkTarget})
|
||||
}
|
||||
// Validate the destination path
|
||||
if err == nil {
|
||||
if err := command.ValidateOutputPathFileMode(dst.Stat.Mode); err != nil {
|
||||
return fmt.Errorf(`destination "%s:%s" must be a directory or a regular file: %w`, copyConfig.container, dstPath, err)
|
||||
}
|
||||
dstInfo.Exists, dstInfo.IsDir = true, dst.Stat.Mode.IsDir()
|
||||
}
|
||||
|
||||
dstInfo.Path = linkTarget
|
||||
dstStat, err = apiClient.ContainerStatPath(ctx, copyConfig.container, linkTarget)
|
||||
// FIXME(thaJeztah): unhandled error (should this return?)
|
||||
}
|
||||
|
||||
// Validate the destination path
|
||||
if err := command.ValidateOutputPathFileMode(dstStat.Mode); err != nil {
|
||||
return errors.Wrapf(err, `destination "%s:%s" must be a directory or a regular file`, copyConfig.container, dstPath)
|
||||
}
|
||||
|
||||
// Ignore any error and assume that the parent directory of the destination
|
||||
// path exists, in which case the copy may still succeed. If there is any
|
||||
// type of conflict (e.g., non-directory overwriting an existing directory
|
||||
// or vice versa) the extraction will fail. If the destination simply did
|
||||
// not exist, but the parent directory does, the extraction will still
|
||||
// succeed.
|
||||
if err == nil {
|
||||
dstInfo.Exists, dstInfo.IsDir = true, dstStat.Mode.IsDir()
|
||||
// Ignore any error and assume that the parent directory of the destination
|
||||
// path exists, in which case the copy may still succeed. If there is any
|
||||
// type of conflict (e.g., non-directory overwriting an existing directory
|
||||
// or vice versa) the extraction will fail. If the destination simply did
|
||||
// not exist, but the parent directory does, the extraction will still
|
||||
// succeed.
|
||||
_ = err // Intentionally ignore stat errors (see above)
|
||||
}
|
||||
|
||||
var (
|
||||
@ -354,7 +360,7 @@ func copyToContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cpCo
|
||||
content = os.Stdin
|
||||
resolvedDstPath = dstInfo.Path
|
||||
if !dstInfo.IsDir {
|
||||
return errors.Errorf("destination \"%s:%s\" must be a directory", copyConfig.container, dstPath)
|
||||
return fmt.Errorf(`destination "%s:%s" must be a directory`, copyConfig.container, dstPath)
|
||||
}
|
||||
} else {
|
||||
// Prepare source copy info.
|
||||
@ -397,23 +403,27 @@ func copyToContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cpCo
|
||||
}
|
||||
}
|
||||
|
||||
options := container.CopyToContainerOptions{
|
||||
CopyUIDGID: copyConfig.copyUIDGID,
|
||||
options := client.CopyToContainerOptions{
|
||||
DestinationPath: resolvedDstPath,
|
||||
Content: content,
|
||||
CopyUIDGID: copyConfig.copyUIDGID,
|
||||
}
|
||||
|
||||
if copyConfig.quiet {
|
||||
return apiClient.CopyToContainer(ctx, copyConfig.container, resolvedDstPath, content, options)
|
||||
_, err := apiClient.CopyToContainer(ctx, copyConfig.container, options)
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, cancel := signal.NotifyContext(ctx, os.Interrupt)
|
||||
restore, done := copyProgress(ctx, dockerCLI.Err(), copyToContainerHeader, &copiedSize)
|
||||
res := apiClient.CopyToContainer(ctx, copyConfig.container, resolvedDstPath, content, options)
|
||||
// TODO(thaJeztah): error-handling looks odd here; should it be handled differently?
|
||||
_, err := apiClient.CopyToContainer(ctx, copyConfig.container, options)
|
||||
cancel()
|
||||
<-done
|
||||
restore()
|
||||
fmt.Fprintln(dockerCLI.Err(), "Successfully copied", progressHumanSize(copiedSize), "to", copyConfig.container+":"+dstInfo.Path)
|
||||
_, _ = fmt.Fprintln(dockerCLI.Err(), "Successfully copied", progressHumanSize(copiedSize), "to", copyConfig.container+":"+dstInfo.Path)
|
||||
|
||||
return res
|
||||
return err
|
||||
}
|
||||
|
||||
// We use `:` as a delimiter between CONTAINER and PATH, but `:` could also be
|
||||
|
||||
@ -9,9 +9,9 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/moby/go-archive"
|
||||
"github.com/moby/go-archive/compression"
|
||||
"github.com/moby/moby/client"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
"gotest.tools/v3/fs"
|
||||
@ -52,9 +52,11 @@ func TestRunCopyFromContainerToStdout(t *testing.T) {
|
||||
tarContent := "the tar content"
|
||||
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
containerCopyFromFunc: func(ctr, srcPath string) (io.ReadCloser, container.PathStat, error) {
|
||||
containerCopyFromFunc: func(ctr, srcPath string) (client.CopyFromContainerResult, error) {
|
||||
assert.Check(t, is.Equal("container", ctr))
|
||||
return io.NopCloser(strings.NewReader(tarContent)), container.PathStat{}, nil
|
||||
return client.CopyFromContainerResult{
|
||||
Content: io.NopCloser(strings.NewReader(tarContent)),
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
err := runCopy(context.TODO(), cli, copyOptions{
|
||||
@ -73,10 +75,12 @@ func TestRunCopyFromContainerToFilesystem(t *testing.T) {
|
||||
destDir := fs.NewDir(t, "cp-test")
|
||||
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
containerCopyFromFunc: func(ctr, srcPath string) (io.ReadCloser, container.PathStat, error) {
|
||||
containerCopyFromFunc: func(ctr, srcPath string) (client.CopyFromContainerResult, error) {
|
||||
assert.Check(t, is.Equal("container", ctr))
|
||||
readCloser, err := archive.Tar(srcDir.Path(), compression.None)
|
||||
return readCloser, container.PathStat{}, err
|
||||
return client.CopyFromContainerResult{
|
||||
Content: readCloser,
|
||||
}, err
|
||||
},
|
||||
})
|
||||
err := runCopy(context.TODO(), cli, copyOptions{
|
||||
@ -99,10 +103,12 @@ func TestRunCopyFromContainerToFilesystemMissingDestinationDirectory(t *testing.
|
||||
defer destDir.Remove()
|
||||
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
containerCopyFromFunc: func(ctr, srcPath string) (io.ReadCloser, container.PathStat, error) {
|
||||
containerCopyFromFunc: func(ctr, srcPath string) (client.CopyFromContainerResult, error) {
|
||||
assert.Check(t, is.Equal("container", ctr))
|
||||
readCloser, err := archive.TarWithOptions(destDir.Path(), &archive.TarOptions{})
|
||||
return readCloser, container.PathStat{}, err
|
||||
return client.CopyFromContainerResult{
|
||||
Content: readCloser,
|
||||
}, err
|
||||
},
|
||||
})
|
||||
err := runCopy(context.TODO(), cli, copyOptions{
|
||||
|
||||
@ -4,14 +4,14 @@ import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/netip"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
cerrdefs "github.com/containerd/errdefs"
|
||||
"github.com/containerd/errdefs"
|
||||
"github.com/containerd/platforms"
|
||||
"github.com/distribution/reference"
|
||||
"github.com/docker/cli/cli"
|
||||
@ -24,13 +24,9 @@ import (
|
||||
"github.com/docker/cli/cli/trust"
|
||||
"github.com/docker/cli/internal/jsonstream"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
imagetypes "github.com/docker/docker/api/types/image"
|
||||
"github.com/docker/docker/api/types/mount"
|
||||
"github.com/docker/docker/api/types/versions"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/moby/moby/api/types/mount"
|
||||
"github.com/moby/moby/client"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
@ -51,8 +47,8 @@ type createOptions struct {
|
||||
useAPISocket bool
|
||||
}
|
||||
|
||||
// NewCreateCommand creates a new cobra.Command for `docker create`
|
||||
func NewCreateCommand(dockerCli command.Cli) *cobra.Command {
|
||||
// newCreateCommand creates a new cobra.Command for `docker create`
|
||||
func newCreateCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
var options createOptions
|
||||
var copts *containerOptions
|
||||
|
||||
@ -65,12 +61,13 @@ func NewCreateCommand(dockerCli command.Cli) *cobra.Command {
|
||||
if len(args) > 1 {
|
||||
copts.Args = args[1:]
|
||||
}
|
||||
return runCreate(cmd.Context(), dockerCli, cmd.Flags(), &options, copts)
|
||||
return runCreate(cmd.Context(), dockerCLI, cmd.Flags(), &options, copts)
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"aliases": "docker container create, docker create",
|
||||
},
|
||||
ValidArgsFunction: completion.ImageNames(dockerCli, -1),
|
||||
ValidArgsFunction: completion.ImageNames(dockerCLI, -1),
|
||||
DisableFlagsInUseLine: true,
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
@ -80,24 +77,21 @@ func NewCreateCommand(dockerCli command.Cli) *cobra.Command {
|
||||
flags.StringVar(&options.pull, "pull", PullImageMissing, `Pull image before creating ("`+PullImageAlways+`", "|`+PullImageMissing+`", "`+PullImageNever+`")`)
|
||||
flags.BoolVarP(&options.quiet, "quiet", "q", false, "Suppress the pull output")
|
||||
flags.BoolVarP(&options.useAPISocket, "use-api-socket", "", false, "Bind mount Docker API socket and required auth")
|
||||
flags.SetAnnotation("use-api-socket", "experimentalCLI", nil) // Marks flag as experimental for now.
|
||||
_ = flags.SetAnnotation("use-api-socket", "experimentalCLI", nil) // Mark flag as experimental for now.
|
||||
|
||||
// Add an explicit help that doesn't have a `-h` to prevent the conflict
|
||||
// with hostname
|
||||
flags.Bool("help", false, "Print usage")
|
||||
|
||||
command.AddPlatformFlag(flags, &options.platform)
|
||||
command.AddTrustVerificationFlags(flags, &options.untrusted, dockerCli.ContentTrustEnabled())
|
||||
// TODO(thaJeztah): consider adding platform as "image create option" on containerOptions
|
||||
flags.StringVar(&options.platform, "platform", os.Getenv("DOCKER_DEFAULT_PLATFORM"), "Set platform if server is multi-platform capable")
|
||||
_ = flags.SetAnnotation("platform", "version", []string{"1.32"})
|
||||
_ = cmd.RegisterFlagCompletionFunc("platform", completion.Platforms())
|
||||
|
||||
flags.BoolVar(&options.untrusted, "disable-content-trust", !trust.Enabled(), "Skip image verification")
|
||||
copts = addFlags(flags)
|
||||
|
||||
addCompletions(cmd, dockerCli)
|
||||
|
||||
flags.VisitAll(func(flag *pflag.Flag) {
|
||||
// Set a default completion function if none was set. We don't look
|
||||
// up if it does already have one set, because Cobra does this for
|
||||
// us, and returns an error (which we ignore for this reason).
|
||||
_ = cmd.RegisterFlagCompletionFunc(flag.Name, completion.NoComplete)
|
||||
})
|
||||
addCompletions(cmd, dockerCLI)
|
||||
|
||||
return cmd
|
||||
}
|
||||
@ -110,7 +104,7 @@ func runCreate(ctx context.Context, dockerCli command.Cli, flags *pflag.FlagSet,
|
||||
}
|
||||
}
|
||||
proxyConfig := dockerCli.ConfigFile().ParseProxyConfig(dockerCli.Client().DaemonHost(), opts.ConvertKVStringsToMapWithNil(copts.env.GetSlice()))
|
||||
newEnv := []string{}
|
||||
newEnv := make([]string, 0, len(proxyConfig))
|
||||
for k, v := range proxyConfig {
|
||||
if v == nil {
|
||||
newEnv = append(newEnv, k)
|
||||
@ -141,20 +135,27 @@ func pullImage(ctx context.Context, dockerCli command.Cli, img string, options *
|
||||
return err
|
||||
}
|
||||
|
||||
responseBody, err := dockerCli.Client().ImageCreate(ctx, img, imagetypes.CreateOptions{
|
||||
var ociPlatforms []ocispec.Platform
|
||||
if options.platform != "" {
|
||||
// Already validated.
|
||||
ociPlatforms = append(ociPlatforms, platforms.MustParse(options.platform))
|
||||
}
|
||||
resp, err := dockerCli.Client().ImagePull(ctx, img, client.ImagePullOptions{
|
||||
RegistryAuth: encodedAuth,
|
||||
Platform: options.platform,
|
||||
Platforms: ociPlatforms,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer responseBody.Close()
|
||||
defer func() {
|
||||
_ = resp.Close()
|
||||
}()
|
||||
|
||||
out := dockerCli.Err()
|
||||
if options.quiet {
|
||||
out = streams.NewOut(io.Discard)
|
||||
}
|
||||
return jsonstream.Display(ctx, responseBody, out)
|
||||
return jsonstream.Display(ctx, resp, out)
|
||||
}
|
||||
|
||||
type cidFile struct {
|
||||
@ -167,13 +168,13 @@ func (cid *cidFile) Close() error {
|
||||
if cid.file == nil {
|
||||
return nil
|
||||
}
|
||||
cid.file.Close()
|
||||
_ = cid.file.Close()
|
||||
|
||||
if cid.written {
|
||||
return nil
|
||||
}
|
||||
if err := os.Remove(cid.path); err != nil {
|
||||
return errors.Wrapf(err, "failed to remove the CID file '%s'", cid.path)
|
||||
return fmt.Errorf("failed to remove the CID file '%s': %w", cid.path, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
@ -184,7 +185,7 @@ func (cid *cidFile) Write(id string) error {
|
||||
return nil
|
||||
}
|
||||
if _, err := cid.file.Write([]byte(id)); err != nil {
|
||||
return errors.Wrap(err, "failed to write the container ID to the file")
|
||||
return fmt.Errorf("failed to write the container ID to the file: %w", err)
|
||||
}
|
||||
cid.written = true
|
||||
return nil
|
||||
@ -195,12 +196,12 @@ func newCIDFile(cidPath string) (*cidFile, error) {
|
||||
return &cidFile{}, nil
|
||||
}
|
||||
if _, err := os.Stat(cidPath); err == nil {
|
||||
return nil, errors.Errorf("container ID file found, make sure the other container isn't running or delete %s", cidPath)
|
||||
return nil, errors.New("container ID file found, make sure the other container isn't running or delete " + cidPath)
|
||||
}
|
||||
|
||||
f, err := os.Create(cidPath)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to create the container ID file")
|
||||
return nil, fmt.Errorf("failed to create the container ID file: %w", err)
|
||||
}
|
||||
|
||||
return &cidFile{path: cidPath, file: f}, nil
|
||||
@ -217,11 +218,21 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *c
|
||||
namedRef reference.Named
|
||||
)
|
||||
|
||||
// TODO(thaJeztah): add a platform option-type / flag-type.
|
||||
if options.platform != "" {
|
||||
_, err = platforms.Parse(options.platform)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
containerIDFile, err := newCIDFile(hostConfig.ContainerIDFile)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer containerIDFile.Close()
|
||||
defer func() {
|
||||
_ = containerIDFile.Close()
|
||||
}()
|
||||
|
||||
ref, err := reference.ParseAnyReference(config.Image)
|
||||
if err != nil {
|
||||
@ -308,14 +319,10 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *c
|
||||
}
|
||||
|
||||
var platform *ocispec.Platform
|
||||
// Engine API version 1.41 first introduced the option to specify platform on
|
||||
// create. It will produce an error if you try to set a platform on older API
|
||||
// versions, so check the API version here to maintain backwards
|
||||
// compatibility for CLI users.
|
||||
if options.platform != "" && versions.GreaterThanOrEqualTo(dockerCli.Client().ClientVersion(), "1.41") {
|
||||
if options.platform != "" {
|
||||
p, err := platforms.Parse(options.platform)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(invalidParameter(err), "error parsing specified platform")
|
||||
return "", invalidParameter(fmt.Errorf("error parsing specified platform: %w", err))
|
||||
}
|
||||
platform = &p
|
||||
}
|
||||
@ -338,10 +345,17 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *c
|
||||
|
||||
hostConfig.ConsoleSize[0], hostConfig.ConsoleSize[1] = dockerCli.Out().GetTtySize()
|
||||
|
||||
response, err := dockerCli.Client().ContainerCreate(ctx, config, hostConfig, networkingConfig, platform, options.name)
|
||||
response, err := dockerCli.Client().ContainerCreate(ctx, client.ContainerCreateOptions{
|
||||
Name: options.name,
|
||||
// Image: config.Image, // TODO(thaJeztah): pass image-ref separate
|
||||
Platform: platform,
|
||||
Config: config,
|
||||
HostConfig: hostConfig,
|
||||
NetworkingConfig: networkingConfig,
|
||||
})
|
||||
if err != nil {
|
||||
// Pull image if it does not exist locally and we have the PullImageMissing option. Default behavior.
|
||||
if cerrdefs.IsNotFound(err) && namedRef != nil && options.pull == PullImageMissing {
|
||||
if errdefs.IsNotFound(err) && namedRef != nil && options.pull == PullImageMissing {
|
||||
if !options.quiet {
|
||||
// we don't want to write to stdout anything apart from container.ID
|
||||
_, _ = fmt.Fprintf(dockerCli.Err(), "Unable to find image '%s' locally\n", reference.FamiliarString(namedRef))
|
||||
@ -352,7 +366,14 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *c
|
||||
}
|
||||
|
||||
var retryErr error
|
||||
response, retryErr = dockerCli.Client().ContainerCreate(ctx, config, hostConfig, networkingConfig, platform, options.name)
|
||||
response, retryErr = dockerCli.Client().ContainerCreate(ctx, client.ContainerCreateOptions{
|
||||
Name: options.name,
|
||||
// Image: config.Image, // TODO(thaJeztah): pass image-ref separate
|
||||
Platform: platform,
|
||||
Config: config,
|
||||
HostConfig: hostConfig,
|
||||
NetworkingConfig: networkingConfig,
|
||||
})
|
||||
if retryErr != nil {
|
||||
return "", retryErr
|
||||
}
|
||||
@ -361,10 +382,6 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *c
|
||||
}
|
||||
}
|
||||
|
||||
if warn := localhostDNSWarning(*hostConfig); warn != "" {
|
||||
response.Warnings = append(response.Warnings, warn)
|
||||
}
|
||||
|
||||
containerID = response.ID
|
||||
for _, w := range response.Warnings {
|
||||
_, _ = fmt.Fprintln(dockerCli.Err(), "WARNING:", w)
|
||||
@ -385,19 +402,6 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *c
|
||||
return containerID, err
|
||||
}
|
||||
|
||||
// check the DNS settings passed via --dns against localhost regexp to warn if
|
||||
// they are trying to set a DNS to a localhost address.
|
||||
//
|
||||
// TODO(thaJeztah): move this to the daemon, which can make a better call if it will work or not (depending on networking mode).
|
||||
func localhostDNSWarning(hostConfig container.HostConfig) string {
|
||||
for _, dnsIP := range hostConfig.DNS {
|
||||
if addr, err := netip.ParseAddr(dnsIP); err == nil && addr.IsLoopback() {
|
||||
return fmt.Sprintf("Localhost DNS (%s) may fail in containers.", addr)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func validatePullOpt(val string) error {
|
||||
switch val {
|
||||
case PullImageAlways, PullImageMissing, PullImageNever, "":
|
||||
@ -419,7 +423,7 @@ func validatePullOpt(val string) error {
|
||||
//
|
||||
// The path should be an absolute path in the container, commonly
|
||||
// /root/.docker/config.json.
|
||||
func copyDockerConfigIntoContainer(ctx context.Context, dockerAPI client.APIClient, containerID string, configPath string, config *configfile.ConfigFile) error {
|
||||
func copyDockerConfigIntoContainer(ctx context.Context, apiClient client.APIClient, containerID string, configPath string, config *configfile.ConfigFile) error {
|
||||
var configBuf bytes.Buffer
|
||||
if err := config.SaveToWriter(&configBuf); err != nil {
|
||||
return fmt.Errorf("saving creds: %w", err)
|
||||
@ -428,7 +432,7 @@ func copyDockerConfigIntoContainer(ctx context.Context, dockerAPI client.APIClie
|
||||
// We don't need to get super fancy with the tar creation.
|
||||
var tarBuf bytes.Buffer
|
||||
tarWriter := tar.NewWriter(&tarBuf)
|
||||
tarWriter.WriteHeader(&tar.Header{
|
||||
_ = tarWriter.WriteHeader(&tar.Header{
|
||||
Name: configPath,
|
||||
Size: int64(configBuf.Len()),
|
||||
Mode: 0o600,
|
||||
@ -442,8 +446,11 @@ func copyDockerConfigIntoContainer(ctx context.Context, dockerAPI client.APIClie
|
||||
return fmt.Errorf("closing tar for config copy failed: %w", err)
|
||||
}
|
||||
|
||||
if err := dockerAPI.CopyToContainer(ctx, containerID, "/",
|
||||
&tarBuf, container.CopyToContainerOptions{}); err != nil {
|
||||
_, err := apiClient.CopyToContainer(ctx, containerID, client.CopyToContainerOptions{
|
||||
DestinationPath: "/",
|
||||
Content: &tarBuf,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("copying config.json into container failed: %w", err)
|
||||
}
|
||||
|
||||
|
||||
@ -14,12 +14,10 @@ import (
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/cli/internal/test/notary"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/docker/api/types/system"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/api/types/system"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/spf13/pflag"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
@ -116,31 +114,27 @@ func TestCreateContainerImagePullPolicy(t *testing.T) {
|
||||
t.Run(tc.PullPolicy, func(t *testing.T) {
|
||||
pullCounter := 0
|
||||
|
||||
client := &fakeClient{
|
||||
createContainerFunc: func(
|
||||
config *container.Config,
|
||||
hostConfig *container.HostConfig,
|
||||
networkingConfig *network.NetworkingConfig,
|
||||
platform *ocispec.Platform,
|
||||
containerName string,
|
||||
) (container.CreateResponse, error) {
|
||||
apiClient := &fakeClient{
|
||||
createContainerFunc: func(options client.ContainerCreateOptions) (client.ContainerCreateResult, error) {
|
||||
defer func() { tc.ResponseCounter++ }()
|
||||
switch tc.ResponseCounter {
|
||||
case 0:
|
||||
return container.CreateResponse{}, fakeNotFound{}
|
||||
return client.ContainerCreateResult{}, fakeNotFound{}
|
||||
default:
|
||||
return container.CreateResponse{ID: containerID}, nil
|
||||
return client.ContainerCreateResult{ID: containerID}, nil
|
||||
}
|
||||
},
|
||||
imageCreateFunc: func(ctx context.Context, parentReference string, options image.CreateOptions) (io.ReadCloser, error) {
|
||||
imagePullFunc: func(ctx context.Context, parentReference string, options client.ImagePullOptions) (client.ImagePullResponse, error) {
|
||||
defer func() { pullCounter++ }()
|
||||
return io.NopCloser(strings.NewReader("")), nil
|
||||
return fakeStreamResult{ReadCloser: io.NopCloser(strings.NewReader(""))}, nil
|
||||
},
|
||||
infoFunc: func() (system.Info, error) {
|
||||
return system.Info{IndexServerAddress: "https://indexserver.example.com"}, nil
|
||||
infoFunc: func() (client.SystemInfoResult, error) {
|
||||
return client.SystemInfoResult{
|
||||
Info: system.Info{IndexServerAddress: "https://indexserver.example.com"},
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
fakeCLI := test.NewFakeCli(client)
|
||||
fakeCLI := test.NewFakeCli(apiClient)
|
||||
id, err := createContainer(context.Background(), fakeCLI, config, &createOptions{
|
||||
name: "name",
|
||||
platform: runtime.GOOS,
|
||||
@ -206,7 +200,7 @@ func TestCreateContainerValidateFlags(t *testing.T) {
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cmd := NewCreateCommand(test.NewFakeCli(&fakeClient{}))
|
||||
cmd := newCreateCommand(test.NewFakeCli(&fakeClient{}))
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
@ -249,18 +243,14 @@ func TestNewCreateCommandWithContentTrustErrors(t *testing.T) {
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Setenv("DOCKER_CONTENT_TRUST", "true")
|
||||
fakeCLI := test.NewFakeCli(&fakeClient{
|
||||
createContainerFunc: func(config *container.Config,
|
||||
hostConfig *container.HostConfig,
|
||||
networkingConfig *network.NetworkingConfig,
|
||||
platform *ocispec.Platform,
|
||||
containerName string,
|
||||
) (container.CreateResponse, error) {
|
||||
return container.CreateResponse{}, errors.New("shouldn't try to pull image")
|
||||
createContainerFunc: func(options client.ContainerCreateOptions) (client.ContainerCreateResult, error) {
|
||||
return client.ContainerCreateResult{}, errors.New("shouldn't try to pull image")
|
||||
},
|
||||
}, test.EnableContentTrust)
|
||||
})
|
||||
fakeCLI.SetNotaryClient(tc.notaryFunc)
|
||||
cmd := NewCreateCommand(fakeCLI)
|
||||
cmd := newCreateCommand(fakeCLI)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
@ -291,30 +281,15 @@ func TestNewCreateCommandWithWarnings(t *testing.T) {
|
||||
args: []string{"image:tag"},
|
||||
warnings: []string{"warning from daemon", "another warning from daemon"},
|
||||
},
|
||||
{
|
||||
name: "container-create-localhost-dns",
|
||||
args: []string{"--dns=127.0.0.11", "image:tag"},
|
||||
warning: true,
|
||||
},
|
||||
{
|
||||
name: "container-create-localhost-dns-ipv6",
|
||||
args: []string{"--dns=::1", "image:tag"},
|
||||
warning: true,
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
fakeCLI := test.NewFakeCli(&fakeClient{
|
||||
createContainerFunc: func(config *container.Config,
|
||||
hostConfig *container.HostConfig,
|
||||
networkingConfig *network.NetworkingConfig,
|
||||
platform *ocispec.Platform,
|
||||
containerName string,
|
||||
) (container.CreateResponse, error) {
|
||||
return container.CreateResponse{Warnings: tc.warnings}, nil
|
||||
createContainerFunc: func(options client.ContainerCreateOptions) (client.ContainerCreateResult, error) {
|
||||
return client.ContainerCreateResult{Warnings: tc.warnings}, nil
|
||||
},
|
||||
})
|
||||
cmd := NewCreateCommand(fakeCLI)
|
||||
cmd := newCreateCommand(fakeCLI)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
err := cmd.Execute()
|
||||
@ -344,15 +319,10 @@ func TestCreateContainerWithProxyConfig(t *testing.T) {
|
||||
sort.Strings(expected)
|
||||
|
||||
fakeCLI := test.NewFakeCli(&fakeClient{
|
||||
createContainerFunc: func(config *container.Config,
|
||||
hostConfig *container.HostConfig,
|
||||
networkingConfig *network.NetworkingConfig,
|
||||
platform *ocispec.Platform,
|
||||
containerName string,
|
||||
) (container.CreateResponse, error) {
|
||||
sort.Strings(config.Env)
|
||||
assert.DeepEqual(t, config.Env, expected)
|
||||
return container.CreateResponse{}, nil
|
||||
createContainerFunc: func(options client.ContainerCreateOptions) (client.ContainerCreateResult, error) {
|
||||
sort.Strings(options.Config.Env)
|
||||
assert.DeepEqual(t, options.Config.Env, expected)
|
||||
return client.ContainerCreateResult{}, nil
|
||||
},
|
||||
})
|
||||
fakeCLI.SetConfigFile(&configfile.ConfigFile{
|
||||
@ -366,7 +336,7 @@ func TestCreateContainerWithProxyConfig(t *testing.T) {
|
||||
},
|
||||
},
|
||||
})
|
||||
cmd := NewCreateCommand(fakeCLI)
|
||||
cmd := newCreateCommand(fakeCLI)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetArgs([]string{"image:tag"})
|
||||
err := cmd.Execute()
|
||||
|
||||
@ -7,33 +7,35 @@ import (
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// NewDiffCommand creates a new cobra.Command for `docker diff`
|
||||
func NewDiffCommand(dockerCli command.Cli) *cobra.Command {
|
||||
// newDiffCommand creates a new cobra.Command for `docker diff`
|
||||
func newDiffCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "diff CONTAINER",
|
||||
Short: "Inspect changes to files or directories on a container's filesystem",
|
||||
Args: cli.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runDiff(cmd.Context(), dockerCli, args[0])
|
||||
return runDiff(cmd.Context(), dockerCLI, args[0])
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"aliases": "docker container diff, docker diff",
|
||||
},
|
||||
ValidArgsFunction: completion.ContainerNames(dockerCli, false),
|
||||
ValidArgsFunction: completion.ContainerNames(dockerCLI, false),
|
||||
DisableFlagsInUseLine: true,
|
||||
}
|
||||
}
|
||||
|
||||
func runDiff(ctx context.Context, dockerCLI command.Cli, containerID string) error {
|
||||
changes, err := dockerCLI.Client().ContainerDiff(ctx, containerID)
|
||||
res, err := dockerCLI.Client().ContainerDiff(ctx, containerID, client.ContainerDiffOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
diffCtx := formatter.Context{
|
||||
Output: dockerCLI.Out(),
|
||||
Format: NewDiffFormat("{{.Type}} {{.Path}}"),
|
||||
Format: newDiffFormat("{{.Type}} {{.Path}}"),
|
||||
}
|
||||
return DiffFormatWrite(diffCtx, changes)
|
||||
return diffFormatWrite(diffCtx, res)
|
||||
}
|
||||
|
||||
@ -8,35 +8,35 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/client"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
|
||||
func TestRunDiff(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
containerDiffFunc: func(
|
||||
ctx context.Context,
|
||||
containerID string,
|
||||
) ([]container.FilesystemChange, error) {
|
||||
return []container.FilesystemChange{
|
||||
{
|
||||
Kind: container.ChangeModify,
|
||||
Path: "/path/to/file0",
|
||||
},
|
||||
{
|
||||
Kind: container.ChangeAdd,
|
||||
Path: "/path/to/file1",
|
||||
},
|
||||
{
|
||||
Kind: container.ChangeDelete,
|
||||
Path: "/path/to/file2",
|
||||
containerDiffFunc: func(ctx context.Context, containerID string) (client.ContainerDiffResult, error) {
|
||||
return client.ContainerDiffResult{
|
||||
Changes: []container.FilesystemChange{
|
||||
{
|
||||
Kind: container.ChangeModify,
|
||||
Path: "/path/to/file0",
|
||||
},
|
||||
{
|
||||
Kind: container.ChangeAdd,
|
||||
Path: "/path/to/file1",
|
||||
},
|
||||
{
|
||||
Kind: container.ChangeDelete,
|
||||
Path: "/path/to/file2",
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
|
||||
cmd := NewDiffCommand(cli)
|
||||
cmd := newDiffCommand(cli)
|
||||
cmd.SetOut(io.Discard)
|
||||
|
||||
cmd.SetArgs([]string{"container-id"})
|
||||
@ -60,15 +60,12 @@ func TestRunDiffClientError(t *testing.T) {
|
||||
clientError := errors.New("client error")
|
||||
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
containerDiffFunc: func(
|
||||
ctx context.Context,
|
||||
containerID string,
|
||||
) ([]container.FilesystemChange, error) {
|
||||
return nil, clientError
|
||||
containerDiffFunc: func(ctx context.Context, containerID string) (client.ContainerDiffResult, error) {
|
||||
return client.ContainerDiffResult{}, clientError
|
||||
},
|
||||
})
|
||||
|
||||
cmd := NewDiffCommand(cli)
|
||||
cmd := newDiffCommand(cli)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
package container
|
||||
|
||||
import cerrdefs "github.com/containerd/errdefs"
|
||||
import "github.com/containerd/errdefs"
|
||||
|
||||
func invalidParameter(err error) error {
|
||||
if err == nil || cerrdefs.IsInvalidArgument(err) {
|
||||
if err == nil || errdefs.IsInvalidArgument(err) {
|
||||
return err
|
||||
}
|
||||
return invalidParameterErr{err}
|
||||
@ -17,7 +17,7 @@ func (e invalidParameterErr) Unwrap() error {
|
||||
}
|
||||
|
||||
func notFound(err error) error {
|
||||
if err == nil || cerrdefs.IsNotFound(err) {
|
||||
if err == nil || errdefs.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
return notFoundErr{err}
|
||||
|
||||
@ -2,6 +2,7 @@ package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
@ -10,9 +11,8 @@ import (
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@ -39,8 +39,8 @@ func NewExecOptions() ExecOptions {
|
||||
}
|
||||
}
|
||||
|
||||
// NewExecCommand creates a new cobra.Command for `docker exec`
|
||||
func NewExecCommand(dockerCli command.Cli) *cobra.Command {
|
||||
// newExecCommand creates a new cobra.Command for "docker exec".
|
||||
func newExecCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
options := NewExecOptions()
|
||||
|
||||
cmd := &cobra.Command{
|
||||
@ -50,15 +50,16 @@ func NewExecCommand(dockerCli command.Cli) *cobra.Command {
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
containerIDorName := args[0]
|
||||
options.Command = args[1:]
|
||||
return RunExec(cmd.Context(), dockerCli, containerIDorName, options)
|
||||
return RunExec(cmd.Context(), dockerCLI, containerIDorName, options)
|
||||
},
|
||||
ValidArgsFunction: completion.ContainerNames(dockerCli, false, func(ctr container.Summary) bool {
|
||||
ValidArgsFunction: completion.ContainerNames(dockerCLI, false, func(ctr container.Summary) bool {
|
||||
return ctr.State != container.StatePaused
|
||||
}),
|
||||
Annotations: map[string]string{
|
||||
"category-top": "2",
|
||||
"aliases": "docker container exec, docker exec",
|
||||
},
|
||||
DisableFlagsInUseLine: true,
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
@ -71,14 +72,14 @@ func NewExecCommand(dockerCli command.Cli) *cobra.Command {
|
||||
flags.StringVarP(&options.User, "user", "u", "", `Username or UID (format: "<name|uid>[:<group|gid>]")`)
|
||||
flags.BoolVar(&options.Privileged, "privileged", false, "Give extended privileges to the command")
|
||||
flags.VarP(&options.Env, "env", "e", "Set environment variables")
|
||||
flags.SetAnnotation("env", "version", []string{"1.25"})
|
||||
_ = flags.SetAnnotation("env", "version", []string{"1.25"})
|
||||
flags.Var(&options.EnvFile, "env-file", "Read in a file of environment variables")
|
||||
flags.SetAnnotation("env-file", "version", []string{"1.25"})
|
||||
_ = flags.SetAnnotation("env-file", "version", []string{"1.25"})
|
||||
flags.StringVarP(&options.Workdir, "workdir", "w", "", "Working directory inside the container")
|
||||
flags.SetAnnotation("workdir", "version", []string{"1.35"})
|
||||
_ = flags.SetAnnotation("workdir", "version", []string{"1.35"})
|
||||
|
||||
_ = cmd.RegisterFlagCompletionFunc("env", completion.EnvVarNames)
|
||||
_ = cmd.RegisterFlagCompletionFunc("env-file", completion.FileNames)
|
||||
_ = cmd.RegisterFlagCompletionFunc("env", completion.EnvVarNames())
|
||||
_ = cmd.RegisterFlagCompletionFunc("env-file", completion.FileNames())
|
||||
|
||||
return cmd
|
||||
}
|
||||
@ -96,7 +97,7 @@ func RunExec(ctx context.Context, dockerCLI command.Cli, containerIDorName strin
|
||||
// otherwise if we error out we will leak execIDs on the server (and
|
||||
// there's no easy way to clean those up). But also in order to make "not
|
||||
// exist" errors take precedence we do a dummy inspect first.
|
||||
if _, err := apiClient.ContainerInspect(ctx, containerIDorName); err != nil {
|
||||
if _, err := apiClient.ContainerInspect(ctx, containerIDorName, client.ContainerInspectOptions{}); err != nil {
|
||||
return err
|
||||
}
|
||||
if !options.Detach {
|
||||
@ -107,7 +108,7 @@ func RunExec(ctx context.Context, dockerCLI command.Cli, containerIDorName strin
|
||||
|
||||
fillConsoleSize(execOptions, dockerCLI)
|
||||
|
||||
response, err := apiClient.ContainerExecCreate(ctx, containerIDorName, *execOptions)
|
||||
response, err := apiClient.ExecCreate(ctx, containerIDorName, *execOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -118,23 +119,31 @@ func RunExec(ctx context.Context, dockerCLI command.Cli, containerIDorName strin
|
||||
}
|
||||
|
||||
if options.Detach {
|
||||
return apiClient.ContainerExecStart(ctx, execID, container.ExecStartOptions{
|
||||
var cs client.ConsoleSize
|
||||
if execOptions.ConsoleSize != nil {
|
||||
cs = client.ConsoleSize{
|
||||
Height: execOptions.ConsoleSize[0],
|
||||
Width: execOptions.ConsoleSize[1],
|
||||
}
|
||||
}
|
||||
_, err := apiClient.ExecStart(ctx, execID, client.ExecStartOptions{
|
||||
Detach: options.Detach,
|
||||
Tty: execOptions.Tty,
|
||||
ConsoleSize: execOptions.ConsoleSize,
|
||||
TTY: execOptions.Tty,
|
||||
ConsoleSize: cs,
|
||||
})
|
||||
return err
|
||||
}
|
||||
return interactiveExec(ctx, dockerCLI, execOptions, execID)
|
||||
}
|
||||
|
||||
func fillConsoleSize(execOptions *container.ExecOptions, dockerCli command.Cli) {
|
||||
func fillConsoleSize(execOptions *client.ExecCreateOptions, dockerCli command.Cli) {
|
||||
if execOptions.Tty {
|
||||
height, width := dockerCli.Out().GetTtySize()
|
||||
execOptions.ConsoleSize = &[2]uint{height, width}
|
||||
}
|
||||
}
|
||||
|
||||
func interactiveExec(ctx context.Context, dockerCli command.Cli, execOptions *container.ExecOptions, execID string) error {
|
||||
func interactiveExec(ctx context.Context, dockerCli command.Cli, execOptions *client.ExecCreateOptions, execID string) error {
|
||||
// Interactive exec requested.
|
||||
var (
|
||||
out, stderr io.Writer
|
||||
@ -157,9 +166,16 @@ func interactiveExec(ctx context.Context, dockerCli command.Cli, execOptions *co
|
||||
fillConsoleSize(execOptions, dockerCli)
|
||||
|
||||
apiClient := dockerCli.Client()
|
||||
resp, err := apiClient.ContainerExecAttach(ctx, execID, container.ExecAttachOptions{
|
||||
Tty: execOptions.Tty,
|
||||
ConsoleSize: execOptions.ConsoleSize,
|
||||
var cs client.ConsoleSize
|
||||
if execOptions.ConsoleSize != nil {
|
||||
cs = client.ConsoleSize{
|
||||
Height: execOptions.ConsoleSize[0],
|
||||
Width: execOptions.ConsoleSize[1],
|
||||
}
|
||||
}
|
||||
resp, err := apiClient.ExecAttach(ctx, execID, client.ExecAttachOptions{
|
||||
TTY: execOptions.Tty,
|
||||
ConsoleSize: cs,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
@ -176,7 +192,7 @@ func interactiveExec(ctx context.Context, dockerCli command.Cli, execOptions *co
|
||||
inputStream: in,
|
||||
outputStream: out,
|
||||
errorStream: stderr,
|
||||
resp: resp,
|
||||
resp: resp.HijackedResponse,
|
||||
tty: execOptions.Tty,
|
||||
detachKeys: execOptions.DetachKeys,
|
||||
}
|
||||
@ -200,7 +216,7 @@ func interactiveExec(ctx context.Context, dockerCli command.Cli, execOptions *co
|
||||
}
|
||||
|
||||
func getExecExitStatus(ctx context.Context, apiClient client.ContainerAPIClient, execID string) error {
|
||||
resp, err := apiClient.ContainerExecInspect(ctx, execID)
|
||||
resp, err := apiClient.ExecInspect(ctx, execID, client.ExecInspectOptions{})
|
||||
if err != nil {
|
||||
// If we can't connect, then the daemon probably died.
|
||||
if !client.IsErrConnectionFailed(err) {
|
||||
@ -217,8 +233,8 @@ func getExecExitStatus(ctx context.Context, apiClient client.ContainerAPIClient,
|
||||
|
||||
// parseExec parses the specified args for the specified command and generates
|
||||
// an ExecConfig from it.
|
||||
func parseExec(execOpts ExecOptions, configFile *configfile.ConfigFile) (*container.ExecOptions, error) {
|
||||
execOptions := &container.ExecOptions{
|
||||
func parseExec(execOpts ExecOptions, configFile *configfile.ConfigFile) (*client.ExecCreateOptions, error) {
|
||||
execOptions := &client.ExecCreateOptions{
|
||||
User: execOpts.User,
|
||||
Privileged: execOpts.Privileged,
|
||||
Tty: execOpts.TTY,
|
||||
|
||||
@ -12,7 +12,7 @@ import (
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/moby/moby/client"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
"gotest.tools/v3/fs"
|
||||
@ -38,10 +38,10 @@ TWO=2
|
||||
testcases := []struct {
|
||||
options ExecOptions
|
||||
configFile configfile.ConfigFile
|
||||
expected container.ExecOptions
|
||||
expected client.ExecCreateOptions
|
||||
}{
|
||||
{
|
||||
expected: container.ExecOptions{
|
||||
expected: client.ExecCreateOptions{
|
||||
Cmd: []string{"command"},
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
@ -49,7 +49,7 @@ TWO=2
|
||||
options: withDefaultOpts(ExecOptions{}),
|
||||
},
|
||||
{
|
||||
expected: container.ExecOptions{
|
||||
expected: client.ExecCreateOptions{
|
||||
Cmd: []string{"command1", "command2"},
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
@ -64,7 +64,7 @@ TWO=2
|
||||
TTY: true,
|
||||
User: "uid",
|
||||
}),
|
||||
expected: container.ExecOptions{
|
||||
expected: client.ExecCreateOptions{
|
||||
User: "uid",
|
||||
AttachStdin: true,
|
||||
AttachStdout: true,
|
||||
@ -75,7 +75,7 @@ TWO=2
|
||||
},
|
||||
{
|
||||
options: withDefaultOpts(ExecOptions{Detach: true}),
|
||||
expected: container.ExecOptions{
|
||||
expected: client.ExecCreateOptions{
|
||||
Cmd: []string{"command"},
|
||||
},
|
||||
},
|
||||
@ -85,7 +85,7 @@ TWO=2
|
||||
Interactive: true,
|
||||
Detach: true,
|
||||
}),
|
||||
expected: container.ExecOptions{
|
||||
expected: client.ExecCreateOptions{
|
||||
Tty: true,
|
||||
Cmd: []string{"command"},
|
||||
},
|
||||
@ -93,7 +93,7 @@ TWO=2
|
||||
{
|
||||
options: withDefaultOpts(ExecOptions{Detach: true}),
|
||||
configFile: configfile.ConfigFile{DetachKeys: "de"},
|
||||
expected: container.ExecOptions{
|
||||
expected: client.ExecCreateOptions{
|
||||
Cmd: []string{"command"},
|
||||
DetachKeys: "de",
|
||||
},
|
||||
@ -104,13 +104,13 @@ TWO=2
|
||||
DetachKeys: "ab",
|
||||
}),
|
||||
configFile: configfile.ConfigFile{DetachKeys: "de"},
|
||||
expected: container.ExecOptions{
|
||||
expected: client.ExecCreateOptions{
|
||||
Cmd: []string{"command"},
|
||||
DetachKeys: "ab",
|
||||
},
|
||||
},
|
||||
{
|
||||
expected: container.ExecOptions{
|
||||
expected: client.ExecCreateOptions{
|
||||
Cmd: []string{"command"},
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
@ -118,12 +118,12 @@ TWO=2
|
||||
},
|
||||
options: func() ExecOptions {
|
||||
o := withDefaultOpts(ExecOptions{})
|
||||
o.EnvFile.Set(tmpFile.Path())
|
||||
_ = o.EnvFile.Set(tmpFile.Path())
|
||||
return o
|
||||
}(),
|
||||
},
|
||||
{
|
||||
expected: container.ExecOptions{
|
||||
expected: client.ExecCreateOptions{
|
||||
Cmd: []string{"command"},
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
@ -131,8 +131,8 @@ TWO=2
|
||||
},
|
||||
options: func() ExecOptions {
|
||||
o := withDefaultOpts(ExecOptions{})
|
||||
o.EnvFile.Set(tmpFile.Path())
|
||||
o.Env.Set("ONE=override")
|
||||
_ = o.EnvFile.Set(tmpFile.Path())
|
||||
_ = o.Env.Set("ONE=override")
|
||||
return o
|
||||
}(),
|
||||
},
|
||||
@ -149,7 +149,7 @@ TWO=2
|
||||
|
||||
func TestParseExecNoSuchFile(t *testing.T) {
|
||||
execOpts := withDefaultOpts(ExecOptions{})
|
||||
execOpts.EnvFile.Set("no-such-env-file")
|
||||
assert.Check(t, execOpts.EnvFile.Set("no-such-env-file"))
|
||||
execConfig, err := parseExec(execOpts, &configfile.ConfigFile{})
|
||||
assert.ErrorContains(t, err, "no-such-env-file")
|
||||
assert.Check(t, os.IsNotExist(err))
|
||||
@ -176,8 +176,8 @@ func TestRunExec(t *testing.T) {
|
||||
doc: "inspect error",
|
||||
options: NewExecOptions(),
|
||||
client: &fakeClient{
|
||||
inspectFunc: func(string) (container.InspectResponse, error) {
|
||||
return container.InspectResponse{}, errors.New("failed inspect")
|
||||
inspectFunc: func(string) (client.ContainerInspectResult, error) {
|
||||
return client.ContainerInspectResult{}, errors.New("failed inspect")
|
||||
},
|
||||
},
|
||||
expectedError: "failed inspect",
|
||||
@ -194,7 +194,7 @@ func TestRunExec(t *testing.T) {
|
||||
t.Run(testcase.doc, func(t *testing.T) {
|
||||
fakeCLI := test.NewFakeCli(testcase.client)
|
||||
|
||||
err := RunExec(context.TODO(), fakeCLI, "thecontainer", testcase.options)
|
||||
err := RunExec(context.TODO(), fakeCLI, "the-container", testcase.options)
|
||||
if testcase.expectedError != "" {
|
||||
assert.ErrorContains(t, err, testcase.expectedError)
|
||||
} else if !assert.Check(t, err) {
|
||||
@ -206,8 +206,8 @@ func TestRunExec(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func execCreateWithID(_ string, _ container.ExecOptions) (container.ExecCreateResponse, error) {
|
||||
return container.ExecCreateResponse{ID: "execid"}, nil
|
||||
func execCreateWithID(_ string, _ client.ExecCreateOptions) (client.ExecCreateResult, error) {
|
||||
return client.ExecCreateResult{ID: "exec-id"}, nil
|
||||
}
|
||||
|
||||
func TestGetExecExitStatus(t *testing.T) {
|
||||
@ -234,13 +234,13 @@ func TestGetExecExitStatus(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, testcase := range testcases {
|
||||
client := &fakeClient{
|
||||
execInspectFunc: func(id string) (container.ExecInspect, error) {
|
||||
apiClient := &fakeClient{
|
||||
execInspectFunc: func(id string) (client.ExecInspectResult, error) {
|
||||
assert.Check(t, is.Equal(execID, id))
|
||||
return container.ExecInspect{ExitCode: testcase.exitCode}, testcase.inspectError
|
||||
return client.ExecInspectResult{ExitCode: testcase.exitCode}, testcase.inspectError
|
||||
},
|
||||
}
|
||||
err := getExecExitStatus(context.Background(), client, execID)
|
||||
err := getExecExitStatus(context.Background(), apiClient, execID)
|
||||
assert.Check(t, is.Equal(testcase.expectedError, err))
|
||||
}
|
||||
}
|
||||
@ -250,20 +250,20 @@ func TestNewExecCommandErrors(t *testing.T) {
|
||||
name string
|
||||
args []string
|
||||
expectedError string
|
||||
containerInspectFunc func(img string) (container.InspectResponse, error)
|
||||
containerInspectFunc func(img string) (client.ContainerInspectResult, error)
|
||||
}{
|
||||
{
|
||||
name: "client-error",
|
||||
args: []string{"5cb5bb5e4a3b", "-t", "-i", "bash"},
|
||||
expectedError: "something went wrong",
|
||||
containerInspectFunc: func(containerID string) (container.InspectResponse, error) {
|
||||
return container.InspectResponse{}, errors.New("something went wrong")
|
||||
containerInspectFunc: func(containerID string) (client.ContainerInspectResult, error) {
|
||||
return client.ContainerInspectResult{}, errors.New("something went wrong")
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
fakeCLI := test.NewFakeCli(&fakeClient{inspectFunc: tc.containerInspectFunc})
|
||||
cmd := NewExecCommand(fakeCLI)
|
||||
cmd := newExecCommand(fakeCLI)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
|
||||
@ -2,13 +2,15 @@ package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/moby/sys/atomicwriter"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@ -17,8 +19,8 @@ type exportOptions struct {
|
||||
output string
|
||||
}
|
||||
|
||||
// NewExportCommand creates a new `docker export` command
|
||||
func NewExportCommand(dockerCli command.Cli) *cobra.Command {
|
||||
// newExportCommand creates a new "docker container export" command.
|
||||
func newExportCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
var opts exportOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
@ -27,12 +29,13 @@ func NewExportCommand(dockerCli command.Cli) *cobra.Command {
|
||||
Args: cli.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.container = args[0]
|
||||
return runExport(cmd.Context(), dockerCli, opts)
|
||||
return runExport(cmd.Context(), dockerCLI, opts)
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"aliases": "docker container export, docker export",
|
||||
},
|
||||
ValidArgsFunction: completion.ContainerNames(dockerCli, true),
|
||||
ValidArgsFunction: completion.ContainerNames(dockerCLI, true),
|
||||
DisableFlagsInUseLine: true,
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
@ -52,13 +55,13 @@ func runExport(ctx context.Context, dockerCLI command.Cli, opts exportOptions) e
|
||||
} else {
|
||||
writer, err := atomicwriter.New(opts.output, 0o600)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to export container")
|
||||
return fmt.Errorf("failed to export container: %w", err)
|
||||
}
|
||||
defer writer.Close()
|
||||
output = writer
|
||||
}
|
||||
|
||||
responseBody, err := dockerCLI.Client().ContainerExport(ctx, opts.container)
|
||||
responseBody, err := dockerCLI.Client().ContainerExport(ctx, opts.container, client.ContainerExportOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -2,10 +2,10 @@ package container
|
||||
|
||||
import (
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/moby/moby/client"
|
||||
"gotest.tools/v3/assert"
|
||||
"gotest.tools/v3/fs"
|
||||
)
|
||||
@ -15,11 +15,12 @@ func TestContainerExportOutputToFile(t *testing.T) {
|
||||
defer dir.Remove()
|
||||
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
containerExportFunc: func(container string) (io.ReadCloser, error) {
|
||||
return io.NopCloser(strings.NewReader("bar")), nil
|
||||
containerExportFunc: func(container string) (client.ContainerExportResult, error) {
|
||||
// FIXME(thaJeztah): how to mock this?
|
||||
return mockContainerExportResult("bar"), nil
|
||||
},
|
||||
})
|
||||
cmd := NewExportCommand(cli)
|
||||
cmd := newExportCommand(cli)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetArgs([]string{"-o", dir.Join("foo"), "container"})
|
||||
assert.NilError(t, cmd.Execute())
|
||||
@ -33,11 +34,12 @@ func TestContainerExportOutputToFile(t *testing.T) {
|
||||
|
||||
func TestContainerExportOutputToIrregularFile(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
containerExportFunc: func(container string) (io.ReadCloser, error) {
|
||||
return io.NopCloser(strings.NewReader("foo")), nil
|
||||
containerExportFunc: func(container string) (client.ContainerExportResult, error) {
|
||||
// FIXME(thaJeztah): how to mock this?
|
||||
return mockContainerExportResult("foo"), nil
|
||||
},
|
||||
})
|
||||
cmd := NewExportCommand(cli)
|
||||
cmd := newExportCommand(cli)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
cmd.SetArgs([]string{"-o", "/dev/random", "container"})
|
||||
|
||||
@ -2,7 +2,8 @@ package container
|
||||
|
||||
import (
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/client"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -12,25 +13,24 @@ const (
|
||||
pathHeader = "PATH"
|
||||
)
|
||||
|
||||
// NewDiffFormat returns a format for use with a diff Context
|
||||
func NewDiffFormat(source string) formatter.Format {
|
||||
// newDiffFormat returns a format for use with a diff [formatter.Context].
|
||||
func newDiffFormat(source string) formatter.Format {
|
||||
if source == formatter.TableFormatKey {
|
||||
return defaultDiffTableFormat
|
||||
}
|
||||
return formatter.Format(source)
|
||||
}
|
||||
|
||||
// DiffFormatWrite writes formatted diff using the Context
|
||||
func DiffFormatWrite(ctx formatter.Context, changes []container.FilesystemChange) error {
|
||||
render := func(format func(subContext formatter.SubContext) error) error {
|
||||
for _, change := range changes {
|
||||
// diffFormatWrite writes formatted diff using the [formatter.Context].
|
||||
func diffFormatWrite(fmtCtx formatter.Context, changes client.ContainerDiffResult) error {
|
||||
return fmtCtx.Write(newDiffContext(), func(format func(subContext formatter.SubContext) error) error {
|
||||
for _, change := range changes.Changes {
|
||||
if err := format(&diffContext{c: change}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return ctx.Write(newDiffContext(), render)
|
||||
})
|
||||
}
|
||||
|
||||
type diffContext struct {
|
||||
@ -39,12 +39,14 @@ type diffContext struct {
|
||||
}
|
||||
|
||||
func newDiffContext() *diffContext {
|
||||
diffCtx := diffContext{}
|
||||
diffCtx.Header = formatter.SubHeaderContext{
|
||||
"Type": changeTypeHeader,
|
||||
"Path": pathHeader,
|
||||
return &diffContext{
|
||||
HeaderContext: formatter.HeaderContext{
|
||||
Header: formatter.SubHeaderContext{
|
||||
"Type": changeTypeHeader,
|
||||
"Path": pathHeader,
|
||||
},
|
||||
},
|
||||
}
|
||||
return &diffCtx
|
||||
}
|
||||
|
||||
func (d *diffContext) MarshalJSON() ([]byte, error) {
|
||||
|
||||
@ -5,7 +5,8 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/client"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
@ -16,7 +17,7 @@ func TestDiffContextFormatWrite(t *testing.T) {
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
formatter.Context{Format: NewDiffFormat("table")},
|
||||
formatter.Context{Format: newDiffFormat("table")},
|
||||
`CHANGE TYPE PATH
|
||||
C /var/log/app.log
|
||||
A /usr/app/app.js
|
||||
@ -24,7 +25,7 @@ D /usr/app/old_app.js
|
||||
`,
|
||||
},
|
||||
{
|
||||
formatter.Context{Format: NewDiffFormat("table {{.Path}}")},
|
||||
formatter.Context{Format: newDiffFormat("table {{.Path}}")},
|
||||
`PATH
|
||||
/var/log/app.log
|
||||
/usr/app/app.js
|
||||
@ -32,7 +33,7 @@ D /usr/app/old_app.js
|
||||
`,
|
||||
},
|
||||
{
|
||||
formatter.Context{Format: NewDiffFormat("{{.Type}}: {{.Path}}")},
|
||||
formatter.Context{Format: newDiffFormat("{{.Type}}: {{.Path}}")},
|
||||
`C: /var/log/app.log
|
||||
A: /usr/app/app.js
|
||||
D: /usr/app/old_app.js
|
||||
@ -40,17 +41,19 @@ D: /usr/app/old_app.js
|
||||
},
|
||||
}
|
||||
|
||||
diffs := []container.FilesystemChange{
|
||||
{Kind: container.ChangeModify, Path: "/var/log/app.log"},
|
||||
{Kind: container.ChangeAdd, Path: "/usr/app/app.js"},
|
||||
{Kind: container.ChangeDelete, Path: "/usr/app/old_app.js"},
|
||||
diffs := client.ContainerDiffResult{
|
||||
Changes: []container.FilesystemChange{
|
||||
{Kind: container.ChangeModify, Path: "/var/log/app.log"},
|
||||
{Kind: container.ChangeAdd, Path: "/usr/app/app.js"},
|
||||
{Kind: container.ChangeDelete, Path: "/usr/app/old_app.js"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(string(tc.context.Format), func(t *testing.T) {
|
||||
out := bytes.NewBufferString("")
|
||||
tc.context.Output = out
|
||||
err := DiffFormatWrite(tc.context, diffs)
|
||||
err := diffFormatWrite(tc.context, diffs)
|
||||
if err != nil {
|
||||
assert.Error(t, err, tc.expected)
|
||||
} else {
|
||||
|
||||
@ -167,6 +167,7 @@ func (c *statsContext) Container() string {
|
||||
}
|
||||
|
||||
func (c *statsContext) Name() string {
|
||||
// TODO(thaJeztah): make this explicitly trim the "/" prefix, not just any char.
|
||||
if len(c.s.Name) > 1 {
|
||||
return c.s.Name[1:]
|
||||
}
|
||||
|
||||
@ -5,45 +5,181 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/docker/cli/internal/test"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
|
||||
func TestContainerStatsContext(t *testing.T) {
|
||||
containerID := test.RandomID()
|
||||
const actorID = "c74518277ddc15a6afeaaeb06ee5f7433dcb27188224777c1efa7df1e8766d65"
|
||||
|
||||
var ctx statsContext
|
||||
tt := []struct {
|
||||
tests := []struct {
|
||||
name string
|
||||
stats StatsEntry
|
||||
osType string
|
||||
expValue string
|
||||
expHeader string
|
||||
call func() string
|
||||
}{
|
||||
{StatsEntry{Container: containerID}, "", containerID, containerHeader, ctx.Container},
|
||||
{StatsEntry{CPUPercentage: 5.5}, "", "5.50%", cpuPercHeader, ctx.CPUPerc},
|
||||
{StatsEntry{CPUPercentage: 5.5, IsInvalid: true}, "", "--", cpuPercHeader, ctx.CPUPerc},
|
||||
{StatsEntry{NetworkRx: 0.31, NetworkTx: 12.3}, "", "0.31B / 12.3B", netIOHeader, ctx.NetIO},
|
||||
{StatsEntry{NetworkRx: 0.31, NetworkTx: 12.3, IsInvalid: true}, "", "--", netIOHeader, ctx.NetIO},
|
||||
{StatsEntry{BlockRead: 0.1, BlockWrite: 2.3}, "", "0.1B / 2.3B", blockIOHeader, ctx.BlockIO},
|
||||
{StatsEntry{BlockRead: 0.1, BlockWrite: 2.3, IsInvalid: true}, "", "--", blockIOHeader, ctx.BlockIO},
|
||||
{StatsEntry{MemoryPercentage: 10.2}, "", "10.20%", memPercHeader, ctx.MemPerc},
|
||||
{StatsEntry{MemoryPercentage: 10.2, IsInvalid: true}, "", "--", memPercHeader, ctx.MemPerc},
|
||||
{StatsEntry{MemoryPercentage: 10.2}, "windows", "--", memPercHeader, ctx.MemPerc},
|
||||
{StatsEntry{Memory: 24, MemoryLimit: 30}, "", "24B / 30B", memUseHeader, ctx.MemUsage},
|
||||
{StatsEntry{Memory: 24, MemoryLimit: 30, IsInvalid: true}, "", "-- / --", memUseHeader, ctx.MemUsage},
|
||||
{StatsEntry{Memory: 24, MemoryLimit: 30}, "windows", "24B", winMemUseHeader, ctx.MemUsage},
|
||||
{StatsEntry{PidsCurrent: 10}, "", "10", pidsHeader, ctx.PIDs},
|
||||
{StatsEntry{PidsCurrent: 10, IsInvalid: true}, "", "--", pidsHeader, ctx.PIDs},
|
||||
{StatsEntry{PidsCurrent: 10}, "windows", "--", pidsHeader, ctx.PIDs},
|
||||
{
|
||||
name: "Container id",
|
||||
stats: StatsEntry{ID: actorID, Container: actorID},
|
||||
expValue: actorID,
|
||||
expHeader: containerHeader,
|
||||
call: ctx.Container,
|
||||
},
|
||||
{
|
||||
name: "Container name",
|
||||
stats: StatsEntry{ID: actorID, Container: "a-long-container-name"},
|
||||
expValue: "a-long-container-name",
|
||||
expHeader: containerHeader,
|
||||
call: ctx.Container,
|
||||
},
|
||||
{
|
||||
name: "ID",
|
||||
stats: StatsEntry{ID: actorID},
|
||||
expValue: actorID,
|
||||
expHeader: formatter.ContainerIDHeader,
|
||||
call: ctx.ID,
|
||||
},
|
||||
{
|
||||
name: "Name",
|
||||
stats: StatsEntry{Name: "/container-name"},
|
||||
expValue: "container-name",
|
||||
expHeader: formatter.ContainerIDHeader,
|
||||
call: ctx.Name,
|
||||
},
|
||||
{
|
||||
name: "Name empty",
|
||||
stats: StatsEntry{Name: ""},
|
||||
expValue: "--",
|
||||
expHeader: formatter.ContainerIDHeader,
|
||||
call: ctx.Name,
|
||||
},
|
||||
{
|
||||
name: "Name prefix only",
|
||||
stats: StatsEntry{Name: "/"},
|
||||
expValue: "--",
|
||||
expHeader: formatter.ContainerIDHeader,
|
||||
call: ctx.Name,
|
||||
},
|
||||
{
|
||||
name: "CPUPerc",
|
||||
stats: StatsEntry{CPUPercentage: 5.5},
|
||||
expValue: "5.50%",
|
||||
expHeader: cpuPercHeader,
|
||||
call: ctx.CPUPerc,
|
||||
},
|
||||
{
|
||||
name: "CPUPerc invalid",
|
||||
stats: StatsEntry{CPUPercentage: 5.5, IsInvalid: true},
|
||||
expValue: "--",
|
||||
expHeader: cpuPercHeader,
|
||||
call: ctx.CPUPerc,
|
||||
},
|
||||
{
|
||||
name: "NetIO",
|
||||
stats: StatsEntry{NetworkRx: 0.31, NetworkTx: 12.3},
|
||||
expValue: "0.31B / 12.3B",
|
||||
expHeader: netIOHeader,
|
||||
call: ctx.NetIO,
|
||||
},
|
||||
{
|
||||
name: "NetIO invalid",
|
||||
stats: StatsEntry{NetworkRx: 0.31, NetworkTx: 12.3, IsInvalid: true},
|
||||
expValue: "--",
|
||||
expHeader: netIOHeader,
|
||||
call: ctx.NetIO,
|
||||
},
|
||||
{
|
||||
name: "BlockIO",
|
||||
stats: StatsEntry{BlockRead: 0.1, BlockWrite: 2.3},
|
||||
expValue: "0.1B / 2.3B",
|
||||
expHeader: blockIOHeader,
|
||||
call: ctx.BlockIO,
|
||||
},
|
||||
{
|
||||
name: "BlockIO invalid",
|
||||
stats: StatsEntry{BlockRead: 0.1, BlockWrite: 2.3, IsInvalid: true},
|
||||
expValue: "--",
|
||||
expHeader: blockIOHeader,
|
||||
call: ctx.BlockIO,
|
||||
},
|
||||
{
|
||||
name: "MemPerc",
|
||||
stats: StatsEntry{MemoryPercentage: 10.2},
|
||||
expValue: "10.20%",
|
||||
expHeader: memPercHeader,
|
||||
call: ctx.MemPerc,
|
||||
},
|
||||
{
|
||||
name: "MemPerc invalid",
|
||||
stats: StatsEntry{MemoryPercentage: 10.2, IsInvalid: true},
|
||||
expValue: "--",
|
||||
expHeader: memPercHeader,
|
||||
call: ctx.MemPerc,
|
||||
},
|
||||
{
|
||||
name: "MemPerc windows",
|
||||
stats: StatsEntry{MemoryPercentage: 10.2},
|
||||
osType: "windows",
|
||||
expValue: "--",
|
||||
expHeader: memPercHeader,
|
||||
call: ctx.MemPerc,
|
||||
},
|
||||
{
|
||||
name: "MemUsage",
|
||||
stats: StatsEntry{Memory: 24, MemoryLimit: 30},
|
||||
expValue: "24B / 30B",
|
||||
expHeader: memUseHeader,
|
||||
call: ctx.MemUsage,
|
||||
},
|
||||
{
|
||||
name: "MemUsage invalid",
|
||||
stats: StatsEntry{Memory: 24, MemoryLimit: 30, IsInvalid: true},
|
||||
expValue: "-- / --",
|
||||
expHeader: memUseHeader,
|
||||
call: ctx.MemUsage,
|
||||
},
|
||||
{
|
||||
name: "MemUsage windows",
|
||||
stats: StatsEntry{Memory: 24, MemoryLimit: 30},
|
||||
osType: "windows",
|
||||
expValue: "24B",
|
||||
expHeader: winMemUseHeader,
|
||||
call: ctx.MemUsage,
|
||||
},
|
||||
{
|
||||
name: "PIDs",
|
||||
stats: StatsEntry{PidsCurrent: 10},
|
||||
expValue: "10",
|
||||
expHeader: pidsHeader,
|
||||
call: ctx.PIDs,
|
||||
},
|
||||
{
|
||||
name: "PIDs invalid",
|
||||
stats: StatsEntry{PidsCurrent: 10, IsInvalid: true},
|
||||
expValue: "--",
|
||||
expHeader: pidsHeader,
|
||||
call: ctx.PIDs,
|
||||
},
|
||||
{
|
||||
name: "PIDs windows",
|
||||
stats: StatsEntry{PidsCurrent: 10},
|
||||
osType: "windows",
|
||||
expValue: "--",
|
||||
expHeader: pidsHeader,
|
||||
call: ctx.PIDs,
|
||||
},
|
||||
}
|
||||
|
||||
for _, te := range tt {
|
||||
ctx = statsContext{s: te.stats, os: te.osType}
|
||||
if v := te.call(); v != te.expValue {
|
||||
t.Fatalf("Expected %q, got %q", te.expValue, v)
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
ctx = statsContext{s: tc.stats, os: tc.osType}
|
||||
if v := tc.call(); v != tc.expValue {
|
||||
t.Fatalf("Expected %q, got %q", tc.expValue, v)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -8,8 +8,8 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/pkg/stdcopy"
|
||||
"github.com/moby/moby/api/pkg/stdcopy"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/moby/term"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
@ -38,7 +38,7 @@ type hijackedIOStreamer struct {
|
||||
outputStream io.Writer
|
||||
errorStream io.Writer
|
||||
|
||||
resp types.HijackedResponse
|
||||
resp client.HijackedResponse
|
||||
|
||||
tty bool
|
||||
detachKeys string
|
||||
|
||||
@ -11,6 +11,7 @@ import (
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/docker/cli/cli/command/inspect"
|
||||
flagsHelper "github.com/docker/cli/cli/flags"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@ -21,7 +22,7 @@ type inspectOptions struct {
|
||||
}
|
||||
|
||||
// newInspectCommand creates a new cobra.Command for `docker container inspect`
|
||||
func newInspectCommand(dockerCli command.Cli) *cobra.Command {
|
||||
func newInspectCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
var opts inspectOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
@ -30,9 +31,10 @@ func newInspectCommand(dockerCli command.Cli) *cobra.Command {
|
||||
Args: cli.RequiresMinArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.refs = args
|
||||
return runInspect(cmd.Context(), dockerCli, opts)
|
||||
return runInspect(cmd.Context(), dockerCLI, opts)
|
||||
},
|
||||
ValidArgsFunction: completion.ContainerNames(dockerCli, true),
|
||||
ValidArgsFunction: completion.ContainerNames(dockerCLI, true),
|
||||
DisableFlagsInUseLine: true,
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
@ -45,6 +47,10 @@ func newInspectCommand(dockerCli command.Cli) *cobra.Command {
|
||||
func runInspect(ctx context.Context, dockerCLI command.Cli, opts inspectOptions) error {
|
||||
apiClient := dockerCLI.Client()
|
||||
return inspect.Inspect(dockerCLI.Out(), opts.refs, opts.format, func(ref string) (any, []byte, error) {
|
||||
return apiClient.ContainerInspectWithRaw(ctx, ref, opts.size)
|
||||
res, err := apiClient.ContainerInspect(ctx, ref, client.ContainerInspectOptions{Size: opts.size})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return &res.Container, res.Raw, nil
|
||||
})
|
||||
}
|
||||
|
||||
@ -8,6 +8,7 @@ import (
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@ -17,8 +18,8 @@ type killOptions struct {
|
||||
containers []string
|
||||
}
|
||||
|
||||
// NewKillCommand creates a new cobra.Command for `docker kill`
|
||||
func NewKillCommand(dockerCli command.Cli) *cobra.Command {
|
||||
// newKillCommand creates a new cobra.Command for "docker container kill"
|
||||
func newKillCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
var opts killOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
@ -27,12 +28,13 @@ func NewKillCommand(dockerCli command.Cli) *cobra.Command {
|
||||
Args: cli.RequiresMinArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.containers = args
|
||||
return runKill(cmd.Context(), dockerCli, &opts)
|
||||
return runKill(cmd.Context(), dockerCLI, &opts)
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"aliases": "docker container kill, docker kill",
|
||||
},
|
||||
ValidArgsFunction: completion.ContainerNames(dockerCli, false),
|
||||
ValidArgsFunction: completion.ContainerNames(dockerCLI, false),
|
||||
DisableFlagsInUseLine: true,
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
@ -46,7 +48,10 @@ func NewKillCommand(dockerCli command.Cli) *cobra.Command {
|
||||
func runKill(ctx context.Context, dockerCLI command.Cli, opts *killOptions) error {
|
||||
apiClient := dockerCLI.Client()
|
||||
errChan := parallelOperation(ctx, opts.containers, func(ctx context.Context, container string) error {
|
||||
return apiClient.ContainerKill(ctx, container, opts.signal)
|
||||
_, err := apiClient.ContainerKill(ctx, container, client.ContainerKillOptions{
|
||||
Signal: opts.signal,
|
||||
})
|
||||
return err
|
||||
})
|
||||
|
||||
var errs []error
|
||||
|
||||
@ -8,23 +8,20 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/moby/moby/client"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
|
||||
func TestRunKill(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
containerKillFunc: func(
|
||||
ctx context.Context,
|
||||
container string,
|
||||
signal string,
|
||||
) error {
|
||||
assert.Assert(t, is.Equal(signal, "STOP"))
|
||||
return nil
|
||||
containerKillFunc: func(ctx context.Context, container string, options client.ContainerKillOptions) (client.ContainerKillResult, error) {
|
||||
assert.Assert(t, is.Equal(options.Signal, "STOP"))
|
||||
return client.ContainerKillResult{}, nil
|
||||
},
|
||||
})
|
||||
|
||||
cmd := NewKillCommand(cli)
|
||||
cmd := newKillCommand(cli)
|
||||
cmd.SetOut(io.Discard)
|
||||
|
||||
cmd.SetArgs([]string{
|
||||
@ -47,16 +44,12 @@ func TestRunKill(t *testing.T) {
|
||||
|
||||
func TestRunKillClientError(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
containerKillFunc: func(
|
||||
ctx context.Context,
|
||||
container string,
|
||||
signal string,
|
||||
) error {
|
||||
return fmt.Errorf("client error for container %s", container)
|
||||
containerKillFunc: func(ctx context.Context, container string, options client.ContainerKillOptions) (client.ContainerKillResult, error) {
|
||||
return client.ContainerKillResult{}, fmt.Errorf("client error for container %s", container)
|
||||
},
|
||||
})
|
||||
|
||||
cmd := NewKillCommand(cli)
|
||||
cmd := newKillCommand(cli)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
|
||||
|
||||
@ -2,17 +2,16 @@ package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
flagsHelper "github.com/docker/cli/cli/flags"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/cli/templates"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@ -28,8 +27,8 @@ type psOptions struct {
|
||||
filter opts.FilterOpt
|
||||
}
|
||||
|
||||
// NewPsCommand creates a new cobra.Command for `docker ps`
|
||||
func NewPsCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
// newPsCommand creates a new cobra.Command for "docker container ps"
|
||||
func newPsCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
options := psOptions{filter: opts.NewFilterOpt()}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
@ -44,7 +43,8 @@ func NewPsCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
"category-top": "3",
|
||||
"aliases": "docker container ls, docker container list, docker container ps, docker ps",
|
||||
},
|
||||
ValidArgsFunction: completion.NoComplete,
|
||||
ValidArgsFunction: cobra.NoFileCompletions,
|
||||
DisableFlagsInUseLine: true,
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
@ -62,14 +62,14 @@ func NewPsCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
}
|
||||
|
||||
func newListCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
cmd := *NewPsCommand(dockerCLI)
|
||||
cmd := *newPsCommand(dockerCLI)
|
||||
cmd.Aliases = []string{"ps", "list"}
|
||||
cmd.Use = "ls [OPTIONS]"
|
||||
return &cmd
|
||||
}
|
||||
|
||||
func buildContainerListOptions(options *psOptions) (*container.ListOptions, error) {
|
||||
listOptions := &container.ListOptions{
|
||||
func buildContainerListOptions(options *psOptions) (client.ContainerListOptions, error) {
|
||||
listOptions := client.ContainerListOptions{
|
||||
All: options.all,
|
||||
Limit: options.last,
|
||||
Size: options.size,
|
||||
@ -82,9 +82,9 @@ func buildContainerListOptions(options *psOptions) (*container.ListOptions, erro
|
||||
|
||||
// always validate template when `--format` is used, for consistency
|
||||
if len(options.format) > 0 {
|
||||
tmpl, err := templates.NewParse("", options.format)
|
||||
tmpl, err := templates.Parse(options.format)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to parse template")
|
||||
return client.ContainerListOptions{}, fmt.Errorf("failed to parse template: %w", err)
|
||||
}
|
||||
|
||||
optionsProcessor := formatter.NewContainerContext()
|
||||
@ -92,7 +92,7 @@ func buildContainerListOptions(options *psOptions) (*container.ListOptions, erro
|
||||
// This shouldn't error out but swallowing the error makes it harder
|
||||
// to track down if preProcessor issues come up.
|
||||
if err := tmpl.Execute(io.Discard, optionsProcessor); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to execute template")
|
||||
return client.ContainerListOptions{}, fmt.Errorf("failed to execute template: %w", err)
|
||||
}
|
||||
|
||||
// if `size` was not explicitly set to false (with `--size=false`)
|
||||
@ -127,7 +127,7 @@ func runPs(ctx context.Context, dockerCLI command.Cli, options *psOptions) error
|
||||
return err
|
||||
}
|
||||
|
||||
containers, err := dockerCLI.Client().ContainerList(ctx, *listOptions)
|
||||
res, err := dockerCLI.Client().ContainerList(ctx, listOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -137,5 +137,5 @@ func runPs(ctx context.Context, dockerCLI command.Cli, options *psOptions) error
|
||||
Format: formatter.NewContainerFormat(options.format, options.quiet, listOptions.Size),
|
||||
Trunc: !options.noTrunc,
|
||||
}
|
||||
return formatter.ContainerWrite(containerCtx, containers)
|
||||
return formatter.ContainerWrite(containerCtx, res.Items)
|
||||
}
|
||||
|
||||
@ -9,7 +9,8 @@ import (
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/cli/internal/test/builders"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/client"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
"gotest.tools/v3/golden"
|
||||
@ -25,7 +26,7 @@ func TestContainerListBuildContainerListOptions(t *testing.T) {
|
||||
expectedAll bool
|
||||
expectedSize bool
|
||||
expectedLimit int
|
||||
expectedFilters map[string]string
|
||||
expectedFilters client.Filters
|
||||
}{
|
||||
{
|
||||
psOpts: &psOptions{
|
||||
@ -34,13 +35,10 @@ func TestContainerListBuildContainerListOptions(t *testing.T) {
|
||||
last: 5,
|
||||
filter: filters,
|
||||
},
|
||||
expectedAll: true,
|
||||
expectedSize: true,
|
||||
expectedLimit: 5,
|
||||
expectedFilters: map[string]string{
|
||||
"foo": "bar",
|
||||
"baz": "foo",
|
||||
},
|
||||
expectedAll: true,
|
||||
expectedSize: true,
|
||||
expectedLimit: 5,
|
||||
expectedFilters: make(client.Filters).Add("foo", "bar").Add("baz", "foo"),
|
||||
},
|
||||
{
|
||||
psOpts: &psOptions{
|
||||
@ -49,10 +47,9 @@ func TestContainerListBuildContainerListOptions(t *testing.T) {
|
||||
last: -1,
|
||||
nLatest: true,
|
||||
},
|
||||
expectedAll: true,
|
||||
expectedSize: true,
|
||||
expectedLimit: 1,
|
||||
expectedFilters: make(map[string]string),
|
||||
expectedAll: true,
|
||||
expectedSize: true,
|
||||
expectedLimit: 1,
|
||||
},
|
||||
{
|
||||
psOpts: &psOptions{
|
||||
@ -63,13 +60,10 @@ func TestContainerListBuildContainerListOptions(t *testing.T) {
|
||||
// With .Size, size should be true
|
||||
format: "{{.Size}}",
|
||||
},
|
||||
expectedAll: true,
|
||||
expectedSize: true,
|
||||
expectedLimit: 5,
|
||||
expectedFilters: map[string]string{
|
||||
"foo": "bar",
|
||||
"baz": "foo",
|
||||
},
|
||||
expectedAll: true,
|
||||
expectedSize: true,
|
||||
expectedLimit: 5,
|
||||
expectedFilters: make(client.Filters).Add("foo", "bar").Add("baz", "foo"),
|
||||
},
|
||||
{
|
||||
psOpts: &psOptions{
|
||||
@ -80,13 +74,10 @@ func TestContainerListBuildContainerListOptions(t *testing.T) {
|
||||
// With .Size, size should be true
|
||||
format: "{{.Size}} {{.CreatedAt}} {{upper .Networks}}",
|
||||
},
|
||||
expectedAll: true,
|
||||
expectedSize: true,
|
||||
expectedLimit: 5,
|
||||
expectedFilters: map[string]string{
|
||||
"foo": "bar",
|
||||
"baz": "foo",
|
||||
},
|
||||
expectedAll: true,
|
||||
expectedSize: true,
|
||||
expectedLimit: 5,
|
||||
expectedFilters: make(client.Filters).Add("foo", "bar").Add("baz", "foo"),
|
||||
},
|
||||
{
|
||||
psOpts: &psOptions{
|
||||
@ -97,13 +88,10 @@ func TestContainerListBuildContainerListOptions(t *testing.T) {
|
||||
// Without .Size, size should be false
|
||||
format: "{{.CreatedAt}} {{.Networks}}",
|
||||
},
|
||||
expectedAll: true,
|
||||
expectedSize: false,
|
||||
expectedLimit: 5,
|
||||
expectedFilters: map[string]string{
|
||||
"foo": "bar",
|
||||
"baz": "foo",
|
||||
},
|
||||
expectedAll: true,
|
||||
expectedSize: false,
|
||||
expectedLimit: 5,
|
||||
expectedFilters: make(client.Filters).Add("foo", "bar").Add("baz", "foo"),
|
||||
},
|
||||
}
|
||||
|
||||
@ -114,21 +102,14 @@ func TestContainerListBuildContainerListOptions(t *testing.T) {
|
||||
assert.Check(t, is.Equal(c.expectedAll, options.All))
|
||||
assert.Check(t, is.Equal(c.expectedSize, options.Size))
|
||||
assert.Check(t, is.Equal(c.expectedLimit, options.Limit))
|
||||
assert.Check(t, is.Equal(len(c.expectedFilters), options.Filters.Len()))
|
||||
|
||||
for k, v := range c.expectedFilters {
|
||||
f := options.Filters
|
||||
if !f.ExactMatch(k, v) {
|
||||
t.Fatalf("Expected filter with key %s to be %s but got %s", k, v, f.Get(k))
|
||||
}
|
||||
}
|
||||
assert.Check(t, is.DeepEqual(c.expectedFilters, options.Filters))
|
||||
}
|
||||
}
|
||||
|
||||
func TestContainerListErrors(t *testing.T) {
|
||||
testCases := []struct {
|
||||
flags map[string]string
|
||||
containerListFunc func(container.ListOptions) ([]container.Summary, error)
|
||||
containerListFunc func(client.ContainerListOptions) (client.ContainerListResult, error)
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
@ -144,8 +125,8 @@ func TestContainerListErrors(t *testing.T) {
|
||||
expectedError: `wrong number of args for join`,
|
||||
},
|
||||
{
|
||||
containerListFunc: func(_ container.ListOptions) ([]container.Summary, error) {
|
||||
return nil, errors.New("error listing containers")
|
||||
containerListFunc: func(_ client.ContainerListOptions) (client.ContainerListResult, error) {
|
||||
return client.ContainerListResult{}, errors.New("error listing containers")
|
||||
},
|
||||
expectedError: "error listing containers",
|
||||
},
|
||||
@ -168,13 +149,15 @@ func TestContainerListErrors(t *testing.T) {
|
||||
|
||||
func TestContainerListWithoutFormat(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
containerListFunc: func(_ container.ListOptions) ([]container.Summary, error) {
|
||||
return []container.Summary{
|
||||
*builders.Container("c1"),
|
||||
*builders.Container("c2", builders.WithName("foo")),
|
||||
*builders.Container("c3", builders.WithPort(80, 80, builders.TCP), builders.WithPort(81, 81, builders.TCP), builders.WithPort(82, 82, builders.TCP)),
|
||||
*builders.Container("c4", builders.WithPort(81, 81, builders.UDP)),
|
||||
*builders.Container("c5", builders.WithPort(82, 82, builders.IP("8.8.8.8"), builders.TCP)),
|
||||
containerListFunc: func(_ client.ContainerListOptions) (client.ContainerListResult, error) {
|
||||
return client.ContainerListResult{
|
||||
Items: []container.Summary{
|
||||
*builders.Container("c1"),
|
||||
*builders.Container("c2", builders.WithName("foo")),
|
||||
*builders.Container("c3", builders.WithPort(80, 80, builders.TCP), builders.WithPort(81, 81, builders.TCP), builders.WithPort(82, 82, builders.TCP)),
|
||||
*builders.Container("c4", builders.WithPort(81, 81, builders.UDP)),
|
||||
*builders.Container("c5", builders.WithPort(82, 82, builders.IP("8.8.8.8"), builders.TCP)),
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
@ -188,10 +171,12 @@ func TestContainerListWithoutFormat(t *testing.T) {
|
||||
|
||||
func TestContainerListNoTrunc(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
containerListFunc: func(_ container.ListOptions) ([]container.Summary, error) {
|
||||
return []container.Summary{
|
||||
*builders.Container("c1"),
|
||||
*builders.Container("c2", builders.WithName("foo/bar")),
|
||||
containerListFunc: func(_ client.ContainerListOptions) (client.ContainerListResult, error) {
|
||||
return client.ContainerListResult{
|
||||
Items: []container.Summary{
|
||||
*builders.Container("c1"),
|
||||
*builders.Container("c2", builders.WithName("foo/bar")),
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
@ -207,10 +192,12 @@ func TestContainerListNoTrunc(t *testing.T) {
|
||||
// Test for GitHub issue docker/docker#21772
|
||||
func TestContainerListNamesMultipleTime(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
containerListFunc: func(_ container.ListOptions) ([]container.Summary, error) {
|
||||
return []container.Summary{
|
||||
*builders.Container("c1"),
|
||||
*builders.Container("c2", builders.WithName("foo/bar")),
|
||||
containerListFunc: func(_ client.ContainerListOptions) (client.ContainerListResult, error) {
|
||||
return client.ContainerListResult{
|
||||
Items: []container.Summary{
|
||||
*builders.Container("c1"),
|
||||
*builders.Container("c2", builders.WithName("foo/bar")),
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
@ -226,10 +213,12 @@ func TestContainerListNamesMultipleTime(t *testing.T) {
|
||||
// Test for GitHub issue docker/docker#30291
|
||||
func TestContainerListFormatTemplateWithArg(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
containerListFunc: func(_ container.ListOptions) ([]container.Summary, error) {
|
||||
return []container.Summary{
|
||||
*builders.Container("c1", builders.WithLabel("some.label", "value")),
|
||||
*builders.Container("c2", builders.WithName("foo/bar"), builders.WithLabel("foo", "bar")),
|
||||
containerListFunc: func(_ client.ContainerListOptions) (client.ContainerListResult, error) {
|
||||
return client.ContainerListResult{
|
||||
Items: []container.Summary{
|
||||
*builders.Container("c1", builders.WithLabel("some.label", "value")),
|
||||
*builders.Container("c2", builders.WithName("foo/bar"), builders.WithLabel("foo", "bar")),
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
@ -279,9 +268,9 @@ func TestContainerListFormatSizeSetsOption(t *testing.T) {
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.doc, func(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
containerListFunc: func(options container.ListOptions) ([]container.Summary, error) {
|
||||
containerListFunc: func(options client.ContainerListOptions) (client.ContainerListResult, error) {
|
||||
assert.Check(t, is.Equal(options.Size, tc.sizeExpected))
|
||||
return []container.Summary{}, nil
|
||||
return client.ContainerListResult{}, nil
|
||||
},
|
||||
})
|
||||
cmd := newListCommand(cli)
|
||||
@ -299,10 +288,12 @@ func TestContainerListFormatSizeSetsOption(t *testing.T) {
|
||||
|
||||
func TestContainerListWithConfigFormat(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
containerListFunc: func(_ container.ListOptions) ([]container.Summary, error) {
|
||||
return []container.Summary{
|
||||
*builders.Container("c1", builders.WithLabel("some.label", "value"), builders.WithSize(10700000)),
|
||||
*builders.Container("c2", builders.WithName("foo/bar"), builders.WithLabel("foo", "bar"), builders.WithSize(3200000)),
|
||||
containerListFunc: func(_ client.ContainerListOptions) (client.ContainerListResult, error) {
|
||||
return client.ContainerListResult{
|
||||
Items: []container.Summary{
|
||||
*builders.Container("c1", builders.WithLabel("some.label", "value"), builders.WithSize(10700000)),
|
||||
*builders.Container("c2", builders.WithName("foo/bar"), builders.WithLabel("foo", "bar"), builders.WithSize(3200000)),
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
@ -319,10 +310,12 @@ func TestContainerListWithConfigFormat(t *testing.T) {
|
||||
|
||||
func TestContainerListWithFormat(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
containerListFunc: func(_ container.ListOptions) ([]container.Summary, error) {
|
||||
return []container.Summary{
|
||||
*builders.Container("c1", builders.WithLabel("some.label", "value")),
|
||||
*builders.Container("c2", builders.WithName("foo/bar"), builders.WithLabel("foo", "bar")),
|
||||
containerListFunc: func(_ client.ContainerListOptions) (client.ContainerListResult, error) {
|
||||
return client.ContainerListResult{
|
||||
Items: []container.Summary{
|
||||
*builders.Container("c1", builders.WithLabel("some.label", "value")),
|
||||
*builders.Container("c2", builders.WithName("foo/bar"), builders.WithLabel("foo", "bar")),
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
|
||||
@ -7,8 +7,8 @@ import (
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/pkg/stdcopy"
|
||||
"github.com/moby/moby/api/pkg/stdcopy"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@ -23,8 +23,8 @@ type logsOptions struct {
|
||||
container string
|
||||
}
|
||||
|
||||
// NewLogsCommand creates a new cobra.Command for `docker logs`
|
||||
func NewLogsCommand(dockerCli command.Cli) *cobra.Command {
|
||||
// newLogsCommand creates a new cobra.Command for "docker container logs"
|
||||
func newLogsCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
var opts logsOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
@ -33,12 +33,13 @@ func NewLogsCommand(dockerCli command.Cli) *cobra.Command {
|
||||
Args: cli.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.container = args[0]
|
||||
return runLogs(cmd.Context(), dockerCli, &opts)
|
||||
return runLogs(cmd.Context(), dockerCLI, &opts)
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"aliases": "docker container logs, docker logs",
|
||||
},
|
||||
ValidArgsFunction: completion.ContainerNames(dockerCli, true),
|
||||
ValidArgsFunction: completion.ContainerNames(dockerCLI, true),
|
||||
DisableFlagsInUseLine: true,
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
@ -53,12 +54,12 @@ func NewLogsCommand(dockerCli command.Cli) *cobra.Command {
|
||||
}
|
||||
|
||||
func runLogs(ctx context.Context, dockerCli command.Cli, opts *logsOptions) error {
|
||||
c, err := dockerCli.Client().ContainerInspect(ctx, opts.container)
|
||||
c, err := dockerCli.Client().ContainerInspect(ctx, opts.container, client.ContainerInspectOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
responseBody, err := dockerCli.Client().ContainerLogs(ctx, c.ID, container.LogsOptions{
|
||||
resp, err := dockerCli.Client().ContainerLogs(ctx, c.Container.ID, client.ContainerLogsOptions{
|
||||
ShowStdout: true,
|
||||
ShowStderr: true,
|
||||
Since: opts.since,
|
||||
@ -71,12 +72,12 @@ func runLogs(ctx context.Context, dockerCli command.Cli, opts *logsOptions) erro
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer responseBody.Close()
|
||||
defer func() { _ = resp.Close() }()
|
||||
|
||||
if c.Config.Tty {
|
||||
_, err = io.Copy(dockerCli.Out(), responseBody)
|
||||
if c.Container.Config.Tty {
|
||||
_, err = io.Copy(dockerCli.Out(), resp)
|
||||
} else {
|
||||
_, err = stdcopy.StdCopy(dockerCli.Out(), dockerCli.Err(), responseBody)
|
||||
_, err = stdcopy.StdCopy(dockerCli.Out(), dockerCli.Err(), resp)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@ -2,27 +2,22 @@ package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/client"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
|
||||
var logFn = func(expectedOut string) func(string, container.LogsOptions) (io.ReadCloser, error) {
|
||||
return func(container string, opts container.LogsOptions) (io.ReadCloser, error) {
|
||||
return io.NopCloser(strings.NewReader(expectedOut)), nil
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunLogs(t *testing.T) {
|
||||
inspectFn := func(containerID string) (container.InspectResponse, error) {
|
||||
return container.InspectResponse{
|
||||
Config: &container.Config{Tty: true},
|
||||
ContainerJSONBase: &container.ContainerJSONBase{State: &container.State{Running: false}},
|
||||
inspectFn := func(containerID string) (client.ContainerInspectResult, error) {
|
||||
return client.ContainerInspectResult{
|
||||
Container: container.InspectResponse{
|
||||
Config: &container.Config{Tty: true},
|
||||
State: &container.State{Running: false},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
@ -38,7 +33,13 @@ func TestRunLogs(t *testing.T) {
|
||||
doc: "successful logs",
|
||||
expectedOut: "foo",
|
||||
options: &logsOptions{},
|
||||
client: &fakeClient{logFunc: logFn("foo"), inspectFunc: inspectFn},
|
||||
client: &fakeClient{
|
||||
logFunc: func(container string, opts client.ContainerLogsOptions) (client.ContainerLogsResult, error) {
|
||||
// FIXME(thaJeztah): how to mock this?
|
||||
return mockContainerLogsResult("foo"), nil
|
||||
},
|
||||
inspectFunc: inspectFn,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@ -1,25 +1,30 @@
|
||||
// FIXME(vvoland): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
||||
//go:build go1.23
|
||||
|
||||
package container
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/docker/cli/cli/compose/loader"
|
||||
"github.com/docker/cli/internal/lazyregexp"
|
||||
"github.com/docker/cli/internal/volumespec"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
mounttypes "github.com/docker/docker/api/types/mount"
|
||||
networktypes "github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/go-connections/nat"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/api/types/mount"
|
||||
"github.com/moby/moby/api/types/network"
|
||||
"github.com/spf13/pflag"
|
||||
cdi "tags.cncf.io/container-device-interface/pkg/parser"
|
||||
)
|
||||
@ -51,7 +56,7 @@ type containerOptions struct {
|
||||
deviceWriteBps opts.ThrottledeviceOpt
|
||||
links opts.ListOpts
|
||||
aliases opts.ListOpts
|
||||
linkLocalIPs opts.ListOpts
|
||||
linkLocalIPs opts.ListOpts // TODO(thaJeztah): we need a flag-type to handle []netip.Addr directly
|
||||
deviceReadIOps opts.ThrottledeviceOpt
|
||||
deviceWriteIOps opts.ThrottledeviceOpt
|
||||
env opts.ListOpts
|
||||
@ -63,7 +68,7 @@ type containerOptions struct {
|
||||
sysctls *opts.MapOpts
|
||||
publish opts.ListOpts
|
||||
expose opts.ListOpts
|
||||
dns opts.ListOpts
|
||||
dns opts.ListOpts // TODO(thaJeztah): we need a flag-type to handle []netip.Addr directly
|
||||
dnsSearch opts.ListOpts
|
||||
dnsOptions opts.ListOpts
|
||||
extraHosts opts.ListOpts
|
||||
@ -93,7 +98,6 @@ type containerOptions struct {
|
||||
memory opts.MemBytes
|
||||
memoryReservation opts.MemBytes
|
||||
memorySwap opts.MemSwapBytes
|
||||
kernelMemory opts.MemBytes
|
||||
user string
|
||||
workingDir string
|
||||
cpuCount int64
|
||||
@ -112,8 +116,8 @@ type containerOptions struct {
|
||||
swappiness int64
|
||||
netMode opts.NetworkOpt
|
||||
macAddress string
|
||||
ipv4Address string
|
||||
ipv6Address string
|
||||
ipv4Address net.IP // TODO(thaJeztah): we need a flag-type to handle netip.Addr directly
|
||||
ipv6Address net.IP // TODO(thaJeztah): we need a flag-type to handle netip.Addr directly
|
||||
ipcMode string
|
||||
pidsLimit int64
|
||||
restartPolicy string
|
||||
@ -229,8 +233,8 @@ func addFlags(flags *pflag.FlagSet) *containerOptions {
|
||||
flags.MarkHidden("dns-opt")
|
||||
flags.Var(&copts.dnsSearch, "dns-search", "Set custom DNS search domains")
|
||||
flags.Var(&copts.expose, "expose", "Expose a port or a range of ports")
|
||||
flags.StringVar(&copts.ipv4Address, "ip", "", "IPv4 address (e.g., 172.30.100.104)")
|
||||
flags.StringVar(&copts.ipv6Address, "ip6", "", "IPv6 address (e.g., 2001:db8::33)")
|
||||
flags.IPVar(&copts.ipv4Address, "ip", nil, "IPv4 address (e.g., 172.30.100.104)")
|
||||
flags.IPVar(&copts.ipv6Address, "ip6", nil, "IPv6 address (e.g., 2001:db8::33)")
|
||||
flags.Var(&copts.links, "link", "Add link to another container")
|
||||
flags.Var(&copts.linkLocalIPs, "link-local-ip", "Container IPv4/IPv6 link-local addresses")
|
||||
flags.StringVar(&copts.macAddress, "mac-address", "", "Container MAC address (e.g., 92:d0:c6:0a:29:33)")
|
||||
@ -293,7 +297,6 @@ func addFlags(flags *pflag.FlagSet) *containerOptions {
|
||||
flags.SetAnnotation("io-maxbandwidth", "ostype", []string{"windows"})
|
||||
flags.Uint64Var(&copts.ioMaxIOps, "io-maxiops", 0, "Maximum IOps limit for the system drive (Windows only)")
|
||||
flags.SetAnnotation("io-maxiops", "ostype", []string{"windows"})
|
||||
flags.Var(&copts.kernelMemory, "kernel-memory", "Kernel memory limit")
|
||||
flags.VarP(&copts.memory, "memory", "m", "Memory limit")
|
||||
flags.Var(&copts.memoryReservation, "memory-reservation", "Memory soft limit")
|
||||
flags.Var(&copts.memorySwap, "memory-swap", "Swap limit equal to memory plus swap: '-1' to enable unlimited swap")
|
||||
@ -317,13 +320,18 @@ func addFlags(flags *pflag.FlagSet) *containerOptions {
|
||||
flags.Var(copts.annotations, "annotation", "Add an annotation to the container (passed through to the OCI runtime)")
|
||||
flags.SetAnnotation("annotation", "version", []string{"1.43"})
|
||||
|
||||
// TODO(thaJeztah): remove in next release (v30.0, or v29.x)
|
||||
var stub opts.MemBytes
|
||||
flags.Var(&stub, "kernel-memory", "Kernel memory limit (deprecated)")
|
||||
_ = flags.MarkDeprecated("kernel-memory", "and no longer supported by the kernel")
|
||||
|
||||
return copts
|
||||
}
|
||||
|
||||
type containerConfig struct {
|
||||
Config *container.Config
|
||||
HostConfig *container.HostConfig
|
||||
NetworkingConfig *networktypes.NetworkingConfig
|
||||
NetworkingConfig *network.NetworkingConfig
|
||||
}
|
||||
|
||||
// parse parses the args for the specified command and generates a Config,
|
||||
@ -340,8 +348,8 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
|
||||
|
||||
// Validate the input mac address
|
||||
if copts.macAddress != "" {
|
||||
if _, err := opts.ValidateMACAddress(copts.macAddress); err != nil {
|
||||
return nil, errors.Errorf("%s is not a valid mac address", copts.macAddress)
|
||||
if _, err := net.ParseMAC(strings.TrimSpace(copts.macAddress)); err != nil {
|
||||
return nil, fmt.Errorf("%s is not a valid mac address", copts.macAddress)
|
||||
}
|
||||
}
|
||||
if copts.stdin {
|
||||
@ -357,14 +365,14 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
|
||||
|
||||
swappiness := copts.swappiness
|
||||
if swappiness != -1 && (swappiness < 0 || swappiness > 100) {
|
||||
return nil, errors.Errorf("invalid value: %d. Valid memory swappiness range is 0-100", swappiness)
|
||||
return nil, fmt.Errorf("invalid value: %d. Valid memory swappiness range is 0-100", swappiness)
|
||||
}
|
||||
|
||||
var binds []string
|
||||
volumes := copts.volumes.GetMap()
|
||||
// add any bind targets to the list of container volumes
|
||||
for bind := range copts.volumes.GetMap() {
|
||||
parsed, err := loader.ParseVolume(bind)
|
||||
parsed, err := volumespec.Parse(bind)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -372,7 +380,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
|
||||
if parsed.Source != "" {
|
||||
toBind := bind
|
||||
|
||||
if parsed.Type == string(mounttypes.TypeBind) {
|
||||
if parsed.Type == string(mount.TypeBind) {
|
||||
if hostPart, targetPath, ok := strings.Cut(bind, ":"); ok {
|
||||
if !filepath.IsAbs(hostPart) && strings.HasPrefix(hostPart, ".") {
|
||||
if absHostPart, err := filepath.Abs(hostPart); err == nil {
|
||||
@ -412,45 +420,60 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
|
||||
entrypoint = []string{""}
|
||||
}
|
||||
|
||||
publishOpts := copts.publish.GetSlice()
|
||||
var (
|
||||
ports map[nat.Port]struct{}
|
||||
portBindings map[nat.Port][]nat.PortBinding
|
||||
convertedOpts []string
|
||||
)
|
||||
|
||||
convertedOpts, err = convertToStandardNotation(publishOpts)
|
||||
// TODO(thaJeztah): remove uses of go-connections/nat here.
|
||||
convertedOpts, err := convertToStandardNotation(copts.publish.GetSlice())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ports, portBindings, err = nat.ParsePortSpecs(convertedOpts)
|
||||
ports, natPortBindings, err := nat.ParsePortSpecs(convertedOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
portBindings := network.PortMap{}
|
||||
for port, bindings := range natPortBindings {
|
||||
p, err := network.ParsePort(string(port))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
portBindings[p] = []network.PortBinding{}
|
||||
for _, b := range bindings {
|
||||
var hostIP netip.Addr
|
||||
if b.HostIP != "" {
|
||||
hostIP, err = netip.ParseAddr(b.HostIP)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
portBindings[p] = append(portBindings[p], network.PortBinding{
|
||||
HostIP: hostIP,
|
||||
HostPort: b.HostPort,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Add published ports as exposed ports.
|
||||
exposedPorts := network.PortSet{}
|
||||
for port := range ports {
|
||||
p, err := network.ParsePort(string(port))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
exposedPorts[p] = struct{}{}
|
||||
}
|
||||
|
||||
// Merge in exposed ports to the map of published ports
|
||||
for _, e := range copts.expose.GetSlice() {
|
||||
if strings.Contains(e, ":") {
|
||||
return nil, errors.Errorf("invalid port format for --expose: %s", e)
|
||||
}
|
||||
// support two formats for expose, original format <portnum>/[<proto>]
|
||||
// or <startport-endport>/[<proto>]
|
||||
proto, port := nat.SplitProtoPort(e)
|
||||
pr, err := network.ParsePortRange(e)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid range format for --expose: %w", err)
|
||||
}
|
||||
// parse the start and end port and create a sequence of ports to expose
|
||||
// if expose a port, the start and end port are the same
|
||||
start, end, err := nat.ParsePortRange(port)
|
||||
if err != nil {
|
||||
return nil, errors.Errorf("invalid range format for --expose: %s, error: %s", e, err)
|
||||
}
|
||||
for i := start; i <= end; i++ {
|
||||
p, err := nat.NewPort(proto, strconv.FormatUint(i, 10))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, exists := ports[p]; !exists {
|
||||
ports[p] = struct{}{}
|
||||
}
|
||||
for p := range pr.All() {
|
||||
exposedPorts[p] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
@ -458,23 +481,19 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
|
||||
// device path (as opposed to during flag parsing), as at the time we are
|
||||
// parsing flags, we haven't yet sent a _ping to the daemon to determine
|
||||
// what operating system it is.
|
||||
deviceMappings := []container.DeviceMapping{}
|
||||
var cdiDeviceNames []string
|
||||
for _, device := range copts.devices.GetSlice() {
|
||||
var (
|
||||
validated string
|
||||
deviceMapping container.DeviceMapping
|
||||
err error
|
||||
)
|
||||
devices := copts.devices.GetSlice()
|
||||
deviceMappings := make([]container.DeviceMapping, 0, len(devices))
|
||||
cdiDeviceNames := make([]string, 0, len(devices))
|
||||
for _, device := range devices {
|
||||
if cdi.IsQualifiedName(device) {
|
||||
cdiDeviceNames = append(cdiDeviceNames, device)
|
||||
continue
|
||||
}
|
||||
validated, err = validateDevice(device, serverOS)
|
||||
validated, err := validateDevice(device, serverOS)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
deviceMapping, err = parseDevice(validated, serverOS)
|
||||
deviceMapping, err := parseDevice(validated, serverOS)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -495,22 +514,22 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
|
||||
|
||||
pidMode := container.PidMode(copts.pidMode)
|
||||
if !pidMode.Valid() {
|
||||
return nil, errors.Errorf("--pid: invalid PID mode")
|
||||
return nil, errors.New("--pid: invalid PID mode")
|
||||
}
|
||||
|
||||
utsMode := container.UTSMode(copts.utsMode)
|
||||
if !utsMode.Valid() {
|
||||
return nil, errors.Errorf("--uts: invalid UTS mode")
|
||||
return nil, errors.New("--uts: invalid UTS mode")
|
||||
}
|
||||
|
||||
usernsMode := container.UsernsMode(copts.usernsMode)
|
||||
if !usernsMode.Valid() {
|
||||
return nil, errors.Errorf("--userns: invalid USER mode")
|
||||
return nil, errors.New("--userns: invalid USER mode")
|
||||
}
|
||||
|
||||
cgroupnsMode := container.CgroupnsMode(copts.cgroupnsMode)
|
||||
if !cgroupnsMode.Valid() {
|
||||
return nil, errors.Errorf("--cgroupns: invalid CGROUP mode")
|
||||
return nil, errors.New("--cgroupns: invalid CGROUP mode")
|
||||
}
|
||||
|
||||
restartPolicy, err := opts.ParseRestartPolicy(copts.restartPolicy)
|
||||
@ -545,7 +564,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
|
||||
copts.healthStartInterval != 0
|
||||
if copts.noHealthcheck {
|
||||
if haveHealthSettings {
|
||||
return nil, errors.Errorf("--no-healthcheck conflicts with --health-* options")
|
||||
return nil, errors.New("--no-healthcheck conflicts with --health-* options")
|
||||
}
|
||||
healthConfig = &container.HealthConfig{Test: []string{"NONE"}}
|
||||
} else if haveHealthSettings {
|
||||
@ -554,13 +573,13 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
|
||||
probe = []string{"CMD-SHELL", copts.healthCmd}
|
||||
}
|
||||
if copts.healthInterval < 0 {
|
||||
return nil, errors.Errorf("--health-interval cannot be negative")
|
||||
return nil, errors.New("--health-interval cannot be negative")
|
||||
}
|
||||
if copts.healthTimeout < 0 {
|
||||
return nil, errors.Errorf("--health-timeout cannot be negative")
|
||||
return nil, errors.New("--health-timeout cannot be negative")
|
||||
}
|
||||
if copts.healthRetries < 0 {
|
||||
return nil, errors.Errorf("--health-retries cannot be negative")
|
||||
return nil, errors.New("--health-retries cannot be negative")
|
||||
}
|
||||
if copts.healthStartPeriod < 0 {
|
||||
return nil, errors.New("--health-start-period cannot be negative")
|
||||
@ -594,7 +613,6 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
|
||||
MemoryReservation: copts.memoryReservation.Value(),
|
||||
MemorySwap: copts.memorySwap.Value(),
|
||||
MemorySwappiness: &copts.swappiness,
|
||||
KernelMemory: copts.kernelMemory.Value(),
|
||||
OomKillDisable: &copts.oomKillDisable,
|
||||
NanoCPUs: copts.cpus.Value(),
|
||||
CPUCount: copts.cpuCount,
|
||||
@ -624,7 +642,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
|
||||
config := &container.Config{
|
||||
Hostname: copts.hostname,
|
||||
Domainname: copts.domainname,
|
||||
ExposedPorts: ports,
|
||||
ExposedPorts: exposedPorts,
|
||||
User: copts.user,
|
||||
Tty: copts.tty,
|
||||
OpenStdin: copts.stdin,
|
||||
@ -635,7 +653,6 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
|
||||
Cmd: runCmd,
|
||||
Image: copts.Image,
|
||||
Volumes: volumes,
|
||||
MacAddress: copts.macAddress,
|
||||
Entrypoint: entrypoint,
|
||||
WorkingDir: copts.workingDir,
|
||||
Labels: opts.ConvertKVStringsToMap(labels),
|
||||
@ -660,7 +677,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
|
||||
// but pre created containers can still have those nil values.
|
||||
// See https://github.com/docker/docker/pull/17779
|
||||
// for a more detailed explanation on why we don't want that.
|
||||
DNS: copts.dns.GetAllOrEmpty(),
|
||||
DNS: toNetipAddrSlice(copts.dns.GetAllOrEmpty()),
|
||||
DNSSearch: copts.dnsSearch.GetAllOrEmpty(),
|
||||
DNSOptions: copts.dnsOptions.GetAllOrEmpty(),
|
||||
ExtraHosts: copts.extraHosts.GetSlice(),
|
||||
@ -693,7 +710,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
|
||||
}
|
||||
|
||||
if copts.autoRemove && !hostConfig.RestartPolicy.IsNone() {
|
||||
return nil, errors.Errorf("conflicting options: cannot specify both --restart and --rm")
|
||||
return nil, errors.New("conflicting options: cannot specify both --restart and --rm")
|
||||
}
|
||||
|
||||
// only set this value if the user provided the flag, else it should default to nil
|
||||
@ -706,25 +723,17 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
|
||||
config.StdinOnce = true
|
||||
}
|
||||
|
||||
networkingConfig := &networktypes.NetworkingConfig{
|
||||
EndpointsConfig: make(map[string]*networktypes.EndpointSettings),
|
||||
}
|
||||
|
||||
networkingConfig.EndpointsConfig, err = parseNetworkOpts(copts)
|
||||
epCfg, err := parseNetworkOpts(copts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Put the endpoint-specific MacAddress of the "main" network attachment into the container Config for backward
|
||||
// compatibility with older daemons.
|
||||
if nw, ok := networkingConfig.EndpointsConfig[hostConfig.NetworkMode.NetworkName()]; ok {
|
||||
config.MacAddress = nw.MacAddress //nolint:staticcheck // ignore SA1019: field is deprecated, but still used on API < v1.44.
|
||||
}
|
||||
|
||||
return &containerConfig{
|
||||
Config: config,
|
||||
HostConfig: hostConfig,
|
||||
NetworkingConfig: networkingConfig,
|
||||
Config: config,
|
||||
HostConfig: hostConfig,
|
||||
NetworkingConfig: &network.NetworkingConfig{
|
||||
EndpointsConfig: epCfg,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
@ -735,9 +744,9 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
|
||||
// this function may return _multiple_ endpoints, which is not currently supported
|
||||
// by the daemon, but may be in future; it's up to the daemon to produce an error
|
||||
// in case that is not supported.
|
||||
func parseNetworkOpts(copts *containerOptions) (map[string]*networktypes.EndpointSettings, error) {
|
||||
func parseNetworkOpts(copts *containerOptions) (map[string]*network.EndpointSettings, error) {
|
||||
var (
|
||||
endpoints = make(map[string]*networktypes.EndpointSettings, len(copts.netMode.Value()))
|
||||
endpoints = make(map[string]*network.EndpointSettings, len(copts.netMode.Value()))
|
||||
hasUserDefined, hasNonUserDefined bool
|
||||
)
|
||||
|
||||
@ -776,14 +785,14 @@ func parseNetworkOpts(copts *containerOptions) (map[string]*networktypes.Endpoin
|
||||
return nil, err
|
||||
}
|
||||
if _, ok := endpoints[n.Target]; ok {
|
||||
return nil, invalidParameter(errors.Errorf("network %q is specified multiple times", n.Target))
|
||||
return nil, invalidParameter(fmt.Errorf("network %q is specified multiple times", n.Target))
|
||||
}
|
||||
|
||||
// For backward compatibility: if no custom options are provided for the network,
|
||||
// and only a single network is specified, omit the endpoint-configuration
|
||||
// on the client (the daemon will still create it when creating the container)
|
||||
if i == 0 && len(copts.netMode.Value()) == 1 {
|
||||
if ep == nil || reflect.DeepEqual(*ep, networktypes.EndpointSettings{}) {
|
||||
if ep == nil || reflect.DeepEqual(*ep, network.EndpointSettings{}) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
@ -803,10 +812,10 @@ func applyContainerOptions(n *opts.NetworkAttachmentOpts, copts *containerOption
|
||||
if len(n.Links) > 0 && copts.links.Len() > 0 {
|
||||
return invalidParameter(errors.New("conflicting options: cannot specify both --link and per-network links"))
|
||||
}
|
||||
if n.IPv4Address != "" && copts.ipv4Address != "" {
|
||||
if n.IPv4Address.IsValid() && copts.ipv4Address != nil {
|
||||
return invalidParameter(errors.New("conflicting options: cannot specify both --ip and per-network IPv4 address"))
|
||||
}
|
||||
if n.IPv6Address != "" && copts.ipv6Address != "" {
|
||||
if n.IPv6Address.IsValid() && copts.ipv6Address != nil {
|
||||
return invalidParameter(errors.New("conflicting options: cannot specify both --ip6 and per-network IPv6 address"))
|
||||
}
|
||||
if n.MacAddress != "" && copts.macAddress != "" {
|
||||
@ -825,23 +834,26 @@ func applyContainerOptions(n *opts.NetworkAttachmentOpts, copts *containerOption
|
||||
n.Links = make([]string, copts.links.Len())
|
||||
copy(n.Links, copts.links.GetSlice())
|
||||
}
|
||||
if copts.ipv4Address != "" {
|
||||
n.IPv4Address = copts.ipv4Address
|
||||
if copts.ipv4Address != nil {
|
||||
if ipv4, ok := netip.AddrFromSlice(copts.ipv4Address.To4()); ok {
|
||||
n.IPv4Address = ipv4
|
||||
}
|
||||
}
|
||||
if copts.ipv6Address != "" {
|
||||
n.IPv6Address = copts.ipv6Address
|
||||
if copts.ipv6Address != nil {
|
||||
if ipv6, ok := netip.AddrFromSlice(copts.ipv6Address.To16()); ok {
|
||||
n.IPv6Address = ipv6
|
||||
}
|
||||
}
|
||||
if copts.macAddress != "" {
|
||||
n.MacAddress = copts.macAddress
|
||||
}
|
||||
if copts.linkLocalIPs.Len() > 0 {
|
||||
n.LinkLocalIPs = make([]string, copts.linkLocalIPs.Len())
|
||||
copy(n.LinkLocalIPs, copts.linkLocalIPs.GetSlice())
|
||||
n.LinkLocalIPs = toNetipAddrSlice(copts.linkLocalIPs.GetSlice())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseNetworkAttachmentOpt(ep opts.NetworkAttachmentOpts) (*networktypes.EndpointSettings, error) {
|
||||
func parseNetworkAttachmentOpt(ep opts.NetworkAttachmentOpts) (*network.EndpointSettings, error) {
|
||||
if strings.TrimSpace(ep.Target) == "" {
|
||||
return nil, errors.New("no name set for network")
|
||||
}
|
||||
@ -854,7 +866,7 @@ func parseNetworkAttachmentOpt(ep opts.NetworkAttachmentOpts) (*networktypes.End
|
||||
}
|
||||
}
|
||||
|
||||
epConfig := &networktypes.EndpointSettings{
|
||||
epConfig := &network.EndpointSettings{
|
||||
GwPriority: ep.GwPriority,
|
||||
}
|
||||
epConfig.Aliases = append(epConfig.Aliases, ep.Aliases...)
|
||||
@ -865,18 +877,19 @@ func parseNetworkAttachmentOpt(ep opts.NetworkAttachmentOpts) (*networktypes.End
|
||||
if len(ep.Links) > 0 {
|
||||
epConfig.Links = ep.Links
|
||||
}
|
||||
if ep.IPv4Address != "" || ep.IPv6Address != "" || len(ep.LinkLocalIPs) > 0 {
|
||||
epConfig.IPAMConfig = &networktypes.EndpointIPAMConfig{
|
||||
if ep.IPv4Address.IsValid() || ep.IPv6Address.IsValid() || len(ep.LinkLocalIPs) > 0 {
|
||||
epConfig.IPAMConfig = &network.EndpointIPAMConfig{
|
||||
IPv4Address: ep.IPv4Address,
|
||||
IPv6Address: ep.IPv6Address,
|
||||
LinkLocalIPs: ep.LinkLocalIPs,
|
||||
}
|
||||
}
|
||||
if ep.MacAddress != "" {
|
||||
if _, err := opts.ValidateMACAddress(ep.MacAddress); err != nil {
|
||||
return nil, errors.Errorf("%s is not a valid mac address", ep.MacAddress)
|
||||
ma, err := net.ParseMAC(strings.TrimSpace(ep.MacAddress))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s is not a valid mac address", ep.MacAddress)
|
||||
}
|
||||
epConfig.MacAddress = ep.MacAddress
|
||||
epConfig.MacAddress = network.HardwareAddr(ma)
|
||||
}
|
||||
return epConfig, nil
|
||||
}
|
||||
@ -889,7 +902,7 @@ func convertToStandardNotation(ports []string) ([]string, error) {
|
||||
for _, param := range strings.Split(publish, ",") {
|
||||
k, v, ok := strings.Cut(param, "=")
|
||||
if !ok || k == "" {
|
||||
return optsList, errors.Errorf("invalid publish opts format (should be name=value but got '%s')", param)
|
||||
return optsList, fmt.Errorf("invalid publish opts format (should be name=value but got '%s')", param)
|
||||
}
|
||||
params[k] = v
|
||||
}
|
||||
@ -904,7 +917,7 @@ func convertToStandardNotation(ports []string) ([]string, error) {
|
||||
func parseLoggingOpts(loggingDriver string, loggingOpts []string) (map[string]string, error) {
|
||||
loggingOptsMap := opts.ConvertKVStringsToMap(loggingOpts)
|
||||
if loggingDriver == "none" && len(loggingOpts) > 0 {
|
||||
return map[string]string{}, errors.Errorf("invalid logging opts for driver %s", loggingDriver)
|
||||
return map[string]string{}, fmt.Errorf("invalid logging opts for driver %s", loggingDriver)
|
||||
}
|
||||
return loggingOptsMap, nil
|
||||
}
|
||||
@ -918,7 +931,7 @@ func parseSecurityOpts(securityOpts []string) ([]string, error) {
|
||||
}
|
||||
if (!ok || v == "") && k != "no-new-privileges" {
|
||||
// "no-new-privileges" is the only option that does not require a value.
|
||||
return securityOpts, errors.Errorf("Invalid --security-opt: %q", opt)
|
||||
return securityOpts, fmt.Errorf("invalid --security-opt: %q", opt)
|
||||
}
|
||||
if k == "seccomp" {
|
||||
switch v {
|
||||
@ -929,11 +942,11 @@ func parseSecurityOpts(securityOpts []string) ([]string, error) {
|
||||
// content if it's valid JSON.
|
||||
f, err := os.ReadFile(v)
|
||||
if err != nil {
|
||||
return securityOpts, errors.Errorf("opening seccomp profile (%s) failed: %v", v, err)
|
||||
return securityOpts, fmt.Errorf("opening seccomp profile (%s) failed: %w", v, err)
|
||||
}
|
||||
b := bytes.NewBuffer(nil)
|
||||
if err := json.Compact(b, f); err != nil {
|
||||
return securityOpts, errors.Errorf("compacting json for seccomp profile (%s) failed: %v", v, err)
|
||||
return securityOpts, fmt.Errorf("compacting json for seccomp profile (%s) failed: %w", v, err)
|
||||
}
|
||||
securityOpts[key] = fmt.Sprintf("seccomp=%s", b.Bytes())
|
||||
}
|
||||
@ -968,7 +981,7 @@ func parseStorageOpts(storageOpts []string) (map[string]string, error) {
|
||||
for _, option := range storageOpts {
|
||||
k, v, ok := strings.Cut(option, "=")
|
||||
if !ok {
|
||||
return nil, errors.Errorf("invalid storage option")
|
||||
return nil, errors.New("invalid storage option")
|
||||
}
|
||||
m[k] = v
|
||||
}
|
||||
@ -983,7 +996,7 @@ func parseDevice(device, serverOS string) (container.DeviceMapping, error) {
|
||||
case "windows":
|
||||
return parseWindowsDevice(device)
|
||||
}
|
||||
return container.DeviceMapping{}, errors.Errorf("unknown server OS: %s", serverOS)
|
||||
return container.DeviceMapping{}, fmt.Errorf("unknown server OS: %s", serverOS)
|
||||
}
|
||||
|
||||
// parseLinuxDevice parses a device mapping string to a container.DeviceMapping struct
|
||||
@ -1007,7 +1020,7 @@ func parseLinuxDevice(device string) (container.DeviceMapping, error) {
|
||||
case 1:
|
||||
src = arr[0]
|
||||
default:
|
||||
return container.DeviceMapping{}, errors.Errorf("invalid device specification: %s", device)
|
||||
return container.DeviceMapping{}, fmt.Errorf("invalid device specification: %s", device)
|
||||
}
|
||||
|
||||
if dst == "" {
|
||||
@ -1037,7 +1050,7 @@ func validateDeviceCgroupRule(val string) (string, error) {
|
||||
return val, nil
|
||||
}
|
||||
|
||||
return val, errors.Errorf("invalid device cgroup format '%s'", val)
|
||||
return val, fmt.Errorf("invalid device cgroup format '%s'", val)
|
||||
}
|
||||
|
||||
// validDeviceMode checks if the mode for device is valid or not.
|
||||
@ -1069,7 +1082,7 @@ func validateDevice(val string, serverOS string) (string, error) {
|
||||
// Windows does validation entirely server-side
|
||||
return val, nil
|
||||
}
|
||||
return "", errors.Errorf("unknown server OS: %s", serverOS)
|
||||
return "", fmt.Errorf("unknown server OS: %s", serverOS)
|
||||
}
|
||||
|
||||
// validateLinuxPath is the implementation of validateDevice knowing that the
|
||||
@ -1084,12 +1097,12 @@ func validateLinuxPath(val string, validator func(string) bool) (string, error)
|
||||
var mode string
|
||||
|
||||
if strings.Count(val, ":") > 2 {
|
||||
return val, errors.Errorf("bad format for path: %s", val)
|
||||
return val, fmt.Errorf("bad format for path: %s", val)
|
||||
}
|
||||
|
||||
split := strings.SplitN(val, ":", 3)
|
||||
if split[0] == "" {
|
||||
return val, errors.Errorf("bad format for path: %s", val)
|
||||
return val, fmt.Errorf("bad format for path: %s", val)
|
||||
}
|
||||
switch len(split) {
|
||||
case 1:
|
||||
@ -1108,13 +1121,13 @@ func validateLinuxPath(val string, validator func(string) bool) (string, error)
|
||||
containerPath = split[1]
|
||||
mode = split[2]
|
||||
if isValid := validator(split[2]); !isValid {
|
||||
return val, errors.Errorf("bad mode specified: %s", mode)
|
||||
return val, fmt.Errorf("bad mode specified: %s", mode)
|
||||
}
|
||||
val = fmt.Sprintf("%s:%s:%s", split[0], containerPath, mode)
|
||||
}
|
||||
|
||||
if !path.IsAbs(containerPath) {
|
||||
return val, errors.Errorf("%s is not an absolute path", containerPath)
|
||||
return val, fmt.Errorf("%s is not an absolute path", containerPath)
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
@ -1122,10 +1135,23 @@ func validateLinuxPath(val string, validator func(string) bool) (string, error)
|
||||
// validateAttach validates that the specified string is a valid attach option.
|
||||
func validateAttach(val string) (string, error) {
|
||||
s := strings.ToLower(val)
|
||||
for _, str := range []string{"stdin", "stdout", "stderr"} {
|
||||
if s == str {
|
||||
return s, nil
|
||||
}
|
||||
if slices.Contains([]string{"stdin", "stdout", "stderr"}, s) {
|
||||
return s, nil
|
||||
}
|
||||
return val, errors.Errorf("valid streams are STDIN, STDOUT and STDERR")
|
||||
return val, errors.New("valid streams are STDIN, STDOUT and STDERR")
|
||||
}
|
||||
|
||||
func toNetipAddrSlice(ips []string) []netip.Addr {
|
||||
if len(ips) == 0 {
|
||||
return nil
|
||||
}
|
||||
netIPs := make([]netip.Addr, 0, len(ips))
|
||||
for _, ip := range ips {
|
||||
addr, err := netip.ParseAddr(ip)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
netIPs = append(netIPs, addr)
|
||||
}
|
||||
return netIPs
|
||||
}
|
||||
|
||||
@ -4,21 +4,31 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
networktypes "github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/go-connections/nat"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
networktypes "github.com/moby/moby/api/types/network"
|
||||
"github.com/spf13/pflag"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
"gotest.tools/v3/skip"
|
||||
)
|
||||
|
||||
func mustParseMAC(s string) networktypes.HardwareAddr {
|
||||
mac, err := net.ParseMAC(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return networktypes.HardwareAddr(mac)
|
||||
}
|
||||
|
||||
func TestValidateAttach(t *testing.T) {
|
||||
valid := []string{
|
||||
"stdin",
|
||||
@ -48,7 +58,7 @@ func parseRun(args []string) (*container.Config, *container.HostConfig, *network
|
||||
if err := flags.Parse(args); err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
// TODO: fix tests to accept ContainerConfig
|
||||
// TODO(dnephin): fix tests to accept ContainerConfig; see https://github.com/moby/moby/pull/31621
|
||||
containerCfg, err := parse(flags, copts, runtime.GOOS)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
@ -350,13 +360,9 @@ func TestParseWithMacAddress(t *testing.T) {
|
||||
if _, _, _, err := parseRun([]string{invalidMacAddress, "img", "cmd"}); err != nil && err.Error() != "invalidMacAddress is not a valid mac address" {
|
||||
t.Fatalf("Expected an error with %v mac-address, got %v", invalidMacAddress, err)
|
||||
}
|
||||
config, hostConfig, nwConfig := mustParse(t, validMacAddress)
|
||||
if config.MacAddress != "92:d0:c6:0a:29:33" { //nolint:staticcheck // ignore SA1019: field is deprecated, but still used on API < v1.44.
|
||||
t.Fatalf("Expected the config to have '92:d0:c6:0a:29:33' as container-wide MacAddress, got '%v'",
|
||||
config.MacAddress) //nolint:staticcheck // ignore SA1019: field is deprecated, but still used on API < v1.44.
|
||||
}
|
||||
_, hostConfig, nwConfig := mustParse(t, validMacAddress)
|
||||
defaultNw := hostConfig.NetworkMode.NetworkName()
|
||||
if nwConfig.EndpointsConfig[defaultNw].MacAddress != "92:d0:c6:0a:29:33" {
|
||||
if nwConfig.EndpointsConfig[defaultNw].MacAddress.String() != "92:d0:c6:0a:29:33" {
|
||||
t.Fatalf("Expected the default endpoint to have the MacAddress '92:d0:c6:0a:29:33' set, got '%v'", nwConfig.EndpointsConfig[defaultNw].MacAddress)
|
||||
}
|
||||
}
|
||||
@ -429,56 +435,55 @@ func TestParseHostnameDomainname(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseWithExpose(t *testing.T) {
|
||||
invalids := map[string]string{
|
||||
":": "invalid port format for --expose: :",
|
||||
"8080:9090": "invalid port format for --expose: 8080:9090",
|
||||
"/tcp": "invalid range format for --expose: /tcp, error: empty string specified for ports",
|
||||
"/udp": "invalid range format for --expose: /udp, error: empty string specified for ports",
|
||||
"NaN/tcp": `invalid range format for --expose: NaN/tcp, error: strconv.ParseUint: parsing "NaN": invalid syntax`,
|
||||
"NaN-NaN/tcp": `invalid range format for --expose: NaN-NaN/tcp, error: strconv.ParseUint: parsing "NaN": invalid syntax`,
|
||||
"8080-NaN/tcp": `invalid range format for --expose: 8080-NaN/tcp, error: strconv.ParseUint: parsing "NaN": invalid syntax`,
|
||||
"1234567890-8080/tcp": `invalid range format for --expose: 1234567890-8080/tcp, error: strconv.ParseUint: parsing "1234567890": value out of range`,
|
||||
}
|
||||
valids := map[string][]nat.Port{
|
||||
"8080/tcp": {"8080/tcp"},
|
||||
"8080/udp": {"8080/udp"},
|
||||
"8080/ncp": {"8080/ncp"},
|
||||
"8080-8080/udp": {"8080/udp"},
|
||||
"8080-8082/tcp": {"8080/tcp", "8081/tcp", "8082/tcp"},
|
||||
}
|
||||
for expose, expectedError := range invalids {
|
||||
if _, _, _, err := parseRun([]string{fmt.Sprintf("--expose=%v", expose), "img", "cmd"}); err == nil || err.Error() != expectedError {
|
||||
t.Fatalf("Expected error '%v' with '--expose=%v', got '%v'", expectedError, expose, err)
|
||||
t.Run("invalid", func(t *testing.T) {
|
||||
tests := map[string]string{
|
||||
":": `invalid range format for --expose: invalid start port ':': invalid syntax`,
|
||||
"8080:9090": `invalid range format for --expose: invalid start port '8080:9090': invalid syntax`,
|
||||
"/tcp": `invalid range format for --expose: invalid start port '': value is empty`,
|
||||
"/udp": `invalid range format for --expose: invalid start port '': value is empty`,
|
||||
"NaN/tcp": `invalid range format for --expose: invalid start port 'NaN': invalid syntax`,
|
||||
"NaN-NaN/tcp": `invalid range format for --expose: invalid start port 'NaN': invalid syntax`,
|
||||
"8080-NaN/tcp": `invalid range format for --expose: invalid end port 'NaN': invalid syntax`,
|
||||
"1234567890-8080/tcp": `invalid range format for --expose: invalid start port '1234567890': value out of range`,
|
||||
}
|
||||
}
|
||||
for expose, exposedPorts := range valids {
|
||||
config, _, _, err := parseRun([]string{fmt.Sprintf("--expose=%v", expose), "img", "cmd"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
for expose, expectedError := range tests {
|
||||
t.Run(expose, func(t *testing.T) {
|
||||
_, _, _, err := parseRun([]string{fmt.Sprintf("--expose=%v", expose), "img", "cmd"})
|
||||
assert.Error(t, err, expectedError)
|
||||
})
|
||||
}
|
||||
if len(config.ExposedPorts) != len(exposedPorts) {
|
||||
t.Fatalf("Expected %v exposed port, got %v", len(exposedPorts), len(config.ExposedPorts))
|
||||
})
|
||||
t.Run("valid", func(t *testing.T) {
|
||||
tests := map[string][]networktypes.Port{
|
||||
"8080/tcp": {networktypes.MustParsePort("8080/tcp")},
|
||||
"8080/udp": {networktypes.MustParsePort("8080/udp")},
|
||||
"8080/ncp": {networktypes.MustParsePort("8080/ncp")},
|
||||
"8080-8080/udp": {networktypes.MustParsePort("8080/udp")},
|
||||
"8080-8082/tcp": {networktypes.MustParsePort("8080/tcp"), networktypes.MustParsePort("8081/tcp"), networktypes.MustParsePort("8082/tcp")},
|
||||
}
|
||||
for _, port := range exposedPorts {
|
||||
if _, ok := config.ExposedPorts[port]; !ok {
|
||||
t.Fatalf("Expected %v, got %v", exposedPorts, config.ExposedPorts)
|
||||
}
|
||||
for expose, exposedPorts := range tests {
|
||||
t.Run(expose, func(t *testing.T) {
|
||||
config, _, _, err := parseRun([]string{fmt.Sprintf("--expose=%v", expose), "img", "cmd"})
|
||||
assert.NilError(t, err)
|
||||
for _, port := range exposedPorts {
|
||||
_, ok := config.ExposedPorts[port]
|
||||
assert.Check(t, ok, "missing port %q in exposed ports: %#+v", port, config.ExposedPorts[port])
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
// Merge with actual published port
|
||||
config, _, _, err := parseRun([]string{"--publish=80", "--expose=80-81/tcp", "img", "cmd"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(config.ExposedPorts) != 2 {
|
||||
t.Fatalf("Expected 2 exposed ports, got %v", config.ExposedPorts)
|
||||
}
|
||||
ports := []nat.Port{"80/tcp", "81/tcp"}
|
||||
for _, port := range ports {
|
||||
if _, ok := config.ExposedPorts[port]; !ok {
|
||||
t.Fatalf("Expected %v, got %v", ports, config.ExposedPorts)
|
||||
})
|
||||
|
||||
t.Run("merge with published", func(t *testing.T) {
|
||||
// Merge with actual published port
|
||||
config, _, _, err := parseRun([]string{"--publish=80", "--expose=80-81/tcp", "img", "cmd"})
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, is.Len(config.ExposedPorts, 2))
|
||||
ports := []networktypes.Port{networktypes.MustParsePort("80/tcp"), networktypes.MustParsePort("81/tcp")}
|
||||
for _, port := range ports {
|
||||
_, ok := config.ExposedPorts[port]
|
||||
assert.Check(t, ok, "missing port %q in exposed ports: %#+v", port, config.ExposedPorts[port])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestParseDevice(t *testing.T) {
|
||||
@ -576,7 +581,6 @@ func TestParseNetworkConfig(t *testing.T) {
|
||||
name string
|
||||
flags []string
|
||||
expected map[string]*networktypes.EndpointSettings
|
||||
expectedCfg container.Config
|
||||
expectedHostCfg container.HostConfig
|
||||
expectedErr string
|
||||
}{
|
||||
@ -608,9 +612,9 @@ func TestParseNetworkConfig(t *testing.T) {
|
||||
expected: map[string]*networktypes.EndpointSettings{
|
||||
"net1": {
|
||||
IPAMConfig: &networktypes.EndpointIPAMConfig{
|
||||
IPv4Address: "172.20.88.22",
|
||||
IPv6Address: "2001:db8::8822",
|
||||
LinkLocalIPs: []string{"169.254.2.2", "fe80::169:254:2:2"},
|
||||
IPv4Address: netip.MustParseAddr("172.20.88.22"),
|
||||
IPv6Address: netip.MustParseAddr("2001:db8::8822"),
|
||||
LinkLocalIPs: []netip.Addr{netip.MustParseAddr("169.254.2.2"), netip.MustParseAddr("fe80::169:254:2:2")},
|
||||
},
|
||||
Links: []string{"foo:bar", "bar:baz"},
|
||||
Aliases: []string{"web1", "web2"},
|
||||
@ -638,9 +642,9 @@ func TestParseNetworkConfig(t *testing.T) {
|
||||
"net1": {
|
||||
DriverOpts: map[string]string{"field1": "value1"},
|
||||
IPAMConfig: &networktypes.EndpointIPAMConfig{
|
||||
IPv4Address: "172.20.88.22",
|
||||
IPv6Address: "2001:db8::8822",
|
||||
LinkLocalIPs: []string{"169.254.2.2", "fe80::169:254:2:2"},
|
||||
IPv4Address: netip.MustParseAddr("172.20.88.22"),
|
||||
IPv6Address: netip.MustParseAddr("2001:db8::8822"),
|
||||
LinkLocalIPs: []netip.Addr{netip.MustParseAddr("169.254.2.2"), netip.MustParseAddr("fe80::169:254:2:2")},
|
||||
},
|
||||
Links: []string{"foo:bar", "bar:baz"},
|
||||
Aliases: []string{"web1", "web2"},
|
||||
@ -649,15 +653,15 @@ func TestParseNetworkConfig(t *testing.T) {
|
||||
"net3": {
|
||||
DriverOpts: map[string]string{"field3": "value3"},
|
||||
IPAMConfig: &networktypes.EndpointIPAMConfig{
|
||||
IPv4Address: "172.20.88.22",
|
||||
IPv6Address: "2001:db8::8822",
|
||||
IPv4Address: netip.MustParseAddr("172.20.88.22"),
|
||||
IPv6Address: netip.MustParseAddr("2001:db8::8822"),
|
||||
},
|
||||
Aliases: []string{"web3"},
|
||||
},
|
||||
"net4": {
|
||||
MacAddress: "02:32:1c:23:00:04",
|
||||
MacAddress: mustParseMAC("02:32:1c:23:00:04"),
|
||||
IPAMConfig: &networktypes.EndpointIPAMConfig{
|
||||
LinkLocalIPs: []string{"169.254.169.254"},
|
||||
LinkLocalIPs: []netip.Addr{netip.MustParseAddr("169.254.169.254")},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -673,14 +677,13 @@ func TestParseNetworkConfig(t *testing.T) {
|
||||
"field2": "value2",
|
||||
},
|
||||
IPAMConfig: &networktypes.EndpointIPAMConfig{
|
||||
IPv4Address: "172.20.88.22",
|
||||
IPv6Address: "2001:db8::8822",
|
||||
IPv4Address: netip.MustParseAddr("172.20.88.22"),
|
||||
IPv6Address: netip.MustParseAddr("2001:db8::8822"),
|
||||
},
|
||||
Aliases: []string{"web1", "web2"},
|
||||
MacAddress: "02:32:1c:23:00:04",
|
||||
MacAddress: mustParseMAC("02:32:1c:23:00:04"),
|
||||
},
|
||||
},
|
||||
expectedCfg: container.Config{MacAddress: "02:32:1c:23:00:04"},
|
||||
expectedHostCfg: container.HostConfig{NetworkMode: "net1"},
|
||||
},
|
||||
{
|
||||
@ -695,10 +698,9 @@ func TestParseNetworkConfig(t *testing.T) {
|
||||
expected: map[string]*networktypes.EndpointSettings{
|
||||
"net1": {
|
||||
Aliases: []string{"foobar"},
|
||||
MacAddress: "52:0f:f3:dc:50:10",
|
||||
MacAddress: mustParseMAC("52:0f:f3:dc:50:10"),
|
||||
},
|
||||
},
|
||||
expectedCfg: container.Config{MacAddress: "52:0f:f3:dc:50:10"},
|
||||
expectedHostCfg: container.HostConfig{NetworkMode: "net1"},
|
||||
},
|
||||
{
|
||||
@ -745,7 +747,7 @@ func TestParseNetworkConfig(t *testing.T) {
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
config, hConfig, nwConfig, err := parseRun(tc.flags)
|
||||
_, hConfig, nwConfig, err := parseRun(tc.flags)
|
||||
|
||||
if tc.expectedErr != "" {
|
||||
assert.Error(t, err, tc.expectedErr)
|
||||
@ -753,9 +755,8 @@ func TestParseNetworkConfig(t *testing.T) {
|
||||
}
|
||||
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, config.MacAddress, tc.expectedCfg.MacAddress) //nolint:staticcheck // ignore SA1019: field is deprecated, but still used on API < v1.44.
|
||||
assert.DeepEqual(t, hConfig.NetworkMode, tc.expectedHostCfg.NetworkMode)
|
||||
assert.DeepEqual(t, nwConfig.EndpointsConfig, tc.expected)
|
||||
assert.DeepEqual(t, nwConfig.EndpointsConfig, tc.expected, cmpopts.EquateComparable(netip.Addr{}))
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -1017,7 +1018,7 @@ func TestParseLabelfileVariables(t *testing.T) {
|
||||
func TestParseEntryPoint(t *testing.T) {
|
||||
config, _, _, err := parseRun([]string{"--entrypoint=anything", "cmd", "img"})
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, is.DeepEqual([]string(config.Entrypoint), []string{"anything"}))
|
||||
assert.Check(t, is.DeepEqual(config.Entrypoint, []string{"anything"}))
|
||||
}
|
||||
|
||||
func TestValidateDevice(t *testing.T) {
|
||||
|
||||
@ -8,7 +8,8 @@ import (
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@ -16,8 +17,8 @@ type pauseOptions struct {
|
||||
containers []string
|
||||
}
|
||||
|
||||
// NewPauseCommand creates a new cobra.Command for `docker pause`
|
||||
func NewPauseCommand(dockerCli command.Cli) *cobra.Command {
|
||||
// newPauseCommand creates a new cobra.Command for "docker container pause"
|
||||
func newPauseCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
var opts pauseOptions
|
||||
|
||||
return &cobra.Command{
|
||||
@ -26,20 +27,24 @@ func NewPauseCommand(dockerCli command.Cli) *cobra.Command {
|
||||
Args: cli.RequiresMinArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.containers = args
|
||||
return runPause(cmd.Context(), dockerCli, &opts)
|
||||
return runPause(cmd.Context(), dockerCLI, &opts)
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"aliases": "docker container pause, docker pause",
|
||||
},
|
||||
ValidArgsFunction: completion.ContainerNames(dockerCli, false, func(ctr container.Summary) bool {
|
||||
ValidArgsFunction: completion.ContainerNames(dockerCLI, false, func(ctr container.Summary) bool {
|
||||
return ctr.State != container.StatePaused
|
||||
}),
|
||||
DisableFlagsInUseLine: true,
|
||||
}
|
||||
}
|
||||
|
||||
func runPause(ctx context.Context, dockerCLI command.Cli, opts *pauseOptions) error {
|
||||
apiClient := dockerCLI.Client()
|
||||
errChan := parallelOperation(ctx, opts.containers, apiClient.ContainerPause)
|
||||
errChan := parallelOperation(ctx, opts.containers, func(ctx context.Context, container string) error {
|
||||
_, err := apiClient.ContainerPause(ctx, container, client.ContainerPauseOptions{})
|
||||
return err
|
||||
})
|
||||
|
||||
var errs []error
|
||||
for _, ctr := range opts.containers {
|
||||
|
||||
@ -8,20 +8,15 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/moby/moby/client"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
|
||||
func TestRunPause(t *testing.T) {
|
||||
cli := test.NewFakeCli(
|
||||
&fakeClient{
|
||||
containerPauseFunc: func(ctx context.Context, container string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
)
|
||||
cli := test.NewFakeCli(&fakeClient{})
|
||||
|
||||
cmd := NewPauseCommand(cli)
|
||||
cmd := newPauseCommand(cli)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetArgs([]string{"container-id-1", "container-id-2"})
|
||||
|
||||
@ -41,13 +36,13 @@ func TestRunPause(t *testing.T) {
|
||||
func TestRunPauseClientError(t *testing.T) {
|
||||
cli := test.NewFakeCli(
|
||||
&fakeClient{
|
||||
containerPauseFunc: func(ctx context.Context, container string) error {
|
||||
return fmt.Errorf("client error for container %s", container)
|
||||
containerPauseFunc: func(ctx context.Context, container string, options client.ContainerPauseOptions) (client.ContainerPauseResult, error) {
|
||||
return client.ContainerPauseResult{}, fmt.Errorf("client error for container %s", container)
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
cmd := NewPauseCommand(cli)
|
||||
cmd := newPauseCommand(cli)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
cmd.SetArgs([]string{"container-id-1", "container-id-2"})
|
||||
|
||||
@ -5,15 +5,14 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/docker/go-connections/nat"
|
||||
"github.com/fvbommel/sortorder"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/moby/moby/api/types/network"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@ -23,8 +22,8 @@ type portOptions struct {
|
||||
port string
|
||||
}
|
||||
|
||||
// NewPortCommand creates a new cobra.Command for `docker port`
|
||||
func NewPortCommand(dockerCli command.Cli) *cobra.Command {
|
||||
// newPortCommand creates a new cobra.Command for "docker container port".
|
||||
func newPortCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
var opts portOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
@ -36,12 +35,13 @@ func NewPortCommand(dockerCli command.Cli) *cobra.Command {
|
||||
if len(args) > 1 {
|
||||
opts.port = args[1]
|
||||
}
|
||||
return runPort(cmd.Context(), dockerCli, &opts)
|
||||
return runPort(cmd.Context(), dockerCLI, &opts)
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"aliases": "docker container port, docker port",
|
||||
},
|
||||
ValidArgsFunction: completion.ContainerNames(dockerCli, false),
|
||||
ValidArgsFunction: completion.ContainerNames(dockerCLI, false),
|
||||
DisableFlagsInUseLine: true,
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
@ -53,31 +53,28 @@ func NewPortCommand(dockerCli command.Cli) *cobra.Command {
|
||||
// proto is specified. We should consider changing this to "any" protocol
|
||||
// for the given private port.
|
||||
func runPort(ctx context.Context, dockerCli command.Cli, opts *portOptions) error {
|
||||
c, err := dockerCli.Client().ContainerInspect(ctx, opts.container)
|
||||
c, err := dockerCli.Client().ContainerInspect(ctx, opts.container, client.ContainerInspectOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var out []string
|
||||
if opts.port != "" {
|
||||
port, proto, _ := strings.Cut(opts.port, "/")
|
||||
if proto == "" {
|
||||
proto = "tcp"
|
||||
port, err := network.ParsePort(opts.port)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = strconv.ParseUint(port, 10, 16); err != nil {
|
||||
return errors.Wrapf(err, "Error: invalid port (%s)", port)
|
||||
}
|
||||
frontends, exists := c.NetworkSettings.Ports[nat.Port(port+"/"+proto)]
|
||||
frontends, exists := c.Container.NetworkSettings.Ports[port]
|
||||
if !exists || len(frontends) == 0 {
|
||||
return errors.Errorf("Error: No public port '%s' published for %s", opts.port, opts.container)
|
||||
return fmt.Errorf("no public port '%s' published for %s", opts.port, opts.container)
|
||||
}
|
||||
for _, frontend := range frontends {
|
||||
out = append(out, net.JoinHostPort(frontend.HostIP, frontend.HostPort))
|
||||
out = append(out, net.JoinHostPort(frontend.HostIP.String(), frontend.HostPort))
|
||||
}
|
||||
} else {
|
||||
for from, frontends := range c.NetworkSettings.Ports {
|
||||
for from, frontends := range c.Container.NetworkSettings.Ports {
|
||||
for _, frontend := range frontends {
|
||||
out = append(out, fmt.Sprintf("%s -> %s", from, net.JoinHostPort(frontend.HostIP, frontend.HostPort)))
|
||||
out = append(out, fmt.Sprintf("%s -> %s", from, net.JoinHostPort(frontend.HostIP.String(), frontend.HostPort)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,11 +2,13 @@ package container
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/go-connections/nat"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/api/types/network"
|
||||
"github.com/moby/moby/client"
|
||||
"gotest.tools/v3/assert"
|
||||
"gotest.tools/v3/golden"
|
||||
)
|
||||
@ -14,59 +16,59 @@ import (
|
||||
func TestNewPortCommandOutput(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
ips []string
|
||||
ips []netip.Addr
|
||||
port string
|
||||
}{
|
||||
{
|
||||
name: "container-port-ipv4",
|
||||
ips: []string{"0.0.0.0"},
|
||||
ips: []netip.Addr{netip.MustParseAddr("0.0.0.0")},
|
||||
port: "80",
|
||||
},
|
||||
{
|
||||
name: "container-port-ipv6",
|
||||
ips: []string{"::"},
|
||||
ips: []netip.Addr{netip.MustParseAddr("::")},
|
||||
port: "80",
|
||||
},
|
||||
{
|
||||
name: "container-port-ipv6-and-ipv4",
|
||||
ips: []string{"::", "0.0.0.0"},
|
||||
ips: []netip.Addr{netip.MustParseAddr("::"), netip.MustParseAddr("0.0.0.0")},
|
||||
port: "80",
|
||||
},
|
||||
{
|
||||
name: "container-port-ipv6-and-ipv4-443-udp",
|
||||
ips: []string{"::", "0.0.0.0"},
|
||||
ips: []netip.Addr{netip.MustParseAddr("::"), netip.MustParseAddr("0.0.0.0")},
|
||||
port: "443/udp",
|
||||
},
|
||||
{
|
||||
name: "container-port-all-ports",
|
||||
ips: []string{"::", "0.0.0.0"},
|
||||
ips: []netip.Addr{netip.MustParseAddr("::"), netip.MustParseAddr("0.0.0.0")},
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
inspectFunc: func(string) (container.InspectResponse, error) {
|
||||
inspectFunc: func(string) (client.ContainerInspectResult, error) {
|
||||
ci := container.InspectResponse{NetworkSettings: &container.NetworkSettings{}}
|
||||
ci.NetworkSettings.Ports = nat.PortMap{
|
||||
"80/tcp": make([]nat.PortBinding, len(tc.ips)),
|
||||
"443/tcp": make([]nat.PortBinding, len(tc.ips)),
|
||||
"443/udp": make([]nat.PortBinding, len(tc.ips)),
|
||||
ci.NetworkSettings.Ports = network.PortMap{
|
||||
network.MustParsePort("80/tcp"): make([]network.PortBinding, len(tc.ips)),
|
||||
network.MustParsePort("443/tcp"): make([]network.PortBinding, len(tc.ips)),
|
||||
network.MustParsePort("443/udp"): make([]network.PortBinding, len(tc.ips)),
|
||||
}
|
||||
for i, ip := range tc.ips {
|
||||
ci.NetworkSettings.Ports["80/tcp"][i] = nat.PortBinding{
|
||||
ci.NetworkSettings.Ports[network.MustParsePort("80/tcp")][i] = network.PortBinding{
|
||||
HostIP: ip, HostPort: "3456",
|
||||
}
|
||||
ci.NetworkSettings.Ports["443/tcp"][i] = nat.PortBinding{
|
||||
ci.NetworkSettings.Ports[network.MustParsePort("443/tcp")][i] = network.PortBinding{
|
||||
HostIP: ip, HostPort: "4567",
|
||||
}
|
||||
ci.NetworkSettings.Ports["443/udp"][i] = nat.PortBinding{
|
||||
ci.NetworkSettings.Ports[network.MustParsePort("443/udp")][i] = network.PortBinding{
|
||||
HostIP: ip, HostPort: "5678",
|
||||
}
|
||||
}
|
||||
return ci, nil
|
||||
return client.ContainerInspectResult{Container: ci}, nil
|
||||
},
|
||||
})
|
||||
cmd := NewPortCommand(cli)
|
||||
cmd := newPortCommand(cli)
|
||||
cmd.SetErr(io.Discard)
|
||||
cmd.SetArgs([]string{"some_container", tc.port})
|
||||
err := cmd.Execute()
|
||||
|
||||
@ -2,25 +2,33 @@ package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/docker/cli/cli/command/system/pruner"
|
||||
"github.com/docker/cli/internal/prompt"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/go-units"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Register the prune command to run as part of "docker system prune"
|
||||
if err := pruner.Register(pruner.TypeContainer, pruneFn); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
type pruneOptions struct {
|
||||
force bool
|
||||
filter opts.FilterOpt
|
||||
}
|
||||
|
||||
// NewPruneCommand returns a new cobra prune command for containers
|
||||
func NewPruneCommand(dockerCli command.Cli) *cobra.Command {
|
||||
// newPruneCommand returns a new cobra prune command for containers.
|
||||
func newPruneCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
options := pruneOptions{filter: opts.NewFilterOpt()}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
@ -28,18 +36,19 @@ func NewPruneCommand(dockerCli command.Cli) *cobra.Command {
|
||||
Short: "Remove all stopped containers",
|
||||
Args: cli.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
spaceReclaimed, output, err := runPrune(cmd.Context(), dockerCli, options)
|
||||
spaceReclaimed, output, err := runPrune(cmd.Context(), dockerCLI, options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if output != "" {
|
||||
fmt.Fprintln(dockerCli.Out(), output)
|
||||
fmt.Fprintln(dockerCLI.Out(), output)
|
||||
}
|
||||
fmt.Fprintln(dockerCli.Out(), "Total reclaimed space:", units.HumanSize(float64(spaceReclaimed)))
|
||||
fmt.Fprintln(dockerCLI.Out(), "Total reclaimed space:", units.HumanSize(float64(spaceReclaimed)))
|
||||
return nil
|
||||
},
|
||||
Annotations: map[string]string{"version": "1.25"},
|
||||
ValidArgsFunction: completion.NoComplete,
|
||||
Annotations: map[string]string{"version": "1.25"},
|
||||
ValidArgsFunction: cobra.NoFileCompletions,
|
||||
DisableFlagsInUseLine: true,
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
@ -65,17 +74,19 @@ func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions)
|
||||
}
|
||||
}
|
||||
|
||||
report, err := dockerCli.Client().ContainersPrune(ctx, pruneFilters)
|
||||
res, err := dockerCli.Client().ContainersPrune(ctx, client.ContainerPruneOptions{
|
||||
Filters: pruneFilters,
|
||||
})
|
||||
if err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
|
||||
if len(report.ContainersDeleted) > 0 {
|
||||
if len(res.Report.ContainersDeleted) > 0 {
|
||||
output = "Deleted Containers:\n"
|
||||
for _, id := range report.ContainersDeleted {
|
||||
for _, id := range res.Report.ContainersDeleted {
|
||||
output += id + "\n"
|
||||
}
|
||||
spaceReclaimed = report.SpaceReclaimed
|
||||
spaceReclaimed = res.Report.SpaceReclaimed
|
||||
}
|
||||
|
||||
return spaceReclaimed, output, nil
|
||||
@ -85,8 +96,16 @@ type cancelledErr struct{ error }
|
||||
|
||||
func (cancelledErr) Cancelled() {}
|
||||
|
||||
// RunPrune calls the Container Prune API
|
||||
// This returns the amount of space reclaimed and a detailed output string
|
||||
func RunPrune(ctx context.Context, dockerCli command.Cli, _ bool, filter opts.FilterOpt) (uint64, string, error) {
|
||||
return runPrune(ctx, dockerCli, pruneOptions{force: true, filter: filter})
|
||||
// pruneFn calls the Container Prune API for use in "docker system prune",
|
||||
// and returns the amount of space reclaimed and a detailed output string.
|
||||
func pruneFn(ctx context.Context, dockerCLI command.Cli, options pruner.PruneOptions) (uint64, string, error) {
|
||||
if !options.Confirmed {
|
||||
// Dry-run: perform validation and produce confirmation before pruning.
|
||||
confirmMsg := "all stopped containers"
|
||||
return 0, confirmMsg, cancelledErr{errors.New("containers prune has been cancelled")}
|
||||
}
|
||||
return runPrune(ctx, dockerCLI, pruneOptions{
|
||||
force: true,
|
||||
filter: options.Filter,
|
||||
})
|
||||
}
|
||||
|
||||
@ -7,8 +7,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/moby/moby/client"
|
||||
)
|
||||
|
||||
func TestContainerPrunePromptTermination(t *testing.T) {
|
||||
@ -16,11 +15,11 @@ func TestContainerPrunePromptTermination(t *testing.T) {
|
||||
t.Cleanup(cancel)
|
||||
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
containerPruneFunc: func(ctx context.Context, pruneFilters filters.Args) (container.PruneReport, error) {
|
||||
return container.PruneReport{}, errors.New("fakeClient containerPruneFunc should not be called")
|
||||
containerPruneFunc: func(ctx context.Context, opts client.ContainerPruneOptions) (client.ContainerPruneResult, error) {
|
||||
return client.ContainerPruneResult{}, errors.New("fakeClient containerPruneFunc should not be called")
|
||||
},
|
||||
})
|
||||
cmd := NewPruneCommand(cli)
|
||||
cmd := newPruneCommand(cli)
|
||||
cmd.SetArgs([]string{})
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
|
||||
@ -1,54 +1,36 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type renameOptions struct {
|
||||
oldName string
|
||||
newName string
|
||||
}
|
||||
|
||||
// NewRenameCommand creates a new cobra.Command for `docker rename`
|
||||
func NewRenameCommand(dockerCli command.Cli) *cobra.Command {
|
||||
var opts renameOptions
|
||||
|
||||
// newRenameCommand creates a new cobra.Command for "docker container rename".
|
||||
func newRenameCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "rename CONTAINER NEW_NAME",
|
||||
Short: "Rename a container",
|
||||
Args: cli.ExactArgs(2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.oldName = args[0]
|
||||
opts.newName = args[1]
|
||||
return runRename(cmd.Context(), dockerCli, &opts)
|
||||
oldName, newName := args[0], args[1]
|
||||
_, err := dockerCLI.Client().ContainerRename(cmd.Context(), oldName, client.ContainerRenameOptions{
|
||||
NewName: newName,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to rename container: %w", err)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"aliases": "docker container rename, docker rename",
|
||||
},
|
||||
ValidArgsFunction: completion.ContainerNames(dockerCli, true),
|
||||
ValidArgsFunction: completion.ContainerNames(dockerCLI, true),
|
||||
DisableFlagsInUseLine: true,
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runRename(ctx context.Context, dockerCli command.Cli, opts *renameOptions) error {
|
||||
oldName := strings.TrimSpace(opts.oldName)
|
||||
newName := strings.TrimSpace(opts.newName)
|
||||
|
||||
if oldName == "" || newName == "" {
|
||||
return errors.New("Error: Neither old nor new names may be empty")
|
||||
}
|
||||
|
||||
if err := dockerCli.Client().ContainerRename(ctx, oldName, newName); err != nil {
|
||||
fmt.Fprintln(dockerCli.Err(), err)
|
||||
return errors.Errorf("Error: failed to rename container named %s", oldName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -1,77 +0,0 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
|
||||
func TestRunRename(t *testing.T) {
|
||||
testcases := []struct {
|
||||
doc, oldName, newName, expectedErr string
|
||||
}{
|
||||
{
|
||||
doc: "success",
|
||||
oldName: "oldName",
|
||||
newName: "newName",
|
||||
expectedErr: "",
|
||||
},
|
||||
{
|
||||
doc: "empty old name",
|
||||
oldName: "",
|
||||
newName: "newName",
|
||||
expectedErr: "Error: Neither old nor new names may be empty",
|
||||
},
|
||||
{
|
||||
doc: "empty new name",
|
||||
oldName: "oldName",
|
||||
newName: "",
|
||||
expectedErr: "Error: Neither old nor new names may be empty",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testcases {
|
||||
t.Run(tc.doc, func(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
containerRenameFunc: func(ctx context.Context, oldName, newName string) error {
|
||||
return nil
|
||||
},
|
||||
})
|
||||
|
||||
cmd := NewRenameCommand(cli)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
cmd.SetArgs([]string{tc.oldName, tc.newName})
|
||||
|
||||
err := cmd.Execute()
|
||||
|
||||
if tc.expectedErr != "" {
|
||||
assert.ErrorContains(t, err, tc.expectedErr)
|
||||
} else {
|
||||
assert.NilError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunRenameClientError(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
containerRenameFunc: func(ctx context.Context, oldName, newName string) error {
|
||||
return errors.New("client error")
|
||||
},
|
||||
})
|
||||
|
||||
cmd := NewRenameCommand(cli)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
cmd.SetArgs([]string{"oldName", "newName"})
|
||||
|
||||
err := cmd.Execute()
|
||||
|
||||
assert.Check(t, is.Error(err, "Error: failed to rename container named oldName"))
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user