Compare commits
49 Commits
v0.34.0-be
...
master
Author | SHA1 | Date |
---|---|---|
|
2a6e583a42 | |
|
9411a867c0 | |
|
aa186336f3 | |
|
4dec168032 | |
|
48317edcc3 | |
|
9f67d52cb6 | |
|
33f48403eb | |
|
ceae52f082 | |
|
74f9adbfa6 | |
|
a628f84f45 | |
|
e98b7e433d | |
|
cce124a8d8 | |
|
9e0a26615e | |
|
4ce6135c24 | |
|
22d999d2fc | |
|
0889899212 | |
|
f4a1e23b35 | |
|
3c19b8deb3 | |
|
cbe60c952e | |
|
12dc58ebf7 | |
|
7b4e1d1699 | |
|
4d28f5cea7 | |
|
4b52eef334 | |
|
3962f4011e | |
|
af99d1e41c | |
|
4f56b5ec5b | |
|
ce4d90902a | |
|
21b32eea57 | |
|
403b4a41e8 | |
|
9fb3eee5e8 | |
|
8185d35b7a | |
|
80ffc392a2 | |
|
3f6dbadba7 | |
|
55aec96de2 | |
|
8f1bcdea60 | |
|
98b4b33964 | |
|
3cb662b4be | |
|
fb7d414ec2 | |
|
e7f17cb570 | |
|
29d75ee358 | |
|
8067f3a1a6 | |
|
483c28b281 | |
|
02042ef887 | |
|
8ed5bb5f0a | |
|
1b3f4fd0f4 | |
|
9c02ed6a6e | |
|
cf54a4ea54 | |
|
a7bf48f663 | |
|
5c587a03ba |
|
@ -1,3 +1,8 @@
|
|||
> ⚠️ **This is an automatically published [staged repository](https://git.k8s.io/kubernetes/staging#external-repository-staging-area) for Kubernetes**.
|
||||
> Pull requests, should be made to the main Kubernetes repository: [https://github.com/kubernetes/kubernetes](https://github.com/kubernetes/kubernetes).
|
||||
> This repository is read-only for importing, and not used for direct contributions.
|
||||
> See [CONTRIBUTING.md](./CONTRIBUTING.md) for more details.
|
||||
|
||||
# Kubectl
|
||||
|
||||

|
||||
|
|
46
go.mod
46
go.mod
|
@ -27,24 +27,24 @@ require (
|
|||
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-20250715090528-7da28ad7db85
|
||||
k8s.io/apimachinery v0.0.0-20250715090235-1ebcba2516a6
|
||||
k8s.io/cli-runtime v0.0.0-20250715094709-a28ba5287690
|
||||
k8s.io/client-go v0.0.0-20250715090929-f78427e36774
|
||||
k8s.io/component-base v0.0.0-20250715092145-ef3b5b450d5d
|
||||
k8s.io/component-helpers v0.0.0-20250715092311-11b5d493a527
|
||||
golang.org/x/sys v0.33.0
|
||||
gopkg.in/evanphx/json-patch.v4 v4.13.0
|
||||
k8s.io/api v0.0.0-20250830163657-b903cd06836a
|
||||
k8s.io/apimachinery v0.0.0-20250830163350-eb2c6e0d1ec4
|
||||
k8s.io/cli-runtime v0.0.0-20250830171832-3e7914c55f7e
|
||||
k8s.io/client-go v0.0.0-20250830164107-2a8d855d0d97
|
||||
k8s.io/component-base v0.0.0-20250830165319-75dce96cfea7
|
||||
k8s.io/component-helpers v0.0.0-20250830165448-f84446610ecc
|
||||
k8s.io/klog/v2 v2.130.1
|
||||
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b
|
||||
k8s.io/metrics v0.0.0-20250715094530-e16345c4c004
|
||||
k8s.io/metrics v0.0.0-20250830171643-3daf20f585bd
|
||||
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/kustomize/kustomize/v5 v5.7.1
|
||||
sigs.k8s.io/kustomize/kyaml v0.20.1
|
||||
sigs.k8s.io/randfill v1.0.0
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.2.0
|
||||
sigs.k8s.io/yaml v1.5.0
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0
|
||||
sigs.k8s.io/yaml v1.6.0
|
||||
)
|
||||
|
||||
require (
|
||||
|
@ -52,16 +52,15 @@ require (
|
|||
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.12.2 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.8.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||
github.com/go-errors/errors v1.4.2 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // 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/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.6.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
|
||||
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
|
||||
|
@ -77,20 +76,19 @@ require (
|
|||
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.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/net v0.40.0 // indirect
|
||||
golang.org/x/oauth2 v0.30.0 // indirect
|
||||
golang.org/x/sync v0.14.0 // indirect
|
||||
golang.org/x/term v0.32.0 // indirect
|
||||
golang.org/x/text v0.25.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
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
sigs.k8s.io/kustomize/api v0.19.0 // indirect
|
||||
sigs.k8s.io/kustomize/api v0.20.1 // indirect
|
||||
)
|
||||
|
|
92
go.sum
92
go.sum
|
@ -23,12 +23,12 @@ github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2
|
|||
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/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/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
||||
github.com/fxamacker/cbor/v2 v2.9.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.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/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=
|
||||
|
@ -50,8 +50,6 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX
|
|||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
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.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=
|
||||
|
@ -107,8 +105,6 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8
|
|||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI=
|
||||
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
|
||||
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/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
|
@ -155,27 +151,27 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
|
|||
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.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/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
||||
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||
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.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
|
||||
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
|
||||
golang.org/x/sync v0.14.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-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
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/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
|
||||
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
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/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||
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=
|
||||
|
@ -188,49 +184,49 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
|
|||
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/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
|
||||
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
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/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo=
|
||||
gopkg.in/evanphx/json-patch.v4 v4.13.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.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=
|
||||
k8s.io/api v0.0.0-20250715090528-7da28ad7db85 h1:QfiiAaLAQKj+yvMS1ySLpj3UWzxJixBEaWvKNtzwnQM=
|
||||
k8s.io/api v0.0.0-20250715090528-7da28ad7db85/go.mod h1:MCwhlGL+lP5brf6CuU20fWxaLh/8tUSlu4VM1cOD8Lo=
|
||||
k8s.io/apimachinery v0.0.0-20250715090235-1ebcba2516a6 h1:lH8NMkqxmCWN1CvOiqbfphUcl+EAk95z+3Le9bfEbJ4=
|
||||
k8s.io/apimachinery v0.0.0-20250715090235-1ebcba2516a6/go.mod h1:TP8uyOuDEOnzGpLOdffo8hPnIjVDljZCxCM/fruV+5M=
|
||||
k8s.io/cli-runtime v0.0.0-20250715094709-a28ba5287690 h1:1YwSuyjcnhvaAaes2zGdbilWjLeHBxZkJ1hWKz0/IXc=
|
||||
k8s.io/cli-runtime v0.0.0-20250715094709-a28ba5287690/go.mod h1:WykG0uYhov9/a+lenZsbNR4Z5AvFgUzk4bRkfB6SVdY=
|
||||
k8s.io/client-go v0.0.0-20250715090929-f78427e36774 h1:OJXhumReMNIzlpFEEQvl89u+u7KmQ6fa4I3TpZQYjIg=
|
||||
k8s.io/client-go v0.0.0-20250715090929-f78427e36774/go.mod h1:y02d1W5RQ3IDA7qs1unUQEkERwkgLrd7fuDANdUN31E=
|
||||
k8s.io/component-base v0.0.0-20250715092145-ef3b5b450d5d h1:dwVoeQ3VARtU56EIoD/h9N5P8HertH7rBD99fESoxN4=
|
||||
k8s.io/component-base v0.0.0-20250715092145-ef3b5b450d5d/go.mod h1:Cx9XNntOC5C9BXP8nozdA2se8LRKYnh2rlhglgoCxp8=
|
||||
k8s.io/component-helpers v0.0.0-20250715092311-11b5d493a527 h1:KUTvlyyLHBRb9BLmlvu2EEsDUM7FATEpZx370bUZ9a8=
|
||||
k8s.io/component-helpers v0.0.0-20250715092311-11b5d493a527/go.mod h1:u8G9DUkNxykMmVeOEc6Pf1Tagyjc/nKm0QXG9VQYKY8=
|
||||
k8s.io/api v0.0.0-20250830163657-b903cd06836a h1:qS+abmAu2zbGFkbN1vA7LKS07jsXBN1BvTFXFvaGOLI=
|
||||
k8s.io/api v0.0.0-20250830163657-b903cd06836a/go.mod h1:/IpJMZ4ur2JBuX+kkBc115bnq09sFfUnbuFNrdEe5yc=
|
||||
k8s.io/apimachinery v0.0.0-20250830163350-eb2c6e0d1ec4 h1:ObQoOWhkcPbMnU7PIHT2pkO2wK66CcBn6vD+77CidHM=
|
||||
k8s.io/apimachinery v0.0.0-20250830163350-eb2c6e0d1ec4/go.mod h1:fDax9lidUgmNSmBlzUrSISURQmHpeyamBbKX9jGbJ3g=
|
||||
k8s.io/cli-runtime v0.0.0-20250830171832-3e7914c55f7e h1:dovWIA2PM0UrJrZdUBV8uy5pExliSBXSFeL0bI6IX6E=
|
||||
k8s.io/cli-runtime v0.0.0-20250830171832-3e7914c55f7e/go.mod h1:/yp9r2rD6AV7MYM/gmb55/6LttRuURzjhgqbfiFQ0Rg=
|
||||
k8s.io/client-go v0.0.0-20250830164107-2a8d855d0d97 h1:6ks7Y8CNm05xZ6eyE0db5IDP54PIyRM3aZhpflG55hI=
|
||||
k8s.io/client-go v0.0.0-20250830164107-2a8d855d0d97/go.mod h1:xTpANYjBhGsmpO7Gdw8kMt3yQfciVwyRhbqcq77qwyI=
|
||||
k8s.io/component-base v0.0.0-20250830165319-75dce96cfea7 h1:JQ/dO+EIXNQ+y3Vlez6PC6r7T+0JvNQWrBx6CD3jKls=
|
||||
k8s.io/component-base v0.0.0-20250830165319-75dce96cfea7/go.mod h1:ZZMk2BFRSF/kI9Y5qKmvTk4SJM654XsQ5eJ9cP7mrhw=
|
||||
k8s.io/component-helpers v0.0.0-20250830165448-f84446610ecc h1:hEo6RVciD0e0QvtMBgwG9a7fFWb/vkx0Jvw/iQ5i+lQ=
|
||||
k8s.io/component-helpers v0.0.0-20250830165448-f84446610ecc/go.mod h1:TmnJ20kJrkgbEqHgHoUxoTksgIUJNCWtv/QM+yBqCF0=
|
||||
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-20250715094530-e16345c4c004 h1:JMUcdZhgvBQOMwlDWU+DYuxu/msdf2vlP19crwbaF18=
|
||||
k8s.io/metrics v0.0.0-20250715094530-e16345c4c004/go.mod h1:oDIgK3jknb/hium+pU4Dca0PB6o53GwvdUA576x1xb8=
|
||||
k8s.io/metrics v0.0.0-20250830171643-3daf20f585bd h1:GNGeQ8o/xw8TwhavcXmWrGKHd0ez5TRx8qRqyncbFH4=
|
||||
k8s.io/metrics v0.0.0-20250830171643-3daf20f585bd/go.mod h1:OYYW2zJf2TAQHmxQwQgSsP/i40dCZig4RQkw3o3vFPE=
|
||||
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/kustomize/api v0.20.1 h1:iWP1Ydh3/lmldBnH/S5RXgT98vWYMaTUL1ADcr+Sv7I=
|
||||
sigs.k8s.io/kustomize/api v0.20.1/go.mod h1:t6hUFxO+Ph0VxIk1sKp1WS0dOjbPCtLJ4p8aADLwqjM=
|
||||
sigs.k8s.io/kustomize/kustomize/v5 v5.7.1 h1:sYJsarwy/SDJfjjLMUqwFDGPwzUtMOQ1i1Ed49+XSbw=
|
||||
sigs.k8s.io/kustomize/kustomize/v5 v5.7.1/go.mod h1:+5/SrBcJ4agx1SJknGuR/c9thwRSKLxnKoI5BzXFaLU=
|
||||
sigs.k8s.io/kustomize/kyaml v0.20.1 h1:PCMnA2mrVbRP3NIB6v9kYCAc38uvFLVs8j/CD567A78=
|
||||
sigs.k8s.io/kustomize/kyaml v0.20.1/go.mod h1:0EmkQHRUsJxY8Ug9Niig1pUMSCGHxQ5RklbpV/Ri6po=
|
||||
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.2.0 h1:msyqjP8Nyd5sF3QSmJouFSzcBIdwq4ct8d1/7VSBHIQ=
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.2.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=
|
||||
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.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
|
||||
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
|
||||
|
|
|
@ -23,10 +23,11 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
utilerrors "k8s.io/apimachinery/pkg/util/errors"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/errors"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/cli-runtime/pkg/genericiooptions"
|
||||
|
@ -61,12 +62,10 @@ var (
|
|||
// APIResourceOptions is the start of the data required to perform the operation.
|
||||
// As new fields are added, add them here instead of referencing the cmd.Flags()
|
||||
type APIResourceOptions struct {
|
||||
Output string
|
||||
SortBy string
|
||||
APIGroup string
|
||||
Namespaced bool
|
||||
Verbs []string
|
||||
NoHeaders bool
|
||||
Cached bool
|
||||
Categories []string
|
||||
|
||||
|
@ -76,13 +75,8 @@ type APIResourceOptions struct {
|
|||
discoveryClient discovery.CachedDiscoveryInterface
|
||||
|
||||
genericiooptions.IOStreams
|
||||
}
|
||||
|
||||
// groupResource contains the APIGroup and APIResource
|
||||
type groupResource struct {
|
||||
APIGroup string
|
||||
APIGroupVersion string
|
||||
APIResource metav1.APIResource
|
||||
PrintFlags *PrintFlags
|
||||
PrintObj printers.ResourcePrinterFunc
|
||||
}
|
||||
|
||||
// NewAPIResourceOptions creates the options for APIResource
|
||||
|
@ -90,6 +84,7 @@ func NewAPIResourceOptions(ioStreams genericiooptions.IOStreams) *APIResourceOpt
|
|||
return &APIResourceOptions{
|
||||
IOStreams: ioStreams,
|
||||
Namespaced: true,
|
||||
PrintFlags: NewPrintFlags(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -109,8 +104,7 @@ func NewCmdAPIResources(restClientGetter genericclioptions.RESTClientGetter, ioS
|
|||
},
|
||||
}
|
||||
|
||||
cmd.Flags().BoolVar(&o.NoHeaders, "no-headers", o.NoHeaders, "When using the default or custom-column output format, don't print headers (default print headers).")
|
||||
cmd.Flags().StringVarP(&o.Output, "output", "o", o.Output, `Output format. One of: (wide, name).`)
|
||||
o.PrintFlags.AddFlags(cmd)
|
||||
|
||||
cmd.Flags().StringVar(&o.APIGroup, "api-group", o.APIGroup, "Limit to resources in the specified API group.")
|
||||
cmd.Flags().BoolVar(&o.Namespaced, "namespaced", o.Namespaced, "If false, non-namespaced resources will be returned, otherwise returning namespaced resources by default.")
|
||||
|
@ -123,10 +117,6 @@ 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.New[string]("", "wide", "name")
|
||||
if !supportedOutputTypes.Has(o.Output) {
|
||||
return fmt.Errorf("--output %v is not available", o.Output)
|
||||
}
|
||||
supportedSortTypes := sets.New[string]("", "name", "kind")
|
||||
if len(o.SortBy) > 0 {
|
||||
if !supportedSortTypes.Has(o.SortBy) {
|
||||
|
@ -151,6 +141,28 @@ func (o *APIResourceOptions) Complete(restClientGetter genericclioptions.RESTCli
|
|||
o.groupChanged = cmd.Flags().Changed("api-group")
|
||||
o.nsChanged = cmd.Flags().Changed("namespaced")
|
||||
|
||||
var printer printers.ResourcePrinter
|
||||
if o.PrintFlags.OutputFormat != nil {
|
||||
printer, err = o.PrintFlags.ToPrinter()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
o.PrintObj = func(object runtime.Object, out io.Writer) error {
|
||||
errs := []error{}
|
||||
if !*o.PrintFlags.NoHeaders &&
|
||||
(o.PrintFlags.OutputFormat == nil || *o.PrintFlags.OutputFormat == "" || *o.PrintFlags.OutputFormat == "wide") {
|
||||
if err = printContextHeaders(out, *o.PrintFlags.OutputFormat); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
if err := printer.PrintObj(object, out); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
return utilerrors.NewAggregate(errs)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -170,7 +182,7 @@ func (o *APIResourceOptions) RunAPIResources() error {
|
|||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
resources := []groupResource{}
|
||||
var allResources []*metav1.APIResourceList
|
||||
|
||||
for _, list := range lists {
|
||||
if len(list.APIResources) == 0 {
|
||||
|
@ -180,6 +192,14 @@ func (o *APIResourceOptions) RunAPIResources() error {
|
|||
if err != nil {
|
||||
continue
|
||||
}
|
||||
apiList := &metav1.APIResourceList{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "APIResourceList",
|
||||
APIVersion: "v1",
|
||||
},
|
||||
GroupVersion: gv.String(),
|
||||
}
|
||||
var apiResources []metav1.APIResource
|
||||
for _, resource := range list.APIResources {
|
||||
if len(resource.Verbs) == 0 {
|
||||
continue
|
||||
|
@ -200,58 +220,32 @@ func (o *APIResourceOptions) RunAPIResources() error {
|
|||
if len(o.Categories) > 0 && !sets.New[string](resource.Categories...).HasAll(o.Categories...) {
|
||||
continue
|
||||
}
|
||||
resources = append(resources, groupResource{
|
||||
APIGroup: gv.Group,
|
||||
APIGroupVersion: gv.String(),
|
||||
APIResource: resource,
|
||||
})
|
||||
// set these because we display a concatenation of these two values under APIVERSION column of human-readable output
|
||||
resource.Group = gv.Group
|
||||
resource.Version = gv.Version
|
||||
apiResources = append(apiResources, resource)
|
||||
}
|
||||
apiList.APIResources = apiResources
|
||||
allResources = append(allResources, apiList)
|
||||
}
|
||||
|
||||
if o.NoHeaders == false && o.Output != "name" {
|
||||
if err = printContextHeaders(w, o.Output); err != nil {
|
||||
return err
|
||||
}
|
||||
flatList := &metav1.APIResourceList{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: allResources[0].APIVersion,
|
||||
Kind: allResources[0].Kind,
|
||||
},
|
||||
}
|
||||
for _, resource := range allResources {
|
||||
flatList.APIResources = append(flatList.APIResources, resource.APIResources...)
|
||||
}
|
||||
|
||||
sort.Stable(sortableResource{resources, o.SortBy})
|
||||
for _, r := range resources {
|
||||
switch o.Output {
|
||||
case "name":
|
||||
name := r.APIResource.Name
|
||||
if len(r.APIGroup) > 0 {
|
||||
name += "." + r.APIGroup
|
||||
}
|
||||
if _, err := fmt.Fprintf(w, "%s\n", name); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
case "wide":
|
||||
if _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%v\t%s\t%v\t%v\n",
|
||||
r.APIResource.Name,
|
||||
strings.Join(r.APIResource.ShortNames, ","),
|
||||
r.APIGroupVersion,
|
||||
r.APIResource.Namespaced,
|
||||
r.APIResource.Kind,
|
||||
strings.Join(r.APIResource.Verbs, ","),
|
||||
strings.Join(r.APIResource.Categories, ",")); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
case "":
|
||||
if _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%v\t%s\n",
|
||||
r.APIResource.Name,
|
||||
strings.Join(r.APIResource.ShortNames, ","),
|
||||
r.APIGroupVersion,
|
||||
r.APIResource.Namespaced,
|
||||
r.APIResource.Kind); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
sort.Stable(sortableResource{flatList.APIResources, o.SortBy})
|
||||
|
||||
if len(errs) > 0 {
|
||||
return errors.NewAggregate(errs)
|
||||
err = o.PrintObj(flatList, w)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
return nil
|
||||
return utilerrors.NewAggregate(errs)
|
||||
}
|
||||
|
||||
func printContextHeaders(out io.Writer, output string) error {
|
||||
|
@ -264,7 +258,7 @@ func printContextHeaders(out io.Writer, output string) error {
|
|||
}
|
||||
|
||||
type sortableResource struct {
|
||||
resources []groupResource
|
||||
resources []metav1.APIResource
|
||||
sortBy string
|
||||
}
|
||||
|
||||
|
@ -277,7 +271,7 @@ func (s sortableResource) Less(i, j int) bool {
|
|||
if ret > 0 {
|
||||
return false
|
||||
} else if ret == 0 {
|
||||
return strings.Compare(s.resources[i].APIResource.Name, s.resources[j].APIResource.Name) < 0
|
||||
return strings.Compare(s.resources[i].Name, s.resources[j].Name) < 0
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
@ -285,9 +279,9 @@ func (s sortableResource) Less(i, j int) bool {
|
|||
func (s sortableResource) compareValues(i, j int) (string, string) {
|
||||
switch s.sortBy {
|
||||
case "name":
|
||||
return s.resources[i].APIResource.Name, s.resources[j].APIResource.Name
|
||||
return s.resources[i].Name, s.resources[j].Name
|
||||
case "kind":
|
||||
return s.resources[i].APIResource.Kind, s.resources[j].APIResource.Kind
|
||||
return s.resources[i].Kind, s.resources[j].Kind
|
||||
}
|
||||
return s.resources[i].APIGroup, s.resources[j].APIGroup
|
||||
return s.resources[i].Group, s.resources[j].Group
|
||||
}
|
||||
|
|
|
@ -17,13 +17,17 @@ limitations under the License.
|
|||
package apiresources
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/cli-runtime/pkg/genericiooptions"
|
||||
cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
func TestAPIResourcesComplete(t *testing.T) {
|
||||
|
@ -48,6 +52,16 @@ See 'kubectl api-resources -h' for help and examples`
|
|||
if err.Error() != expectedError {
|
||||
t.Fatalf("Unexpected error: %v\n expected: %v", err, expectedError)
|
||||
}
|
||||
|
||||
*o.PrintFlags.OutputFormat = "foo"
|
||||
err = o.Complete(tf, cmd, []string{})
|
||||
if err == nil {
|
||||
t.Fatalf("An error was expected but not returned")
|
||||
}
|
||||
expectedError = `unable to match a printer suitable for the output format "foo", allowed formats are:`
|
||||
if !strings.HasPrefix(err.Error(), expectedError) {
|
||||
t.Fatalf("Unexpected error: %v\n expected: %v", err, expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPIResourcesValidate(t *testing.T) {
|
||||
|
@ -61,13 +75,6 @@ func TestAPIResourcesValidate(t *testing.T) {
|
|||
optionSetupFn: func(o *APIResourceOptions) {},
|
||||
expectedError: "",
|
||||
},
|
||||
{
|
||||
name: "invalid output",
|
||||
optionSetupFn: func(o *APIResourceOptions) {
|
||||
o.Output = "foo"
|
||||
},
|
||||
expectedError: "--output foo is not available",
|
||||
},
|
||||
{
|
||||
name: "invalid sort by",
|
||||
optionSetupFn: func(o *APIResourceOptions) {
|
||||
|
@ -322,3 +329,92 @@ bazzes b somegroup/v1 true Baz
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestAPIResourcesRunJsonYaml is doing same thing as TestAPIResourcesRun but for JSON and YAML outputs
|
||||
// A separate test function is created because we are using apieqaulity.Semantic.DeepEqual
|
||||
// to check equality between input and output
|
||||
func TestAPIResourcesRunJsonYaml(t *testing.T) {
|
||||
dc := cmdtesting.NewFakeCachedDiscoveryClient()
|
||||
tf := cmdtesting.NewTestFactory().WithDiscoveryClient(dc)
|
||||
defer tf.Cleanup()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
expectedInvalidations int
|
||||
preferredResources []*v1.APIResourceList
|
||||
}{
|
||||
{
|
||||
name: "one",
|
||||
preferredResources: []*v1.APIResourceList{
|
||||
{
|
||||
GroupVersion: "v1",
|
||||
APIResources: []v1.APIResource{
|
||||
{
|
||||
Name: "foos",
|
||||
Namespaced: false,
|
||||
Kind: "Foo",
|
||||
Verbs: []string{"get", "list"},
|
||||
ShortNames: []string{"f", "fo"},
|
||||
Categories: []string{"some-category"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "two",
|
||||
preferredResources: []*v1.APIResourceList{
|
||||
{
|
||||
GroupVersion: "somegroup/v1",
|
||||
APIResources: []v1.APIResource{
|
||||
{
|
||||
Name: "bazzes",
|
||||
Namespaced: true,
|
||||
Kind: "Baz",
|
||||
Verbs: []string{"get", "list", "create", "delete"},
|
||||
ShortNames: []string{"b"},
|
||||
Categories: []string{"some-category", "another-category"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(tt *testing.T) {
|
||||
dc.PreferredResources = tc.preferredResources
|
||||
ioStreams, _, out, errOut := genericiooptions.NewTestIOStreams()
|
||||
|
||||
for _, v := range []string{"json", "yaml"} {
|
||||
cmd := NewCmdAPIResources(tf, ioStreams)
|
||||
err := cmd.Flags().Set("output", v)
|
||||
require.NoError(tt, err)
|
||||
cmd.Run(cmd, []string{})
|
||||
|
||||
if errOut.Len() > 0 {
|
||||
t.Fatalf("unexpected error output: %s", errOut.String())
|
||||
}
|
||||
apiResourceList := v1.APIResourceList{}
|
||||
switch v {
|
||||
case "json":
|
||||
err = json.Unmarshal(out.Bytes(), &apiResourceList)
|
||||
case "yaml":
|
||||
err = yaml.Unmarshal(out.Bytes(), &apiResourceList)
|
||||
}
|
||||
require.NoError(tt, err)
|
||||
|
||||
// this will undo custom value we add in RunAPIResources in the lines:
|
||||
// resource.Group = gv.Group
|
||||
// resource.Version = gv.Version
|
||||
apiResourceList.GroupVersion = apiResourceList.APIResources[0].Group + "/" + apiResourceList.APIResources[0].Version
|
||||
apiResourceList.APIResources[0].Version = ""
|
||||
apiResourceList.APIResources[0].Group = ""
|
||||
|
||||
if !apiequality.Semantic.DeepEqual(tc.preferredResources[0].APIResources[0], apiResourceList.APIResources[0]) {
|
||||
tt.Fatalf("expected output: [%v]\n, but got [%v]", tc.preferredResources[0].APIResources[0], apiResourceList.APIResources[0])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,233 @@
|
|||
/*
|
||||
Copyright 2025 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package apiresources
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
utilerrors "k8s.io/apimachinery/pkg/util/errors"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/cli-runtime/pkg/printers"
|
||||
)
|
||||
|
||||
type PrintFlags struct {
|
||||
JSONYamlPrintFlags *genericclioptions.JSONYamlPrintFlags
|
||||
NamePrintFlags NamePrintFlags
|
||||
HumanReadableFlags HumanPrintFlags
|
||||
|
||||
NoHeaders *bool
|
||||
OutputFormat *string
|
||||
}
|
||||
|
||||
func NewPrintFlags() *PrintFlags {
|
||||
outputFormat := ""
|
||||
noHeaders := false
|
||||
|
||||
return &PrintFlags{
|
||||
OutputFormat: &outputFormat,
|
||||
NoHeaders: &noHeaders,
|
||||
JSONYamlPrintFlags: genericclioptions.NewJSONYamlPrintFlags(),
|
||||
NamePrintFlags: APIResourcesNewNamePrintFlags(),
|
||||
HumanReadableFlags: APIResourcesHumanReadableFlags(),
|
||||
}
|
||||
}
|
||||
|
||||
func (f *PrintFlags) AddFlags(cmd *cobra.Command) {
|
||||
f.JSONYamlPrintFlags.AddFlags(cmd)
|
||||
f.HumanReadableFlags.AddFlags(cmd)
|
||||
f.NamePrintFlags.AddFlags(cmd)
|
||||
|
||||
if f.OutputFormat != nil {
|
||||
cmd.Flags().StringVarP(f.OutputFormat, "output", "o", *f.OutputFormat, fmt.Sprintf("Output format. One of: (%s).", strings.Join(f.AllowedFormats(), ", ")))
|
||||
}
|
||||
if f.NoHeaders != nil {
|
||||
cmd.Flags().BoolVar(f.NoHeaders, "no-headers", *f.NoHeaders, "When using the default or custom-column output format, don't print headers (default print headers).")
|
||||
}
|
||||
}
|
||||
|
||||
// PrintOptions struct defines a struct for various print options
|
||||
type PrintOptions struct {
|
||||
SortBy *string
|
||||
NoHeaders bool
|
||||
Wide bool
|
||||
}
|
||||
|
||||
type HumanPrintFlags struct {
|
||||
SortBy *string
|
||||
NoHeaders bool
|
||||
}
|
||||
|
||||
func (f *HumanPrintFlags) AllowedFormats() []string {
|
||||
return []string{"wide"}
|
||||
}
|
||||
|
||||
// AddFlags receives a *cobra.Command reference and binds
|
||||
// flags related to human-readable printing to it
|
||||
func (f *HumanPrintFlags) AddFlags(c *cobra.Command) {
|
||||
if f.SortBy != nil {
|
||||
c.Flags().StringVar(f.SortBy, "sort-by", *f.SortBy, "If non-empty, sort list types using this field specification. The field specification is expressed as a JSONPath expression (e.g. '{.metadata.name}'). The field in the API resource specified by this JSONPath expression must be an integer or a string.")
|
||||
}
|
||||
}
|
||||
|
||||
// ToPrinter receives an outputFormat and returns a printer capable of
|
||||
// handling human-readable output.
|
||||
func (f *HumanPrintFlags) ToPrinter(outputFormat string) (printers.ResourcePrinter, error) {
|
||||
if len(outputFormat) > 0 && outputFormat != "wide" {
|
||||
return nil, genericclioptions.NoCompatiblePrinterError{Options: f, AllowedFormats: f.AllowedFormats()}
|
||||
}
|
||||
|
||||
p := HumanReadablePrinter{
|
||||
options: PrintOptions{
|
||||
NoHeaders: f.NoHeaders,
|
||||
Wide: outputFormat == "wide",
|
||||
},
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
type HumanReadablePrinter struct {
|
||||
options PrintOptions
|
||||
}
|
||||
|
||||
func (f HumanReadablePrinter) PrintObj(obj runtime.Object, w io.Writer) error {
|
||||
flatList, ok := obj.(*metav1.APIResourceList)
|
||||
if !ok {
|
||||
return fmt.Errorf("object is not a APIResourceList")
|
||||
}
|
||||
var errs []error
|
||||
for _, r := range flatList.APIResources {
|
||||
gv, err := schema.ParseGroupVersion(strings.Join([]string{r.Group, r.Version}, "/"))
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
continue
|
||||
}
|
||||
if f.options.Wide {
|
||||
if _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%v\t%s\t%v\t%v\n",
|
||||
r.Name,
|
||||
strings.Join(r.ShortNames, ","),
|
||||
gv.String(),
|
||||
r.Namespaced,
|
||||
r.Kind,
|
||||
strings.Join(r.Verbs, ","),
|
||||
strings.Join(r.Categories, ",")); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%v\t%s\n",
|
||||
r.Name,
|
||||
strings.Join(r.ShortNames, ","),
|
||||
gv.String(),
|
||||
r.Namespaced,
|
||||
r.Kind); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
return utilerrors.NewAggregate(errs)
|
||||
}
|
||||
|
||||
type NamePrintFlags struct{}
|
||||
|
||||
func APIResourcesNewNamePrintFlags() NamePrintFlags {
|
||||
return NamePrintFlags{}
|
||||
}
|
||||
|
||||
func (f *NamePrintFlags) AllowedFormats() []string {
|
||||
return []string{"name"}
|
||||
}
|
||||
|
||||
// AddFlags receives a *cobra.Command reference and binds
|
||||
// flags related to name printing to it
|
||||
func (f *NamePrintFlags) AddFlags(_ *cobra.Command) {}
|
||||
|
||||
// ToPrinter receives an outputFormat and returns a printer capable of
|
||||
// handling human-readable output.
|
||||
func (f *NamePrintFlags) ToPrinter(outputFormat string) (printers.ResourcePrinter, error) {
|
||||
if outputFormat == "name" {
|
||||
return NamePrinter{}, nil
|
||||
}
|
||||
return nil, genericclioptions.NoCompatiblePrinterError{Options: f, AllowedFormats: f.AllowedFormats()}
|
||||
}
|
||||
|
||||
type NamePrinter struct{}
|
||||
|
||||
func (f NamePrinter) PrintObj(obj runtime.Object, w io.Writer) error {
|
||||
flatList, ok := obj.(*metav1.APIResourceList)
|
||||
if !ok {
|
||||
return fmt.Errorf("object is not a APIResourceList")
|
||||
}
|
||||
var errs []error
|
||||
for _, r := range flatList.APIResources {
|
||||
name := r.Name
|
||||
if len(r.Group) > 0 {
|
||||
name += "." + r.Group
|
||||
}
|
||||
if _, err := fmt.Fprintf(w, "%s\n", name); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
return utilerrors.NewAggregate(errs)
|
||||
}
|
||||
|
||||
func APIResourcesHumanReadableFlags() HumanPrintFlags {
|
||||
return HumanPrintFlags{
|
||||
SortBy: nil,
|
||||
NoHeaders: false,
|
||||
}
|
||||
}
|
||||
|
||||
func (f *PrintFlags) AllowedFormats() []string {
|
||||
ret := []string{}
|
||||
ret = append(ret, f.JSONYamlPrintFlags.AllowedFormats()...)
|
||||
ret = append(ret, f.NamePrintFlags.AllowedFormats()...)
|
||||
ret = append(ret, f.HumanReadableFlags.AllowedFormats()...)
|
||||
return ret
|
||||
}
|
||||
|
||||
func (f *PrintFlags) ToPrinter() (printers.ResourcePrinter, error) {
|
||||
outputFormat := ""
|
||||
if f.OutputFormat != nil {
|
||||
outputFormat = *f.OutputFormat
|
||||
}
|
||||
|
||||
noHeaders := false
|
||||
if f.NoHeaders != nil {
|
||||
noHeaders = *f.NoHeaders
|
||||
}
|
||||
f.HumanReadableFlags.NoHeaders = noHeaders
|
||||
|
||||
if p, err := f.JSONYamlPrintFlags.ToPrinter(outputFormat); !genericclioptions.IsNoCompatiblePrinterError(err) {
|
||||
return p, err
|
||||
}
|
||||
|
||||
if p, err := f.HumanReadableFlags.ToPrinter(outputFormat); !genericclioptions.IsNoCompatiblePrinterError(err) {
|
||||
return p, err
|
||||
}
|
||||
|
||||
if p, err := f.NamePrintFlags.ToPrinter(outputFormat); !genericclioptions.IsNoCompatiblePrinterError(err) {
|
||||
return p, err
|
||||
}
|
||||
|
||||
return nil, genericclioptions.NoCompatiblePrinterError{OutputFormat: &outputFormat, AllowedFormats: f.AllowedFormats()}
|
||||
}
|
|
@ -42,6 +42,7 @@ import (
|
|||
"k8s.io/kubectl/pkg/util/completion"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
"k8s.io/kubectl/pkg/util/term"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -300,7 +301,9 @@ func (o *AttachOptions) Run() error {
|
|||
sizePlusOne.Height++
|
||||
|
||||
// this call spawns a goroutine to monitor/update the terminal size
|
||||
sizeQueue = t.MonitorSize(&sizePlusOne, size)
|
||||
sizeQueue = &terminalSizeQueueAdapter{
|
||||
delegate: t.MonitorSize(&sizePlusOne, size),
|
||||
}
|
||||
}
|
||||
|
||||
o.DisableStderr = true
|
||||
|
@ -357,3 +360,18 @@ func (o *AttachOptions) reattachMessage(containerName string, rawTTY bool) strin
|
|||
}
|
||||
return fmt.Sprintf("Session ended, resume using '%s %s -c %s -i -t' command when the pod is running", o.CommandName, o.Pod.Name, containerName)
|
||||
}
|
||||
|
||||
type terminalSizeQueueAdapter struct {
|
||||
delegate term.TerminalSizeQueue
|
||||
}
|
||||
|
||||
func (a *terminalSizeQueueAdapter) Next() *remotecommand.TerminalSize {
|
||||
next := a.delegate.Next()
|
||||
if next == nil {
|
||||
return nil
|
||||
}
|
||||
return &remotecommand.TerminalSize{
|
||||
Width: next.Width,
|
||||
Height: next.Height,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ import (
|
|||
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/klog/v2"
|
||||
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
|
@ -33,6 +34,7 @@ import (
|
|||
"k8s.io/cli-runtime/pkg/resource"
|
||||
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||
rbacv1client "k8s.io/client-go/kubernetes/typed/rbac/v1"
|
||||
"k8s.io/client-go/util/retry"
|
||||
"k8s.io/component-helpers/auth/rbac/reconciliation"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/scheme"
|
||||
|
@ -197,6 +199,9 @@ func (o *ReconcileOptions) Validate() error {
|
|||
|
||||
// RunReconcile performs the execution
|
||||
func (o *ReconcileOptions) RunReconcile() error {
|
||||
// conflictBackoff retries up to 3 times on conflict, with no delay
|
||||
conflictBackoff := wait.Backoff{Steps: 3}
|
||||
|
||||
return o.Visitor.Visit(func(info *resource.Info, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -213,7 +218,14 @@ func (o *ReconcileOptions) RunReconcile() error {
|
|||
Client: o.RBACClient,
|
||||
},
|
||||
}
|
||||
result, err := reconcileOptions.Run()
|
||||
var (
|
||||
result *reconciliation.ReconcileClusterRoleResult
|
||||
err error
|
||||
)
|
||||
retry.RetryOnConflict(conflictBackoff, func() error {
|
||||
result, err = reconcileOptions.Run()
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -228,7 +240,14 @@ func (o *ReconcileOptions) RunReconcile() error {
|
|||
Client: o.RBACClient.ClusterRoles(),
|
||||
},
|
||||
}
|
||||
result, err := reconcileOptions.Run()
|
||||
var (
|
||||
result *reconciliation.ReconcileClusterRoleResult
|
||||
err error
|
||||
)
|
||||
retry.RetryOnConflict(conflictBackoff, func() error {
|
||||
result, err = reconcileOptions.Run()
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -244,10 +263,14 @@ func (o *ReconcileOptions) RunReconcile() error {
|
|||
NamespaceClient: o.NamespaceClient.Namespaces(),
|
||||
},
|
||||
}
|
||||
result, err := reconcileOptions.Run()
|
||||
if err != nil {
|
||||
var (
|
||||
result *reconciliation.ReconcileClusterRoleBindingResult
|
||||
err error
|
||||
)
|
||||
retry.RetryOnConflict(conflictBackoff, func() error {
|
||||
result, err = reconcileOptions.Run()
|
||||
return err
|
||||
}
|
||||
})
|
||||
o.printResults(result.RoleBinding.GetObject(), result.MissingSubjects, result.ExtraSubjects, nil, nil, result.Operation, result.Protected)
|
||||
|
||||
case *rbacv1.ClusterRoleBinding:
|
||||
|
@ -259,10 +282,14 @@ func (o *ReconcileOptions) RunReconcile() error {
|
|||
Client: o.RBACClient.ClusterRoleBindings(),
|
||||
},
|
||||
}
|
||||
result, err := reconcileOptions.Run()
|
||||
if err != nil {
|
||||
var (
|
||||
result *reconciliation.ReconcileClusterRoleBindingResult
|
||||
err error
|
||||
)
|
||||
retry.RetryOnConflict(conflictBackoff, func() error {
|
||||
result, err = reconcileOptions.Run()
|
||||
return err
|
||||
}
|
||||
})
|
||||
o.printResults(result.RoleBinding.GetObject(), result.MissingSubjects, result.ExtraSubjects, nil, nil, result.Operation, result.Protected)
|
||||
|
||||
case *rbacv1beta1.Role,
|
||||
|
|
|
@ -129,7 +129,8 @@ func NewCmdConfigSetCredentials(out io.Writer, configAccess clientcmd.ConfigAcce
|
|||
}
|
||||
|
||||
// NewCmdConfigSetAuthInfo returns a Command instance for 'config set-credentials' sub command
|
||||
// DEPRECATED: Use NewCmdConfigSetCredentials instead
|
||||
//
|
||||
// Deprecated: Use NewCmdConfigSetCredentials instead
|
||||
func NewCmdConfigSetAuthInfo(out io.Writer, configAccess clientcmd.ConfigAccess) *cobra.Command {
|
||||
return NewCmdConfigSetCredentials(out, configAccess)
|
||||
}
|
||||
|
|
|
@ -106,7 +106,7 @@ func NewCmdCreateJob(f cmdutil.Factory, ioStreams genericiooptions.IOStreams) *c
|
|||
cmdutil.AddValidateFlags(cmd)
|
||||
cmdutil.AddDryRunFlag(cmd)
|
||||
cmd.Flags().StringVar(&o.Image, "image", o.Image, "Image name to run.")
|
||||
cmd.Flags().StringVar(&o.From, "from", o.From, "The name of the resource to create a Job from (only cronjob is supported).")
|
||||
cmd.Flags().StringVar(&o.From, "from", o.From, "The name of the resource to create a Job from (only CronJob is supported).")
|
||||
cmdutil.AddFieldManagerFlagVar(cmd, &o.FieldManager, "kubectl-create")
|
||||
return cmd
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ package drain
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
|
@ -151,10 +152,11 @@ func NewDrainCmdOptions(f cmdutil.Factory, ioStreams genericiooptions.IOStreams)
|
|||
PrintFlags: genericclioptions.NewPrintFlags("drained").WithTypeSetter(scheme.Scheme),
|
||||
IOStreams: ioStreams,
|
||||
drainer: &drain.Helper{
|
||||
GracePeriodSeconds: -1,
|
||||
Out: ioStreams.Out,
|
||||
ErrOut: ioStreams.ErrOut,
|
||||
ChunkSize: cmdutil.DefaultChunkSize,
|
||||
GracePeriodSeconds: -1,
|
||||
EvictErrorRetryDelay: 5 * time.Second,
|
||||
Out: ioStreams.Out,
|
||||
ErrOut: ioStreams.ErrOut,
|
||||
ChunkSize: cmdutil.DefaultChunkSize,
|
||||
},
|
||||
}
|
||||
o.drainer.OnPodDeletionOrEvictionFinished = o.onPodDeletionOrEvictionFinished
|
||||
|
|
|
@ -366,7 +366,9 @@ func (p *ExecOptions) Run() error {
|
|||
var sizeQueue remotecommand.TerminalSizeQueue
|
||||
if t.Raw {
|
||||
// this call spawns a goroutine to monitor/update the terminal size
|
||||
sizeQueue = t.MonitorSize(t.GetSize())
|
||||
sizeQueue = &terminalSizeQueueAdapter{
|
||||
delegate: t.MonitorSize(t.GetSize()),
|
||||
}
|
||||
|
||||
// unset p.Err if it was previously set because both stdout and stderr go over p.Out when tty is
|
||||
// true
|
||||
|
@ -403,3 +405,18 @@ func (p *ExecOptions) Run() error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
type terminalSizeQueueAdapter struct {
|
||||
delegate term.TerminalSizeQueue
|
||||
}
|
||||
|
||||
func (a *terminalSizeQueueAdapter) Next() *remotecommand.TerminalSize {
|
||||
next := a.delegate.Next()
|
||||
if next == nil {
|
||||
return nil
|
||||
}
|
||||
return &remotecommand.TerminalSize{
|
||||
Width: next.Width,
|
||||
Height: next.Height,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -206,7 +206,7 @@ func (flags *ExposeServiceFlags) AddFlags(cmd *cobra.Command) {
|
|||
cmd.Flags().StringVar(&flags.Port, "port", flags.Port, i18n.T("The port that the service should serve on. Copied from the resource being exposed, if unspecified"))
|
||||
cmd.Flags().StringVar(&flags.Type, "type", flags.Type, i18n.T("Type for this service: ClusterIP, NodePort, LoadBalancer, or ExternalName. Default is 'ClusterIP'."))
|
||||
cmd.Flags().StringVar(&flags.LoadBalancerIP, "load-balancer-ip", flags.LoadBalancerIP, i18n.T("IP to assign to the LoadBalancer. If empty, an ephemeral IP will be created and used (cloud-provider specific)."))
|
||||
cmd.Flags().StringVar(&flags.Selector, "selector", flags.Selector, i18n.T("A label selector to use for this service. Only equality-based selector requirements are supported. If empty (the default) infer the selector from the replication controller or replica set.)"))
|
||||
cmd.Flags().StringVar(&flags.Selector, "selector", flags.Selector, i18n.T("A label selector to use for this service. Only equality-based selector requirements are supported. If empty (the default) infer the selector from the resource being exposed."))
|
||||
cmd.Flags().StringVarP(&flags.Labels, "labels", "l", flags.Labels, "Labels to apply to the service created by this call.")
|
||||
cmd.Flags().StringVar(&flags.TargetPort, "target-port", flags.TargetPort, i18n.T("Name or number for the port on the container that the service should direct traffic to. Optional."))
|
||||
cmd.Flags().StringVar(&flags.ExternalIP, "external-ip", flags.ExternalIP, i18n.T("Additional external IP address (not managed by Kubernetes) to accept for the service. If this IP is routed to a node, the service can be accessed by this IP in addition to its generated service IP."))
|
||||
|
|
|
@ -74,7 +74,7 @@ var (
|
|||
selectorExample = templates.Examples(`
|
||||
# Set the labels and selector before creating a deployment/service pair
|
||||
kubectl create service clusterip my-svc --clusterip="None" -o yaml --dry-run=client | kubectl set selector --local -f - 'environment=qa' -o yaml | kubectl create -f -
|
||||
kubectl create deployment my-dep -o yaml --dry-run=client | kubectl label --local -f - environment=qa -o yaml | kubectl create -f -`)
|
||||
kubectl create deployment my-dep --image=nginx -o yaml --dry-run=client | kubectl label --local -f - environment=qa -o yaml | kubectl create -f -`)
|
||||
)
|
||||
|
||||
// NewSelectorOptions returns an initialized SelectorOptions instance
|
||||
|
|
|
@ -455,6 +455,12 @@ const (
|
|||
// Transition to WebSockets.
|
||||
RemoteCommandWebsockets FeatureGate = "KUBECTL_REMOTE_COMMAND_WEBSOCKETS"
|
||||
PortForwardWebsockets FeatureGate = "KUBECTL_PORT_FORWARD_WEBSOCKETS"
|
||||
|
||||
// owner: @thockin
|
||||
// kep: https://kep.k8s.io/5296
|
||||
//
|
||||
// Support KYAML output.
|
||||
KYAMLOutput FeatureGate = "KUBECTL_KYAML"
|
||||
)
|
||||
|
||||
// IsEnabled returns true iff environment variable is set to true.
|
||||
|
|
|
@ -36,7 +36,7 @@ import (
|
|||
)
|
||||
|
||||
// TODO(knverey): remove this hardcoding once kubectl being built with module support makes BuildInfo available.
|
||||
const kustomizeVersion = "v5.5.0"
|
||||
const kustomizeVersion = "v5.7.1"
|
||||
|
||||
// Version is a struct for version information
|
||||
type Version struct {
|
||||
|
|
|
@ -174,7 +174,7 @@ func getObjAndCheckCondition(ctx context.Context, info *resource.Info, o *WaitOp
|
|||
return err
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, wait.ErrWaitTimeout) { // nolint:staticcheck // SA1019
|
||||
if wait.Interrupted(err) { // nolint:staticcheck // SA1019
|
||||
return result, false, errWaitTimeoutWithName
|
||||
}
|
||||
return result, false, err
|
||||
|
|
|
@ -113,7 +113,7 @@ func IsDeleted(ctx context.Context, info *resource.Info, o *WaitOptions) (runtim
|
|||
return err
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, wait.ErrWaitTimeout) { // nolint:staticcheck // SA1019
|
||||
if wait.Interrupted(err) { // nolint:staticcheck // SA1019
|
||||
return gottenObj, false, errWaitTimeoutWithName
|
||||
}
|
||||
return gottenObj, false, err
|
||||
|
|
|
@ -46,7 +46,7 @@ import (
|
|||
|
||||
var (
|
||||
waitLong = templates.LongDesc(i18n.T(`
|
||||
Experimental: Wait for a specific condition on one or many resources.
|
||||
Wait for a specific condition on one or many resources.
|
||||
|
||||
The command takes multiple resources and waits until the specified condition
|
||||
is seen in the Status field of every given resource.
|
||||
|
|
|
@ -53,7 +53,6 @@ import (
|
|||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
schedulingv1 "k8s.io/api/scheduling/v1"
|
||||
storagev1 "k8s.io/api/storage/v1"
|
||||
storagev1beta1 "k8s.io/api/storage/v1beta1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
|
@ -231,7 +230,7 @@ func describerMap(clientConfig *rest.Config) (map[schema.GroupKind]ResourceDescr
|
|||
{Group: certificatesv1beta1.GroupName, Kind: "CertificateSigningRequest"}: &CertificateSigningRequestDescriber{c},
|
||||
{Group: storagev1.GroupName, Kind: "StorageClass"}: &StorageClassDescriber{c},
|
||||
{Group: storagev1.GroupName, Kind: "CSINode"}: &CSINodeDescriber{c},
|
||||
{Group: storagev1beta1.GroupName, Kind: "VolumeAttributesClass"}: &VolumeAttributesClassDescriber{c},
|
||||
{Group: storagev1.GroupName, Kind: "VolumeAttributesClass"}: &VolumeAttributesClassDescriber{c},
|
||||
{Group: policyv1beta1.GroupName, Kind: "PodDisruptionBudget"}: &PodDisruptionBudgetDescriber{c},
|
||||
{Group: policyv1.GroupName, Kind: "PodDisruptionBudget"}: &PodDisruptionBudgetDescriber{c},
|
||||
{Group: rbacv1.GroupName, Kind: "Role"}: &RoleDescriber{c},
|
||||
|
@ -1865,7 +1864,11 @@ func describeContainerBasicInfo(container corev1.Container, status corev1.Contai
|
|||
func describeContainerPorts(cPorts []corev1.ContainerPort) string {
|
||||
ports := make([]string, 0, len(cPorts))
|
||||
for _, cPort := range cPorts {
|
||||
ports = append(ports, fmt.Sprintf("%d/%s", cPort.ContainerPort, cPort.Protocol))
|
||||
portStr := fmt.Sprintf("%d/%s", cPort.ContainerPort, cPort.Protocol)
|
||||
if cPort.Name != "" {
|
||||
portStr = fmt.Sprintf("%s (%s)", portStr, cPort.Name)
|
||||
}
|
||||
ports = append(ports, portStr)
|
||||
}
|
||||
return strings.Join(ports, ", ")
|
||||
}
|
||||
|
@ -1873,7 +1876,11 @@ func describeContainerPorts(cPorts []corev1.ContainerPort) string {
|
|||
func describeContainerHostPorts(cPorts []corev1.ContainerPort) string {
|
||||
ports := make([]string, 0, len(cPorts))
|
||||
for _, cPort := range cPorts {
|
||||
ports = append(ports, fmt.Sprintf("%d/%s", cPort.HostPort, cPort.Protocol))
|
||||
portStr := fmt.Sprintf("%d/%s", cPort.HostPort, cPort.Protocol)
|
||||
if cPort.Name != "" {
|
||||
portStr = fmt.Sprintf("%s (%s)", portStr, cPort.Name)
|
||||
}
|
||||
ports = append(ports, portStr)
|
||||
}
|
||||
return strings.Join(ports, ", ")
|
||||
}
|
||||
|
@ -3438,61 +3445,15 @@ func (d *ServiceAccountDescriber) Describe(namespace, name string, describerSett
|
|||
return "", err
|
||||
}
|
||||
|
||||
tokens := []corev1.Secret{}
|
||||
|
||||
// missingSecrets is the set of all secrets present in the
|
||||
// serviceAccount but not present in the set of existing secrets.
|
||||
missingSecrets := sets.New[string]()
|
||||
secrets := corev1.SecretList{}
|
||||
err = runtimeresource.FollowContinue(&metav1.ListOptions{Limit: describerSettings.ChunkSize},
|
||||
func(options metav1.ListOptions) (runtime.Object, error) {
|
||||
newList, err := d.CoreV1().Secrets(namespace).List(context.TODO(), options)
|
||||
if err != nil {
|
||||
return nil, runtimeresource.EnhanceListError(err, options, corev1.ResourceSecrets.String())
|
||||
}
|
||||
secrets.Items = append(secrets.Items, newList.Items...)
|
||||
return newList, nil
|
||||
})
|
||||
|
||||
// errors are tolerated here in order to describe the serviceAccount with all
|
||||
// of the secrets that it references, even if those secrets cannot be fetched.
|
||||
if err == nil {
|
||||
// existingSecrets is the set of all secrets remaining on a
|
||||
// service account that are not present in the "tokens" slice.
|
||||
existingSecrets := sets.New[string]()
|
||||
|
||||
for _, s := range secrets.Items {
|
||||
if s.Type == corev1.SecretTypeServiceAccountToken {
|
||||
name := s.Annotations[corev1.ServiceAccountNameKey]
|
||||
uid := s.Annotations[corev1.ServiceAccountUIDKey]
|
||||
if name == serviceAccount.Name && uid == string(serviceAccount.UID) {
|
||||
tokens = append(tokens, s)
|
||||
}
|
||||
}
|
||||
existingSecrets.Insert(s.Name)
|
||||
}
|
||||
|
||||
for _, s := range serviceAccount.Secrets {
|
||||
if !existingSecrets.Has(s.Name) {
|
||||
missingSecrets.Insert(s.Name)
|
||||
}
|
||||
}
|
||||
for _, s := range serviceAccount.ImagePullSecrets {
|
||||
if !existingSecrets.Has(s.Name) {
|
||||
missingSecrets.Insert(s.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var events *corev1.EventList
|
||||
if describerSettings.ShowEvents {
|
||||
events, _ = searchEvents(d.CoreV1(), serviceAccount, describerSettings.ChunkSize)
|
||||
}
|
||||
|
||||
return describeServiceAccount(serviceAccount, tokens, missingSecrets, events)
|
||||
return describeServiceAccount(serviceAccount, events)
|
||||
}
|
||||
|
||||
func describeServiceAccount(serviceAccount *corev1.ServiceAccount, tokens []corev1.Secret, missingSecrets sets.Set[string], events *corev1.EventList) (string, error) {
|
||||
func describeServiceAccount(serviceAccount *corev1.ServiceAccount, events *corev1.EventList) (string, error) {
|
||||
return tabbedString(func(out io.Writer) error {
|
||||
w := NewPrefixWriter(out)
|
||||
w.Write(LEVEL_0, "Name:\t%s\n", serviceAccount.Name)
|
||||
|
@ -3503,28 +3464,16 @@ func describeServiceAccount(serviceAccount *corev1.ServiceAccount, tokens []core
|
|||
var (
|
||||
emptyHeader = " "
|
||||
pullHeader = "Image pull secrets:"
|
||||
mountHeader = "Mountable secrets: "
|
||||
tokenHeader = "Tokens: "
|
||||
|
||||
pullSecretNames = []string{}
|
||||
mountSecretNames = []string{}
|
||||
tokenSecretNames = []string{}
|
||||
pullSecretNames = []string{}
|
||||
)
|
||||
|
||||
for _, s := range serviceAccount.ImagePullSecrets {
|
||||
pullSecretNames = append(pullSecretNames, s.Name)
|
||||
}
|
||||
for _, s := range serviceAccount.Secrets {
|
||||
mountSecretNames = append(mountSecretNames, s.Name)
|
||||
}
|
||||
for _, s := range tokens {
|
||||
tokenSecretNames = append(tokenSecretNames, s.Name)
|
||||
}
|
||||
|
||||
types := map[string][]string{
|
||||
pullHeader: pullSecretNames,
|
||||
mountHeader: mountSecretNames,
|
||||
tokenHeader: tokenSecretNames,
|
||||
pullHeader: pullSecretNames,
|
||||
}
|
||||
for _, header := range sets.List(sets.KeySet(types)) {
|
||||
names := types[header]
|
||||
|
@ -3533,11 +3482,7 @@ func describeServiceAccount(serviceAccount *corev1.ServiceAccount, tokens []core
|
|||
} else {
|
||||
prefix := header
|
||||
for _, name := range names {
|
||||
if missingSecrets.Has(name) {
|
||||
w.Write(LEVEL_0, "%s\t%s (not found)\n", prefix, name)
|
||||
} else {
|
||||
w.Write(LEVEL_0, "%s\t%s\n", prefix, name)
|
||||
}
|
||||
w.Write(LEVEL_0, "%s\t%s\n", prefix, name)
|
||||
prefix = emptyHeader
|
||||
}
|
||||
}
|
||||
|
@ -4816,7 +4761,7 @@ type VolumeAttributesClassDescriber struct {
|
|||
}
|
||||
|
||||
func (d *VolumeAttributesClassDescriber) Describe(namespace, name string, describerSettings DescriberSettings) (string, error) {
|
||||
vac, err := d.StorageV1beta1().VolumeAttributesClasses().Get(context.TODO(), name, metav1.GetOptions{})
|
||||
vac, err := d.StorageV1().VolumeAttributesClasses().Get(context.TODO(), name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
@ -4829,7 +4774,7 @@ func (d *VolumeAttributesClassDescriber) Describe(namespace, name string, descri
|
|||
return describeVolumeAttributesClass(vac, events)
|
||||
}
|
||||
|
||||
func describeVolumeAttributesClass(vac *storagev1beta1.VolumeAttributesClass, events *corev1.EventList) (string, error) {
|
||||
func describeVolumeAttributesClass(vac *storagev1.VolumeAttributesClass, events *corev1.EventList) (string, error) {
|
||||
return tabbedString(func(out io.Writer) error {
|
||||
w := NewPrefixWriter(out)
|
||||
w.Write(LEVEL_0, "Name:\t%s\n", vac.Name)
|
||||
|
|
|
@ -43,7 +43,6 @@ import (
|
|||
policyv1beta1 "k8s.io/api/policy/v1beta1"
|
||||
schedulingv1 "k8s.io/api/scheduling/v1"
|
||||
storagev1 "k8s.io/api/storage/v1"
|
||||
storagev1beta1 "k8s.io/api/storage/v1beta1"
|
||||
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
@ -1372,6 +1371,74 @@ func TestDescribeResources(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestDescribeContainerPorts(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
ports []corev1.ContainerPort
|
||||
expectedContainer string
|
||||
expectedHost string
|
||||
}{
|
||||
{
|
||||
name: "no ports",
|
||||
ports: []corev1.ContainerPort{},
|
||||
expectedContainer: "",
|
||||
expectedHost: "",
|
||||
},
|
||||
{
|
||||
name: "container and host port, with name",
|
||||
ports: []corev1.ContainerPort{
|
||||
{Name: "web", ContainerPort: 8080, HostPort: 8080, Protocol: corev1.ProtocolTCP},
|
||||
},
|
||||
expectedContainer: "8080/TCP (web)",
|
||||
expectedHost: "8080/TCP (web)",
|
||||
},
|
||||
{
|
||||
name: "container and host port, no name",
|
||||
ports: []corev1.ContainerPort{
|
||||
{ContainerPort: 8080, HostPort: 8080, Protocol: corev1.ProtocolTCP},
|
||||
},
|
||||
expectedContainer: "8080/TCP",
|
||||
expectedHost: "8080/TCP",
|
||||
},
|
||||
{
|
||||
name: "multiple ports with mixed configuration",
|
||||
ports: []corev1.ContainerPort{
|
||||
{Name: "controller", ContainerPort: 9093, HostPort: 9093, Protocol: corev1.ProtocolTCP},
|
||||
{ContainerPort: 9092, Protocol: corev1.ProtocolTCP},
|
||||
{Name: "interbroker", ContainerPort: 9094, HostPort: 9094, Protocol: corev1.ProtocolTCP},
|
||||
},
|
||||
expectedContainer: "9093/TCP (controller), 9092/TCP, 9094/TCP (interbroker)",
|
||||
expectedHost: "9093/TCP (controller), 0/TCP, 9094/TCP (interbroker)",
|
||||
},
|
||||
{
|
||||
name: "all ports with mixed configuration",
|
||||
ports: []corev1.ContainerPort{
|
||||
{Name: "controller", ContainerPort: 9093, HostPort: 9093, Protocol: corev1.ProtocolTCP},
|
||||
{Name: "client", ContainerPort: 9092, HostPort: 9092, Protocol: corev1.ProtocolTCP},
|
||||
{Name: "interbroker", ContainerPort: 9094, HostPort: 9094, Protocol: corev1.ProtocolTCP},
|
||||
},
|
||||
expectedContainer: "9093/TCP (controller), 9092/TCP (client), 9094/TCP (interbroker)",
|
||||
expectedHost: "9093/TCP (controller), 9092/TCP (client), 9094/TCP (interbroker)",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name+" - container ports", func(t *testing.T) {
|
||||
result := describeContainerPorts(tc.ports)
|
||||
if result != tc.expectedContainer {
|
||||
t.Errorf("describeContainerPorts: expected %q, got %q", tc.expectedContainer, result)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run(tc.name+" - host ports", func(t *testing.T) {
|
||||
result := describeContainerHostPorts(tc.ports)
|
||||
if result != tc.expectedHost {
|
||||
t.Errorf("describeContainerHostPorts: expected %q, got %q", tc.expectedHost, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDescribeContainers(t *testing.T) {
|
||||
trueVal := true
|
||||
testCases := []struct {
|
||||
|
@ -3964,7 +4031,7 @@ Parameters: param1=value1,param2=value2
|
|||
Events: <none>
|
||||
`
|
||||
|
||||
f := fake.NewSimpleClientset(&storagev1beta1.VolumeAttributesClass{
|
||||
f := fake.NewSimpleClientset(&storagev1.VolumeAttributesClass{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "foo",
|
||||
ResourceVersion: "4",
|
||||
|
@ -6171,9 +6238,7 @@ func TestDescribeServiceAccount(t *testing.T) {
|
|||
Namespace: foo
|
||||
Labels: <none>
|
||||
Annotations: <none>
|
||||
Image pull secrets: test-local-ref (not found)
|
||||
Mountable secrets: test-objectref (not found)
|
||||
Tokens: <none>
|
||||
Image pull secrets: test-local-ref
|
||||
Events: <none>` + "\n"
|
||||
if out != expectedOut {
|
||||
t.Errorf("expected : %q\n but got output:\n %q", expectedOut, out)
|
||||
|
|
|
@ -74,6 +74,9 @@ type Helper struct {
|
|||
// won't drain otherwise
|
||||
SkipWaitForDeleteTimeoutSeconds int
|
||||
|
||||
// EvictErrorRetryDelay is used to control the retry delay after a pod eviction error
|
||||
EvictErrorRetryDelay time.Duration
|
||||
|
||||
// AdditionalFilters are applied sequentially after base drain filters to
|
||||
// exclude pods using custom logic. Any filter that returns PodDeleteStatus
|
||||
// with Delete == false will immediately stop execution of further filters.
|
||||
|
@ -278,37 +281,27 @@ func (d *Helper) evictPods(pods []corev1.Pod, evictionGroupVersion schema.GroupV
|
|||
defer cancel()
|
||||
for _, pod := range pods {
|
||||
go func(pod corev1.Pod, returnCh chan error) {
|
||||
refreshPod := false
|
||||
activePod := pod
|
||||
for {
|
||||
switch d.DryRunStrategy {
|
||||
case cmdutil.DryRunServer:
|
||||
fmt.Fprintf(d.Out, "evicting pod %s/%s (server dry run)\n", pod.Namespace, pod.Name)
|
||||
//nolint:errcheck
|
||||
fmt.Fprintf(d.Out, "evicting pod %s/%s (server dry run)\n", activePod.Namespace, activePod.Name)
|
||||
default:
|
||||
if d.OnPodDeletionOrEvictionStarted != nil {
|
||||
d.OnPodDeletionOrEvictionStarted(&pod, true)
|
||||
d.OnPodDeletionOrEvictionStarted(&activePod, true)
|
||||
}
|
||||
fmt.Fprintf(d.Out, "evicting pod %s/%s\n", pod.Namespace, pod.Name)
|
||||
//nolint:errcheck
|
||||
fmt.Fprintf(d.Out, "evicting pod %s/%s\n", activePod.Namespace, activePod.Name)
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
// return here or we'll leak a goroutine.
|
||||
returnCh <- fmt.Errorf("error when evicting pods/%q -n %q: global timeout reached: %v", pod.Name, pod.Namespace, globalTimeout)
|
||||
returnCh <- fmt.Errorf("error when evicting pods/%q -n %q: global timeout reached: %v", activePod.Name, activePod.Namespace, globalTimeout)
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
// Create a temporary pod so we don't mutate the pod in the loop.
|
||||
activePod := pod
|
||||
if refreshPod {
|
||||
freshPod, err := getPodFn(pod.Namespace, pod.Name)
|
||||
// We ignore errors and let eviction sort it out with
|
||||
// the original pod.
|
||||
if err == nil {
|
||||
activePod = *freshPod
|
||||
}
|
||||
refreshPod = false
|
||||
}
|
||||
|
||||
err := d.EvictPod(activePod, evictionGroupVersion)
|
||||
if err == nil {
|
||||
break
|
||||
|
@ -316,8 +309,9 @@ func (d *Helper) evictPods(pods []corev1.Pod, evictionGroupVersion schema.GroupV
|
|||
returnCh <- nil
|
||||
return
|
||||
} else if apierrors.IsTooManyRequests(err) {
|
||||
fmt.Fprintf(d.ErrOut, "error when evicting pods/%q -n %q (will retry after 5s): %v\n", activePod.Name, activePod.Namespace, err)
|
||||
time.Sleep(5 * time.Second)
|
||||
//nolint:errcheck
|
||||
fmt.Fprintf(d.ErrOut, "error when evicting pods/%q -n %q (will retry after %v): %v\n", activePod.Name, activePod.Namespace, d.EvictErrorRetryDelay, err)
|
||||
time.Sleep(d.EvictErrorRetryDelay)
|
||||
} else if !activePod.ObjectMeta.DeletionTimestamp.IsZero() && apierrors.IsForbidden(err) && apierrors.HasStatusCause(err, corev1.NamespaceTerminatingCause) {
|
||||
// an eviction request in a deleting namespace will throw a forbidden error,
|
||||
// if the pod is already marked deleted, we can ignore this error, an eviction
|
||||
|
@ -326,12 +320,19 @@ func (d *Helper) evictPods(pods []corev1.Pod, evictionGroupVersion schema.GroupV
|
|||
} else if apierrors.IsForbidden(err) && apierrors.HasStatusCause(err, corev1.NamespaceTerminatingCause) {
|
||||
// an eviction request in a deleting namespace will throw a forbidden error,
|
||||
// if the pod is not marked deleted, we retry until it is.
|
||||
fmt.Fprintf(d.ErrOut, "error when evicting pod %q from terminating namespace %q (will retry after 5s): %v\n", activePod.Name, activePod.Namespace, err)
|
||||
time.Sleep(5 * time.Second)
|
||||
//nolint:errcheck
|
||||
fmt.Fprintf(d.ErrOut, "error when evicting pod %q from terminating namespace %q (will retry after %v): %v\n", activePod.Name, activePod.Namespace, d.EvictErrorRetryDelay, err)
|
||||
time.Sleep(d.EvictErrorRetryDelay)
|
||||
} else {
|
||||
returnCh <- fmt.Errorf("error when evicting pods/%q -n %q: %v", activePod.Name, activePod.Namespace, err)
|
||||
return
|
||||
}
|
||||
|
||||
freshPod, err := getPodFn(activePod.Namespace, activePod.Name)
|
||||
// we ignore errors and let eviction sort it out with the original pod.
|
||||
if err == nil {
|
||||
activePod = *freshPod
|
||||
}
|
||||
}
|
||||
if d.DryRunStrategy == cmdutil.DryRunServer {
|
||||
returnCh <- nil
|
||||
|
@ -339,7 +340,7 @@ func (d *Helper) evictPods(pods []corev1.Pod, evictionGroupVersion schema.GroupV
|
|||
}
|
||||
params := waitForDeleteParams{
|
||||
ctx: ctx,
|
||||
pods: []corev1.Pod{pod},
|
||||
pods: []corev1.Pod{activePod},
|
||||
interval: 1 * time.Second,
|
||||
timeout: time.Duration(math.MaxInt64),
|
||||
usingEviction: true,
|
||||
|
|
|
@ -529,3 +529,110 @@ func TestFilterPods(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvictDuringNamespaceTerminating(t *testing.T) {
|
||||
testPodUID := types.UID("test-uid")
|
||||
testPodName := "test-pod"
|
||||
testNamespace := "default"
|
||||
|
||||
retryDelay := 5 * time.Millisecond
|
||||
globalTimeout := 2 * retryDelay
|
||||
|
||||
tests := []struct {
|
||||
description string
|
||||
refresh bool
|
||||
err error
|
||||
}{
|
||||
{
|
||||
description: "Pod refreshed after NamespaceTerminating error",
|
||||
refresh: true,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
description: "Pod not refreshed after NamespaceTerminating error",
|
||||
refresh: false,
|
||||
err: fmt.Errorf("error when evicting pods/%q -n %q: global timeout reached: %v", testPodName, testNamespace, globalTimeout),
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.description, func(t *testing.T) {
|
||||
var retry bool
|
||||
|
||||
initialPod := &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: testPodName,
|
||||
Namespace: testNamespace,
|
||||
UID: testPodUID,
|
||||
},
|
||||
}
|
||||
|
||||
// pod with DeletionTimestamp, indicating deletion in progress
|
||||
deletedPod := &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: testPodName,
|
||||
Namespace: testNamespace,
|
||||
UID: testPodUID,
|
||||
DeletionTimestamp: &metav1.Time{Time: time.Now()},
|
||||
},
|
||||
}
|
||||
|
||||
evictPods := []corev1.Pod{*initialPod}
|
||||
|
||||
k := fake.NewClientset(initialPod)
|
||||
addEvictionSupport(t, k, "v1")
|
||||
|
||||
// mock eviction to return NamespaceTerminating error
|
||||
k.PrependReactor("create", "pods", func(action ktest.Action) (bool, runtime.Object, error) {
|
||||
if action.GetSubresource() != "eviction" {
|
||||
return false, nil, nil
|
||||
}
|
||||
|
||||
err := apierrors.NewForbidden(
|
||||
schema.GroupResource{Resource: "pods"},
|
||||
testPodName,
|
||||
errors.New("namespace is terminating"),
|
||||
)
|
||||
|
||||
err.ErrStatus.Details.Causes = append(err.ErrStatus.Details.Causes, metav1.StatusCause{
|
||||
Type: corev1.NamespaceTerminatingCause,
|
||||
})
|
||||
|
||||
return true, nil, err
|
||||
})
|
||||
|
||||
k.PrependReactor("get", "pods", func(action ktest.Action) (bool, runtime.Object, error) {
|
||||
if !test.refresh {
|
||||
// for non-refresh test, always return the initial pod
|
||||
return true, initialPod, nil
|
||||
}
|
||||
|
||||
if retry {
|
||||
// second call, pod is deleted
|
||||
return true, nil, apierrors.NewNotFound(schema.GroupResource{Resource: "pods"}, testPodName)
|
||||
}
|
||||
|
||||
// first call, pod is being deleted
|
||||
retry = true
|
||||
|
||||
return true, deletedPod, nil
|
||||
})
|
||||
|
||||
h := &Helper{
|
||||
Client: k,
|
||||
DisableEviction: false,
|
||||
Out: os.Stdout,
|
||||
ErrOut: os.Stderr,
|
||||
Timeout: globalTimeout,
|
||||
EvictErrorRetryDelay: retryDelay,
|
||||
}
|
||||
|
||||
err := h.DeleteOrEvictPods(evictPods)
|
||||
if test.err == nil && err != nil {
|
||||
t.Errorf("expected no error, got: %v", err)
|
||||
} else if test.err != nil && (err == nil || err.Error() != test.err.Error()) {
|
||||
t.Errorf("%s: unexpected eviction; actual %v; expected %v", test.description, err, test.err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/utils/ptr"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
|
@ -318,16 +319,18 @@ func compGetResourceList(restClientGetter genericclioptions.RESTClientGetter, cm
|
|||
streams := genericiooptions.IOStreams{In: os.Stdin, Out: buf, ErrOut: io.Discard}
|
||||
o := apiresources.NewAPIResourceOptions(streams)
|
||||
|
||||
o.Complete(restClientGetter, cmd, nil)
|
||||
|
||||
// Get the list of resources
|
||||
o.Output = "name"
|
||||
o.PrintFlags.OutputFormat = ptr.To("name")
|
||||
o.Cached = true
|
||||
o.Verbs = []string{"get"}
|
||||
// TODO:Should set --request-timeout=5s
|
||||
|
||||
if err := o.Complete(restClientGetter, cmd, nil); err != nil {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
// Ignore errors as the output may still be valid
|
||||
o.RunAPIResources()
|
||||
_ = o.RunAPIResources()
|
||||
|
||||
// Resources can be a comma-separated list. The last element is then
|
||||
// the one we should complete. For example if toComplete=="pods,secre"
|
||||
|
|
|
@ -492,6 +492,101 @@ func TestResourceAndPortCompletionFunc(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestResourceTypeAndNameCompletionFuncResourceList(t *testing.T) {
|
||||
// Set up a fake discovery client with some API resources
|
||||
dc := cmdtesting.NewFakeCachedDiscoveryClient()
|
||||
dc.PreferredResources = []*metav1.APIResourceList{
|
||||
{
|
||||
GroupVersion: "v1",
|
||||
APIResources: []metav1.APIResource{
|
||||
{
|
||||
Name: "pods",
|
||||
Namespaced: true,
|
||||
Kind: "Pod",
|
||||
Verbs: []string{"get", "list"},
|
||||
},
|
||||
{
|
||||
Name: "services",
|
||||
Namespaced: true,
|
||||
Kind: "Service",
|
||||
Verbs: []string{"get", "list"},
|
||||
},
|
||||
{
|
||||
Name: "secrets",
|
||||
Namespaced: true,
|
||||
Kind: "Secret",
|
||||
Verbs: []string{"get", "list"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
GroupVersion: "apps/v1",
|
||||
APIResources: []metav1.APIResource{
|
||||
{
|
||||
Name: "deployments",
|
||||
Namespaced: true,
|
||||
Kind: "Deployment",
|
||||
Verbs: []string{"get", "list"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
args []string
|
||||
toComplete string
|
||||
expectedComps []string
|
||||
expectedDirective cobra.ShellCompDirective
|
||||
}{
|
||||
{
|
||||
name: "complete resources starting with 's'",
|
||||
args: []string{},
|
||||
toComplete: "s",
|
||||
expectedComps: []string{"secrets", "services"},
|
||||
expectedDirective: cobra.ShellCompDirectiveNoFileComp,
|
||||
},
|
||||
{
|
||||
name: "complete resources starting with 'p'",
|
||||
args: []string{},
|
||||
toComplete: "p",
|
||||
expectedComps: []string{"pods"},
|
||||
expectedDirective: cobra.ShellCompDirectiveNoFileComp,
|
||||
},
|
||||
{
|
||||
name: "complete resources starting with 'd'",
|
||||
args: []string{},
|
||||
toComplete: "d",
|
||||
expectedComps: []string{"deployments.apps"},
|
||||
expectedDirective: cobra.ShellCompDirectiveNoFileComp,
|
||||
},
|
||||
{
|
||||
name: "complete all resources with empty string",
|
||||
args: []string{},
|
||||
toComplete: "",
|
||||
expectedComps: []string{"deployments.apps", "pods", "secrets", "services"},
|
||||
expectedDirective: cobra.ShellCompDirectiveNoFileComp,
|
||||
},
|
||||
{
|
||||
name: "no matches",
|
||||
args: []string{},
|
||||
toComplete: "xyz",
|
||||
expectedComps: []string{},
|
||||
expectedDirective: cobra.ShellCompDirectiveNoFileComp,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
tf, cmd := prepareCompletionTest()
|
||||
tf.WithDiscoveryClient(dc)
|
||||
compFunc := ResourceTypeAndNameCompletionFunc(tf)
|
||||
comps, directive := compFunc(cmd, tc.args, tc.toComplete)
|
||||
checkCompletion(t, comps, tc.expectedComps, directive, tc.expectedDirective)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func setMockFactory(config api.Config) {
|
||||
clientConfig := clientcmd.NewDefaultClientConfig(config, nil)
|
||||
testFactory := cmdtesting.NewTestFactory().WithClientConfig(clientConfig)
|
||||
|
|
|
@ -21,12 +21,28 @@ import (
|
|||
|
||||
"github.com/moby/term"
|
||||
"k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/client-go/tools/remotecommand"
|
||||
)
|
||||
|
||||
// TerminalSize represents the width and height of a terminal.
|
||||
// It is the same as staging/src/k8s.io/client-go/tools/remotecommand.TerminalSize.
|
||||
// Copied to decouple the packages. Terminal-related package should not depend on API client and vice versa.
|
||||
type TerminalSize struct {
|
||||
Width uint16
|
||||
Height uint16
|
||||
}
|
||||
|
||||
// TerminalSizeQueue is capable of returning terminal resize events as they occur.
|
||||
// It is the same as staging/src/k8s.io/client-go/tools/remotecommand.TerminalSizeQueue.
|
||||
// Copied to decouple the packages. Terminal-related package should not depend on API client and vice versa.
|
||||
type TerminalSizeQueue interface {
|
||||
// Next returns the new terminal size after the terminal has been resized. It returns nil when
|
||||
// monitoring has been stopped.
|
||||
Next() *TerminalSize
|
||||
}
|
||||
|
||||
// GetSize returns the current size of the user's terminal. If it isn't a terminal,
|
||||
// nil is returned.
|
||||
func (t TTY) GetSize() *remotecommand.TerminalSize {
|
||||
func (t TTY) GetSize() *TerminalSize {
|
||||
outFd, isTerminal := term.GetFdInfo(t.Out)
|
||||
if !isTerminal {
|
||||
return nil
|
||||
|
@ -35,19 +51,19 @@ func (t TTY) GetSize() *remotecommand.TerminalSize {
|
|||
}
|
||||
|
||||
// GetSize returns the current size of the terminal associated with fd.
|
||||
func GetSize(fd uintptr) *remotecommand.TerminalSize {
|
||||
func GetSize(fd uintptr) *TerminalSize {
|
||||
winsize, err := term.GetWinsize(fd)
|
||||
if err != nil {
|
||||
runtime.HandleError(fmt.Errorf("unable to get terminal size: %v", err))
|
||||
return nil
|
||||
}
|
||||
|
||||
return &remotecommand.TerminalSize{Width: winsize.Width, Height: winsize.Height}
|
||||
return &TerminalSize{Width: winsize.Width, Height: winsize.Height}
|
||||
}
|
||||
|
||||
// MonitorSize monitors the terminal's size. It returns a TerminalSizeQueue primed with
|
||||
// initialSizes, or nil if there's no TTY present.
|
||||
func (t *TTY) MonitorSize(initialSizes ...*remotecommand.TerminalSize) remotecommand.TerminalSizeQueue {
|
||||
func (t *TTY) MonitorSize(initialSizes ...*TerminalSize) TerminalSizeQueue {
|
||||
outFd, isTerminal := term.GetFdInfo(t.Out)
|
||||
if !isTerminal {
|
||||
return nil
|
||||
|
@ -57,7 +73,7 @@ func (t *TTY) MonitorSize(initialSizes ...*remotecommand.TerminalSize) remotecom
|
|||
t: *t,
|
||||
// make it buffered so we can send the initial terminal sizes without blocking, prior to starting
|
||||
// the streaming below
|
||||
resizeChan: make(chan remotecommand.TerminalSize, len(initialSizes)),
|
||||
resizeChan: make(chan TerminalSize, len(initialSizes)),
|
||||
stopResizing: make(chan struct{}),
|
||||
}
|
||||
|
||||
|
@ -70,16 +86,16 @@ func (t *TTY) MonitorSize(initialSizes ...*remotecommand.TerminalSize) remotecom
|
|||
type sizeQueue struct {
|
||||
t TTY
|
||||
// resizeChan receives a Size each time the user's terminal is resized.
|
||||
resizeChan chan remotecommand.TerminalSize
|
||||
resizeChan chan TerminalSize
|
||||
stopResizing chan struct{}
|
||||
}
|
||||
|
||||
// make sure sizeQueue implements the resize.TerminalSizeQueue interface
|
||||
var _ remotecommand.TerminalSizeQueue = &sizeQueue{}
|
||||
// make sure sizeQueue implements the TerminalSizeQueue interface
|
||||
var _ TerminalSizeQueue = &sizeQueue{}
|
||||
|
||||
// monitorSize primes resizeChan with initialSizes and then monitors for resize events. With each
|
||||
// new event, it sends the current terminal size to resizeChan.
|
||||
func (s *sizeQueue) monitorSize(outFd uintptr, initialSizes ...*remotecommand.TerminalSize) {
|
||||
func (s *sizeQueue) monitorSize(outFd uintptr, initialSizes ...*TerminalSize) {
|
||||
// send the initial sizes
|
||||
for i := range initialSizes {
|
||||
if initialSizes[i] != nil {
|
||||
|
@ -87,7 +103,7 @@ func (s *sizeQueue) monitorSize(outFd uintptr, initialSizes ...*remotecommand.Te
|
|||
}
|
||||
}
|
||||
|
||||
resizeEvents := make(chan remotecommand.TerminalSize, 1)
|
||||
resizeEvents := make(chan TerminalSize, 1)
|
||||
|
||||
monitorResizeEvents(outFd, resizeEvents, s.stopResizing)
|
||||
|
||||
|
@ -118,7 +134,7 @@ func (s *sizeQueue) monitorSize(outFd uintptr, initialSizes ...*remotecommand.Te
|
|||
|
||||
// Next returns the new terminal size after the terminal has been resized. It returns nil when
|
||||
// monitoring has been stopped.
|
||||
func (s *sizeQueue) Next() *remotecommand.TerminalSize {
|
||||
func (s *sizeQueue) Next() *TerminalSize {
|
||||
size, ok := <-s.resizeChan
|
||||
if !ok {
|
||||
return nil
|
||||
|
|
|
@ -25,13 +25,12 @@ import (
|
|||
|
||||
"golang.org/x/sys/unix"
|
||||
"k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/client-go/tools/remotecommand"
|
||||
)
|
||||
|
||||
// monitorResizeEvents spawns a goroutine that waits for SIGWINCH signals (these indicate the
|
||||
// terminal has resized). After receiving a SIGWINCH, this gets the terminal size and tries to send
|
||||
// it to the resizeEvents channel. The goroutine stops when the stop channel is closed.
|
||||
func monitorResizeEvents(fd uintptr, resizeEvents chan<- remotecommand.TerminalSize, stop chan struct{}) {
|
||||
func monitorResizeEvents(fd uintptr, resizeEvents chan<- TerminalSize, stop chan struct{}) {
|
||||
go func() {
|
||||
defer runtime.HandleCrash()
|
||||
|
||||
|
|
|
@ -20,13 +20,12 @@ import (
|
|||
"time"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/client-go/tools/remotecommand"
|
||||
)
|
||||
|
||||
// monitorResizeEvents spawns a goroutine that periodically gets the terminal size and tries to send
|
||||
// it to the resizeEvents channel if the size has changed. The goroutine stops when the stop channel
|
||||
// is closed.
|
||||
func monitorResizeEvents(fd uintptr, resizeEvents chan<- remotecommand.TerminalSize, stop chan struct{}) {
|
||||
func monitorResizeEvents(fd uintptr, resizeEvents chan<- TerminalSize, stop chan struct{}) {
|
||||
go func() {
|
||||
defer runtime.HandleCrash()
|
||||
|
||||
|
|
|
@ -23,8 +23,6 @@ import (
|
|||
|
||||
wordwrap "github.com/mitchellh/go-wordwrap"
|
||||
"github.com/moby/term"
|
||||
|
||||
"k8s.io/client-go/tools/remotecommand"
|
||||
)
|
||||
|
||||
type wordWrapWriter struct {
|
||||
|
@ -70,7 +68,7 @@ func NewWordWrapWriter(w io.Writer, limit uint) io.Writer {
|
|||
}
|
||||
}
|
||||
|
||||
func getTerminalLimitWidth(terminalSize *remotecommand.TerminalSize) uint {
|
||||
func getTerminalLimitWidth(terminalSize *TerminalSize) uint {
|
||||
var limit uint
|
||||
switch {
|
||||
case terminalSize.Width >= 120:
|
||||
|
|
|
@ -17,7 +17,7 @@ spec:
|
|||
spec:
|
||||
containers:
|
||||
- name: master
|
||||
image: docker.io/library/redis:5.0.5-alpine
|
||||
image: registry.k8s.io/e2e-test-images/redis:5.0.5-alpine
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
|
|
|
@ -17,7 +17,7 @@ spec:
|
|||
spec:
|
||||
containers:
|
||||
- name: master
|
||||
image: docker.io/library/redis:5.0.5-alpine
|
||||
image: registry.k8s.io/e2e-test-images/redis:5.0.5-alpine
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
|
|
|
@ -17,7 +17,7 @@ spec:
|
|||
spec:
|
||||
containers:
|
||||
- name: slave
|
||||
image: docker.io/library/redis:5.0.5-alpine
|
||||
image: registry.k8s.io/e2e-test-images/redis:5.0.5-alpine
|
||||
# We are only implementing the dns option of:
|
||||
# https://github.com/kubernetes/examples/blob/97c7ed0eb6555a4b667d2877f965d392e00abc45/guestbook/redis-slave/run.sh
|
||||
command: [ "redis-server", "--slaveof", "redis-master", "6379" ]
|
||||
|
|
|
@ -24,7 +24,7 @@ spec:
|
|||
spec:
|
||||
containers:
|
||||
- name: slave
|
||||
image: docker.io/library/redis:5.0.5-alpine
|
||||
image: registry.k8s.io/e2e-test-images/redis:5.0.5-alpine
|
||||
# We are only implementing the dns option of:
|
||||
# https://github.com/kubernetes/examples/blob/97c7ed0eb6555a4b667d2877f965d392e00abc45/guestbook/redis-slave/run.sh
|
||||
command: [ "redis-server", "--slaveof", "redis-master", "6379" ]
|
||||
|
|
Loading…
Reference in New Issue