Compare commits
74 Commits
kubernetes
...
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 | |
|
2e87981eff | |
|
02042ef887 | |
|
8ed5bb5f0a | |
|
f47e5c84cc | |
|
76e6818d8d | |
|
f07a946956 | |
|
45a8bb4283 | |
|
0b7534c13c | |
|
e3a465587f | |
|
e470ab29d4 | |
|
02bb9287e7 | |
|
8aac463c8e | |
|
1b3f4fd0f4 | |
|
142b144574 | |
|
7c82ae36f3 | |
|
d97446c293 | |
|
a8605c1ee2 | |
|
1d576b52f0 | |
|
a8c44498b7 | |
|
daa78b3004 | |
|
9892d492ab | |
|
fe571ee1bb | |
|
7e06b5277c | |
|
9c02ed6a6e | |
|
a6fde79de4 | |
|
edad3048e9 | |
|
4b5ec542ad | |
|
cf54a4ea54 | |
|
e33a30ea7c | |
|
6323c5bc57 | |
|
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
|
||||
|
||||

|
||||
|
|
56
go.mod
56
go.mod
|
@ -13,7 +13,7 @@ require (
|
|||
github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f
|
||||
github.com/fatih/camelcase v1.0.0
|
||||
github.com/go-openapi/jsonreference v0.20.2
|
||||
github.com/google/gnostic-models v0.6.9
|
||||
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
|
||||
|
@ -26,41 +26,41 @@ require (
|
|||
github.com/spf13/cobra v1.9.1
|
||||
github.com/spf13/pflag v1.0.6
|
||||
github.com/stretchr/testify v1.10.0
|
||||
golang.org/x/sys v0.31.0
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0
|
||||
k8s.io/api v0.0.0-20250625172518-2872eaf4bc66
|
||||
k8s.io/apimachinery v0.0.0-20250625172235-f3d86859ab8a
|
||||
k8s.io/cli-runtime v0.0.0-20250625180655-70ae17def10c
|
||||
k8s.io/client-go v0.0.0-20250625172909-cf3a9d7f2e18
|
||||
k8s.io/component-base v0.0.0-20250625174137-670840c797fd
|
||||
k8s.io/component-helpers v0.0.0-20250625174306-f77bef070f81
|
||||
go.yaml.in/yaml/v2 v2.4.2
|
||||
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-20250610211856-8b98d1ed966a
|
||||
k8s.io/metrics v0.0.0-20250625180512-64fb019733cb
|
||||
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b
|
||||
k8s.io/metrics v0.0.0-20250830171643-3daf20f585bd
|
||||
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397
|
||||
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3
|
||||
sigs.k8s.io/kustomize/kustomize/v5 v5.6.0
|
||||
sigs.k8s.io/kustomize/kyaml v0.19.0
|
||||
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8
|
||||
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/v4 v4.7.0
|
||||
sigs.k8s.io/yaml v1.4.0
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0
|
||||
sigs.k8s.io/yaml v1.6.0
|
||||
)
|
||||
|
||||
require (
|
||||
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/fxamacker/cbor/v2 v2.8.0 // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.12.2 // 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
|
||||
|
@ -76,19 +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
|
||||
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
|
||||
go.yaml.in/yaml/v3 v3.0.4 // 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
|
||||
)
|
||||
|
|
114
go.sum
114
go.sum
|
@ -17,18 +17,18 @@ 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/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.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
|
||||
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||
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/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=
|
||||
|
@ -43,16 +43,13 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
|||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
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.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw=
|
||||
github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
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/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=
|
||||
|
@ -108,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=
|
||||
|
@ -143,6 +138,10 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
|
|||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
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=
|
||||
|
@ -152,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=
|
||||
|
@ -185,50 +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-20250625172518-2872eaf4bc66 h1:aMd+Jbx+lq4jR0Cq9r8q0sSul6VjNeJZ6uBuMOW7lcs=
|
||||
k8s.io/api v0.0.0-20250625172518-2872eaf4bc66/go.mod h1:1C5ufHfx8/vwdBx/DXW0BQ7SqsdvoyTyd6wy6XlQPJA=
|
||||
k8s.io/apimachinery v0.0.0-20250625172235-f3d86859ab8a h1:iaRq1UKD/uPXfwZlW5WMGlKwRMNkA87SkgAurlTZk78=
|
||||
k8s.io/apimachinery v0.0.0-20250625172235-f3d86859ab8a/go.mod h1:HYUvaFuBNToT/JW+lhc2D9BYgqj6xhI5eORJ7BnvKpM=
|
||||
k8s.io/cli-runtime v0.0.0-20250625180655-70ae17def10c h1:hki2NGhRYKSOV8vdaqmbdttXDfT7J1wMCu0e+HQ4FtQ=
|
||||
k8s.io/cli-runtime v0.0.0-20250625180655-70ae17def10c/go.mod h1:ul9Ww7hyT5DpWQuCZV+hIarGpS2G5cWpBQI6QAx7YrY=
|
||||
k8s.io/client-go v0.0.0-20250625172909-cf3a9d7f2e18 h1:gYfAKTYq8UkW+OsojWUYYuYtXywr8b/GQRGyW58lcG4=
|
||||
k8s.io/client-go v0.0.0-20250625172909-cf3a9d7f2e18/go.mod h1:DHBCV7+oCeGbWAjutB+9QWRKD4TSQ2ZDlyMY3u6LSVU=
|
||||
k8s.io/component-base v0.0.0-20250625174137-670840c797fd h1:DfaOuQ5YTgNT6I+8K//HO3lBQxqYnksXx/d1hDrlN3Q=
|
||||
k8s.io/component-base v0.0.0-20250625174137-670840c797fd/go.mod h1:8XDN50ASDWy5CdtivLOdvBY0JZeDcFhhftTZxa5dABc=
|
||||
k8s.io/component-helpers v0.0.0-20250625174306-f77bef070f81 h1:F05L8dfGsJWjvHDaLDOHrlIIfMc7jTabD3pclBv4VTg=
|
||||
k8s.io/component-helpers v0.0.0-20250625174306-f77bef070f81/go.mod h1:9QoaWJg2L1dh7aarSlOWTvWR2l76tyDJHHcfGmoEO2I=
|
||||
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-20250610211856-8b98d1ed966a h1:ZV3Zr+/7s7aVbjNGICQt+ppKWsF1tehxggNfbM7XnG8=
|
||||
k8s.io/kube-openapi v0.0.0-20250610211856-8b98d1ed966a/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8=
|
||||
k8s.io/metrics v0.0.0-20250625180512-64fb019733cb h1:2vgjcA7qrOHmcETjgqRQJSHQDnMTlYV8FvREYw8LpVA=
|
||||
k8s.io/metrics v0.0.0-20250625180512-64fb019733cb/go.mod h1:JVuoaELbsTAddkXzh4lWpKQN6yBPaGqyQMSkyiblD2I=
|
||||
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-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-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=
|
||||
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=
|
||||
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 v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
|
||||
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.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/v4 v4.7.0 h1:qPeWmscJcXP0snki5IYF79Z8xrl8ETFxgMd7wez1XkI=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.7.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps=
|
||||
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
|
||||
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
|
||||
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()}
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -19,14 +19,16 @@ 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"
|
||||
|
@ -36,6 +38,7 @@ import (
|
|||
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"
|
||||
|
@ -54,10 +57,16 @@ var (
|
|||
|
||||
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
|
||||
|
@ -74,6 +83,8 @@ type AutoscaleOptions struct {
|
|||
Min int32
|
||||
Max int32
|
||||
CPUPercent int32
|
||||
CPU string
|
||||
Memory string
|
||||
|
||||
createAnnotation bool
|
||||
args []string
|
||||
|
@ -109,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,
|
||||
|
@ -129,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)
|
||||
|
@ -189,7 +204,22 @@ 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
|
||||
}
|
||||
|
||||
|
@ -214,16 +244,24 @@ 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)
|
||||
}
|
||||
|
||||
// handles the creation of HorizontalPodAutoscaler objects for both v2 and v1 APIs.
|
||||
// If v2 API fails, try to create and handle HorizontalPodAutoscaler using v1 API
|
||||
hpaV2 := o.createHorizontalPodAutoscalerV2(info.Name, mapping)
|
||||
if err := o.handleHPA(hpaV2); err != nil {
|
||||
klog.V(1).Infof("Encountered an error with the v2 HorizontalPodAutoscaler: %v. "+
|
||||
"Falling back to try the v1 HorizontalPodAutoscaler", 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 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
|
||||
|
@ -241,6 +279,18 @@ func (o *AutoscaleOptions) Run() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
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 {
|
||||
|
@ -288,7 +338,7 @@ func (o *AutoscaleOptions) handleHPA(hpa runtime.Object) error {
|
|||
return printer.PrintObj(actualHPA, o.Out)
|
||||
}
|
||||
|
||||
func (o *AutoscaleOptions) createHorizontalPodAutoscalerV2(refName string, mapping *meta.RESTMapping) *autoscalingv2.HorizontalPodAutoscaler {
|
||||
func (o *AutoscaleOptions) createHorizontalPodAutoscalerV2(refName string, mapping *meta.RESTMapping) (*autoscalingv2.HorizontalPodAutoscaler, error) {
|
||||
name := o.Name
|
||||
if len(name) == 0 {
|
||||
name = refName
|
||||
|
@ -312,22 +362,83 @@ func (o *AutoscaleOptions) createHorizontalPodAutoscalerV2(refName string, mappi
|
|||
scaler.Spec.MinReplicas = &o.Min
|
||||
}
|
||||
|
||||
if o.CPUPercent >= 0 {
|
||||
scaler.Spec.Metrics = []autoscalingv2.MetricSpec{
|
||||
{
|
||||
Type: autoscalingv2.ResourceMetricSourceType,
|
||||
Resource: &autoscalingv2.ResourceMetricSource{
|
||||
Name: corev1.ResourceCPU,
|
||||
Target: autoscalingv2.MetricTarget{
|
||||
Type: autoscalingv2.UtilizationMetricType,
|
||||
AverageUtilization: &o.CPUPercent,
|
||||
},
|
||||
},
|
||||
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)
|
||||
}
|
||||
|
||||
return &scaler
|
||||
// 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 {
|
||||
|
@ -361,3 +472,72 @@ func (o *AutoscaleOptions) createHorizontalPodAutoscalerV1(refName string, mappi
|
|||
|
||||
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
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ import (
|
|||
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/schema"
|
||||
"k8s.io/utils/ptr"
|
||||
|
@ -79,11 +80,113 @@ func TestAutoscaleValidate(t *testing.T) {
|
|||
},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "CPUPercent appears with CPU",
|
||||
options: &AutoscaleOptions{
|
||||
Max: 5,
|
||||
Min: 0,
|
||||
CPU: "800",
|
||||
CPUPercent: 20,
|
||||
},
|
||||
expectedError: fmt.Errorf("--cpu-percent and --cpu are mutually exclusive"),
|
||||
},
|
||||
{
|
||||
name: "CPUPercent default (-1) with CPU",
|
||||
options: &AutoscaleOptions{
|
||||
Max: 5,
|
||||
Min: 0,
|
||||
CPU: "800",
|
||||
CPUPercent: -1,
|
||||
},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "valid CPU percentage",
|
||||
options: &AutoscaleOptions{
|
||||
Max: 5,
|
||||
CPU: "70%",
|
||||
},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "valid CPU numeric without unit",
|
||||
options: &AutoscaleOptions{
|
||||
Max: 5,
|
||||
CPU: "500",
|
||||
},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "valid CPU with unit",
|
||||
options: &AutoscaleOptions{
|
||||
Max: 5,
|
||||
CPU: "500m",
|
||||
},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "invalid CPU value (non-numeric)",
|
||||
options: &AutoscaleOptions{
|
||||
Max: 5,
|
||||
CPU: "abc",
|
||||
},
|
||||
expectedError: fmt.Errorf("invalid resource cpu value: abc"),
|
||||
},
|
||||
{
|
||||
name: "invalid CPU value (malformed unit)",
|
||||
options: &AutoscaleOptions{
|
||||
Max: 5,
|
||||
CPU: "500xyz",
|
||||
},
|
||||
expectedError: fmt.Errorf("invalid resource cpu value: 500xyz"),
|
||||
},
|
||||
{
|
||||
name: "valid memory percentage",
|
||||
options: &AutoscaleOptions{
|
||||
Max: 5,
|
||||
Memory: "60%",
|
||||
},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "valid memory numeric without unit",
|
||||
options: &AutoscaleOptions{
|
||||
Max: 5,
|
||||
Memory: "512",
|
||||
},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "valid memory with unit",
|
||||
options: &AutoscaleOptions{
|
||||
Max: 5,
|
||||
Memory: "512Mi",
|
||||
},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "invalid memory value (non-numeric)",
|
||||
options: &AutoscaleOptions{
|
||||
Max: 5,
|
||||
Memory: "xyz",
|
||||
},
|
||||
expectedError: fmt.Errorf("invalid resource memory value: xyz"),
|
||||
},
|
||||
{
|
||||
name: "invalid memory value (MiB unit)",
|
||||
options: &AutoscaleOptions{
|
||||
Max: 5,
|
||||
Memory: "512MiB",
|
||||
},
|
||||
expectedError: fmt.Errorf("invalid resource memory value: 512MiB"),
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
errorGot := tc.options.Validate()
|
||||
assert.Equal(t, tc.expectedError, errorGot)
|
||||
if errorGot != nil {
|
||||
assert.Equal(t, tc.expectedError.Error(), errorGot.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -98,6 +201,10 @@ type createHorizontalPodAutoscalerTestCase struct {
|
|||
}
|
||||
|
||||
func TestCreateHorizontalPodAutoscalerV2(t *testing.T) {
|
||||
cpu500m := apiresource.MustParse("500m")
|
||||
mem512Mi := apiresource.MustParse("512Mi")
|
||||
cpu2000m := apiresource.MustParse("2000m")
|
||||
mem3Gi := apiresource.MustParse("3Gi")
|
||||
tests := []createHorizontalPodAutoscalerTestCase{
|
||||
{
|
||||
name: "create with all options",
|
||||
|
@ -360,10 +467,396 @@ func TestCreateHorizontalPodAutoscalerV2(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "create with memory(use %) options",
|
||||
options: &AutoscaleOptions{
|
||||
Name: "custom-name",
|
||||
Max: 10,
|
||||
Min: 2,
|
||||
Memory: "50%",
|
||||
},
|
||||
refName: "deployment-1",
|
||||
mapping: &meta.RESTMapping{
|
||||
GroupVersionKind: schema.GroupVersionKind{
|
||||
Group: "apps",
|
||||
Version: "v1",
|
||||
Kind: "Deployment",
|
||||
},
|
||||
},
|
||||
expectedHPAV2: &autoscalingv2.HorizontalPodAutoscaler{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "custom-name",
|
||||
},
|
||||
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
|
||||
ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{
|
||||
APIVersion: "apps/v1",
|
||||
Kind: "Deployment",
|
||||
Name: "deployment-1",
|
||||
},
|
||||
MinReplicas: ptr.To(int32(2)),
|
||||
MaxReplicas: int32(10),
|
||||
Metrics: []autoscalingv2.MetricSpec{
|
||||
// Reference: https://pkg.go.dev/k8s.io/api/autoscaling/v2#MetricSpec
|
||||
{
|
||||
Type: autoscalingv2.ResourceMetricSourceType,
|
||||
Resource: &autoscalingv2.ResourceMetricSource{
|
||||
// Reference: https://pkg.go.dev/k8s.io/api/autoscaling/v2#ResourceMetricSource
|
||||
Name: corev1.ResourceMemory,
|
||||
Target: autoscalingv2.MetricTarget{
|
||||
// Reference: https://pkg.go.dev/k8s.io/api/autoscaling/v2#MetricTarget
|
||||
Type: autoscalingv2.UtilizationMetricType,
|
||||
AverageUtilization: ptr.To(int32(50)),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "create with both cpu(use %) and memory(use %) options",
|
||||
options: &AutoscaleOptions{
|
||||
Name: "custom-name",
|
||||
Max: 10,
|
||||
Min: 2,
|
||||
CPU: "70%",
|
||||
Memory: "50%",
|
||||
},
|
||||
refName: "deployment-1",
|
||||
mapping: &meta.RESTMapping{
|
||||
GroupVersionKind: schema.GroupVersionKind{
|
||||
Group: "apps",
|
||||
Version: "v1",
|
||||
Kind: "Deployment",
|
||||
},
|
||||
},
|
||||
expectedHPAV2: &autoscalingv2.HorizontalPodAutoscaler{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "custom-name",
|
||||
},
|
||||
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
|
||||
ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{
|
||||
APIVersion: "apps/v1",
|
||||
Kind: "Deployment",
|
||||
Name: "deployment-1",
|
||||
},
|
||||
MinReplicas: ptr.To(int32(2)),
|
||||
MaxReplicas: int32(10),
|
||||
Metrics: []autoscalingv2.MetricSpec{
|
||||
// Reference: https://pkg.go.dev/k8s.io/api/autoscaling/v2#MetricSpec
|
||||
{
|
||||
Type: autoscalingv2.ResourceMetricSourceType,
|
||||
Resource: &autoscalingv2.ResourceMetricSource{
|
||||
// Reference: https://pkg.go.dev/k8s.io/api/autoscaling/v2#ResourceMetricSource
|
||||
Name: corev1.ResourceCPU,
|
||||
Target: autoscalingv2.MetricTarget{
|
||||
// Reference: https://pkg.go.dev/k8s.io/api/autoscaling/v2#MetricTarget
|
||||
Type: autoscalingv2.UtilizationMetricType,
|
||||
AverageUtilization: ptr.To(int32(70)),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: autoscalingv2.ResourceMetricSourceType,
|
||||
Resource: &autoscalingv2.ResourceMetricSource{
|
||||
// Reference: https://pkg.go.dev/k8s.io/api/autoscaling/v2#ResourceMetricSource
|
||||
Name: corev1.ResourceMemory,
|
||||
Target: autoscalingv2.MetricTarget{
|
||||
// Reference: https://pkg.go.dev/k8s.io/api/autoscaling/v2#MetricTarget
|
||||
Type: autoscalingv2.UtilizationMetricType,
|
||||
AverageUtilization: ptr.To(int32(50)),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "create with both cpu(use m unit) and memory(use %) options",
|
||||
options: &AutoscaleOptions{
|
||||
Name: "custom-name",
|
||||
Max: 10,
|
||||
Min: 2,
|
||||
CPU: "500m",
|
||||
Memory: "50%",
|
||||
},
|
||||
refName: "deployment-1",
|
||||
mapping: &meta.RESTMapping{
|
||||
GroupVersionKind: schema.GroupVersionKind{
|
||||
Group: "apps",
|
||||
Version: "v1",
|
||||
Kind: "Deployment",
|
||||
},
|
||||
},
|
||||
expectedHPAV2: &autoscalingv2.HorizontalPodAutoscaler{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "custom-name",
|
||||
},
|
||||
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
|
||||
ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{
|
||||
APIVersion: "apps/v1",
|
||||
Kind: "Deployment",
|
||||
Name: "deployment-1",
|
||||
},
|
||||
MinReplicas: ptr.To(int32(2)),
|
||||
MaxReplicas: int32(10),
|
||||
Metrics: []autoscalingv2.MetricSpec{
|
||||
// Reference: https://pkg.go.dev/k8s.io/api/autoscaling/v2#MetricSpec
|
||||
{
|
||||
Type: autoscalingv2.ResourceMetricSourceType,
|
||||
Resource: &autoscalingv2.ResourceMetricSource{
|
||||
// Reference: https://pkg.go.dev/k8s.io/api/autoscaling/v2#ResourceMetricSource
|
||||
Name: corev1.ResourceCPU,
|
||||
Target: autoscalingv2.MetricTarget{
|
||||
// Reference: https://pkg.go.dev/k8s.io/api/autoscaling/v2#MetricTarget
|
||||
Type: autoscalingv2.AverageValueMetricType,
|
||||
AverageValue: &cpu500m,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: autoscalingv2.ResourceMetricSourceType,
|
||||
Resource: &autoscalingv2.ResourceMetricSource{
|
||||
// Reference: https://pkg.go.dev/k8s.io/api/autoscaling/v2#ResourceMetricSource
|
||||
Name: corev1.ResourceMemory,
|
||||
Target: autoscalingv2.MetricTarget{
|
||||
// Reference: https://pkg.go.dev/k8s.io/api/autoscaling/v2#MetricTarget
|
||||
Type: autoscalingv2.UtilizationMetricType,
|
||||
AverageUtilization: ptr.To(int32(50)),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "create with both cpu(no use unit) and memory(use %) options",
|
||||
options: &AutoscaleOptions{
|
||||
Name: "custom-name",
|
||||
Max: 10,
|
||||
Min: 2,
|
||||
CPU: "500",
|
||||
Memory: "50%",
|
||||
},
|
||||
refName: "deployment-1",
|
||||
mapping: &meta.RESTMapping{
|
||||
GroupVersionKind: schema.GroupVersionKind{
|
||||
Group: "apps",
|
||||
Version: "v1",
|
||||
Kind: "Deployment",
|
||||
},
|
||||
},
|
||||
expectedHPAV2: &autoscalingv2.HorizontalPodAutoscaler{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "custom-name",
|
||||
},
|
||||
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
|
||||
ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{
|
||||
APIVersion: "apps/v1",
|
||||
Kind: "Deployment",
|
||||
Name: "deployment-1",
|
||||
},
|
||||
MinReplicas: ptr.To(int32(2)),
|
||||
MaxReplicas: int32(10),
|
||||
Metrics: []autoscalingv2.MetricSpec{
|
||||
// Reference: https://pkg.go.dev/k8s.io/api/autoscaling/v2#MetricSpec
|
||||
{
|
||||
Type: autoscalingv2.ResourceMetricSourceType,
|
||||
Resource: &autoscalingv2.ResourceMetricSource{
|
||||
// Reference: https://pkg.go.dev/k8s.io/api/autoscaling/v2#ResourceMetricSource
|
||||
Name: corev1.ResourceCPU,
|
||||
Target: autoscalingv2.MetricTarget{
|
||||
// Reference: https://pkg.go.dev/k8s.io/api/autoscaling/v2#MetricTarget
|
||||
Type: autoscalingv2.AverageValueMetricType,
|
||||
AverageValue: &cpu500m,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: autoscalingv2.ResourceMetricSourceType,
|
||||
Resource: &autoscalingv2.ResourceMetricSource{
|
||||
// Reference: https://pkg.go.dev/k8s.io/api/autoscaling/v2#ResourceMetricSource
|
||||
Name: corev1.ResourceMemory,
|
||||
Target: autoscalingv2.MetricTarget{
|
||||
// Reference: https://pkg.go.dev/k8s.io/api/autoscaling/v2#MetricTarget
|
||||
Type: autoscalingv2.UtilizationMetricType,
|
||||
AverageUtilization: ptr.To(int32(50)),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "create with memory(no use unit) options",
|
||||
options: &AutoscaleOptions{
|
||||
Name: "custom-name",
|
||||
Max: 10,
|
||||
Min: 2,
|
||||
Memory: "512",
|
||||
},
|
||||
refName: "deployment-1",
|
||||
mapping: &meta.RESTMapping{
|
||||
GroupVersionKind: schema.GroupVersionKind{
|
||||
Group: "apps",
|
||||
Version: "v1",
|
||||
Kind: "Deployment",
|
||||
},
|
||||
},
|
||||
expectedHPAV2: &autoscalingv2.HorizontalPodAutoscaler{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "custom-name",
|
||||
},
|
||||
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
|
||||
ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{
|
||||
APIVersion: "apps/v1",
|
||||
Kind: "Deployment",
|
||||
Name: "deployment-1",
|
||||
},
|
||||
MinReplicas: ptr.To(int32(2)),
|
||||
MaxReplicas: int32(10),
|
||||
Metrics: []autoscalingv2.MetricSpec{
|
||||
{
|
||||
Type: autoscalingv2.ResourceMetricSourceType,
|
||||
Resource: &autoscalingv2.ResourceMetricSource{
|
||||
// Reference: https://pkg.go.dev/k8s.io/api/autoscaling/v2#ResourceMetricSource
|
||||
Name: corev1.ResourceMemory,
|
||||
Target: autoscalingv2.MetricTarget{
|
||||
// Reference: https://pkg.go.dev/k8s.io/api/autoscaling/v2#MetricTarget
|
||||
Type: autoscalingv2.AverageValueMetricType,
|
||||
AverageValue: &mem512Mi,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "create with cpu(no use unit) and memory(no use unit) options",
|
||||
options: &AutoscaleOptions{
|
||||
Name: "custom-name",
|
||||
Max: 10,
|
||||
Min: 2,
|
||||
CPU: "500",
|
||||
Memory: "512",
|
||||
},
|
||||
refName: "deployment-1",
|
||||
mapping: &meta.RESTMapping{
|
||||
GroupVersionKind: schema.GroupVersionKind{
|
||||
Group: "apps",
|
||||
Version: "v1",
|
||||
Kind: "Deployment",
|
||||
},
|
||||
},
|
||||
expectedHPAV2: &autoscalingv2.HorizontalPodAutoscaler{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "custom-name",
|
||||
},
|
||||
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
|
||||
ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{
|
||||
APIVersion: "apps/v1",
|
||||
Kind: "Deployment",
|
||||
Name: "deployment-1",
|
||||
},
|
||||
MinReplicas: ptr.To(int32(2)),
|
||||
MaxReplicas: int32(10),
|
||||
Metrics: []autoscalingv2.MetricSpec{
|
||||
// Reference: https://pkg.go.dev/k8s.io/api/autoscaling/v2#MetricSpec
|
||||
{
|
||||
Type: autoscalingv2.ResourceMetricSourceType,
|
||||
Resource: &autoscalingv2.ResourceMetricSource{
|
||||
// Reference: https://pkg.go.dev/k8s.io/api/autoscaling/v2#ResourceMetricSource
|
||||
Name: corev1.ResourceCPU,
|
||||
Target: autoscalingv2.MetricTarget{
|
||||
// Reference: https://pkg.go.dev/k8s.io/api/autoscaling/v2#MetricTarget
|
||||
Type: autoscalingv2.AverageValueMetricType,
|
||||
AverageValue: &cpu500m,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: autoscalingv2.ResourceMetricSourceType,
|
||||
Resource: &autoscalingv2.ResourceMetricSource{
|
||||
// Reference: https://pkg.go.dev/k8s.io/api/autoscaling/v2#ResourceMetricSource
|
||||
Name: corev1.ResourceMemory,
|
||||
Target: autoscalingv2.MetricTarget{
|
||||
// Reference: https://pkg.go.dev/k8s.io/api/autoscaling/v2#MetricTarget
|
||||
Type: autoscalingv2.AverageValueMetricType,
|
||||
AverageValue: &mem512Mi,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "create with both cpu(use m unit) and memory(use Gi unit) options",
|
||||
options: &AutoscaleOptions{
|
||||
Name: "custom-name",
|
||||
Max: 10,
|
||||
Min: 2,
|
||||
CPU: "2000m",
|
||||
Memory: "3Gi",
|
||||
},
|
||||
refName: "deployment-1",
|
||||
mapping: &meta.RESTMapping{
|
||||
GroupVersionKind: schema.GroupVersionKind{
|
||||
Group: "apps",
|
||||
Version: "v1",
|
||||
Kind: "Deployment",
|
||||
},
|
||||
},
|
||||
expectedHPAV2: &autoscalingv2.HorizontalPodAutoscaler{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "custom-name",
|
||||
},
|
||||
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
|
||||
ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{
|
||||
APIVersion: "apps/v1",
|
||||
Kind: "Deployment",
|
||||
Name: "deployment-1",
|
||||
},
|
||||
MinReplicas: ptr.To(int32(2)),
|
||||
MaxReplicas: int32(10),
|
||||
Metrics: []autoscalingv2.MetricSpec{
|
||||
// Reference: https://pkg.go.dev/k8s.io/api/autoscaling/v2#MetricSpec
|
||||
{
|
||||
Type: autoscalingv2.ResourceMetricSourceType,
|
||||
Resource: &autoscalingv2.ResourceMetricSource{
|
||||
// Reference: https://pkg.go.dev/k8s.io/api/autoscaling/v2#ResourceMetricSource
|
||||
Name: corev1.ResourceCPU,
|
||||
Target: autoscalingv2.MetricTarget{
|
||||
// Reference: https://pkg.go.dev/k8s.io/api/autoscaling/v2#MetricTarget
|
||||
Type: autoscalingv2.AverageValueMetricType,
|
||||
AverageValue: &cpu2000m,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: autoscalingv2.ResourceMetricSourceType,
|
||||
Resource: &autoscalingv2.ResourceMetricSource{
|
||||
// Reference: https://pkg.go.dev/k8s.io/api/autoscaling/v2#ResourceMetricSource
|
||||
Name: corev1.ResourceMemory,
|
||||
Target: autoscalingv2.MetricTarget{
|
||||
// Reference: https://pkg.go.dev/k8s.io/api/autoscaling/v2#MetricTarget
|
||||
Type: autoscalingv2.AverageValueMetricType,
|
||||
AverageValue: &mem3Gi,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
hpa := tc.options.createHorizontalPodAutoscalerV2(tc.refName, tc.mapping)
|
||||
hpa, _ := tc.options.createHorizontalPodAutoscalerV2(tc.refName, tc.mapping)
|
||||
assert.Equal(t, tc.expectedHPAV2, hpa)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -76,7 +76,7 @@ var (
|
|||
* 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
|
||||
Note: When a non-root user is configured for the entire target Pod, some capabilities granted
|
||||
by debug profile may not work.
|
||||
`))
|
||||
|
||||
|
@ -727,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{
|
||||
|
|
|
@ -1468,6 +1468,9 @@ func TestGenerateNodeDebugPod(t *testing.T) {
|
|||
expected: &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "node-debugger-node-XXX-1",
|
||||
Labels: map[string]string{
|
||||
"app.kubernetes.io/managed-by": "kubectl-debug",
|
||||
},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
|
@ -1522,6 +1525,9 @@ func TestGenerateNodeDebugPod(t *testing.T) {
|
|||
expected: &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "node-debugger-node-XXX-1",
|
||||
Labels: map[string]string{
|
||||
"app.kubernetes.io/managed-by": "kubectl-debug",
|
||||
},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
|
@ -1578,6 +1584,9 @@ func TestGenerateNodeDebugPod(t *testing.T) {
|
|||
expected: &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "node-debugger-node-XXX-1",
|
||||
Labels: map[string]string{
|
||||
"app.kubernetes.io/managed-by": "kubectl-debug",
|
||||
},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
|
@ -1631,6 +1640,9 @@ func TestGenerateNodeDebugPod(t *testing.T) {
|
|||
expected: &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "node-debugger-node-XXX-1",
|
||||
Labels: map[string]string{
|
||||
"app.kubernetes.io/managed-by": "kubectl-debug",
|
||||
},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
|
@ -1683,6 +1695,9 @@ func TestGenerateNodeDebugPod(t *testing.T) {
|
|||
expected: &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "node-debugger-node-XXX-1",
|
||||
Labels: map[string]string{
|
||||
"app.kubernetes.io/managed-by": "kubectl-debug",
|
||||
},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
|
@ -1723,6 +1738,9 @@ func TestGenerateNodeDebugPod(t *testing.T) {
|
|||
expected: &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "node-debugger-node-XXX-1",
|
||||
Labels: map[string]string{
|
||||
"app.kubernetes.io/managed-by": "kubectl-debug",
|
||||
},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
|
@ -1771,6 +1789,9 @@ func TestGenerateNodeDebugPod(t *testing.T) {
|
|||
expected: &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "node-debugger-node-XXX-1",
|
||||
Labels: map[string]string{
|
||||
"app.kubernetes.io/managed-by": "kubectl-debug",
|
||||
},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
|
@ -1863,6 +1884,9 @@ func TestGenerateNodeDebugPodCustomProfile(t *testing.T) {
|
|||
expected: &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "node-debugger-node-XXX-1",
|
||||
Labels: map[string]string{
|
||||
"app.kubernetes.io/managed-by": "kubectl-debug",
|
||||
},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
|
@ -1916,6 +1940,9 @@ func TestGenerateNodeDebugPodCustomProfile(t *testing.T) {
|
|||
expected: &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "node-debugger-node-XXX-1",
|
||||
Labels: map[string]string{
|
||||
"app.kubernetes.io/managed-by": "kubectl-debug",
|
||||
},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
|
@ -1972,6 +1999,11 @@ func TestGenerateNodeDebugPodCustomProfile(t *testing.T) {
|
|||
},
|
||||
},
|
||||
expected: &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{
|
||||
"app.kubernetes.io/managed-by": "kubectl-debug",
|
||||
},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
|
@ -2035,6 +2067,11 @@ func TestGenerateNodeDebugPodCustomProfile(t *testing.T) {
|
|||
},
|
||||
},
|
||||
expected: &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{
|
||||
"app.kubernetes.io/managed-by": "kubectl-debug",
|
||||
},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -30,6 +30,7 @@ import (
|
|||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
yaml "go.yaml.in/yaml/v2"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/cli-runtime/pkg/genericiooptions"
|
||||
|
@ -39,7 +40,6 @@ import (
|
|||
"k8s.io/kubectl/pkg/cmd/create"
|
||||
cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
yaml "sigs.k8s.io/yaml/goyaml.v2"
|
||||
)
|
||||
|
||||
type EditTestCase struct {
|
||||
|
|
|
@ -25,7 +25,7 @@ import (
|
|||
"os"
|
||||
"strings"
|
||||
|
||||
yaml "sigs.k8s.io/yaml/goyaml.v2"
|
||||
yaml "go.yaml.in/yaml/v2"
|
||||
)
|
||||
|
||||
type EditTestCase struct {
|
||||
|
|
|
@ -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."))
|
||||
|
|
|
@ -182,7 +182,7 @@ 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)
|
||||
|
@ -623,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) {
|
||||
|
|
|
@ -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)
|
||||
|
@ -2126,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"}})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -39,9 +39,31 @@ var (
|
|||
topLong = templates.LongDesc(i18n.T(`
|
||||
Display resource (CPU/memory) usage.
|
||||
|
||||
The top command allows you to see the resource consumption for nodes or pods.
|
||||
This command provides a view of recent resource consumption for nodes and pods.
|
||||
It fetches metrics from the Metrics Server, which aggregates this data from the
|
||||
kubelet on each node. The Metrics Server must be installed and running in the
|
||||
cluster for this command to work.
|
||||
|
||||
This command requires Metrics Server to be correctly configured and working on the server. `))
|
||||
The metrics shown are specifically optimized for Kubernetes autoscaling
|
||||
decisions, such as those made by the Horizontal Pod Autoscaler (HPA) and
|
||||
Vertical Pod Autoscaler (VPA). Because of this, the values may not match those
|
||||
from standard OS tools like 'top', as the metrics are designed to provide a
|
||||
stable signal for autoscalers rather than for pinpoint accuracy.
|
||||
|
||||
When to use this command:
|
||||
|
||||
* For on-the-fly spot-checks of resource usage (e.g. identify which pods
|
||||
are consuming the most resources at a glance, or get a quick sense of the load
|
||||
on your nodes)
|
||||
* Understand current resource consumption patterns
|
||||
* Validate the behavior of your HPA or VPA configurations by seeing the metrics
|
||||
they use for scaling decisions.
|
||||
|
||||
It is not intended to be a replacement for full-featured monitoring solutions.
|
||||
Its primary design goal is to provide a low-overhead signal for autoscalers,
|
||||
not to be a perfectly accurate monitoring tool. For high-accuracy reporting,
|
||||
historical analysis, dashboarding, or alerting, you should use a dedicated
|
||||
monitoring solution.`))
|
||||
)
|
||||
|
||||
func NewCmdTop(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command {
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
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 util
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/cli-runtime/pkg/resource"
|
||||
)
|
||||
|
||||
// cachingVerifier wraps the given Verifier to cache return values forever.
|
||||
type cachingVerifier struct {
|
||||
cache map[schema.GroupVersionKind]error
|
||||
mu sync.RWMutex
|
||||
next resource.Verifier
|
||||
}
|
||||
|
||||
// newCachingVerifier creates a new cache using the given underlying verifier.
|
||||
func newCachingVerifier(next resource.Verifier) *cachingVerifier {
|
||||
return &cachingVerifier{
|
||||
cache: make(map[schema.GroupVersionKind]error),
|
||||
next: next,
|
||||
}
|
||||
}
|
||||
|
||||
// HasSupport implements resource.Verifier. It cached return values from the underlying verifier forever.
|
||||
func (cv *cachingVerifier) HasSupport(gvk schema.GroupVersionKind) error {
|
||||
// Try to get the cached value.
|
||||
cv.mu.RLock()
|
||||
err, ok := cv.cache[gvk]
|
||||
cv.mu.RUnlock()
|
||||
if ok {
|
||||
return err
|
||||
}
|
||||
|
||||
// Cache miss. Get the actual result.
|
||||
err = cv.next.HasSupport(gvk)
|
||||
|
||||
// Update the cache.
|
||||
cv.mu.Lock()
|
||||
cv.cache[gvk] = err
|
||||
cv.mu.Unlock()
|
||||
return err
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
/*
|
||||
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 util
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
type verifierCall struct {
|
||||
GVK schema.GroupVersionKind
|
||||
Err error
|
||||
}
|
||||
|
||||
type mockVerifier struct {
|
||||
t *testing.T
|
||||
expectedCalls map[schema.GroupVersionKind]error
|
||||
}
|
||||
|
||||
func (m *mockVerifier) CheckExpectations() {
|
||||
if len(m.expectedCalls) != 0 {
|
||||
m.t.Errorf("Expected calls remaining: %v", m.expectedCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mockVerifier) HasSupport(gvk schema.GroupVersionKind) error {
|
||||
returnErr, ok := m.expectedCalls[gvk]
|
||||
if !ok {
|
||||
m.t.Errorf("Unexpected HasSupport call with GVK=%v", gvk)
|
||||
}
|
||||
delete(m.expectedCalls, gvk)
|
||||
return returnErr
|
||||
}
|
||||
|
||||
func TestCachingVerifier(t *testing.T) {
|
||||
gvk1 := schema.GroupVersionKind{
|
||||
Group: "group",
|
||||
Version: "version",
|
||||
Kind: "kind",
|
||||
}
|
||||
gvk2 := schema.GroupVersionKind{
|
||||
Group: "group2",
|
||||
Version: "version2",
|
||||
Kind: "kind2",
|
||||
}
|
||||
|
||||
err1 := errors.New("some error")
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
calls []verifierCall
|
||||
expectedUnderlyingCalls map[schema.GroupVersionKind]error
|
||||
}{
|
||||
{
|
||||
name: "return value is cached",
|
||||
calls: []verifierCall{
|
||||
{GVK: gvk1, Err: nil},
|
||||
{GVK: gvk1, Err: nil},
|
||||
{GVK: gvk1, Err: nil},
|
||||
{GVK: gvk2, Err: err1},
|
||||
{GVK: gvk2, Err: err1},
|
||||
{GVK: gvk2, Err: err1},
|
||||
},
|
||||
expectedUnderlyingCalls: map[schema.GroupVersionKind]error{
|
||||
gvk1: nil,
|
||||
gvk2: err1,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
m := &mockVerifier{
|
||||
t: t,
|
||||
expectedCalls: tc.expectedUnderlyingCalls,
|
||||
}
|
||||
verifier := newCachingVerifier(m)
|
||||
|
||||
for _, call := range tc.calls {
|
||||
err := verifier.HasSupport(call.GVK)
|
||||
if !errors.Is(err, call.Err) {
|
||||
t.Errorf("Expected error: %v, got: %v", call.Err, err)
|
||||
}
|
||||
}
|
||||
|
||||
m.CheckExpectations()
|
||||
})
|
||||
}
|
||||
}
|
|
@ -172,7 +172,7 @@ func (f *factoryImpl) Validator(validationDirective string) (validation.Schema,
|
|||
// the discovery client.
|
||||
oapiV3Client := cached.NewClient(discoveryClient.OpenAPIV3())
|
||||
queryParam := resource.QueryParamFieldValidation
|
||||
primary := resource.NewQueryParamVerifierV3(dynamicClient, oapiV3Client, queryParam)
|
||||
primary := newCachingVerifier(resource.NewQueryParamVerifierV3(dynamicClient, oapiV3Client, queryParam))
|
||||
secondary := resource.NewQueryParamVerifier(dynamicClient, f.openAPIGetter(), queryParam)
|
||||
fallback := resource.NewFallbackQueryParamVerifier(primary, secondary)
|
||||
return validation.NewParamVerifyingSchema(schema, fallback, string(validationDirective)), nil
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -171,6 +171,12 @@ func hasLocalStorage(pod corev1.Pod) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
func isControllerRefDaemonSet(workloadRef *metav1.OwnerReference) bool {
|
||||
// find if workloadRef is daemonSet
|
||||
daemonSetAPIVersion, daemonSetKind := appsv1.SchemeGroupVersion.WithKind("DaemonSet").ToAPIVersionAndKind()
|
||||
return workloadRef.Kind == daemonSetKind && workloadRef.APIVersion == daemonSetAPIVersion
|
||||
}
|
||||
|
||||
func (d *Helper) daemonSetFilter(pod corev1.Pod) PodDeleteStatus {
|
||||
// Note that we return false in cases where the pod is DaemonSet managed,
|
||||
// regardless of flags.
|
||||
|
@ -179,7 +185,7 @@ func (d *Helper) daemonSetFilter(pod corev1.Pod) PodDeleteStatus {
|
|||
// management resource - including DaemonSet - is not found).
|
||||
// Such pods will be deleted if --force is used.
|
||||
controllerRef := metav1.GetControllerOf(&pod)
|
||||
if controllerRef == nil || controllerRef.Kind != appsv1.SchemeGroupVersion.WithKind("DaemonSet").Kind {
|
||||
if controllerRef == nil || !isControllerRefDaemonSet(controllerRef) {
|
||||
return MakePodDeleteStatusOkay()
|
||||
}
|
||||
// Any finished pod can be removed.
|
||||
|
|
|
@ -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