Compare commits
649 Commits
kubernetes
...
master
Author | SHA1 | Date |
---|---|---|
|
e7f17cb570 | |
|
29d75ee358 | |
|
8067f3a1a6 | |
|
483c28b281 | |
|
2e87981eff | |
|
f47e5c84cc | |
|
76e6818d8d | |
|
f07a946956 | |
|
45a8bb4283 | |
|
0b7534c13c | |
|
e3a465587f | |
|
e470ab29d4 | |
|
02bb9287e7 | |
|
8aac463c8e | |
|
142b144574 | |
|
7c82ae36f3 | |
|
d97446c293 | |
|
a8605c1ee2 | |
|
1d576b52f0 | |
|
a8c44498b7 | |
|
daa78b3004 | |
|
9892d492ab | |
|
fe571ee1bb | |
|
4966ba7250 | |
|
1c1291fbf7 | |
|
7e06b5277c | |
|
6b06b293f9 | |
|
da311e8e7e | |
|
2fa4a24584 | |
|
a6fde79de4 | |
|
2da25b2322 | |
|
110ba5998e | |
|
716737e46a | |
|
8a11631ed9 | |
|
330f86df8e | |
|
55101ca654 | |
|
ab62ac8cf1 | |
|
af5ade99d8 | |
|
4d27286e9c | |
|
9130183f39 | |
|
e808dbbf1f | |
|
579a7a5a35 | |
|
0b4adb247f | |
|
49afb3c466 | |
|
17c0dde6b1 | |
|
bc4c094b08 | |
|
800afb48a7 | |
|
2375a3a9f6 | |
|
18f24e791d | |
|
9c13527bac | |
|
17bb82b84d | |
|
abe43f6e92 | |
|
bb9c5182ea | |
|
4ee16d2b51 | |
|
edad3048e9 | |
|
46d6f63709 | |
|
8500d2979d | |
|
4b5ec542ad | |
|
ca5a831a47 | |
|
b011cffff8 | |
|
cb7efba696 | |
|
2be4847754 | |
|
6096dfa3cf | |
|
279ddf3abe | |
|
5ff92a69e3 | |
|
777f5e3cd1 | |
|
4afda566a9 | |
|
c37ca76b9c | |
|
47f13bd18b | |
|
88bb12ba04 | |
|
52ec1da081 | |
|
307936eb9d | |
|
90ee929b88 | |
|
105c831190 | |
|
d35aa2c630 | |
|
f4a8c5b53e | |
|
5bcd2add11 | |
|
bb3c0d9f3a | |
|
3323e167c5 | |
|
3013d81bdf | |
|
38e8d36c38 | |
|
b0f5f0c0aa | |
|
65d852d39c | |
|
7ec7bb7cc1 | |
|
722397942b | |
|
0cdb311ed6 | |
|
4d172bd365 | |
|
285ed6ce48 | |
|
4627533853 | |
|
ae92d5f0bd | |
|
3a0b77ee9b | |
|
9301b2a1f6 | |
|
d40094d4e4 | |
|
46f95a7c68 | |
|
b2ed890887 | |
|
265eadfda5 | |
|
6c0aa1995f | |
|
72b3a7e9b0 | |
|
2e566591a6 | |
|
fec9d5b3d5 | |
|
dc53668bca | |
|
399c585899 | |
|
ece3c8c1d0 | |
|
5b96de1a99 | |
|
5366de04e1 | |
|
7577f36fbc | |
|
f38b1de6c2 | |
|
0d5516dcdc | |
|
5cbdedb625 | |
|
6203603c4a | |
|
f97d1f5267 | |
|
90ba96e4f3 | |
|
ed22800c0c | |
|
920f676703 | |
|
88b66ece35 | |
|
5e1a193aef | |
|
e4ccb76c1a | |
|
277c37ab57 | |
|
2bbe460b18 | |
|
f630fabade | |
|
a108757aa8 | |
|
8c173253d9 | |
|
deef152435 | |
|
cfdaf20447 | |
|
8883001c90 | |
|
24bba46b9d | |
|
cab51f793f | |
|
a9d88dcafc | |
|
561b18e2ed | |
|
594231109f | |
|
1fe7110fa9 | |
|
3a69c59961 | |
|
246e544fc4 | |
|
ef54a58dd7 | |
|
71aa003817 | |
|
4ccb6a52b9 | |
|
480491c908 | |
|
f6d73c948b | |
|
f1e93ac324 | |
|
7f2f474232 | |
|
2e38fc2204 | |
|
2fd8e3617a | |
|
313e838cfa | |
|
e33a30ea7c | |
|
79fb20cb8b | |
|
316ed014ab | |
|
d74f0e1af1 | |
|
1ab40ed1c8 | |
|
6323c5bc57 | |
|
81562142de | |
|
bcafb59748 | |
|
8af785f4be | |
|
064a840924 | |
|
7346aee2d4 | |
|
d384e79d1a | |
|
7d49790b11 | |
|
7f667b45ca | |
|
cee2a595e4 | |
|
3e68cbc2c4 | |
|
6990be915a | |
|
1613733931 | |
|
be364695f5 | |
|
ddd9a046e7 | |
|
a8a00dbee1 | |
|
120105a0a9 | |
|
088003e485 | |
|
816d854fc8 | |
|
1a5fd321ec | |
|
575c21a9f0 | |
|
94246703f3 | |
|
74629f681a | |
|
86048014d6 | |
|
56c9d286fe | |
|
92bb3cdeda | |
|
beeec3d7a0 | |
|
40fde549ca | |
|
ee2ac712ef | |
|
e26c49a5f9 | |
|
9f75b795dc | |
|
348f12d905 | |
|
61dc2ecb38 | |
|
e2968fb8ad | |
|
608a0553d7 | |
|
169a952417 | |
|
d0bc9691f3 | |
|
133d258d8e | |
|
1811ebafa9 | |
|
7ff7f80add | |
|
6e4fe32a45 | |
|
9a565d149e | |
|
1f9df3421a | |
|
2bb31e1a0f | |
|
2cccb2df6a | |
|
4ced6667a4 | |
|
b7988e1775 | |
|
2ebf801b89 | |
|
b22c40d47c | |
|
4b9836ac7a | |
|
913dcdf388 | |
|
e91a2f1400 | |
|
2fceb953a8 | |
|
dc24746c3a | |
|
6989e5df8d | |
|
739aca7b89 | |
|
7ec5989b2d | |
|
c5abf9ddc7 | |
|
a886863a93 | |
|
4532c53760 | |
|
3cd67e0222 | |
|
69f03b081e | |
|
b67296a41b | |
|
3553001de4 | |
|
c7e911a6b5 | |
|
bb485d40d9 | |
|
dc6975b78c | |
|
af49e676b2 | |
|
a499023358 | |
|
1014b1d5f2 | |
|
816c382177 | |
|
4c8c153513 | |
|
484ede079f | |
|
73f8c2980c | |
|
87fbd1d830 | |
|
f829c4b789 | |
|
1b29da9296 | |
|
80dc149179 | |
|
84e8e731c9 | |
|
c1f9faf7c3 | |
|
c90e4a0fd8 | |
|
86e0d9e48c | |
|
3aac470db0 | |
|
1d2b08a308 | |
|
55b0096f22 | |
|
fb82836788 | |
|
9e16ef8eda | |
|
fa1f5fc526 | |
|
a0488cfbe5 | |
|
184746c687 | |
|
7ed2361d8a | |
|
983dddad9d | |
|
2bb291358c | |
|
7c89fa5dca | |
|
0f462824c7 | |
|
c329ccff7c | |
|
4106fc8791 | |
|
f3a7f811d0 | |
|
5f5894cd61 | |
|
867c9194ad | |
|
4315ad9aaa | |
|
36d3a02b4d | |
|
d6ad8b12b7 | |
|
4d5b682af1 | |
|
5d2e9155d4 | |
|
e4a11c9548 | |
|
c0e80201dc | |
|
6c52e7fa5d | |
|
bbfe64c1ac | |
|
f92e952318 | |
|
cfb4ff091f | |
|
a0d920bb63 | |
|
20877bc489 | |
|
5e52c27b4e | |
|
90452b2e92 | |
|
650e81b1f2 | |
|
262825a8a6 | |
|
fee9d648a7 | |
|
1945b46b05 | |
|
36d480f43c | |
|
3aa701d129 | |
|
111ebd712e | |
|
11efae9ae4 | |
|
9ca6564d24 | |
|
e13117fcac | |
|
57edd7ccc7 | |
|
7640fec64e | |
|
b2a7d2626d | |
|
9a0e211708 | |
|
dca3776905 | |
|
14f6a11dd8 | |
|
b0a9adb1c6 | |
|
9035822a02 | |
|
c3d194bcd2 | |
|
b06b2ef141 | |
|
e1b9b58b72 | |
|
c4be63c54b | |
|
bd15ba4612 | |
|
4a18df8277 | |
|
2c705c6f91 | |
|
823a551020 | |
|
d80cbcdb4d | |
|
cf5c214b25 | |
|
c63fb82d09 | |
|
ee337ad2d6 | |
|
03a826a301 | |
|
4fe61440d3 | |
|
3f1b31120b | |
|
8f2f49349b | |
|
619e8f9f0d | |
|
acdf12bd01 | |
|
2e31adb070 | |
|
05a8bf872e | |
|
b315eb8455 | |
|
2c588bc5ed | |
|
b4d17b87f5 | |
|
60dccb9d13 | |
|
072e9981f1 | |
|
d0ed078ea5 | |
|
58672617cf | |
|
15aefcb085 | |
|
eaa3175fb1 | |
|
78100b683e | |
|
df17d35554 | |
|
a6de79e3d4 | |
|
a421f02b7c | |
|
07726d29e8 | |
|
68dbd0767a | |
|
1c0cdd03d9 | |
|
5eeba3a134 | |
|
1a58d7f437 | |
|
b3015b8e3e | |
|
b2d7179a03 | |
|
69ef2e6a97 | |
|
172bb858a6 | |
|
2f6a8aff11 | |
|
980dabe2f2 | |
|
3b755991af | |
|
582c9dabb0 | |
|
e91710120d | |
|
770b201e9c | |
|
dcb1192020 | |
|
42f728c3d6 | |
|
872eb0883e | |
|
c5396bb9d0 | |
|
a9fe9eb5f7 | |
|
269ced5ce8 | |
|
6c46eadd33 | |
|
7e6366765e | |
|
d5a8f05803 | |
|
3f7d004bdf | |
|
f508e12184 | |
|
6921b6008e | |
|
030f865b1b | |
|
d187b13731 | |
|
8a8cd02be0 | |
|
97bc4a2eb7 | |
|
d087fa13f4 | |
|
e799af9379 | |
|
13611d4701 | |
|
3802ec81a2 | |
|
ce7ca659e3 | |
|
0b359223b4 | |
|
041879def4 | |
|
cefe6cb0a0 | |
|
157dc2e404 | |
|
81ad0ffc39 | |
|
209732ca71 | |
|
25c985cf66 | |
|
4657409ea3 | |
|
290ff2d971 | |
|
f74c97d4ab | |
|
21275cf4d0 | |
|
b57bb4f06e | |
|
f183fdb3b3 | |
|
6b4730ae70 | |
|
79f145bee1 | |
|
24a31a929b | |
|
d3ad753245 | |
|
627b5dabb0 | |
|
382ad8ff11 | |
|
e06e0c6134 | |
|
ff551a76e3 | |
|
e5da5e005a | |
|
5b2540dc63 | |
|
dec98f7709 | |
|
b56ea4761e | |
|
8941dd6b94 | |
|
85119a1e56 | |
|
fe3be32c18 | |
|
0bd9881b18 | |
|
df7520c1f3 | |
|
7d69e0104e | |
|
a4551ef19a | |
|
55821cad07 | |
|
b31dce3246 | |
|
8d8b5a6120 | |
|
5b7c8b24b4 | |
|
bfb7f4aa02 | |
|
2e9d71e05d | |
|
b9d568d7e4 | |
|
5450795bac | |
|
514f46729f | |
|
8288417631 | |
|
57112bd241 | |
|
b1dfef4f42 | |
|
0f357874d3 | |
|
f825bd0b72 | |
|
6985b81ab4 | |
|
5f33246cf8 | |
|
649e09882b | |
|
5aca4630e4 | |
|
771a13a844 | |
|
5ff591adc6 | |
|
3d99e3ab7e | |
|
c572c1d825 | |
|
4f380d07c5 | |
|
801b2f2907 | |
|
150571ea52 | |
|
4196229029 | |
|
852454f7f7 | |
|
1cb4e5b9e2 | |
|
d0e936c703 | |
|
94aa535f7b | |
|
41b0711fd3 | |
|
32e71fd995 | |
|
f0276dce1a | |
|
04bb64c802 | |
|
389b2eeaf6 | |
|
54dfe4c559 | |
|
b465426392 | |
|
adfcfcf7c1 | |
|
25dbaefaa9 | |
|
eded7a36b1 | |
|
426055bedd | |
|
0140ee6944 | |
|
bea8dbbff9 | |
|
41e94233a1 | |
|
31199ade16 | |
|
3e85d33843 | |
|
eb98199dbe | |
|
64300085bb | |
|
d982873ba4 | |
|
90ec7ffac4 | |
|
f8b298459c | |
|
10fe45a704 | |
|
6c2c451048 | |
|
2f762a69a2 | |
|
713b7e79c0 | |
|
6b5d9dfa70 | |
|
9957529f9c | |
|
916a6ea031 | |
|
64a1fe556b | |
|
9a6ad3e08e | |
|
49f3ec973c | |
|
733ee4d85e | |
|
14d23cc879 | |
|
2855ed117e | |
|
18888dae3e | |
|
1ac3b7558a | |
|
858d3a5b72 | |
|
ecc9600ea2 | |
|
0f1712d674 | |
|
c39218876d | |
|
fb0d60bd81 | |
|
0184416a0c | |
|
dc117ccb86 | |
|
a17735c5a5 | |
|
ec7f2aed54 | |
|
2b8ea3563c | |
|
acf4a09f2d | |
|
10f0d20be6 | |
|
18e7a287d1 | |
|
3217641329 | |
|
0e715866c7 | |
|
a03f363d8c | |
|
c689318b81 | |
|
c077418362 | |
|
65a57a36aa | |
|
2ee8f95181 | |
|
c4d840c46e | |
|
16b821ae90 | |
|
9d1817215f | |
|
f17e427a18 | |
|
60b309860d | |
|
b73518af09 | |
|
97bd96adbc | |
|
9e4e16118e | |
|
52a1a774ea | |
|
22db5a11df | |
|
3a4f05a7c9 | |
|
d178a0367b | |
|
3731515d71 | |
|
2d05e1425e | |
|
136d701b1b | |
|
6672d5762a | |
|
9079505b81 | |
|
654e5eb2dc | |
|
7cd57900bf | |
|
8073c5e9c0 | |
|
2687c08c5c | |
|
b54e299c96 | |
|
66888859bf | |
|
fc575c8a25 | |
|
3e5acb915d | |
|
da83f8c3d7 | |
|
a01a48e661 | |
|
bd6c3274b0 | |
|
7ee7011bb3 | |
|
2f8da6f89c | |
|
8b51c4efe3 | |
|
2bb798d735 | |
|
e7a74669e6 | |
|
9767edf07e | |
|
66d8b6a16b | |
|
d8e26d70e5 | |
|
c64d77d39d | |
|
b84e274c59 | |
|
cb3e3dd9f6 | |
|
2f794d398e | |
|
7af2fcf712 | |
|
03a9d1b276 | |
|
b7a900337d | |
|
7f19a8d4f7 | |
|
4bcd969a9e | |
|
4afdc439d1 | |
|
5c2e0d7c4c | |
|
9c3c55a263 | |
|
3bdfe98073 | |
|
bac5c6b559 | |
|
54c3cc2496 | |
|
ac02b740ef | |
|
b35935138f | |
|
1aab366c00 | |
|
d77d3a1e72 | |
|
6a876632fa | |
|
5ef5a91cb3 | |
|
057aff35d1 | |
|
f01cf14f7e | |
|
57a70fba93 | |
|
cebf2416d4 | |
|
b23bdc08c6 | |
|
351ba8bb1c | |
|
931e867953 | |
|
d034b745b4 | |
|
8c8a04f6d0 | |
|
9a67ed3d47 | |
|
03a47d93ec | |
|
8b0ab9a40e | |
|
539f801f01 | |
|
5a39ea02a1 | |
|
468fb1fd0c | |
|
b32cbdf03a | |
|
5cdf0dd00c | |
|
fe1c2e26c6 | |
|
348bd97228 | |
|
e22d02e126 | |
|
bcc98ec471 | |
|
e8aab9cee9 | |
|
e08a7605f8 | |
|
74689ab597 | |
|
438887afb3 | |
|
d574b30d56 | |
|
22cc602cb3 | |
|
2ccb201c19 | |
|
bdf68c0441 | |
|
c2f0957625 | |
|
382968b0b0 | |
|
98ee3cbdfe | |
|
6bda16c651 | |
|
62da289db3 | |
|
430425021b | |
|
12b2dc638b | |
|
d0e3ed461d | |
|
a469079b0a | |
|
ce4811fc2f | |
|
7b29ca0b6c | |
|
4480efc1fb | |
|
30ffb2dadb | |
|
eb3138bd9f | |
|
a623ac040f | |
|
7f2868f3ed | |
|
507fbad069 | |
|
6060f231cb | |
|
53059d67bc | |
|
6a40eadf4b | |
|
d35c2e8ddf | |
|
f0c21d5c66 | |
|
4b4bed25b3 | |
|
b2260abf49 | |
|
582f2b8418 | |
|
654e18cb7b | |
|
b67052c752 | |
|
302f330c87 | |
|
de9ccdd79b | |
|
7ce6599fc1 | |
|
8b210a3504 | |
|
f7937ee1e4 | |
|
7de41ef887 | |
|
564480672d | |
|
19409f86a9 | |
|
c96e079228 | |
|
a041ac5115 | |
|
55333c0817 | |
|
67ef0ca456 | |
|
601fbee0aa | |
|
84a37fbc5d | |
|
021f2d92ba | |
|
898fa15b6a | |
|
3eb40ffbb0 | |
|
cacd4f8556 | |
|
35d73b6cda | |
|
64fc8b1f67 | |
|
969a47ceed | |
|
eda2d31bdd | |
|
33795a4293 | |
|
663031e23e | |
|
ac00f9091c | |
|
d17fe37375 | |
|
cb14bf83e4 | |
|
fba54d4820 | |
|
f579e1a195 | |
|
4f608e3655 | |
|
2f46880ac6 | |
|
90963a2f06 | |
|
a51ca7c43d | |
|
a7dd95d9e9 | |
|
3b64cc566e | |
|
626d956ab6 | |
|
3225b852aa | |
|
30772740ee | |
|
2a258862e4 | |
|
fbb786d1e3 | |
|
1fb897b3e0 | |
|
71b74708cf | |
|
188da45fa7 | |
|
6466bd3b4e | |
|
53768a14a7 | |
|
296dae746e | |
|
e01bb717e0 | |
|
faf0c761da | |
|
826006cdb9 | |
|
bc9397b64f | |
|
f8c6b2ac72 | |
|
0266cec8bc | |
|
5aeb89205f | |
|
3d8ca1bc77 | |
|
1e115c1960 | |
|
366796b7e2 | |
|
f5a7ea080f | |
|
af716b7f7b | |
|
4729836f7f | |
|
eb76fe466c | |
|
1b4373f26b | |
|
5691f5e6f9 | |
|
04a8b4e325 | |
|
e112354038 | |
|
1d05cd7a4e | |
|
17964a895f | |
|
0ee0d225f2 | |
|
58079640cc |
|
@ -10,6 +10,7 @@
|
|||
# DO NOT REPORT SECURITY VULNERABILITIES DIRECTLY TO THESE NAMES, FOLLOW THE
|
||||
# INSTRUCTIONS AT https://kubernetes.io/security/
|
||||
|
||||
pwittrock
|
||||
seans3
|
||||
ardaguclu
|
||||
eddiezane
|
||||
mpuckett159
|
||||
soltysh
|
||||
|
|
2
doc.go
2
doc.go
|
@ -14,4 +14,4 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
package kubectl // import "k8s.io/kubectl"
|
||||
package kubectl
|
||||
|
|
|
@ -37,7 +37,7 @@ Look for:
|
|||
|
||||
### Test triage
|
||||
|
||||
Monitor [test grid](https://k8s-testgrid.appspot.com/sig-cli-master)
|
||||
Monitor [test grid](https://testgrid.k8s.io/sig-cli-master)
|
||||
and make sure the tests are passing.
|
||||
|
||||
If any tests are failing, debug them and send a fix. Ask for help if you get stuck.
|
||||
|
|
131
go.mod
131
go.mod
|
@ -2,106 +2,97 @@
|
|||
|
||||
module k8s.io/kubectl
|
||||
|
||||
go 1.20
|
||||
go 1.24.0
|
||||
|
||||
godebug default=go1.24
|
||||
|
||||
require (
|
||||
github.com/MakeNowJust/heredoc v1.0.0
|
||||
github.com/chai2010/gettext-go v1.0.2
|
||||
github.com/daviddengcn/go-colortext v1.0.0
|
||||
github.com/docker/distribution v2.8.2+incompatible
|
||||
github.com/evanphx/json-patch v5.6.0+incompatible
|
||||
github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d
|
||||
github.com/distribution/reference v0.6.0
|
||||
github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f
|
||||
github.com/fatih/camelcase v1.0.0
|
||||
github.com/fvbommel/sortorder v1.1.0
|
||||
github.com/go-openapi/jsonreference v0.20.2
|
||||
github.com/google/gnostic-models v0.6.8
|
||||
github.com/google/go-cmp v0.5.9
|
||||
github.com/jonboulle/clockwork v0.2.2
|
||||
github.com/google/gnostic-models v0.7.0
|
||||
github.com/google/go-cmp v0.7.0
|
||||
github.com/jonboulle/clockwork v0.5.0
|
||||
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de
|
||||
github.com/lithammer/dedent v1.1.0
|
||||
github.com/mitchellh/go-wordwrap v1.0.1
|
||||
github.com/moby/term v0.0.0-20221205130635-1aeaba878587
|
||||
github.com/onsi/ginkgo/v2 v2.9.4
|
||||
github.com/onsi/gomega v1.27.6
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/moby/term v0.5.0
|
||||
github.com/onsi/ginkgo/v2 v2.21.0
|
||||
github.com/onsi/gomega v1.35.1
|
||||
github.com/russross/blackfriday/v2 v2.1.0
|
||||
github.com/spf13/cobra v1.7.0
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/stretchr/testify v1.8.2
|
||||
golang.org/x/sys v0.10.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
k8s.io/api v0.0.0
|
||||
k8s.io/apimachinery v0.0.0
|
||||
k8s.io/cli-runtime v0.0.0
|
||||
k8s.io/client-go v0.0.0
|
||||
k8s.io/component-base v0.0.0
|
||||
k8s.io/component-helpers v0.0.0
|
||||
k8s.io/klog/v2 v2.100.1
|
||||
k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9
|
||||
k8s.io/metrics v0.0.0
|
||||
k8s.io/utils v0.0.0-20230406110748-d93618cff8a2
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd
|
||||
sigs.k8s.io/kustomize/kustomize/v5 v5.0.4-0.20230601165947-6ce0bf390ce3
|
||||
sigs.k8s.io/kustomize/kyaml v0.14.3-0.20230601165947-6ce0bf390ce3
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.2.3
|
||||
sigs.k8s.io/yaml v1.3.0
|
||||
github.com/spf13/cobra v1.9.1
|
||||
github.com/spf13/pflag v1.0.6
|
||||
github.com/stretchr/testify v1.10.0
|
||||
go.yaml.in/yaml/v2 v2.4.2
|
||||
golang.org/x/sys v0.31.0
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0
|
||||
k8s.io/api v0.0.0-20250718010531-33ab3a26f4b3
|
||||
k8s.io/apimachinery v0.0.0-20250717210244-b92abb2d8139
|
||||
k8s.io/cli-runtime v0.0.0-20250717174531-64776d0a280f
|
||||
k8s.io/client-go v0.0.0-20250718010928-be36413bbca7
|
||||
k8s.io/component-base v0.0.0-20250717172125-4e07767df717
|
||||
k8s.io/component-helpers v0.0.0-20250717172249-5095859f5100
|
||||
k8s.io/klog/v2 v2.130.1
|
||||
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b
|
||||
k8s.io/metrics v0.0.0-20250717174355-244095fcc1c1
|
||||
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397
|
||||
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8
|
||||
sigs.k8s.io/kustomize/kustomize/v5 v5.6.0
|
||||
sigs.k8s.io/kustomize/kyaml v0.19.0
|
||||
sigs.k8s.io/randfill v1.0.0
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0
|
||||
sigs.k8s.io/yaml v1.5.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
|
||||
github.com/blang/semver/v4 v4.0.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.9.0 // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.12.2 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.8.0 // indirect
|
||||
github.com/go-errors/errors v1.4.2 // indirect
|
||||
github.com/go-logr/logr v1.2.4 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.6 // indirect
|
||||
github.com/go-openapi/swag v0.22.3 // indirect
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||
github.com/go-openapi/swag v0.23.0 // indirect
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/google/btree v1.0.1 // indirect
|
||||
github.com/google/gofuzz v1.2.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect
|
||||
github.com/google/btree v1.1.3 // indirect
|
||||
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db // indirect
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect
|
||||
github.com/imdario/mergo v0.3.6 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
|
||||
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/moby/spdystream v0.2.0 // indirect
|
||||
github.com/moby/spdystream v0.5.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
|
||||
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/xlab/treeprint v1.2.0 // indirect
|
||||
go.starlark.net v0.0.0-20230525235612-a134d8f9ddca // indirect
|
||||
golang.org/x/net v0.13.0 // indirect
|
||||
golang.org/x/oauth2 v0.8.0 // indirect
|
||||
golang.org/x/sync v0.2.0 // indirect
|
||||
golang.org/x/term v0.10.0 // indirect
|
||||
golang.org/x/text v0.11.0 // indirect
|
||||
golang.org/x/time v0.3.0 // indirect
|
||||
golang.org/x/tools v0.8.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/protobuf v1.30.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/net v0.38.0 // indirect
|
||||
golang.org/x/oauth2 v0.27.0 // indirect
|
||||
golang.org/x/sync v0.12.0 // indirect
|
||||
golang.org/x/term v0.30.0 // indirect
|
||||
golang.org/x/text v0.23.0 // indirect
|
||||
golang.org/x/time v0.9.0 // indirect
|
||||
golang.org/x/tools v0.26.0 // indirect
|
||||
google.golang.org/protobuf v1.36.5 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
sigs.k8s.io/kustomize/api v0.13.5-0.20230601165947-6ce0bf390ce3 // indirect
|
||||
sigs.k8s.io/kustomize/api v0.19.0 // indirect
|
||||
)
|
||||
|
||||
replace (
|
||||
k8s.io/api => ../api
|
||||
k8s.io/apimachinery => ../apimachinery
|
||||
k8s.io/cli-runtime => ../cli-runtime
|
||||
k8s.io/client-go => ../client-go
|
||||
k8s.io/code-generator => ../code-generator
|
||||
k8s.io/component-base => ../component-base
|
||||
k8s.io/component-helpers => ../component-helpers
|
||||
k8s.io/kubectl => ../kubectl
|
||||
k8s.io/metrics => ../metrics
|
||||
)
|
||||
replace k8s.io/code-generator => k8s.io/code-generator v0.0.0-20250718051115-9eb96548a40e
|
||||
|
|
342
go.sum
342
go.sum
|
@ -1,119 +1,67 @@
|
|||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
|
||||
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
|
||||
github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
|
||||
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
|
||||
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk=
|
||||
github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
|
||||
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/daviddengcn/go-colortext v1.0.0 h1:ANqDyC0ys6qCSvuEK7l3g5RaehL/Xck9EX8ATG8oKsE=
|
||||
github.com/daviddengcn/go-colortext v1.0.0/go.mod h1:zDqEI5NVUop5QPpVJUxE9UO10hRnmkD5G4Pmri9+m4c=
|
||||
github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8=
|
||||
github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/emicklei/go-restful/v3 v3.9.0 h1:XwGDlfxEnQZzuopoqxwSEllNcCOM9DhhFyhFIIGKwxE=
|
||||
github.com/emicklei/go-restful/v3 v3.9.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U=
|
||||
github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
|
||||
github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d h1:105gxyaGwCFad8crR9dcMQWvV9Hvulu6hwUh4tWPJnM=
|
||||
github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4=
|
||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU=
|
||||
github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||
github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4=
|
||||
github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc=
|
||||
github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8=
|
||||
github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc=
|
||||
github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/fvbommel/sortorder v1.1.0 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQmYw=
|
||||
github.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0=
|
||||
github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU=
|
||||
github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
|
||||
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
|
||||
github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
|
||||
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-logr/zapr v1.2.3/go.mod h1:eIauM6P8qSvTw5o2ez6UEAfGjQKrxQTl5EoK+Qa2oG4=
|
||||
github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
|
||||
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
|
||||
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
|
||||
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
|
||||
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
|
||||
github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g=
|
||||
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
|
||||
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
|
||||
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golangplus/bytes v0.0.0-20160111154220-45c989fe5450/go.mod h1:Bk6SMAONeMXrxql8uvOKuAZSu8aM5RUGv+1C6IJaEho=
|
||||
github.com/golangplus/bytes v1.0.0/go.mod h1:AdRaCFwmc/00ZzELMWb01soso6W1R/++O1XL80yAn+A=
|
||||
github.com/golangplus/fmt v1.0.0/go.mod h1:zpM0OfbMCjPtd2qkTD/jX2MgiFCqklhSUFyDW44gVQE=
|
||||
github.com/golangplus/testing v1.0.0 h1:+ZeeiKZENNOMkTTELoSySazi+XaEhVO0mb+eanrSEUQ=
|
||||
github.com/golangplus/testing v1.0.0/go.mod h1:ZDreixUV3YzhoVraIDyOzHrr76p6NUh6k/pPg/Q3gYA=
|
||||
github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
|
||||
github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
|
||||
github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I=
|
||||
github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
|
||||
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||
github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
|
||||
github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec=
|
||||
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo=
|
||||
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM=
|
||||
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28=
|
||||
github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
|
||||
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA=
|
||||
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ=
|
||||
github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
|
||||
github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
|
||||
github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
|
@ -133,28 +81,28 @@ github.com/lithammer/dedent v1.1.0 h1:VNzHMVCBNG1j0fh3OrsFRkVUwStdDArbgBWoPAffkt
|
|||
github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
||||
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
|
||||
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
|
||||
github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8=
|
||||
github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c=
|
||||
github.com/moby/term v0.0.0-20221205130635-1aeaba878587 h1:HfkjXDfhgVaN5rmueG8cL8KKeFNecRCXFhaJ2qZ5SKA=
|
||||
github.com/moby/term v0.0.0-20221205130635-1aeaba878587/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
|
||||
github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU=
|
||||
github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI=
|
||||
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
|
||||
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0=
|
||||
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus=
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
|
||||
github.com/onsi/ginkgo/v2 v2.9.4 h1:xR7vG4IXt5RWx6FfIjyAtsoMAtnc3C/rFXBBd2AjZwE=
|
||||
github.com/onsi/ginkgo/v2 v2.9.4/go.mod h1:gCQYp2Q+kSoIj7ykSVb9nskRSsR6PUj4AiLywzIhbKM=
|
||||
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
|
||||
github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
|
||||
github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM=
|
||||
github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
|
||||
github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4=
|
||||
github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI=
|
||||
|
@ -163,176 +111,126 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
|||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
|
||||
github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY=
|
||||
github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
|
||||
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
|
||||
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
|
||||
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ=
|
||||
github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.35.1/go.mod h1:9NiG9I2aHTKkcxqCILhjtyNA1QEiCjdBACv4IvrFQ+c=
|
||||
go.opentelemetry.io/otel v1.10.0/go.mod h1:NbvWjCthWHKBEUMpf0/v8ZRZlni86PpGFEMA9pnQSnQ=
|
||||
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.10.0/go.mod h1:78XhIg8Ht9vR4tbLNUhXsiOnE2HOuSeKAiAcoVQEpOY=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.10.0/go.mod h1:Krqnjl22jUJ0HgMzw5eveuCvFDXY4nSYb4F8t5gdrag=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.10.0/go.mod h1:OfUCyyIiDvNXHWpcWgbF+MWvqPZiNa3YDEnivcnYsV0=
|
||||
go.opentelemetry.io/otel/metric v0.31.0/go.mod h1:ohmwj9KTSIeBnDBm/ZwH2PSZxZzoOaG2xZeekTRzL5A=
|
||||
go.opentelemetry.io/otel/sdk v1.10.0/go.mod h1:vO06iKzD5baltJz1zarxMCNHFpUlUiOy4s65ECtn6kE=
|
||||
go.opentelemetry.io/otel/trace v1.10.0/go.mod h1:Sij3YYczqAdz+EhmGhE6TpTxUO5/F/AzrK+kxfGqySM=
|
||||
go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
|
||||
go.starlark.net v0.0.0-20230525235612-a134d8f9ddca h1:VdD38733bfYv5tUZwEIskMM93VanwNIi5bIKnDrJdEY=
|
||||
go.starlark.net v0.0.0-20230525235612-a134d8f9ddca/go.mod h1:jxU+3+j+71eXOW14274+SmmuW82qJzl6iZSeqEtTGds=
|
||||
go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
|
||||
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.13.0 h1:Nvo8UFsZ8X3BhAC9699Z1j7XQ3rsZnUUm7jfBEk1ueY=
|
||||
golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8=
|
||||
golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
||||
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
|
||||
golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI=
|
||||
golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
|
||||
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c=
|
||||
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
|
||||
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
|
||||
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
|
||||
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
||||
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
|
||||
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.8.0 h1:vSDcovVPld282ceKgDimkRSC8kpaH1dgyc9UMzlt84Y=
|
||||
golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4=
|
||||
golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
|
||||
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
|
||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20230526161137-0005af68ea54/go.mod h1:zqTuNwFlFRsw5zIts5VnzLQxSRqh+CGOTVMlYbY0Eyk=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230525234035-dd9d682886f9/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
||||
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
|
||||
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
|
||||
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
k8s.io/gengo v0.0.0-20220902162205-c0856e24416d/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E=
|
||||
k8s.io/klog/v2 v2.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg=
|
||||
k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
|
||||
k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 h1:LyMgNKD2P8Wn1iAwQU5OhxCKlKJy0sHc+PcDwFB24dQ=
|
||||
k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9/go.mod h1:wZK2AVp1uHCp4VamDVgBP2COHZjqD1T68Rf0CM3YjSM=
|
||||
k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 h1:qY1Ad8PODbnymg2pRbkyMT/ylpTrCM8P2RJ0yroCyIk=
|
||||
k8s.io/utils v0.0.0-20230406110748-d93618cff8a2/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
|
||||
sigs.k8s.io/kustomize/api v0.13.5-0.20230601165947-6ce0bf390ce3 h1:XX3Ajgzov2RKUdc5jW3t5jwY7Bo7dcRm+tFxT+NfgY0=
|
||||
sigs.k8s.io/kustomize/api v0.13.5-0.20230601165947-6ce0bf390ce3/go.mod h1:9n16EZKMhXBNSiUC5kSdFQJkdH3zbxS/JoO619G1VAY=
|
||||
sigs.k8s.io/kustomize/cmd/config v0.11.2/go.mod h1:PCpHxyu10daTnbMfn3xhH1vppn7L8jsS3qpRKXb7Lkc=
|
||||
sigs.k8s.io/kustomize/kustomize/v5 v5.0.4-0.20230601165947-6ce0bf390ce3 h1:vq2TtoDcQomhy7OxXLUOzSbHMuMYq0Bjn93cDtJEdKw=
|
||||
sigs.k8s.io/kustomize/kustomize/v5 v5.0.4-0.20230601165947-6ce0bf390ce3/go.mod h1:/d88dHCvoy7d0AKFT0yytezSGZKjsZBVs9YTkBHSGFk=
|
||||
sigs.k8s.io/kustomize/kyaml v0.14.3-0.20230601165947-6ce0bf390ce3 h1:W6cLQc5pnqM7vh3b7HvGNfXrJ/xL6BDMS0v1V/HHg5U=
|
||||
sigs.k8s.io/kustomize/kyaml v0.14.3-0.20230601165947-6ce0bf390ce3/go.mod h1:JWP1Fj0VWGHyw3YUPjXSQnRnrwezrZSrApfX5S0nIag=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E=
|
||||
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
|
||||
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
|
||||
k8s.io/api v0.0.0-20250718010531-33ab3a26f4b3 h1:UnuyCQyBmdFlYypApF2w6Ld0R0kAt8b+0Lt9dYAr23I=
|
||||
k8s.io/api v0.0.0-20250718010531-33ab3a26f4b3/go.mod h1:K8dwhtttsRR0RHeSRF8XQ77gfMgyAj3q78/TkxEXhoc=
|
||||
k8s.io/apimachinery v0.0.0-20250717210244-b92abb2d8139 h1:jWBClrBPuk+GEA9pJzMa9IvxncSBbw7fmvey15nVm0w=
|
||||
k8s.io/apimachinery v0.0.0-20250717210244-b92abb2d8139/go.mod h1:v1p1Jsze3IHLy5gU17yVqR2qLO7jgYeX6mw3HZy2AEU=
|
||||
k8s.io/cli-runtime v0.0.0-20250717174531-64776d0a280f h1:E/GB1lzzKbz3HPJ6Zu1bJYrey6oDAIAA+RMEozCpPpU=
|
||||
k8s.io/cli-runtime v0.0.0-20250717174531-64776d0a280f/go.mod h1:SybB6wdHGt8FXxaHyNQqsUAhWcZKIDPurWPB5mfFLD0=
|
||||
k8s.io/client-go v0.0.0-20250718010928-be36413bbca7 h1:LNOJkn+3JlAEzdZzYheQM97gq6kKQfkrBN0GikI5nbc=
|
||||
k8s.io/client-go v0.0.0-20250718010928-be36413bbca7/go.mod h1:a14VvgYhux7oUSE9mWdzBuFKDZSGtperboMjQ1JtVgc=
|
||||
k8s.io/component-base v0.0.0-20250717172125-4e07767df717 h1:07oqkM0FzuGUw/bJw2rJubzccG7ShpGcTJ7SBDGp5Fc=
|
||||
k8s.io/component-base v0.0.0-20250717172125-4e07767df717/go.mod h1:/ehREU84M2OxVgU8WfxuUIi4/c5XsT6rIsEGQfhgxEQ=
|
||||
k8s.io/component-helpers v0.0.0-20250717172249-5095859f5100 h1:XEHmjwZgMNRuVgpqaRH/RR+n4BU0evfitU0RpWGPMUM=
|
||||
k8s.io/component-helpers v0.0.0-20250717172249-5095859f5100/go.mod h1:yxuY+YMknW7H9Bj7B29INyMOacJBa6oEG7gi7IKUzEQ=
|
||||
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
|
||||
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
||||
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA=
|
||||
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts=
|
||||
k8s.io/metrics v0.0.0-20250717174355-244095fcc1c1 h1:V4I6U/hfhZYYmDotL7ukG0nua1luMQSox5QtveZaSv0=
|
||||
k8s.io/metrics v0.0.0-20250717174355-244095fcc1c1/go.mod h1:TkHVkU+vMKy7qppbMybraSCK8Y+LLpoqk/6Jl+M8EoU=
|
||||
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y=
|
||||
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE=
|
||||
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
|
||||
sigs.k8s.io/kustomize/api v0.19.0 h1:F+2HB2mU1MSiR9Hp1NEgoU2q9ItNOaBJl0I4Dlus5SQ=
|
||||
sigs.k8s.io/kustomize/api v0.19.0/go.mod h1:/BbwnivGVcBh1r+8m3tH1VNxJmHSk1PzP5fkP6lbL1o=
|
||||
sigs.k8s.io/kustomize/kustomize/v5 v5.6.0 h1:MWtRRDWCwQEeW2rnJTqJMuV6Agy56P53SkbVoJpN7wA=
|
||||
sigs.k8s.io/kustomize/kustomize/v5 v5.6.0/go.mod h1:XuuZiQF7WdcvZzEYyNww9A0p3LazCKeJmCjeycN8e1I=
|
||||
sigs.k8s.io/kustomize/kyaml v0.19.0 h1:RFge5qsO1uHhwJsu3ipV7RNolC7Uozc0jUBC/61XSlA=
|
||||
sigs.k8s.io/kustomize/kyaml v0.19.0/go.mod h1:FeKD5jEOH+FbZPpqUghBP8mrLjJ3+zD3/rf9NNu1cwY=
|
||||
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
|
||||
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco=
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
|
||||
sigs.k8s.io/yaml v1.5.0 h1:M10b2U7aEUY6hRtU870n2VTPgR5RZiL/I6Lcc2F4NUQ=
|
||||
sigs.k8s.io/yaml v1.5.0/go.mod h1:wZs27Rbxoai4C0f8/9urLZtZtF3avA3gKvGyPdDqTO4=
|
||||
|
|
|
@ -21,8 +21,8 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
|
||||
jsonpatch "github.com/evanphx/json-patch"
|
||||
"github.com/spf13/cobra"
|
||||
jsonpatch "gopkg.in/evanphx/json-patch.v4"
|
||||
"k8s.io/klog/v2"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
|
@ -32,6 +32,8 @@ import (
|
|||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/json"
|
||||
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/cli-runtime/pkg/genericiooptions"
|
||||
"k8s.io/cli-runtime/pkg/printers"
|
||||
|
@ -227,7 +229,7 @@ func (flags *AnnotateFlags) ToOptions(f cmdutil.Factory, cmd *cobra.Command, arg
|
|||
}
|
||||
|
||||
options.namespace, options.enforceNamespace, err = f.ToRawKubeConfigLoader().Namespace()
|
||||
if err != nil {
|
||||
if err != nil && !(options.local && clientcmd.IsEmptyConfig(err)) {
|
||||
return nil, err
|
||||
}
|
||||
options.builder = f.NewBuilder()
|
||||
|
|
|
@ -123,11 +123,11 @@ func NewCmdAPIResources(restClientGetter genericclioptions.RESTClientGetter, ioS
|
|||
|
||||
// Validate checks to the APIResourceOptions to see if there is sufficient information run the command
|
||||
func (o *APIResourceOptions) Validate() error {
|
||||
supportedOutputTypes := sets.NewString("", "wide", "name")
|
||||
supportedOutputTypes := sets.New[string]("", "wide", "name")
|
||||
if !supportedOutputTypes.Has(o.Output) {
|
||||
return fmt.Errorf("--output %v is not available", o.Output)
|
||||
}
|
||||
supportedSortTypes := sets.NewString("", "name", "kind")
|
||||
supportedSortTypes := sets.New[string]("", "name", "kind")
|
||||
if len(o.SortBy) > 0 {
|
||||
if !supportedSortTypes.Has(o.SortBy) {
|
||||
return fmt.Errorf("--sort-by accepts only name or kind")
|
||||
|
@ -193,11 +193,11 @@ func (o *APIResourceOptions) RunAPIResources() error {
|
|||
continue
|
||||
}
|
||||
// filter to resources that support the specified verbs
|
||||
if len(o.Verbs) > 0 && !sets.NewString(resource.Verbs...).HasAll(o.Verbs...) {
|
||||
if len(o.Verbs) > 0 && !sets.New[string](resource.Verbs...).HasAll(o.Verbs...) {
|
||||
continue
|
||||
}
|
||||
// filter to resources that belong to the specified categories
|
||||
if len(o.Categories) > 0 && !sets.NewString(resource.Categories...).HasAll(o.Categories...) {
|
||||
if len(o.Categories) > 0 && !sets.New[string](resource.Categories...).HasAll(o.Categories...) {
|
||||
continue
|
||||
}
|
||||
resources = append(resources, groupResource{
|
||||
|
|
|
@ -23,7 +23,7 @@ import (
|
|||
"net/http"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
|
||||
"sigs.k8s.io/structured-merge-diff/v6/fieldpath"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
|
@ -39,17 +39,17 @@ import (
|
|||
"k8s.io/cli-runtime/pkg/printers"
|
||||
"k8s.io/cli-runtime/pkg/resource"
|
||||
"k8s.io/client-go/dynamic"
|
||||
"k8s.io/client-go/openapi3"
|
||||
"k8s.io/client-go/util/csaupgrade"
|
||||
"k8s.io/component-base/version"
|
||||
"k8s.io/klog/v2"
|
||||
"k8s.io/kubectl/pkg/cmd/delete"
|
||||
cmddelete "k8s.io/kubectl/pkg/cmd/delete"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/scheme"
|
||||
"k8s.io/kubectl/pkg/util"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
"k8s.io/kubectl/pkg/util/openapi"
|
||||
"k8s.io/kubectl/pkg/util/prune"
|
||||
"k8s.io/kubectl/pkg/util/slice"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
"k8s.io/kubectl/pkg/validation"
|
||||
)
|
||||
|
@ -61,7 +61,7 @@ type ApplyFlags struct {
|
|||
RecordFlags *genericclioptions.RecordFlags
|
||||
PrintFlags *genericclioptions.PrintFlags
|
||||
|
||||
DeleteFlags *delete.DeleteFlags
|
||||
DeleteFlags *cmddelete.DeleteFlags
|
||||
|
||||
FieldManager string
|
||||
Selector string
|
||||
|
@ -71,9 +71,8 @@ type ApplyFlags struct {
|
|||
All bool
|
||||
Overwrite bool
|
||||
OpenAPIPatch bool
|
||||
Subresource string
|
||||
|
||||
// DEPRECATED: Use PruneAllowlist instead
|
||||
PruneWhitelist []string // TODO: Remove this in kubectl 1.28 or later
|
||||
PruneAllowlist []string
|
||||
|
||||
genericiooptions.IOStreams
|
||||
|
@ -86,7 +85,7 @@ type ApplyOptions struct {
|
|||
PrintFlags *genericclioptions.PrintFlags
|
||||
ToPrinter func(string) (printers.ResourcePrinter, error)
|
||||
|
||||
DeleteOptions *delete.DeleteOptions
|
||||
DeleteOptions *cmddelete.DeleteOptions
|
||||
|
||||
ServerSideApply bool
|
||||
ForceConflicts bool
|
||||
|
@ -99,13 +98,15 @@ type ApplyOptions struct {
|
|||
All bool
|
||||
Overwrite bool
|
||||
OpenAPIPatch bool
|
||||
Subresource string
|
||||
|
||||
ValidationDirective string
|
||||
Validator validation.Schema
|
||||
Builder *resource.Builder
|
||||
Mapper meta.RESTMapper
|
||||
DynamicClient dynamic.Interface
|
||||
OpenAPISchema openapi.Resources
|
||||
OpenAPIGetter openapi.OpenAPIResourcesGetter
|
||||
OpenAPIV3Root openapi3.Root
|
||||
|
||||
Namespace string
|
||||
EnforceNamespace bool
|
||||
|
@ -183,7 +184,7 @@ var ApplySetToolVersion = version.Get().GitVersion
|
|||
func NewApplyFlags(streams genericiooptions.IOStreams) *ApplyFlags {
|
||||
return &ApplyFlags{
|
||||
RecordFlags: genericclioptions.NewRecordFlags(),
|
||||
DeleteFlags: delete.NewDeleteFlags("The files that contain the configurations to apply."),
|
||||
DeleteFlags: cmddelete.NewDeleteFlags("The files that contain the configurations to apply."),
|
||||
PrintFlags: genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme),
|
||||
|
||||
Overwrite: true,
|
||||
|
@ -233,10 +234,10 @@ func (flags *ApplyFlags) AddFlags(cmd *cobra.Command) {
|
|||
cmdutil.AddServerSideApplyFlags(cmd)
|
||||
cmdutil.AddFieldManagerFlagVar(cmd, &flags.FieldManager, FieldManagerClientSideApply)
|
||||
cmdutil.AddLabelSelectorFlagVar(cmd, &flags.Selector)
|
||||
cmdutil.AddPruningFlags(cmd, &flags.Prune, &flags.PruneAllowlist, &flags.PruneWhitelist, &flags.All, &flags.ApplySetRef)
|
||||
|
||||
cmdutil.AddPruningFlags(cmd, &flags.Prune, &flags.PruneAllowlist, &flags.All, &flags.ApplySetRef)
|
||||
cmd.Flags().BoolVar(&flags.Overwrite, "overwrite", flags.Overwrite, "Automatically resolve conflicts between the modified and live configuration by using values from the modified configuration")
|
||||
cmd.Flags().BoolVar(&flags.OpenAPIPatch, "openapi-patch", flags.OpenAPIPatch, "If true, use openapi to calculate diff when the openapi presents and the resource can be found in the openapi spec. Otherwise, fall back to use baked-in types.")
|
||||
cmdutil.AddSubresourceFlags(cmd, &flags.Subresource, "If specified, apply will operate on the subresource of the requested object. Only allowed when using --server-side.")
|
||||
}
|
||||
|
||||
// ToOptions converts from CLI inputs to runtime inputs
|
||||
|
@ -282,7 +283,15 @@ func (flags *ApplyFlags) ToOptions(f cmdutil.Factory, cmd *cobra.Command, baseNa
|
|||
return nil, err
|
||||
}
|
||||
|
||||
openAPISchema, _ := f.OpenAPISchema()
|
||||
var openAPIV3Root openapi3.Root
|
||||
if !cmdutil.OpenAPIV3Patch.IsDisabled() {
|
||||
openAPIV3Client, err := f.OpenAPIV3Client()
|
||||
if err == nil {
|
||||
openAPIV3Root = openapi3.NewRoot(openAPIV3Client)
|
||||
} else {
|
||||
klog.V(4).Infof("warning: OpenAPI V3 Patch is enabled but is unable to be loaded. Will fall back to OpenAPI V2")
|
||||
}
|
||||
}
|
||||
|
||||
validationDirective, err := cmdutil.GetValidationDirective(cmd)
|
||||
if err != nil {
|
||||
|
@ -325,8 +334,7 @@ func (flags *ApplyFlags) ToOptions(f cmdutil.Factory, cmd *cobra.Command, baseNa
|
|||
applySet = NewApplySet(parent, tooling, mapper, restClient)
|
||||
}
|
||||
if flags.Prune {
|
||||
pruneAllowlist := slice.ToSet(flags.PruneAllowlist, flags.PruneWhitelist)
|
||||
flags.PruneResources, err = prune.ParseResources(mapper, pruneAllowlist)
|
||||
flags.PruneResources, err = prune.ParseResources(mapper, flags.PruneAllowlist)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -351,6 +359,7 @@ func (flags *ApplyFlags) ToOptions(f cmdutil.Factory, cmd *cobra.Command, baseNa
|
|||
All: flags.All,
|
||||
Overwrite: flags.Overwrite,
|
||||
OpenAPIPatch: flags.OpenAPIPatch,
|
||||
Subresource: flags.Subresource,
|
||||
|
||||
Recorder: recorder,
|
||||
Namespace: namespace,
|
||||
|
@ -360,7 +369,8 @@ func (flags *ApplyFlags) ToOptions(f cmdutil.Factory, cmd *cobra.Command, baseNa
|
|||
Builder: builder,
|
||||
Mapper: mapper,
|
||||
DynamicClient: dynamicClient,
|
||||
OpenAPISchema: openAPISchema,
|
||||
OpenAPIGetter: f,
|
||||
OpenAPIV3Root: openAPIV3Root,
|
||||
|
||||
IOStreams: flags.IOStreams,
|
||||
|
||||
|
@ -432,6 +442,9 @@ func (o *ApplyOptions) Validate() error {
|
|||
}
|
||||
}
|
||||
}
|
||||
if len(o.Subresource) > 0 && !o.ServerSideApply {
|
||||
return fmt.Errorf("--subresource can only be specified for --server-side")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -571,13 +584,15 @@ func (o *ApplyOptions) applyOneObject(info *resource.Info) error {
|
|||
options := metav1.PatchOptions{
|
||||
Force: &o.ForceConflicts,
|
||||
}
|
||||
obj, err := helper.Patch(
|
||||
info.Namespace,
|
||||
info.Name,
|
||||
types.ApplyPatchType,
|
||||
data,
|
||||
&options,
|
||||
)
|
||||
obj, err := helper.
|
||||
WithSubresource(o.Subresource).
|
||||
Patch(
|
||||
info.Namespace,
|
||||
info.Name,
|
||||
types.ApplyPatchType,
|
||||
data,
|
||||
&options,
|
||||
)
|
||||
if err != nil {
|
||||
if isIncompatibleServerError(err) {
|
||||
err = fmt.Errorf("Server-side apply not available on the server: (%v)", err)
|
||||
|
@ -675,6 +690,12 @@ See https://kubernetes.io/docs/reference/using-api/server-side-apply/#conflicts`
|
|||
return cmdutil.AddSourceToErr("creating", info.Source, err)
|
||||
}
|
||||
|
||||
// prune nulls when client-side apply does a create to match what will happen when client-side applying an update.
|
||||
// do this after CreateApplyAnnotation so the annotation matches what will be persisted on an update apply of the same manifest.
|
||||
if u, ok := info.Object.(runtime.Unstructured); ok {
|
||||
pruneNullsFromMap(u.UnstructuredContent())
|
||||
}
|
||||
|
||||
if o.DryRunStrategy != cmdutil.DryRunClient {
|
||||
// Then create the resource and skip the three-way merge
|
||||
obj, err := helper.Create(info.Namespace, true, info.Object)
|
||||
|
@ -753,6 +774,29 @@ See https://kubernetes.io/docs/reference/using-api/server-side-apply/#conflicts`
|
|||
return nil
|
||||
}
|
||||
|
||||
func pruneNullsFromMap(data map[string]interface{}) {
|
||||
for k, v := range data {
|
||||
if v == nil {
|
||||
delete(data, k)
|
||||
} else {
|
||||
pruneNulls(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
func pruneNullsFromSlice(data []interface{}) {
|
||||
for _, v := range data {
|
||||
pruneNulls(v)
|
||||
}
|
||||
}
|
||||
func pruneNulls(v interface{}) {
|
||||
switch v := v.(type) {
|
||||
case map[string]interface{}:
|
||||
pruneNullsFromMap(v)
|
||||
case []interface{}:
|
||||
pruneNullsFromSlice(v)
|
||||
}
|
||||
}
|
||||
|
||||
// Saves the last-applied-configuration annotation in a separate SSA field manager
|
||||
// to prevent it from being dropped by users who have transitioned to SSA.
|
||||
//
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -54,13 +54,22 @@ const (
|
|||
// Example value: "kube-system,ns1,ns2".
|
||||
ApplySetAdditionalNamespacesAnnotation = "applyset.kubernetes.io/additional-namespaces"
|
||||
|
||||
// ApplySetGRsAnnotation is a list of group-resources used to optimize listing of ApplySet member objects.
|
||||
// Deprecated: ApplySetGRsAnnotation is a list of group-resources used to optimize listing of ApplySet member objects.
|
||||
// It is optional in the ApplySet specification, as tools can perform discovery or use a different optimization.
|
||||
// However, it is currently required in kubectl.
|
||||
// When present, the value of this annotation must be a comma separated list of the group-resources,
|
||||
// in the fully-qualified name format, i.e. <resourcename>.<group>.
|
||||
// Example value: "certificates.cert-manager.io,configmaps,deployments.apps,secrets,services"
|
||||
// Deprecated and replaced by ApplySetGKsAnnotation, support for this can be removed in applyset beta or GA.
|
||||
DeprecatedApplySetGRsAnnotation = "applyset.kubernetes.io/contains-group-resources"
|
||||
|
||||
// ApplySetGKsAnnotation is a list of group-kinds used to optimize listing of ApplySet member objects.
|
||||
// It is optional in the ApplySet specification, as tools can perform discovery or use a different optimization.
|
||||
// However, it is currently required in kubectl.
|
||||
// When present, the value of this annotation must be a comma separated list of the group-kinds,
|
||||
// in the fully-qualified name format, i.e. <resourcename>.<group>.
|
||||
// Example value: "certificates.cert-manager.io,configmaps,deployments.apps,secrets,services"
|
||||
ApplySetGRsAnnotation = "applyset.kubernetes.io/contains-group-resources"
|
||||
// in the fully-qualified name format, i.e. <kind>.<group>.
|
||||
// Example value: "Certificate.cert-manager.io,ConfigMap,deployments.apps,Secret,Service"
|
||||
ApplySetGKsAnnotation = "applyset.kubernetes.io/contains-group-kinds"
|
||||
|
||||
// ApplySetParentIDLabel is the key of the label that makes object an ApplySet parent object.
|
||||
// Its value MUST use the format specified in V1ApplySetIdFormat below
|
||||
|
@ -92,13 +101,13 @@ type ApplySet struct {
|
|||
toolingID ApplySetTooling
|
||||
|
||||
// currentResources is the set of resources that are part of the sever-side set as of when the current operation started.
|
||||
currentResources map[schema.GroupVersionResource]*meta.RESTMapping
|
||||
currentResources map[schema.GroupKind]*kindInfo
|
||||
|
||||
// currentNamespaces is the set of namespaces that contain objects in this applyset as of when the current operation started.
|
||||
currentNamespaces sets.Set[string]
|
||||
|
||||
// updatedResources is the set of resources that will be part of the set as of when the current operation completes.
|
||||
updatedResources map[schema.GroupVersionResource]*meta.RESTMapping
|
||||
updatedResources map[schema.GroupKind]*kindInfo
|
||||
|
||||
// updatedNamespaces is the set of namespaces that will contain objects in this applyset as of when the current operation completes.
|
||||
updatedNamespaces sets.Set[string]
|
||||
|
@ -143,9 +152,9 @@ func (t ApplySetTooling) String() string {
|
|||
// NewApplySet creates a new ApplySet object tracked by the given parent object.
|
||||
func NewApplySet(parent *ApplySetParentRef, tooling ApplySetTooling, mapper meta.RESTMapper, client resource.RESTClient) *ApplySet {
|
||||
return &ApplySet{
|
||||
currentResources: make(map[schema.GroupVersionResource]*meta.RESTMapping),
|
||||
currentResources: make(map[schema.GroupKind]*kindInfo),
|
||||
currentNamespaces: make(sets.Set[string]),
|
||||
updatedResources: make(map[schema.GroupVersionResource]*meta.RESTMapping),
|
||||
updatedResources: make(map[schema.GroupKind]*kindInfo),
|
||||
updatedNamespaces: make(sets.Set[string]),
|
||||
parentRef: parent,
|
||||
toolingID: tooling,
|
||||
|
@ -284,7 +293,7 @@ func (a *ApplySet) fetchParent() error {
|
|||
return fmt.Errorf("ApplySet parent object %q exists and has incorrect value for label %q (got: %s, want: %s)", a.parentRef, ApplySetParentIDLabel, idLabel, a.ID())
|
||||
}
|
||||
|
||||
if a.currentResources, err = parseResourcesAnnotation(annotations, a.restMapper); err != nil {
|
||||
if a.currentResources, err = parseKindAnnotation(annotations, a.restMapper); err != nil {
|
||||
// TODO: handle GVRs for now-deleted CRDs
|
||||
return fmt.Errorf("parsing ApplySet annotation on %q: %w", a.parentRef, err)
|
||||
}
|
||||
|
@ -302,8 +311,8 @@ func (a *ApplySet) LabelSelectorForMembers() string {
|
|||
|
||||
// AllPrunableResources returns the list of all resources that should be considered for pruning.
|
||||
// This is potentially a superset of the resources types that actually contain resources.
|
||||
func (a *ApplySet) AllPrunableResources() []*meta.RESTMapping {
|
||||
var ret []*meta.RESTMapping
|
||||
func (a *ApplySet) AllPrunableResources() []*kindInfo {
|
||||
var ret []*kindInfo
|
||||
for _, m := range a.currentResources {
|
||||
ret = append(ret, m)
|
||||
}
|
||||
|
@ -336,14 +345,43 @@ func toolingBaseName(toolAnnotation string) string {
|
|||
return toolAnnotation
|
||||
}
|
||||
|
||||
func parseResourcesAnnotation(annotations map[string]string, mapper meta.RESTMapper) (map[schema.GroupVersionResource]*meta.RESTMapping, error) {
|
||||
annotation, ok := annotations[ApplySetGRsAnnotation]
|
||||
// kindInfo holds type information about a particular resource type.
|
||||
type kindInfo struct {
|
||||
restMapping *meta.RESTMapping
|
||||
}
|
||||
|
||||
func parseKindAnnotation(annotations map[string]string, mapper meta.RESTMapper) (map[schema.GroupKind]*kindInfo, error) {
|
||||
annotation, ok := annotations[ApplySetGKsAnnotation]
|
||||
if !ok {
|
||||
if annotations[DeprecatedApplySetGRsAnnotation] != "" {
|
||||
return parseDeprecatedResourceAnnotation(annotations[DeprecatedApplySetGRsAnnotation], mapper)
|
||||
}
|
||||
|
||||
// The spec does not require this annotation. However, 'missing' means 'perform discovery'.
|
||||
// We return an error because we do not currently support dynamic discovery in kubectl apply.
|
||||
return nil, fmt.Errorf("kubectl requires the %q annotation to be set on all ApplySet parent objects", ApplySetGRsAnnotation)
|
||||
return nil, fmt.Errorf("kubectl requires the %q annotation to be set on all ApplySet parent objects", ApplySetGKsAnnotation)
|
||||
}
|
||||
mappings := make(map[schema.GroupVersionResource]*meta.RESTMapping)
|
||||
mappings := make(map[schema.GroupKind]*kindInfo)
|
||||
// Annotation present but empty means that this is currently an empty set.
|
||||
if annotation == "" {
|
||||
return mappings, nil
|
||||
}
|
||||
for _, gkString := range strings.Split(annotation, ",") {
|
||||
gk := schema.ParseGroupKind(gkString)
|
||||
restMapping, err := mapper.RESTMapping(gk)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not find mapping for kind in %q annotation: %w", ApplySetGKsAnnotation, err)
|
||||
}
|
||||
mappings[gk] = &kindInfo{
|
||||
restMapping: restMapping,
|
||||
}
|
||||
}
|
||||
|
||||
return mappings, nil
|
||||
}
|
||||
|
||||
func parseDeprecatedResourceAnnotation(annotation string, mapper meta.RESTMapper) (map[schema.GroupKind]*kindInfo, error) {
|
||||
mappings := make(map[schema.GroupKind]*kindInfo)
|
||||
// Annotation present but empty means that this is currently an empty set.
|
||||
if annotation == "" {
|
||||
return mappings, nil
|
||||
|
@ -352,13 +390,15 @@ func parseResourcesAnnotation(annotations map[string]string, mapper meta.RESTMap
|
|||
gr := schema.ParseGroupResource(grString)
|
||||
gvk, err := mapper.KindFor(gr.WithVersion(""))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid group resource in %q annotation: %w", ApplySetGRsAnnotation, err)
|
||||
return nil, fmt.Errorf("invalid group resource in %q annotation: %w", DeprecatedApplySetGRsAnnotation, err)
|
||||
}
|
||||
mapping, err := mapper.RESTMapping(gvk.GroupKind())
|
||||
restMapping, err := mapper.RESTMapping(gvk.GroupKind())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not find kind for resource in %q annotation: %w", ApplySetGRsAnnotation, err)
|
||||
return nil, fmt.Errorf("could not find kind for resource in %q annotation: %w", DeprecatedApplySetGRsAnnotation, err)
|
||||
}
|
||||
mappings[gvk.GroupKind()] = &kindInfo{
|
||||
restMapping: restMapping,
|
||||
}
|
||||
mappings[mapping.Resource] = mapping
|
||||
}
|
||||
return mappings, nil
|
||||
}
|
||||
|
@ -377,9 +417,14 @@ func parseNamespacesAnnotation(annotations map[string]string) sets.Set[string] {
|
|||
|
||||
// addResource registers the given resource and namespace as being part of the updated set of
|
||||
// resources being applied by the current operation.
|
||||
func (a *ApplySet) addResource(resource *meta.RESTMapping, namespace string) {
|
||||
a.updatedResources[resource.Resource] = resource
|
||||
if resource.Scope == meta.RESTScopeNamespace && namespace != "" {
|
||||
func (a *ApplySet) addResource(restMapping *meta.RESTMapping, namespace string) {
|
||||
gk := restMapping.GroupVersionKind.GroupKind()
|
||||
if _, found := a.updatedResources[gk]; !found {
|
||||
a.updatedResources[gk] = &kindInfo{
|
||||
restMapping: restMapping,
|
||||
}
|
||||
}
|
||||
if restMapping.Scope == meta.RESTScopeNamespace && namespace != "" {
|
||||
a.updatedNamespaces.Insert(namespace)
|
||||
}
|
||||
}
|
||||
|
@ -394,6 +439,8 @@ func (a *ApplySet) updateParent(mode ApplySetUpdateMode, dryRun cmdutil.DryRunSt
|
|||
if err != nil {
|
||||
return fmt.Errorf("failed to encode patch for ApplySet parent: %w", err)
|
||||
}
|
||||
// Note that because we are using SSA, we will remove any annotations we don't specify,
|
||||
// which is how we remove the deprecated contains-group-resources annotation.
|
||||
err = serverSideApplyRequest(a, data, dryRun, validation, false)
|
||||
if err != nil && errors.IsConflict(err) {
|
||||
// Try again with conflicts forced
|
||||
|
@ -429,17 +476,17 @@ func serverSideApplyRequest(a *ApplySet, data []byte, dryRun cmdutil.DryRunStrat
|
|||
}
|
||||
|
||||
func (a *ApplySet) buildParentPatch(mode ApplySetUpdateMode) *metav1.PartialObjectMetadata {
|
||||
var newGRsAnnotation, newNsAnnotation string
|
||||
var newGKsAnnotation, newNsAnnotation string
|
||||
switch mode {
|
||||
case updateToSuperset:
|
||||
// If the apply succeeded but pruning failed, the set of group resources that
|
||||
// the ApplySet should track is the superset of the previous and current resources.
|
||||
// This ensures that the resources that failed to be pruned are not orphaned from the set.
|
||||
grSuperset := sets.KeySet(a.currentResources).Union(sets.KeySet(a.updatedResources))
|
||||
newGRsAnnotation = generateResourcesAnnotation(grSuperset)
|
||||
newGKsAnnotation = generateKindsAnnotation(grSuperset)
|
||||
newNsAnnotation = generateNamespacesAnnotation(a.currentNamespaces.Union(a.updatedNamespaces), a.parentRef.Namespace)
|
||||
case updateToLatestSet:
|
||||
newGRsAnnotation = generateResourcesAnnotation(sets.KeySet(a.updatedResources))
|
||||
newGKsAnnotation = generateKindsAnnotation(sets.KeySet(a.updatedResources))
|
||||
newNsAnnotation = generateNamespacesAnnotation(a.updatedNamespaces, a.parentRef.Namespace)
|
||||
}
|
||||
|
||||
|
@ -453,7 +500,7 @@ func (a *ApplySet) buildParentPatch(mode ApplySetUpdateMode) *metav1.PartialObje
|
|||
Namespace: a.parentRef.Namespace,
|
||||
Annotations: map[string]string{
|
||||
ApplySetToolingAnnotation: a.toolingID.String(),
|
||||
ApplySetGRsAnnotation: newGRsAnnotation,
|
||||
ApplySetGKsAnnotation: newGKsAnnotation,
|
||||
ApplySetAdditionalNamespacesAnnotation: newNsAnnotation,
|
||||
},
|
||||
Labels: map[string]string{
|
||||
|
@ -469,13 +516,13 @@ func generateNamespacesAnnotation(namespaces sets.Set[string], skip string) stri
|
|||
return strings.Join(nsList, ",")
|
||||
}
|
||||
|
||||
func generateResourcesAnnotation(resources sets.Set[schema.GroupVersionResource]) string {
|
||||
var grs []string
|
||||
for gvr := range resources {
|
||||
grs = append(grs, gvr.GroupResource().String())
|
||||
func generateKindsAnnotation(resources sets.Set[schema.GroupKind]) string {
|
||||
var gks []string
|
||||
for gk := range resources {
|
||||
gks = append(gks, gk.String())
|
||||
}
|
||||
sort.Strings(grs)
|
||||
return strings.Join(grs, ",")
|
||||
sort.Strings(gks)
|
||||
return strings.Join(gks, ",")
|
||||
}
|
||||
|
||||
func (a ApplySet) FieldManager() string {
|
||||
|
|
|
@ -77,27 +77,29 @@ func (a *ApplySet) FindAllObjectsToPrune(ctx context.Context, dynamicClient dyna
|
|||
|
||||
// We run discovery in parallel, in as many goroutines as priority and fairness will allow
|
||||
// (We don't expect many requests in real-world scenarios - maybe tens, unlikely to be hundreds)
|
||||
for _, restMapping := range a.AllPrunableResources() {
|
||||
switch restMapping.Scope.Name() {
|
||||
for gvk, resource := range a.AllPrunableResources() {
|
||||
scope := resource.restMapping.Scope
|
||||
|
||||
switch scope.Name() {
|
||||
case meta.RESTScopeNameNamespace:
|
||||
for _, namespace := range a.AllPrunableNamespaces() {
|
||||
if namespace == "" {
|
||||
// Just double-check because otherwise we get cryptic error messages
|
||||
return nil, fmt.Errorf("unexpectedly encountered empty namespace during prune of namespace-scoped resource %v", restMapping.GroupVersionKind)
|
||||
return nil, fmt.Errorf("unexpectedly encountered empty namespace during prune of namespace-scoped resource %v", gvk)
|
||||
}
|
||||
tasks = append(tasks, &task{
|
||||
namespace: namespace,
|
||||
restMapping: restMapping,
|
||||
restMapping: resource.restMapping,
|
||||
})
|
||||
}
|
||||
|
||||
case meta.RESTScopeNameRoot:
|
||||
tasks = append(tasks, &task{
|
||||
restMapping: restMapping,
|
||||
restMapping: resource.restMapping,
|
||||
})
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unhandled scope %q", restMapping.Scope.Name())
|
||||
return nil, fmt.Errorf("unhandled scope %q", scope.Name())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -17,13 +17,12 @@ limitations under the License.
|
|||
package apply
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/jonboulle/clockwork"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
|
@ -37,6 +36,9 @@ import (
|
|||
"k8s.io/apimachinery/pkg/util/strategicpatch"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/cli-runtime/pkg/resource"
|
||||
"k8s.io/client-go/openapi3"
|
||||
"k8s.io/klog/v2"
|
||||
"k8s.io/kube-openapi/pkg/validation/spec"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/scheme"
|
||||
"k8s.io/kubectl/pkg/util"
|
||||
|
@ -46,13 +48,18 @@ import (
|
|||
const (
|
||||
// maxPatchRetry is the maximum number of conflicts retry for during a patch operation before returning failure
|
||||
maxPatchRetry = 5
|
||||
// backOffPeriod is the period to back off when apply patch results in error.
|
||||
backOffPeriod = 1 * time.Second
|
||||
// how many times we can retry before back off
|
||||
triesBeforeBackOff = 1
|
||||
// groupVersionKindExtensionKey is the key used to lookup the
|
||||
// GroupVersionKind value for an object definition from the
|
||||
// definition's "extensions" map.
|
||||
groupVersionKindExtensionKey = "x-kubernetes-group-version-kind"
|
||||
)
|
||||
|
||||
var createPatchErrFormat = "creating patch with:\noriginal:\n%s\nmodified:\n%s\ncurrent:\n%s\nfor:"
|
||||
// patchRetryBackOffPeriod is the period to back off when apply patch results in error.
|
||||
var patchRetryBackOffPeriod = 1 * time.Second
|
||||
|
||||
var createPatchErrFormat = "creating patch with:\noriginal:\n%s\nmodified:\n%s\ncurrent:\n%s\nfor: %w"
|
||||
|
||||
// Patcher defines options to patch OpenAPI objects.
|
||||
type Patcher struct {
|
||||
|
@ -73,13 +80,17 @@ type Patcher struct {
|
|||
// Number of retries to make if the patch fails with conflict
|
||||
Retries int
|
||||
|
||||
OpenapiSchema openapi.Resources
|
||||
OpenAPIGetter openapi.OpenAPIResourcesGetter
|
||||
OpenAPIV3Root openapi3.Root
|
||||
}
|
||||
|
||||
func newPatcher(o *ApplyOptions, info *resource.Info, helper *resource.Helper) (*Patcher, error) {
|
||||
var openapiSchema openapi.Resources
|
||||
var openAPIGetter openapi.OpenAPIResourcesGetter
|
||||
var openAPIV3Root openapi3.Root
|
||||
|
||||
if o.OpenAPIPatch {
|
||||
openapiSchema = o.OpenAPISchema
|
||||
openAPIGetter = o.OpenAPIGetter
|
||||
openAPIV3Root = o.OpenAPIV3Root
|
||||
}
|
||||
|
||||
return &Patcher{
|
||||
|
@ -91,7 +102,8 @@ func newPatcher(o *ApplyOptions, info *resource.Info, helper *resource.Helper) (
|
|||
CascadingStrategy: o.DeleteOptions.CascadingStrategy,
|
||||
Timeout: o.DeleteOptions.Timeout,
|
||||
GracePeriod: o.DeleteOptions.GracePeriod,
|
||||
OpenapiSchema: openapiSchema,
|
||||
OpenAPIGetter: openAPIGetter,
|
||||
OpenAPIV3Root: openAPIV3Root,
|
||||
Retries: maxPatchRetry,
|
||||
}, nil
|
||||
}
|
||||
|
@ -106,26 +118,54 @@ func (p *Patcher) patchSimple(obj runtime.Object, modified []byte, namespace, na
|
|||
// Serialize the current configuration of the object from the server.
|
||||
current, err := runtime.Encode(unstructured.UnstructuredJSONScheme, obj)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrapf(err, "serializing current configuration from:\n%v\nfor:", obj)
|
||||
return nil, nil, fmt.Errorf("serializing current configuration from:\n%v\nfor: %w", obj, err)
|
||||
}
|
||||
|
||||
// Retrieve the original configuration of the object from the annotation.
|
||||
original, err := util.GetOriginalConfiguration(obj)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrapf(err, "retrieving original configuration from:\n%v\nfor:", obj)
|
||||
return nil, nil, fmt.Errorf("retrieving original configuration from:\n%v\nfor: %w", obj, err)
|
||||
}
|
||||
|
||||
var patchType types.PatchType
|
||||
var patch []byte
|
||||
|
||||
if p.OpenapiSchema != nil {
|
||||
// if openapischema is used, we'll try to get required patch type for this GVK from Open API.
|
||||
// if it fails or could not find any patch type, fall back to baked-in patch type determination.
|
||||
if patchType, err = p.getPatchTypeFromOpenAPI(p.Mapping.GroupVersionKind); err == nil && patchType == types.StrategicMergePatchType {
|
||||
patch, err = p.buildStrategicMergeFromOpenAPI(original, modified, current)
|
||||
if p.OpenAPIV3Root != nil {
|
||||
gvkSupported, err := p.gvkSupportsPatchOpenAPIV3(p.Mapping.GroupVersionKind)
|
||||
if err != nil {
|
||||
// Realistically this error logging is not needed (not present in V2),
|
||||
// but would help us in debugging if users encounter a problem
|
||||
// with OpenAPI V3 not present in V2.
|
||||
klog.V(5).Infof("warning: OpenAPI V3 path does not exist - group: %s, version %s, kind %s\n",
|
||||
p.Mapping.GroupVersionKind.Group, p.Mapping.GroupVersionKind.Version, p.Mapping.GroupVersionKind.Kind)
|
||||
} else if gvkSupported {
|
||||
patch, err = p.buildStrategicMergePatchFromOpenAPIV3(original, modified, current)
|
||||
if err != nil {
|
||||
// Warn user about problem and continue strategic merge patching using builtin types.
|
||||
fmt.Fprintf(errOut, "warning: error calculating patch from openapi spec: %v\n", err)
|
||||
// Fall back to OpenAPI V2 if there is a problem
|
||||
// We should remove the fallback in the future,
|
||||
// but for the first release it might be beneficial
|
||||
// to fall back to OpenAPI V2 while logging the error
|
||||
// and seeing if we get any bug reports.
|
||||
fmt.Fprintf(errOut, "warning: error calculating patch from openapi v3 spec: %v\n", err)
|
||||
} else {
|
||||
patchType = types.StrategicMergePatchType
|
||||
}
|
||||
} else {
|
||||
klog.V(5).Infof("warning: OpenAPI V3 path does not support strategic merge patch - group: %s, version %s, kind %s\n",
|
||||
p.Mapping.GroupVersionKind.Group, p.Mapping.GroupVersionKind.Version, p.Mapping.GroupVersionKind.Kind)
|
||||
}
|
||||
}
|
||||
|
||||
if patch == nil && p.OpenAPIGetter != nil {
|
||||
if openAPISchema, err := p.OpenAPIGetter.OpenAPISchema(); err == nil && openAPISchema != nil {
|
||||
// if openapischema is used, we'll try to get required patch type for this GVK from Open API.
|
||||
// if it fails or could not find any patch type, fall back to baked-in patch type determination.
|
||||
if patchType, err = p.getPatchTypeFromOpenAPI(openAPISchema, p.Mapping.GroupVersionKind); err == nil && patchType == types.StrategicMergePatchType {
|
||||
patch, err = p.buildStrategicMergeFromOpenAPI(openAPISchema, original, modified, current)
|
||||
if err != nil {
|
||||
// Warn user about problem and continue strategic merge patching using builtin types.
|
||||
fmt.Fprintf(errOut, "warning: error calculating patch from openapi spec: %v\n", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -136,17 +176,17 @@ func (p *Patcher) patchSimple(obj runtime.Object, modified []byte, namespace, na
|
|||
patchType = types.StrategicMergePatchType
|
||||
patch, err = p.buildStrategicMergeFromBuiltins(versionedObj, original, modified, current)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrapf(err, createPatchErrFormat, original, modified, current)
|
||||
return nil, nil, fmt.Errorf(createPatchErrFormat, original, modified, current, err)
|
||||
}
|
||||
} else {
|
||||
if !runtime.IsNotRegisteredError(err) {
|
||||
return nil, nil, errors.Wrapf(err, "getting instance of versioned object for %v:", p.Mapping.GroupVersionKind)
|
||||
return nil, nil, fmt.Errorf("getting instance of versioned object for %v: %w", p.Mapping.GroupVersionKind, err)
|
||||
}
|
||||
|
||||
patchType = types.MergePatchType
|
||||
patch, err = p.buildMergePatch(original, modified, current)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrapf(err, createPatchErrFormat, original, modified, current)
|
||||
return nil, nil, fmt.Errorf(createPatchErrFormat, original, modified, current, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -158,7 +198,7 @@ func (p *Patcher) patchSimple(obj runtime.Object, modified []byte, namespace, na
|
|||
if p.ResourceVersion != nil {
|
||||
patch, err = addResourceVersion(patch, *p.ResourceVersion)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(err, "Failed to insert resourceVersion in patch")
|
||||
return nil, nil, fmt.Errorf("failed to insert resourceVersion in patch: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -182,10 +222,94 @@ func (p *Patcher) buildMergePatch(original, modified, current []byte) ([]byte, e
|
|||
return patch, nil
|
||||
}
|
||||
|
||||
// gvkSupportsPatchOpenAPIV3 checks if a particular GVK supports the patch operation.
|
||||
// It returns an error if the OpenAPI V3 could not be downloaded.
|
||||
func (p *Patcher) gvkSupportsPatchOpenAPIV3(gvk schema.GroupVersionKind) (bool, error) {
|
||||
gvSpec, err := p.OpenAPIV3Root.GVSpec(schema.GroupVersion{
|
||||
Group: p.Mapping.GroupVersionKind.Group,
|
||||
Version: p.Mapping.GroupVersionKind.Version,
|
||||
})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if gvSpec == nil || gvSpec.Paths == nil || gvSpec.Paths.Paths == nil {
|
||||
return false, fmt.Errorf("gvk group: %s, version: %s, kind: %s does not exist for OpenAPI V3", gvk.Group, gvk.Version, gvk.Kind)
|
||||
}
|
||||
for _, path := range gvSpec.Paths.Paths {
|
||||
if path.Patch != nil {
|
||||
if gvkMatchesSingle(p.Mapping.GroupVersionKind, path.Patch.Extensions) {
|
||||
if path.Patch.RequestBody == nil || path.Patch.RequestBody.Content == nil {
|
||||
// GVK exists but does not support requestBody. Indication of malformed OpenAPI.
|
||||
return false, nil
|
||||
}
|
||||
if _, ok := path.Patch.RequestBody.Content["application/strategic-merge-patch+json"]; ok {
|
||||
return true, nil
|
||||
}
|
||||
// GVK exists but strategic-merge-patch is not supported. Likely to be a CRD or aggregated resource.
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func gvkMatchesArray(targetGVK schema.GroupVersionKind, ext spec.Extensions) bool {
|
||||
var gvkList []map[string]string
|
||||
err := ext.GetObject(groupVersionKindExtensionKey, &gvkList)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
for _, gvkMap := range gvkList {
|
||||
if gvkMap["group"] == targetGVK.Group &&
|
||||
gvkMap["version"] == targetGVK.Version &&
|
||||
gvkMap["kind"] == targetGVK.Kind {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func gvkMatchesSingle(targetGVK schema.GroupVersionKind, ext spec.Extensions) bool {
|
||||
var gvkMap map[string]string
|
||||
err := ext.GetObject(groupVersionKindExtensionKey, &gvkMap)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return gvkMap["group"] == targetGVK.Group &&
|
||||
gvkMap["version"] == targetGVK.Version &&
|
||||
gvkMap["kind"] == targetGVK.Kind
|
||||
}
|
||||
|
||||
func (p *Patcher) buildStrategicMergePatchFromOpenAPIV3(original, modified, current []byte) ([]byte, error) {
|
||||
gvSpec, err := p.OpenAPIV3Root.GVSpec(schema.GroupVersion{
|
||||
Group: p.Mapping.GroupVersionKind.Group,
|
||||
Version: p.Mapping.GroupVersionKind.Version,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if gvSpec == nil || gvSpec.Components == nil {
|
||||
return nil, fmt.Errorf("OpenAPI V3 Components is nil")
|
||||
}
|
||||
for _, c := range gvSpec.Components.Schemas {
|
||||
if !gvkMatchesArray(p.Mapping.GroupVersionKind, c.Extensions) {
|
||||
continue
|
||||
}
|
||||
lookupPatchMeta := strategicpatch.PatchMetaFromOpenAPIV3{Schema: c, SchemaList: gvSpec.Components.Schemas}
|
||||
if openapiv3Patch, err := strategicpatch.CreateThreeWayMergePatch(original, modified, current, lookupPatchMeta, p.Overwrite); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return openapiv3Patch, nil
|
||||
}
|
||||
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// buildStrategicMergeFromOpenAPI builds patch from OpenAPI if it is enabled.
|
||||
// This is used for core types which is published in openapi.
|
||||
func (p *Patcher) buildStrategicMergeFromOpenAPI(original, modified, current []byte) ([]byte, error) {
|
||||
schema := p.OpenapiSchema.LookupResource(p.Mapping.GroupVersionKind)
|
||||
func (p *Patcher) buildStrategicMergeFromOpenAPI(openAPISchema openapi.Resources, original, modified, current []byte) ([]byte, error) {
|
||||
schema := openAPISchema.LookupResource(p.Mapping.GroupVersionKind)
|
||||
if schema == nil {
|
||||
// Missing schema returns nil patch; also no error.
|
||||
return nil, nil
|
||||
|
@ -199,8 +323,8 @@ func (p *Patcher) buildStrategicMergeFromOpenAPI(original, modified, current []b
|
|||
}
|
||||
|
||||
// getPatchTypeFromOpenAPI looks up patch types supported by given GroupVersionKind in Open API.
|
||||
func (p *Patcher) getPatchTypeFromOpenAPI(gvk schema.GroupVersionKind) (types.PatchType, error) {
|
||||
if pc := p.OpenapiSchema.GetConsumes(p.Mapping.GroupVersionKind, "PATCH"); pc != nil {
|
||||
func (p *Patcher) getPatchTypeFromOpenAPI(openAPISchema openapi.Resources, gvk schema.GroupVersionKind) (types.PatchType, error) {
|
||||
if pc := openAPISchema.GetConsumes(p.Mapping.GroupVersionKind, "PATCH"); pc != nil {
|
||||
for _, c := range pc {
|
||||
if c == string(types.StrategicMergePatchType) {
|
||||
return types.StrategicMergePatchType, nil
|
||||
|
@ -239,7 +363,7 @@ func (p *Patcher) Patch(current runtime.Object, modified []byte, source, namespa
|
|||
}
|
||||
for i := 1; i <= p.Retries && apierrors.IsConflict(err); i++ {
|
||||
if i > triesBeforeBackOff {
|
||||
p.BackOff.Sleep(backOffPeriod)
|
||||
p.BackOff.Sleep(patchRetryBackOffPeriod)
|
||||
}
|
||||
current, getErr = p.Helper.Get(namespace, name)
|
||||
if getErr != nil {
|
||||
|
@ -262,7 +386,7 @@ func (p *Patcher) deleteAndCreate(original runtime.Object, modified []byte, name
|
|||
return modified, nil, err
|
||||
}
|
||||
// TODO: use wait
|
||||
if err := wait.PollImmediate(1*time.Second, p.Timeout, func() (bool, error) {
|
||||
if err := wait.PollUntilContextTimeout(context.Background(), 1*time.Second, p.Timeout, true, func(ctx context.Context) (bool, error) {
|
||||
if _, err := p.Helper.Get(namespace, name); !apierrors.IsNotFound(err) {
|
||||
return false, err
|
||||
}
|
||||
|
|
|
@ -28,6 +28,7 @@ import (
|
|||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/httpstream"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/cli-runtime/pkg/genericiooptions"
|
||||
"k8s.io/cli-runtime/pkg/resource"
|
||||
|
@ -125,7 +126,7 @@ func NewCmdAttach(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.
|
|||
|
||||
// RemoteAttach defines the interface accepted by the Attach command - provided for test stubbing
|
||||
type RemoteAttach interface {
|
||||
Attach(method string, url *url.URL, config *restclient.Config, stdin io.Reader, stdout, stderr io.Writer, tty bool, terminalSizeQueue remotecommand.TerminalSizeQueue) error
|
||||
Attach(url *url.URL, config *restclient.Config, stdin io.Reader, stdout, stderr io.Writer, tty bool, terminalSizeQueue remotecommand.TerminalSizeQueue) error
|
||||
}
|
||||
|
||||
// DefaultAttachFunc is the default AttachFunc used
|
||||
|
@ -148,7 +149,7 @@ func DefaultAttachFunc(o *AttachOptions, containerToAttach *corev1.Container, ra
|
|||
TTY: raw,
|
||||
}, scheme.ParameterCodec)
|
||||
|
||||
return o.Attach.Attach("POST", req.URL(), o.Config, o.In, o.Out, o.ErrOut, raw, sizeQueue)
|
||||
return o.Attach.Attach(req.URL(), o.Config, o.In, o.Out, o.ErrOut, raw, sizeQueue)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -156,8 +157,8 @@ func DefaultAttachFunc(o *AttachOptions, containerToAttach *corev1.Container, ra
|
|||
type DefaultRemoteAttach struct{}
|
||||
|
||||
// Attach executes attach to a running container
|
||||
func (*DefaultRemoteAttach) Attach(method string, url *url.URL, config *restclient.Config, stdin io.Reader, stdout, stderr io.Writer, tty bool, terminalSizeQueue remotecommand.TerminalSizeQueue) error {
|
||||
exec, err := remotecommand.NewSPDYExecutor(config, method, url)
|
||||
func (*DefaultRemoteAttach) Attach(url *url.URL, config *restclient.Config, stdin io.Reader, stdout, stderr io.Writer, tty bool, terminalSizeQueue remotecommand.TerminalSizeQueue) error {
|
||||
exec, err := createExecutor(url, config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -170,6 +171,29 @@ func (*DefaultRemoteAttach) Attach(method string, url *url.URL, config *restclie
|
|||
})
|
||||
}
|
||||
|
||||
// createExecutor returns the Executor or an error if one occurred.
|
||||
func createExecutor(url *url.URL, config *restclient.Config) (remotecommand.Executor, error) {
|
||||
exec, err := remotecommand.NewSPDYExecutor(config, "POST", url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Fallback executor is default, unless feature flag is explicitly disabled.
|
||||
if !cmdutil.RemoteCommandWebsockets.IsDisabled() {
|
||||
// WebSocketExecutor must be "GET" method as described in RFC 6455 Sec. 4.1 (page 17).
|
||||
websocketExec, err := remotecommand.NewWebSocketExecutor(config, "GET", url.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
exec, err = remotecommand.NewFallbackExecutor(websocketExec, exec, func(err error) bool {
|
||||
return httpstream.IsUpgradeFailure(err) || httpstream.IsHTTPSProxyError(err)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return exec, nil
|
||||
}
|
||||
|
||||
// Complete verifies command line arguments and loads data from the command environment
|
||||
func (o *AttachOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error {
|
||||
var err error
|
||||
|
@ -182,7 +206,7 @@ func (o *AttachOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []s
|
|||
|
||||
o.GetPodTimeout, err = cmdutil.GetPodRunningTimeoutFlag(cmd)
|
||||
if err != nil {
|
||||
return cmdutil.UsageErrorf(cmd, err.Error())
|
||||
return cmdutil.UsageErrorf(cmd, "%s", err.Error())
|
||||
}
|
||||
|
||||
o.Builder = f.NewBuilder
|
||||
|
@ -283,6 +307,7 @@ func (o *AttachOptions) Run() error {
|
|||
}
|
||||
|
||||
if !o.Quiet {
|
||||
_, _ = fmt.Fprintln(o.ErrOut, "All commands and output from this session will be recorded in container logs, including credentials and sensitive information passed through the command prompt.")
|
||||
fmt.Fprintln(o.ErrOut, "If you don't see a command prompt, try pressing enter.")
|
||||
}
|
||||
if err := t.Safe(o.AttachFunc(o, containerToAttach, t.Raw, sizeQueue)); err != nil {
|
||||
|
|
|
@ -37,19 +37,18 @@ import (
|
|||
"k8s.io/client-go/tools/remotecommand"
|
||||
"k8s.io/kubectl/pkg/cmd/exec"
|
||||
cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/cmd/util/podcmd"
|
||||
"k8s.io/kubectl/pkg/polymorphichelpers"
|
||||
"k8s.io/kubectl/pkg/scheme"
|
||||
)
|
||||
|
||||
type fakeRemoteAttach struct {
|
||||
method string
|
||||
url *url.URL
|
||||
err error
|
||||
url *url.URL
|
||||
err error
|
||||
}
|
||||
|
||||
func (f *fakeRemoteAttach) Attach(method string, url *url.URL, config *restclient.Config, stdin io.Reader, stdout, stderr io.Writer, tty bool, terminalSizeQueue remotecommand.TerminalSizeQueue) error {
|
||||
f.method = method
|
||||
func (f *fakeRemoteAttach) Attach(url *url.URL, config *restclient.Config, stdin io.Reader, stdout, stderr io.Writer, tty bool, terminalSizeQueue remotecommand.TerminalSizeQueue) error {
|
||||
f.url = url
|
||||
return f.err
|
||||
}
|
||||
|
@ -243,6 +242,7 @@ func TestAttach(t *testing.T) {
|
|||
pod *corev1.Pod
|
||||
remoteAttachErr bool
|
||||
expectedErr string
|
||||
expectedErrOut []string
|
||||
}{
|
||||
{
|
||||
name: "pod attach",
|
||||
|
@ -252,6 +252,10 @@ func TestAttach(t *testing.T) {
|
|||
attachPath: "/api/" + version + "/namespaces/test/pods/foo/attach",
|
||||
pod: attachPod(),
|
||||
container: "bar",
|
||||
expectedErrOut: []string{
|
||||
"All commands and output from this session will be recorded in container logs, including credentials and sensitive information passed through the command prompt.",
|
||||
"If you don't see a command prompt, try pressing enter.",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "pod attach error",
|
||||
|
@ -306,10 +310,11 @@ func TestAttach(t *testing.T) {
|
|||
if test.remoteAttachErr {
|
||||
remoteAttach.err = fmt.Errorf("attach error")
|
||||
}
|
||||
streams, _, _, errOut := genericiooptions.NewTestIOStreams()
|
||||
options := &AttachOptions{
|
||||
StreamOptions: exec.StreamOptions{
|
||||
ContainerName: test.container,
|
||||
IOStreams: genericiooptions.NewTestIOStreamsDiscard(),
|
||||
IOStreams: streams,
|
||||
},
|
||||
Attach: remoteAttach,
|
||||
GetPodTimeout: 1000,
|
||||
|
@ -327,7 +332,7 @@ func TestAttach(t *testing.T) {
|
|||
return err
|
||||
}
|
||||
|
||||
return options.Attach.Attach("POST", u, nil, nil, nil, nil, raw, sizeQueue)
|
||||
return options.Attach.Attach(u, nil, nil, nil, nil, raw, sizeQueue)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -347,12 +352,17 @@ func TestAttach(t *testing.T) {
|
|||
t.Errorf("%s: Did not get expected path for exec request: %q %q", test.name, test.attachPath, remoteAttach.url.Path)
|
||||
return
|
||||
}
|
||||
if remoteAttach.method != "POST" {
|
||||
t.Errorf("%s: Did not get method for attach request: %s", test.name, remoteAttach.method)
|
||||
}
|
||||
if remoteAttach.url.Query().Get("container") != "bar" {
|
||||
t.Errorf("%s: Did not have query parameters: %s", test.name, remoteAttach.url.Query())
|
||||
}
|
||||
if test.expectedErrOut != nil {
|
||||
for _, expect := range test.expectedErrOut {
|
||||
if !strings.Contains(errOut.String(), expect) {
|
||||
t.Errorf("%s: expected message %s not found, got: %s", test.name, expect, strings.ReplaceAll(errOut.String(), "\n", ""))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -428,7 +438,7 @@ func TestAttachWarnings(t *testing.T) {
|
|||
return err
|
||||
}
|
||||
|
||||
return options.Attach.Attach("POST", u, nil, nil, nil, nil, raw, sizeQueue)
|
||||
return options.Attach.Attach(u, nil, nil, nil, nil, raw, sizeQueue)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -558,3 +568,37 @@ func TestReattachMessage(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateExecutor(t *testing.T) {
|
||||
url, err := url.Parse("http://localhost:8080/index.html")
|
||||
if err != nil {
|
||||
t.Fatalf("unable to parse test url: %v", err)
|
||||
}
|
||||
config := cmdtesting.DefaultClientConfig()
|
||||
// First, ensure that no environment variable creates the fallback executor.
|
||||
executor, err := createExecutor(url, config)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create executor: %v", err)
|
||||
}
|
||||
if _, isFallback := executor.(*remotecommand.FallbackExecutor); !isFallback {
|
||||
t.Errorf("expected fallback executor, got %#v", executor)
|
||||
}
|
||||
// Next, check turning on feature flag explicitly also creates fallback executor.
|
||||
t.Setenv(string(cmdutil.RemoteCommandWebsockets), "true")
|
||||
executor, err = createExecutor(url, config)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create executor: %v", err)
|
||||
}
|
||||
if _, isFallback := executor.(*remotecommand.FallbackExecutor); !isFallback {
|
||||
t.Errorf("expected fallback executor, got %#v", executor)
|
||||
}
|
||||
// Finally, check explicit disabling does NOT create the fallback executor.
|
||||
t.Setenv(string(cmdutil.RemoteCommandWebsockets), "false")
|
||||
executor, err = createExecutor(url, config)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create executor: %v", err)
|
||||
}
|
||||
if _, isFallback := executor.(*remotecommand.FallbackExecutor); isFallback {
|
||||
t.Errorf("expected fallback executor, got %#v", executor)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -83,9 +83,8 @@ var (
|
|||
# Check to see if I can list deployments in my current namespace
|
||||
kubectl auth can-i list deployments.apps
|
||||
|
||||
# Check to see if service account "foo" of namespace "dev" can list pods
|
||||
# in the namespace "prod".
|
||||
# You must be allowed to use impersonation for the global option "--as".
|
||||
# Check to see if service account "foo" of namespace "dev" can list pods in the namespace "prod"
|
||||
# You must be allowed to use impersonation for the global option "--as"
|
||||
kubectl auth can-i list pods --as=system:serviceaccount:dev:foo -n prod
|
||||
|
||||
# Check to see if I can do everything in my current namespace ("*" means all)
|
||||
|
@ -100,13 +99,16 @@ var (
|
|||
# Check to see if I can access the URL /logs/
|
||||
kubectl auth can-i get /logs/
|
||||
|
||||
# Check to see if I can approve certificates.k8s.io
|
||||
kubectl auth can-i approve certificates.k8s.io
|
||||
|
||||
# List all allowed actions in namespace "foo"
|
||||
kubectl auth can-i --list --namespace=foo`)
|
||||
|
||||
resourceVerbs = sets.NewString("get", "list", "watch", "create", "update", "patch", "delete", "deletecollection", "use", "bind", "impersonate", "*")
|
||||
nonResourceURLVerbs = sets.NewString("get", "put", "post", "head", "options", "delete", "patch", "*")
|
||||
resourceVerbs = sets.New[string]("get", "list", "watch", "create", "update", "patch", "delete", "deletecollection", "use", "bind", "impersonate", "*", "approve", "sign", "escalate", "attest")
|
||||
nonResourceURLVerbs = sets.New[string]("get", "put", "post", "head", "options", "delete", "patch", "*")
|
||||
// holds all the server-supported resources that cannot be discovered by clients. i.e. users and groups for the impersonate verb
|
||||
nonStandardResourceNames = sets.NewString("users", "groups")
|
||||
nonStandardResourceNames = sets.New[string]("users", "groups")
|
||||
)
|
||||
|
||||
// NewCmdCanI returns an initialized Command for 'auth can-i' sub command
|
||||
|
@ -183,7 +185,7 @@ func (o *CanIOptions) Complete(f cmdutil.Factory, args []string) error {
|
|||
default:
|
||||
errString := "you must specify two arguments: verb resource or verb resource/resourceName."
|
||||
usageString := "See 'kubectl auth can-i -h' for help and examples."
|
||||
return errors.New(fmt.Sprintf("%s\n%s", errString, usageString))
|
||||
return fmt.Errorf("%s\n%s", errString, usageString)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -161,7 +161,7 @@ func TestRunAccessCheck(t *testing.T) {
|
|||
test.serverErr
|
||||
}),
|
||||
}
|
||||
tf.ClientConfigVal = &restclient.Config{ContentConfig: restclient.ContentConfig{GroupVersion: &schema.GroupVersion{Group: "", Version: "v1"}}}
|
||||
tf.ClientConfigVal = &restclient.Config{ContentConfig: restclient.ContentConfig{GroupVersion: &schema.GroupVersion{Group: "", Version: "v1"}, ContentType: runtime.ContentTypeJSON}}
|
||||
|
||||
if err := test.o.Complete(tf, test.args); err != nil {
|
||||
t.Errorf("%s: %v", test.name, err)
|
||||
|
@ -196,6 +196,7 @@ func TestRunAccessList(t *testing.T) {
|
|||
" [/version] [] [get]\n"
|
||||
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
tf.ClientConfigVal.ContentType = runtime.ContentTypeJSON
|
||||
defer tf.Cleanup()
|
||||
|
||||
ns := scheme.Codecs.WithoutConversion()
|
||||
|
|
|
@ -122,16 +122,16 @@ var (
|
|||
whoAmILong = templates.LongDesc(`
|
||||
Experimental: Check who you are and your attributes (groups, extra).
|
||||
|
||||
This command is helpful to get yourself aware of the current user attributes,
|
||||
especially when dynamic authentication, e.g., token webhook, auth proxy, or OIDC provider,
|
||||
This command is helpful to get yourself aware of the current user attributes,
|
||||
especially when dynamic authentication, e.g., token webhook, auth proxy, or OIDC provider,
|
||||
is enabled in the Kubernetes cluster.
|
||||
`)
|
||||
|
||||
whoAmIExample = templates.Examples(`
|
||||
# Get your subject attributes.
|
||||
# Get your subject attributes
|
||||
kubectl auth whoami
|
||||
|
||||
# Get your subject attributes in JSON format.
|
||||
|
||||
# Get your subject attributes in JSON format
|
||||
kubectl auth whoami -o json
|
||||
`)
|
||||
)
|
||||
|
@ -250,7 +250,7 @@ func printTableSelfSubjectAccessReview(obj runtime.Object, out io.Writer) error
|
|||
}
|
||||
|
||||
if len(ui.Extra) > 0 {
|
||||
for _, k := range sets.StringKeySet(ui.Extra).List() {
|
||||
for _, k := range sets.List(sets.KeySet(ui.Extra)) {
|
||||
v := ui.Extra[k]
|
||||
_, err := fmt.Fprintf(w, "Extra: %s\t%v\n", k, v)
|
||||
if err != nil {
|
||||
|
|
|
@ -75,9 +75,7 @@ func TestWhoAmIRun(t *testing.T) {
|
|||
`{
|
||||
"kind": "SelfSubjectReview",
|
||||
"apiVersion": "authentication.k8s.io/v1",
|
||||
"metadata": {
|
||||
"creationTimestamp": null
|
||||
},
|
||||
"metadata": {},
|
||||
"status": {
|
||||
"userInfo": {
|
||||
"username": "jane.doe",
|
||||
|
@ -131,9 +129,7 @@ func TestWhoAmIRun(t *testing.T) {
|
|||
`{
|
||||
"kind": "SelfSubjectReview",
|
||||
"apiVersion": "authentication.k8s.io/v1beta1",
|
||||
"metadata": {
|
||||
"creationTimestamp": null
|
||||
},
|
||||
"metadata": {},
|
||||
"status": {
|
||||
"userInfo": {
|
||||
"username": "jane.doe",
|
||||
|
@ -186,9 +182,7 @@ func TestWhoAmIRun(t *testing.T) {
|
|||
`{
|
||||
"kind": "SelfSubjectReview",
|
||||
"apiVersion": "authentication.k8s.io/v1",
|
||||
"metadata": {
|
||||
"creationTimestamp": null
|
||||
},
|
||||
"metadata": {},
|
||||
"status": {
|
||||
"userInfo": {
|
||||
"username": "jane.doe",
|
||||
|
|
|
@ -19,19 +19,26 @@ package autoscale
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/klog/v2"
|
||||
|
||||
autoscalingv1 "k8s.io/api/autoscaling/v1"
|
||||
autoscalingv2 "k8s.io/api/autoscaling/v2"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
apiresource "k8s.io/apimachinery/pkg/api/resource"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/cli-runtime/pkg/genericiooptions"
|
||||
"k8s.io/cli-runtime/pkg/printers"
|
||||
"k8s.io/cli-runtime/pkg/resource"
|
||||
autoscalingv1client "k8s.io/client-go/kubernetes/typed/autoscaling/v1"
|
||||
autoscalingv2client "k8s.io/client-go/kubernetes/typed/autoscaling/v2"
|
||||
"k8s.io/client-go/scale"
|
||||
"k8s.io/klog/v2"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/scheme"
|
||||
"k8s.io/kubectl/pkg/util"
|
||||
|
@ -43,16 +50,23 @@ import (
|
|||
var (
|
||||
autoscaleLong = templates.LongDesc(i18n.T(`
|
||||
Creates an autoscaler that automatically chooses and sets the number of pods that run in a Kubernetes cluster.
|
||||
The command will attempt to use the autoscaling/v2 API first, in case of an error, it will fall back to autoscaling/v1 API.
|
||||
|
||||
Looks up a deployment, replica set, stateful set, or replication controller by name and creates an autoscaler that uses the given resource as a reference.
|
||||
An autoscaler can automatically increase or decrease number of pods deployed within the system as needed.`))
|
||||
|
||||
autoscaleExample = templates.Examples(i18n.T(`
|
||||
# Auto scale a deployment "foo", with the number of pods between 2 and 10, no target CPU utilization specified so a default autoscaling policy will be used
|
||||
kubectl autoscale deployment foo --min=2 --max=10
|
||||
kubectl autoscale deployment foo --min=2 --max=10
|
||||
|
||||
# Auto scale a replication controller "foo", with the number of pods between 1 and 5, target CPU utilization at 80%
|
||||
kubectl autoscale rc foo --max=5 --cpu-percent=80`))
|
||||
kubectl autoscale rc foo --max=5 --cpu=80%
|
||||
|
||||
# Auto scale a deployment "bar", with the number of pods between 3 and 6, target average CPU of 500m and memory of 200Mi
|
||||
kubectl autoscale deployment bar --min=3 --max=6 --cpu=500m --memory=200Mi
|
||||
|
||||
# Auto scale a deployment "bar", with the number of pods between 2 and 8, target CPU utilization 60% and memory utilization 70%
|
||||
kubectl autoscale deployment bar --min=3 --max=6 --cpu=60% --memory=70%`))
|
||||
)
|
||||
|
||||
// AutoscaleOptions declares the arguments accepted by the Autoscale command
|
||||
|
@ -69,6 +83,8 @@ type AutoscaleOptions struct {
|
|||
Min int32
|
||||
Max int32
|
||||
CPUPercent int32
|
||||
CPU string
|
||||
Memory string
|
||||
|
||||
createAnnotation bool
|
||||
args []string
|
||||
|
@ -78,7 +94,8 @@ type AutoscaleOptions struct {
|
|||
builder *resource.Builder
|
||||
fieldManager string
|
||||
|
||||
HPAClient autoscalingv1client.HorizontalPodAutoscalersGetter
|
||||
HPAClientV1 autoscalingv1client.HorizontalPodAutoscalersGetter
|
||||
HPAClientV2 autoscalingv2client.HorizontalPodAutoscalersGetter
|
||||
scaleKindResolver scale.ScaleKindResolver
|
||||
|
||||
genericiooptions.IOStreams
|
||||
|
@ -103,7 +120,7 @@ func NewCmdAutoscale(f cmdutil.Factory, ioStreams genericiooptions.IOStreams) *c
|
|||
validArgs := []string{"deployment", "replicaset", "replicationcontroller", "statefulset"}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "autoscale (-f FILENAME | TYPE NAME | TYPE/NAME) [--min=MINPODS] --max=MAXPODS [--cpu-percent=CPU]",
|
||||
Use: "autoscale (-f FILENAME | TYPE NAME | TYPE/NAME) [--min=MINPODS] --max=MAXPODS [--cpu=CPU] [--memory=MEMORY]",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: i18n.T("Auto-scale a deployment, replica set, stateful set, or replication controller"),
|
||||
Long: autoscaleLong,
|
||||
|
@ -123,7 +140,11 @@ func NewCmdAutoscale(f cmdutil.Factory, ioStreams genericiooptions.IOStreams) *c
|
|||
cmd.Flags().Int32Var(&o.Max, "max", -1, "The upper limit for the number of pods that can be set by the autoscaler. Required.")
|
||||
cmd.MarkFlagRequired("max")
|
||||
cmd.Flags().Int32Var(&o.CPUPercent, "cpu-percent", -1, "The target average CPU utilization (represented as a percent of requested CPU) over all the pods. If it's not specified or negative, a default autoscaling policy will be used.")
|
||||
cmd.Flags().StringVar(&o.CPU, "cpu", "", `Target CPU utilization over all the pods. When specified as a percentage (e.g."70%" for 70% of requested CPU) it will target average utilization. When specified as quantity (e.g."500m" for 500 milliCPU) it will target average value. Value without units is treated as a quantity with miliCPU being the unit (e.g."500" is "500m").`)
|
||||
cmd.Flags().StringVar(&o.Memory, "memory", "", `Target memory utilization over all the pods. When specified as a percentage (e.g."60%" for 60% of requested memory) it will target average utilization. When specified as quantity (e.g."200Mi" for 200 MiB, "1Gi" for 1 GiB) it will target average value. Value without units is treated as a quantity with mebibytes being the unit (e.g."200" is "200Mi").`)
|
||||
cmd.Flags().StringVar(&o.Name, "name", "", i18n.T("The name for the newly created object. If not specified, the name of the input resource will be used."))
|
||||
_ = cmd.Flags().MarkDeprecated("cpu-percent",
|
||||
"Use --cpu with percentage or resource quantity format (e.g., '70%' for utilization or '500m' for milliCPU).")
|
||||
cmdutil.AddDryRunFlag(cmd)
|
||||
cmdutil.AddFilenameOptionFlags(cmd, o.FilenameOptions, "identifying the resource to autoscale.")
|
||||
cmdutil.AddApplyAnnotationFlags(cmd)
|
||||
|
@ -157,7 +178,8 @@ func (o *AutoscaleOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
o.HPAClient = kubeClient.AutoscalingV1()
|
||||
o.HPAClientV2 = kubeClient.AutoscalingV2()
|
||||
o.HPAClientV1 = kubeClient.AutoscalingV1()
|
||||
|
||||
o.namespace, o.enforceNamespace, err = f.ToRawKubeConfigLoader().Namespace()
|
||||
if err != nil {
|
||||
|
@ -182,11 +204,25 @@ func (o *AutoscaleOptions) Validate() error {
|
|||
if o.Max < o.Min {
|
||||
return fmt.Errorf("--max=MAXPODS must be larger or equal to --min=MINPODS, max: %d, min: %d", o.Max, o.Min)
|
||||
}
|
||||
|
||||
// only one of the CPUPercent or CPU param is allowed
|
||||
if o.CPUPercent > 0 && o.CPU != "" {
|
||||
return fmt.Errorf("--cpu-percent and --cpu are mutually exclusive")
|
||||
}
|
||||
// validate CPU target if specified
|
||||
if o.CPU != "" {
|
||||
if _, _, _, err := parseResourceInput(o.CPU, corev1.ResourceCPU); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// validate Memory target if specified
|
||||
if o.Memory != "" {
|
||||
if _, _, _, err := parseResourceInput(o.Memory, corev1.ResourceMemory); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Run performs the execution
|
||||
func (o *AutoscaleOptions) Run() error {
|
||||
r := o.builder.
|
||||
Unstructured().
|
||||
|
@ -208,48 +244,31 @@ func (o *AutoscaleOptions) Run() error {
|
|||
|
||||
mapping := info.ResourceMapping()
|
||||
gvr := mapping.GroupVersionKind.GroupVersion().WithResource(mapping.Resource.Resource)
|
||||
if _, err := o.scaleKindResolver.ScaleForResource(gvr); err != nil {
|
||||
return fmt.Errorf("cannot autoscale a %v: %v", mapping.GroupVersionKind.Kind, err)
|
||||
if _, err = o.scaleKindResolver.ScaleForResource(gvr); err != nil {
|
||||
return fmt.Errorf("cannot autoscale a %s: %w", mapping.GroupVersionKind.Kind, err)
|
||||
}
|
||||
|
||||
hpa := o.createHorizontalPodAutoscaler(info.Name, mapping)
|
||||
|
||||
if err := o.Recorder.Record(hpa); err != nil {
|
||||
klog.V(4).Infof("error recording current command: %v", err)
|
||||
// handles the creation of HorizontalPodAutoscaler objects for both autoscaling/v2 and autoscaling/v1 APIs.
|
||||
// If autoscaling/v2 API fails, try to create and handle HorizontalPodAutoscaler using autoscaling/v1 API
|
||||
var hpaV2 runtime.Object
|
||||
hpaV2, err = o.createHorizontalPodAutoscalerV2(info.Name, mapping)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create HorizontalPodAutoscaler using autoscaling/v2 API: %w", err)
|
||||
}
|
||||
|
||||
if o.dryRunStrategy == cmdutil.DryRunClient {
|
||||
count++
|
||||
|
||||
printer, err := o.ToPrinter("created")
|
||||
if err != nil {
|
||||
if err = o.handleHPA(hpaV2); err != nil {
|
||||
klog.V(1).Infof("Encountered an error with the autoscaling/v2 HorizontalPodAutoscaler: %v. "+
|
||||
"Falling back to try the autoscaling/v1 HorizontalPodAutoscaler", err)
|
||||
// check if the HPA can be created using v1 API.
|
||||
if ok, err := o.canCreateHPAV1(); !ok {
|
||||
return fmt.Errorf("failed to create autoscaling/v2 HPA and the configuration is incompatible with autoscaling/v1: %w", err)
|
||||
}
|
||||
hpaV1 := o.createHorizontalPodAutoscalerV1(info.Name, mapping)
|
||||
if err := o.handleHPA(hpaV1); err != nil {
|
||||
return err
|
||||
}
|
||||
return printer.PrintObj(hpa, o.Out)
|
||||
}
|
||||
|
||||
if err := util.CreateOrUpdateAnnotation(o.createAnnotation, hpa, scheme.DefaultJSONEncoder()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
createOptions := metav1.CreateOptions{}
|
||||
if o.fieldManager != "" {
|
||||
createOptions.FieldManager = o.fieldManager
|
||||
}
|
||||
if o.dryRunStrategy == cmdutil.DryRunServer {
|
||||
createOptions.DryRun = []string{metav1.DryRunAll}
|
||||
}
|
||||
actualHPA, err := o.HPAClient.HorizontalPodAutoscalers(o.namespace).Create(context.TODO(), hpa, createOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
count++
|
||||
printer, err := o.ToPrinter("autoscaled")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return printer.PrintObj(actualHPA, o.Out)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -260,7 +279,169 @@ func (o *AutoscaleOptions) Run() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (o *AutoscaleOptions) createHorizontalPodAutoscaler(refName string, mapping *meta.RESTMapping) *autoscalingv1.HorizontalPodAutoscaler {
|
||||
func (o *AutoscaleOptions) canCreateHPAV1() (bool, error) {
|
||||
// Allow fallback to v1 HPA only if:
|
||||
// 1. CPUPercent is set and Memory is not set.
|
||||
// 2. Or, Memory is not set and the metric type is UtilizationMetricType.
|
||||
_, _, metricsType, err := parseResourceInput(o.CPU, corev1.ResourceCPU)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return (o.CPUPercent >= 0 && o.Memory == "") ||
|
||||
(o.Memory == "" && metricsType == autoscalingv2.UtilizationMetricType), nil
|
||||
}
|
||||
|
||||
// handleHPA handles the creation and management of a single HPA object.
|
||||
func (o *AutoscaleOptions) handleHPA(hpa runtime.Object) error {
|
||||
if err := o.Recorder.Record(hpa); err != nil {
|
||||
return fmt.Errorf("error recording current command: %w", err)
|
||||
}
|
||||
|
||||
if o.dryRunStrategy == cmdutil.DryRunClient {
|
||||
printer, err := o.ToPrinter("created")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return printer.PrintObj(hpa, o.Out)
|
||||
}
|
||||
|
||||
if err := util.CreateOrUpdateAnnotation(o.createAnnotation, hpa, scheme.DefaultJSONEncoder()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
createOptions := metav1.CreateOptions{}
|
||||
if o.fieldManager != "" {
|
||||
createOptions.FieldManager = o.fieldManager
|
||||
}
|
||||
if o.dryRunStrategy == cmdutil.DryRunServer {
|
||||
createOptions.DryRun = []string{metav1.DryRunAll}
|
||||
}
|
||||
|
||||
var actualHPA runtime.Object
|
||||
var err error
|
||||
switch typedHPA := hpa.(type) {
|
||||
case *autoscalingv2.HorizontalPodAutoscaler:
|
||||
actualHPA, err = o.HPAClientV2.HorizontalPodAutoscalers(o.namespace).Create(context.TODO(), typedHPA, createOptions)
|
||||
case *autoscalingv1.HorizontalPodAutoscaler:
|
||||
actualHPA, err = o.HPAClientV1.HorizontalPodAutoscalers(o.namespace).Create(context.TODO(), typedHPA, createOptions)
|
||||
default:
|
||||
return fmt.Errorf("unsupported HorizontalPodAutoscaler type %T", hpa)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printer, err := o.ToPrinter("autoscaled")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return printer.PrintObj(actualHPA, o.Out)
|
||||
}
|
||||
|
||||
func (o *AutoscaleOptions) createHorizontalPodAutoscalerV2(refName string, mapping *meta.RESTMapping) (*autoscalingv2.HorizontalPodAutoscaler, error) {
|
||||
name := o.Name
|
||||
if len(name) == 0 {
|
||||
name = refName
|
||||
}
|
||||
|
||||
scaler := autoscalingv2.HorizontalPodAutoscaler{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
},
|
||||
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
|
||||
ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{
|
||||
APIVersion: mapping.GroupVersionKind.GroupVersion().String(),
|
||||
Kind: mapping.GroupVersionKind.Kind,
|
||||
Name: refName,
|
||||
},
|
||||
MaxReplicas: o.Max,
|
||||
},
|
||||
}
|
||||
|
||||
if o.Min > 0 {
|
||||
scaler.Spec.MinReplicas = &o.Min
|
||||
}
|
||||
|
||||
metrics := []autoscalingv2.MetricSpec{}
|
||||
|
||||
// add CPU metric if any of the CPU targets are specified
|
||||
if o.CPUPercent > 0 {
|
||||
cpuMetric := autoscalingv2.MetricSpec{
|
||||
Type: autoscalingv2.ResourceMetricSourceType,
|
||||
Resource: &autoscalingv2.ResourceMetricSource{
|
||||
Name: corev1.ResourceCPU,
|
||||
Target: autoscalingv2.MetricTarget{},
|
||||
},
|
||||
}
|
||||
cpuMetric.Resource.Target.Type = autoscalingv2.UtilizationMetricType
|
||||
cpuMetric.Resource.Target.AverageUtilization = &o.CPUPercent
|
||||
metrics = append(metrics, cpuMetric)
|
||||
}
|
||||
|
||||
// add Cpu metric if any of the cpu targets are specified
|
||||
if o.CPU != "" {
|
||||
cpuMetric := autoscalingv2.MetricSpec{
|
||||
Type: autoscalingv2.ResourceMetricSourceType,
|
||||
Resource: &autoscalingv2.ResourceMetricSource{
|
||||
Name: corev1.ResourceCPU,
|
||||
Target: autoscalingv2.MetricTarget{},
|
||||
},
|
||||
}
|
||||
|
||||
quantity, value, metricsType, err := parseResourceInput(o.CPU, corev1.ResourceCPU)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch metricsType {
|
||||
case autoscalingv2.UtilizationMetricType:
|
||||
cpuMetric.Resource.Target.Type = autoscalingv2.UtilizationMetricType
|
||||
cpuMetric.Resource.Target.AverageUtilization = &value
|
||||
case autoscalingv2.AverageValueMetricType:
|
||||
cpuMetric.Resource.Target.Type = autoscalingv2.AverageValueMetricType
|
||||
cpuMetric.Resource.Target.AverageValue = &quantity
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported metric type: %v", metricsType)
|
||||
}
|
||||
metrics = append(metrics, cpuMetric)
|
||||
}
|
||||
|
||||
// add Memory metric if any of the memory targets are specified
|
||||
if o.Memory != "" {
|
||||
memoryMetric := autoscalingv2.MetricSpec{
|
||||
Type: autoscalingv2.ResourceMetricSourceType,
|
||||
Resource: &autoscalingv2.ResourceMetricSource{
|
||||
Name: corev1.ResourceMemory,
|
||||
Target: autoscalingv2.MetricTarget{},
|
||||
},
|
||||
}
|
||||
quantity, value, metricsType, err := parseResourceInput(o.Memory, corev1.ResourceMemory)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch metricsType {
|
||||
case autoscalingv2.UtilizationMetricType:
|
||||
memoryMetric.Resource.Target.Type = autoscalingv2.UtilizationMetricType
|
||||
memoryMetric.Resource.Target.AverageUtilization = &value
|
||||
case autoscalingv2.AverageValueMetricType:
|
||||
memoryMetric.Resource.Target.Type = autoscalingv2.AverageValueMetricType
|
||||
memoryMetric.Resource.Target.AverageValue = &quantity
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported metric type: %v", metricsType)
|
||||
}
|
||||
metrics = append(metrics, memoryMetric)
|
||||
}
|
||||
|
||||
// Only set Metrics if there are any defined
|
||||
if len(metrics) > 0 {
|
||||
scaler.Spec.Metrics = metrics
|
||||
} else {
|
||||
scaler.Spec.Metrics = nil
|
||||
}
|
||||
|
||||
return &scaler, nil
|
||||
}
|
||||
|
||||
func (o *AutoscaleOptions) createHorizontalPodAutoscalerV1(refName string, mapping *meta.RESTMapping) *autoscalingv1.HorizontalPodAutoscaler {
|
||||
name := o.Name
|
||||
if len(name) == 0 {
|
||||
name = refName
|
||||
|
@ -291,3 +472,72 @@ func (o *AutoscaleOptions) createHorizontalPodAutoscaler(refName string, mapping
|
|||
|
||||
return &scaler
|
||||
}
|
||||
|
||||
// parseResourceInput parses a resource input string into either a utilization percentage or a quantity value.
|
||||
// It supports:
|
||||
// - Percentage values (e.g., "70%") for UtilizationMetricType
|
||||
// - Quantity values with units (e.g., "500m", "2Gi")
|
||||
// - Bare numbers without units, which are interpreted as:
|
||||
// - CPU: milliCPU ("500" → "500m")
|
||||
// - Memory: Mebibytes ("512" → "512Mi")
|
||||
func parseResourceInput(input string, resourceType corev1.ResourceName) (apiresource.Quantity, int32, autoscalingv2.MetricTargetType, error) {
|
||||
input = strings.TrimSpace(input)
|
||||
if input == "" {
|
||||
return apiresource.Quantity{}, 0, "", fmt.Errorf("empty input")
|
||||
}
|
||||
|
||||
// Case 1: Handle percentage-based metrics like "70%"
|
||||
percentValue, isPercent, err := parsePercentage(input)
|
||||
if isPercent {
|
||||
if err != nil {
|
||||
return apiresource.Quantity{}, 0, "", err
|
||||
}
|
||||
return apiresource.Quantity{}, percentValue, autoscalingv2.UtilizationMetricType, nil
|
||||
}
|
||||
|
||||
// Case 2: Try to interpret input as a bare number (e.g., "500"), and apply default float
|
||||
valueFloat, err := strconv.ParseFloat(input, 64)
|
||||
if err == nil {
|
||||
unit, err := getDefaultUnitForResource(resourceType)
|
||||
if err != nil {
|
||||
return apiresource.Quantity{}, 0, "", err
|
||||
}
|
||||
|
||||
inputWithUnit := fmt.Sprintf("%g%s", valueFloat, unit)
|
||||
quantity, err := apiresource.ParseQuantity(inputWithUnit)
|
||||
if err != nil {
|
||||
return apiresource.Quantity{}, 0, "", err
|
||||
}
|
||||
return quantity, 0, autoscalingv2.AverageValueMetricType, nil
|
||||
}
|
||||
|
||||
// Case 3: Parse normally if input has a valid unit (e.g., "500m", "2Gi")
|
||||
quantity, err := apiresource.ParseQuantity(input)
|
||||
if err != nil {
|
||||
return apiresource.Quantity{}, 0, "", fmt.Errorf("invalid resource %s value: %s", resourceType, input)
|
||||
}
|
||||
return quantity, 0, autoscalingv2.AverageValueMetricType, nil
|
||||
}
|
||||
|
||||
func getDefaultUnitForResource(resourceType corev1.ResourceName) (string, error) {
|
||||
switch resourceType {
|
||||
case corev1.ResourceCPU:
|
||||
return "m", nil
|
||||
case corev1.ResourceMemory:
|
||||
return "Mi", nil
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported resource type: %v", resourceType)
|
||||
}
|
||||
}
|
||||
|
||||
func parsePercentage(input string) (int32, bool, error) {
|
||||
if !strings.HasSuffix(input, "%") {
|
||||
return 0, false, nil
|
||||
}
|
||||
trimmed := strings.TrimSuffix(input, "%")
|
||||
valueInt64, err := strconv.ParseInt(trimmed, 10, 32)
|
||||
if err != nil || valueInt64 < 0 {
|
||||
return 0, true, fmt.Errorf("invalid percentage value: %s", trimmed)
|
||||
}
|
||||
return int32(valueInt64), true, nil
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -33,7 +33,6 @@ import (
|
|||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
|
||||
ct "github.com/daviddengcn/go-colortext"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
@ -156,12 +155,8 @@ func (o *ClusterInfoOptions) Run() error {
|
|||
}
|
||||
|
||||
func printService(out io.Writer, name, link string) {
|
||||
ct.ChangeColor(ct.Green, false, ct.None, false)
|
||||
fmt.Fprint(out, name)
|
||||
ct.ResetColor()
|
||||
fmt.Fprint(out, " is running at ")
|
||||
ct.ChangeColor(ct.Yellow, false, ct.None, false)
|
||||
fmt.Fprint(out, link)
|
||||
ct.ResetColor()
|
||||
fmt.Fprintln(out, "")
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ import (
|
|||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
@ -118,8 +119,8 @@ func setupOutputWriter(dir string, defaultWriter io.Writer, filename string, fil
|
|||
if len(dir) == 0 || dir == "-" {
|
||||
return defaultWriter
|
||||
}
|
||||
fullFile := path.Join(dir, filename) + fileExtension
|
||||
parent := path.Dir(fullFile)
|
||||
fullFile := filepath.Join(dir, filename) + fileExtension
|
||||
parent := filepath.Dir(fullFile)
|
||||
cmdutil.CheckErr(os.MkdirAll(parent, 0755))
|
||||
|
||||
file, err := os.Create(fullFile)
|
||||
|
|
|
@ -18,7 +18,7 @@ package clusterinfo
|
|||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"k8s.io/cli-runtime/pkg/genericiooptions"
|
||||
|
@ -60,7 +60,7 @@ func TestSetupOutputWriterFile(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
fullPath := path.Join(dir, file) + extension
|
||||
fullPath := filepath.Join(dir, file) + extension
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
_, _, buf, _ := genericiooptions.NewTestIOStreams()
|
||||
|
|
123
pkg/cmd/cmd.go
123
pkg/cmd/cmd.go
|
@ -73,6 +73,7 @@ import (
|
|||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/cmd/version"
|
||||
"k8s.io/kubectl/pkg/cmd/wait"
|
||||
"k8s.io/kubectl/pkg/kuberc"
|
||||
utilcomp "k8s.io/kubectl/pkg/util/completion"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
|
@ -82,8 +83,6 @@ import (
|
|||
"k8s.io/kubectl/pkg/cmd/kustomize"
|
||||
)
|
||||
|
||||
const kubectlCmdHeaders = "KUBECTL_COMMAND_HEADERS"
|
||||
|
||||
type KubectlOptions struct {
|
||||
PluginHandler PluginHandler
|
||||
Arguments []string
|
||||
|
@ -92,15 +91,18 @@ type KubectlOptions struct {
|
|||
genericiooptions.IOStreams
|
||||
}
|
||||
|
||||
var defaultConfigFlags = genericclioptions.NewConfigFlags(true).WithDeprecatedPasswordFlag().WithDiscoveryBurst(300).WithDiscoveryQPS(50.0)
|
||||
func defaultConfigFlags() *genericclioptions.ConfigFlags {
|
||||
return genericclioptions.NewConfigFlags(true).WithDeprecatedPasswordFlag().WithDiscoveryBurst(300).WithDiscoveryQPS(50.0)
|
||||
}
|
||||
|
||||
// NewDefaultKubectlCommand creates the `kubectl` command with default arguments
|
||||
func NewDefaultKubectlCommand() *cobra.Command {
|
||||
ioStreams := genericiooptions.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr}
|
||||
return NewDefaultKubectlCommandWithArgs(KubectlOptions{
|
||||
PluginHandler: NewDefaultPluginHandler(plugin.ValidPluginFilenamePrefixes),
|
||||
Arguments: os.Args,
|
||||
ConfigFlags: defaultConfigFlags,
|
||||
IOStreams: genericiooptions.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr},
|
||||
ConfigFlags: defaultConfigFlags().WithWarningPrinter(ioStreams),
|
||||
IOStreams: ioStreams,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -133,36 +135,29 @@ func NewDefaultKubectlCommandWithArgs(o KubectlOptions) *cobra.Command {
|
|||
case "help", cobra.ShellCompRequestCmd, cobra.ShellCompNoDescRequestCmd:
|
||||
// Don't search for a plugin
|
||||
default:
|
||||
if err := HandlePluginCommand(o.PluginHandler, cmdPathPieces, false); err != nil {
|
||||
if err := HandlePluginCommand(o.PluginHandler, cmdPathPieces, 1); err != nil {
|
||||
fmt.Fprintf(o.IOStreams.ErrOut, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
} else if err == nil {
|
||||
if cmdutil.CmdPluginAsSubcommand.IsEnabled() {
|
||||
// Command exists(e.g. kubectl create), but it is not certain that
|
||||
// subcommand also exists (e.g. kubectl create networkpolicy)
|
||||
if IsSubcommandPluginAllowed(foundCmd.Name()) {
|
||||
var subcommand string
|
||||
for _, arg := range foundArgs { // first "non-flag" argument as subcommand
|
||||
if !strings.HasPrefix(arg, "-") {
|
||||
subcommand = arg
|
||||
break
|
||||
}
|
||||
}
|
||||
builtinSubcmdExist := false
|
||||
for _, subcmd := range foundCmd.Commands() {
|
||||
if subcmd.Name() == subcommand {
|
||||
builtinSubcmdExist = true
|
||||
break
|
||||
}
|
||||
// Command exists(e.g. kubectl create), but it is not certain that
|
||||
// subcommand also exists (e.g. kubectl create networkpolicy)
|
||||
// we also have to eliminate kubectl create -f
|
||||
if IsSubcommandPluginAllowed(foundCmd.Name()) && len(foundArgs) >= 1 && !strings.HasPrefix(foundArgs[0], "-") {
|
||||
subcommand := foundArgs[0]
|
||||
builtinSubcmdExist := false
|
||||
for _, subcmd := range foundCmd.Commands() {
|
||||
if subcmd.Name() == subcommand {
|
||||
builtinSubcmdExist = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !builtinSubcmdExist {
|
||||
if err := HandlePluginCommand(o.PluginHandler, cmdPathPieces, true); err != nil {
|
||||
fmt.Fprintf(o.IOStreams.ErrOut, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if !builtinSubcmdExist {
|
||||
if err := HandlePluginCommand(o.PluginHandler, cmdPathPieces, len(cmdPathPieces)-len(foundArgs)+1); err != nil {
|
||||
fmt.Fprintf(o.IOStreams.ErrOut, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -261,7 +256,7 @@ func (h *DefaultPluginHandler) Execute(executablePath string, cmdArgs, environme
|
|||
|
||||
// HandlePluginCommand receives a pluginHandler and command-line arguments and attempts to find
|
||||
// a plugin executable on the PATH that satisfies the given arguments.
|
||||
func HandlePluginCommand(pluginHandler PluginHandler, cmdArgs []string, exactMatch bool) error {
|
||||
func HandlePluginCommand(pluginHandler PluginHandler, cmdArgs []string, minArgs int) error {
|
||||
var remainingArgs []string // all "non-flag" arguments
|
||||
for _, arg := range cmdArgs {
|
||||
if strings.HasPrefix(arg, "-") {
|
||||
|
@ -281,13 +276,14 @@ func HandlePluginCommand(pluginHandler PluginHandler, cmdArgs []string, exactMat
|
|||
for len(remainingArgs) > 0 {
|
||||
path, found := pluginHandler.Lookup(strings.Join(remainingArgs, "-"))
|
||||
if !found {
|
||||
if exactMatch {
|
||||
// if exactMatch is true, we shouldn't continue searching with shorter names.
|
||||
remainingArgs = remainingArgs[:len(remainingArgs)-1]
|
||||
if len(remainingArgs) < minArgs {
|
||||
// we shouldn't continue searching with shorter names.
|
||||
// this is especially for not searching kubectl-create plugin
|
||||
// when kubectl-create-foo plugin is not found.
|
||||
break
|
||||
}
|
||||
remainingArgs = remainingArgs[:len(remainingArgs)-1]
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
|
@ -362,9 +358,14 @@ func NewKubectlCommand(o KubectlOptions) *cobra.Command {
|
|||
|
||||
flags.BoolVar(&warningsAsErrors, "warnings-as-errors", warningsAsErrors, "Treat warnings received from the server as errors and exit with a non-zero exit code")
|
||||
|
||||
pref := kuberc.NewPreferences()
|
||||
if !cmdutil.KubeRC.IsDisabled() {
|
||||
pref.AddFlags(flags)
|
||||
}
|
||||
|
||||
kubeConfigFlags := o.ConfigFlags
|
||||
if kubeConfigFlags == nil {
|
||||
kubeConfigFlags = defaultConfigFlags
|
||||
kubeConfigFlags = defaultConfigFlags().WithWarningPrinter(o.IOStreams)
|
||||
}
|
||||
kubeConfigFlags.AddFlags(flags)
|
||||
matchVersionKubeConfigFlags := cmdutil.NewMatchVersionFlags(kubeConfigFlags)
|
||||
|
@ -384,6 +385,8 @@ func NewKubectlCommand(o KubectlOptions) *cobra.Command {
|
|||
// Avoid import cycle by setting ValidArgsFunction here instead of in NewCmdGet()
|
||||
getCmd := get.NewCmdGet("kubectl", f, o.IOStreams)
|
||||
getCmd.ValidArgsFunction = utilcomp.ResourceTypeAndNameCompletionFunc(f)
|
||||
debugCmd := debug.NewCmdDebug(f, o.IOStreams)
|
||||
debugCmd.ValidArgsFunction = utilcomp.ResourceTypeAndNameCompletionFunc(f)
|
||||
|
||||
groups := templates.CommandGroups{
|
||||
{
|
||||
|
@ -435,7 +438,7 @@ func NewKubectlCommand(o KubectlOptions) *cobra.Command {
|
|||
proxyCmd,
|
||||
cp.NewCmdCp(f, o.IOStreams),
|
||||
auth.NewCmdAuth(f, o.IOStreams),
|
||||
debug.NewCmdDebug(f, o.IOStreams),
|
||||
debugCmd,
|
||||
events.NewCmdEvents(f, o.IOStreams),
|
||||
},
|
||||
},
|
||||
|
@ -469,13 +472,19 @@ func NewKubectlCommand(o KubectlOptions) *cobra.Command {
|
|||
filters = append(filters, alpha.Name())
|
||||
}
|
||||
|
||||
// Add plugin command group to the list of command groups.
|
||||
// The commands are only injected for the scope of showing help and completion, they are not
|
||||
// invoked directly.
|
||||
pluginCommandGroup := plugin.GetPluginCommandGroup(cmds)
|
||||
groups = append(groups, pluginCommandGroup)
|
||||
|
||||
templates.ActsAsRootCommand(cmds, filters, groups...)
|
||||
|
||||
utilcomp.SetFactoryForCompletion(f)
|
||||
registerCompletionFuncForGlobalFlags(cmds, f)
|
||||
|
||||
cmds.AddCommand(alpha)
|
||||
cmds.AddCommand(cmdconfig.NewCmdConfig(clientcmd.NewDefaultPathOptions(), o.IOStreams))
|
||||
cmds.AddCommand(cmdconfig.NewCmdConfig(f, clientcmd.NewDefaultPathOptions(), o.IOStreams))
|
||||
cmds.AddCommand(plugin.NewCmdPlugin(o.IOStreams))
|
||||
cmds.AddCommand(version.NewCmdVersion(f, o.IOStreams))
|
||||
cmds.AddCommand(apiresources.NewCmdAPIVersions(f, o.IOStreams))
|
||||
|
@ -485,6 +494,15 @@ func NewKubectlCommand(o KubectlOptions) *cobra.Command {
|
|||
// Stop warning about normalization of flags. That makes it possible to
|
||||
// add the klog flags later.
|
||||
cmds.SetGlobalNormalizationFunc(cliflag.WordSepNormalizeFunc)
|
||||
|
||||
if !cmdutil.KubeRC.IsDisabled() {
|
||||
_, err := pref.Apply(cmds, o.Arguments, o.IOStreams.ErrOut)
|
||||
if err != nil {
|
||||
fmt.Fprintf(o.IOStreams.ErrOut, "error occurred while applying preferences %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
return cmds
|
||||
}
|
||||
|
||||
|
@ -502,12 +520,9 @@ func NewKubectlCommand(o KubectlOptions) *cobra.Command {
|
|||
//
|
||||
// https://github.com/kubernetes/enhancements/tree/master/keps/sig-cli/859-kubectl-headers
|
||||
func addCmdHeaderHooks(cmds *cobra.Command, kubeConfigFlags *genericclioptions.ConfigFlags) {
|
||||
// If the feature gate env var is set to "false", then do no add kubectl command headers.
|
||||
if value, exists := os.LookupEnv(kubectlCmdHeaders); exists {
|
||||
if value == "false" || value == "0" {
|
||||
klog.V(5).Infoln("kubectl command headers turned off")
|
||||
return
|
||||
}
|
||||
if cmdutil.CmdHeaders.IsDisabled() {
|
||||
klog.V(5).Infoln("kubectl command headers turned off")
|
||||
return
|
||||
}
|
||||
klog.V(5).Infoln("kubectl command headers turned on")
|
||||
crt := &genericclioptions.CommandHeaderRoundTripper{}
|
||||
|
@ -561,3 +576,29 @@ func registerCompletionFuncForGlobalFlags(cmd *cobra.Command, f cmdutil.Factory)
|
|||
return utilcomp.ListUsersInConfig(toComplete), cobra.ShellCompDirectiveNoFileComp
|
||||
}))
|
||||
}
|
||||
|
||||
// GetLogVerbosity parses the provided command-line arguments to determine
|
||||
// the verbosity level for logging. Returns string representing the verbosity
|
||||
// level, or 0 if no verbosity flag is specified.
|
||||
func GetLogVerbosity(args []string) string {
|
||||
for i, arg := range args {
|
||||
if arg == "--" {
|
||||
// flags after "--" does not represent any flag of
|
||||
// the command. We should short cut the iteration in here.
|
||||
break
|
||||
}
|
||||
|
||||
if arg == "--v" || arg == "-v" {
|
||||
if i+1 < len(args) {
|
||||
return args[i+1]
|
||||
}
|
||||
} else if strings.Contains(arg, "--v=") || strings.Contains(arg, "-v=") {
|
||||
parg := strings.Split(arg, "=")
|
||||
if len(parg) > 1 && parg[1] != "" {
|
||||
return parg[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "0"
|
||||
}
|
||||
|
|
|
@ -29,7 +29,6 @@ import (
|
|||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/cli-runtime/pkg/genericiooptions"
|
||||
"k8s.io/kubectl/pkg/cmd/plugin"
|
||||
cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
)
|
||||
|
||||
|
@ -129,47 +128,45 @@ func TestKubectlSubcommandShadowPlugin(t *testing.T) {
|
|||
}
|
||||
|
||||
for _, test := range tests {
|
||||
cmdtesting.WithAlphaEnvs([]cmdutil.FeatureGate{cmdutil.CmdPluginAsSubcommand}, t, func(t *testing.T) {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
pluginsHandler := &testPluginHandler{
|
||||
pluginsDirectory: "plugin/testdata",
|
||||
validPrefixes: plugin.ValidPluginFilenamePrefixes,
|
||||
}
|
||||
ioStreams, _, _, _ := genericiooptions.NewTestIOStreams()
|
||||
root := NewDefaultKubectlCommandWithArgs(KubectlOptions{PluginHandler: pluginsHandler, Arguments: test.args, IOStreams: ioStreams})
|
||||
// original plugin handler (DefaultPluginHandler) is implemented by exec call so no additional actions are expected on the cobra command if we activate the plugin flow
|
||||
if !pluginsHandler.lookedup && !pluginsHandler.executed {
|
||||
// args must be set, otherwise Execute will use os.Args (args used for starting the test) and test.args would not be passed
|
||||
// to the command which might invoke only "kubectl" without any additional args and give false positives
|
||||
root.SetArgs(test.args[1:])
|
||||
// Important note! Incorrect command or command failing validation might just call os.Exit(1) which would interrupt execution of the test
|
||||
if err := root.Execute(); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
pluginsHandler := &testPluginHandler{
|
||||
pluginsDirectory: "plugin/testdata",
|
||||
validPrefixes: plugin.ValidPluginFilenamePrefixes,
|
||||
}
|
||||
ioStreams, _, _, _ := genericiooptions.NewTestIOStreams()
|
||||
root := NewDefaultKubectlCommandWithArgs(KubectlOptions{PluginHandler: pluginsHandler, Arguments: test.args, IOStreams: ioStreams})
|
||||
// original plugin handler (DefaultPluginHandler) is implemented by exec call so no additional actions are expected on the cobra command if we activate the plugin flow
|
||||
if !pluginsHandler.lookedup && !pluginsHandler.executed {
|
||||
// args must be set, otherwise Execute will use os.Args (args used for starting the test) and test.args would not be passed
|
||||
// to the command which might invoke only "kubectl" without any additional args and give false positives
|
||||
root.SetArgs(test.args[1:])
|
||||
// Important note! Incorrect command or command failing validation might just call os.Exit(1) which would interrupt execution of the test
|
||||
if err := root.Execute(); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if (pluginsHandler.lookupErr != nil && pluginsHandler.lookupErr.Error() != test.expectLookupError) ||
|
||||
(pluginsHandler.lookupErr == nil && len(test.expectLookupError) > 0) {
|
||||
t.Fatalf("unexpected error: expected %q to occur, but got %q", test.expectLookupError, pluginsHandler.lookupErr)
|
||||
}
|
||||
if (pluginsHandler.lookupErr != nil && pluginsHandler.lookupErr.Error() != test.expectLookupError) ||
|
||||
(pluginsHandler.lookupErr == nil && len(test.expectLookupError) > 0) {
|
||||
t.Fatalf("unexpected error: expected %q to occur, but got %q", test.expectLookupError, pluginsHandler.lookupErr)
|
||||
}
|
||||
|
||||
if pluginsHandler.lookedup && !pluginsHandler.executed && len(test.expectLookupError) == 0 {
|
||||
// we have to fail here, because we have found the plugin, but not executed the plugin, nor the command (this would normally result in an error: unknown command)
|
||||
t.Fatalf("expected plugin execution, but did not occur")
|
||||
}
|
||||
if pluginsHandler.lookedup && !pluginsHandler.executed && len(test.expectLookupError) == 0 {
|
||||
// we have to fail here, because we have found the plugin, but not executed the plugin, nor the command (this would normally result in an error: unknown command)
|
||||
t.Fatalf("expected plugin execution, but did not occur")
|
||||
}
|
||||
|
||||
if pluginsHandler.executedPlugin != test.expectPlugin {
|
||||
t.Fatalf("unexpected plugin execution: expected %q, got %q", test.expectPlugin, pluginsHandler.executedPlugin)
|
||||
}
|
||||
if pluginsHandler.executedPlugin != test.expectPlugin {
|
||||
t.Fatalf("unexpected plugin execution: expected %q, got %q", test.expectPlugin, pluginsHandler.executedPlugin)
|
||||
}
|
||||
|
||||
if pluginsHandler.executed && len(test.expectPlugin) == 0 {
|
||||
t.Fatalf("unexpected plugin execution: expected no plugin, got %q", pluginsHandler.executedPlugin)
|
||||
}
|
||||
if pluginsHandler.executed && len(test.expectPlugin) == 0 {
|
||||
t.Fatalf("unexpected plugin execution: expected no plugin, got %q", pluginsHandler.executedPlugin)
|
||||
}
|
||||
|
||||
if !cmp.Equal(pluginsHandler.withArgs, test.expectPluginArgs, cmpopts.EquateEmpty()) {
|
||||
t.Fatalf("unexpected plugin execution args: expected %q, got %q", test.expectPluginArgs, pluginsHandler.withArgs)
|
||||
}
|
||||
})
|
||||
if !cmp.Equal(pluginsHandler.withArgs, test.expectPluginArgs, cmpopts.EquateEmpty()) {
|
||||
t.Fatalf("unexpected plugin execution args: expected %q, got %q", test.expectPluginArgs, pluginsHandler.withArgs)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -200,6 +197,18 @@ func TestKubectlCommandHandlesPlugins(t *testing.T) {
|
|||
expectPlugin: "plugin/testdata/kubectl-foo",
|
||||
expectPluginArgs: []string{"--bar"},
|
||||
},
|
||||
{
|
||||
name: "test that a plugin executable is found based on command args with positional argument",
|
||||
args: []string{"kubectl", "foo", "positional", "--bar"},
|
||||
expectPlugin: "plugin/testdata/kubectl-foo",
|
||||
expectPluginArgs: []string{"positional", "--bar"},
|
||||
},
|
||||
{
|
||||
name: "test that an allowed subcommand plugin executable is found based on command args with positional argument",
|
||||
args: []string{"kubectl", "create", "foo", "positional", "--bar"},
|
||||
expectPlugin: "plugin/testdata/kubectl-create-foo",
|
||||
expectPluginArgs: []string{"positional", "--bar"},
|
||||
},
|
||||
{
|
||||
name: "test that a plugin does not execute over an existing command by the same name",
|
||||
args: []string{"kubectl", "version", "--client=true"},
|
||||
|
@ -334,6 +343,7 @@ func (h *testPluginHandler) Lookup(filename string) (string, bool) {
|
|||
for _, p := range plugins {
|
||||
filenameWithSuportedPrefix = fmt.Sprintf("%s-%s", prefix, filename)
|
||||
if p.Name() == filenameWithSuportedPrefix {
|
||||
h.lookupErr = nil
|
||||
return fmt.Sprintf("%s/%s", h.pluginsDirectory, p.Name()), true
|
||||
}
|
||||
}
|
||||
|
@ -372,10 +382,6 @@ func TestKubectlCommandHeadersHooks(t *testing.T) {
|
|||
envVar: "false",
|
||||
addsHooks: false,
|
||||
},
|
||||
"zero env var value; hooks NOT added": {
|
||||
envVar: "0",
|
||||
addsHooks: false,
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range tests {
|
||||
|
@ -385,7 +391,7 @@ func TestKubectlCommandHeadersHooks(t *testing.T) {
|
|||
if kubeConfigFlags.WrapConfigFn != nil {
|
||||
t.Fatal("expected initial nil WrapConfigFn")
|
||||
}
|
||||
t.Setenv(kubectlCmdHeaders, testCase.envVar)
|
||||
t.Setenv(string(cmdutil.CmdHeaders), testCase.envVar)
|
||||
addCmdHeaderHooks(cmds, kubeConfigFlags)
|
||||
// Valdidate whether the hooks were added.
|
||||
if testCase.addsHooks && kubeConfigFlags.WrapConfigFn == nil {
|
||||
|
|
|
@ -23,6 +23,7 @@ import (
|
|||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/cli-runtime/pkg/genericiooptions"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
|
@ -31,7 +32,7 @@ import (
|
|||
)
|
||||
|
||||
// NewCmdConfig creates a command object for the "config" action, and adds all child commands to it.
|
||||
func NewCmdConfig(pathOptions *clientcmd.PathOptions, streams genericiooptions.IOStreams) *cobra.Command {
|
||||
func NewCmdConfig(restClientGetter genericclioptions.RESTClientGetter, pathOptions *clientcmd.PathOptions, streams genericiooptions.IOStreams) *cobra.Command {
|
||||
if len(pathOptions.ExplicitFileFlag) == 0 {
|
||||
pathOptions.ExplicitFileFlag = clientcmd.RecommendedConfigPathFlag
|
||||
}
|
||||
|
@ -58,7 +59,7 @@ func NewCmdConfig(pathOptions *clientcmd.PathOptions, streams genericiooptions.I
|
|||
cmd.AddCommand(NewCmdConfigView(streams, pathOptions))
|
||||
cmd.AddCommand(NewCmdConfigSetCluster(streams.Out, pathOptions))
|
||||
cmd.AddCommand(NewCmdConfigSetCredentials(streams.Out, pathOptions))
|
||||
cmd.AddCommand(NewCmdConfigSetContext(streams.Out, pathOptions))
|
||||
cmd.AddCommand(NewCmdConfigSetContext(restClientGetter, streams.Out, pathOptions))
|
||||
cmd.AddCommand(NewCmdConfigSet(streams.Out, pathOptions))
|
||||
cmd.AddCommand(NewCmdConfigUnset(streams.Out, pathOptions))
|
||||
cmd.AddCommand(NewCmdConfigCurrentContext(streams.Out, pathOptions))
|
||||
|
|
|
@ -19,7 +19,7 @@ package config
|
|||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
@ -30,6 +30,7 @@ import (
|
|||
"k8s.io/client-go/tools/clientcmd"
|
||||
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
||||
utiltesting "k8s.io/client-go/util/testing"
|
||||
cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
)
|
||||
|
||||
|
@ -68,7 +69,6 @@ func Example_view() {
|
|||
// name: federal-context
|
||||
// current-context: federal-context
|
||||
// kind: Config
|
||||
// preferences: {}
|
||||
// users:
|
||||
// - name: red-user
|
||||
// user:
|
||||
|
@ -279,6 +279,91 @@ func TestEmbedClientCert(t *testing.T) {
|
|||
test.run(t)
|
||||
}
|
||||
|
||||
func TestExecPlugin(t *testing.T) {
|
||||
fakeCertFile, _ := os.CreateTemp(os.TempDir(), "")
|
||||
defer utiltesting.CloseAndRemove(t, fakeCertFile)
|
||||
fakeData := []byte("fake-data")
|
||||
err := os.WriteFile(fakeCertFile.Name(), fakeData, 0600)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error %v", err)
|
||||
}
|
||||
expectedConfig := newRedFederalCowHammerConfig()
|
||||
authInfo := clientcmdapi.NewAuthInfo()
|
||||
authInfo.Exec = &clientcmdapi.ExecConfig{
|
||||
Command: "example-client-go-exec-plugin",
|
||||
Args: []string{"arg1", "arg2"},
|
||||
Env: []clientcmdapi.ExecEnvVar{
|
||||
{
|
||||
Name: "FOO",
|
||||
Value: "bar",
|
||||
},
|
||||
},
|
||||
APIVersion: "client.authentication.k8s.io/v1",
|
||||
ProvideClusterInfo: false,
|
||||
InteractiveMode: "Never",
|
||||
}
|
||||
expectedConfig.AuthInfos["cred-exec-user"] = authInfo
|
||||
|
||||
test := configCommandTest{
|
||||
args: []string{
|
||||
"set-credentials",
|
||||
"cred-exec-user",
|
||||
"--exec-api-version=client.authentication.k8s.io/v1",
|
||||
"--exec-command=example-client-go-exec-plugin",
|
||||
"--exec-arg=arg1,arg2",
|
||||
"--exec-env=FOO=bar",
|
||||
"--exec-interactive-mode=Never",
|
||||
},
|
||||
startingConfig: newRedFederalCowHammerConfig(),
|
||||
expectedConfig: expectedConfig,
|
||||
}
|
||||
|
||||
test.run(t)
|
||||
}
|
||||
|
||||
func TestExecPluginWithProveClusterInfo(t *testing.T) {
|
||||
fakeCertFile, _ := os.CreateTemp(os.TempDir(), "")
|
||||
defer utiltesting.CloseAndRemove(t, fakeCertFile)
|
||||
fakeData := []byte("fake-data")
|
||||
err := os.WriteFile(fakeCertFile.Name(), fakeData, 0600)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error %v", err)
|
||||
}
|
||||
expectedConfig := newRedFederalCowHammerConfig()
|
||||
authInfo := clientcmdapi.NewAuthInfo()
|
||||
authInfo.Exec = &clientcmdapi.ExecConfig{
|
||||
Command: "example-client-go-exec-plugin",
|
||||
Args: []string{"arg1", "arg2"},
|
||||
Env: []clientcmdapi.ExecEnvVar{
|
||||
{
|
||||
Name: "FOO",
|
||||
Value: "bar",
|
||||
},
|
||||
},
|
||||
APIVersion: "client.authentication.k8s.io/v1",
|
||||
ProvideClusterInfo: true,
|
||||
InteractiveMode: "Always",
|
||||
}
|
||||
expectedConfig.AuthInfos["cred-exec-user"] = authInfo
|
||||
|
||||
test := configCommandTest{
|
||||
args: []string{
|
||||
"set-credentials",
|
||||
"cred-exec-user",
|
||||
"--exec-api-version=client.authentication.k8s.io/v1",
|
||||
"--exec-command=example-client-go-exec-plugin",
|
||||
"--exec-arg=arg1,arg2",
|
||||
"--exec-env=FOO=bar",
|
||||
"--exec-interactive-mode=Always",
|
||||
"--exec-provide-cluster-info=true",
|
||||
},
|
||||
startingConfig: newRedFederalCowHammerConfig(),
|
||||
expectedConfig: expectedConfig,
|
||||
}
|
||||
|
||||
test.run(t)
|
||||
}
|
||||
|
||||
func TestEmbedClientKey(t *testing.T) {
|
||||
fakeKeyFile, _ := os.CreateTemp(os.TempDir(), "")
|
||||
defer utiltesting.CloseAndRemove(t, fakeKeyFile)
|
||||
|
@ -330,7 +415,7 @@ func TestEmptyTokenAndCertAllowed(t *testing.T) {
|
|||
defer utiltesting.CloseAndRemove(t, fakeCertFile)
|
||||
expectedConfig := newRedFederalCowHammerConfig()
|
||||
authInfo := clientcmdapi.NewAuthInfo()
|
||||
authInfo.ClientCertificate = path.Base(fakeCertFile.Name())
|
||||
authInfo.ClientCertificate = filepath.Base(fakeCertFile.Name())
|
||||
expectedConfig.AuthInfos["another-user"] = authInfo
|
||||
|
||||
test := configCommandTest{
|
||||
|
@ -575,7 +660,7 @@ func TestCAClearsInsecure(t *testing.T) {
|
|||
clusterInfoWithInsecure.InsecureSkipTLSVerify = true
|
||||
|
||||
clusterInfoWithCA := clientcmdapi.NewCluster()
|
||||
clusterInfoWithCA.CertificateAuthority = path.Base(fakeCAFile.Name())
|
||||
clusterInfoWithCA.CertificateAuthority = filepath.Base(fakeCAFile.Name())
|
||||
|
||||
startingConfig := newRedFederalCowHammerConfig()
|
||||
startingConfig.Clusters["another-cluster"] = clusterInfoWithInsecure
|
||||
|
@ -863,8 +948,11 @@ func testConfigCommand(args []string, startingConfig clientcmdapi.Config, t *tes
|
|||
argsToUse = append(argsToUse, "--kubeconfig="+fakeKubeFile.Name())
|
||||
argsToUse = append(argsToUse, args...)
|
||||
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
defer tf.Cleanup()
|
||||
|
||||
streams, _, buf, _ := genericiooptions.NewTestIOStreams()
|
||||
cmd := NewCmdConfig(clientcmd.NewDefaultPathOptions(), streams)
|
||||
cmd := NewCmdConfig(tf, clientcmd.NewDefaultPathOptions(), streams)
|
||||
// "context" is a global flag, inherited from base kubectl command in the real world
|
||||
cmd.PersistentFlags().String("context", "", "The name of the kubeconfig context to use")
|
||||
cmd.SetArgs(argsToUse)
|
||||
|
|
|
@ -89,7 +89,7 @@ func NewCmdConfigGetContexts(streams genericiooptions.IOStreams, configAccess cl
|
|||
|
||||
// Complete assigns GetContextsOptions from the args.
|
||||
func (o *GetContextsOptions) Complete(cmd *cobra.Command, args []string) error {
|
||||
supportedOutputTypes := sets.NewString("", "name")
|
||||
supportedOutputTypes := sets.New[string]("", "name")
|
||||
if !supportedOutputTypes.Has(o.outputFormat) {
|
||||
return fmt.Errorf("--output %v is not available in kubectl config get-contexts; resetting to default output format", o.outputFormat)
|
||||
}
|
||||
|
|
|
@ -55,7 +55,7 @@ func newNavigationSteps(path string) (*navigationSteps, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nextPart := findNameStep(individualParts[currPartIndex:], sets.StringKeySet(mapValueOptions))
|
||||
nextPart := findNameStep(individualParts[currPartIndex:], sets.KeySet(mapValueOptions))
|
||||
|
||||
steps = append(steps, navigationStep{nextPart, mapValueType})
|
||||
currPartIndex += len(strings.Split(nextPart, "."))
|
||||
|
@ -98,7 +98,7 @@ func (s *navigationSteps) moreStepsRemaining() bool {
|
|||
|
||||
// findNameStep takes the list of parts and a set of valid tags that can be used after the name. It then walks the list of parts
|
||||
// until it find a valid "next" tag or until it reaches the end of the parts and then builds the name back up out of the individual parts
|
||||
func findNameStep(parts []string, typeOptions sets.String) string {
|
||||
func findNameStep(parts []string, typeOptions sets.Set[string]) string {
|
||||
if len(parts) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
@ -136,7 +136,7 @@ func getPotentialTypeValues(typeValue reflect.Type) (map[string]reflect.Type, er
|
|||
return ret, nil
|
||||
}
|
||||
|
||||
func findKnownValue(parts []string, valueOptions sets.String) int {
|
||||
func findKnownValue(parts []string, valueOptions sets.Set[string]) int {
|
||||
for i := range parts {
|
||||
if valueOptions.Has(parts[i]) {
|
||||
return i
|
||||
|
|
|
@ -23,6 +23,7 @@ import (
|
|||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
||||
cliflag "k8s.io/component-base/cli/flag"
|
||||
|
@ -53,7 +54,7 @@ var (
|
|||
)
|
||||
|
||||
// NewCmdConfigSetContext returns a Command instance for 'config set-context' sub command
|
||||
func NewCmdConfigSetContext(out io.Writer, configAccess clientcmd.ConfigAccess) *cobra.Command {
|
||||
func NewCmdConfigSetContext(restClientGetter genericclioptions.RESTClientGetter, out io.Writer, configAccess clientcmd.ConfigAccess) *cobra.Command {
|
||||
options := &setContextOptions{configAccess: configAccess}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
|
@ -79,6 +80,12 @@ func NewCmdConfigSetContext(out io.Writer, configAccess clientcmd.ConfigAccess)
|
|||
cmd.Flags().Var(&options.cluster, clientcmd.FlagClusterName, clientcmd.FlagClusterName+" for the context entry in kubeconfig")
|
||||
cmd.Flags().Var(&options.authInfo, clientcmd.FlagAuthInfoName, clientcmd.FlagAuthInfoName+" for the context entry in kubeconfig")
|
||||
cmd.Flags().Var(&options.namespace, clientcmd.FlagNamespace, clientcmd.FlagNamespace+" for the context entry in kubeconfig")
|
||||
cmdutil.CheckErr(cmd.RegisterFlagCompletionFunc(
|
||||
"namespace",
|
||||
func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
return completion.CompGetResource(cmdutil.NewFactory(restClientGetter), "namespace", toComplete), cobra.ShellCompDirectiveNoFileComp
|
||||
},
|
||||
))
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
|
|
@ -21,10 +21,10 @@ import (
|
|||
"os"
|
||||
"testing"
|
||||
|
||||
utiltesting "k8s.io/client-go/util/testing"
|
||||
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
||||
utiltesting "k8s.io/client-go/util/testing"
|
||||
cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
|
||||
)
|
||||
|
||||
type setContextTest struct {
|
||||
|
@ -122,7 +122,11 @@ func (test setContextTest) run(t *testing.T) {
|
|||
pathOptions.GlobalFile = fakeKubeFile.Name()
|
||||
pathOptions.EnvVar = ""
|
||||
buf := bytes.NewBuffer([]byte{})
|
||||
cmd := NewCmdConfigSetContext(buf, pathOptions)
|
||||
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
defer tf.Cleanup()
|
||||
|
||||
cmd := NewCmdConfigSetContext(tf, buf, pathOptions)
|
||||
cmd.SetArgs(test.args)
|
||||
cmd.Flags().Parse(test.flags)
|
||||
if err := cmd.Execute(); err != nil {
|
||||
|
|
|
@ -48,21 +48,25 @@ type setCredentialsOptions struct {
|
|||
authProviderArgs map[string]string
|
||||
authProviderArgsToRemove []string
|
||||
|
||||
execCommand cliflag.StringFlag
|
||||
execAPIVersion cliflag.StringFlag
|
||||
execArgs []string
|
||||
execEnv map[string]string
|
||||
execEnvToRemove []string
|
||||
execCommand cliflag.StringFlag
|
||||
execAPIVersion cliflag.StringFlag
|
||||
execInteractiveMode cliflag.StringFlag
|
||||
execProvideClusterInfo cliflag.Tristate
|
||||
execArgs []string
|
||||
execEnv map[string]string
|
||||
execEnvToRemove []string
|
||||
}
|
||||
|
||||
const (
|
||||
flagAuthProvider = "auth-provider"
|
||||
flagAuthProviderArg = "auth-provider-arg"
|
||||
|
||||
flagExecCommand = "exec-command"
|
||||
flagExecAPIVersion = "exec-api-version"
|
||||
flagExecArg = "exec-arg"
|
||||
flagExecEnv = "exec-env"
|
||||
flagExecCommand = "exec-command"
|
||||
flagExecAPIVersion = "exec-api-version"
|
||||
flagExecArg = "exec-arg"
|
||||
flagExecEnv = "exec-env"
|
||||
flagExecInteractiveMode = "exec-interactive-mode"
|
||||
flagExecProvideClusterInfo = "exec-provide-cluster-info"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -105,6 +109,9 @@ var (
|
|||
# Enable new exec auth plugin for the "cluster-admin" entry
|
||||
kubectl config set-credentials cluster-admin --exec-command=/path/to/the/executable --exec-api-version=client.authentication.k8s.io/v1beta1
|
||||
|
||||
# Enable new exec auth plugin for the "cluster-admin" entry with interactive mode
|
||||
kubectl config set-credentials cluster-admin --exec-command=/path/to/the/executable --exec-api-version=client.authentication.k8s.io/v1beta1 --exec-interactive-mode=Never
|
||||
|
||||
# Define new exec auth plugin arguments for the "cluster-admin" entry
|
||||
kubectl config set-credentials cluster-admin --exec-arg=arg1 --exec-arg=arg2
|
||||
|
||||
|
@ -179,6 +186,9 @@ func newCmdConfigSetCredentials(out io.Writer, options *setCredentialsOptions) *
|
|||
cmd.Flags().StringSlice(flagAuthProviderArg, nil, "'key=value' arguments for the auth provider")
|
||||
cmd.Flags().Var(&options.execCommand, flagExecCommand, "Command for the exec credential plugin for the user entry in kubeconfig")
|
||||
cmd.Flags().Var(&options.execAPIVersion, flagExecAPIVersion, "API version of the exec credential plugin for the user entry in kubeconfig")
|
||||
cmd.Flags().Var(&options.execInteractiveMode, flagExecInteractiveMode, "InteractiveMode of the exec credentials plugin for the user entry in kubeconfig")
|
||||
flagClusterInfo := cmd.Flags().VarPF(&options.execProvideClusterInfo, flagExecProvideClusterInfo, "", "ProvideClusterInfo of the exec credentials plugin for the user entry in kubeconfig")
|
||||
flagClusterInfo.NoOptDefVal = "true"
|
||||
cmd.Flags().StringSlice(flagExecArg, nil, "New arguments for the exec credential plugin command for the user entry in kubeconfig")
|
||||
cmd.Flags().StringArray(flagExecEnv, nil, "'key=value' environment values for the exec credential plugin")
|
||||
f := cmd.Flags().VarPF(&options.embedCertData, clientcmd.FlagEmbedCerts, "", "Embed client cert/key for the user entry in kubeconfig")
|
||||
|
@ -306,6 +316,14 @@ func (o *setCredentialsOptions) modifyAuthInfo(existingAuthInfo clientcmdapi.Aut
|
|||
modifiedAuthInfo.Exec.Args = o.execArgs
|
||||
}
|
||||
|
||||
if o.execInteractiveMode.Provided() {
|
||||
modifiedAuthInfo.Exec.InteractiveMode = clientcmdapi.ExecInteractiveMode(o.execInteractiveMode.Value())
|
||||
}
|
||||
|
||||
if o.execProvideClusterInfo.Provided() {
|
||||
modifiedAuthInfo.Exec.ProvideClusterInfo = o.execProvideClusterInfo.Value()
|
||||
}
|
||||
|
||||
// iterate over the existing exec env values and remove the specified
|
||||
if o.execEnvToRemove != nil {
|
||||
newExecEnv := []clientcmdapi.ExecEnvVar{}
|
||||
|
@ -437,5 +455,14 @@ func (o setCredentialsOptions) validate() error {
|
|||
}
|
||||
}
|
||||
|
||||
if o.execInteractiveMode.Provided() {
|
||||
interactiveMode := o.execInteractiveMode.Value()
|
||||
if interactiveMode != string(clientcmdapi.IfAvailableExecInteractiveMode) &&
|
||||
interactiveMode != string(clientcmdapi.AlwaysExecInteractiveMode) &&
|
||||
interactiveMode != string(clientcmdapi.NeverExecInteractiveMode) {
|
||||
return fmt.Errorf("invalid interactive mode type, can be only IfAvailable, Never, Always")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -85,7 +85,6 @@ contexts:
|
|||
name: my-cluster
|
||||
current-context: minikube
|
||||
kind: Config
|
||||
preferences: {}
|
||||
users:
|
||||
- name: minikube
|
||||
user:
|
||||
|
@ -165,7 +164,6 @@ contexts:
|
|||
name: my-cluster
|
||||
current-context: minikube
|
||||
kind: Config
|
||||
preferences: {}
|
||||
users:
|
||||
- name: minikube
|
||||
user:
|
||||
|
@ -247,7 +245,6 @@ contexts:
|
|||
name: minikube
|
||||
current-context: minikube
|
||||
kind: Config
|
||||
preferences: {}
|
||||
users:
|
||||
- name: minikube
|
||||
user:
|
||||
|
@ -272,7 +269,6 @@ contexts:
|
|||
name: my-cluster
|
||||
current-context: my-cluster
|
||||
kind: Config
|
||||
preferences: {}
|
||||
users:
|
||||
- name: mu-cluster
|
||||
user:
|
||||
|
|
|
@ -18,12 +18,13 @@ package cp
|
|||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
|
@ -267,8 +268,8 @@ func (o *CopyOptions) checkDestinationIsDir(dest fileSpec) error {
|
|||
options := &exec.ExecOptions{
|
||||
StreamOptions: exec.StreamOptions{
|
||||
IOStreams: genericiooptions.IOStreams{
|
||||
Out: bytes.NewBuffer([]byte{}),
|
||||
ErrOut: bytes.NewBuffer([]byte{}),
|
||||
Out: io.Discard,
|
||||
ErrOut: io.Discard,
|
||||
},
|
||||
|
||||
Namespace: dest.PodNamespace,
|
||||
|
@ -279,7 +280,21 @@ func (o *CopyOptions) checkDestinationIsDir(dest fileSpec) error {
|
|||
Executor: &exec.DefaultRemoteExecutor{},
|
||||
}
|
||||
|
||||
return o.execute(options)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
done := make(chan error)
|
||||
|
||||
go func() {
|
||||
done <- o.execute(options)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case err := <-done:
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func (o *CopyOptions) copyToPod(src, dest fileSpec, options *exec.ExecOptions) error {
|
||||
|
@ -295,6 +310,10 @@ func (o *CopyOptions) copyToPod(src, dest fileSpec, options *exec.ExecOptions) e
|
|||
// If no error, dest.File was found to be a directory.
|
||||
// Copy specified src into it
|
||||
destFile = destFile.Join(srcFile.Base())
|
||||
} else if errors.Is(err, context.DeadlineExceeded) {
|
||||
// we haven't decided destination is directory or not because context timeout is exceeded.
|
||||
// That's why, we should shortcut the process in here.
|
||||
return err
|
||||
}
|
||||
|
||||
go func(src localPath, dest remotePath, writer io.WriteCloser) {
|
||||
|
@ -568,8 +587,5 @@ func (o *CopyOptions) execute(options *exec.ExecOptions) error {
|
|||
return err
|
||||
}
|
||||
|
||||
if err := options.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return options.Run()
|
||||
}
|
||||
|
|
|
@ -988,6 +988,6 @@ func cmpFileData(t *testing.T, filePath, data string) {
|
|||
type testWriter testing.T
|
||||
|
||||
func (t *testWriter) Write(p []byte) (n int, err error) {
|
||||
t.Logf(string(p))
|
||||
t.Log(string(p))
|
||||
return len(p), nil
|
||||
}
|
||||
|
|
|
@ -109,12 +109,6 @@ func NewCmdCreate(f cmdutil.Factory, ioStreams genericiooptions.IOStreams) *cobr
|
|||
Long: createLong,
|
||||
Example: createExample,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if cmdutil.IsFilenameSliceEmpty(o.FilenameOptions.Filenames, o.FilenameOptions.Kustomize) {
|
||||
ioStreams.ErrOut.Write([]byte("Error: must specify one of -f and -k\n\n"))
|
||||
defaultRunFunc := cmdutil.DefaultSubCommandRun(ioStreams.ErrOut)
|
||||
defaultRunFunc(cmd, args)
|
||||
return
|
||||
}
|
||||
cmdutil.CheckErr(o.Complete(f, cmd, args))
|
||||
cmdutil.CheckErr(o.Validate())
|
||||
cmdutil.CheckErr(o.RunCreate(f, cmd))
|
||||
|
@ -159,8 +153,12 @@ func NewCmdCreate(f cmdutil.Factory, ioStreams genericiooptions.IOStreams) *cobr
|
|||
return cmd
|
||||
}
|
||||
|
||||
// Validate makes sure there is no discrepency in command options
|
||||
// Validate makes sure there is no discrepancy in command options
|
||||
func (o *CreateOptions) Validate() error {
|
||||
if err := o.FilenameOptions.RequireFilenameOrKustomize(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(o.Raw) > 0 {
|
||||
if o.EditBeforeCreate {
|
||||
return fmt.Errorf("--raw and --edit are mutually exclusive")
|
||||
|
|
|
@ -20,7 +20,7 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
|
@ -320,7 +320,7 @@ func handleConfigMapFromFileSources(configMap *corev1.ConfigMap, fileSources []s
|
|||
return fmt.Errorf("error listing files in %s: %v", filePath, err)
|
||||
}
|
||||
for _, item := range fileList {
|
||||
itemPath := path.Join(filePath, item.Name())
|
||||
itemPath := filepath.Join(filePath, item.Name())
|
||||
if item.Type().IsRegular() {
|
||||
keyName = item.Name()
|
||||
err = addKeyFromFileToConfigMap(configMap, keyName, itemPath)
|
||||
|
|
|
@ -53,7 +53,10 @@ var (
|
|||
kubectl create deployment my-dep --image=nginx --replicas=3
|
||||
|
||||
# Create a deployment named my-dep that runs the busybox image and expose port 5701
|
||||
kubectl create deployment my-dep --image=busybox --port=5701`))
|
||||
kubectl create deployment my-dep --image=busybox --port=5701
|
||||
|
||||
# Create a deployment named my-dep that runs multiple containers
|
||||
kubectl create deployment my-dep --image=busybox:latest --image=ubuntu:latest --image=nginx`))
|
||||
)
|
||||
|
||||
// CreateDeploymentOptions is returned by NewCmdCreateDeployment
|
||||
|
@ -112,9 +115,9 @@ func NewCmdCreateDeployment(f cmdutil.Factory, ioStreams genericiooptions.IOStre
|
|||
cmdutil.AddApplyAnnotationFlags(cmd)
|
||||
cmdutil.AddValidateFlags(cmd)
|
||||
cmdutil.AddDryRunFlag(cmd)
|
||||
cmd.Flags().StringSliceVar(&o.Images, "image", o.Images, "Image names to run.")
|
||||
cmd.Flags().StringSliceVar(&o.Images, "image", o.Images, "Image names to run. A deployment can have multiple images set for multi-container pod.")
|
||||
cmd.MarkFlagRequired("image")
|
||||
cmd.Flags().Int32Var(&o.Port, "port", o.Port, "The port that this container exposes.")
|
||||
cmd.Flags().Int32Var(&o.Port, "port", o.Port, "The containerPort that this deployment exposes.")
|
||||
cmd.Flags().Int32VarP(&o.Replicas, "replicas", "r", o.Replicas, "Number of replicas to create. Default is 1.")
|
||||
cmdutil.AddFieldManagerFlagVar(cmd, &o.FieldManager, "kubectl-create")
|
||||
|
||||
|
|
|
@ -35,6 +35,7 @@ import (
|
|||
"k8s.io/kubectl/pkg/util"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
"k8s.io/utils/ptr"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -261,10 +262,21 @@ func (o *CreateJobOptions) createJobFromCronJob(cronJob *batchv1.CronJob) *batch
|
|||
// this is ok because we know exactly how we want to be serialized
|
||||
TypeMeta: metav1.TypeMeta{APIVersion: batchv1.SchemeGroupVersion.String(), Kind: "Job"},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: o.Name,
|
||||
Annotations: annotations,
|
||||
Labels: cronJob.Spec.JobTemplate.Labels,
|
||||
OwnerReferences: []metav1.OwnerReference{*metav1.NewControllerRef(cronJob, batchv1.SchemeGroupVersion.WithKind("CronJob"))},
|
||||
Name: o.Name,
|
||||
Annotations: annotations,
|
||||
Labels: cronJob.Spec.JobTemplate.Labels,
|
||||
OwnerReferences: []metav1.OwnerReference{
|
||||
{
|
||||
// we are not using metav1.NewControllerRef because it
|
||||
// sets BlockOwnerDeletion to true which additionally mandates
|
||||
// cronjobs/finalizer role and not backwards-compatible.
|
||||
APIVersion: batchv1.SchemeGroupVersion.String(),
|
||||
Kind: "CronJob",
|
||||
Name: cronJob.GetName(),
|
||||
UID: cronJob.GetUID(),
|
||||
Controller: ptr.To(true),
|
||||
},
|
||||
},
|
||||
},
|
||||
Spec: cronJob.Spec.JobTemplate.Spec,
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ import (
|
|||
corev1 "k8s.io/api/core/v1"
|
||||
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/utils/ptr"
|
||||
)
|
||||
|
||||
func TestCreateJobValidation(t *testing.T) {
|
||||
|
@ -161,9 +162,17 @@ func TestCreateJobFromCronJob(t *testing.T) {
|
|||
expected: &batchv1.Job{
|
||||
TypeMeta: metav1.TypeMeta{APIVersion: batchv1.SchemeGroupVersion.String(), Kind: "Job"},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: jobName,
|
||||
Annotations: map[string]string{"cronjob.kubernetes.io/instantiate": "manual"},
|
||||
OwnerReferences: []metav1.OwnerReference{*metav1.NewControllerRef(cronJob, batchv1.SchemeGroupVersion.WithKind("CronJob"))},
|
||||
Name: jobName,
|
||||
Annotations: map[string]string{"cronjob.kubernetes.io/instantiate": "manual"},
|
||||
OwnerReferences: []metav1.OwnerReference{
|
||||
{
|
||||
APIVersion: batchv1.SchemeGroupVersion.String(),
|
||||
Kind: "CronJob",
|
||||
Name: cronJob.GetName(),
|
||||
UID: cronJob.GetUID(),
|
||||
Controller: ptr.To(true),
|
||||
},
|
||||
},
|
||||
},
|
||||
Spec: batchv1.JobSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
|
|
|
@ -116,7 +116,7 @@ var (
|
|||
func AddSpecialVerb(verb string, gr schema.GroupResource) {
|
||||
resources, ok := specialVerbs[verb]
|
||||
if !ok {
|
||||
resources = make([]schema.GroupResource, 1)
|
||||
resources = make([]schema.GroupResource, 0, 1)
|
||||
}
|
||||
resources = append(resources, gr)
|
||||
specialVerbs[verb] = resources
|
||||
|
@ -425,7 +425,7 @@ func generateResourcePolicyRules(mapper meta.RESTMapper, verbs []string, resourc
|
|||
|
||||
// Create separate rule for each of the api group.
|
||||
rules := []rbacv1.PolicyRule{}
|
||||
for _, g := range sets.StringKeySet(groupResourceMapping).List() {
|
||||
for _, g := range sets.List(sets.KeySet(groupResourceMapping)) {
|
||||
rule := rbacv1.PolicyRule{}
|
||||
rule.Verbs = verbs
|
||||
rule.Resources = groupResourceMapping[g]
|
||||
|
|
|
@ -684,14 +684,17 @@ func TestAddSpecialVerb(t *testing.T) {
|
|||
testCases := map[string]struct {
|
||||
verb string
|
||||
resource schema.GroupResource
|
||||
isNew bool
|
||||
}{
|
||||
"existing verb": {
|
||||
verb: "use",
|
||||
resource: schema.GroupResource{Group: "my.custom.io", Resource: "one"},
|
||||
isNew: false,
|
||||
},
|
||||
"new verb": {
|
||||
verb: "new",
|
||||
resource: schema.GroupResource{Group: "my.custom.io", Resource: "two"},
|
||||
isNew: true,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -703,6 +706,16 @@ func TestAddSpecialVerb(t *testing.T) {
|
|||
t.Errorf("missing expected verb: %s", tc.verb)
|
||||
}
|
||||
|
||||
if tc.isNew {
|
||||
if len(resources) != 1 {
|
||||
t.Errorf("new verb should only contain one resource resources:%#v", resources)
|
||||
}
|
||||
if !reflect.DeepEqual(tc.resource, resources[0]) {
|
||||
t.Errorf("miss expected resource:%#v", tc.resource)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
for _, res := range resources {
|
||||
if reflect.DeepEqual(tc.resource, res) {
|
||||
return
|
||||
|
|
|
@ -44,7 +44,7 @@ var (
|
|||
# Create a role binding for user1, user2, and group1 using the admin cluster role
|
||||
kubectl create rolebinding admin --clusterrole=admin --user=user1 --user=user2 --group=group1
|
||||
|
||||
# Create a role binding for serviceaccount monitoring:sa-dev using the admin role
|
||||
# Create a role binding for service account monitoring:sa-dev using the admin role
|
||||
kubectl create rolebinding admin-binding --role=admin --serviceaccount=monitoring:sa-dev`))
|
||||
)
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
@ -349,7 +349,7 @@ func handleSecretFromFileSources(secret *corev1.Secret, fileSources []string) er
|
|||
return fmt.Errorf("error listing files in %s: %v", filePath, err)
|
||||
}
|
||||
for _, item := range fileList {
|
||||
itemPath := path.Join(filePath, item.Name())
|
||||
itemPath := filepath.Join(filePath, item.Name())
|
||||
if item.Type().IsRegular() {
|
||||
keyName = item.Name()
|
||||
if err := addKeyFromFileToSecret(secret, keyName, itemPath); err != nil {
|
||||
|
|
|
@ -21,6 +21,7 @@ import (
|
|||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
|
@ -58,7 +59,7 @@ var (
|
|||
kubectl create secret docker-registry my-secret --docker-server=DOCKER_REGISTRY_SERVER --docker-username=DOCKER_USER --docker-password=DOCKER_PASSWORD --docker-email=DOCKER_EMAIL
|
||||
|
||||
# Create a new secret named my-secret from ~/.docker/config.json
|
||||
kubectl create secret docker-registry my-secret --from-file=.dockerconfigjson=path/to/.docker/config.json`))
|
||||
kubectl create secret docker-registry my-secret --from-file=path/to/.docker/config.json`))
|
||||
)
|
||||
|
||||
// DockerConfigJSON represents a local docker auth config file
|
||||
|
@ -152,7 +153,11 @@ func NewCmdCreateSecretDockerRegistry(f cmdutil.Factory, ioStreams genericioopti
|
|||
cmd.Flags().StringVar(&o.Email, "docker-email", o.Email, i18n.T("Email for Docker registry"))
|
||||
cmd.Flags().StringVar(&o.Server, "docker-server", o.Server, i18n.T("Server location for Docker registry"))
|
||||
cmd.Flags().BoolVar(&o.AppendHash, "append-hash", o.AppendHash, "Append a hash of the secret to its name.")
|
||||
cmd.Flags().StringSliceVar(&o.FileSources, "from-file", o.FileSources, "Key files can be specified using their file path, in which case a default name will be given to them, or optionally with a name and file path, in which case the given name will be used. Specifying a directory will iterate each named file in the directory that is a valid secret key.")
|
||||
cmd.Flags().StringSliceVar(&o.FileSources, "from-file", o.FileSources, "Key files can be specified using their file path, "+
|
||||
"in which case a default name of "+corev1.DockerConfigJsonKey+" will be given to them, "+
|
||||
"or optionally with a name and file path, in which case the given name will be used. "+
|
||||
"Specifying a directory will iterate each named file in the directory that is a valid secret key. "+
|
||||
"For this command, the key should always be "+corev1.DockerConfigJsonKey+".")
|
||||
|
||||
cmdutil.AddFieldManagerFlagVar(cmd, &o.FieldManager, "kubectl-create")
|
||||
|
||||
|
@ -204,6 +209,11 @@ func (o *CreateSecretDockerRegistryOptions) Complete(f cmdutil.Factory, cmd *cob
|
|||
return err
|
||||
}
|
||||
|
||||
for i := range o.FileSources {
|
||||
if !strings.Contains(o.FileSources[i], "=") {
|
||||
o.FileSources[i] = corev1.DockerConfigJsonKey + "=" + o.FileSources[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -17,11 +17,16 @@ limitations under the License.
|
|||
package create
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/cli-runtime/pkg/genericiooptions"
|
||||
cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
|
||||
)
|
||||
|
||||
func TestCreateSecretDockerRegistry(t *testing.T) {
|
||||
|
@ -183,3 +188,80 @@ func TestCreateSecretDockerRegistry(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateSecretDockerRegistryFromFile(t *testing.T) {
|
||||
username, password, email, server := "test-user", "test-password", "test-user@example.org", "https://index.docker.io/v1/"
|
||||
secretData, err := handleDockerCfgJSONContent(username, password, email, server)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
secret := &corev1.Secret{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: corev1.SchemeGroupVersion.String(),
|
||||
Kind: "Secret",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "foo",
|
||||
},
|
||||
Type: corev1.SecretTypeDockerConfigJson,
|
||||
Data: map[string][]byte{
|
||||
corev1.DockerConfigJsonKey: secretData,
|
||||
},
|
||||
}
|
||||
|
||||
tests := map[string]struct {
|
||||
withKey bool
|
||||
expected *corev1.Secret
|
||||
}{
|
||||
"create_secret_docker_registry_from_file_with_keyname": {
|
||||
withKey: true,
|
||||
expected: secret,
|
||||
},
|
||||
"create_secret_docker_registry_from_file_without_keyname": {
|
||||
withKey: false,
|
||||
expected: secret,
|
||||
},
|
||||
}
|
||||
|
||||
// Run all the tests
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
tmp, _ := os.MkdirTemp("", "input")
|
||||
defer func() {
|
||||
err := os.RemoveAll(tmp)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to teardown: %s", err)
|
||||
}
|
||||
}()
|
||||
dockerCfgFile := tmp + "/dockerconfig.json"
|
||||
err := os.WriteFile(dockerCfgFile, secretData, 0644)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
tf := cmdtesting.NewTestFactory()
|
||||
defer tf.Cleanup()
|
||||
ioStreams, _, out, _ := genericiooptions.NewTestIOStreams()
|
||||
cmd := NewCmdCreateSecretDockerRegistry(tf, ioStreams)
|
||||
args := []string{"foo", "--dry-run=client", "-ojson"}
|
||||
if test.withKey {
|
||||
args = append(args, fmt.Sprintf("--from-file=%s=%s", corev1.DockerConfigJsonKey, dockerCfgFile))
|
||||
} else {
|
||||
args = append(args, fmt.Sprintf("--from-file=%s", dockerCfgFile))
|
||||
}
|
||||
cmd.SetArgs(args)
|
||||
err = cmd.Execute()
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
got := &corev1.Secret{}
|
||||
err = json.Unmarshal(out.Bytes(), got)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
if !apiequality.Semantic.DeepEqual(got, test.expected) {
|
||||
t.Errorf("test %s\n expected:\n%#v\ngot:\n%#v", name, test.expected, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,7 +46,7 @@ var (
|
|||
|
||||
secretForTLSExample = templates.Examples(i18n.T(`
|
||||
# Create a new TLS secret named tls-secret with the given key pair
|
||||
kubectl create secret tls tls-secret --cert=path/to/tls.cert --key=path/to/tls.key`))
|
||||
kubectl create secret tls tls-secret --cert=path/to/tls.crt --key=path/to/tls.key`))
|
||||
)
|
||||
|
||||
// CreateSecretTLSOptions holds the options for 'create secret tls' sub command
|
||||
|
|
|
@ -18,7 +18,7 @@ package create
|
|||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
|
@ -197,8 +197,8 @@ func write(path, contents string, t *testing.T) {
|
|||
}
|
||||
|
||||
func writeKeyPair(tmpDirPath, key, cert string, t *testing.T) (keyPath, certPath string) {
|
||||
keyPath = path.Join(tmpDirPath, "tls.key")
|
||||
certPath = path.Join(tmpDirPath, "tls.cert")
|
||||
keyPath = filepath.Join(tmpDirPath, "tls.key")
|
||||
certPath = filepath.Join(tmpDirPath, "tls.cert")
|
||||
write(keyPath, key, t)
|
||||
write(certPath, cert, t)
|
||||
return
|
||||
|
|
|
@ -18,6 +18,7 @@ package create
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
@ -399,12 +400,12 @@ func parsePorts(portString string) (int32, intstr.IntOrString, error) {
|
|||
var targetPort intstr.IntOrString
|
||||
if portNum, err := strconv.Atoi(portStringSlice[1]); err != nil {
|
||||
if errs := validation.IsValidPortName(portStringSlice[1]); len(errs) != 0 {
|
||||
return 0, intstr.FromInt32(0), fmt.Errorf(strings.Join(errs, ","))
|
||||
return 0, intstr.FromInt32(0), errors.New(strings.Join(errs, ","))
|
||||
}
|
||||
targetPort = intstr.FromString(portStringSlice[1])
|
||||
} else {
|
||||
if errs := validation.IsValidPortNum(portNum); len(errs) != 0 {
|
||||
return 0, intstr.FromInt32(0), fmt.Errorf(strings.Join(errs, ","))
|
||||
return 0, intstr.FromInt32(0), errors.New(strings.Join(errs, ","))
|
||||
}
|
||||
targetPort = intstr.FromInt32(int32(portNum))
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ import (
|
|||
"k8s.io/cli-runtime/pkg/resource"
|
||||
"k8s.io/client-go/rest/fake"
|
||||
cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/scheme"
|
||||
)
|
||||
|
||||
|
@ -150,3 +151,35 @@ func TestCreateDirectory(t *testing.T) {
|
|||
t.Errorf("unexpected output: %s", buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestMissingFilenameError(t *testing.T) {
|
||||
var errStr string
|
||||
var exitCode int
|
||||
cmdutil.BehaviorOnFatal(func(str string, code int) {
|
||||
if errStr == "" {
|
||||
errStr = str
|
||||
exitCode = code
|
||||
}
|
||||
})
|
||||
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
defer tf.Cleanup()
|
||||
|
||||
ioStreams, _, buf, _ := genericiooptions.NewTestIOStreams()
|
||||
cmd := NewCmdCreate(tf, ioStreams)
|
||||
cmd.Run(cmd, []string{})
|
||||
|
||||
if buf.Len() > 0 {
|
||||
t.Errorf("unexpected output: %s", buf.String())
|
||||
}
|
||||
|
||||
if len(errStr) == 0 {
|
||||
t.Errorf("unexpected non-error")
|
||||
} else if errStr != "error: must specify one of -f and -k" {
|
||||
t.Errorf("unexpected error: %s", errStr)
|
||||
}
|
||||
|
||||
if exitCode != 1 {
|
||||
t.Errorf("unexpected exit code: %d", exitCode)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
authenticationv1 "k8s.io/api/authentication/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
@ -37,7 +38,7 @@ import (
|
|||
"k8s.io/kubectl/pkg/util/completion"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
"k8s.io/kubectl/pkg/util/term"
|
||||
"k8s.io/utils/pointer"
|
||||
"k8s.io/utils/ptr"
|
||||
)
|
||||
|
||||
// TokenOptions is the data required to perform a token request operation.
|
||||
|
@ -46,6 +47,9 @@ type TokenOptions struct {
|
|||
PrintFlags *genericclioptions.PrintFlags
|
||||
PrintObj func(obj runtime.Object) error
|
||||
|
||||
// Flags hold the parsed CLI flags.
|
||||
Flags *pflag.FlagSet
|
||||
|
||||
// Name and namespace of service account to create a token for
|
||||
Name string
|
||||
Namespace string
|
||||
|
@ -92,13 +96,14 @@ var (
|
|||
# Request a token bound to an instance of a Secret object with a specific UID
|
||||
kubectl create token myapp --bound-object-kind Secret --bound-object-name mysecret --bound-object-uid 0d4691ed-659b-4935-a832-355f77ee47cc
|
||||
`)
|
||||
|
||||
boundObjectKindToAPIVersion = map[string]string{
|
||||
"Pod": "v1",
|
||||
"Secret": "v1",
|
||||
}
|
||||
)
|
||||
|
||||
var boundObjectKinds = map[string]string{
|
||||
"Pod": "v1",
|
||||
"Secret": "v1",
|
||||
"Node": "v1",
|
||||
}
|
||||
|
||||
func NewTokenOpts(ioStreams genericiooptions.IOStreams) *TokenOptions {
|
||||
return &TokenOptions{
|
||||
PrintFlags: genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme),
|
||||
|
@ -137,10 +142,10 @@ func NewCmdCreateToken(f cmdutil.Factory, ioStreams genericiooptions.IOStreams)
|
|||
|
||||
cmd.Flags().StringArrayVar(&o.Audiences, "audience", o.Audiences, "Audience of the requested token. If unset, defaults to requesting a token for use with the Kubernetes API server. May be repeated to request a token valid for multiple audiences.")
|
||||
|
||||
cmd.Flags().DurationVar(&o.Duration, "duration", o.Duration, "Requested lifetime of the issued token. The server may return a token with a longer or shorter lifetime.")
|
||||
cmd.Flags().DurationVar(&o.Duration, "duration", o.Duration, "Requested lifetime of the issued token. If not set or if set to 0, the lifetime will be determined by the server automatically. The server may return a token with a longer or shorter lifetime.")
|
||||
|
||||
cmd.Flags().StringVar(&o.BoundObjectKind, "bound-object-kind", o.BoundObjectKind, "Kind of an object to bind the token to. "+
|
||||
"Supported kinds are "+strings.Join(sets.StringKeySet(boundObjectKindToAPIVersion).List(), ", ")+". "+
|
||||
"Supported kinds are "+strings.Join(sets.List(sets.KeySet(boundObjectKinds)), ", ")+". "+
|
||||
"If set, --bound-object-name must be provided.")
|
||||
cmd.Flags().StringVar(&o.BoundObjectName, "bound-object-name", o.BoundObjectName, "Name of an object to bind the token to. "+
|
||||
"The token will expire when the object is deleted. "+
|
||||
|
@ -149,6 +154,8 @@ func NewCmdCreateToken(f cmdutil.Factory, ioStreams genericiooptions.IOStreams)
|
|||
"Requires --bound-object-kind and --bound-object-name. "+
|
||||
"If unset, the UID of the existing object is used.")
|
||||
|
||||
o.Flags = cmd.Flags()
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
|
@ -196,7 +203,7 @@ func (o *TokenOptions) Validate() error {
|
|||
return fmt.Errorf("--namespace is required")
|
||||
}
|
||||
if o.Duration < 0 {
|
||||
return fmt.Errorf("--duration must be positive")
|
||||
return fmt.Errorf("--duration must be greater than or equal to 0")
|
||||
}
|
||||
if o.Duration%time.Second != 0 {
|
||||
return fmt.Errorf("--duration cannot be expressed in units less than seconds")
|
||||
|
@ -215,8 +222,8 @@ func (o *TokenOptions) Validate() error {
|
|||
return fmt.Errorf("--bound-object-uid can only be set if --bound-object-kind is provided")
|
||||
}
|
||||
} else {
|
||||
if _, ok := boundObjectKindToAPIVersion[o.BoundObjectKind]; !ok {
|
||||
return fmt.Errorf("supported --bound-object-kind values are %s", strings.Join(sets.StringKeySet(boundObjectKindToAPIVersion).List(), ", "))
|
||||
if _, ok := boundObjectKinds[o.BoundObjectKind]; !ok {
|
||||
return fmt.Errorf("supported --bound-object-kind values are %s", strings.Join(sets.List(sets.KeySet(boundObjectKinds)), ", "))
|
||||
}
|
||||
if len(o.BoundObjectName) == 0 {
|
||||
return fmt.Errorf("--bound-object-name is required if --bound-object-kind is provided")
|
||||
|
@ -234,12 +241,12 @@ func (o *TokenOptions) Run() error {
|
|||
},
|
||||
}
|
||||
if o.Duration > 0 {
|
||||
request.Spec.ExpirationSeconds = pointer.Int64(int64(o.Duration / time.Second))
|
||||
request.Spec.ExpirationSeconds = ptr.To(int64(o.Duration / time.Second))
|
||||
}
|
||||
if len(o.BoundObjectKind) > 0 {
|
||||
request.Spec.BoundObjectRef = &authenticationv1.BoundObjectReference{
|
||||
Kind: o.BoundObjectKind,
|
||||
APIVersion: boundObjectKindToAPIVersion[o.BoundObjectKind],
|
||||
APIVersion: boundObjectKinds[o.BoundObjectKind],
|
||||
Name: o.BoundObjectName,
|
||||
UID: types.UID(o.BoundObjectUID),
|
||||
}
|
||||
|
|
|
@ -26,7 +26,6 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"k8s.io/utils/pointer"
|
||||
kjson "sigs.k8s.io/json"
|
||||
|
||||
authenticationv1 "k8s.io/api/authentication/v1"
|
||||
|
@ -38,6 +37,7 @@ import (
|
|||
cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/scheme"
|
||||
"k8s.io/utils/ptr"
|
||||
)
|
||||
|
||||
func TestCreateToken(t *testing.T) {
|
||||
|
@ -99,8 +99,7 @@ func TestCreateToken(t *testing.T) {
|
|||
serverResponseToken: "abc",
|
||||
expectStdout: `apiVersion: authentication.k8s.io/v1
|
||||
kind: TokenRequest
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
metadata: {}
|
||||
spec:
|
||||
audiences: null
|
||||
boundObjectRef: null
|
||||
|
@ -115,7 +114,13 @@ status:
|
|||
test: "bad bound object kind",
|
||||
name: "mysa",
|
||||
boundObjectKind: "Foo",
|
||||
expectStderr: `error: supported --bound-object-kind values are Pod, Secret`,
|
||||
expectStderr: `error: supported --bound-object-kind values are Node, Pod, Secret`,
|
||||
},
|
||||
{
|
||||
test: "bad bound object kind (node feature enabled)",
|
||||
name: "mysa",
|
||||
boundObjectKind: "Foo",
|
||||
expectStderr: `error: supported --bound-object-kind values are Node, Pod, Secret`,
|
||||
},
|
||||
{
|
||||
test: "missing bound object name",
|
||||
|
@ -158,7 +163,29 @@ status:
|
|||
serverResponseToken: "abc",
|
||||
expectStdout: "abc",
|
||||
},
|
||||
{
|
||||
test: "valid bound object (Node)",
|
||||
name: "mysa",
|
||||
|
||||
boundObjectKind: "Node",
|
||||
boundObjectName: "mynode",
|
||||
boundObjectUID: "myuid",
|
||||
|
||||
expectRequestPath: "/api/v1/namespaces/test/serviceaccounts/mysa/token",
|
||||
expectTokenRequest: &authenticationv1.TokenRequest{
|
||||
TypeMeta: metav1.TypeMeta{APIVersion: "authentication.k8s.io/v1", Kind: "TokenRequest"},
|
||||
Spec: authenticationv1.TokenRequestSpec{
|
||||
BoundObjectRef: &authenticationv1.BoundObjectReference{
|
||||
Kind: "Node",
|
||||
APIVersion: "v1",
|
||||
Name: "mynode",
|
||||
UID: "myuid",
|
||||
},
|
||||
},
|
||||
},
|
||||
serverResponseToken: "abc",
|
||||
expectStdout: "abc",
|
||||
},
|
||||
{
|
||||
test: "invalid audience",
|
||||
name: "mysa",
|
||||
|
@ -186,7 +213,7 @@ status:
|
|||
test: "invalid duration",
|
||||
name: "mysa",
|
||||
duration: -1,
|
||||
expectStderr: `error: --duration must be positive`,
|
||||
expectStderr: `error: --duration must be greater than or equal to 0`,
|
||||
},
|
||||
{
|
||||
test: "invalid duration unit",
|
||||
|
@ -204,13 +231,28 @@ status:
|
|||
expectTokenRequest: &authenticationv1.TokenRequest{
|
||||
TypeMeta: metav1.TypeMeta{APIVersion: "authentication.k8s.io/v1", Kind: "TokenRequest"},
|
||||
Spec: authenticationv1.TokenRequestSpec{
|
||||
ExpirationSeconds: pointer.Int64(1000),
|
||||
ExpirationSeconds: ptr.To[int64](1000),
|
||||
},
|
||||
},
|
||||
serverResponseToken: "abc",
|
||||
expectStdout: "abc",
|
||||
},
|
||||
{
|
||||
test: "zero duration act as default",
|
||||
name: "mysa",
|
||||
|
||||
duration: 0 * time.Second,
|
||||
|
||||
expectRequestPath: "/api/v1/namespaces/test/serviceaccounts/mysa/token",
|
||||
expectTokenRequest: &authenticationv1.TokenRequest{
|
||||
TypeMeta: metav1.TypeMeta{APIVersion: "authentication.k8s.io/v1", Kind: "TokenRequest"},
|
||||
Spec: authenticationv1.TokenRequestSpec{
|
||||
ExpirationSeconds: nil,
|
||||
},
|
||||
},
|
||||
serverResponseToken: "abc",
|
||||
expectStdout: "abc",
|
||||
},
|
||||
{
|
||||
test: "server error",
|
||||
name: "mysa",
|
||||
|
|
|
@ -20,12 +20,11 @@ import (
|
|||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/distribution/reference"
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/klog/v2"
|
||||
"k8s.io/utils/pointer"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
|
@ -45,6 +44,7 @@ import (
|
|||
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
watchtools "k8s.io/client-go/tools/watch"
|
||||
"k8s.io/klog/v2"
|
||||
"k8s.io/kubectl/pkg/cmd/attach"
|
||||
"k8s.io/kubectl/pkg/cmd/exec"
|
||||
"k8s.io/kubectl/pkg/cmd/logs"
|
||||
|
@ -55,6 +55,8 @@ import (
|
|||
"k8s.io/kubectl/pkg/util/interrupt"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
"k8s.io/kubectl/pkg/util/term"
|
||||
"k8s.io/utils/ptr"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -73,6 +75,9 @@ var (
|
|||
debugging utilities without restarting the pod.
|
||||
* Node: Create a new pod that runs in the node's host namespaces and can access
|
||||
the node's filesystem.
|
||||
|
||||
Note: When a non-root user is configured for the entire target Pod, some capabilities granted
|
||||
by debug profile may not work.
|
||||
`))
|
||||
|
||||
debugExample = templates.Examples(i18n.T(`
|
||||
|
@ -106,28 +111,39 @@ var (
|
|||
|
||||
var nameSuffixFunc = utilrand.String
|
||||
|
||||
type DebugAttachFunc func(ctx context.Context, restClientGetter genericclioptions.RESTClientGetter, cmdPath string, ns, podName, containerName string) error
|
||||
|
||||
// DebugOptions holds the options for an invocation of kubectl debug.
|
||||
type DebugOptions struct {
|
||||
Args []string
|
||||
ArgsOnly bool
|
||||
Attach bool
|
||||
Container string
|
||||
CopyTo string
|
||||
Replace bool
|
||||
Env []corev1.EnvVar
|
||||
Image string
|
||||
Interactive bool
|
||||
Namespace string
|
||||
TargetNames []string
|
||||
PullPolicy corev1.PullPolicy
|
||||
Quiet bool
|
||||
SameNode bool
|
||||
SetImages map[string]string
|
||||
ShareProcesses bool
|
||||
TargetContainer string
|
||||
TTY bool
|
||||
Profile string
|
||||
Applier ProfileApplier
|
||||
Args []string
|
||||
ArgsOnly bool
|
||||
Attach bool
|
||||
AttachFunc DebugAttachFunc
|
||||
Container string
|
||||
CopyTo string
|
||||
Replace bool
|
||||
Env []corev1.EnvVar
|
||||
Image string
|
||||
Interactive bool
|
||||
KeepLabels bool
|
||||
KeepAnnotations bool
|
||||
KeepLiveness bool
|
||||
KeepReadiness bool
|
||||
KeepStartup bool
|
||||
KeepInitContainers bool
|
||||
Namespace string
|
||||
TargetNames []string
|
||||
PullPolicy corev1.PullPolicy
|
||||
Quiet bool
|
||||
SameNode bool
|
||||
SetImages map[string]string
|
||||
ShareProcesses bool
|
||||
TargetContainer string
|
||||
TTY bool
|
||||
Profile string
|
||||
CustomProfileFile string
|
||||
CustomProfile *corev1.Container
|
||||
Applier ProfileApplier
|
||||
|
||||
explicitNamespace bool
|
||||
attachChanged bool
|
||||
|
@ -145,10 +161,11 @@ type DebugOptions struct {
|
|||
// NewDebugOptions returns a DebugOptions initialized with default values.
|
||||
func NewDebugOptions(streams genericiooptions.IOStreams) *DebugOptions {
|
||||
return &DebugOptions{
|
||||
Args: []string{},
|
||||
IOStreams: streams,
|
||||
TargetNames: []string{},
|
||||
ShareProcesses: true,
|
||||
Args: []string{},
|
||||
IOStreams: streams,
|
||||
KeepInitContainers: true,
|
||||
TargetNames: []string{},
|
||||
ShareProcesses: true,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -183,6 +200,12 @@ func (o *DebugOptions) AddFlags(cmd *cobra.Command) {
|
|||
cmd.Flags().BoolVar(&o.Replace, "replace", o.Replace, i18n.T("When used with '--copy-to', delete the original Pod."))
|
||||
cmd.Flags().StringToString("env", nil, i18n.T("Environment variables to set in the container."))
|
||||
cmd.Flags().StringVar(&o.Image, "image", o.Image, i18n.T("Container image to use for debug container."))
|
||||
cmd.Flags().BoolVar(&o.KeepLabels, "keep-labels", o.KeepLabels, i18n.T("If true, keep the original pod labels.(This flag only works when used with '--copy-to')"))
|
||||
cmd.Flags().BoolVar(&o.KeepAnnotations, "keep-annotations", o.KeepAnnotations, i18n.T("If true, keep the original pod annotations.(This flag only works when used with '--copy-to')"))
|
||||
cmd.Flags().BoolVar(&o.KeepLiveness, "keep-liveness", o.KeepLiveness, i18n.T("If true, keep the original pod liveness probes.(This flag only works when used with '--copy-to')"))
|
||||
cmd.Flags().BoolVar(&o.KeepReadiness, "keep-readiness", o.KeepReadiness, i18n.T("If true, keep the original pod readiness probes.(This flag only works when used with '--copy-to')"))
|
||||
cmd.Flags().BoolVar(&o.KeepStartup, "keep-startup", o.KeepStartup, i18n.T("If true, keep the original startup probes.(This flag only works when used with '--copy-to')"))
|
||||
cmd.Flags().BoolVar(&o.KeepInitContainers, "keep-init-containers", o.KeepInitContainers, i18n.T("Run the init containers for the pod. Defaults to true.(This flag only works when used with '--copy-to')"))
|
||||
cmd.Flags().StringToStringVar(&o.SetImages, "set-image", o.SetImages, i18n.T("When used with '--copy-to', a list of name=image pairs for changing container images, similar to how 'kubectl set image' works."))
|
||||
cmd.Flags().String("image-pull-policy", "", i18n.T("The image pull policy for the container. If left empty, this value will not be specified by the client and defaulted by the server."))
|
||||
cmd.Flags().BoolVarP(&o.Interactive, "stdin", "i", o.Interactive, i18n.T("Keep stdin open on the container(s) in the pod, even if nothing is attached."))
|
||||
|
@ -191,7 +214,8 @@ func (o *DebugOptions) AddFlags(cmd *cobra.Command) {
|
|||
cmd.Flags().BoolVar(&o.ShareProcesses, "share-processes", o.ShareProcesses, i18n.T("When used with '--copy-to', enable process namespace sharing in the copy."))
|
||||
cmd.Flags().StringVar(&o.TargetContainer, "target", "", i18n.T("When using an ephemeral container, target processes in this container name."))
|
||||
cmd.Flags().BoolVarP(&o.TTY, "tty", "t", o.TTY, i18n.T("Allocate a TTY for the debugging container."))
|
||||
cmd.Flags().StringVar(&o.Profile, "profile", ProfileLegacy, i18n.T(`Debugging profile. Options are "legacy", "general", "baseline", "netadmin", or "restricted".`))
|
||||
cmd.Flags().StringVar(&o.Profile, "profile", ProfileLegacy, i18n.T(`Options are "legacy", "general", "baseline", "netadmin", "restricted" or "sysadmin".`))
|
||||
cmd.Flags().StringVar(&o.CustomProfileFile, "custom", o.CustomProfileFile, i18n.T("Path to a JSON or YAML file containing a partial container spec to customize built-in debug profiles."))
|
||||
}
|
||||
|
||||
// Complete finishes run-time initialization of debug.DebugOptions.
|
||||
|
@ -214,6 +238,15 @@ func (o *DebugOptions) Complete(restClientGetter genericclioptions.RESTClientGet
|
|||
o.Attach = true
|
||||
}
|
||||
|
||||
// Downstream tools may want to use their own customized
|
||||
// attach function to do extra work or use attach command
|
||||
// with different flags instead of the static one defined in
|
||||
// handleAttachPod. But if this function is not set explicitly,
|
||||
// we fall back to default.
|
||||
if o.AttachFunc == nil {
|
||||
o.AttachFunc = o.handleAttachPod
|
||||
}
|
||||
|
||||
// Environment
|
||||
envStrings, err := cmd.Flags().GetStringToString("env")
|
||||
if err != nil {
|
||||
|
@ -239,13 +272,36 @@ func (o *DebugOptions) Complete(restClientGetter genericclioptions.RESTClientGet
|
|||
}
|
||||
|
||||
if o.Applier == nil {
|
||||
applier, err := NewProfileApplier(o.Profile)
|
||||
kflags := KeepFlags{
|
||||
Labels: o.KeepLabels,
|
||||
Annotations: o.KeepAnnotations,
|
||||
Liveness: o.KeepLiveness,
|
||||
Readiness: o.KeepReadiness,
|
||||
Startup: o.KeepStartup,
|
||||
InitContainers: o.KeepInitContainers,
|
||||
}
|
||||
applier, err := NewProfileApplier(o.Profile, kflags)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
o.Applier = applier
|
||||
}
|
||||
|
||||
if o.CustomProfileFile != "" {
|
||||
customProfileBytes, err := os.ReadFile(o.CustomProfileFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("must pass a container spec json file for custom profile: %w", err)
|
||||
}
|
||||
|
||||
err = json.Unmarshal(customProfileBytes, &o.CustomProfile)
|
||||
if err != nil {
|
||||
err = yaml.Unmarshal(customProfileBytes, &o.CustomProfile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s does not contain a valid container spec: %w", o.CustomProfileFile, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clientConfig, err := restClientGetter.ToRESTConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -338,6 +394,17 @@ func (o *DebugOptions) Validate() error {
|
|||
return fmt.Errorf("WarningPrinter can not be used without initialization")
|
||||
}
|
||||
|
||||
if o.CustomProfile != nil {
|
||||
if o.CustomProfile.Name != "" || len(o.CustomProfile.Command) > 0 || o.CustomProfile.Image != "" || o.CustomProfile.Lifecycle != nil || len(o.CustomProfile.VolumeDevices) > 0 {
|
||||
return fmt.Errorf("name, command, image, lifecycle and volume devices are not modifiable via custom profile")
|
||||
}
|
||||
}
|
||||
|
||||
// Warning for legacy profile
|
||||
if o.Profile == ProfileLegacy {
|
||||
fmt.Fprintln(o.ErrOut, `--profile=legacy is deprecated and will be removed in the future. It is recommended to explicitly specify a profile, for example "--profile=general".`)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -377,26 +444,8 @@ func (o *DebugOptions) Run(restClientGetter genericclioptions.RESTClientGetter,
|
|||
return visitErr
|
||||
}
|
||||
|
||||
if o.Attach && len(containerName) > 0 {
|
||||
opts := &attach.AttachOptions{
|
||||
StreamOptions: exec.StreamOptions{
|
||||
IOStreams: o.IOStreams,
|
||||
Stdin: o.Interactive,
|
||||
TTY: o.TTY,
|
||||
Quiet: o.Quiet,
|
||||
},
|
||||
CommandName: cmd.Parent().CommandPath() + " attach",
|
||||
|
||||
Attach: &attach.DefaultRemoteAttach{},
|
||||
}
|
||||
config, err := restClientGetter.ToRESTConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
opts.Config = config
|
||||
opts.AttachFunc = attach.DefaultAttachFunc
|
||||
|
||||
if err := o.handleAttachPod(ctx, restClientGetter, debugPod.Namespace, debugPod.Name, containerName, opts); err != nil {
|
||||
if o.Attach && len(containerName) > 0 && o.AttachFunc != nil {
|
||||
if err := o.AttachFunc(ctx, restClientGetter, cmd.Parent().CommandPath(), debugPod.Namespace, debugPod.Name, containerName); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
@ -449,6 +498,8 @@ func (o *DebugOptions) debugByEphemeralContainer(ctx context.Context, pod *corev
|
|||
}
|
||||
klog.V(2).Infof("new ephemeral container: %#v", debugContainer)
|
||||
|
||||
o.displayWarning((*corev1.Container)(&debugContainer.EphemeralContainerCommon), pod)
|
||||
|
||||
debugJS, err := json.Marshal(debugPod)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("error creating JSON for debug container: %v", err)
|
||||
|
@ -469,51 +520,94 @@ func (o *DebugOptions) debugByEphemeralContainer(ctx context.Context, pod *corev
|
|||
return nil, "", fmt.Errorf("ephemeral containers are disabled for this cluster (error from server: %q)", err)
|
||||
}
|
||||
|
||||
// The Kind used for the /ephemeralcontainers subresource changed in 1.22. When presented with an unexpected
|
||||
// Kind the api server will respond with a not-registered error. When this happens we can optimistically try
|
||||
// using the old API.
|
||||
if runtime.IsNotRegisteredError(err) {
|
||||
klog.V(1).Infof("Falling back to legacy API because server returned error: %v", err)
|
||||
return o.debugByEphemeralContainerLegacy(ctx, pod, debugContainer)
|
||||
}
|
||||
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
return result, debugContainer.Name, nil
|
||||
}
|
||||
|
||||
// debugByEphemeralContainerLegacy adds debugContainer as an ephemeral container using the pre-1.22 /ephemeralcontainers API
|
||||
// This may be removed when we no longer wish to support releases prior to 1.22.
|
||||
func (o *DebugOptions) debugByEphemeralContainerLegacy(ctx context.Context, pod *corev1.Pod, debugContainer *corev1.EphemeralContainer) (*corev1.Pod, string, error) {
|
||||
// We no longer have the v1.EphemeralContainers Kind since it was removed in 1.22, but
|
||||
// we can present a JSON 6902 patch that the api server will apply.
|
||||
patch, err := json.Marshal([]map[string]interface{}{{
|
||||
"op": "add",
|
||||
"path": "/ephemeralContainers/-",
|
||||
"value": debugContainer,
|
||||
}})
|
||||
// applyCustomProfile applies given partial container json file on to the profile
|
||||
// incorporated debug pod.
|
||||
func (o *DebugOptions) applyCustomProfile(debugPod *corev1.Pod, containerName string) error {
|
||||
o.CustomProfile.Name = containerName
|
||||
customJS, err := json.Marshal(o.CustomProfile)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("error creating JSON 6902 patch for old /ephemeralcontainers API: %s", err)
|
||||
return fmt.Errorf("unable to marshall custom profile: %w", err)
|
||||
}
|
||||
|
||||
result := o.podClient.RESTClient().Patch(types.JSONPatchType).
|
||||
Namespace(pod.Namespace).
|
||||
Resource("pods").
|
||||
Name(pod.Name).
|
||||
SubResource("ephemeralcontainers").
|
||||
Body(patch).
|
||||
Do(ctx)
|
||||
if err := result.Error(); err != nil {
|
||||
return nil, "", err
|
||||
var index int
|
||||
found := false
|
||||
for i, val := range debugPod.Spec.Containers {
|
||||
if val.Name == containerName {
|
||||
index = i
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
newPod, err := o.podClient.Pods(pod.Namespace).Get(ctx, pod.Name, metav1.GetOptions{})
|
||||
if !found {
|
||||
return fmt.Errorf("unable to find the %s container in the pod %s", containerName, debugPod.Name)
|
||||
}
|
||||
|
||||
var debugContainerJS []byte
|
||||
debugContainerJS, err = json.Marshal(debugPod.Spec.Containers[index])
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
return fmt.Errorf("unable to marshall container: %w", err)
|
||||
}
|
||||
|
||||
return newPod, debugContainer.Name, nil
|
||||
patchedContainer, err := strategicpatch.StrategicMergePatch(debugContainerJS, customJS, corev1.Container{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating three way patch to add debug container: %w", err)
|
||||
}
|
||||
|
||||
err = json.Unmarshal(patchedContainer, &debugPod.Spec.Containers[index])
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to unmarshall patched container to container: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// applyCustomProfileEphemeral applies given partial container json file on to the profile
|
||||
// incorporated ephemeral container of the pod.
|
||||
func (o *DebugOptions) applyCustomProfileEphemeral(debugPod *corev1.Pod, containerName string) error {
|
||||
o.CustomProfile.Name = containerName
|
||||
customJS, err := json.Marshal(o.CustomProfile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to marshall custom profile: %w", err)
|
||||
}
|
||||
|
||||
var index int
|
||||
found := false
|
||||
for i, val := range debugPod.Spec.EphemeralContainers {
|
||||
if val.Name == containerName {
|
||||
index = i
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return fmt.Errorf("unable to find the %s ephemeral container in the pod %s", containerName, debugPod.Name)
|
||||
}
|
||||
|
||||
var debugContainerJS []byte
|
||||
debugContainerJS, err = json.Marshal(debugPod.Spec.EphemeralContainers[index])
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to marshall ephemeral container:%w", err)
|
||||
}
|
||||
|
||||
patchedContainer, err := strategicpatch.StrategicMergePatch(debugContainerJS, customJS, corev1.Container{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating three way patch to add debug container: %w", err)
|
||||
}
|
||||
|
||||
err = json.Unmarshal(patchedContainer, &debugPod.Spec.EphemeralContainers[index])
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to unmarshall patched container to ephemeral container: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// debugByCopy runs a copy of the target Pod with a debug container added or an original container modified
|
||||
|
@ -522,6 +616,16 @@ func (o *DebugOptions) debugByCopy(ctx context.Context, pod *corev1.Pod) (*corev
|
|||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
var debugContainer *corev1.Container
|
||||
for i := range copied.Spec.Containers {
|
||||
if copied.Spec.Containers[i].Name == dc {
|
||||
debugContainer = &copied.Spec.Containers[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
o.displayWarning(debugContainer, copied)
|
||||
|
||||
created, err := o.podClient.Pods(copied.Namespace).Create(ctx, copied, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
|
@ -535,6 +639,32 @@ func (o *DebugOptions) debugByCopy(ctx context.Context, pod *corev1.Pod) (*corev
|
|||
return created, dc, nil
|
||||
}
|
||||
|
||||
// Display warning message if some capabilities are set by profile and non-root user is specified in .Spec.SecurityContext.RunAsUser.(#1650)
|
||||
func (o *DebugOptions) displayWarning(container *corev1.Container, pod *corev1.Pod) {
|
||||
if container == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if pod.Spec.SecurityContext.RunAsUser == nil || *pod.Spec.SecurityContext.RunAsUser == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if container.SecurityContext == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if container.SecurityContext.RunAsUser != nil && *container.SecurityContext.RunAsUser == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if (container.SecurityContext.Privileged == nil || !*container.SecurityContext.Privileged) &&
|
||||
(container.SecurityContext.Capabilities == nil || len(container.SecurityContext.Capabilities.Add) == 0) {
|
||||
return
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintln(o.ErrOut, `Warning: Non-root user is configured for the entire target Pod, and some capabilities granted by debug profile may not work. Please consider using "--custom" with a custom profile that specifies "securityContext.runAsUser: 0".`)
|
||||
}
|
||||
|
||||
// generateDebugContainer returns a debugging pod and an EphemeralContainer suitable for use as a debug container
|
||||
// in the given pod.
|
||||
func (o *DebugOptions) generateDebugContainer(pod *corev1.Pod) (*corev1.Pod, *corev1.EphemeralContainer, error) {
|
||||
|
@ -564,6 +694,13 @@ func (o *DebugOptions) generateDebugContainer(pod *corev1.Pod) (*corev1.Pod, *co
|
|||
return nil, nil, err
|
||||
}
|
||||
|
||||
if o.CustomProfile != nil {
|
||||
err := o.applyCustomProfileEphemeral(copied, ec.Name)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
ec = &copied.Spec.EphemeralContainers[len(copied.Spec.EphemeralContainers)-1]
|
||||
|
||||
return copied, ec, nil
|
||||
|
@ -590,6 +727,9 @@ func (o *DebugOptions) generateNodeDebugPod(node *corev1.Node) (*corev1.Pod, err
|
|||
p := &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: pn,
|
||||
Labels: map[string]string{
|
||||
"app.kubernetes.io/managed-by": "kubectl-debug",
|
||||
},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
|
@ -623,6 +763,13 @@ func (o *DebugOptions) generateNodeDebugPod(node *corev1.Node) (*corev1.Pod, err
|
|||
return nil, err
|
||||
}
|
||||
|
||||
if o.CustomProfile != nil {
|
||||
err := o.applyCustomProfile(p, cn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
|
@ -633,6 +780,7 @@ func (o *DebugOptions) generatePodCopyWithDebugContainer(pod *corev1.Pod) (*core
|
|||
Name: o.CopyTo,
|
||||
Namespace: pod.Namespace,
|
||||
Annotations: pod.Annotations,
|
||||
Labels: pod.Labels,
|
||||
},
|
||||
Spec: *pod.Spec.DeepCopy(),
|
||||
}
|
||||
|
@ -640,7 +788,7 @@ func (o *DebugOptions) generatePodCopyWithDebugContainer(pod *corev1.Pod) (*core
|
|||
copied.Spec.EphemeralContainers = nil
|
||||
// change ShareProcessNamespace configuration only when commanded explicitly
|
||||
if o.shareProcessedChanged {
|
||||
copied.Spec.ShareProcessNamespace = pointer.Bool(o.ShareProcesses)
|
||||
copied.Spec.ShareProcessNamespace = ptr.To(o.ShareProcesses)
|
||||
}
|
||||
if !o.SameNode {
|
||||
copied.Spec.NodeName = ""
|
||||
|
@ -705,6 +853,13 @@ func (o *DebugOptions) generatePodCopyWithDebugContainer(pod *corev1.Pod) (*core
|
|||
return nil, "", err
|
||||
}
|
||||
|
||||
if o.CustomProfile != nil {
|
||||
err = o.applyCustomProfile(copied, name)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
}
|
||||
|
||||
return copied, name, nil
|
||||
}
|
||||
|
||||
|
@ -795,7 +950,25 @@ func (o *DebugOptions) waitForContainer(ctx context.Context, ns, podName, contai
|
|||
return result, err
|
||||
}
|
||||
|
||||
func (o *DebugOptions) handleAttachPod(ctx context.Context, restClientGetter genericclioptions.RESTClientGetter, ns, podName, containerName string, opts *attach.AttachOptions) error {
|
||||
func (o *DebugOptions) handleAttachPod(ctx context.Context, restClientGetter genericclioptions.RESTClientGetter, cmdPath string, ns, podName, containerName string) error {
|
||||
opts := &attach.AttachOptions{
|
||||
StreamOptions: exec.StreamOptions{
|
||||
IOStreams: o.IOStreams,
|
||||
Stdin: o.Interactive,
|
||||
TTY: o.TTY,
|
||||
Quiet: o.Quiet,
|
||||
},
|
||||
CommandName: cmdPath + " attach",
|
||||
|
||||
Attach: &attach.DefaultRemoteAttach{},
|
||||
}
|
||||
config, err := restClientGetter.ToRESTConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
opts.Config = config
|
||||
opts.AttachFunc = attach.DefaultAttachFunc
|
||||
|
||||
pod, err := o.waitForContainer(ctx, ns, podName, containerName)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -816,12 +989,12 @@ func (o *DebugOptions) handleAttachPod(ctx context.Context, restClientGetter gen
|
|||
}
|
||||
if status.State.Terminated != nil {
|
||||
klog.V(1).Info("Ephemeral container terminated, falling back to logs")
|
||||
return logOpts(restClientGetter, pod, opts)
|
||||
return logOpts(ctx, restClientGetter, pod, opts)
|
||||
}
|
||||
|
||||
if err := opts.Run(); err != nil {
|
||||
fmt.Fprintf(opts.ErrOut, "warning: couldn't attach to pod/%s, falling back to streaming logs: %v\n", podName, err)
|
||||
return logOpts(restClientGetter, pod, opts)
|
||||
return logOpts(ctx, restClientGetter, pod, opts)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -839,7 +1012,7 @@ func getContainerStatusByName(pod *corev1.Pod, containerName string) *corev1.Con
|
|||
}
|
||||
|
||||
// logOpts logs output from opts to the pods log.
|
||||
func logOpts(restClientGetter genericclioptions.RESTClientGetter, pod *corev1.Pod, opts *attach.AttachOptions) error {
|
||||
func logOpts(ctx context.Context, restClientGetter genericclioptions.RESTClientGetter, pod *corev1.Pod, opts *attach.AttachOptions) error {
|
||||
ctrName, err := opts.GetContainerName(pod)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -850,7 +1023,7 @@ func logOpts(restClientGetter genericclioptions.RESTClientGetter, pod *corev1.Po
|
|||
return err
|
||||
}
|
||||
for _, request := range requests {
|
||||
if err := logs.DefaultConsumeRequest(request, opts.Out); err != nil {
|
||||
if err := logs.DefaultConsumeRequest(ctx, request, opts.Out); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -22,7 +22,7 @@ import (
|
|||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/kubectl/pkg/util/podutils"
|
||||
"k8s.io/utils/pointer"
|
||||
"k8s.io/utils/ptr"
|
||||
)
|
||||
|
||||
type debugStyle int
|
||||
|
@ -54,6 +54,8 @@ const (
|
|||
ProfileRestricted = "restricted"
|
||||
// ProfileNetadmin offers elevated privileges for network debugging.
|
||||
ProfileNetadmin = "netadmin"
|
||||
// ProfileSysadmin offers elevated privileges for debugging.
|
||||
ProfileSysadmin = "sysadmin"
|
||||
)
|
||||
|
||||
type ProfileApplier interface {
|
||||
|
@ -62,49 +64,91 @@ type ProfileApplier interface {
|
|||
}
|
||||
|
||||
// NewProfileApplier returns a new Options for the given profile name.
|
||||
func NewProfileApplier(profile string) (ProfileApplier, error) {
|
||||
func NewProfileApplier(profile string, kflags KeepFlags) (ProfileApplier, error) {
|
||||
switch profile {
|
||||
case ProfileLegacy:
|
||||
return &legacyProfile{}, nil
|
||||
return &legacyProfile{kflags}, nil
|
||||
case ProfileGeneral:
|
||||
return &generalProfile{}, nil
|
||||
return &generalProfile{kflags}, nil
|
||||
case ProfileBaseline:
|
||||
return &baselineProfile{}, nil
|
||||
return &baselineProfile{kflags}, nil
|
||||
case ProfileRestricted:
|
||||
return &restrictedProfile{}, nil
|
||||
return &restrictedProfile{kflags}, nil
|
||||
case ProfileNetadmin:
|
||||
return &netadminProfile{}, nil
|
||||
return &netadminProfile{kflags}, nil
|
||||
case ProfileSysadmin:
|
||||
return &sysadminProfile{kflags}, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unknown profile: %s", profile)
|
||||
}
|
||||
|
||||
type legacyProfile struct {
|
||||
KeepFlags
|
||||
}
|
||||
|
||||
type generalProfile struct {
|
||||
KeepFlags
|
||||
}
|
||||
|
||||
type baselineProfile struct {
|
||||
KeepFlags
|
||||
}
|
||||
|
||||
type restrictedProfile struct {
|
||||
KeepFlags
|
||||
}
|
||||
|
||||
type netadminProfile struct {
|
||||
KeepFlags
|
||||
}
|
||||
|
||||
func (p *legacyProfile) Apply(pod *corev1.Pod, containerName string, target runtime.Object) error {
|
||||
switch target.(type) {
|
||||
case *corev1.Pod:
|
||||
// do nothing to the copied pod
|
||||
return nil
|
||||
case *corev1.Node:
|
||||
mountRootPartition(pod, containerName)
|
||||
useHostNamespaces(pod)
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("the %s profile doesn't support objects of type %T", ProfileLegacy, target)
|
||||
type sysadminProfile struct {
|
||||
KeepFlags
|
||||
}
|
||||
|
||||
// KeepFlags holds the flag set that determine which fields to keep in the copy pod.
|
||||
type KeepFlags struct {
|
||||
Labels bool
|
||||
Annotations bool
|
||||
Liveness bool
|
||||
Readiness bool
|
||||
Startup bool
|
||||
InitContainers bool
|
||||
}
|
||||
|
||||
// RemoveLabels removes labels from the pod.
|
||||
func (kflags KeepFlags) RemoveLabels(p *corev1.Pod) {
|
||||
if !kflags.Labels {
|
||||
p.Labels = nil
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveAnnotations remove annotations from the pod.
|
||||
func (kflags KeepFlags) RemoveAnnotations(p *corev1.Pod) {
|
||||
if !kflags.Annotations {
|
||||
p.Annotations = nil
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveProbes remove probes from all containers of the pod.
|
||||
func (kflags KeepFlags) RemoveProbes(p *corev1.Pod) {
|
||||
for i := range p.Spec.Containers {
|
||||
if !kflags.Liveness {
|
||||
p.Spec.Containers[i].LivenessProbe = nil
|
||||
}
|
||||
if !kflags.Readiness {
|
||||
p.Spec.Containers[i].ReadinessProbe = nil
|
||||
}
|
||||
if !kflags.Startup {
|
||||
p.Spec.Containers[i].StartupProbe = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveInitContainers remove initContainers from the pod.
|
||||
func (kflags KeepFlags) RemoveInitContainers(p *corev1.Pod) {
|
||||
if !kflags.InitContainers {
|
||||
p.Spec.InitContainers = nil
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -123,10 +167,32 @@ func getDebugStyle(pod *corev1.Pod, target runtime.Object) (debugStyle, error) {
|
|||
return unsupported, fmt.Errorf("objects of type %T are not supported", target)
|
||||
}
|
||||
|
||||
func (p *legacyProfile) Apply(pod *corev1.Pod, containerName string, target runtime.Object) error {
|
||||
style, err := getDebugStyle(pod, target)
|
||||
if err != nil {
|
||||
return fmt.Errorf("legacy profile: %w", err)
|
||||
}
|
||||
|
||||
switch style {
|
||||
case node:
|
||||
mountRootPartition(pod, containerName)
|
||||
useHostNamespaces(pod)
|
||||
|
||||
case podCopy:
|
||||
p.Labels = false
|
||||
p.RemoveLabels(pod)
|
||||
|
||||
case ephemeral:
|
||||
// no additional modifications needed
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *generalProfile) Apply(pod *corev1.Pod, containerName string, target runtime.Object) error {
|
||||
style, err := getDebugStyle(pod, target)
|
||||
if err != nil {
|
||||
return fmt.Errorf("general profile: %s", err)
|
||||
return fmt.Errorf("general profile: %w", err)
|
||||
}
|
||||
|
||||
switch style {
|
||||
|
@ -136,7 +202,10 @@ func (p *generalProfile) Apply(pod *corev1.Pod, containerName string, target run
|
|||
useHostNamespaces(pod)
|
||||
|
||||
case podCopy:
|
||||
removeLabelsAndProbes(pod)
|
||||
p.RemoveLabels(pod)
|
||||
p.RemoveAnnotations(pod)
|
||||
p.RemoveProbes(pod)
|
||||
p.RemoveInitContainers(pod)
|
||||
allowProcessTracing(pod, containerName)
|
||||
shareProcessNamespace(pod)
|
||||
|
||||
|
@ -150,14 +219,17 @@ func (p *generalProfile) Apply(pod *corev1.Pod, containerName string, target run
|
|||
func (p *baselineProfile) Apply(pod *corev1.Pod, containerName string, target runtime.Object) error {
|
||||
style, err := getDebugStyle(pod, target)
|
||||
if err != nil {
|
||||
return fmt.Errorf("baseline profile: %s", err)
|
||||
return fmt.Errorf("baseline profile: %w", err)
|
||||
}
|
||||
|
||||
clearSecurityContext(pod, containerName)
|
||||
|
||||
switch style {
|
||||
case podCopy:
|
||||
removeLabelsAndProbes(pod)
|
||||
p.RemoveLabels(pod)
|
||||
p.RemoveAnnotations(pod)
|
||||
p.RemoveProbes(pod)
|
||||
p.RemoveInitContainers(pod)
|
||||
shareProcessNamespace(pod)
|
||||
|
||||
case ephemeral, node:
|
||||
|
@ -170,7 +242,7 @@ func (p *baselineProfile) Apply(pod *corev1.Pod, containerName string, target ru
|
|||
func (p *restrictedProfile) Apply(pod *corev1.Pod, containerName string, target runtime.Object) error {
|
||||
style, err := getDebugStyle(pod, target)
|
||||
if err != nil {
|
||||
return fmt.Errorf("restricted profile: %s", err)
|
||||
return fmt.Errorf("restricted profile: %w", err)
|
||||
}
|
||||
|
||||
clearSecurityContext(pod, containerName)
|
||||
|
@ -181,6 +253,10 @@ func (p *restrictedProfile) Apply(pod *corev1.Pod, containerName string, target
|
|||
|
||||
switch style {
|
||||
case podCopy:
|
||||
p.RemoveLabels(pod)
|
||||
p.RemoveAnnotations(pod)
|
||||
p.RemoveProbes(pod)
|
||||
p.RemoveInitContainers(pod)
|
||||
shareProcessNamespace(pod)
|
||||
|
||||
case ephemeral, node:
|
||||
|
@ -193,7 +269,7 @@ func (p *restrictedProfile) Apply(pod *corev1.Pod, containerName string, target
|
|||
func (p *netadminProfile) Apply(pod *corev1.Pod, containerName string, target runtime.Object) error {
|
||||
style, err := getDebugStyle(pod, target)
|
||||
if err != nil {
|
||||
return fmt.Errorf("netadmin profile: %s", err)
|
||||
return fmt.Errorf("netadmin profile: %w", err)
|
||||
}
|
||||
|
||||
allowNetadminCapability(pod, containerName)
|
||||
|
@ -201,24 +277,47 @@ func (p *netadminProfile) Apply(pod *corev1.Pod, containerName string, target ru
|
|||
switch style {
|
||||
case node:
|
||||
useHostNamespaces(pod)
|
||||
setPrivileged(pod, containerName)
|
||||
|
||||
case podCopy, ephemeral:
|
||||
case podCopy:
|
||||
p.RemoveLabels(pod)
|
||||
p.RemoveAnnotations(pod)
|
||||
p.RemoveProbes(pod)
|
||||
p.RemoveInitContainers(pod)
|
||||
shareProcessNamespace(pod)
|
||||
|
||||
case ephemeral:
|
||||
// no additional modifications needed
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// removeLabelsAndProbes removes labels from the pod and remove probes
|
||||
// from all containers of the pod.
|
||||
func removeLabelsAndProbes(p *corev1.Pod) {
|
||||
p.Labels = nil
|
||||
for i := range p.Spec.Containers {
|
||||
p.Spec.Containers[i].LivenessProbe = nil
|
||||
p.Spec.Containers[i].ReadinessProbe = nil
|
||||
p.Spec.Containers[i].StartupProbe = nil
|
||||
func (p *sysadminProfile) Apply(pod *corev1.Pod, containerName string, target runtime.Object) error {
|
||||
style, err := getDebugStyle(pod, target)
|
||||
if err != nil {
|
||||
return fmt.Errorf("sysadmin profile: %w", err)
|
||||
}
|
||||
|
||||
setPrivileged(pod, containerName)
|
||||
|
||||
switch style {
|
||||
case node:
|
||||
useHostNamespaces(pod)
|
||||
mountRootPartition(pod, containerName)
|
||||
|
||||
case podCopy:
|
||||
// to mimic general, default and baseline
|
||||
p.RemoveLabels(pod)
|
||||
p.RemoveAnnotations(pod)
|
||||
p.RemoveProbes(pod)
|
||||
p.RemoveInitContainers(pod)
|
||||
shareProcessNamespace(pod)
|
||||
|
||||
case ephemeral:
|
||||
// no additional modifications needed
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// mountRootPartition mounts the host's root path at "/host" in the container.
|
||||
|
@ -254,7 +353,7 @@ func useHostNamespaces(p *corev1.Pod) {
|
|||
// process namespace.
|
||||
func shareProcessNamespace(p *corev1.Pod) {
|
||||
if p.Spec.ShareProcessNamespace == nil {
|
||||
p.Spec.ShareProcessNamespace = pointer.Bool(true)
|
||||
p.Spec.ShareProcessNamespace = ptr.To(true)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -278,7 +377,7 @@ func setPrivileged(p *corev1.Pod, containerName string) {
|
|||
if c.SecurityContext == nil {
|
||||
c.SecurityContext = &corev1.SecurityContext{}
|
||||
}
|
||||
c.SecurityContext.Privileged = pointer.Bool(true)
|
||||
c.SecurityContext.Privileged = ptr.To(true)
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
@ -292,7 +391,7 @@ func disallowRoot(p *corev1.Pod, containerName string) {
|
|||
if c.SecurityContext == nil {
|
||||
c.SecurityContext = &corev1.SecurityContext{}
|
||||
}
|
||||
c.SecurityContext.RunAsNonRoot = pointer.Bool(true)
|
||||
c.SecurityContext.RunAsNonRoot = ptr.To(true)
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
@ -326,13 +425,14 @@ func allowProcessTracing(p *corev1.Pod, containerName string) {
|
|||
})
|
||||
}
|
||||
|
||||
// allowNetadminCapability grants NET_ADMIN capability to the container.
|
||||
// allowNetadminCapability grants NET_ADMIN and NET_RAW capability to the container.
|
||||
func allowNetadminCapability(p *corev1.Pod, containerName string) {
|
||||
podutils.VisitContainers(&p.Spec, podutils.AllContainers, func(c *corev1.Container, _ podutils.ContainerType) bool {
|
||||
if c.Name != containerName {
|
||||
return true
|
||||
}
|
||||
addCapability(c, "NET_ADMIN")
|
||||
addCapability(c, "NET_RAW")
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
@ -356,7 +456,7 @@ func disallowPrivilegeEscalation(p *corev1.Pod, containerName string) {
|
|||
if c.SecurityContext == nil {
|
||||
c.SecurityContext = &corev1.SecurityContext{}
|
||||
}
|
||||
c.SecurityContext.AllowPrivilegeEscalation = pointer.Bool(false)
|
||||
c.SecurityContext.AllowPrivilegeEscalation = ptr.To(false)
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ import (
|
|||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/utils/pointer"
|
||||
"k8s.io/utils/ptr"
|
||||
)
|
||||
|
||||
var testNode = &corev1.Node{
|
||||
|
@ -34,6 +34,203 @@ var testNode = &corev1.Node{
|
|||
},
|
||||
}
|
||||
|
||||
func TestLegacyProfile(t *testing.T) {
|
||||
pod := &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "pod"},
|
||||
Spec: corev1.PodSpec{EphemeralContainers: []corev1.EphemeralContainer{
|
||||
{
|
||||
EphemeralContainerCommon: corev1.EphemeralContainerCommon{
|
||||
Name: "dbg", Image: "dbgimage",
|
||||
},
|
||||
},
|
||||
}},
|
||||
}
|
||||
|
||||
tests := map[string]struct {
|
||||
pod *corev1.Pod
|
||||
containerName string
|
||||
target runtime.Object
|
||||
expectPod *corev1.Pod
|
||||
expectErr bool
|
||||
}{
|
||||
"bad inputs results in error": {
|
||||
pod: nil,
|
||||
containerName: "dbg",
|
||||
target: runtime.Object(nil),
|
||||
expectErr: true,
|
||||
},
|
||||
"debug by ephemeral container": {
|
||||
pod: pod,
|
||||
containerName: "dbg",
|
||||
target: pod,
|
||||
expectPod: &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "pod"},
|
||||
Spec: corev1.PodSpec{EphemeralContainers: []corev1.EphemeralContainer{
|
||||
{
|
||||
EphemeralContainerCommon: corev1.EphemeralContainerCommon{Name: "dbg", Image: "dbgimage"},
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
"debug by pod copy": {
|
||||
pod: &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "podcopy",
|
||||
Labels: map[string]string{
|
||||
"app": "podcopy",
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"test": "test",
|
||||
},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
InitContainers: []corev1.Container{{Name: "init-container"}},
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "app",
|
||||
Image: "appimage",
|
||||
LivenessProbe: &corev1.Probe{},
|
||||
ReadinessProbe: &corev1.Probe{},
|
||||
StartupProbe: &corev1.Probe{},
|
||||
},
|
||||
{
|
||||
Name: "dbg",
|
||||
Image: "dbgimage",
|
||||
SecurityContext: &corev1.SecurityContext{
|
||||
Capabilities: &corev1.Capabilities{
|
||||
Add: []corev1.Capability{"NET_ADMIN"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
containerName: "dbg",
|
||||
target: &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "podcopy",
|
||||
Labels: map[string]string{
|
||||
"app": "podcopy",
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"test": "test",
|
||||
},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
InitContainers: []corev1.Container{{Name: "init-container"}},
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "app",
|
||||
Image: "appimage",
|
||||
LivenessProbe: &corev1.Probe{},
|
||||
ReadinessProbe: &corev1.Probe{},
|
||||
StartupProbe: &corev1.Probe{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectPod: &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "podcopy",
|
||||
Annotations: map[string]string{
|
||||
"test": "test",
|
||||
},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
InitContainers: []corev1.Container{{Name: "init-container"}},
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "app",
|
||||
Image: "appimage",
|
||||
LivenessProbe: &corev1.Probe{},
|
||||
ReadinessProbe: &corev1.Probe{},
|
||||
StartupProbe: &corev1.Probe{},
|
||||
},
|
||||
{
|
||||
Name: "dbg",
|
||||
Image: "dbgimage",
|
||||
SecurityContext: &corev1.SecurityContext{
|
||||
Capabilities: &corev1.Capabilities{
|
||||
Add: []corev1.Capability{"NET_ADMIN"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"debug by node": {
|
||||
pod: &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "pod"},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "dbg",
|
||||
Image: "dbgimage",
|
||||
SecurityContext: &corev1.SecurityContext{
|
||||
Capabilities: &corev1.Capabilities{
|
||||
Add: []corev1.Capability{"NET_ADMIN"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
containerName: "dbg",
|
||||
target: testNode,
|
||||
expectPod: &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "pod"},
|
||||
Spec: corev1.PodSpec{
|
||||
HostNetwork: true,
|
||||
HostPID: true,
|
||||
HostIPC: true,
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "dbg",
|
||||
Image: "dbgimage",
|
||||
SecurityContext: &corev1.SecurityContext{
|
||||
Capabilities: &corev1.Capabilities{
|
||||
Add: []corev1.Capability{"NET_ADMIN"},
|
||||
},
|
||||
},
|
||||
VolumeMounts: []corev1.VolumeMount{
|
||||
{
|
||||
MountPath: "/host",
|
||||
Name: "host-root",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Volumes: []corev1.Volume{
|
||||
{
|
||||
Name: "host-root",
|
||||
VolumeSource: corev1.VolumeSource{
|
||||
HostPath: &corev1.HostPathVolumeSource{Path: "/"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
applier := &legacyProfile{KeepFlags{InitContainers: true}}
|
||||
err := applier.Apply(test.pod, test.containerName, test.target)
|
||||
if (err != nil) != test.expectErr {
|
||||
t.Fatalf("expect error: %v, got error: %v", test.expectErr, (err != nil))
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if diff := cmp.Diff(test.expectPod, test.pod); diff != "" {
|
||||
t.Error("unexpected diff in generated object: (-want +got):\n", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGeneralProfile(t *testing.T) {
|
||||
pod := &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "pod"},
|
||||
|
@ -81,10 +278,25 @@ func TestGeneralProfile(t *testing.T) {
|
|||
},
|
||||
"debug by pod copy": {
|
||||
pod: &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "podcopy"},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "podcopy",
|
||||
Labels: map[string]string{
|
||||
"app": "podcopy",
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"test": "test",
|
||||
},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
InitContainers: []corev1.Container{{Name: "init-container"}},
|
||||
Containers: []corev1.Container{
|
||||
{Name: "app", Image: "appimage"},
|
||||
{
|
||||
Name: "app",
|
||||
Image: "appimage",
|
||||
LivenessProbe: &corev1.Probe{},
|
||||
ReadinessProbe: &corev1.Probe{},
|
||||
StartupProbe: &corev1.Probe{},
|
||||
},
|
||||
{
|
||||
Name: "dbg",
|
||||
Image: "dbgimage",
|
||||
|
@ -99,16 +311,32 @@ func TestGeneralProfile(t *testing.T) {
|
|||
},
|
||||
containerName: "dbg",
|
||||
target: &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "podcopy"},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "podcopy",
|
||||
Labels: map[string]string{
|
||||
"app": "podcopy",
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"test": "test",
|
||||
},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
InitContainers: []corev1.Container{{Name: "init-container"}},
|
||||
Containers: []corev1.Container{
|
||||
{Name: "app", Image: "appimage"},
|
||||
{
|
||||
Name: "app",
|
||||
Image: "appimage",
|
||||
LivenessProbe: &corev1.Probe{},
|
||||
ReadinessProbe: &corev1.Probe{},
|
||||
StartupProbe: &corev1.Probe{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectPod: &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "podcopy"},
|
||||
Spec: corev1.PodSpec{
|
||||
InitContainers: []corev1.Container{{Name: "init-container"}},
|
||||
Containers: []corev1.Container{
|
||||
{Name: "app", Image: "appimage"},
|
||||
{
|
||||
|
@ -121,7 +349,7 @@ func TestGeneralProfile(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
ShareProcessNamespace: pointer.Bool(true),
|
||||
ShareProcessNamespace: ptr.To(true),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -169,7 +397,8 @@ func TestGeneralProfile(t *testing.T) {
|
|||
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
err := (&generalProfile{}).Apply(test.pod, test.containerName, test.target)
|
||||
applier := &generalProfile{KeepFlags{InitContainers: true}}
|
||||
err := applier.Apply(test.pod, test.containerName, test.target)
|
||||
if (err != nil) != test.expectErr {
|
||||
t.Fatalf("expect error: %v, got error: %v", test.expectErr, (err != nil))
|
||||
}
|
||||
|
@ -230,27 +459,58 @@ func TestBaselineProfile(t *testing.T) {
|
|||
},
|
||||
"debug by pod copy": {
|
||||
pod: &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "podcopy"},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "podcopy",
|
||||
Labels: map[string]string{
|
||||
"app": "podcopy",
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"test": "test",
|
||||
},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
InitContainers: []corev1.Container{{Name: "init-container"}},
|
||||
Containers: []corev1.Container{
|
||||
{Name: "app", Image: "appimage"},
|
||||
{
|
||||
Name: "app",
|
||||
Image: "appimage",
|
||||
LivenessProbe: &corev1.Probe{},
|
||||
ReadinessProbe: &corev1.Probe{},
|
||||
StartupProbe: &corev1.Probe{},
|
||||
},
|
||||
{Name: "dbg", Image: "dbgimage"},
|
||||
},
|
||||
},
|
||||
},
|
||||
containerName: "dbg",
|
||||
target: &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "podcopy"},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "podcopy",
|
||||
Labels: map[string]string{
|
||||
"app": "podcopy",
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"test": "test",
|
||||
},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
InitContainers: []corev1.Container{{Name: "init-container"}},
|
||||
Containers: []corev1.Container{
|
||||
{Name: "app", Image: "appimage"},
|
||||
{
|
||||
Name: "app",
|
||||
Image: "appimage",
|
||||
LivenessProbe: &corev1.Probe{},
|
||||
ReadinessProbe: &corev1.Probe{},
|
||||
StartupProbe: &corev1.Probe{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectPod: &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "podcopy"},
|
||||
Spec: corev1.PodSpec{
|
||||
ShareProcessNamespace: pointer.Bool(true),
|
||||
ShareProcessNamespace: ptr.To(true),
|
||||
InitContainers: []corev1.Container{{Name: "init-container"}},
|
||||
Containers: []corev1.Container{
|
||||
{Name: "app", Image: "appimage"},
|
||||
{
|
||||
|
@ -288,7 +548,8 @@ func TestBaselineProfile(t *testing.T) {
|
|||
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
err := (&baselineProfile{}).Apply(test.pod, test.containerName, test.target)
|
||||
applier := &baselineProfile{KeepFlags{InitContainers: true}}
|
||||
err := applier.Apply(test.pod, test.containerName, test.target)
|
||||
if (err != nil) != test.expectErr {
|
||||
t.Fatalf("expect error: %v, got error: %v", test.expectErr, (err != nil))
|
||||
}
|
||||
|
@ -343,11 +604,11 @@ func TestRestrictedProfile(t *testing.T) {
|
|||
EphemeralContainerCommon: corev1.EphemeralContainerCommon{
|
||||
Name: "dbg", Image: "dbgimage",
|
||||
SecurityContext: &corev1.SecurityContext{
|
||||
RunAsNonRoot: pointer.Bool(true),
|
||||
RunAsNonRoot: ptr.To(true),
|
||||
Capabilities: &corev1.Capabilities{
|
||||
Drop: []corev1.Capability{"ALL"},
|
||||
},
|
||||
AllowPrivilegeEscalation: pointer.Bool(false),
|
||||
AllowPrivilegeEscalation: ptr.To(false),
|
||||
SeccompProfile: &corev1.SeccompProfile{Type: "RuntimeDefault"},
|
||||
},
|
||||
},
|
||||
|
@ -357,38 +618,69 @@ func TestRestrictedProfile(t *testing.T) {
|
|||
},
|
||||
"debug by pod copy": {
|
||||
pod: &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "podcopy"},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "podcopy",
|
||||
Labels: map[string]string{
|
||||
"app": "podcopy",
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"test": "test",
|
||||
},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
InitContainers: []corev1.Container{{Name: "init-container"}},
|
||||
Containers: []corev1.Container{
|
||||
{Name: "app", Image: "appimage"},
|
||||
{
|
||||
Name: "app",
|
||||
Image: "appimage",
|
||||
LivenessProbe: &corev1.Probe{},
|
||||
ReadinessProbe: &corev1.Probe{},
|
||||
StartupProbe: &corev1.Probe{},
|
||||
},
|
||||
{Name: "dbg", Image: "dbgimage"},
|
||||
},
|
||||
},
|
||||
},
|
||||
containerName: "dbg",
|
||||
target: &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "podcopy"},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "podcopy",
|
||||
Labels: map[string]string{
|
||||
"app": "podcopy",
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"test": "test",
|
||||
},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
InitContainers: []corev1.Container{{Name: "init-container"}},
|
||||
Containers: []corev1.Container{
|
||||
{Name: "app", Image: "appimage"},
|
||||
{
|
||||
Name: "app",
|
||||
Image: "appimage",
|
||||
LivenessProbe: &corev1.Probe{},
|
||||
ReadinessProbe: &corev1.Probe{},
|
||||
StartupProbe: &corev1.Probe{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectPod: &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "podcopy"},
|
||||
Spec: corev1.PodSpec{
|
||||
ShareProcessNamespace: pointer.Bool(true),
|
||||
ShareProcessNamespace: ptr.To(true),
|
||||
InitContainers: []corev1.Container{{Name: "init-container"}},
|
||||
Containers: []corev1.Container{
|
||||
{Name: "app", Image: "appimage"},
|
||||
{
|
||||
Name: "dbg",
|
||||
Image: "dbgimage",
|
||||
SecurityContext: &corev1.SecurityContext{
|
||||
RunAsNonRoot: pointer.Bool(true),
|
||||
RunAsNonRoot: ptr.To(true),
|
||||
Capabilities: &corev1.Capabilities{
|
||||
Drop: []corev1.Capability{"ALL"},
|
||||
},
|
||||
AllowPrivilegeEscalation: pointer.Bool(false),
|
||||
AllowPrivilegeEscalation: ptr.To(false),
|
||||
SeccompProfile: &corev1.SeccompProfile{Type: "RuntimeDefault"},
|
||||
},
|
||||
},
|
||||
|
@ -408,7 +700,7 @@ func TestRestrictedProfile(t *testing.T) {
|
|||
Capabilities: &corev1.Capabilities{
|
||||
Add: []corev1.Capability{"ALL"},
|
||||
},
|
||||
AllowPrivilegeEscalation: pointer.Bool(false),
|
||||
AllowPrivilegeEscalation: ptr.To(false),
|
||||
SeccompProfile: &corev1.SeccompProfile{Type: "RuntimeDefault"},
|
||||
},
|
||||
},
|
||||
|
@ -425,11 +717,11 @@ func TestRestrictedProfile(t *testing.T) {
|
|||
Name: "dbg",
|
||||
Image: "dbgimage",
|
||||
SecurityContext: &corev1.SecurityContext{
|
||||
RunAsNonRoot: pointer.Bool(true),
|
||||
RunAsNonRoot: ptr.To(true),
|
||||
Capabilities: &corev1.Capabilities{
|
||||
Drop: []corev1.Capability{"ALL"},
|
||||
},
|
||||
AllowPrivilegeEscalation: pointer.Bool(false),
|
||||
AllowPrivilegeEscalation: ptr.To(false),
|
||||
SeccompProfile: &corev1.SeccompProfile{Type: "RuntimeDefault"},
|
||||
},
|
||||
},
|
||||
|
@ -441,7 +733,8 @@ func TestRestrictedProfile(t *testing.T) {
|
|||
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
err := (&restrictedProfile{}).Apply(test.pod, test.containerName, test.target)
|
||||
applier := &restrictedProfile{KeepFlags{InitContainers: true}}
|
||||
err := applier.Apply(test.pod, test.containerName, test.target)
|
||||
if (err != nil) != test.expectErr {
|
||||
t.Fatalf("expect error: %v, got error: %v", test.expectErr, (err != nil))
|
||||
}
|
||||
|
@ -495,7 +788,7 @@ func TestNetAdminProfile(t *testing.T) {
|
|||
Name: "dbg", Image: "dbgimage",
|
||||
SecurityContext: &corev1.SecurityContext{
|
||||
Capabilities: &corev1.Capabilities{
|
||||
Add: []corev1.Capability{"NET_ADMIN"},
|
||||
Add: []corev1.Capability{"NET_ADMIN", "NET_RAW"},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -506,26 +799,58 @@ func TestNetAdminProfile(t *testing.T) {
|
|||
{
|
||||
name: "debug by pod copy",
|
||||
pod: &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "podcopy"},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "podcopy",
|
||||
Labels: map[string]string{
|
||||
"app": "podcopy",
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"test": "test",
|
||||
},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
InitContainers: []corev1.Container{{Name: "init-container"}},
|
||||
Containers: []corev1.Container{
|
||||
{Name: "app", Image: "appimage"},
|
||||
{
|
||||
Name: "app",
|
||||
Image: "appimage",
|
||||
LivenessProbe: &corev1.Probe{},
|
||||
ReadinessProbe: &corev1.Probe{},
|
||||
StartupProbe: &corev1.Probe{},
|
||||
},
|
||||
{Name: "dbg", Image: "dbgimage"},
|
||||
},
|
||||
},
|
||||
},
|
||||
containerName: "dbg",
|
||||
target: &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "podcopy"},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "podcopy",
|
||||
Labels: map[string]string{
|
||||
"app": "podcopy",
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"test": "test",
|
||||
},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
InitContainers: []corev1.Container{{Name: "init-container"}},
|
||||
Containers: []corev1.Container{
|
||||
{Name: "app", Image: "appimage"},
|
||||
{
|
||||
Name: "app",
|
||||
Image: "appimage",
|
||||
LivenessProbe: &corev1.Probe{},
|
||||
ReadinessProbe: &corev1.Probe{},
|
||||
StartupProbe: &corev1.Probe{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectPod: &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "podcopy"},
|
||||
Spec: corev1.PodSpec{
|
||||
ShareProcessNamespace: ptr.To(true),
|
||||
InitContainers: []corev1.Container{{Name: "init-container"}},
|
||||
Containers: []corev1.Container{
|
||||
{Name: "app", Image: "appimage"},
|
||||
{
|
||||
|
@ -533,7 +858,7 @@ func TestNetAdminProfile(t *testing.T) {
|
|||
Image: "dbgimage",
|
||||
SecurityContext: &corev1.SecurityContext{
|
||||
Capabilities: &corev1.Capabilities{
|
||||
Add: []corev1.Capability{"NET_ADMIN"},
|
||||
Add: []corev1.Capability{"NET_ADMIN", "NET_RAW"},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -547,7 +872,13 @@ func TestNetAdminProfile(t *testing.T) {
|
|||
ObjectMeta: metav1.ObjectMeta{Name: "podcopy"},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{Name: "app", Image: "appimage"},
|
||||
{
|
||||
Name: "app",
|
||||
Image: "appimage",
|
||||
LivenessProbe: &corev1.Probe{},
|
||||
ReadinessProbe: &corev1.Probe{},
|
||||
StartupProbe: &corev1.Probe{},
|
||||
},
|
||||
{
|
||||
Name: "dbg",
|
||||
Image: "dbgimage",
|
||||
|
@ -565,13 +896,20 @@ func TestNetAdminProfile(t *testing.T) {
|
|||
ObjectMeta: metav1.ObjectMeta{Name: "podcopy"},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{Name: "app", Image: "appimage"},
|
||||
{
|
||||
Name: "app",
|
||||
Image: "appimage",
|
||||
LivenessProbe: &corev1.Probe{},
|
||||
ReadinessProbe: &corev1.Probe{},
|
||||
StartupProbe: &corev1.Probe{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectPod: &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "podcopy"},
|
||||
Spec: corev1.PodSpec{
|
||||
ShareProcessNamespace: ptr.To(true),
|
||||
Containers: []corev1.Container{
|
||||
{Name: "app", Image: "appimage"},
|
||||
{
|
||||
|
@ -579,7 +917,7 @@ func TestNetAdminProfile(t *testing.T) {
|
|||
Image: "dbgimage",
|
||||
SecurityContext: &corev1.SecurityContext{
|
||||
Capabilities: &corev1.Capabilities{
|
||||
Add: []corev1.Capability{"SYS_PTRACE", "NET_ADMIN"},
|
||||
Add: []corev1.Capability{"SYS_PTRACE", "NET_ADMIN", "NET_RAW"},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -610,9 +948,8 @@ func TestNetAdminProfile(t *testing.T) {
|
|||
Name: "dbg",
|
||||
Image: "dbgimage",
|
||||
SecurityContext: &corev1.SecurityContext{
|
||||
Privileged: pointer.BoolPtr(true),
|
||||
Capabilities: &corev1.Capabilities{
|
||||
Add: []corev1.Capability{"NET_ADMIN"},
|
||||
Add: []corev1.Capability{"NET_ADMIN", "NET_RAW"},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -630,7 +967,6 @@ func TestNetAdminProfile(t *testing.T) {
|
|||
Name: "dbg",
|
||||
Image: "dbgimage",
|
||||
SecurityContext: &corev1.SecurityContext{
|
||||
Privileged: pointer.BoolPtr(true),
|
||||
Capabilities: &corev1.Capabilities{
|
||||
Add: []corev1.Capability{"SYS_PTRACE"},
|
||||
},
|
||||
|
@ -652,9 +988,8 @@ func TestNetAdminProfile(t *testing.T) {
|
|||
Name: "dbg",
|
||||
Image: "dbgimage",
|
||||
SecurityContext: &corev1.SecurityContext{
|
||||
Privileged: pointer.BoolPtr(true),
|
||||
Capabilities: &corev1.Capabilities{
|
||||
Add: []corev1.Capability{"SYS_PTRACE", "NET_ADMIN"},
|
||||
Add: []corev1.Capability{"SYS_PTRACE", "NET_ADMIN", "NET_RAW"},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -666,7 +1001,286 @@ func TestNetAdminProfile(t *testing.T) {
|
|||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
err := (&netadminProfile{}).Apply(test.pod, test.containerName, test.target)
|
||||
applier := &netadminProfile{KeepFlags{InitContainers: true}}
|
||||
err := applier.Apply(test.pod, test.containerName, test.target)
|
||||
if (err == nil) != (test.expectErr == nil) || (err != nil && test.expectErr != nil && err.Error() != test.expectErr.Error()) {
|
||||
t.Fatalf("expect error: %v, got error: %v", test.expectErr, err)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if diff := cmp.Diff(test.expectPod, test.pod); diff != "" {
|
||||
t.Error("unexpected diff in generated object: (-want +got):\n", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSysAdminProfile(t *testing.T) {
|
||||
pod := &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "pod"},
|
||||
Spec: corev1.PodSpec{EphemeralContainers: []corev1.EphemeralContainer{
|
||||
{
|
||||
EphemeralContainerCommon: corev1.EphemeralContainerCommon{
|
||||
Name: "dbg", Image: "dbgimage",
|
||||
},
|
||||
},
|
||||
}},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
pod *corev1.Pod
|
||||
containerName string
|
||||
target runtime.Object
|
||||
expectPod *corev1.Pod
|
||||
expectErr error
|
||||
}{
|
||||
{
|
||||
name: "nil target",
|
||||
pod: pod,
|
||||
containerName: "dbg",
|
||||
target: nil,
|
||||
expectErr: fmt.Errorf("sysadmin profile: objects of type <nil> are not supported"),
|
||||
},
|
||||
{
|
||||
name: "debug by ephemeral container",
|
||||
pod: pod,
|
||||
containerName: "dbg",
|
||||
target: pod,
|
||||
expectPod: &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "pod"},
|
||||
Spec: corev1.PodSpec{EphemeralContainers: []corev1.EphemeralContainer{
|
||||
{
|
||||
EphemeralContainerCommon: corev1.EphemeralContainerCommon{
|
||||
Name: "dbg", Image: "dbgimage",
|
||||
SecurityContext: &corev1.SecurityContext{
|
||||
Privileged: ptr.To(true),
|
||||
},
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "debug by pod copy",
|
||||
pod: &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "podcopy",
|
||||
Labels: map[string]string{
|
||||
"app": "podcopy",
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"test": "test",
|
||||
},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
InitContainers: []corev1.Container{{Name: "init-container"}},
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "app",
|
||||
Image: "appimage",
|
||||
LivenessProbe: &corev1.Probe{},
|
||||
ReadinessProbe: &corev1.Probe{},
|
||||
StartupProbe: &corev1.Probe{},
|
||||
},
|
||||
{Name: "dbg", Image: "dbgimage"},
|
||||
},
|
||||
},
|
||||
},
|
||||
containerName: "dbg",
|
||||
target: &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "podcopy",
|
||||
Labels: map[string]string{
|
||||
"app": "podcopy",
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"test": "test",
|
||||
},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
InitContainers: []corev1.Container{{Name: "init-container"}},
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "app",
|
||||
Image: "appimage",
|
||||
LivenessProbe: &corev1.Probe{},
|
||||
ReadinessProbe: &corev1.Probe{},
|
||||
StartupProbe: &corev1.Probe{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectPod: &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "podcopy"},
|
||||
Spec: corev1.PodSpec{
|
||||
InitContainers: []corev1.Container{{Name: "init-container"}},
|
||||
Containers: []corev1.Container{
|
||||
{Name: "app", Image: "appimage"},
|
||||
{
|
||||
Name: "dbg",
|
||||
Image: "dbgimage",
|
||||
SecurityContext: &corev1.SecurityContext{
|
||||
Privileged: ptr.To(true),
|
||||
},
|
||||
},
|
||||
},
|
||||
ShareProcessNamespace: ptr.To(true),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "debug by pod copy preserve existing capability",
|
||||
pod: &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "podcopy"},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "app",
|
||||
Image: "appimage",
|
||||
LivenessProbe: &corev1.Probe{},
|
||||
ReadinessProbe: &corev1.Probe{},
|
||||
StartupProbe: &corev1.Probe{},
|
||||
},
|
||||
{
|
||||
Name: "dbg",
|
||||
Image: "dbgimage",
|
||||
SecurityContext: &corev1.SecurityContext{
|
||||
Capabilities: &corev1.Capabilities{
|
||||
Add: []corev1.Capability{"SYS_PTRACE"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
containerName: "dbg",
|
||||
target: &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "podcopy"},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "app",
|
||||
Image: "appimage",
|
||||
LivenessProbe: &corev1.Probe{},
|
||||
ReadinessProbe: &corev1.Probe{},
|
||||
StartupProbe: &corev1.Probe{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectPod: &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "podcopy"},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{Name: "app", Image: "appimage"},
|
||||
{
|
||||
Name: "dbg",
|
||||
Image: "dbgimage",
|
||||
SecurityContext: &corev1.SecurityContext{
|
||||
Privileged: ptr.To(true),
|
||||
Capabilities: &corev1.Capabilities{
|
||||
Add: []corev1.Capability{"SYS_PTRACE"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ShareProcessNamespace: ptr.To(true),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "debug by node",
|
||||
pod: &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "pod"},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{Name: "dbg", Image: "dbgimage"},
|
||||
},
|
||||
},
|
||||
},
|
||||
containerName: "dbg",
|
||||
target: testNode,
|
||||
expectPod: &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "pod"},
|
||||
Spec: corev1.PodSpec{
|
||||
HostNetwork: true,
|
||||
HostPID: true,
|
||||
HostIPC: true,
|
||||
Volumes: []corev1.Volume{
|
||||
{
|
||||
Name: "host-root",
|
||||
VolumeSource: corev1.VolumeSource{HostPath: &corev1.HostPathVolumeSource{Path: "/"}},
|
||||
},
|
||||
},
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "dbg",
|
||||
Image: "dbgimage",
|
||||
SecurityContext: &corev1.SecurityContext{
|
||||
Privileged: ptr.To(true),
|
||||
},
|
||||
VolumeMounts: []corev1.VolumeMount{{Name: "host-root", MountPath: "/host"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "debug by node preserve existing capability",
|
||||
pod: &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "pod"},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "dbg",
|
||||
Image: "dbgimage",
|
||||
SecurityContext: &corev1.SecurityContext{
|
||||
Capabilities: &corev1.Capabilities{
|
||||
Add: []corev1.Capability{"SYS_PTRACE"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
containerName: "dbg",
|
||||
target: testNode,
|
||||
expectPod: &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "pod"},
|
||||
Spec: corev1.PodSpec{
|
||||
HostNetwork: true,
|
||||
HostPID: true,
|
||||
HostIPC: true,
|
||||
Volumes: []corev1.Volume{
|
||||
{
|
||||
Name: "host-root",
|
||||
VolumeSource: corev1.VolumeSource{HostPath: &corev1.HostPathVolumeSource{Path: "/"}},
|
||||
},
|
||||
},
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "dbg",
|
||||
Image: "dbgimage",
|
||||
SecurityContext: &corev1.SecurityContext{
|
||||
Privileged: ptr.To(true),
|
||||
Capabilities: &corev1.Capabilities{
|
||||
Add: []corev1.Capability{"SYS_PTRACE"},
|
||||
},
|
||||
},
|
||||
VolumeMounts: []corev1.VolumeMount{{Name: "host-root", MountPath: "/host"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
applier := &sysadminProfile{KeepFlags{InitContainers: true}}
|
||||
err := applier.Apply(test.pod, test.containerName, test.target)
|
||||
if (err == nil) != (test.expectErr == nil) || (err != nil && test.expectErr != nil && err.Error() != test.expectErr.Error()) {
|
||||
t.Fatalf("expect error: %v, got error: %v", test.expectErr, err)
|
||||
}
|
||||
|
|
|
@ -103,7 +103,10 @@ var (
|
|||
kubectl delete pod foo --force
|
||||
|
||||
# Delete all pods
|
||||
kubectl delete pods --all`))
|
||||
kubectl delete pods --all
|
||||
|
||||
# Delete all pods only if the user confirms the deletion
|
||||
kubectl delete pods --all --interactive`))
|
||||
)
|
||||
|
||||
type DeleteOptions struct {
|
||||
|
@ -503,6 +506,10 @@ func (o *DeleteOptions) PrintObj(info *resource.Info) {
|
|||
operation = "force deleted"
|
||||
}
|
||||
|
||||
if info.Namespaced() {
|
||||
operation = fmt.Sprintf("%s from %s namespace", operation, info.Namespace)
|
||||
}
|
||||
|
||||
switch o.DryRunStrategy {
|
||||
case cmdutil.DryRunClient:
|
||||
operation = fmt.Sprintf("%s (dry run)", operation)
|
||||
|
@ -521,7 +528,7 @@ func (o *DeleteOptions) PrintObj(info *resource.Info) {
|
|||
}
|
||||
|
||||
func (o *DeleteOptions) confirmation(infos []*resource.Info) bool {
|
||||
fmt.Fprintf(o.Out, i18n.T("You are about to delete the following %d resource(s):\n"), len(infos))
|
||||
fmt.Fprintf(o.Out, i18n.T("You are about to delete the following %d resource(s):\n"), len(infos)) //nolint:errcheck
|
||||
for _, info := range infos {
|
||||
groupKind := info.Mapping.GroupVersionKind
|
||||
kindString := fmt.Sprintf("%s.%s", strings.ToLower(groupKind.Kind), groupKind.Group)
|
||||
|
@ -529,11 +536,11 @@ func (o *DeleteOptions) confirmation(infos []*resource.Info) bool {
|
|||
kindString = strings.ToLower(groupKind.Kind)
|
||||
}
|
||||
|
||||
fmt.Fprintf(o.Out, "%s/%s\n", kindString, info.Name)
|
||||
fmt.Fprintf(o.Out, "%s/%s\n", kindString, info.Name) //nolint:errcheck
|
||||
}
|
||||
fmt.Fprintf(o.Out, i18n.T("Do you want to continue?")+" (y/n): ")
|
||||
fmt.Fprint(o.Out, i18n.T("Do you want to continue?")+" (y/N): ") //nolint:errcheck
|
||||
var input string
|
||||
_, err := fmt.Fscan(o.In, &input)
|
||||
_, err := fmt.Fscanln(o.In, &input)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -160,10 +160,8 @@ func (f *DeleteFlags) AddFlags(cmd *cobra.Command) {
|
|||
if f.Raw != nil {
|
||||
cmd.Flags().StringVar(f.Raw, "raw", *f.Raw, "Raw URI to DELETE to the server. Uses the transport specified by the kubeconfig file.")
|
||||
}
|
||||
if cmdutil.InteractiveDelete.IsEnabled() {
|
||||
if f.Interactive != nil {
|
||||
cmd.Flags().BoolVarP(f.Interactive, "interactive", "i", *f.Interactive, "If true, delete resource only when user confirms. This flag is in Alpha.")
|
||||
}
|
||||
if f.Interactive != nil {
|
||||
cmd.Flags().BoolVarP(f.Interactive, "interactive", "i", *f.Interactive, "If true, delete resource only when user confirms.")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -36,7 +36,7 @@ import (
|
|||
cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/scheme"
|
||||
"k8s.io/utils/pointer"
|
||||
"k8s.io/utils/ptr"
|
||||
)
|
||||
|
||||
func fakecmd() *cobra.Command {
|
||||
|
@ -54,41 +54,37 @@ func TestDeleteFlagValidation(t *testing.T) {
|
|||
defer f.Cleanup()
|
||||
|
||||
tests := []struct {
|
||||
flags DeleteFlags
|
||||
enableAlphas []cmdutil.FeatureGate
|
||||
args [][]string
|
||||
expectedErr string
|
||||
flags DeleteFlags
|
||||
args [][]string
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
flags: DeleteFlags{
|
||||
Raw: pointer.String("test"),
|
||||
Interactive: pointer.Bool(true),
|
||||
Raw: ptr.To("test"),
|
||||
Interactive: ptr.To(true),
|
||||
},
|
||||
enableAlphas: []cmdutil.FeatureGate{cmdutil.InteractiveDelete},
|
||||
expectedErr: "--interactive can not be used with --raw",
|
||||
expectedErr: "--interactive can not be used with --raw",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
cmd := fakecmd()
|
||||
cmdtesting.WithAlphaEnvs(test.enableAlphas, t, func(t *testing.T) {
|
||||
deleteOptions, err := test.flags.ToOptions(nil, genericiooptions.NewTestIOStreamsDiscard())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error creating delete options: %s", err)
|
||||
}
|
||||
deleteOptions.Filenames = []string{"../../../testdata/redis-master-controller.yaml"}
|
||||
err = deleteOptions.Complete(f, nil, cmd)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error creating delete options: %s", err)
|
||||
}
|
||||
err = deleteOptions.Validate()
|
||||
if err == nil {
|
||||
t.Fatalf("missing expected error")
|
||||
}
|
||||
if test.expectedErr != err.Error() {
|
||||
t.Errorf("expected error %s, got %s", test.expectedErr, err)
|
||||
}
|
||||
})
|
||||
deleteOptions, err := test.flags.ToOptions(nil, genericiooptions.NewTestIOStreamsDiscard())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error creating delete options: %s", err)
|
||||
}
|
||||
deleteOptions.Filenames = []string{"../../../testdata/redis-master-controller.yaml"}
|
||||
err = deleteOptions.Complete(f, nil, cmd)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error creating delete options: %s", err)
|
||||
}
|
||||
err = deleteOptions.Validate()
|
||||
if err == nil {
|
||||
t.Fatalf("missing expected error")
|
||||
}
|
||||
if test.expectedErr != err.Error() {
|
||||
t.Errorf("expected error %s, got %s", test.expectedErr, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -310,7 +306,7 @@ func TestDeleteObject(t *testing.T) {
|
|||
|
||||
func TestPreviewResultEqualToResult(t *testing.T) {
|
||||
deleteFlags := NewDeleteCommandFlags("")
|
||||
deleteFlags.Interactive = pointer.Bool(true)
|
||||
deleteFlags.Interactive = ptr.To(true)
|
||||
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
defer tf.Cleanup()
|
||||
|
@ -362,34 +358,50 @@ func TestDeleteObjectWithInteractive(t *testing.T) {
|
|||
}),
|
||||
}
|
||||
|
||||
cmdtesting.WithAlphaEnvs([]cmdutil.FeatureGate{cmdutil.InteractiveDelete}, t, func(t *testing.T) {
|
||||
streams, in, buf, _ := genericiooptions.NewTestIOStreams()
|
||||
fmt.Fprint(in, "y")
|
||||
cmd := NewCmdDelete(tf, streams)
|
||||
cmd.Flags().Set("filename", "../../../testdata/redis-master-controller.yaml")
|
||||
cmd.Flags().Set("output", "name")
|
||||
cmd.Flags().Set("interactive", "true")
|
||||
cmd.Run(cmd, []string{})
|
||||
streams, in, buf, _ := genericiooptions.NewTestIOStreams()
|
||||
fmt.Fprint(in, "y")
|
||||
cmd := NewCmdDelete(tf, streams)
|
||||
err := cmd.Flags().Set("filename", "../../../testdata/redis-master-controller.yaml")
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error %v", err)
|
||||
}
|
||||
err = cmd.Flags().Set("output", "name")
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error %v", err)
|
||||
}
|
||||
err = cmd.Flags().Set("interactive", "true")
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error %v", err)
|
||||
}
|
||||
cmd.Run(cmd, []string{})
|
||||
|
||||
if buf.String() != "You are about to delete the following 1 resource(s):\nreplicationcontroller/redis-master\nDo you want to continue? (y/n): replicationcontroller/redis-master\n" {
|
||||
t.Errorf("unexpected output: %s", buf.String())
|
||||
}
|
||||
if buf.String() != "You are about to delete the following 1 resource(s):\nreplicationcontroller/redis-master\nDo you want to continue? (y/N): replicationcontroller/redis-master\n" {
|
||||
t.Errorf("unexpected output: %s", buf.String())
|
||||
}
|
||||
|
||||
streams, in, buf, _ = genericiooptions.NewTestIOStreams()
|
||||
fmt.Fprint(in, "n")
|
||||
cmd = NewCmdDelete(tf, streams)
|
||||
cmd.Flags().Set("filename", "../../../testdata/redis-master-controller.yaml")
|
||||
cmd.Flags().Set("output", "name")
|
||||
cmd.Flags().Set("interactive", "true")
|
||||
cmd.Run(cmd, []string{})
|
||||
streams, in, buf, _ = genericiooptions.NewTestIOStreams()
|
||||
fmt.Fprint(in, "n")
|
||||
cmd = NewCmdDelete(tf, streams)
|
||||
err = cmd.Flags().Set("filename", "../../../testdata/redis-master-controller.yaml")
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error %v", err)
|
||||
}
|
||||
err = cmd.Flags().Set("output", "name")
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error %v", err)
|
||||
}
|
||||
err = cmd.Flags().Set("interactive", "true")
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error %v", err)
|
||||
}
|
||||
cmd.Run(cmd, []string{})
|
||||
|
||||
if buf.String() != "You are about to delete the following 1 resource(s):\nreplicationcontroller/redis-master\nDo you want to continue? (y/n): deletion is cancelled\n" {
|
||||
t.Errorf("unexpected output: %s", buf.String())
|
||||
}
|
||||
if buf.String() == ": replicationcontroller/redis-master\n" {
|
||||
t.Errorf("unexpected output: %s", buf.String())
|
||||
}
|
||||
})
|
||||
if buf.String() != "You are about to delete the following 1 resource(s):\nreplicationcontroller/redis-master\nDo you want to continue? (y/N): deletion is cancelled\n" {
|
||||
t.Errorf("unexpected output: %s", buf.String())
|
||||
}
|
||||
if buf.String() == ": replicationcontroller/redis-master\n" {
|
||||
t.Errorf("unexpected output: %s", buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestGracePeriodScenarios(t *testing.T) {
|
||||
|
@ -960,3 +972,39 @@ func TestResourceErrors(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteMessageOutput(t *testing.T) {
|
||||
cmdtesting.InitTestErrorHandler(t)
|
||||
_, _, rc := cmdtesting.TestData()
|
||||
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test-specific")
|
||||
defer tf.Cleanup()
|
||||
|
||||
codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
|
||||
|
||||
tf.UnstructuredClient = &fake.RESTClient{
|
||||
NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
|
||||
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||
switch p, m := req.URL.Path, req.Method; {
|
||||
case p == "/namespaces/test-specific/replicationcontrollers/redis-master" && m == "DELETE":
|
||||
return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &rc.Items[0])}, nil
|
||||
default:
|
||||
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
|
||||
return nil, nil
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
streams, _, buf, _ := genericiooptions.NewTestIOStreams()
|
||||
cmd := NewCmdDelete(tf, streams)
|
||||
err := cmd.Flags().Set("filename", "../../../testdata/redis-master-controller.yaml")
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
cmd.Run(cmd, []string{})
|
||||
|
||||
if buf.String() != "replicationcontroller \"redis-master\" deleted from test-specific namespace\n" {
|
||||
t.Errorf("unexpected output: %s", buf.String())
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -193,7 +193,7 @@ func (o *DescribeOptions) Run() error {
|
|||
allErrs = append(allErrs, err)
|
||||
}
|
||||
|
||||
errs := sets.NewString()
|
||||
errs := sets.New[string]()
|
||||
first := true
|
||||
for _, info := range infos {
|
||||
mapping := info.ResourceMapping()
|
||||
|
|
|
@ -35,6 +35,7 @@ import (
|
|||
"k8s.io/cli-runtime/pkg/genericiooptions"
|
||||
"k8s.io/cli-runtime/pkg/resource"
|
||||
"k8s.io/client-go/dynamic"
|
||||
"k8s.io/client-go/openapi3"
|
||||
"k8s.io/klog/v2"
|
||||
"k8s.io/kubectl/pkg/cmd/apply"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
|
@ -109,7 +110,8 @@ type DiffOptions struct {
|
|||
|
||||
Concurrency int
|
||||
Selector string
|
||||
OpenAPISchema openapi.Resources
|
||||
OpenAPIGetter openapi.OpenAPIResourcesGetter
|
||||
OpenAPIV3Root openapi3.Root
|
||||
DynamicClient dynamic.Interface
|
||||
CmdNamespace string
|
||||
EnforceNamespace bool
|
||||
|
@ -159,12 +161,12 @@ func NewCmdDiff(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Co
|
|||
// command it means changes were found.
|
||||
// Thus, it should return status code greater than 1.
|
||||
cmd.SetFlagErrorFunc(func(command *cobra.Command, err error) error {
|
||||
cmdutil.CheckDiffErr(cmdutil.UsageErrorf(cmd, err.Error()))
|
||||
cmdutil.CheckDiffErr(cmdutil.UsageErrorf(cmd, "%s", err.Error()))
|
||||
return nil
|
||||
})
|
||||
|
||||
usage := "contains the configuration to diff"
|
||||
cmd.Flags().StringArray("prune-allowlist", []string{}, "Overwrite the default whitelist with <group/version/kind> for --prune")
|
||||
cmd.Flags().StringArray("prune-allowlist", []string{}, "Overwrite the default allowlist with <group/version/kind> for --prune")
|
||||
cmd.Flags().Bool("prune", false, "Include resources that would be deleted by pruning. Can be used with -l and default shows all resources would be pruned")
|
||||
cmd.Flags().BoolVar(&options.ShowManagedFields, "show-managed-fields", options.ShowManagedFields, "If true, include managed fields in the diff.")
|
||||
cmd.Flags().IntVar(&options.Concurrency, "concurrency", 1, "Number of objects to process in parallel when diffing against the live version. Larger number = faster, but more memory, I/O and CPU over that shorter period of time.")
|
||||
|
@ -323,7 +325,8 @@ type InfoObject struct {
|
|||
LocalObj runtime.Object
|
||||
Info *resource.Info
|
||||
Encoder runtime.Encoder
|
||||
OpenAPI openapi.Resources
|
||||
OpenAPIGetter openapi.OpenAPIResourcesGetter
|
||||
OpenAPIV3Root openapi3.Root
|
||||
Force bool
|
||||
ServerSideApply bool
|
||||
FieldManager string
|
||||
|
@ -395,7 +398,8 @@ func (obj InfoObject) Merged() (runtime.Object, error) {
|
|||
Helper: helper,
|
||||
Overwrite: true,
|
||||
BackOff: clockwork.NewRealClock(),
|
||||
OpenapiSchema: obj.OpenAPI,
|
||||
OpenAPIGetter: obj.OpenAPIGetter,
|
||||
OpenAPIV3Root: obj.OpenAPIV3Root,
|
||||
ResourceVersion: resourceVersion,
|
||||
}
|
||||
|
||||
|
@ -637,9 +641,14 @@ func (o *DiffOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []str
|
|||
}
|
||||
|
||||
if !o.ServerSideApply {
|
||||
o.OpenAPISchema, err = f.OpenAPISchema()
|
||||
if err != nil {
|
||||
return err
|
||||
o.OpenAPIGetter = f
|
||||
if !cmdutil.OpenAPIV3Patch.IsDisabled() {
|
||||
openAPIV3Client, err := f.OpenAPIV3Client()
|
||||
if err == nil {
|
||||
o.OpenAPIV3Root = openapi3.NewRoot(openAPIV3Client)
|
||||
} else {
|
||||
klog.V(4).Infof("warning: OpenAPI V3 Patch is enabled but is unable to be loaded. Will fall back to OpenAPI V2")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -721,7 +730,8 @@ func (o *DiffOptions) Run() error {
|
|||
LocalObj: local,
|
||||
Info: info,
|
||||
Encoder: scheme.DefaultJSONEncoder(),
|
||||
OpenAPI: o.OpenAPISchema,
|
||||
OpenAPIGetter: o.OpenAPIGetter,
|
||||
OpenAPIV3Root: o.OpenAPIV3Root,
|
||||
Force: force,
|
||||
ServerSideApply: o.ServerSideApply,
|
||||
FieldManager: o.FieldManager,
|
||||
|
|
|
@ -20,7 +20,6 @@ import (
|
|||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
@ -65,6 +64,7 @@ func TestDiffProgram(t *testing.T) {
|
|||
externalDiffCommands := [3]string{"diff", "diff -ruN", "diff --report-identical-files"}
|
||||
|
||||
t.Setenv("LANG", "C")
|
||||
t.Setenv("LANGUAGE", "en_US")
|
||||
|
||||
for i, c := range externalDiffCommands {
|
||||
t.Setenv("KUBECTL_EXTERNAL_DIFF", c)
|
||||
|
@ -130,7 +130,7 @@ func TestDiffVersion(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fcontent, err := os.ReadFile(path.Join(diff.Dir.Name, obj.Name()))
|
||||
fcontent, err := os.ReadFile(filepath.Join(diff.Dir.Name, obj.Name()))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -201,7 +201,7 @@ func TestDiffer(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fcontent, err := os.ReadFile(path.Join(diff.From.Dir.Name, obj.Name()))
|
||||
fcontent, err := os.ReadFile(filepath.Join(diff.From.Dir.Name, obj.Name()))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -210,7 +210,7 @@ func TestDiffer(t *testing.T) {
|
|||
t.Fatalf("File has %q, expected %q", string(fcontent), econtent)
|
||||
}
|
||||
|
||||
fcontent, err = os.ReadFile(path.Join(diff.To.Dir.Name, obj.Name()))
|
||||
fcontent, err = os.ReadFile(filepath.Join(diff.To.Dir.Name, obj.Name()))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -286,12 +286,12 @@ metadata:
|
|||
t.Fatal(err)
|
||||
}
|
||||
|
||||
actualFromContent, _ := os.ReadFile(path.Join(diff.From.Dir.Name, obj.Name()))
|
||||
actualFromContent, _ := os.ReadFile(filepath.Join(diff.From.Dir.Name, obj.Name()))
|
||||
if string(actualFromContent) != tc.expectedFromContent {
|
||||
t.Fatalf("File has %q, expected %q", string(actualFromContent), tc.expectedFromContent)
|
||||
}
|
||||
|
||||
actualToContent, _ := os.ReadFile(path.Join(diff.To.Dir.Name, obj.Name()))
|
||||
actualToContent, _ := os.ReadFile(filepath.Join(diff.To.Dir.Name, obj.Name()))
|
||||
if string(actualToContent) != tc.expectedToContent {
|
||||
t.Fatalf("File has %q, expected %q", string(actualToContent), tc.expectedToContent)
|
||||
}
|
||||
|
|
|
@ -31,6 +31,7 @@ import (
|
|||
"k8s.io/cli-runtime/pkg/genericiooptions"
|
||||
"k8s.io/cli-runtime/pkg/printers"
|
||||
"k8s.io/cli-runtime/pkg/resource"
|
||||
"k8s.io/klog/v2"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/drain"
|
||||
"k8s.io/kubectl/pkg/scheme"
|
||||
|
@ -156,24 +157,53 @@ func NewDrainCmdOptions(f cmdutil.Factory, ioStreams genericiooptions.IOStreams)
|
|||
ChunkSize: cmdutil.DefaultChunkSize,
|
||||
},
|
||||
}
|
||||
o.drainer.OnPodDeletedOrEvicted = o.onPodDeletedOrEvicted
|
||||
o.drainer.OnPodDeletionOrEvictionFinished = o.onPodDeletionOrEvictionFinished
|
||||
o.drainer.OnPodDeletionOrEvictionStarted = o.onPodDeletionOrEvictionStarted
|
||||
return o
|
||||
}
|
||||
|
||||
// onPodDeletedOrEvicted is called by drain.Helper, when the pod has been deleted or evicted
|
||||
func (o *DrainCmdOptions) onPodDeletedOrEvicted(pod *corev1.Pod, usingEviction bool) {
|
||||
// onPodDeletionOrEvictionFinished is called by drain.Helper, when eviction/deletetion of the pod is finished
|
||||
func (o *DrainCmdOptions) onPodDeletionOrEvictionFinished(pod *corev1.Pod, usingEviction bool, err error) {
|
||||
var verbStr string
|
||||
if usingEviction {
|
||||
verbStr = "evicted"
|
||||
if err != nil {
|
||||
verbStr = "eviction failed"
|
||||
} else {
|
||||
verbStr = "evicted"
|
||||
}
|
||||
} else {
|
||||
verbStr = "deleted"
|
||||
if err != nil {
|
||||
verbStr = "deletion failed"
|
||||
} else {
|
||||
verbStr = "deleted"
|
||||
}
|
||||
}
|
||||
printObj, err := o.ToPrinter(verbStr)
|
||||
if err != nil {
|
||||
fmt.Fprintf(o.ErrOut, "error building printer: %v\n", err)
|
||||
fmt.Fprintf(o.Out, "pod %s/%s %s\n", pod.Namespace, pod.Name, verbStr)
|
||||
} else {
|
||||
printObj(pod, o.Out)
|
||||
_ = printObj(pod, o.Out)
|
||||
}
|
||||
}
|
||||
|
||||
// onPodDeletionOrEvictionStarted is called by drain.Helper, when eviction/deletion of the pod is started
|
||||
func (o *DrainCmdOptions) onPodDeletionOrEvictionStarted(pod *corev1.Pod, usingEviction bool) {
|
||||
if !klog.V(2).Enabled() {
|
||||
return
|
||||
}
|
||||
var verbStr string
|
||||
if usingEviction {
|
||||
verbStr = "eviction started"
|
||||
} else {
|
||||
verbStr = "deletion started"
|
||||
}
|
||||
printObj, err := o.ToPrinter(verbStr)
|
||||
if err != nil {
|
||||
fmt.Fprintf(o.ErrOut, "error building printer: %v\n", err)
|
||||
fmt.Fprintf(o.Out, "pod %s/%s %s\n", pod.Namespace, pod.Name, verbStr)
|
||||
} else {
|
||||
_ = printObj(pod, o.Out)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -194,8 +224,6 @@ func NewCmdDrain(f cmdutil.Factory, ioStreams genericiooptions.IOStreams) *cobra
|
|||
}
|
||||
cmd.Flags().BoolVar(&o.drainer.Force, "force", o.drainer.Force, "Continue even if there are pods that do not declare a controller.")
|
||||
cmd.Flags().BoolVar(&o.drainer.IgnoreAllDaemonSets, "ignore-daemonsets", o.drainer.IgnoreAllDaemonSets, "Ignore DaemonSet-managed pods.")
|
||||
cmd.Flags().BoolVar(&o.drainer.DeleteEmptyDirData, "delete-local-data", o.drainer.DeleteEmptyDirData, "Continue even if there are pods using emptyDir (local data that will be deleted when the node is drained).")
|
||||
cmd.Flags().MarkDeprecated("delete-local-data", "This option is deprecated and will be deleted. Use --delete-emptydir-data.")
|
||||
cmd.Flags().BoolVar(&o.drainer.DeleteEmptyDirData, "delete-emptydir-data", o.drainer.DeleteEmptyDirData, "Continue even if there are pods using emptyDir (local data that will be deleted when the node is drained).")
|
||||
cmd.Flags().IntVar(&o.drainer.GracePeriodSeconds, "grace-period", o.drainer.GracePeriodSeconds, "Period of time in seconds given to each pod to terminate gracefully. If negative, the default value specified in the pod will be used.")
|
||||
cmd.Flags().DurationVar(&o.drainer.Timeout, "timeout", o.drainer.Timeout, "The length of time to wait before giving up, zero means infinite")
|
||||
|
@ -215,7 +243,7 @@ func (o *DrainCmdOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args [
|
|||
var err error
|
||||
|
||||
if len(args) == 0 && !cmd.Flags().Changed("selector") {
|
||||
return cmdutil.UsageErrorf(cmd, fmt.Sprintf("USAGE: %s [flags]", cmd.Use))
|
||||
return cmdutil.UsageErrorf(cmd, "USAGE: %s [flags]", cmd.Use)
|
||||
}
|
||||
if len(args) > 0 && len(o.drainer.Selector) > 0 {
|
||||
return cmdutil.UsageErrorf(cmd, "error: cannot specify both a node name and a --selector option")
|
||||
|
@ -298,7 +326,7 @@ func (o *DrainCmdOptions) RunDrain() error {
|
|||
return err
|
||||
}
|
||||
|
||||
drainedNodes := sets.NewString()
|
||||
drainedNodes := sets.New[string]()
|
||||
var fatal []error
|
||||
|
||||
remainingNodes := []string{}
|
||||
|
@ -313,7 +341,7 @@ func (o *DrainCmdOptions) RunDrain() error {
|
|||
|
||||
printObj(info.Object, o.Out)
|
||||
} else {
|
||||
fmt.Fprintf(o.ErrOut, "error: unable to drain node %q due to error:%s, continuing command...\n", info.Name, err)
|
||||
fmt.Fprintf(o.ErrOut, "error: unable to drain node %q due to error: %s, continuing command...\n", info.Name, err)
|
||||
|
||||
if !drainedNodes.Has(info.Name) {
|
||||
fatal = append(fatal, err)
|
||||
|
|
|
@ -42,7 +42,7 @@ import (
|
|||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/drain"
|
||||
"k8s.io/kubectl/pkg/scheme"
|
||||
utilpointer "k8s.io/utils/pointer"
|
||||
"k8s.io/utils/ptr"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -277,8 +277,8 @@ func TestDrain(t *testing.T) {
|
|||
Kind: "ReplicationController",
|
||||
Name: "rc",
|
||||
UID: "123",
|
||||
BlockOwnerDeletion: utilpointer.BoolPtr(true),
|
||||
Controller: utilpointer.BoolPtr(true),
|
||||
BlockOwnerDeletion: ptr.To(true),
|
||||
Controller: ptr.To(true),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -309,8 +309,8 @@ func TestDrain(t *testing.T) {
|
|||
APIVersion: "apps/v1",
|
||||
Kind: "DaemonSet",
|
||||
Name: "ds",
|
||||
BlockOwnerDeletion: utilpointer.BoolPtr(true),
|
||||
Controller: utilpointer.BoolPtr(true),
|
||||
BlockOwnerDeletion: ptr.To(true),
|
||||
Controller: ptr.To(true),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -330,8 +330,8 @@ func TestDrain(t *testing.T) {
|
|||
APIVersion: "apps/v1",
|
||||
Kind: "DaemonSet",
|
||||
Name: "ds",
|
||||
BlockOwnerDeletion: utilpointer.BoolPtr(true),
|
||||
Controller: utilpointer.BoolPtr(true),
|
||||
BlockOwnerDeletion: ptr.To(true),
|
||||
Controller: ptr.To(true),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -354,8 +354,8 @@ func TestDrain(t *testing.T) {
|
|||
APIVersion: "apps/v1",
|
||||
Kind: "DaemonSet",
|
||||
Name: "ds",
|
||||
BlockOwnerDeletion: utilpointer.BoolPtr(true),
|
||||
Controller: utilpointer.BoolPtr(true),
|
||||
BlockOwnerDeletion: ptr.To(true),
|
||||
Controller: ptr.To(true),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -404,8 +404,8 @@ func TestDrain(t *testing.T) {
|
|||
APIVersion: "v1",
|
||||
Kind: "Job",
|
||||
Name: "job",
|
||||
BlockOwnerDeletion: utilpointer.BoolPtr(true),
|
||||
Controller: utilpointer.BoolPtr(true),
|
||||
BlockOwnerDeletion: ptr.To(true),
|
||||
Controller: ptr.To(true),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -431,8 +431,8 @@ func TestDrain(t *testing.T) {
|
|||
APIVersion: "v1",
|
||||
Kind: "Job",
|
||||
Name: "job",
|
||||
BlockOwnerDeletion: utilpointer.BoolPtr(true),
|
||||
Controller: utilpointer.BoolPtr(true),
|
||||
BlockOwnerDeletion: ptr.To(true),
|
||||
Controller: ptr.To(true),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -473,8 +473,8 @@ func TestDrain(t *testing.T) {
|
|||
APIVersion: "v1",
|
||||
Kind: "ReplicaSet",
|
||||
Name: "rs",
|
||||
BlockOwnerDeletion: utilpointer.BoolPtr(true),
|
||||
Controller: utilpointer.BoolPtr(true),
|
||||
BlockOwnerDeletion: ptr.To(true),
|
||||
Controller: ptr.To(true),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -635,17 +635,6 @@ func TestDrain(t *testing.T) {
|
|||
expectDelete: true,
|
||||
expectOutputToContain: "node/node drained",
|
||||
},
|
||||
{
|
||||
description: "Ensure compatibility for --delete-local-data until fully deprecated",
|
||||
node: node,
|
||||
expected: cordonedNode,
|
||||
pods: []corev1.Pod{jobPod},
|
||||
rcs: []corev1.ReplicationController{rc},
|
||||
args: []string{"node", "--force", "--delete-local-data=true"},
|
||||
expectFatal: false,
|
||||
expectDelete: true,
|
||||
expectOutputToContain: "node/node drained",
|
||||
},
|
||||
{
|
||||
description: "Job-managed terminated pod",
|
||||
node: node,
|
||||
|
|
|
@ -103,6 +103,6 @@ func NewCmdEdit(f cmdutil.Factory, ioStreams genericiooptions.IOStreams) *cobra.
|
|||
"Defaults to the line ending native to your platform.")
|
||||
cmdutil.AddFieldManagerFlagVar(cmd, &o.FieldManager, "kubectl-edit")
|
||||
cmdutil.AddApplyAnnotationVarFlags(cmd, &o.ApplyAnnotation)
|
||||
cmdutil.AddSubresourceFlags(cmd, &o.Subresource, "If specified, edit will operate on the subresource of the requested object.", editor.SupportedSubresources...)
|
||||
cmdutil.AddSubresourceFlags(cmd, &o.Subresource, "If specified, edit will operate on the subresource of the requested object.")
|
||||
return cmd
|
||||
}
|
||||
|
|
|
@ -30,8 +30,7 @@ import (
|
|||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
yaml "gopkg.in/yaml.v2"
|
||||
|
||||
yaml "go.yaml.in/yaml/v2"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/cli-runtime/pkg/genericiooptions"
|
||||
|
@ -173,7 +172,7 @@ func TestEdit(t *testing.T) {
|
|||
t.Setenv("KUBE_EDITOR", "testdata/test_editor.sh")
|
||||
t.Setenv("KUBE_EDITOR_CALLBACK", server.URL+"/callback")
|
||||
|
||||
testcases := sets.NewString()
|
||||
testcases := sets.New[string]()
|
||||
filepath.Walk("testdata", func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -195,7 +194,7 @@ func TestEdit(t *testing.T) {
|
|||
t.Fatalf("Error locating edit testcases")
|
||||
}
|
||||
|
||||
for _, testcaseName := range testcases.List() {
|
||||
for _, testcaseName := range testcases.UnsortedList() {
|
||||
t.Run(testcaseName, func(t *testing.T) {
|
||||
i = 0
|
||||
name = testcaseName
|
||||
|
|
|
@ -25,7 +25,7 @@ import (
|
|||
"os"
|
||||
"strings"
|
||||
|
||||
yaml "gopkg.in/yaml.v2"
|
||||
yaml "go.yaml.in/yaml/v2"
|
||||
)
|
||||
|
||||
type EditTestCase struct {
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"kind": "Service",
|
||||
"metadata": {
|
||||
"annotations": {
|
||||
"kubectl.kubernetes.io/last-applied-configuration": "{\"kind\":\"Service\",\"apiVersion\":\"v1\",\"metadata\":{\"name\":\"svc1\",\"creationTimestamp\":null,\"labels\":{\"app\":\"svc1\"}},\"spec\":{\"ports\":[{\"name\":\"80\",\"protocol\":\"TCP\",\"port\":80,\"targetPort\":80}],\"selector\":{\"app\":\"svc1\"},\"type\":\"ClusterIP\"},\"status\":{\"loadBalancer\":{}}}\n"
|
||||
"kubectl.kubernetes.io/last-applied-configuration": "{\"kind\":\"Service\",\"apiVersion\":\"v1\",\"metadata\":{\"name\":\"svc1\",\"labels\":{\"app\":\"svc1\"}},\"spec\":{\"ports\":[{\"name\":\"80\",\"protocol\":\"TCP\",\"port\":80,\"targetPort\":80}],\"selector\":{\"app\":\"svc1\"},\"type\":\"ClusterIP\"},\"status\":{\"loadBalancer\":{}}}\n"
|
||||
},
|
||||
"creationTimestamp": "2017-02-27T19:40:53Z",
|
||||
"labels": {
|
||||
|
|
|
@ -7,7 +7,7 @@ kind: Service
|
|||
metadata:
|
||||
annotations:
|
||||
kubectl.kubernetes.io/last-applied-configuration: |
|
||||
{"kind":"Service","apiVersion":"v1","metadata":{"name":"svc1","creationTimestamp":null,"labels":{"app":"svc1"}},"spec":{"ports":[{"name":"80","protocol":"TCP","port":80,"targetPort":80}],"selector":{"app":"svc1"},"type":"ClusterIP"},"status":{"loadBalancer":{}}}
|
||||
{"kind":"Service","apiVersion":"v1","metadata":{"name":"svc1","labels":{"app":"svc1"}},"spec":{"ports":[{"name":"80","protocol":"TCP","port":80,"targetPort":80}],"selector":{"app":"svc1"},"type":"ClusterIP"},"status":{"loadBalancer":{}}}
|
||||
creationTimestamp: "2017-02-27T19:40:53Z"
|
||||
labels:
|
||||
app: svc1
|
||||
|
|
|
@ -7,7 +7,7 @@ kind: Service
|
|||
metadata:
|
||||
annotations:
|
||||
kubectl.kubernetes.io/last-applied-configuration: |
|
||||
{"kind":"Service","apiVersion":"v1","metadata":{"name":"svc1","creationTimestamp":null,"labels":{"app":"svc1"}},"spec":{"ports":[{"name":"80","protocol":"TCP","port":80,"targetPort":80}],"selector":{"app":"svc1"},"type":"ClusterIP"},"status":{"loadBalancer":{}}}
|
||||
{"kind":"Service","apiVersion":"v1","metadata":{"name":"svc1","labels":{"app":"svc1"}},"spec":{"ports":[{"name":"80","protocol":"TCP","port":80,"targetPort":80}],"selector":{"app":"svc1"},"type":"ClusterIP"},"status":{"loadBalancer":{}}}
|
||||
creationTimestamp: "2017-02-27T19:40:53Z"
|
||||
labels:
|
||||
app: svc1
|
||||
|
|
|
@ -33,7 +33,6 @@
|
|||
},
|
||||
"template": {
|
||||
"metadata": {
|
||||
"creationTimestamp": null,
|
||||
"labels": {
|
||||
"app": "nginx"
|
||||
}
|
||||
|
|
|
@ -29,7 +29,6 @@ spec:
|
|||
type: RollingUpdate
|
||||
template:
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
|
|
|
@ -29,7 +29,6 @@ spec:
|
|||
type: RollingUpdate
|
||||
template:
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
|
|
|
@ -33,7 +33,6 @@
|
|||
},
|
||||
"template": {
|
||||
"metadata": {
|
||||
"creationTimestamp": null,
|
||||
"labels": {
|
||||
"app": "nginx"
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"kind": "Service",
|
||||
"metadata": {
|
||||
"annotations": {
|
||||
"kubectl.kubernetes.io/last-applied-configuration": "{\"kind\":\"Service\",\"apiVersion\":\"v1\",\"metadata\":{\"name\":\"svc1\",\"creationTimestamp\":null,\"labels\":{\"app\":\"svc1\"}},\"spec\":{\"ports\":[{\"name\":\"80\",\"protocol\":\"TCP\",\"port\":80,\"targetPort\":80}],\"selector\":{\"app\":\"svc1\"},\"type\":\"ClusterIP\"},\"status\":{\"loadBalancer\":{}}}\n"
|
||||
"kubectl.kubernetes.io/last-applied-configuration": "{\"kind\":\"Service\",\"apiVersion\":\"v1\",\"metadata\":{\"name\":\"svc1\",\"labels\":{\"app\":\"svc1\"}},\"spec\":{\"ports\":[{\"name\":\"80\",\"protocol\":\"TCP\",\"port\":80,\"targetPort\":80}],\"selector\":{\"app\":\"svc1\"},\"type\":\"ClusterIP\"},\"status\":{\"loadBalancer\":{}}}\n"
|
||||
},
|
||||
"creationTimestamp": "2017-02-27T19:40:53Z",
|
||||
"labels": {
|
||||
|
|
|
@ -7,7 +7,7 @@ kind: Service
|
|||
metadata:
|
||||
annotations:
|
||||
kubectl.kubernetes.io/last-applied-configuration: |
|
||||
{"kind":"Service","apiVersion":"v1","metadata":{"name":"svc1","creationTimestamp":null,"labels":{"app":"svc1"}},"spec":{"ports":[{"name":"80","protocol":"TCP","port":80,"targetPort":80}],"selector":{"app":"svc1"},"type":"ClusterIP"},"status":{"loadBalancer":{}}}
|
||||
{"kind":"Service","apiVersion":"v1","metadata":{"name":"svc1","labels":{"app":"svc1"}},"spec":{"ports":[{"name":"80","protocol":"TCP","port":80,"targetPort":80}],"selector":{"app":"svc1"},"type":"ClusterIP"},"status":{"loadBalancer":{}}}
|
||||
creationTimestamp: "2017-02-27T19:40:53Z"
|
||||
labels:
|
||||
app: svc1
|
||||
|
|
|
@ -7,7 +7,7 @@ kind: Service
|
|||
metadata:
|
||||
annotations:
|
||||
kubectl.kubernetes.io/last-applied-configuration: |
|
||||
{"kind":"Service","apiVersion":"v1","metadata":{"name":"svc1","creationTimestamp":null,"labels":{"app":"svc1"}},"spec":{"ports":[{"name":"80","protocol":"TCP","port":80,"targetPort":80}],"selector":{"app":"svc1"},"type":"ClusterIP"},"status":{"loadBalancer":{}}}
|
||||
{"kind":"Service","apiVersion":"v1","metadata":{"name":"svc1","labels":{"app":"svc1"}},"spec":{"ports":[{"name":"80","protocol":"TCP","port":80,"targetPort":80}],"selector":{"app":"svc1"},"type":"ClusterIP"},"status":{"loadBalancer":{}}}
|
||||
creationTimestamp: "2017-02-27T19:40:53Z"
|
||||
labels:
|
||||
app: svc1
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"kind": "Service",
|
||||
"metadata": {
|
||||
"annotations": {
|
||||
"kubectl.kubernetes.io/last-applied-configuration": "{\"kind\":\"Service\",\"apiVersion\":\"v1\",\"metadata\":{\"name\":\"svc1\",\"creationTimestamp\":null,\"labels\":{\"app\":\"svc1\"}},\"spec\":{\"ports\":[{\"name\":\"80\",\"protocol\":\"TCP\",\"port\":80,\"targetPort\":80}],\"selector\":{\"app\":\"svc1\"},\"type\":\"ClusterIP\"},\"status\":{\"loadBalancer\":{}}}\n"
|
||||
"kubectl.kubernetes.io/last-applied-configuration": "{\"kind\":\"Service\",\"apiVersion\":\"v1\",\"metadata\":{\"name\":\"svc1\",\"labels\":{\"app\":\"svc1\"}},\"spec\":{\"ports\":[{\"name\":\"80\",\"protocol\":\"TCP\",\"port\":80,\"targetPort\":80}],\"selector\":{\"app\":\"svc1\"},\"type\":\"ClusterIP\"},\"status\":{\"loadBalancer\":{}}}\n"
|
||||
},
|
||||
"creationTimestamp": "2017-02-27T19:40:53Z",
|
||||
"labels": {
|
||||
|
|
|
@ -7,7 +7,7 @@ kind: Service
|
|||
metadata:
|
||||
annotations:
|
||||
kubectl.kubernetes.io/last-applied-configuration: |
|
||||
{"kind":"Service","apiVersion":"v1","metadata":{"name":"svc1","creationTimestamp":null,"labels":{"app":"svc1"}},"spec":{"ports":[{"name":"80","protocol":"TCP","port":80,"targetPort":80}],"selector":{"app":"svc1"},"type":"ClusterIP"},"status":{"loadBalancer":{}}}
|
||||
{"kind":"Service","apiVersion":"v1","metadata":{"name":"svc1","labels":{"app":"svc1"}},"spec":{"ports":[{"name":"80","protocol":"TCP","port":80,"targetPort":80}],"selector":{"app":"svc1"},"type":"ClusterIP"},"status":{"loadBalancer":{}}}
|
||||
creationTimestamp: "2017-02-27T19:40:53Z"
|
||||
labels:
|
||||
app: svc1
|
||||
|
|
|
@ -7,7 +7,7 @@ kind: Service
|
|||
metadata:
|
||||
annotations:
|
||||
kubectl.kubernetes.io/last-applied-configuration: |
|
||||
{"kind":"Service","apiVersion":"v1","metadata":{"name":"svc1","creationTimestamp":null,"labels":{"app":"svc1"}},"spec":{"ports":[{"name":"80","protocol":"TCP","port":80,"targetPort":80}],"selector":{"app":"svc1"},"type":"ClusterIP"},"status":{"loadBalancer":{}}}
|
||||
{"kind":"Service","apiVersion":"v1","metadata":{"name":"svc1","labels":{"app":"svc1"}},"spec":{"ports":[{"name":"80","protocol":"TCP","port":80,"targetPort":80}],"selector":{"app":"svc1"},"type":"ClusterIP"},"status":{"loadBalancer":{}}}
|
||||
creationTimestamp: "2017-02-27T19:40:53Z"
|
||||
labels:
|
||||
app: svc1
|
||||
|
|
|
@ -18,11 +18,12 @@ package events
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestPrintObj(t *testing.T) {
|
||||
|
@ -50,7 +51,7 @@ func TestPrintObj(t *testing.T) {
|
|||
},
|
||||
Type: corev1.EventTypeNormal,
|
||||
Reason: "ScalingReplicaSet",
|
||||
Message: "Scaled up replica set bar-002 to 1",
|
||||
Message: "Scaled up replica set bar-002 from 0 to 1",
|
||||
ReportingController: "deployment-controller",
|
||||
EventTime: metav1.NewMicroTime(time.Now().Add(-20 * time.Minute)),
|
||||
Series: &corev1.EventSeries{
|
||||
|
@ -59,7 +60,7 @@ func TestPrintObj(t *testing.T) {
|
|||
},
|
||||
},
|
||||
expected: `LAST SEEN TYPE REASON OBJECT MESSAGE
|
||||
12m (x3 over 20m) Normal ScalingReplicaSet Deployment/bar Scaled up replica set bar-002 to 1
|
||||
12m (x3 over 20m) Normal ScalingReplicaSet Deployment/bar Scaled up replica set bar-002 from 0 to 1
|
||||
`,
|
||||
},
|
||||
{
|
||||
|
@ -83,7 +84,7 @@ func TestPrintObj(t *testing.T) {
|
|||
},
|
||||
Type: corev1.EventTypeNormal,
|
||||
Reason: "ScalingReplicaSet",
|
||||
Message: "Scaled up replica set bar-002 to 1",
|
||||
Message: "Scaled up replica set bar-002 from 0 to 1",
|
||||
ReportingController: "deployment-controller",
|
||||
EventTime: metav1.NewMicroTime(time.Now().Add(-20 * time.Minute)),
|
||||
Series: &corev1.EventSeries{
|
||||
|
@ -105,7 +106,7 @@ func TestPrintObj(t *testing.T) {
|
|||
},
|
||||
Type: corev1.EventTypeNormal,
|
||||
Reason: "ScalingReplicaSet",
|
||||
Message: "Scaled up replica set bar-002 to 1",
|
||||
Message: "Scaled up replica set bar-002 from 0 to 1",
|
||||
ReportingController: "deployment-controller",
|
||||
EventTime: metav1.NewMicroTime(time.Now().Add(-15 * time.Minute)),
|
||||
Series: &corev1.EventSeries{
|
||||
|
@ -116,8 +117,8 @@ func TestPrintObj(t *testing.T) {
|
|||
},
|
||||
},
|
||||
expected: `NAMESPACE LAST SEEN TYPE REASON OBJECT MESSAGE
|
||||
foo 12m (x3 over 20m) Normal ScalingReplicaSet Deployment/bar Scaled up replica set bar-002 to 1
|
||||
bar 11m (x3 over 15m) Normal ScalingReplicaSet Deployment/bar2 Scaled up replica set bar-002 to 1
|
||||
foo 12m (x3 over 20m) Normal ScalingReplicaSet Deployment/bar Scaled up replica set bar-002 from 0 to 1
|
||||
bar 11m (x3 over 15m) Normal ScalingReplicaSet Deployment/bar2 Scaled up replica set bar-002 from 0 to 1
|
||||
`,
|
||||
},
|
||||
{
|
||||
|
@ -139,7 +140,7 @@ bar 11m (x3 over 15m) Normal ScalingReplicaSet Deployment/bar2 Scaled up replica
|
|||
},
|
||||
Type: corev1.EventTypeNormal,
|
||||
Reason: "ScalingReplicaSet",
|
||||
Message: "Scaled up replica set bar-002 to 1",
|
||||
Message: "Scaled up replica set bar-002 from 0 to 1",
|
||||
ReportingController: "deployment-controller",
|
||||
EventTime: metav1.NewMicroTime(time.Now().Add(-20 * time.Minute)),
|
||||
Series: &corev1.EventSeries{
|
||||
|
@ -147,7 +148,7 @@ bar 11m (x3 over 15m) Normal ScalingReplicaSet Deployment/bar2 Scaled up replica
|
|||
LastObservedTime: metav1.NewMicroTime(time.Now().Add(-12 * time.Minute)),
|
||||
},
|
||||
},
|
||||
expected: "12m (x3 over 20m) Normal ScalingReplicaSet Deployment/bar Scaled up replica set bar-002 to 1\n",
|
||||
expected: "12m (x3 over 20m) Normal ScalingReplicaSet Deployment/bar Scaled up replica set bar-002 from 0 to 1\n",
|
||||
},
|
||||
{
|
||||
printer: EventPrinter{
|
||||
|
@ -168,7 +169,7 @@ bar 11m (x3 over 15m) Normal ScalingReplicaSet Deployment/bar2 Scaled up replica
|
|||
},
|
||||
Type: corev1.EventTypeNormal,
|
||||
Reason: "ScalingReplicaSet",
|
||||
Message: "Scaled up replica set bar-002 to 1",
|
||||
Message: "Scaled up replica set bar-002 from 0 to 1",
|
||||
ReportingController: "deployment-controller",
|
||||
EventTime: metav1.NewMicroTime(time.Now().Add(-20 * time.Minute)),
|
||||
Series: &corev1.EventSeries{
|
||||
|
@ -177,7 +178,7 @@ bar 11m (x3 over 15m) Normal ScalingReplicaSet Deployment/bar2 Scaled up replica
|
|||
},
|
||||
},
|
||||
expected: `NAMESPACE LAST SEEN TYPE REASON OBJECT MESSAGE
|
||||
foo 12m (x3 over 20m) Normal ScalingReplicaSet Deployment/bar Scaled up replica set bar-002 to 1
|
||||
foo 12m (x3 over 20m) Normal ScalingReplicaSet Deployment/bar Scaled up replica set bar-002 from 0 to 1
|
||||
`,
|
||||
},
|
||||
{
|
||||
|
@ -199,7 +200,7 @@ foo 12m (x3 over 20m) Normal ScalingReplicaSet Deployment/bar Scaled up replica
|
|||
},
|
||||
Type: corev1.EventTypeNormal,
|
||||
Reason: "ScalingReplicaSet",
|
||||
Message: "Scaled up replica set bar-002 to 1",
|
||||
Message: "Scaled up replica set bar-002 from 0 to 1",
|
||||
ReportingController: "deployment-controller",
|
||||
EventTime: metav1.NewMicroTime(time.Now().Add(-20 * time.Minute)),
|
||||
Series: &corev1.EventSeries{
|
||||
|
@ -207,7 +208,7 @@ foo 12m (x3 over 20m) Normal ScalingReplicaSet Deployment/bar Scaled up replica
|
|||
LastObservedTime: metav1.NewMicroTime(time.Now().Add(-12 * time.Minute)),
|
||||
},
|
||||
},
|
||||
expected: `foo 12m (x3 over 20m) Normal ScalingReplicaSet Deployment/bar Scaled up replica set bar-002 to 1
|
||||
expected: `foo 12m (x3 over 20m) Normal ScalingReplicaSet Deployment/bar Scaled up replica set bar-002 from 0 to 1
|
||||
`,
|
||||
},
|
||||
{
|
||||
|
|
|
@ -121,7 +121,7 @@ func NewCmdEvents(restClientGetter genericclioptions.RESTClientGetter, streams g
|
|||
flags := NewEventsFlags(restClientGetter, streams)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: fmt.Sprintf("events [(-o|--output=)%s] [--for TYPE/NAME] [--watch] [--event=Normal,Warning]", strings.Join(flags.PrintFlags.AllowedFormats(), "|")),
|
||||
Use: fmt.Sprintf("events [(-o|--output=)%s] [--for TYPE/NAME] [--watch] [--types=Normal,Warning]", strings.Join(flags.PrintFlags.AllowedFormats(), "|")),
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: i18n.T("List events"),
|
||||
Long: eventsLong,
|
||||
|
@ -188,7 +188,7 @@ func (flags *EventsFlags) ToOptions() (*EventsOptions, error) {
|
|||
}
|
||||
|
||||
if len(o.FilterTypes) > 0 {
|
||||
o.FilterTypes = sets.NewString(o.FilterTypes...).List()
|
||||
o.FilterTypes = sets.List(sets.New[string](o.FilterTypes...))
|
||||
}
|
||||
|
||||
var printer printers.ResourcePrinter
|
||||
|
@ -229,6 +229,7 @@ func (o *EventsOptions) Run() error {
|
|||
if o.forName != "" {
|
||||
listOptions.FieldSelector = fields.AndSelectors(
|
||||
fields.OneTermEqualSelector("involvedObject.kind", o.forGVK.Kind),
|
||||
fields.OneTermEqualSelector("involvedObject.apiVersion", o.forGVK.GroupVersion().String()),
|
||||
fields.OneTermEqualSelector("involvedObject.name", o.forName)).String()
|
||||
}
|
||||
if o.Watch {
|
||||
|
|
|
@ -50,7 +50,7 @@ func getFakeEvents() *corev1.EventList {
|
|||
},
|
||||
Type: corev1.EventTypeNormal,
|
||||
Reason: "ScalingReplicaSet",
|
||||
Message: "Scaled up replica set bar-002 to 1",
|
||||
Message: "Scaled up replica set bar-002 from 0 to 1",
|
||||
ReportingController: "deployment-controller",
|
||||
EventTime: metav1.NewMicroTime(time.Now().Add(-30 * time.Minute)),
|
||||
Series: &corev1.EventSeries{
|
||||
|
@ -72,7 +72,7 @@ func getFakeEvents() *corev1.EventList {
|
|||
},
|
||||
Type: corev1.EventTypeWarning,
|
||||
Reason: "ScalingReplicaSet",
|
||||
Message: "Scaled up replica set bar-002 to 1",
|
||||
Message: "Scaled up replica set bar-002 from 0 to 1",
|
||||
ReportingController: "deployment-controller",
|
||||
EventTime: metav1.NewMicroTime(time.Now().Add(-28 * time.Minute)),
|
||||
Series: &corev1.EventSeries{
|
||||
|
@ -94,7 +94,7 @@ func getFakeEvents() *corev1.EventList {
|
|||
},
|
||||
Type: corev1.EventTypeNormal,
|
||||
Reason: "ScalingReplicaSet",
|
||||
Message: "Scaled up replica set bar-002 to 1",
|
||||
Message: "Scaled up replica set bar-002 from 0 to 1",
|
||||
ReportingController: "deployment-controller",
|
||||
EventTime: metav1.NewMicroTime(time.Now().Add(-25 * time.Minute)),
|
||||
Series: &corev1.EventSeries{
|
||||
|
@ -110,7 +110,7 @@ func TestEventIsSorted(t *testing.T) {
|
|||
codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
|
||||
streams, _, buf, _ := genericiooptions.NewTestIOStreams()
|
||||
clientset, err := kubernetes.NewForConfig(cmdtesting.DefaultClientConfig())
|
||||
if err != err {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
|
@ -135,9 +135,9 @@ func TestEventIsSorted(t *testing.T) {
|
|||
}
|
||||
|
||||
expected := `NAMESPACE LAST SEEN TYPE REASON OBJECT MESSAGE
|
||||
foo 20m (x3 over 30m) Normal ScalingReplicaSet Deployment/bar Scaled up replica set bar-002 to 1
|
||||
foo 18m (x3 over 28m) Warning ScalingReplicaSet Deployment/bar Scaled up replica set bar-002 to 1
|
||||
otherfoo 15m (x3 over 25m) Normal ScalingReplicaSet Deployment/bar Scaled up replica set bar-002 to 1
|
||||
foo 20m (x3 over 30m) Normal ScalingReplicaSet Deployment/bar Scaled up replica set bar-002 from 0 to 1
|
||||
foo 18m (x3 over 28m) Warning ScalingReplicaSet Deployment/bar Scaled up replica set bar-002 from 0 to 1
|
||||
otherfoo 15m (x3 over 25m) Normal ScalingReplicaSet Deployment/bar Scaled up replica set bar-002 from 0 to 1
|
||||
`
|
||||
if e, a := expected, buf.String(); e != a {
|
||||
t.Errorf("expected\n%v\ngot\n%v", e, a)
|
||||
|
@ -148,7 +148,7 @@ func TestEventNoHeaders(t *testing.T) {
|
|||
codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
|
||||
streams, _, buf, _ := genericiooptions.NewTestIOStreams()
|
||||
clientset, err := kubernetes.NewForConfig(cmdtesting.DefaultClientConfig())
|
||||
if err != err {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
|
@ -172,9 +172,9 @@ func TestEventNoHeaders(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
|
||||
expected := `foo 20m (x3 over 30m) Normal ScalingReplicaSet Deployment/bar Scaled up replica set bar-002 to 1
|
||||
foo 18m (x3 over 28m) Warning ScalingReplicaSet Deployment/bar Scaled up replica set bar-002 to 1
|
||||
otherfoo 15m (x3 over 25m) Normal ScalingReplicaSet Deployment/bar Scaled up replica set bar-002 to 1
|
||||
expected := `foo 20m (x3 over 30m) Normal ScalingReplicaSet Deployment/bar Scaled up replica set bar-002 from 0 to 1
|
||||
foo 18m (x3 over 28m) Warning ScalingReplicaSet Deployment/bar Scaled up replica set bar-002 from 0 to 1
|
||||
otherfoo 15m (x3 over 25m) Normal ScalingReplicaSet Deployment/bar Scaled up replica set bar-002 from 0 to 1
|
||||
`
|
||||
if e, a := expected, buf.String(); e != a {
|
||||
t.Errorf("expected\n%v\ngot\n%v", e, a)
|
||||
|
@ -185,7 +185,7 @@ func TestEventFiltered(t *testing.T) {
|
|||
codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
|
||||
streams, _, buf, _ := genericiooptions.NewTestIOStreams()
|
||||
clientset, err := kubernetes.NewForConfig(cmdtesting.DefaultClientConfig())
|
||||
if err != err {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
|
@ -211,7 +211,7 @@ func TestEventFiltered(t *testing.T) {
|
|||
}
|
||||
|
||||
expected := `NAMESPACE LAST SEEN TYPE REASON OBJECT MESSAGE
|
||||
foo 18m (x3 over 28m) Warning ScalingReplicaSet Deployment/bar Scaled up replica set bar-002 to 1
|
||||
foo 18m (x3 over 28m) Warning ScalingReplicaSet Deployment/bar Scaled up replica set bar-002 from 0 to 1
|
||||
`
|
||||
if e, a := expected, buf.String(); e != a {
|
||||
t.Errorf("expected\n%v\ngot\n%v", e, a)
|
||||
|
|
|
@ -27,6 +27,7 @@ import (
|
|||
"github.com/spf13/cobra"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/httpstream"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/cli-runtime/pkg/genericiooptions"
|
||||
"k8s.io/cli-runtime/pkg/resource"
|
||||
|
@ -113,18 +114,26 @@ func NewCmdExec(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Co
|
|||
|
||||
// RemoteExecutor defines the interface accepted by the Exec command - provided for test stubbing
|
||||
type RemoteExecutor interface {
|
||||
Execute(method string, url *url.URL, config *restclient.Config, stdin io.Reader, stdout, stderr io.Writer, tty bool, terminalSizeQueue remotecommand.TerminalSizeQueue) error
|
||||
// Execute supports executing remote command in a pod.
|
||||
Execute(url *url.URL, config *restclient.Config, stdin io.Reader, stdout, stderr io.Writer, tty bool, terminalSizeQueue remotecommand.TerminalSizeQueue) error
|
||||
|
||||
// ExecuteWithContext, in contrast to Execute, supports stopping the remote command via context cancellation.
|
||||
ExecuteWithContext(ctx context.Context, url *url.URL, config *restclient.Config, stdin io.Reader, stdout, stderr io.Writer, tty bool, terminalSizeQueue remotecommand.TerminalSizeQueue) error
|
||||
}
|
||||
|
||||
// DefaultRemoteExecutor is the standard implementation of remote command execution
|
||||
type DefaultRemoteExecutor struct{}
|
||||
|
||||
func (*DefaultRemoteExecutor) Execute(method string, url *url.URL, config *restclient.Config, stdin io.Reader, stdout, stderr io.Writer, tty bool, terminalSizeQueue remotecommand.TerminalSizeQueue) error {
|
||||
exec, err := remotecommand.NewSPDYExecutor(config, method, url)
|
||||
func (d *DefaultRemoteExecutor) Execute(url *url.URL, config *restclient.Config, stdin io.Reader, stdout, stderr io.Writer, tty bool, terminalSizeQueue remotecommand.TerminalSizeQueue) error {
|
||||
return d.ExecuteWithContext(context.Background(), url, config, stdin, stdout, stderr, tty, terminalSizeQueue)
|
||||
}
|
||||
|
||||
func (*DefaultRemoteExecutor) ExecuteWithContext(ctx context.Context, url *url.URL, config *restclient.Config, stdin io.Reader, stdout, stderr io.Writer, tty bool, terminalSizeQueue remotecommand.TerminalSizeQueue) error {
|
||||
exec, err := createExecutor(url, config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return exec.StreamWithContext(context.Background(), remotecommand.StreamOptions{
|
||||
return exec.StreamWithContext(ctx, remotecommand.StreamOptions{
|
||||
Stdin: stdin,
|
||||
Stdout: stdout,
|
||||
Stderr: stderr,
|
||||
|
@ -133,6 +142,29 @@ func (*DefaultRemoteExecutor) Execute(method string, url *url.URL, config *restc
|
|||
})
|
||||
}
|
||||
|
||||
// createExecutor returns the Executor or an error if one occurred.
|
||||
func createExecutor(url *url.URL, config *restclient.Config) (remotecommand.Executor, error) {
|
||||
exec, err := remotecommand.NewSPDYExecutor(config, "POST", url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Fallback executor is default, unless feature flag is explicitly disabled.
|
||||
if !cmdutil.RemoteCommandWebsockets.IsDisabled() {
|
||||
// WebSocketExecutor must be "GET" method as described in RFC 6455 Sec. 4.1 (page 17).
|
||||
websocketExec, err := remotecommand.NewWebSocketExecutor(config, "GET", url.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
exec, err = remotecommand.NewFallbackExecutor(websocketExec, exec, func(err error) bool {
|
||||
return httpstream.IsUpgradeFailure(err) || httpstream.IsHTTPSProxyError(err)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return exec, nil
|
||||
}
|
||||
|
||||
type StreamOptions struct {
|
||||
Namespace string
|
||||
PodName string
|
||||
|
@ -178,17 +210,8 @@ func (p *ExecOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, argsIn []s
|
|||
}
|
||||
if argsLenAtDash > -1 {
|
||||
p.Command = argsIn[argsLenAtDash:]
|
||||
} else if len(argsIn) > 1 {
|
||||
if !p.Quiet {
|
||||
fmt.Fprint(p.ErrOut, "kubectl exec [POD] [COMMAND] is DEPRECATED and will be removed in a future version. Use kubectl exec [POD] -- [COMMAND] instead.\n")
|
||||
}
|
||||
p.Command = argsIn[1:]
|
||||
} else if len(argsIn) > 0 && len(p.FilenameOptions.Filenames) != 0 {
|
||||
if !p.Quiet {
|
||||
fmt.Fprint(p.ErrOut, "kubectl exec [POD] [COMMAND] is DEPRECATED and will be removed in a future version. Use kubectl exec [POD] -- [COMMAND] instead.\n")
|
||||
}
|
||||
p.Command = argsIn[0:]
|
||||
p.ResourceName = ""
|
||||
} else if len(argsIn) > 1 || (len(argsIn) > 0 && len(p.FilenameOptions.Filenames) != 0) {
|
||||
return cmdutil.UsageErrorf(cmd, "exec [POD] [COMMAND] is not supported anymore. Use exec [POD] -- [COMMAND] instead")
|
||||
}
|
||||
|
||||
var err error
|
||||
|
@ -201,7 +224,7 @@ func (p *ExecOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, argsIn []s
|
|||
|
||||
p.GetPodTimeout, err = cmdutil.GetPodRunningTimeoutFlag(cmd)
|
||||
if err != nil {
|
||||
return cmdutil.UsageErrorf(cmd, err.Error())
|
||||
return cmdutil.UsageErrorf(cmd, "%s", err.Error())
|
||||
}
|
||||
|
||||
p.Builder = f.NewBuilder
|
||||
|
@ -371,7 +394,7 @@ func (p *ExecOptions) Run() error {
|
|||
TTY: t.Raw,
|
||||
}, scheme.ParameterCodec)
|
||||
|
||||
return p.Executor.Execute("POST", req.URL(), p.Config, p.In, p.Out, p.ErrOut, t.Raw, sizeQueue)
|
||||
return p.Executor.Execute(req.URL(), p.Config, p.In, p.Out, p.ErrOut, t.Raw, sizeQueue)
|
||||
}
|
||||
|
||||
if err := t.Safe(fn); err != nil {
|
||||
|
|
|
@ -18,6 +18,7 @@ package exec
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
@ -33,20 +34,22 @@ import (
|
|||
restclient "k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/rest/fake"
|
||||
"k8s.io/client-go/tools/remotecommand"
|
||||
|
||||
cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/scheme"
|
||||
"k8s.io/kubectl/pkg/util/term"
|
||||
)
|
||||
|
||||
type fakeRemoteExecutor struct {
|
||||
method string
|
||||
url *url.URL
|
||||
execErr error
|
||||
}
|
||||
|
||||
func (f *fakeRemoteExecutor) Execute(method string, url *url.URL, config *restclient.Config, stdin io.Reader, stdout, stderr io.Writer, tty bool, terminalSizeQueue remotecommand.TerminalSizeQueue) error {
|
||||
f.method = method
|
||||
func (f *fakeRemoteExecutor) Execute(url *url.URL, config *restclient.Config, stdin io.Reader, stdout, stderr io.Writer, tty bool, terminalSizeQueue remotecommand.TerminalSizeQueue) error {
|
||||
return f.ExecuteWithContext(context.Background(), url, config, stdin, stdout, stderr, tty, terminalSizeQueue)
|
||||
}
|
||||
|
||||
func (f *fakeRemoteExecutor) ExecuteWithContext(ctx context.Context, url *url.URL, config *restclient.Config, stdin io.Reader, stdout, stderr io.Writer, tty bool, terminalSizeQueue remotecommand.TerminalSizeQueue) error {
|
||||
f.url = url
|
||||
return f.execErr
|
||||
}
|
||||
|
@ -112,20 +115,17 @@ func TestPodAndContainer(t *testing.T) {
|
|||
p: &ExecOptions{},
|
||||
args: []string{"foo", "cmd"},
|
||||
argsLenAtDash: -1,
|
||||
expectedPod: "foo",
|
||||
expectedArgs: []string{"cmd"},
|
||||
expectError: true,
|
||||
name: "cmd, cmd is behind dash",
|
||||
obj: execPod(),
|
||||
},
|
||||
{
|
||||
p: &ExecOptions{StreamOptions: StreamOptions{ContainerName: "bar"}},
|
||||
args: []string{"foo", "cmd"},
|
||||
argsLenAtDash: -1,
|
||||
expectedPod: "foo",
|
||||
expectedContainer: "bar",
|
||||
expectedArgs: []string{"cmd"},
|
||||
name: "cmd, container in flag",
|
||||
obj: execPod(),
|
||||
p: &ExecOptions{StreamOptions: StreamOptions{ContainerName: "bar"}},
|
||||
args: []string{"foo", "cmd"},
|
||||
argsLenAtDash: -1,
|
||||
expectError: true,
|
||||
name: "cmd, container in flag",
|
||||
obj: execPod(),
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
|
@ -240,8 +240,8 @@ func TestExec(t *testing.T) {
|
|||
Executor: ex,
|
||||
}
|
||||
cmd := NewCmdExec(tf, genericiooptions.NewTestIOStreamsDiscard())
|
||||
args := []string{"pod/foo", "command"}
|
||||
if err := params.Complete(tf, cmd, args, -1); err != nil {
|
||||
args := []string{"pod/foo", "--", "command"}
|
||||
if err := params.Complete(tf, cmd, args, 1); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err := params.Run()
|
||||
|
@ -264,9 +264,6 @@ func TestExec(t *testing.T) {
|
|||
t.Errorf("%s: Did not get expected container query param for exec request", test.name)
|
||||
return
|
||||
}
|
||||
if ex.method != "POST" {
|
||||
t.Errorf("%s: Did not get method for exec request: %s", test.name, ex.method)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -407,3 +404,37 @@ func TestSetupTTY(t *testing.T) {
|
|||
t.Errorf("attach stdin, TTY, is a terminal: tty.Out should equal o.Out")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateExecutor(t *testing.T) {
|
||||
url, err := url.Parse("http://localhost:8080/index.html")
|
||||
if err != nil {
|
||||
t.Fatalf("unable to parse test url: %v", err)
|
||||
}
|
||||
config := cmdtesting.DefaultClientConfig()
|
||||
// First, ensure that no environment variable creates the fallback executor.
|
||||
executor, err := createExecutor(url, config)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create executor: %v", err)
|
||||
}
|
||||
if _, isFallback := executor.(*remotecommand.FallbackExecutor); !isFallback {
|
||||
t.Errorf("expected fallback executor, got %#v", executor)
|
||||
}
|
||||
// Next, check turning on feature flag explicitly also creates fallback executor.
|
||||
t.Setenv(string(cmdutil.RemoteCommandWebsockets), "true")
|
||||
executor, err = createExecutor(url, config)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create executor: %v", err)
|
||||
}
|
||||
if _, isFallback := executor.(*remotecommand.FallbackExecutor); !isFallback {
|
||||
t.Errorf("expected fallback executor, got %#v", executor)
|
||||
}
|
||||
// Finally, check explicit disabling does NOT create the fallback executor.
|
||||
t.Setenv(string(cmdutil.RemoteCommandWebsockets), "false")
|
||||
executor, err = createExecutor(url, config)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create executor: %v", err)
|
||||
}
|
||||
if _, isFallback := executor.(*remotecommand.FallbackExecutor); isFallback {
|
||||
t.Errorf("expected fallback executor, got %#v", executor)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -56,86 +56,113 @@ var (
|
|||
|
||||
# Get the documentation of a specific field of a resource
|
||||
kubectl explain pods.spec.containers
|
||||
|
||||
|
||||
# Get the documentation of resources in different format
|
||||
kubectl explain deployment --output=plaintext-openapiv2`))
|
||||
)
|
||||
|
||||
const (
|
||||
plaintextTemplateName = "plaintext"
|
||||
plaintextOpenAPIV2TemplateName = "plaintext-openapiv2"
|
||||
)
|
||||
|
||||
type ExplainOptions struct {
|
||||
genericiooptions.IOStreams
|
||||
|
||||
CmdParent string
|
||||
APIVersion string
|
||||
Recursive bool
|
||||
|
||||
args []string
|
||||
|
||||
Mapper meta.RESTMapper
|
||||
Schema openapi.Resources
|
||||
|
||||
// Name of the template to use with the openapiv3 template renderer.
|
||||
// ExplainFlags directly reflect the information that CLI is gathering via flags.
|
||||
// They will be converted to Options, which reflect the runtime requirements for
|
||||
// the command.
|
||||
type ExplainFlags struct {
|
||||
APIVersion string
|
||||
OutputFormat string
|
||||
Recursive bool
|
||||
|
||||
// Client capable of fetching openapi documents from the user's cluster
|
||||
OpenAPIV3Client openapiclient.Client
|
||||
genericiooptions.IOStreams
|
||||
}
|
||||
|
||||
func NewExplainOptions(parent string, streams genericiooptions.IOStreams) *ExplainOptions {
|
||||
return &ExplainOptions{
|
||||
IOStreams: streams,
|
||||
CmdParent: parent,
|
||||
// NewExplainFlags returns a default ExplainFlags
|
||||
func NewExplainFlags(streams genericiooptions.IOStreams) *ExplainFlags {
|
||||
return &ExplainFlags{
|
||||
OutputFormat: plaintextTemplateName,
|
||||
IOStreams: streams,
|
||||
}
|
||||
}
|
||||
|
||||
// AddFlags registers flags for a cli
|
||||
func (flags *ExplainFlags) AddFlags(cmd *cobra.Command) {
|
||||
cmd.Flags().BoolVar(&flags.Recursive, "recursive", flags.Recursive, "Print the fields of fields (Currently only 1 level deep)")
|
||||
cmd.Flags().StringVar(&flags.APIVersion, "api-version", flags.APIVersion, "Get different explanations for particular API version (API group/version)")
|
||||
cmd.Flags().StringVarP(&flags.OutputFormat, "output", "o", plaintextTemplateName, "Format in which to render the schema (plaintext, plaintext-openapiv2)")
|
||||
}
|
||||
|
||||
// ToOptions converts from CLI inputs to runtime input
|
||||
func (flags *ExplainFlags) ToOptions(f cmdutil.Factory, parent string, args []string) (*ExplainOptions, error) {
|
||||
mapper, err := f.ToRESTMapper()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Only openapi v3 needs the discovery client.
|
||||
openAPIV3Client, err := f.OpenAPIV3Client()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
o := &ExplainOptions{
|
||||
IOStreams: flags.IOStreams,
|
||||
|
||||
Recursive: flags.Recursive,
|
||||
APIVersion: flags.APIVersion,
|
||||
OutputFormat: flags.OutputFormat,
|
||||
|
||||
CmdParent: parent,
|
||||
args: args,
|
||||
|
||||
Mapper: mapper,
|
||||
openAPIGetter: f,
|
||||
|
||||
OpenAPIV3Client: openAPIV3Client,
|
||||
}
|
||||
|
||||
return o, nil
|
||||
}
|
||||
|
||||
// NewCmdExplain returns a cobra command for swagger docs
|
||||
func NewCmdExplain(parent string, f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command {
|
||||
o := NewExplainOptions(parent, streams)
|
||||
flags := NewExplainFlags(streams)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "explain TYPE [--recursive=FALSE|TRUE] [--api-version=api-version-group] [--output=plaintext|plaintext-openapiv2]",
|
||||
Use: "explain TYPE [--recursive=FALSE|TRUE] [--api-version=api-version-group] [-o|--output=plaintext|plaintext-openapiv2]",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: i18n.T("Get documentation for a resource"),
|
||||
Long: explainLong + "\n\n" + cmdutil.SuggestAPIResources(parent),
|
||||
Example: explainExamples,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(o.Complete(f, cmd, args))
|
||||
o, err := flags.ToOptions(f, parent, args)
|
||||
cmdutil.CheckErr(err)
|
||||
cmdutil.CheckErr(o.Validate())
|
||||
cmdutil.CheckErr(o.Run())
|
||||
},
|
||||
}
|
||||
cmd.Flags().BoolVar(&o.Recursive, "recursive", o.Recursive, "When true, print the name of all the fields recursively. Otherwise, print the available fields with their description.")
|
||||
cmd.Flags().StringVar(&o.APIVersion, "api-version", o.APIVersion, "Use given api-version (group/version) of the resource.")
|
||||
|
||||
// Only enable --output as a valid flag if the feature is enabled
|
||||
cmd.Flags().StringVar(&o.OutputFormat, "output", plaintextTemplateName, "Format in which to render the schema. Valid values are: (plaintext, plaintext-openapiv2).")
|
||||
flags.AddFlags(cmd)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (o *ExplainOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error {
|
||||
var err error
|
||||
o.Mapper, err = f.ToRESTMapper()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
type ExplainOptions struct {
|
||||
genericiooptions.IOStreams
|
||||
|
||||
o.Schema, err = f.OpenAPISchema()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
Recursive bool
|
||||
APIVersion string
|
||||
// Name of the template to use with the openapiv3 template renderer.
|
||||
OutputFormat string
|
||||
|
||||
// Only openapi v3 needs the discovery client.
|
||||
o.OpenAPIV3Client, err = f.OpenAPIV3Client()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
CmdParent string
|
||||
args []string
|
||||
|
||||
o.args = args
|
||||
return nil
|
||||
Mapper meta.RESTMapper
|
||||
openAPIGetter openapi.OpenAPIResourcesGetter
|
||||
|
||||
// Client capable of fetching openapi documents from the user's cluster
|
||||
OpenAPIV3Client openapiclient.Client
|
||||
}
|
||||
|
||||
func (o *ExplainOptions) Validate() error {
|
||||
|
@ -224,7 +251,11 @@ func (o *ExplainOptions) renderOpenAPIV2(
|
|||
gvk = apiVersion.WithKind(gvk.Kind)
|
||||
}
|
||||
|
||||
schema := o.Schema.LookupResource(gvk)
|
||||
resources, err := o.openAPIGetter.OpenAPISchema()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
schema := resources.LookupResource(gvk)
|
||||
if schema == nil {
|
||||
return fmt.Errorf("couldn't find resource for %q", gvk)
|
||||
}
|
||||
|
|
|
@ -57,9 +57,9 @@ func TestExplainInvalidArgs(t *testing.T) {
|
|||
tf := cmdtesting.NewTestFactory()
|
||||
defer tf.Cleanup()
|
||||
|
||||
opts := explain.NewExplainOptions("kubectl", genericiooptions.NewTestIOStreamsDiscard())
|
||||
cmd := explain.NewCmdExplain("kubectl", tf, genericiooptions.NewTestIOStreamsDiscard())
|
||||
err := opts.Complete(tf, cmd, []string{})
|
||||
flags := explain.NewExplainFlags(genericiooptions.NewTestIOStreamsDiscard())
|
||||
|
||||
opts, err := flags.ToOptions(tf, "kubectl", []string{})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error %v", err)
|
||||
}
|
||||
|
@ -69,7 +69,7 @@ func TestExplainInvalidArgs(t *testing.T) {
|
|||
t.Error("unexpected non-error")
|
||||
}
|
||||
|
||||
err = opts.Complete(tf, cmd, []string{"resource1", "resource2"})
|
||||
opts, err = flags.ToOptions(tf, "kubectl", []string{"resource1", "resource2"})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error %v", err)
|
||||
}
|
||||
|
@ -84,9 +84,9 @@ func TestExplainNotExistResource(t *testing.T) {
|
|||
tf := cmdtesting.NewTestFactory()
|
||||
defer tf.Cleanup()
|
||||
|
||||
opts := explain.NewExplainOptions("kubectl", genericiooptions.NewTestIOStreamsDiscard())
|
||||
cmd := explain.NewCmdExplain("kubectl", tf, genericiooptions.NewTestIOStreamsDiscard())
|
||||
err := opts.Complete(tf, cmd, []string{"foo"})
|
||||
flags := explain.NewExplainFlags(genericiooptions.NewTestIOStreamsDiscard())
|
||||
|
||||
opts, err := flags.ToOptions(tf, "kubectl", []string{"foo"})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error %v", err)
|
||||
}
|
||||
|
@ -275,3 +275,44 @@ func runExplainTestCases(t *testing.T, cases []explainTestCase) {
|
|||
buf.Reset()
|
||||
}
|
||||
}
|
||||
|
||||
// OpenAPI V2 specifications retrieval -- should never be called.
|
||||
func panicOpenAPISchemaFn() (openapi.Resources, error) {
|
||||
panic("should never be called")
|
||||
}
|
||||
|
||||
// OpenAPI V3 specifications retrieval does *not* retrieve V2 specifications.
|
||||
func TestExplainOpenAPIV3DoesNotLoadOpenAPIV2Specs(t *testing.T) {
|
||||
// Set up OpenAPI V3 specifications endpoint for explain.
|
||||
fakeServer, err := clienttestutil.NewFakeOpenAPIV3Server(filepath.Join(testDataPath, "openapi", "v3"))
|
||||
if err != nil {
|
||||
t.Fatalf("error starting fake openapi server: %v", err.Error())
|
||||
}
|
||||
defer fakeServer.HttpServer.Close()
|
||||
tf := cmdtesting.NewTestFactory()
|
||||
defer tf.Cleanup()
|
||||
tf.OpenAPIV3ClientFunc = func() (openapiclient.Client, error) {
|
||||
fakeDiscoveryClient := discovery.NewDiscoveryClientForConfigOrDie(&rest.Config{Host: fakeServer.HttpServer.URL})
|
||||
return fakeDiscoveryClient.OpenAPIV3(), nil
|
||||
}
|
||||
// OpenAPI V2 specifications retrieval will panic if called.
|
||||
tf.OpenAPISchemaFunc = panicOpenAPISchemaFn
|
||||
|
||||
// Explain the following resources, validating the command does not panic.
|
||||
cmd := explain.NewCmdExplain("kubectl", tf, genericiooptions.NewTestIOStreamsDiscard())
|
||||
resources := []string{"pods", "services", "endpoints", "configmaps"}
|
||||
for _, resource := range resources {
|
||||
cmd.Run(cmd, []string{resource})
|
||||
}
|
||||
// Verify retrieving OpenAPI V2 specifications will panic.
|
||||
defer func() {
|
||||
if panicErr := recover(); panicErr == nil {
|
||||
t.Fatal("expecting panic for openapi v2 retrieval")
|
||||
}
|
||||
}()
|
||||
// Set OpenAPI V2 output flag for explain.
|
||||
if err := cmd.Flags().Set("output", "plaintext-openapiv2"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cmd.Run(cmd, []string{"pods"})
|
||||
}
|
||||
|
|
|
@ -225,12 +225,12 @@ func (flags *ExposeServiceFlags) AddFlags(cmd *cobra.Command) {
|
|||
}
|
||||
|
||||
func (flags *ExposeServiceFlags) ToOptions(cmd *cobra.Command, args []string) (*ExposeServiceOptions, error) {
|
||||
dryRunStratergy, err := cmdutil.GetDryRunStrategy(cmd)
|
||||
dryRunStrategy, err := cmdutil.GetDryRunStrategy(cmd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cmdutil.PrintFlagsWithDryRunStrategy(flags.PrintFlags, dryRunStratergy)
|
||||
cmdutil.PrintFlagsWithDryRunStrategy(flags.PrintFlags, dryRunStrategy)
|
||||
printer, err := flags.PrintFlags.ToPrinter()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -243,7 +243,7 @@ func (flags *ExposeServiceFlags) ToOptions(cmd *cobra.Command, args []string) (*
|
|||
}
|
||||
|
||||
e := &ExposeServiceOptions{
|
||||
DryRunStrategy: dryRunStratergy,
|
||||
DryRunStrategy: dryRunStrategy,
|
||||
PrintObj: printer.PrintObj,
|
||||
Recorder: recorder,
|
||||
IOStreams: flags.IOStreams,
|
||||
|
@ -532,12 +532,7 @@ func (o *ExposeServiceOptions) createService() (*corev1.Service, error) {
|
|||
}
|
||||
targetPortString := o.TargetPort
|
||||
if len(targetPortString) > 0 {
|
||||
var targetPort intstr.IntOrString
|
||||
if portNum, err := strconv.Atoi(targetPortString); err != nil {
|
||||
targetPort = intstr.FromString(targetPortString)
|
||||
} else {
|
||||
targetPort = intstr.FromInt(portNum)
|
||||
}
|
||||
targetPort := intstr.Parse(targetPortString)
|
||||
// Use the same target-port for every port
|
||||
for i := range service.Spec.Ports {
|
||||
service.Spec.Ports[i].TargetPort = targetPort
|
||||
|
|
|
@ -694,7 +694,6 @@ func TestExposeOverride(t *testing.T) {
|
|||
expected: `apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
svc: test
|
||||
name: foo
|
||||
|
@ -717,7 +716,6 @@ status:
|
|||
expected: `apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
svc: test
|
||||
name: foo
|
||||
|
@ -745,7 +743,6 @@ status:
|
|||
expected: `apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
svc: test
|
||||
name: foo
|
||||
|
@ -773,7 +770,6 @@ status:
|
|||
expected: `apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
svc: test
|
||||
name: foo
|
||||
|
@ -1709,13 +1705,13 @@ func TestGenerateService(t *testing.T) {
|
|||
Name: "port-1-tcp",
|
||||
Port: 53,
|
||||
Protocol: corev1.ProtocolTCP,
|
||||
TargetPort: intstr.FromInt(53),
|
||||
TargetPort: intstr.FromInt32(53),
|
||||
},
|
||||
{
|
||||
Name: "port-1-udp",
|
||||
Port: 53,
|
||||
Protocol: corev1.ProtocolUDP,
|
||||
TargetPort: intstr.FromInt(53),
|
||||
TargetPort: intstr.FromInt32(53),
|
||||
},
|
||||
},
|
||||
ClusterIP: corev1.ClusterIPNone,
|
||||
|
|
|
@ -19,6 +19,7 @@ package get
|
|||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
|
@ -161,7 +162,7 @@ func (s *CustomColumnsPrinter) PrintObj(obj runtime.Object, out io.Writer) error
|
|||
// we need an actual value in order to retrieve the package path for an object.
|
||||
// using reflect.Indirect indiscriminately is valid here, as all runtime.Objects are supposed to be pointers.
|
||||
if printers.InternalObjectPreventer.IsForbidden(reflect.Indirect(reflect.ValueOf(obj)).Type().PkgPath()) {
|
||||
return fmt.Errorf(printers.InternalObjectPrinterErr)
|
||||
return errors.New(printers.InternalObjectPrinterErr)
|
||||
}
|
||||
|
||||
if _, found := out.(*tabwriter.Writer); !found {
|
||||
|
@ -210,7 +211,7 @@ func (s *CustomColumnsPrinter) printOneObject(obj runtime.Object, parsers []*jso
|
|||
switch u := obj.(type) {
|
||||
case *metav1.WatchEvent:
|
||||
if printers.InternalObjectPreventer.IsForbidden(reflect.Indirect(reflect.ValueOf(u.Object.Object)).Type().PkgPath()) {
|
||||
return fmt.Errorf(printers.InternalObjectPrinterErr)
|
||||
return errors.New(printers.InternalObjectPrinterErr)
|
||||
}
|
||||
unstructuredObject, err := runtime.DefaultUnstructuredConverter.ToUnstructured(u.Object.Object)
|
||||
if err != nil {
|
||||
|
|
|
@ -47,9 +47,8 @@ import (
|
|||
"k8s.io/kubectl/pkg/scheme"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
"k8s.io/kubectl/pkg/util/interrupt"
|
||||
"k8s.io/kubectl/pkg/util/slice"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
utilpointer "k8s.io/utils/pointer"
|
||||
"k8s.io/utils/ptr"
|
||||
)
|
||||
|
||||
// GetOptions contains the input to the get command.
|
||||
|
@ -91,8 +90,8 @@ var (
|
|||
|
||||
Prints a table of the most important information about the specified resources.
|
||||
You can filter the list using a label selector and the --selector flag. If the
|
||||
desired resource type is namespaced you will only see results in your current
|
||||
namespace unless you pass --all-namespaces.
|
||||
desired resource type is namespaced you will only see results in the current
|
||||
namespace if you don't specify any namespace.
|
||||
|
||||
By specifying the output as 'template' and providing a Go template as the value
|
||||
of the --template flag, you can filter the attributes of the fetched resources.`))
|
||||
|
@ -132,15 +131,19 @@ var (
|
|||
kubectl get rc/web service/frontend pods/web-pod-13je7
|
||||
|
||||
# List the 'status' subresource for a single pod
|
||||
kubectl get pod web-pod-13je7 --subresource status`))
|
||||
kubectl get pod web-pod-13je7 --subresource status
|
||||
|
||||
# List all deployments in namespace 'backend'
|
||||
kubectl get deployments.apps --namespace backend
|
||||
|
||||
# List all pods existing in all namespaces
|
||||
kubectl get pods --all-namespaces`))
|
||||
)
|
||||
|
||||
const (
|
||||
useServerPrintColumns = "server-print"
|
||||
)
|
||||
|
||||
var supportedSubresources = []string{"status", "scale"}
|
||||
|
||||
// NewGetOptions returns a GetOptions with default chunk size 500.
|
||||
func NewGetOptions(parent string, streams genericiooptions.IOStreams) *GetOptions {
|
||||
return &GetOptions{
|
||||
|
@ -179,14 +182,14 @@ func NewCmdGet(parent string, f cmdutil.Factory, streams genericiooptions.IOStre
|
|||
cmd.Flags().BoolVarP(&o.Watch, "watch", "w", o.Watch, "After listing/getting the requested object, watch for changes.")
|
||||
cmd.Flags().BoolVar(&o.WatchOnly, "watch-only", o.WatchOnly, "Watch for changes to the requested object(s), without listing/getting first.")
|
||||
cmd.Flags().BoolVar(&o.OutputWatchEvents, "output-watch-events", o.OutputWatchEvents, "Output watch event objects when --watch or --watch-only is used. Existing objects are output as initial ADDED events.")
|
||||
cmd.Flags().BoolVar(&o.IgnoreNotFound, "ignore-not-found", o.IgnoreNotFound, "If the requested object does not exist the command will return exit code 0.")
|
||||
cmd.Flags().BoolVar(&o.IgnoreNotFound, "ignore-not-found", o.IgnoreNotFound, "If set to true, suppresses NotFound error for specific objects that do not exist. Using this flag with commands that query for collections of resources has no effect when no resources are found.")
|
||||
cmd.Flags().StringVar(&o.FieldSelector, "field-selector", o.FieldSelector, "Selector (field query) to filter on, supports '=', '==', and '!='.(e.g. --field-selector key1=value1,key2=value2). The server only supports a limited number of field queries per type.")
|
||||
cmd.Flags().BoolVarP(&o.AllNamespaces, "all-namespaces", "A", o.AllNamespaces, "If present, list the requested object(s) across all namespaces. Namespace in current context is ignored even if specified with --namespace.")
|
||||
addServerPrintColumnFlags(cmd, o)
|
||||
cmdutil.AddFilenameOptionFlags(cmd, &o.FilenameOptions, "identifying the resource to get from a server.")
|
||||
cmdutil.AddChunkSizeFlag(cmd, &o.ChunkSize)
|
||||
cmdutil.AddLabelSelectorFlagVar(cmd, &o.LabelSelector)
|
||||
cmdutil.AddSubresourceFlags(cmd, &o.Subresource, "If specified, gets the subresource of the requested object.", supportedSubresources...)
|
||||
cmdutil.AddSubresourceFlags(cmd, &o.Subresource, "If specified, gets the subresource of the requested object.")
|
||||
return cmd
|
||||
}
|
||||
|
||||
|
@ -267,9 +270,13 @@ func (o *GetOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []stri
|
|||
}
|
||||
|
||||
switch {
|
||||
case o.Watch || o.WatchOnly:
|
||||
case o.Watch:
|
||||
if len(o.SortBy) > 0 {
|
||||
fmt.Fprintf(o.IOStreams.ErrOut, "warning: --watch or --watch-only requested, --sort-by will be ignored\n")
|
||||
fmt.Fprintf(o.IOStreams.ErrOut, "warning: --watch requested, --sort-by will be ignored for watch events received\n")
|
||||
}
|
||||
case o.WatchOnly:
|
||||
if len(o.SortBy) > 0 {
|
||||
fmt.Fprintf(o.IOStreams.ErrOut, "warning: --watch-only requested, --sort-by will be ignored\n")
|
||||
}
|
||||
default:
|
||||
if len(args) == 0 && cmdutil.IsFilenameSliceEmpty(o.Filenames, o.Kustomize) {
|
||||
|
@ -280,7 +287,7 @@ func (o *GetOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []stri
|
|||
usageString = fmt.Sprintf("%s\nUse \"%s explain <resource>\" for a detailed description of that resource (e.g. %[2]s explain pods).", usageString, fullCmdName)
|
||||
}
|
||||
|
||||
return cmdutil.UsageErrorf(cmd, usageString)
|
||||
return cmdutil.UsageErrorf(cmd, "%s", usageString)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -309,9 +316,6 @@ func (o *GetOptions) Validate() error {
|
|||
if o.OutputWatchEvents && !(o.Watch || o.WatchOnly) {
|
||||
return fmt.Errorf("--output-watch-events option can only be used with --watch or --watch-only")
|
||||
}
|
||||
if len(o.Subresource) > 0 && !slice.ContainsString(supportedSubresources, o.Subresource, nil) {
|
||||
return fmt.Errorf("invalid subresource value: %q. Must be one of %v", o.Subresource, supportedSubresources)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -482,7 +486,7 @@ func (o *GetOptions) Run(f cmdutil.Factory, args []string) error {
|
|||
}
|
||||
|
||||
allErrs := []error{}
|
||||
errs := sets.NewString()
|
||||
errs := sets.New[string]()
|
||||
infos, err := r.Infos()
|
||||
if err != nil {
|
||||
allErrs = append(allErrs, err)
|
||||
|
@ -619,6 +623,10 @@ func (o *GetOptions) watch(f cmdutil.Factory, args []string) error {
|
|||
}
|
||||
infos, err := r.Infos()
|
||||
if err != nil {
|
||||
// Ignore "NotFound" error when ignore-not-found is set to true
|
||||
if apierrors.IsNotFound(err) && o.IgnoreNotFound {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
if multipleGVKsRequested(infos) {
|
||||
|
@ -627,7 +635,7 @@ func (o *GetOptions) watch(f cmdutil.Factory, args []string) error {
|
|||
|
||||
info := infos[0]
|
||||
mapping := info.ResourceMapping()
|
||||
outputObjects := utilpointer.BoolPtr(!o.WatchOnly)
|
||||
outputObjects := ptr.To(!o.WatchOnly)
|
||||
printer, err := o.ToPrinter(mapping, outputObjects, o.AllNamespaces, false)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
@ -21,6 +21,7 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
@ -711,7 +712,7 @@ func TestGetEmptyTable(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestGetObjectIgnoreNotFound(t *testing.T) {
|
||||
func TestGetNonExistObject(t *testing.T) {
|
||||
cmdtesting.InitTestErrorHandler(t)
|
||||
|
||||
ns := &corev1.NamespaceList{
|
||||
|
@ -745,6 +746,63 @@ func TestGetObjectIgnoreNotFound(t *testing.T) {
|
|||
}),
|
||||
}
|
||||
|
||||
cmdutil.BehaviorOnFatal(func(str string, code int) {
|
||||
expectedErr := "Error from server (NotFound): the server could not find the requested resource (get pods nonexistentpod)"
|
||||
if str != expectedErr {
|
||||
t.Errorf("unexpected error: %s\nexpected: %s", str, expectedErr)
|
||||
}
|
||||
})
|
||||
|
||||
// Get nonexistentpod fails with above error message
|
||||
streams, _, buf, _ := genericiooptions.NewTestIOStreams()
|
||||
cmd := NewCmdGet("kubectl", tf, streams)
|
||||
cmd.SetOut(buf)
|
||||
cmd.SetErr(buf)
|
||||
cmd.Run(cmd, []string{"pods", "nonexistentpod"})
|
||||
}
|
||||
|
||||
func TestGetNonExistObjectIgnoreNotFound(t *testing.T) {
|
||||
cmdtesting.InitTestErrorHandler(t)
|
||||
|
||||
ns := &corev1.NamespaceList{
|
||||
ListMeta: metav1.ListMeta{
|
||||
ResourceVersion: "1",
|
||||
},
|
||||
Items: []corev1.Namespace{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "testns", Namespace: "test", ResourceVersion: "11"},
|
||||
Spec: corev1.NamespaceSpec{},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
defer tf.Cleanup()
|
||||
codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
|
||||
|
||||
tf.UnstructuredClient = &fake.RESTClient{
|
||||
NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
|
||||
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||
switch p, m := req.URL.Path, req.Method; {
|
||||
case p == "/namespaces/test/pods/nonexistentpod" && m == "GET":
|
||||
return &http.Response{StatusCode: http.StatusNotFound, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.StringBody("")}, nil
|
||||
case p == "/api/v1/namespaces/test" && m == "GET":
|
||||
return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &ns.Items[0])}, nil
|
||||
default:
|
||||
t.Fatalf("request url: %#v,and request: %#v", req.URL, req)
|
||||
return nil, nil
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
cmdutil.BehaviorOnFatal(func(str string, code int) {
|
||||
expectedErr := ""
|
||||
if str != expectedErr {
|
||||
t.Errorf("unexpected error: %s\nexpected: %s", str, expectedErr)
|
||||
}
|
||||
})
|
||||
|
||||
// Get nonexistentpod passes without error when setting ignore-not-found to true
|
||||
streams, _, buf, _ := genericiooptions.NewTestIOStreams()
|
||||
cmd := NewCmdGet("kubectl", tf, streams)
|
||||
cmd.SetOut(buf)
|
||||
|
@ -1438,7 +1496,6 @@ func TestGetMultipleTypeObjectsAsList(t *testing.T) {
|
|||
"apiVersion": "v1",
|
||||
"kind": "Pod",
|
||||
"metadata": {
|
||||
"creationTimestamp": null,
|
||||
"name": "foo",
|
||||
"namespace": "test",
|
||||
"resourceVersion": "10"
|
||||
|
@ -1457,7 +1514,6 @@ func TestGetMultipleTypeObjectsAsList(t *testing.T) {
|
|||
"apiVersion": "v1",
|
||||
"kind": "Pod",
|
||||
"metadata": {
|
||||
"creationTimestamp": null,
|
||||
"name": "bar",
|
||||
"namespace": "test",
|
||||
"resourceVersion": "11"
|
||||
|
@ -1476,7 +1532,6 @@ func TestGetMultipleTypeObjectsAsList(t *testing.T) {
|
|||
"apiVersion": "v1",
|
||||
"kind": "Service",
|
||||
"metadata": {
|
||||
"creationTimestamp": null,
|
||||
"name": "baz",
|
||||
"namespace": "test",
|
||||
"resourceVersion": "12"
|
||||
|
@ -2129,6 +2184,93 @@ foo <unknown>
|
|||
}
|
||||
}
|
||||
|
||||
func TestWatchNonExistObject(t *testing.T) {
|
||||
pods, _ := watchTestData()
|
||||
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
defer tf.Cleanup()
|
||||
codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
|
||||
|
||||
tf.UnstructuredClient = &fake.RESTClient{
|
||||
NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
|
||||
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||
switch p, m := req.URL.Path, req.Method; {
|
||||
case p == "/namespaces/test/pods/nonexistentpod" && m == "GET":
|
||||
return &http.Response{StatusCode: http.StatusNotFound, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.StringBody("")}, nil
|
||||
case p == "/api/v1/namespaces/test" && m == "GET":
|
||||
return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &pods[1])}, nil
|
||||
default:
|
||||
t.Fatalf("request url: %#v,and request: %#v", req.URL, req)
|
||||
return nil, nil
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
cmdutil.BehaviorOnFatal(func(str string, code int) {
|
||||
expectedErr := "Error from server (NotFound): the server could not find the requested resource (get pods nonexistentpod)"
|
||||
if str != expectedErr {
|
||||
t.Errorf("unexpected error: %s\nexpected: %s", str, expectedErr)
|
||||
}
|
||||
})
|
||||
|
||||
// Get nonexistentpod fails with above error message
|
||||
streams, _, buf, _ := genericiooptions.NewTestIOStreams()
|
||||
cmd := NewCmdGet("kubectl", tf, streams)
|
||||
cmd.SetOut(buf)
|
||||
cmd.SetErr(buf)
|
||||
cmd.Flags().Set("watch", "true") //nolint:errcheck
|
||||
cmd.Flags().Set("output", "yaml") //nolint:errcheck
|
||||
cmd.Run(cmd, []string{"pods", "nonexistentpod"})
|
||||
|
||||
if buf.String() != "" {
|
||||
t.Errorf("unexpected output: %s", buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestWatchNonExistObjectIgnoreNotFound(t *testing.T) {
|
||||
pods, _ := watchTestData()
|
||||
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
defer tf.Cleanup()
|
||||
codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
|
||||
|
||||
tf.UnstructuredClient = &fake.RESTClient{
|
||||
NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
|
||||
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||
switch p, m := req.URL.Path, req.Method; {
|
||||
case p == "/namespaces/test/pods/nonexistentpod" && m == "GET":
|
||||
return &http.Response{StatusCode: http.StatusNotFound, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.StringBody("")}, nil
|
||||
case p == "/api/v1/namespaces/test" && m == "GET":
|
||||
return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &pods[1])}, nil
|
||||
default:
|
||||
t.Fatalf("request url: %#v,and request: %#v", req.URL, req)
|
||||
return nil, nil
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
cmdutil.BehaviorOnFatal(func(str string, code int) {
|
||||
expectedErr := ""
|
||||
if str != expectedErr {
|
||||
t.Errorf("unexpected error: %s\nexpected: %s", str, expectedErr)
|
||||
}
|
||||
})
|
||||
|
||||
// Get nonexistentpod passes without error when setting ignore-not-found to true
|
||||
streams, _, buf, _ := genericiooptions.NewTestIOStreams()
|
||||
cmd := NewCmdGet("kubectl", tf, streams)
|
||||
cmd.SetOut(buf)
|
||||
cmd.SetErr(buf)
|
||||
cmd.Flags().Set("ignore-not-found", "true") //nolint:errcheck
|
||||
cmd.Flags().Set("watch", "true") //nolint:errcheck
|
||||
cmd.Flags().Set("output", "yaml") //nolint:errcheck
|
||||
cmd.Run(cmd, []string{"pods", "nonexistentpod"})
|
||||
|
||||
if buf.String() != "" {
|
||||
t.Errorf("unexpected output: %s", buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestWatchStatus(t *testing.T) {
|
||||
pods, events := watchTestData()
|
||||
events = append(events, watch.Event{Type: "ERROR", Object: &metav1.Status{Status: "Failure", Reason: "InternalServerError", Message: "Something happened"}})
|
||||
|
@ -2367,10 +2509,10 @@ DELETED test pod/foo 0/0 0 <unknown> <none>
|
|||
},
|
||||
{
|
||||
format: "json",
|
||||
expected: `{"type":"ADDED","object":{"apiVersion":"v1","kind":"Pod","metadata":{"creationTimestamp":null,"name":"bar","namespace":"test","resourceVersion":"9"},"spec":{"containers":null,"dnsPolicy":"ClusterFirst","enableServiceLinks":true,"restartPolicy":"Always","securityContext":{},"terminationGracePeriodSeconds":30},"status":{}}}
|
||||
{"type":"ADDED","object":{"apiVersion":"v1","kind":"Pod","metadata":{"creationTimestamp":null,"name":"foo","namespace":"test","resourceVersion":"10"},"spec":{"containers":null,"dnsPolicy":"ClusterFirst","enableServiceLinks":true,"restartPolicy":"Always","securityContext":{},"terminationGracePeriodSeconds":30},"status":{}}}
|
||||
{"type":"MODIFIED","object":{"apiVersion":"v1","kind":"Pod","metadata":{"creationTimestamp":null,"name":"foo","namespace":"test","resourceVersion":"11"},"spec":{"containers":null,"dnsPolicy":"ClusterFirst","enableServiceLinks":true,"restartPolicy":"Always","securityContext":{},"terminationGracePeriodSeconds":30},"status":{}}}
|
||||
{"type":"DELETED","object":{"apiVersion":"v1","kind":"Pod","metadata":{"creationTimestamp":null,"name":"foo","namespace":"test","resourceVersion":"12"},"spec":{"containers":null,"dnsPolicy":"ClusterFirst","enableServiceLinks":true,"restartPolicy":"Always","securityContext":{},"terminationGracePeriodSeconds":30},"status":{}}}
|
||||
expected: `{"type":"ADDED","object":{"apiVersion":"v1","kind":"Pod","metadata":{"name":"bar","namespace":"test","resourceVersion":"9"},"spec":{"containers":null,"dnsPolicy":"ClusterFirst","enableServiceLinks":true,"restartPolicy":"Always","securityContext":{},"terminationGracePeriodSeconds":30},"status":{}}}
|
||||
{"type":"ADDED","object":{"apiVersion":"v1","kind":"Pod","metadata":{"name":"foo","namespace":"test","resourceVersion":"10"},"spec":{"containers":null,"dnsPolicy":"ClusterFirst","enableServiceLinks":true,"restartPolicy":"Always","securityContext":{},"terminationGracePeriodSeconds":30},"status":{}}}
|
||||
{"type":"MODIFIED","object":{"apiVersion":"v1","kind":"Pod","metadata":{"name":"foo","namespace":"test","resourceVersion":"11"},"spec":{"containers":null,"dnsPolicy":"ClusterFirst","enableServiceLinks":true,"restartPolicy":"Always","securityContext":{},"terminationGracePeriodSeconds":30},"status":{}}}
|
||||
{"type":"DELETED","object":{"apiVersion":"v1","kind":"Pod","metadata":{"name":"foo","namespace":"test","resourceVersion":"12"},"spec":{"containers":null,"dnsPolicy":"ClusterFirst","enableServiceLinks":true,"restartPolicy":"Always","securityContext":{},"terminationGracePeriodSeconds":30},"status":{}}}
|
||||
`,
|
||||
},
|
||||
{
|
||||
|
@ -2379,7 +2521,6 @@ DELETED test pod/foo 0/0 0 <unknown> <none>
|
|||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
name: bar
|
||||
namespace: test
|
||||
resourceVersion: "9"
|
||||
|
@ -2397,7 +2538,6 @@ object:
|
|||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
name: foo
|
||||
namespace: test
|
||||
resourceVersion: "10"
|
||||
|
@ -2415,7 +2555,6 @@ object:
|
|||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
name: foo
|
||||
namespace: test
|
||||
resourceVersion: "11"
|
||||
|
@ -2433,7 +2572,6 @@ object:
|
|||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
name: foo
|
||||
namespace: test
|
||||
resourceVersion: "12"
|
||||
|
|
|
@ -32,9 +32,6 @@ import (
|
|||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/cli-runtime/pkg/printers"
|
||||
"k8s.io/client-go/util/jsonpath"
|
||||
"k8s.io/utils/integer"
|
||||
|
||||
"github.com/fvbommel/sortorder"
|
||||
)
|
||||
|
||||
// SortingPrinter sorts list types before delegating to another printer.
|
||||
|
@ -181,7 +178,7 @@ func isLess(i, j reflect.Value) (bool, error) {
|
|||
case reflect.Float32, reflect.Float64:
|
||||
return i.Float() < j.Float(), nil
|
||||
case reflect.String:
|
||||
return sortorder.NaturalLess(i.String(), j.String()), nil
|
||||
return i.String() < j.String(), nil
|
||||
case reflect.Pointer:
|
||||
return isLess(i.Elem(), j.Elem())
|
||||
case reflect.Struct:
|
||||
|
@ -206,7 +203,7 @@ func isLess(i, j reflect.Value) (bool, error) {
|
|||
return true, nil
|
||||
case reflect.Array, reflect.Slice:
|
||||
// note: the length of i and j may be different
|
||||
for idx := 0; idx < integer.IntMin(i.Len(), j.Len()); idx++ {
|
||||
for idx := 0; idx < min(i.Len(), j.Len()); idx++ {
|
||||
less, err := isLess(i.Index(idx), j.Index(idx))
|
||||
if err != nil || !less {
|
||||
return less, err
|
||||
|
@ -275,11 +272,11 @@ func isLess(i, j reflect.Value) (bool, error) {
|
|||
// check if it's a Quantity
|
||||
itypeQuantity, err := resource.ParseQuantity(itype)
|
||||
if err != nil {
|
||||
return sortorder.NaturalLess(itype, jtype), nil
|
||||
return itype < jtype, nil
|
||||
}
|
||||
jtypeQuantity, err := resource.ParseQuantity(jtype)
|
||||
if err != nil {
|
||||
return sortorder.NaturalLess(itype, jtype), nil
|
||||
return itype < jtype, nil
|
||||
}
|
||||
// Both strings are quantity
|
||||
return itypeQuantity.Cmp(jtypeQuantity) < 0, nil
|
||||
|
|
|
@ -31,6 +31,7 @@ import (
|
|||
metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/kubectl/pkg/scheme"
|
||||
"k8s.io/utils/ptr"
|
||||
)
|
||||
|
||||
func toUnstructuredOrDie(data []byte) *unstructured.Unstructured {
|
||||
|
@ -103,8 +104,6 @@ func createUnstructuredPodResource(t *testing.T, memReq, memLimit, cpuReq, cpuLi
|
|||
}
|
||||
|
||||
func TestSortingPrinter(t *testing.T) {
|
||||
intPtr := func(val int32) *int32 { return &val }
|
||||
|
||||
a := &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "a",
|
||||
|
@ -272,17 +271,17 @@ func TestSortingPrinter(t *testing.T) {
|
|||
Items: []corev1.ReplicationController{
|
||||
{
|
||||
Spec: corev1.ReplicationControllerSpec{
|
||||
Replicas: intPtr(5),
|
||||
Replicas: ptr.To[int32](5),
|
||||
},
|
||||
},
|
||||
{
|
||||
Spec: corev1.ReplicationControllerSpec{
|
||||
Replicas: intPtr(1),
|
||||
Replicas: ptr.To[int32](1),
|
||||
},
|
||||
},
|
||||
{
|
||||
Spec: corev1.ReplicationControllerSpec{
|
||||
Replicas: intPtr(9),
|
||||
Replicas: ptr.To[int32](9),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -291,17 +290,17 @@ func TestSortingPrinter(t *testing.T) {
|
|||
Items: []corev1.ReplicationController{
|
||||
{
|
||||
Spec: corev1.ReplicationControllerSpec{
|
||||
Replicas: intPtr(1),
|
||||
Replicas: ptr.To[int32](1),
|
||||
},
|
||||
},
|
||||
{
|
||||
Spec: corev1.ReplicationControllerSpec{
|
||||
Replicas: intPtr(5),
|
||||
Replicas: ptr.To[int32](5),
|
||||
},
|
||||
},
|
||||
{
|
||||
Spec: corev1.ReplicationControllerSpec{
|
||||
Replicas: intPtr(9),
|
||||
Replicas: ptr.To[int32](9),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -21,8 +21,8 @@ import (
|
|||
"reflect"
|
||||
"strings"
|
||||
|
||||
jsonpatch "github.com/evanphx/json-patch"
|
||||
"github.com/spf13/cobra"
|
||||
jsonpatch "gopkg.in/evanphx/json-patch.v4"
|
||||
"k8s.io/klog/v2"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
|
@ -38,6 +38,7 @@ import (
|
|||
"k8s.io/cli-runtime/pkg/genericiooptions"
|
||||
"k8s.io/cli-runtime/pkg/printers"
|
||||
"k8s.io/cli-runtime/pkg/resource"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/scheme"
|
||||
"k8s.io/kubectl/pkg/util/completion"
|
||||
|
@ -204,7 +205,7 @@ func (o *LabelOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []st
|
|||
}
|
||||
|
||||
o.namespace, o.enforceNamespace, err = f.ToRawKubeConfigLoader().Namespace()
|
||||
if err != nil {
|
||||
if err != nil && !(o.local && clientcmd.IsEmptyConfig(err)) {
|
||||
return err
|
||||
}
|
||||
o.builder = f.NewBuilder()
|
||||
|
|
|
@ -29,6 +29,7 @@ import (
|
|||
"github.com/spf13/cobra"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
|
@ -40,6 +41,7 @@ import (
|
|||
"k8s.io/kubectl/pkg/util"
|
||||
"k8s.io/kubectl/pkg/util/completion"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
"k8s.io/kubectl/pkg/util/interrupt"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
)
|
||||
|
||||
|
@ -56,15 +58,33 @@ var (
|
|||
# Return snapshot logs from pod nginx with only one container
|
||||
kubectl logs nginx
|
||||
|
||||
# Return snapshot logs from pod nginx, prefixing each line with the source pod and container name
|
||||
kubectl logs nginx --prefix
|
||||
|
||||
# Return snapshot logs from pod nginx, limiting output to 500 bytes
|
||||
kubectl logs nginx --limit-bytes=500
|
||||
|
||||
# Return snapshot logs from pod nginx, waiting up to 20 seconds for it to start running.
|
||||
kubectl logs nginx --pod-running-timeout=20s
|
||||
|
||||
# Return snapshot logs from pod nginx with multi containers
|
||||
kubectl logs nginx --all-containers=true
|
||||
|
||||
# Return snapshot logs from all pods in the deployment nginx
|
||||
kubectl logs deployment/nginx --all-pods=true
|
||||
|
||||
# Return snapshot logs from all containers in pods defined by label app=nginx
|
||||
kubectl logs -l app=nginx --all-containers=true
|
||||
|
||||
# Return snapshot logs from all pods defined by label app=nginx, limiting concurrent log requests to 10 pods
|
||||
kubectl logs -l app=nginx --max-log-requests=10
|
||||
|
||||
# Return snapshot of previous terminated ruby container logs from pod web-1
|
||||
kubectl logs -p -c ruby web-1
|
||||
|
||||
# Begin streaming the logs from pod nginx, continuing even if errors occur
|
||||
kubectl logs nginx -f --ignore-errors=true
|
||||
|
||||
# Begin streaming the logs of the ruby container in pod web-1
|
||||
kubectl logs -f -c ruby web-1
|
||||
|
||||
|
@ -76,6 +96,9 @@ var (
|
|||
|
||||
# Show all logs from pod nginx written in the last hour
|
||||
kubectl logs --since=1h nginx
|
||||
|
||||
# Show all logs with timestamps from pod nginx starting from August 30, 2024, at 06:00:00 UTC
|
||||
kubectl logs nginx --since-time=2024-08-30T06:00:00Z --timestamps=true
|
||||
|
||||
# Show logs from a kubelet with an expired serving certificate
|
||||
kubectl logs --insecure-skip-tls-verify-backend nginx
|
||||
|
@ -98,10 +121,11 @@ type LogsOptions struct {
|
|||
Namespace string
|
||||
ResourceArg string
|
||||
AllContainers bool
|
||||
AllPods bool
|
||||
Options runtime.Object
|
||||
Resources []string
|
||||
|
||||
ConsumeRequestFn func(rest.ResponseWrapper, io.Writer) error
|
||||
ConsumeRequestFn func(context.Context, rest.ResponseWrapper, io.Writer) error
|
||||
|
||||
// PodLogOptions
|
||||
SinceTime string
|
||||
|
@ -121,10 +145,11 @@ type LogsOptions struct {
|
|||
MaxFollowConcurrency int
|
||||
Prefix bool
|
||||
|
||||
Object runtime.Object
|
||||
GetPodTimeout time.Duration
|
||||
RESTClientGetter genericclioptions.RESTClientGetter
|
||||
LogsForObject polymorphichelpers.LogsForObjectFunc
|
||||
Object runtime.Object
|
||||
GetPodTimeout time.Duration
|
||||
RESTClientGetter genericclioptions.RESTClientGetter
|
||||
LogsForObject polymorphichelpers.LogsForObjectFunc
|
||||
AllPodLogsForObject polymorphichelpers.AllPodLogsForObjectFunc
|
||||
|
||||
genericiooptions.IOStreams
|
||||
|
||||
|
@ -133,10 +158,9 @@ type LogsOptions struct {
|
|||
containerNameFromRefSpecRegexp *regexp.Regexp
|
||||
}
|
||||
|
||||
func NewLogsOptions(streams genericiooptions.IOStreams, allContainers bool) *LogsOptions {
|
||||
func NewLogsOptions(streams genericiooptions.IOStreams) *LogsOptions {
|
||||
return &LogsOptions{
|
||||
IOStreams: streams,
|
||||
AllContainers: allContainers,
|
||||
Tail: -1,
|
||||
MaxFollowConcurrency: 5,
|
||||
|
||||
|
@ -146,7 +170,7 @@ func NewLogsOptions(streams genericiooptions.IOStreams, allContainers bool) *Log
|
|||
|
||||
// NewCmdLogs creates a new pod logs command
|
||||
func NewCmdLogs(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command {
|
||||
o := NewLogsOptions(streams, false)
|
||||
o := NewLogsOptions(streams)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: logsUsageStr,
|
||||
|
@ -166,6 +190,7 @@ func NewCmdLogs(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Co
|
|||
}
|
||||
|
||||
func (o *LogsOptions) AddFlags(cmd *cobra.Command) {
|
||||
cmd.Flags().BoolVar(&o.AllPods, "all-pods", o.AllPods, "Get logs from all pod(s). Sets prefix to true.")
|
||||
cmd.Flags().BoolVar(&o.AllContainers, "all-containers", o.AllContainers, "Get all containers' logs in the pod(s).")
|
||||
cmd.Flags().BoolVarP(&o.Follow, "follow", "f", o.Follow, "Specify if the logs should be streamed.")
|
||||
cmd.Flags().BoolVar(&o.Timestamps, "timestamps", o.Timestamps, "Include timestamps on each line in the log output")
|
||||
|
@ -242,6 +267,11 @@ func (o *LogsOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []str
|
|||
default:
|
||||
return cmdutil.UsageErrorf(cmd, "%s", logsUsageErrStr)
|
||||
}
|
||||
|
||||
if o.AllPods {
|
||||
o.Prefix = true
|
||||
}
|
||||
|
||||
var err error
|
||||
o.Namespace, _, err = f.ToRawKubeConfigLoader().Namespace()
|
||||
if err != nil {
|
||||
|
@ -262,6 +292,7 @@ func (o *LogsOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []str
|
|||
|
||||
o.RESTClientGetter = f
|
||||
o.LogsForObject = polymorphichelpers.LogsForObjectFn
|
||||
o.AllPodLogsForObject = polymorphichelpers.AllPodLogsForObjectFn
|
||||
|
||||
if o.Object == nil {
|
||||
builder := f.NewBuilder().
|
||||
|
@ -276,6 +307,9 @@ func (o *LogsOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []str
|
|||
}
|
||||
infos, err := builder.Do().Infos()
|
||||
if err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
err = fmt.Errorf("error from server (NotFound): %w in namespace %q", err, o.Namespace)
|
||||
}
|
||||
return err
|
||||
}
|
||||
if o.Selector == "" && len(infos) != 1 {
|
||||
|
@ -324,7 +358,13 @@ func (o LogsOptions) Validate() error {
|
|||
|
||||
// RunLogs retrieves a pod log
|
||||
func (o LogsOptions) RunLogs() error {
|
||||
requests, err := o.LogsForObject(o.RESTClientGetter, o.Object, o.Options, o.GetPodTimeout, o.AllContainers)
|
||||
var requests map[corev1.ObjectReference]rest.ResponseWrapper
|
||||
var err error
|
||||
if o.AllPods {
|
||||
requests, err = o.AllPodLogsForObject(o.RESTClientGetter, o.Object, o.Options, o.GetPodTimeout, o.AllContainers)
|
||||
} else {
|
||||
requests, err = o.LogsForObject(o.RESTClientGetter, o.Object, o.Options, o.GetPodTimeout, o.AllContainers)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -336,14 +376,21 @@ func (o LogsOptions) RunLogs() error {
|
|||
len(requests), o.MaxFollowConcurrency,
|
||||
)
|
||||
}
|
||||
|
||||
return o.parallelConsumeRequest(requests)
|
||||
}
|
||||
|
||||
return o.sequentialConsumeRequest(requests)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
intr := interrupt.New(nil, cancel)
|
||||
return intr.Run(func() error {
|
||||
if o.Follow && len(requests) > 1 {
|
||||
return o.parallelConsumeRequest(ctx, requests)
|
||||
}
|
||||
|
||||
return o.sequentialConsumeRequest(ctx, requests)
|
||||
})
|
||||
}
|
||||
|
||||
func (o LogsOptions) parallelConsumeRequest(requests map[corev1.ObjectReference]rest.ResponseWrapper) error {
|
||||
func (o LogsOptions) parallelConsumeRequest(ctx context.Context, requests map[corev1.ObjectReference]rest.ResponseWrapper) error {
|
||||
reader, writer := io.Pipe()
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(len(requests))
|
||||
|
@ -351,7 +398,7 @@ func (o LogsOptions) parallelConsumeRequest(requests map[corev1.ObjectReference]
|
|||
go func(objRef corev1.ObjectReference, request rest.ResponseWrapper) {
|
||||
defer wg.Done()
|
||||
out := o.addPrefixIfNeeded(objRef, writer)
|
||||
if err := o.ConsumeRequestFn(request, out); err != nil {
|
||||
if err := o.ConsumeRequestFn(ctx, request, out); err != nil {
|
||||
if !o.IgnoreLogErrors {
|
||||
writer.CloseWithError(err)
|
||||
|
||||
|
@ -374,10 +421,10 @@ func (o LogsOptions) parallelConsumeRequest(requests map[corev1.ObjectReference]
|
|||
return err
|
||||
}
|
||||
|
||||
func (o LogsOptions) sequentialConsumeRequest(requests map[corev1.ObjectReference]rest.ResponseWrapper) error {
|
||||
func (o LogsOptions) sequentialConsumeRequest(ctx context.Context, requests map[corev1.ObjectReference]rest.ResponseWrapper) error {
|
||||
for objRef, request := range requests {
|
||||
out := o.addPrefixIfNeeded(objRef, o.Out)
|
||||
if err := o.ConsumeRequestFn(request, out); err != nil {
|
||||
if err := o.ConsumeRequestFn(ctx, request, out); err != nil {
|
||||
if !o.IgnoreLogErrors {
|
||||
return err
|
||||
}
|
||||
|
@ -418,8 +465,8 @@ func (o LogsOptions) addPrefixIfNeeded(ref corev1.ObjectReference, writer io.Wri
|
|||
// A successful read returns err == nil, not err == io.EOF.
|
||||
// Because the function is defined to read from request until io.EOF, it does
|
||||
// not treat an io.EOF as an error to be reported.
|
||||
func DefaultConsumeRequest(request rest.ResponseWrapper, out io.Writer) error {
|
||||
readCloser, err := request.Stream(context.TODO())
|
||||
func DefaultConsumeRequest(ctx context.Context, request rest.ResponseWrapper, out io.Writer) error {
|
||||
readCloser, err := request.Stream(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -29,9 +29,12 @@ import (
|
|||
"testing/iotest"
|
||||
"time"
|
||||
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/cli-runtime/pkg/genericiooptions"
|
||||
restclient "k8s.io/client-go/rest"
|
||||
|
@ -60,7 +63,7 @@ func TestLog(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
o := NewLogsOptions(streams, false)
|
||||
o := NewLogsOptions(streams)
|
||||
o.LogsForObject = mock.mockLogsForObject
|
||||
o.ConsumeRequestFn = mock.mockConsumeRequest
|
||||
|
||||
|
@ -81,7 +84,7 @@ func TestLog(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
o := NewLogsOptions(streams, false)
|
||||
o := NewLogsOptions(streams)
|
||||
o.LogsForObject = mock.mockLogsForObject
|
||||
o.ConsumeRequestFn = mock.mockConsumeRequest
|
||||
o.Prefix = true
|
||||
|
@ -90,6 +93,35 @@ func TestLog(t *testing.T) {
|
|||
},
|
||||
expectedOutSubstrings: []string{"[pod/test-pod/test-container] test log content\n"},
|
||||
},
|
||||
{
|
||||
name: "stateful set logs with all pods",
|
||||
opts: func(streams genericiooptions.IOStreams) *LogsOptions {
|
||||
mock := &logTestMock{
|
||||
logsForObjectRequests: map[corev1.ObjectReference]restclient.ResponseWrapper{
|
||||
{
|
||||
Kind: "Pod",
|
||||
Name: "test-sts-0",
|
||||
FieldPath: "spec.containers{test-container}",
|
||||
}: &responseWrapperMock{data: strings.NewReader("test log content for pod test-sts-0\n")},
|
||||
{
|
||||
Kind: "Pod",
|
||||
Name: "test-sts-1",
|
||||
FieldPath: "spec.containers{test-container}",
|
||||
}: &responseWrapperMock{data: strings.NewReader("test log content for pod test-sts-1\n")},
|
||||
},
|
||||
}
|
||||
|
||||
o := NewLogsOptions(streams)
|
||||
o.LogsForObject = mock.mockLogsForObject
|
||||
o.ConsumeRequestFn = mock.mockConsumeRequest
|
||||
o.Prefix = true
|
||||
return o
|
||||
},
|
||||
expectedOutSubstrings: []string{
|
||||
"[pod/test-sts-0/test-container] test log content for pod test-sts-0\n",
|
||||
"[pod/test-sts-1/test-container] test log content for pod test-sts-1\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "pod logs with prefix: init container",
|
||||
opts: func(streams genericiooptions.IOStreams) *LogsOptions {
|
||||
|
@ -103,7 +135,7 @@ func TestLog(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
o := NewLogsOptions(streams, false)
|
||||
o := NewLogsOptions(streams)
|
||||
o.LogsForObject = mock.mockLogsForObject
|
||||
o.ConsumeRequestFn = mock.mockConsumeRequest
|
||||
o.Prefix = true
|
||||
|
@ -125,7 +157,7 @@ func TestLog(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
o := NewLogsOptions(streams, false)
|
||||
o := NewLogsOptions(streams)
|
||||
o.LogsForObject = mock.mockLogsForObject
|
||||
o.ConsumeRequestFn = mock.mockConsumeRequest
|
||||
o.Prefix = true
|
||||
|
@ -157,7 +189,7 @@ func TestLog(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
o := NewLogsOptions(streams, false)
|
||||
o := NewLogsOptions(streams)
|
||||
o.LogsForObject = mock.mockLogsForObject
|
||||
o.ConsumeRequestFn = mock.mockConsumeRequest
|
||||
return o
|
||||
|
@ -194,7 +226,7 @@ func TestLog(t *testing.T) {
|
|||
}
|
||||
wg.Add(3)
|
||||
|
||||
o := NewLogsOptions(streams, false)
|
||||
o := NewLogsOptions(streams)
|
||||
o.LogsForObject = mock.mockLogsForObject
|
||||
o.ConsumeRequestFn = mock.mockConsumeRequest
|
||||
o.Follow = true
|
||||
|
@ -232,7 +264,7 @@ func TestLog(t *testing.T) {
|
|||
}
|
||||
wg.Add(3)
|
||||
|
||||
o := NewLogsOptions(streams, false)
|
||||
o := NewLogsOptions(streams)
|
||||
o.LogsForObject = mock.mockLogsForObject
|
||||
o.ConsumeRequestFn = mock.mockConsumeRequest
|
||||
o.MaxFollowConcurrency = 2
|
||||
|
@ -244,7 +276,7 @@ func TestLog(t *testing.T) {
|
|||
{
|
||||
name: "fail if LogsForObject fails",
|
||||
opts: func(streams genericiooptions.IOStreams) *LogsOptions {
|
||||
o := NewLogsOptions(streams, false)
|
||||
o := NewLogsOptions(streams)
|
||||
o.LogsForObject = func(restClientGetter genericclioptions.RESTClientGetter, object, options runtime.Object, timeout time.Duration, allContainers bool) (map[corev1.ObjectReference]restclient.ResponseWrapper, error) {
|
||||
return nil, errors.New("Error from the LogsForObject")
|
||||
}
|
||||
|
@ -270,9 +302,9 @@ func TestLog(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
o := NewLogsOptions(streams, false)
|
||||
o := NewLogsOptions(streams)
|
||||
o.LogsForObject = mock.mockLogsForObject
|
||||
o.ConsumeRequestFn = func(req restclient.ResponseWrapper, out io.Writer) error {
|
||||
o.ConsumeRequestFn = func(ctx context.Context, req restclient.ResponseWrapper, out io.Writer) error {
|
||||
return errors.New("Error from the ConsumeRequestFn")
|
||||
}
|
||||
return o
|
||||
|
@ -305,7 +337,7 @@ func TestLog(t *testing.T) {
|
|||
}
|
||||
wg.Add(3)
|
||||
|
||||
o := NewLogsOptions(streams, false)
|
||||
o := NewLogsOptions(streams)
|
||||
o.LogsForObject = mock.mockLogsForObject
|
||||
o.ConsumeRequestFn = mock.mockConsumeRequest
|
||||
o.Follow = true
|
||||
|
@ -344,9 +376,9 @@ func TestLog(t *testing.T) {
|
|||
}
|
||||
wg.Add(3)
|
||||
|
||||
o := NewLogsOptions(streams, false)
|
||||
o := NewLogsOptions(streams)
|
||||
o.LogsForObject = mock.mockLogsForObject
|
||||
o.ConsumeRequestFn = func(req restclient.ResponseWrapper, out io.Writer) error {
|
||||
o.ConsumeRequestFn = func(ctx context.Context, req restclient.ResponseWrapper, out io.Writer) error {
|
||||
return errors.New("Error from the ConsumeRequestFn")
|
||||
}
|
||||
o.Follow = true
|
||||
|
@ -367,9 +399,9 @@ func TestLog(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
o := NewLogsOptions(streams, false)
|
||||
o := NewLogsOptions(streams)
|
||||
o.LogsForObject = mock.mockLogsForObject
|
||||
o.ConsumeRequestFn = func(req restclient.ResponseWrapper, out io.Writer) error {
|
||||
o.ConsumeRequestFn = func(ctx context.Context, req restclient.ResponseWrapper, out io.Writer) error {
|
||||
return errors.New("Error from the ConsumeRequestFn")
|
||||
}
|
||||
o.Follow = true
|
||||
|
@ -400,7 +432,7 @@ func TestLog(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
o := NewLogsOptions(streams, false)
|
||||
o := NewLogsOptions(streams)
|
||||
o.LogsForObject = mock.mockLogsForObject
|
||||
o.ConsumeRequestFn = mock.mockConsumeRequest
|
||||
o.IgnoreLogErrors = true
|
||||
|
@ -430,7 +462,7 @@ func TestLog(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
o := NewLogsOptions(streams, false)
|
||||
o := NewLogsOptions(streams)
|
||||
o.LogsForObject = mock.mockLogsForObject
|
||||
o.ConsumeRequestFn = mock.mockConsumeRequest
|
||||
return o
|
||||
|
@ -460,7 +492,7 @@ func TestLog(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
o := NewLogsOptions(streams, false)
|
||||
o := NewLogsOptions(streams)
|
||||
o.LogsForObject = mock.mockLogsForObject
|
||||
o.ConsumeRequestFn = mock.mockConsumeRequest
|
||||
o.IgnoreLogErrors = true
|
||||
|
@ -491,7 +523,7 @@ func TestLog(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
o := NewLogsOptions(streams, false)
|
||||
o := NewLogsOptions(streams)
|
||||
o.LogsForObject = mock.mockLogsForObject
|
||||
o.ConsumeRequestFn = mock.mockConsumeRequest
|
||||
o.Follow = true
|
||||
|
@ -562,7 +594,7 @@ func TestValidateLogOptions(t *testing.T) {
|
|||
{
|
||||
name: "since & since-time",
|
||||
opts: func(streams genericiooptions.IOStreams) *LogsOptions {
|
||||
o := NewLogsOptions(streams, false)
|
||||
o := NewLogsOptions(streams)
|
||||
o.SinceSeconds = time.Hour
|
||||
o.SinceTime = "2006-01-02T15:04:05Z"
|
||||
|
||||
|
@ -580,7 +612,7 @@ func TestValidateLogOptions(t *testing.T) {
|
|||
{
|
||||
name: "negative since-time",
|
||||
opts: func(streams genericiooptions.IOStreams) *LogsOptions {
|
||||
o := NewLogsOptions(streams, false)
|
||||
o := NewLogsOptions(streams)
|
||||
o.SinceSeconds = -1 * time.Second
|
||||
|
||||
var err error
|
||||
|
@ -597,7 +629,7 @@ func TestValidateLogOptions(t *testing.T) {
|
|||
{
|
||||
name: "negative limit-bytes",
|
||||
opts: func(streams genericiooptions.IOStreams) *LogsOptions {
|
||||
o := NewLogsOptions(streams, false)
|
||||
o := NewLogsOptions(streams)
|
||||
o.LimitBytes = -100
|
||||
|
||||
var err error
|
||||
|
@ -614,7 +646,7 @@ func TestValidateLogOptions(t *testing.T) {
|
|||
{
|
||||
name: "negative tail",
|
||||
opts: func(streams genericiooptions.IOStreams) *LogsOptions {
|
||||
o := NewLogsOptions(streams, false)
|
||||
o := NewLogsOptions(streams)
|
||||
o.Tail = -100
|
||||
|
||||
var err error
|
||||
|
@ -631,7 +663,8 @@ func TestValidateLogOptions(t *testing.T) {
|
|||
{
|
||||
name: "container name combined with --all-containers",
|
||||
opts: func(streams genericiooptions.IOStreams) *LogsOptions {
|
||||
o := NewLogsOptions(streams, true)
|
||||
o := NewLogsOptions(streams)
|
||||
o.AllContainers = true
|
||||
o.Container = "my-container"
|
||||
|
||||
var err error
|
||||
|
@ -648,7 +681,7 @@ func TestValidateLogOptions(t *testing.T) {
|
|||
{
|
||||
name: "container name combined with second argument",
|
||||
opts: func(streams genericiooptions.IOStreams) *LogsOptions {
|
||||
o := NewLogsOptions(streams, false)
|
||||
o := NewLogsOptions(streams)
|
||||
o.Container = "my-container"
|
||||
o.ContainerNameSpecified = true
|
||||
|
||||
|
@ -695,7 +728,7 @@ func TestLogComplete(t *testing.T) {
|
|||
name: "One args case",
|
||||
args: []string{"foo"},
|
||||
opts: func(streams genericiooptions.IOStreams) *LogsOptions {
|
||||
o := NewLogsOptions(streams, false)
|
||||
o := NewLogsOptions(streams)
|
||||
o.Selector = "foo"
|
||||
return o
|
||||
},
|
||||
|
@ -775,7 +808,7 @@ func TestDefaultConsumeRequest(t *testing.T) {
|
|||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
buf := &bytes.Buffer{}
|
||||
err := DefaultConsumeRequest(test.request, buf)
|
||||
err := DefaultConsumeRequest(context.TODO(), test.request, buf)
|
||||
|
||||
if err != nil && !strings.Contains(err.Error(), test.expectedErr) {
|
||||
t.Errorf("%s: expected to find:\n\t%s\nfound:\n\t%s\n", test.name, test.expectedErr, err.Error())
|
||||
|
@ -814,7 +847,7 @@ func TestNoResourceFoundMessage(t *testing.T) {
|
|||
|
||||
streams, _, buf, errbuf := genericiooptions.NewTestIOStreams()
|
||||
cmd := NewCmdLogs(tf, streams)
|
||||
o := NewLogsOptions(streams, false)
|
||||
o := NewLogsOptions(streams)
|
||||
o.Selector = "foo"
|
||||
err := o.Complete(tf, cmd, []string{})
|
||||
|
||||
|
@ -833,6 +866,48 @@ func TestNoResourceFoundMessage(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestNoPodInNamespaceFoundMessage(t *testing.T) {
|
||||
namespace, podName := "test", "bar"
|
||||
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace(namespace)
|
||||
defer tf.Cleanup()
|
||||
|
||||
ns := scheme.Codecs.WithoutConversion()
|
||||
codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
|
||||
errStatus := apierrors.NewNotFound(schema.GroupResource{Resource: "pods"}, podName).Status()
|
||||
|
||||
tf.UnstructuredClient = &fake.RESTClient{
|
||||
NegotiatedSerializer: ns,
|
||||
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||
switch req.URL.Path {
|
||||
case fmt.Sprintf("/namespaces/%s/pods/%s", namespace, podName):
|
||||
fallthrough
|
||||
case fmt.Sprintf("/namespaces/%s/pods", namespace):
|
||||
fallthrough
|
||||
case fmt.Sprintf("/api/v1/namespaces/%s", namespace):
|
||||
return &http.Response{StatusCode: http.StatusNotFound, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &errStatus)}, nil
|
||||
default:
|
||||
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
|
||||
return nil, nil
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
streams, _, _, _ := genericiooptions.NewTestIOStreams()
|
||||
cmd := NewCmdLogs(tf, streams)
|
||||
o := NewLogsOptions(streams)
|
||||
err := o.Complete(tf, cmd, []string{podName})
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("Expected NotFound error, got nil")
|
||||
}
|
||||
|
||||
expected := fmt.Sprintf("error from server (NotFound): pods %q not found in namespace %q", podName, namespace)
|
||||
if e, a := expected, err.Error(); e != a {
|
||||
t.Errorf("expected to find:\n\t%s\nfound:\n\t%s\n", e, a)
|
||||
}
|
||||
}
|
||||
|
||||
type responseWrapperMock struct {
|
||||
data io.Reader
|
||||
err error
|
||||
|
@ -857,8 +932,8 @@ type logTestMock struct {
|
|||
wg *sync.WaitGroup
|
||||
}
|
||||
|
||||
func (l *logTestMock) mockConsumeRequest(request restclient.ResponseWrapper, out io.Writer) error {
|
||||
readCloser, err := request.Stream(context.Background())
|
||||
func (l *logTestMock) mockConsumeRequest(ctx context.Context, request restclient.ResponseWrapper, out io.Writer) error {
|
||||
readCloser, err := request.Stream(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -875,6 +950,13 @@ func (l *logTestMock) mockConsumeRequest(request restclient.ResponseWrapper, out
|
|||
|
||||
func (l *logTestMock) mockLogsForObject(restClientGetter genericclioptions.RESTClientGetter, object, options runtime.Object, timeout time.Duration, allContainers bool) (map[corev1.ObjectReference]restclient.ResponseWrapper, error) {
|
||||
switch object.(type) {
|
||||
case *appsv1.Deployment:
|
||||
_, ok := options.(*corev1.PodLogOptions)
|
||||
if !ok {
|
||||
return nil, errors.New("provided options object is not a PodLogOptions")
|
||||
}
|
||||
|
||||
return l.logsForObjectRequests, nil
|
||||
case *corev1.Pod:
|
||||
_, ok := options.(*corev1.PodLogOptions)
|
||||
if !ok {
|
||||
|
|
|
@ -22,9 +22,8 @@ import (
|
|||
"reflect"
|
||||
"strings"
|
||||
|
||||
jsonpatch "github.com/evanphx/json-patch"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
jsonpatch "gopkg.in/evanphx/json-patch.v4"
|
||||
"k8s.io/klog/v2"
|
||||
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
|
@ -40,11 +39,11 @@ import (
|
|||
"k8s.io/cli-runtime/pkg/genericiooptions"
|
||||
"k8s.io/cli-runtime/pkg/printers"
|
||||
"k8s.io/cli-runtime/pkg/resource"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/scheme"
|
||||
"k8s.io/kubectl/pkg/util/completion"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
"k8s.io/kubectl/pkg/util/slice"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
)
|
||||
|
||||
|
@ -106,8 +105,6 @@ var (
|
|||
kubectl patch deployment nginx-deployment --subresource='scale' --type='merge' -p '{"spec":{"replicas":2}}'`))
|
||||
)
|
||||
|
||||
var supportedSubresources = []string{"status", "scale"}
|
||||
|
||||
func NewPatchOptions(ioStreams genericiooptions.IOStreams) *PatchOptions {
|
||||
return &PatchOptions{
|
||||
RecordFlags: genericclioptions.NewRecordFlags(),
|
||||
|
@ -139,12 +136,12 @@ func NewCmdPatch(f cmdutil.Factory, ioStreams genericiooptions.IOStreams) *cobra
|
|||
|
||||
cmd.Flags().StringVarP(&o.Patch, "patch", "p", "", "The patch to be applied to the resource JSON file.")
|
||||
cmd.Flags().StringVar(&o.PatchFile, "patch-file", "", "A file containing a patch to be applied to the resource.")
|
||||
cmd.Flags().StringVar(&o.PatchType, "type", "strategic", fmt.Sprintf("The type of patch being provided; one of %v", sets.StringKeySet(patchTypes).List()))
|
||||
cmd.Flags().StringVar(&o.PatchType, "type", "strategic", fmt.Sprintf("The type of patch being provided; one of %v", sets.List(sets.KeySet(patchTypes))))
|
||||
cmdutil.AddDryRunFlag(cmd)
|
||||
cmdutil.AddFilenameOptionFlags(cmd, &o.FilenameOptions, "identifying the resource to update")
|
||||
cmd.Flags().BoolVar(&o.Local, "local", o.Local, "If true, patch will operate on the content of the file, not the server-side resource.")
|
||||
cmdutil.AddFieldManagerFlagVar(cmd, &o.fieldManager, "kubectl-patch")
|
||||
cmdutil.AddSubresourceFlags(cmd, &o.Subresource, "If specified, patch will operate on the subresource of the requested object.", supportedSubresources...)
|
||||
cmdutil.AddSubresourceFlags(cmd, &o.Subresource, "If specified, patch will operate on the subresource of the requested object.")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
@ -171,7 +168,7 @@ func (o *PatchOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []st
|
|||
}
|
||||
|
||||
o.namespace, o.enforceNamespace, err = f.ToRawKubeConfigLoader().Namespace()
|
||||
if err != nil {
|
||||
if err != nil && !(o.Local && clientcmd.IsEmptyConfig(err)) {
|
||||
return err
|
||||
}
|
||||
o.args = args
|
||||
|
@ -196,12 +193,9 @@ func (o *PatchOptions) Validate() error {
|
|||
}
|
||||
if len(o.PatchType) != 0 {
|
||||
if _, ok := patchTypes[strings.ToLower(o.PatchType)]; !ok {
|
||||
return fmt.Errorf("--type must be one of %v, not %q", sets.StringKeySet(patchTypes).List(), o.PatchType)
|
||||
return fmt.Errorf("--type must be one of %v, not %q", sets.List(sets.KeySet(patchTypes)), o.PatchType)
|
||||
}
|
||||
}
|
||||
if len(o.Subresource) > 0 && !slice.ContainsString(supportedSubresources, o.Subresource, nil) {
|
||||
return fmt.Errorf("invalid subresource value: %q. Must be one of %v", o.Subresource, supportedSubresources)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -265,7 +259,7 @@ func (o *PatchOptions) RunPatch() error {
|
|||
patchedObj, err := helper.Patch(namespace, name, patchType, patchBytes, nil)
|
||||
if err != nil {
|
||||
if apierrors.IsUnsupportedMediaType(err) {
|
||||
return errors.Wrap(err, fmt.Sprintf("%s is not supported by %s", patchType, mapping.GroupVersionKind))
|
||||
return fmt.Errorf("%s is not supported by %s: %w", patchType, mapping.GroupVersionKind, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -40,15 +40,19 @@ var (
|
|||
Plugins provide extended functionality that is not part of the major command-line distribution.
|
||||
Please refer to the documentation and examples for more information about how write your own plugins.
|
||||
|
||||
The easiest way to discover and install plugins is via the kubernetes sub-project krew.
|
||||
To install krew, visit [krew.sigs.k8s.io](https://krew.sigs.k8s.io/docs/user-guide/setup/install/)`))
|
||||
The easiest way to discover and install plugins is via the kubernetes sub-project krew: [krew.sigs.k8s.io].
|
||||
To install krew, visit https://krew.sigs.k8s.io/docs/user-guide/setup/install`))
|
||||
|
||||
pluginExample = templates.Examples(i18n.T(`
|
||||
# List all available plugins
|
||||
kubectl plugin list`))
|
||||
kubectl plugin list
|
||||
|
||||
# List only binary names of available plugins without paths
|
||||
kubectl plugin list --name-only`))
|
||||
|
||||
pluginListLong = templates.LongDesc(i18n.T(`
|
||||
List all available plugin files on a user's PATH.
|
||||
To see plugins binary names without the full path use --name-only flag.
|
||||
|
||||
Available plugin files are those that are:
|
||||
- executable
|
||||
|
@ -65,6 +69,7 @@ func NewCmdPlugin(streams genericiooptions.IOStreams) *cobra.Command {
|
|||
DisableFlagsInUseLine: true,
|
||||
Short: i18n.T("Provides utilities for interacting with plugins"),
|
||||
Long: pluginLong,
|
||||
Example: pluginExample,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.DefaultSubCommandRun(streams.ErrOut)(cmd, args)
|
||||
},
|
||||
|
|
|
@ -27,14 +27,24 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"k8s.io/cli-runtime/pkg/genericiooptions"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
)
|
||||
|
||||
func GetPluginCommandGroup(kubectl *cobra.Command) templates.CommandGroup {
|
||||
// Find root level
|
||||
return templates.CommandGroup{
|
||||
Message: i18n.T("Subcommands provided by plugins:"),
|
||||
Commands: registerPluginCommands(kubectl, false),
|
||||
}
|
||||
}
|
||||
|
||||
// SetupPluginCompletion adds a Cobra command to the command tree for each
|
||||
// plugin. This is only done when performing shell completion that relate
|
||||
// to plugins.
|
||||
func SetupPluginCompletion(cmd *cobra.Command, args []string) {
|
||||
kubectl := cmd.Root()
|
||||
if len(args) > 0 {
|
||||
if strings.HasPrefix(args[0], "-") {
|
||||
// Plugins are not supported if the first argument is a flag,
|
||||
|
@ -45,7 +55,7 @@ func SetupPluginCompletion(cmd *cobra.Command, args []string) {
|
|||
if len(args) == 1 {
|
||||
// We are completing a subcommand at the first level so
|
||||
// we should include all plugins names.
|
||||
addPluginCommands(cmd)
|
||||
registerPluginCommands(kubectl, true)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -54,7 +64,7 @@ func SetupPluginCompletion(cmd *cobra.Command, args []string) {
|
|||
// If we don't it could be a plugin and we'll need to add
|
||||
// the plugin commands for completion to work.
|
||||
found := false
|
||||
for _, subCmd := range cmd.Root().Commands() {
|
||||
for _, subCmd := range kubectl.Commands() {
|
||||
if args[0] == subCmd.Name() {
|
||||
found = true
|
||||
break
|
||||
|
@ -70,19 +80,20 @@ func SetupPluginCompletion(cmd *cobra.Command, args []string) {
|
|||
// to avoid them being included in the completion choices.
|
||||
// This must be done *before* adding the plugin commands so that
|
||||
// when creating those plugin commands, the flags don't exist.
|
||||
cmd.Root().ResetFlags()
|
||||
kubectl.ResetFlags()
|
||||
cobra.CompDebugln("Cleared global flags for plugin completion", true)
|
||||
|
||||
addPluginCommands(cmd)
|
||||
registerPluginCommands(kubectl, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// addPluginCommand adds a Cobra command to the command tree
|
||||
// for each plugin so that the completion logic knows about the plugins
|
||||
func addPluginCommands(cmd *cobra.Command) {
|
||||
kubectl := cmd.Root()
|
||||
streams := genericiooptions.IOStreams{
|
||||
// registerPluginCommand allows adding Cobra command to the command tree or extracting them for usage in
|
||||
// e.g. the help function or for registering the completion function
|
||||
func registerPluginCommands(kubectl *cobra.Command, list bool) (cmds []*cobra.Command) {
|
||||
userDefinedCommands := []*cobra.Command{}
|
||||
|
||||
streams := genericclioptions.IOStreams{
|
||||
In: &bytes.Buffer{},
|
||||
Out: io.Discard,
|
||||
ErrOut: io.Discard,
|
||||
|
@ -98,10 +109,18 @@ func addPluginCommands(cmd *cobra.Command) {
|
|||
|
||||
// Plugins are named "kubectl-<name>" or with more - such as
|
||||
// "kubectl-<name>-<subcmd1>..."
|
||||
for _, arg := range strings.Split(plugin, "-")[1:] {
|
||||
rawPluginArgs := strings.Split(plugin, "-")[1:]
|
||||
pluginArgs := rawPluginArgs[:1]
|
||||
if list {
|
||||
pluginArgs = rawPluginArgs
|
||||
}
|
||||
|
||||
// Iterate through all segments, for kubectl-my_plugin-sub_cmd, we will end up with
|
||||
// two iterations: one for my_plugin and one for sub_cmd.
|
||||
for _, arg := range pluginArgs {
|
||||
// Underscores (_) in plugin's filename are replaced with dashes(-)
|
||||
// e.g. foo_bar -> foo-bar
|
||||
args = append(args, strings.Replace(arg, "_", "-", -1))
|
||||
args = append(args, strings.ReplaceAll(arg, "_", "-"))
|
||||
}
|
||||
|
||||
// In order to avoid that the same plugin command is added more than once,
|
||||
|
@ -117,17 +136,24 @@ func addPluginCommands(cmd *cobra.Command) {
|
|||
// Add a description that will be shown with completion choices.
|
||||
// Make each one different by including the plugin name to avoid
|
||||
// all plugins being grouped in a single line during completion for zsh.
|
||||
Short: fmt.Sprintf("The command %s is a plugin installed by the user", remainingArg),
|
||||
Short: fmt.Sprintf(i18n.T("The command %s is a plugin installed by the user"), remainingArg),
|
||||
DisableFlagParsing: true,
|
||||
// Allow plugins to provide their own completion choices
|
||||
ValidArgsFunction: pluginCompletion,
|
||||
// A Run is required for it to be a valid command
|
||||
Run: func(cmd *cobra.Command, args []string) {},
|
||||
}
|
||||
parentCmd.AddCommand(cmd)
|
||||
parentCmd = cmd
|
||||
// Add the plugin command to the list of user defined commands
|
||||
userDefinedCommands = append(userDefinedCommands, cmd)
|
||||
|
||||
if list {
|
||||
parentCmd.AddCommand(cmd)
|
||||
parentCmd = cmd
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return userDefinedCommands
|
||||
}
|
||||
|
||||
// pluginCompletion deals with shell completion beyond the plugin name, it allows to complete
|
||||
|
@ -161,7 +187,7 @@ func addPluginCommands(cmd *cobra.Command) {
|
|||
// executable must have executable permissions set on it and must be on $PATH.
|
||||
func pluginCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
// Recreate the plugin name from the commandPath
|
||||
pluginName := strings.Replace(strings.Replace(cmd.CommandPath(), "-", "_", -1), " ", "-", -1)
|
||||
pluginName := strings.ReplaceAll(strings.ReplaceAll(cmd.CommandPath(), "-", "_"), " ", "-")
|
||||
|
||||
path, found := lookupCompletionExec(pluginName)
|
||||
if !found {
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue