Compare commits
528 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 | |
|
9a67ed3d47 | |
|
b32cbdf03a | |
|
1d05cd7a4e | |
|
0ee0d225f2 | |
|
58079640cc |
|
@ -10,7 +10,7 @@
|
|||
# DO NOT REPORT SECURITY VULNERABILITIES DIRECTLY TO THESE NAMES, FOLLOW THE
|
||||
# INSTRUCTIONS AT https://kubernetes.io/security/
|
||||
|
||||
ardaguclu
|
||||
eddiezane
|
||||
KnVerey
|
||||
natasha41575
|
||||
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
|
||||
|
|
131
go.mod
131
go.mod
|
@ -2,106 +2,97 @@
|
|||
|
||||
module k8s.io/kubectl
|
||||
|
||||
go 1.21.3
|
||||
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/distribution/reference v0.5.0
|
||||
github.com/evanphx/json-patch v4.12.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.6.0
|
||||
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.13.0
|
||||
github.com/onsi/gomega v1.29.0
|
||||
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.4
|
||||
golang.org/x/sys v0.13.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
k8s.io/api v0.0.0-20231101171312-cd0ecb048ea5
|
||||
k8s.io/apimachinery v0.0.0-20231101171057-16d50e6708ce
|
||||
k8s.io/cli-runtime v0.0.0-20231101174258-c19cbcf29f4b
|
||||
k8s.io/client-go v0.0.0-20231101171620-66e57f767515
|
||||
k8s.io/component-base v0.0.0-20231101172256-4b808112b779
|
||||
k8s.io/component-helpers v0.0.0-20231101172411-84bd21dfe63e
|
||||
k8s.io/klog/v2 v2.110.1
|
||||
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00
|
||||
k8s.io/metrics v0.0.0-20231101174125-1bb7847f7fec
|
||||
k8s.io/utils v0.0.0-20230726121419-3b25d923346b
|
||||
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.4.1
|
||||
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.11.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.3.0 // 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/gorilla/websocket v1.5.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.17.0 // indirect
|
||||
golang.org/x/oauth2 v0.10.0 // indirect
|
||||
golang.org/x/sync v0.3.0 // indirect
|
||||
golang.org/x/term v0.13.0 // indirect
|
||||
golang.org/x/text v0.13.0 // indirect
|
||||
golang.org/x/time v0.3.0 // indirect
|
||||
golang.org/x/tools v0.12.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/protobuf v1.31.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 => k8s.io/api v0.0.0-20231101171312-cd0ecb048ea5
|
||||
k8s.io/apimachinery => k8s.io/apimachinery v0.0.0-20231101171057-16d50e6708ce
|
||||
k8s.io/cli-runtime => k8s.io/cli-runtime v0.0.0-20231101174258-c19cbcf29f4b
|
||||
k8s.io/client-go => k8s.io/client-go v0.0.0-20231101171620-66e57f767515
|
||||
k8s.io/code-generator => k8s.io/code-generator v0.0.0-20231101170854-66e74b777ae2
|
||||
k8s.io/component-base => k8s.io/component-base v0.0.0-20231101172256-4b808112b779
|
||||
k8s.io/component-helpers => k8s.io/component-helpers v0.0.0-20231101172411-84bd21dfe63e
|
||||
k8s.io/metrics => k8s.io/metrics v0.0.0-20231101174125-1bb7847f7fec
|
||||
)
|
||||
replace k8s.io/code-generator => k8s.io/code-generator v0.0.0-20250718051115-9eb96548a40e
|
||||
|
|
322
go.sum
322
go.sum
|
@ -1,109 +1,67 @@
|
|||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
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/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/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
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/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/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0=
|
||||
github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
|
||||
github.com/emicklei/go-restful/v3 v3.11.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 v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84=
|
||||
github.com/evanphx/json-patch v4.12.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/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.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY=
|
||||
github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
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/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/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/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/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/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/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=
|
||||
|
@ -125,25 +83,26 @@ github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0
|
|||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
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.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4=
|
||||
github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o=
|
||||
github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg=
|
||||
github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ=
|
||||
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=
|
||||
|
@ -152,165 +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_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
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.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
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=
|
||||
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/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/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.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
|
||||
golang.org/x/mod v0.12.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.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.10.0 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8=
|
||||
golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI=
|
||||
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.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
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.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||
golang.org/x/sys v0.13.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.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
|
||||
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
||||
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.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
|
||||
golang.org/x/text v0.13.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.12.0 h1:YW6HUoUmYBpwSgyaGaZq1fHjrBjX1rlpZ54T6mu2kss=
|
||||
golang.org/x/tools v0.12.0/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM=
|
||||
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=
|
||||
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/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/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.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||
google.golang.org/protobuf v1.31.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/api v0.0.0-20231101171312-cd0ecb048ea5 h1:ZfnjV6xXqx4UGcyZm6uANo3TRx2ZzzARWc4sJzdRXOg=
|
||||
k8s.io/api v0.0.0-20231101171312-cd0ecb048ea5/go.mod h1:pe4ymgKfZ6OhusBquRSk+Kqm3zLI7wDLZQoGKcHdLLc=
|
||||
k8s.io/apimachinery v0.0.0-20231101171057-16d50e6708ce h1:oOZHk+iB3pNz2vb9yXNUWs6fQDcsINJePa5YxiAU0Z4=
|
||||
k8s.io/apimachinery v0.0.0-20231101171057-16d50e6708ce/go.mod h1:yFk3nwBh/jXlkMvRKH7BKtX7saT1lRmmGV6Ru0cTSUA=
|
||||
k8s.io/cli-runtime v0.0.0-20231101174258-c19cbcf29f4b h1:1xDANb0oB+0FXAIpSjp24u4vGB8YKJZwjeuQXlIcYpY=
|
||||
k8s.io/cli-runtime v0.0.0-20231101174258-c19cbcf29f4b/go.mod h1:O+HPNATF4jjTR6zhJ9wfjvUxY4NlNl7A8x2CYv6HkBw=
|
||||
k8s.io/client-go v0.0.0-20231101171620-66e57f767515 h1:iAC4m6tArcsiOFI2QMYzpMxGVksKBSH7k5whcUJLtHw=
|
||||
k8s.io/client-go v0.0.0-20231101171620-66e57f767515/go.mod h1:BuOU+TpCaVunHx4J07MTcND+TbxdXG/OLKFu6HXF4Mc=
|
||||
k8s.io/component-base v0.0.0-20231101172256-4b808112b779 h1:Z7qdGV7DKgU9asDkQQusUK8Gzl0dDHrOS59H6+6z80s=
|
||||
k8s.io/component-base v0.0.0-20231101172256-4b808112b779/go.mod h1:nC9MQrFtSP4UJAAPfBF4KVVbenBrNryFuAZJI0T3tp0=
|
||||
k8s.io/component-helpers v0.0.0-20231101172411-84bd21dfe63e h1:XkC6rTM7IAnMvzRKoYEmRNl+rwWOwteLTpv3YDcgjek=
|
||||
k8s.io/component-helpers v0.0.0-20231101172411-84bd21dfe63e/go.mod h1:vFo1NTmGcitSWef+yPDwxVxgCYKBHGwwMl+omzpFfdU=
|
||||
k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0=
|
||||
k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo=
|
||||
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 h1:aVUu9fTY98ivBPKR9Y5w/AuzbMm96cd3YHRTU83I780=
|
||||
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA=
|
||||
k8s.io/metrics v0.0.0-20231101174125-1bb7847f7fec h1:bCaTF9nCFRjXI5+K7S+k1tN8z6BxR5kVTN8A9XhUxgA=
|
||||
k8s.io/metrics v0.0.0-20231101174125-1bb7847f7fec/go.mod h1:tEilUM+LfcboplFK4ak4ac2R0BofX33FPyKmurjybUQ=
|
||||
k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI=
|
||||
k8s.io/utils v0.0.0-20230726121419-3b25d923346b/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/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.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08=
|
||||
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"
|
||||
|
|
|
@ -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"
|
||||
|
@ -43,14 +43,13 @@ import (
|
|||
"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"
|
||||
)
|
||||
|
@ -62,7 +61,7 @@ type ApplyFlags struct {
|
|||
RecordFlags *genericclioptions.RecordFlags
|
||||
PrintFlags *genericclioptions.PrintFlags
|
||||
|
||||
DeleteFlags *delete.DeleteFlags
|
||||
DeleteFlags *cmddelete.DeleteFlags
|
||||
|
||||
FieldManager string
|
||||
Selector string
|
||||
|
@ -72,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
|
||||
|
@ -87,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
|
||||
|
@ -100,6 +98,7 @@ type ApplyOptions struct {
|
|||
All bool
|
||||
Overwrite bool
|
||||
OpenAPIPatch bool
|
||||
Subresource string
|
||||
|
||||
ValidationDirective string
|
||||
Validator validation.Schema
|
||||
|
@ -185,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,
|
||||
|
@ -235,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
|
||||
|
@ -335,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
|
||||
}
|
||||
|
@ -361,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,
|
||||
|
@ -443,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
|
||||
}
|
||||
|
@ -582,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)
|
||||
|
@ -686,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)
|
||||
|
@ -764,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.
|
||||
//
|
||||
|
|
|
@ -25,6 +25,7 @@ import (
|
|||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
@ -59,8 +60,7 @@ import (
|
|||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/scheme"
|
||||
"k8s.io/kubectl/pkg/util/openapi"
|
||||
utilpointer "k8s.io/utils/pointer"
|
||||
"k8s.io/utils/strings/slices"
|
||||
"k8s.io/utils/ptr"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
|
@ -2087,6 +2087,9 @@ func TestForceApply(t *testing.T) {
|
|||
"post": 1,
|
||||
}
|
||||
|
||||
// Set the patch retry back off period to something low, so the test can run more quickly
|
||||
patchRetryBackOffPeriod = 1 * time.Millisecond
|
||||
|
||||
for _, testingOpenAPISchema := range testingOpenAPISchemas {
|
||||
for _, openAPIFeatureToggle := range applyFeatureToggles {
|
||||
|
||||
|
@ -2112,7 +2115,7 @@ func TestForceApply(t *testing.T) {
|
|||
var bodyRC io.ReadCloser
|
||||
if isScaledDownToZero {
|
||||
rcObj := readReplicationControllerFromFile(t, filenameRC)
|
||||
rcObj.Spec.Replicas = utilpointer.Int32Ptr(0)
|
||||
rcObj.Spec.Replicas = ptr.To[int32](0)
|
||||
rcBytes, err := runtime.Encode(codec, rcObj)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -2602,7 +2605,6 @@ metadata:
|
|||
applyset.kubernetes.io/additional-namespaces: ""
|
||||
applyset.kubernetes.io/contains-group-kinds: ReplicationController
|
||||
applyset.kubernetes.io/tooling: kubectl/v0.0.0-master+$Format:%H$
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
applyset.kubernetes.io/id: applyset-0eFHV8ySqp7XoShsGvyWFQD3s96yqwHmzc4e0HR1dsY-v1
|
||||
name: my-set
|
||||
|
@ -2636,7 +2638,6 @@ metadata:
|
|||
applyset.kubernetes.io/additional-namespaces: ""
|
||||
applyset.kubernetes.io/contains-group-kinds: ReplicationController,Service
|
||||
applyset.kubernetes.io/tooling: kubectl/v0.0.0-master+$Format:%H$
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
applyset.kubernetes.io/id: applyset-0eFHV8ySqp7XoShsGvyWFQD3s96yqwHmzc4e0HR1dsY-v1
|
||||
name: my-set
|
||||
|
@ -2671,7 +2672,6 @@ metadata:
|
|||
applyset.kubernetes.io/additional-namespaces: ""
|
||||
applyset.kubernetes.io/contains-group-kinds: ReplicationController,Service
|
||||
applyset.kubernetes.io/tooling: kubectl/v0.0.0-master+$Format:%H$
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
applyset.kubernetes.io/id: applyset-0eFHV8ySqp7XoShsGvyWFQD3s96yqwHmzc4e0HR1dsY-v1
|
||||
name: my-set
|
||||
|
@ -2706,7 +2706,6 @@ metadata:
|
|||
applyset.kubernetes.io/additional-namespaces: ""
|
||||
applyset.kubernetes.io/contains-group-kinds: Service
|
||||
applyset.kubernetes.io/tooling: kubectl/v0.0.0-master+$Format:%H$
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
applyset.kubernetes.io/id: applyset-0eFHV8ySqp7XoShsGvyWFQD3s96yqwHmzc4e0HR1dsY-v1
|
||||
name: my-set
|
||||
|
@ -2869,7 +2868,6 @@ metadata:
|
|||
applyset.kubernetes.io/additional-namespaces: test
|
||||
applyset.kubernetes.io/contains-group-kinds: ReplicationController
|
||||
applyset.kubernetes.io/tooling: kubectl/v0.0.0-master+$Format:%H$
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
applyset.kubernetes.io/id: applyset-rhp1a-HVAVT_dFgyEygyA1BEB82HPp2o10UiFTpqtAs-v1
|
||||
name: my-set
|
||||
|
@ -3087,7 +3085,6 @@ metadata:
|
|||
applyset.kubernetes.io/additional-namespaces: ""
|
||||
applyset.kubernetes.io/contains-group-resources: replicationcontrollers
|
||||
applyset.kubernetes.io/tooling: kubectl/v0.0.0-master+$Format:%H$
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
applyset.kubernetes.io/id: applyset-0eFHV8ySqp7XoShsGvyWFQD3s96yqwHmzc4e0HR1dsY-v1
|
||||
name: my-set
|
||||
|
@ -3429,7 +3426,7 @@ func TestApplySetDryRun(t *testing.T) {
|
|||
cmd.Run(cmd, []string{})
|
||||
})
|
||||
assert.Equal(t, "replicationcontroller/test-rc serverside-applied (server dry run)\n", outbuff.String())
|
||||
assert.Equal(t, len(serverSideData), 1, "unexpected creation")
|
||||
assert.Len(t, serverSideData, 1, "unexpected creation")
|
||||
require.Nil(t, serverSideData[pathSecret], "secret was created")
|
||||
})
|
||||
|
||||
|
@ -3446,7 +3443,7 @@ func TestApplySetDryRun(t *testing.T) {
|
|||
cmd.Run(cmd, []string{})
|
||||
})
|
||||
assert.Equal(t, "replicationcontroller/test-rc configured (dry run)\n", outbuff.String())
|
||||
assert.Equal(t, len(serverSideData), 1, "unexpected creation")
|
||||
assert.Len(t, serverSideData, 1, "unexpected creation")
|
||||
require.Nil(t, serverSideData[pathSecret], "secret was created")
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
@ -49,8 +48,6 @@ 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
|
||||
|
@ -59,7 +56,10 @@ const (
|
|||
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 {
|
||||
|
@ -118,13 +118,13 @@ 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
|
||||
|
@ -156,7 +156,7 @@ func (p *Patcher) patchSimple(obj runtime.Object, modified []byte, namespace, na
|
|||
}
|
||||
}
|
||||
|
||||
if patch == nil {
|
||||
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.
|
||||
|
@ -176,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -198,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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -363,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 {
|
||||
|
@ -386,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
|
||||
}
|
||||
|
|
|
@ -158,23 +158,10 @@ type DefaultRemoteAttach struct{}
|
|||
|
||||
// Attach executes attach to a running container
|
||||
func (*DefaultRemoteAttach) Attach(url *url.URL, config *restclient.Config, stdin io.Reader, stdout, stderr io.Writer, tty bool, terminalSizeQueue remotecommand.TerminalSizeQueue) error {
|
||||
// Legacy SPDY executor is default. If feature gate enabled, fallback
|
||||
// executor attempts websockets first--then SPDY.
|
||||
exec, err := remotecommand.NewSPDYExecutor(config, "POST", url)
|
||||
exec, err := createExecutor(url, config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if cmdutil.RemoteCommandWebsockets.IsEnabled() {
|
||||
// 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 err
|
||||
}
|
||||
exec, err = remotecommand.NewFallbackExecutor(websocketExec, exec, httpstream.IsUpgradeFailure)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return exec.StreamWithContext(context.Background(), remotecommand.StreamOptions{
|
||||
Stdin: stdin,
|
||||
Stdout: stdout,
|
||||
|
@ -184,6 +171,29 @@ func (*DefaultRemoteAttach) Attach(url *url.URL, config *restclient.Config, stdi
|
|||
})
|
||||
}
|
||||
|
||||
// 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
|
||||
|
@ -196,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
|
||||
|
@ -297,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,6 +37,7 @@ 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"
|
||||
|
@ -241,6 +242,7 @@ func TestAttach(t *testing.T) {
|
|||
pod *corev1.Pod
|
||||
remoteAttachErr bool
|
||||
expectedErr string
|
||||
expectedErrOut []string
|
||||
}{
|
||||
{
|
||||
name: "pod attach",
|
||||
|
@ -250,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",
|
||||
|
@ -304,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,
|
||||
|
@ -348,6 +355,14 @@ func TestAttach(t *testing.T) {
|
|||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -553,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()
|
||||
|
|
101
pkg/cmd/cmd.go
101
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
|
||||
|
@ -136,31 +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.IsDisabled() {
|
||||
// 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
|
||||
}
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -259,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, "-") {
|
||||
|
@ -279,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
|
||||
}
|
||||
|
||||
|
@ -360,6 +358,11 @@ 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().WithWarningPrinter(o.IOStreams)
|
||||
|
@ -382,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{
|
||||
{
|
||||
|
@ -433,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),
|
||||
},
|
||||
},
|
||||
|
@ -479,7 +484,7 @@ func NewKubectlCommand(o KubectlOptions) *cobra.Command {
|
|||
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))
|
||||
|
@ -489,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
|
||||
}
|
||||
|
||||
|
@ -506,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{}
|
||||
|
@ -565,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,6 +29,7 @@ import (
|
|||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/cli-runtime/pkg/genericiooptions"
|
||||
"k8s.io/kubectl/pkg/cmd/plugin"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
)
|
||||
|
||||
func TestNormalizationFuncGlobalExistence(t *testing.T) {
|
||||
|
@ -196,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"},
|
||||
|
@ -330,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
|
||||
}
|
||||
}
|
||||
|
@ -368,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 {
|
||||
|
@ -381,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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,6 @@ package create
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -39,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.
|
||||
|
@ -99,15 +98,10 @@ var (
|
|||
`)
|
||||
)
|
||||
|
||||
func boundObjectKindToAPIVersions() map[string]string {
|
||||
kinds := map[string]string{
|
||||
"Pod": "v1",
|
||||
"Secret": "v1",
|
||||
}
|
||||
if os.Getenv("KUBECTL_NODE_BOUND_TOKENS") == "true" {
|
||||
kinds["Node"] = "v1"
|
||||
}
|
||||
return kinds
|
||||
var boundObjectKinds = map[string]string{
|
||||
"Pod": "v1",
|
||||
"Secret": "v1",
|
||||
"Node": "v1",
|
||||
}
|
||||
|
||||
func NewTokenOpts(ioStreams genericiooptions.IOStreams) *TokenOptions {
|
||||
|
@ -148,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. If not set, the lifetime will be determined by the server automatically. 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(boundObjectKindToAPIVersions()).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. "+
|
||||
|
@ -208,8 +202,8 @@ func (o *TokenOptions) Validate() error {
|
|||
if len(o.Namespace) == 0 {
|
||||
return fmt.Errorf("--namespace is required")
|
||||
}
|
||||
if o.Duration < 0 || (o.Duration == 0 && o.Flags.Changed("duration")) {
|
||||
return fmt.Errorf("--duration must be positive")
|
||||
if o.Duration < 0 {
|
||||
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")
|
||||
|
@ -228,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 := boundObjectKindToAPIVersions()[o.BoundObjectKind]; !ok {
|
||||
return fmt.Errorf("supported --bound-object-kind values are %s", strings.Join(sets.StringKeySet(boundObjectKindToAPIVersions()).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")
|
||||
|
@ -247,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: boundObjectKindToAPIVersions()[o.BoundObjectKind],
|
||||
APIVersion: boundObjectKinds[o.BoundObjectKind],
|
||||
Name: o.BoundObjectName,
|
||||
UID: types.UID(o.BoundObjectUID),
|
||||
}
|
||||
|
|
|
@ -21,13 +21,11 @@ import (
|
|||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"k8s.io/utils/pointer"
|
||||
kjson "sigs.k8s.io/json"
|
||||
|
||||
authenticationv1 "k8s.io/api/authentication/v1"
|
||||
|
@ -39,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) {
|
||||
|
@ -54,8 +53,6 @@ func TestCreateToken(t *testing.T) {
|
|||
audiences []string
|
||||
duration time.Duration
|
||||
|
||||
enableNodeBindingFeature bool
|
||||
|
||||
serverResponseToken string
|
||||
serverResponseError string
|
||||
|
||||
|
@ -102,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
|
||||
|
@ -118,14 +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",
|
||||
enableNodeBindingFeature: true,
|
||||
boundObjectKind: "Foo",
|
||||
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",
|
||||
|
@ -172,10 +167,9 @@ status:
|
|||
test: "valid bound object (Node)",
|
||||
name: "mysa",
|
||||
|
||||
enableNodeBindingFeature: true,
|
||||
boundObjectKind: "Node",
|
||||
boundObjectName: "mynode",
|
||||
boundObjectUID: "myuid",
|
||||
boundObjectKind: "Node",
|
||||
boundObjectName: "mynode",
|
||||
boundObjectUID: "myuid",
|
||||
|
||||
expectRequestPath: "/api/v1/namespaces/test/serviceaccounts/mysa/token",
|
||||
expectTokenRequest: &authenticationv1.TokenRequest{
|
||||
|
@ -219,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",
|
||||
|
@ -237,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",
|
||||
|
@ -352,10 +361,6 @@ status:
|
|||
if test.duration != 0 {
|
||||
cmd.Flags().Set("duration", test.duration.String())
|
||||
}
|
||||
if test.enableNodeBindingFeature {
|
||||
os.Setenv("KUBECTL_NODE_BOUND_TOKENS", "true")
|
||||
defer os.Unsetenv("KUBECTL_NODE_BOUND_TOKENS")
|
||||
}
|
||||
cmd.Run(cmd, []string{test.name})
|
||||
|
||||
if !reflect.DeepEqual(tokenRequest, test.expectTokenRequest) {
|
||||
|
|
|
@ -20,12 +20,11 @@ import (
|
|||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"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,29 +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
|
||||
AttachFunc func(ctx context.Context, restClientGetter genericclioptions.RESTClientGetter, cmdPath string, ns, podName, containerName string) error
|
||||
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
|
||||
|
@ -146,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,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -184,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."))
|
||||
|
@ -192,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.
|
||||
|
@ -213,14 +236,15 @@ func (o *DebugOptions) Complete(restClientGetter genericclioptions.RESTClientGet
|
|||
attachFlag := cmd.Flags().Lookup("attach")
|
||||
if !attachFlag.Changed && o.Interactive {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
|
@ -248,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
|
||||
|
@ -347,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
|
||||
}
|
||||
|
||||
|
@ -440,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)
|
||||
|
@ -466,12 +526,106 @@ func (o *DebugOptions) debugByEphemeralContainer(ctx context.Context, pod *corev
|
|||
return result, debugContainer.Name, nil
|
||||
}
|
||||
|
||||
// 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 fmt.Errorf("unable to marshall custom profile: %w", err)
|
||||
}
|
||||
|
||||
var index int
|
||||
found := false
|
||||
for i, val := range debugPod.Spec.Containers {
|
||||
if val.Name == containerName {
|
||||
index = i
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
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 fmt.Errorf("unable to marshall 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.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
|
||||
func (o *DebugOptions) debugByCopy(ctx context.Context, pod *corev1.Pod) (*corev1.Pod, string, error) {
|
||||
copied, dc, err := o.generatePodCopyWithDebugContainer(pod)
|
||||
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
|
||||
|
@ -485,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) {
|
||||
|
@ -514,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
|
||||
|
@ -540,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{
|
||||
|
@ -573,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
|
||||
}
|
||||
|
||||
|
@ -583,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(),
|
||||
}
|
||||
|
@ -590,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 = ""
|
||||
|
@ -655,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
|
||||
}
|
||||
|
||||
|
@ -784,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
|
||||
}
|
||||
|
@ -807,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
|
||||
|
@ -818,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)
|
||||
|
@ -203,6 +279,10 @@ func (p *netadminProfile) Apply(pod *corev1.Pod, containerName string, target ru
|
|||
useHostNamespaces(pod)
|
||||
|
||||
case podCopy:
|
||||
p.RemoveLabels(pod)
|
||||
p.RemoveAnnotations(pod)
|
||||
p.RemoveProbes(pod)
|
||||
p.RemoveInitContainers(pod)
|
||||
shareProcessNamespace(pod)
|
||||
|
||||
case ephemeral:
|
||||
|
@ -212,15 +292,32 @@ func (p *netadminProfile) Apply(pod *corev1.Pod, containerName string, target ru
|
|||
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.
|
||||
|
@ -256,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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -271,6 +368,20 @@ func clearSecurityContext(p *corev1.Pod, containerName string) {
|
|||
})
|
||||
}
|
||||
|
||||
// setPrivileged configures the containers as privileged.
|
||||
func setPrivileged(p *corev1.Pod, containerName string) {
|
||||
podutils.VisitContainers(&p.Spec, podutils.AllContainers, func(c *corev1.Container, _ podutils.ContainerType) bool {
|
||||
if c.Name != containerName {
|
||||
return true
|
||||
}
|
||||
if c.SecurityContext == nil {
|
||||
c.SecurityContext = &corev1.SecurityContext{}
|
||||
}
|
||||
c.SecurityContext.Privileged = ptr.To(true)
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
// disallowRoot configures the container to run as a non-root user.
|
||||
func disallowRoot(p *corev1.Pod, containerName string) {
|
||||
podutils.VisitContainers(&p.Spec, podutils.AllContainers, func(c *corev1.Container, _ podutils.ContainerType) bool {
|
||||
|
@ -280,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
|
||||
})
|
||||
}
|
||||
|
@ -345,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))
|
||||
}
|
||||
|
@ -506,27 +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: pointer.Bool(true),
|
||||
ShareProcessNamespace: ptr.To(true),
|
||||
InitContainers: []corev1.Container{{Name: "init-container"}},
|
||||
Containers: []corev1.Container{
|
||||
{Name: "app", Image: "appimage"},
|
||||
{
|
||||
|
@ -548,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",
|
||||
|
@ -566,14 +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: pointer.Bool(true),
|
||||
ShareProcessNamespace: ptr.To(true),
|
||||
Containers: []corev1.Container{
|
||||
{Name: "app", Image: "appimage"},
|
||||
{
|
||||
|
@ -665,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
|
||||
}
|
||||
|
|
|
@ -161,7 +161,7 @@ func (f *DeleteFlags) AddFlags(cmd *cobra.Command) {
|
|||
cmd.Flags().StringVar(f.Raw, "raw", *f.Raw, "Raw URI to DELETE to the server. Uses the transport specified by the kubeconfig file.")
|
||||
}
|
||||
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.")
|
||||
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 {
|
||||
|
@ -60,8 +60,8 @@ func TestDeleteFlagValidation(t *testing.T) {
|
|||
}{
|
||||
{
|
||||
flags: DeleteFlags{
|
||||
Raw: pointer.String("test"),
|
||||
Interactive: pointer.Bool(true),
|
||||
Raw: ptr.To("test"),
|
||||
Interactive: ptr.To(true),
|
||||
},
|
||||
expectedErr: "--interactive can not be used with --raw",
|
||||
},
|
||||
|
@ -306,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()
|
||||
|
@ -375,7 +375,7 @@ func TestDeleteObjectWithInteractive(t *testing.T) {
|
|||
}
|
||||
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" {
|
||||
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())
|
||||
}
|
||||
|
||||
|
@ -396,7 +396,7 @@ func TestDeleteObjectWithInteractive(t *testing.T) {
|
|||
}
|
||||
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" {
|
||||
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" {
|
||||
|
@ -972,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()
|
||||
|
|
|
@ -161,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.")
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -224,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")
|
||||
|
@ -245,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")
|
||||
|
@ -328,7 +326,7 @@ func (o *DrainCmdOptions) RunDrain() error {
|
|||
return err
|
||||
}
|
||||
|
||||
drainedNodes := sets.NewString()
|
||||
drainedNodes := sets.New[string]()
|
||||
var fatal []error
|
||||
|
||||
remainingNodes := []string{}
|
||||
|
@ -343,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
|
||||
`,
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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{
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
|
|
@ -114,31 +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 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(url *url.URL, config *restclient.Config, stdin io.Reader, stdout, stderr io.Writer, tty bool, terminalSizeQueue remotecommand.TerminalSizeQueue) error {
|
||||
// Legacy SPDY executor is default. If feature gate enabled, fallback
|
||||
// executor attempts websockets first--then SPDY.
|
||||
exec, err := remotecommand.NewSPDYExecutor(config, "POST", 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
|
||||
}
|
||||
if cmdutil.RemoteCommandWebsockets.IsEnabled() {
|
||||
// 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 err
|
||||
}
|
||||
exec, err = remotecommand.NewFallbackExecutor(websocketExec, exec, httpstream.IsUpgradeFailure)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return exec.StreamWithContext(context.Background(), remotecommand.StreamOptions{
|
||||
return exec.StreamWithContext(ctx, remotecommand.StreamOptions{
|
||||
Stdin: stdin,
|
||||
Stdout: stdout,
|
||||
Stderr: stderr,
|
||||
|
@ -147,6 +142,29 @@ func (*DefaultRemoteExecutor) Execute(url *url.URL, config *restclient.Config, s
|
|||
})
|
||||
}
|
||||
|
||||
// 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
|
||||
|
@ -192,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
|
||||
|
@ -215,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
|
||||
|
|
|
@ -18,6 +18,7 @@ package exec
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
@ -33,8 +34,8 @@ 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"
|
||||
)
|
||||
|
@ -45,6 +46,10 @@ type fakeRemoteExecutor struct {
|
|||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
@ -110,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 {
|
||||
|
@ -238,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()
|
||||
|
@ -402,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,83 +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
|
||||
openAPIGetter openapi.OpenAPIResourcesGetter
|
||||
|
||||
// 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
|
||||
|
||||
// Only openapi v3 needs the discovery client.
|
||||
o.OpenAPIV3Client, err = f.OpenAPIV3Client()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
Recursive bool
|
||||
APIVersion string
|
||||
// Name of the template to use with the openapiv3 template renderer.
|
||||
OutputFormat string
|
||||
|
||||
// Lazy-load the OpenAPI V2 Resources, so they're not loaded when using OpenAPI V3.
|
||||
o.openAPIGetter = f
|
||||
o.args = args
|
||||
return nil
|
||||
CmdParent string
|
||||
args []string
|
||||
|
||||
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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
@ -45,7 +44,6 @@ import (
|
|||
"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"
|
||||
)
|
||||
|
||||
|
@ -107,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(),
|
||||
|
@ -140,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
|
||||
}
|
||||
|
@ -197,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
|
||||
}
|
||||
|
||||
|
@ -266,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)
|
||||
},
|
||||
|
|
|
@ -31,6 +31,7 @@ import (
|
|||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/httpstream"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/cli-runtime/pkg/genericiooptions"
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
|
@ -50,7 +51,7 @@ import (
|
|||
type PortForwardOptions struct {
|
||||
Namespace string
|
||||
PodName string
|
||||
RESTClient *restclient.RESTClient
|
||||
RESTClient restclient.Interface
|
||||
Config *restclient.Config
|
||||
PodClient corev1client.PodsGetter
|
||||
Address []string
|
||||
|
@ -99,18 +100,14 @@ const (
|
|||
)
|
||||
|
||||
func NewCmdPortForward(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command {
|
||||
opts := &PortForwardOptions{
|
||||
PortForwarder: &defaultPortForwarder{
|
||||
IOStreams: streams,
|
||||
},
|
||||
}
|
||||
opts := NewDefaultPortForwardOptions(streams)
|
||||
cmd := &cobra.Command{
|
||||
Use: "port-forward TYPE/NAME [options] [LOCAL_PORT:]REMOTE_PORT [...[LOCAL_PORT_N:]REMOTE_PORT_N]",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: i18n.T("Forward one or more local ports to a pod"),
|
||||
Long: portforwardLong,
|
||||
Example: portforwardExample,
|
||||
ValidArgsFunction: completion.PodResourceNameCompletionFunc(f),
|
||||
ValidArgsFunction: completion.ResourceAndPortCompletionFunc(f),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(opts.Complete(f, cmd, args))
|
||||
cmdutil.CheckErr(opts.Validate())
|
||||
|
@ -123,6 +120,14 @@ func NewCmdPortForward(f cmdutil.Factory, streams genericiooptions.IOStreams) *c
|
|||
return cmd
|
||||
}
|
||||
|
||||
func NewDefaultPortForwardOptions(streams genericiooptions.IOStreams) *PortForwardOptions {
|
||||
return &PortForwardOptions{
|
||||
PortForwarder: &defaultPortForwarder{
|
||||
IOStreams: streams,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type portForwarder interface {
|
||||
ForwardPorts(method string, url *url.URL, opts PortForwardOptions) error
|
||||
}
|
||||
|
@ -131,12 +136,30 @@ type defaultPortForwarder struct {
|
|||
genericiooptions.IOStreams
|
||||
}
|
||||
|
||||
func (f *defaultPortForwarder) ForwardPorts(method string, url *url.URL, opts PortForwardOptions) error {
|
||||
func createDialer(method string, url *url.URL, opts PortForwardOptions) (httpstream.Dialer, error) {
|
||||
transport, upgrader, err := spdy.RoundTripperFor(opts.Config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dialer := spdy.NewDialer(upgrader, &http.Client{Transport: transport}, method, url)
|
||||
if !cmdutil.PortForwardWebsockets.IsDisabled() {
|
||||
tunnelingDialer, err := portforward.NewSPDYOverWebsocketDialer(url, opts.Config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// First attempt tunneling (websocket) dialer, then fallback to spdy dialer.
|
||||
dialer = portforward.NewFallbackDialer(tunnelingDialer, dialer, func(err error) bool {
|
||||
return httpstream.IsUpgradeFailure(err) || httpstream.IsHTTPSProxyError(err)
|
||||
})
|
||||
}
|
||||
return dialer, nil
|
||||
}
|
||||
|
||||
func (f *defaultPortForwarder) ForwardPorts(method string, url *url.URL, opts PortForwardOptions) error {
|
||||
dialer, err := createDialer(method, url, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dialer := spdy.NewDialer(upgrader, &http.Client{Transport: transport}, method, url)
|
||||
fw, err := portforward.NewOnAddresses(dialer, opts.Address, opts.Ports, opts.StopChannel, opts.ReadyChannel, f.Out, f.ErrOut)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -200,7 +223,9 @@ func convertPodNamedPortToNumber(ports []string, pod corev1.Pod) ([]string, erro
|
|||
var converted []string
|
||||
for _, port := range ports {
|
||||
localPort, remotePort := splitPort(port)
|
||||
|
||||
if remotePort == "" {
|
||||
return nil, fmt.Errorf("remote port cannot be empty")
|
||||
}
|
||||
containerPortStr := remotePort
|
||||
_, err := strconv.Atoi(remotePort)
|
||||
if err != nil {
|
||||
|
@ -222,7 +247,7 @@ func convertPodNamedPortToNumber(ports []string, pod corev1.Pod) ([]string, erro
|
|||
return converted, nil
|
||||
}
|
||||
|
||||
func checkUDPPorts(udpOnlyPorts sets.Int, ports []string, obj metav1.Object) error {
|
||||
func checkUDPPorts(udpOnlyPorts sets.Set[int], ports []string, obj metav1.Object) error {
|
||||
for _, port := range ports {
|
||||
_, remotePort := splitPort(port)
|
||||
portNum, err := strconv.Atoi(remotePort)
|
||||
|
@ -256,8 +281,8 @@ func checkUDPPorts(udpOnlyPorts sets.Int, ports []string, obj metav1.Object) err
|
|||
// checkUDPPortInService returns an error if remote port in Service is a UDP port
|
||||
// TODO: remove this check after #47862 is solved
|
||||
func checkUDPPortInService(ports []string, svc *corev1.Service) error {
|
||||
udpPorts := sets.NewInt()
|
||||
tcpPorts := sets.NewInt()
|
||||
udpPorts := sets.New[int]()
|
||||
tcpPorts := sets.New[int]()
|
||||
for _, port := range svc.Spec.Ports {
|
||||
portNum := int(port.Port)
|
||||
switch port.Protocol {
|
||||
|
@ -273,8 +298,8 @@ func checkUDPPortInService(ports []string, svc *corev1.Service) error {
|
|||
// checkUDPPortInPod returns an error if remote port in Pod is a UDP port
|
||||
// TODO: remove this check after #47862 is solved
|
||||
func checkUDPPortInPod(ports []string, pod *corev1.Pod) error {
|
||||
udpPorts := sets.NewInt()
|
||||
tcpPorts := sets.NewInt()
|
||||
udpPorts := sets.New[int]()
|
||||
tcpPorts := sets.New[int]()
|
||||
for _, ct := range pod.Spec.Containers {
|
||||
for _, ctPort := range ct.Ports {
|
||||
portNum := int(ctPort.ContainerPort)
|
||||
|
@ -308,7 +333,7 @@ func (o *PortForwardOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, arg
|
|||
|
||||
getPodTimeout, err := cmdutil.GetPodRunningTimeoutFlag(cmd)
|
||||
if err != nil {
|
||||
return cmdutil.UsageErrorf(cmd, err.Error())
|
||||
return cmdutil.UsageErrorf(cmd, "%s", err.Error())
|
||||
}
|
||||
|
||||
resourceName := args[0]
|
||||
|
@ -385,9 +410,17 @@ func (o PortForwardOptions) Validate() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Deprecated: Use RunPortForwardContext instead, which allows canceling.
|
||||
// RunPortForward implements all the necessary functionality for port-forward cmd.
|
||||
func (o PortForwardOptions) RunPortForward() error {
|
||||
pod, err := o.PodClient.Pods(o.Namespace).Get(context.TODO(), o.PodName, metav1.GetOptions{})
|
||||
return o.RunPortForwardContext(context.Background())
|
||||
}
|
||||
|
||||
// RunPortForwardContext implements all the necessary functionality for port-forward cmd.
|
||||
// It ends portforwarding when an error is received from the backend, or an os.Interrupt
|
||||
// signal is received, or the provided context is done.
|
||||
func (o PortForwardOptions) RunPortForwardContext(ctx context.Context) error {
|
||||
pod, err := o.PodClient.Pods(o.Namespace).Get(ctx, o.PodName, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -400,8 +433,14 @@ func (o PortForwardOptions) RunPortForward() error {
|
|||
signal.Notify(signals, os.Interrupt)
|
||||
defer signal.Stop(signals)
|
||||
|
||||
returnCtx, returnCtxCancel := context.WithCancel(ctx)
|
||||
defer returnCtxCancel()
|
||||
|
||||
go func() {
|
||||
<-signals
|
||||
select {
|
||||
case <-signals:
|
||||
case <-returnCtx.Done():
|
||||
}
|
||||
if o.StopChannel != nil {
|
||||
close(o.StopChannel)
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
package portforward
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
@ -31,7 +32,9 @@ import (
|
|||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
"k8s.io/cli-runtime/pkg/genericiooptions"
|
||||
"k8s.io/client-go/rest/fake"
|
||||
"k8s.io/client-go/tools/portforward"
|
||||
cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/scheme"
|
||||
)
|
||||
|
||||
|
@ -101,6 +104,8 @@ func testPortForward(t *testing.T, flags map[string]string, args []string) {
|
|||
}
|
||||
|
||||
opts := &PortForwardOptions{}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
cmd := NewCmdPortForward(tf, genericiooptions.NewTestIOStreamsDiscard())
|
||||
cmd.Run = func(cmd *cobra.Command, args []string) {
|
||||
if err = opts.Complete(tf, cmd, args); err != nil {
|
||||
|
@ -110,7 +115,7 @@ func testPortForward(t *testing.T, flags map[string]string, args []string) {
|
|||
if err = opts.Validate(); err != nil {
|
||||
return
|
||||
}
|
||||
err = opts.RunPortForward()
|
||||
err = opts.RunPortForwardContext(ctx)
|
||||
}
|
||||
|
||||
for name, value := range flags {
|
||||
|
@ -815,6 +820,24 @@ func TestConvertPodNamedPortToNumber(t *testing.T) {
|
|||
ports: []string{"https", "http"},
|
||||
err: true,
|
||||
},
|
||||
{
|
||||
name: "empty port name",
|
||||
pod: corev1.Pod{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Ports: []corev1.ContainerPort{
|
||||
{
|
||||
ContainerPort: int32(27017)},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ports: []string{"28015:"},
|
||||
converted: nil,
|
||||
err: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
|
@ -980,3 +1003,38 @@ func TestCheckUDPPort(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateDialer(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()
|
||||
opts := PortForwardOptions{Config: config}
|
||||
// First, ensure that no environment variable creates the fallback dialer.
|
||||
dialer, err := createDialer("GET", url, opts)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create dialer: %v", err)
|
||||
}
|
||||
if _, isFallback := dialer.(*portforward.FallbackDialer); !isFallback {
|
||||
t.Errorf("expected fallback dialer, got %#v", dialer)
|
||||
}
|
||||
// Next, check turning on feature flag explicitly also creates fallback dialer.
|
||||
t.Setenv(string(cmdutil.PortForwardWebsockets), "true")
|
||||
dialer, err = createDialer("GET", url, opts)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create dialer: %v", err)
|
||||
}
|
||||
if _, isFallback := dialer.(*portforward.FallbackDialer); !isFallback {
|
||||
t.Errorf("expected fallback dialer, got %#v", dialer)
|
||||
}
|
||||
// Finally, check explicit disabling does NOT create the fallback dialer.
|
||||
t.Setenv(string(cmdutil.PortForwardWebsockets), "false")
|
||||
dialer, err = createDialer("GET", url, opts)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create dialer: %v", err)
|
||||
}
|
||||
if _, isFallback := dialer.(*portforward.FallbackDialer); isFallback {
|
||||
t.Errorf("expected fallback dialer, got %#v", dialer)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
package replace
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
|
@ -40,7 +41,6 @@ import (
|
|||
"k8s.io/kubectl/pkg/scheme"
|
||||
"k8s.io/kubectl/pkg/util"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
"k8s.io/kubectl/pkg/util/slice"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
"k8s.io/kubectl/pkg/validation"
|
||||
)
|
||||
|
@ -68,8 +68,6 @@ var (
|
|||
kubectl replace --force -f ./pod.json`))
|
||||
)
|
||||
|
||||
var supportedSubresources = []string{"status", "scale"}
|
||||
|
||||
type ReplaceOptions struct {
|
||||
PrintFlags *genericclioptions.PrintFlags
|
||||
RecordFlags *genericclioptions.RecordFlags
|
||||
|
@ -136,7 +134,7 @@ func NewCmdReplace(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra
|
|||
|
||||
cmd.Flags().StringVar(&o.Raw, "raw", o.Raw, "Raw URI to PUT to the server. Uses the transport specified by the kubeconfig file.")
|
||||
cmdutil.AddFieldManagerFlagVar(cmd, &o.fieldManager, "kubectl-replace")
|
||||
cmdutil.AddSubresourceFlags(cmd, &o.Subresource, "If specified, replace will operate on the subresource of the requested object.", supportedSubresources...)
|
||||
cmdutil.AddSubresourceFlags(cmd, &o.Subresource, "If specified, replace will operate on the subresource of the requested object.")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
@ -249,10 +247,6 @@ func (o *ReplaceOptions) Validate() error {
|
|||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -365,8 +359,7 @@ func (o *ReplaceOptions) forceReplace() error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return wait.PollImmediate(1*time.Second, timeout, func() (bool, error) {
|
||||
return wait.PollUntilContextTimeout(context.Background(), 1*time.Second, timeout, true, func(ctx context.Context) (bool, error) {
|
||||
if err := info.Get(); !errors.IsNotFound(err) {
|
||||
return false, err
|
||||
}
|
||||
|
|
|
@ -105,7 +105,7 @@ func NewCmdRolloutHistory(f cmdutil.Factory, streams genericiooptions.IOStreams)
|
|||
return cmd
|
||||
}
|
||||
|
||||
// Complete completes al the required options
|
||||
// Complete completes all the required options
|
||||
func (o *RolloutHistoryOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error {
|
||||
o.Resources = args
|
||||
|
||||
|
@ -177,7 +177,15 @@ func (o *RolloutHistoryOptions) Run() error {
|
|||
}
|
||||
|
||||
if o.Revision > 0 {
|
||||
printer.PrintObj(historyInfo[o.Revision], o.Out)
|
||||
// Ensure the specified revision exists before printing
|
||||
revision, exists := historyInfo[o.Revision]
|
||||
if !exists {
|
||||
return fmt.Errorf("unable to find the specified revision")
|
||||
}
|
||||
|
||||
if err := printer.PrintObj(revision, o.Out); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
sortedKeys := make([]int64, 0, len(historyInfo))
|
||||
for k := range historyInfo {
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue