Compare commits

..

15 Commits

Author SHA1 Message Date
Kubernetes Publisher aa4f98b43a Update dependencies to v0.28.9 tag 2024-04-17 01:41:14 +00:00
Kubernetes Publisher 1e4d35255e Merge pull request #124179 from MadhavJivrajani/bump-x-net-2023-45288-128
[CVE-2023-45288][1.28] Bump x/net to v0.23.0

Kubernetes-commit: 86b2c8c37de0f2b8b358b4691d2aa03bd3eda7b3
2024-04-04 17:37:21 +00:00
Madhav Jivrajani 82233ac85e [CVE-2023-45288] .*: bump x/net to v0.23.0
Co-authored-by: Davanum Srinivas <davanum@gmail.com>
Signed-off-by: Madhav Jivrajani <madhav.jiv@gmail.com>

Kubernetes-commit: af92f0441687c94b95b3a1c86f8af4f165aa7a61
2024-04-04 14:13:20 +05:30
Kubernetes Publisher b4fed13fd6 sync: update go.mod 2024-03-21 02:13:50 +00:00
Kubernetes Publisher 893e24c109 Merge pull request #123764 from liggitt/proto-1.28
[1.28][CVE-2024-24786] Bump github.com/golang/protobuf v1.5.4, google.golang.org/protobuf v1.33.0

Kubernetes-commit: 25d9edca2cbfb75c2ee84ea8be01b14d50d7ead4
2024-03-07 23:03:19 +00:00
Jordan Liggitt 5b10259d5d [CVE-2024-24786] Bump github.com/golang/protobuf v1.5.4, google.golang.org/protobuf v1.33.0
Kubernetes-commit: 7889bca8b327399bf390cc3d31dd084072c2adf0
2024-03-06 10:47:48 -05:00
Kubernetes Publisher f697a79960 Merge pull request #122428 from MadhavJivrajani/tools-bump-128
[1.28][go1.22] .*: bump golang.org/x/tools to v0.16.1

Kubernetes-commit: 7d018be572572d17d8e51528c2c9324c219f884b
2024-01-10 20:20:15 +00:00
Madhav Jivrajani 20cd469289 .*: bump golang.org/x/tools to v0.16.1
Bumping tools to include the fix for a nil pointer
deref error in go/types. See golang/go#64812
for more details.

This fix is needed for when we bump to go1.22.

Signed-off-by: Madhav Jivrajani <madhav.jiv@gmail.com>

Kubernetes-commit: ad5cf74325652795717d80940e0e04ed5b7c047d
2023-12-21 11:08:40 +05:30
Kubernetes Publisher b8aa229f6a Merge pull request #121545 from dims/automated-cherry-pick-of-#121364-upstream-release-1.28
Automated cherry pick of #121364: bump golang.org/grpc to v1.56.3

Kubernetes-commit: 197e7579adb1bf180617bd3becc2aa4dcceb5291
2023-11-01 15:13:04 +00:00
Jonathan Gonzalez V 92d6c8880f bump golang.org/grpc to v1.56.3
Bumping golang.org/grpc in light of CVE-2023-44487.

Signed-off-by: Jonathan Gonzalez V <jonathan.abdiel@gmail.com>

Kubernetes-commit: 93b91ceea8609012bb6291a4c1f65db3dab4eeb9
2023-10-19 14:58:49 -03:00
Kubernetes Publisher aea32ae8c8 Merge pull request #121128 from MadhavJivrajani/bump-x-net-128
[1.28][CVE-2023-39325] .: bump golang.org/x/net to v0.17.0

Kubernetes-commit: f86a84670e550f31e04df95bd32b0cf035629a0d
2023-10-12 14:30:12 +00:00
Madhav Jivrajani 88c3a5db97 .: bump golang.org/x/net to v0.17.0
Bumping golang.org/x/net in light of CVE-2023-39325 and CVE-2023-44487.

Signed-off-by: Madhav Jivrajani <madhav.jiv@gmail.com>

Kubernetes-commit: 1cd2ac99542ad0b9f82e3c0177ed3c3e18465136
2023-10-11 03:44:03 +05:30
Kubernetes Publisher 6fb6697c77 Merge pull request #120329 from liggitt/automated-cherry-pick-of-#120327-upstream-release-1.28
Automated cherry pick of #120327: Revert to json-patch 4.12.0

Kubernetes-commit: 797b3cf45ec77becceb7d6ae4deb6f6b293fdbf5
2023-09-04 13:35:04 +00:00
Jordan Liggitt 7de4ee011f Revert to json-patch 4.12.0
Kubernetes-commit: 8c7c4f3fc4dcabb1ab2c004b42ff91ebf2e78ede
2023-08-31 19:01:37 -04:00
Kubernetes Publisher fab02cc3a5 Merge remote-tracking branch 'origin/master' into release-1.28
Kubernetes-commit: 6d5bbea0ba66f1cf5a84834eeaeeb9579798b790
2023-08-07 22:56:17 +00:00
245 changed files with 3136 additions and 32765 deletions

View File

@ -10,7 +10,6 @@
# DO NOT REPORT SECURITY VULNERABILITIES DIRECTLY TO THESE NAMES, FOLLOW THE # DO NOT REPORT SECURITY VULNERABILITIES DIRECTLY TO THESE NAMES, FOLLOW THE
# INSTRUCTIONS AT https://kubernetes.io/security/ # INSTRUCTIONS AT https://kubernetes.io/security/
ardaguclu pwittrock
eddiezane seans3
mpuckett159
soltysh soltysh

2
doc.go
View File

@ -14,4 +14,4 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package kubectl package kubectl // import "k8s.io/kubectl"

View File

@ -37,7 +37,7 @@ Look for:
### Test triage ### Test triage
Monitor [test grid](https://testgrid.k8s.io/sig-cli-master) Monitor [test grid](https://k8s-testgrid.appspot.com/sig-cli-master)
and make sure the tests are passing. and make sure the tests are passing.
If any tests are failing, debug them and send a fix. Ask for help if you get stuck. If any tests are failing, debug them and send a fix. Ask for help if you get stuck.

121
go.mod
View File

@ -2,97 +2,94 @@
module k8s.io/kubectl module k8s.io/kubectl
go 1.24.0 go 1.20
godebug default=go1.24
require ( require (
github.com/MakeNowJust/heredoc v1.0.0 github.com/MakeNowJust/heredoc v1.0.0
github.com/chai2010/gettext-go v1.0.2 github.com/chai2010/gettext-go v1.0.2
github.com/distribution/reference v0.6.0 github.com/daviddengcn/go-colortext v1.0.0
github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f github.com/docker/distribution v2.8.2+incompatible
github.com/evanphx/json-patch v4.12.0+incompatible
github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d
github.com/fatih/camelcase v1.0.0 github.com/fatih/camelcase v1.0.0
github.com/fvbommel/sortorder v1.1.0
github.com/go-openapi/jsonreference v0.20.2 github.com/go-openapi/jsonreference v0.20.2
github.com/google/gnostic-models v0.7.0 github.com/google/gnostic-models v0.6.8
github.com/google/go-cmp v0.7.0 github.com/google/go-cmp v0.5.9
github.com/jonboulle/clockwork v0.5.0 github.com/jonboulle/clockwork v0.2.2
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de
github.com/lithammer/dedent v1.1.0 github.com/lithammer/dedent v1.1.0
github.com/mitchellh/go-wordwrap v1.0.1 github.com/mitchellh/go-wordwrap v1.0.1
github.com/moby/term v0.5.0 github.com/moby/term v0.0.0-20221205130635-1aeaba878587
github.com/onsi/ginkgo/v2 v2.21.0 github.com/onsi/ginkgo/v2 v2.9.4
github.com/onsi/gomega v1.35.1 github.com/onsi/gomega v1.27.6
github.com/pkg/errors v0.9.1
github.com/russross/blackfriday/v2 v2.1.0 github.com/russross/blackfriday/v2 v2.1.0
github.com/spf13/cobra v1.9.1 github.com/spf13/cobra v1.7.0
github.com/spf13/pflag v1.0.6 github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.10.0 github.com/stretchr/testify v1.8.2
go.yaml.in/yaml/v2 v2.4.2 golang.org/x/sys v0.18.0
golang.org/x/sys v0.31.0 gopkg.in/yaml.v2 v2.4.0
gopkg.in/evanphx/json-patch.v4 v4.12.0 k8s.io/api v0.28.9
k8s.io/api v0.0.0-20250718010531-33ab3a26f4b3 k8s.io/apimachinery v0.28.9
k8s.io/apimachinery v0.0.0-20250717210244-b92abb2d8139 k8s.io/cli-runtime v0.28.9
k8s.io/cli-runtime v0.0.0-20250717174531-64776d0a280f k8s.io/client-go v0.28.9
k8s.io/client-go v0.0.0-20250718010928-be36413bbca7 k8s.io/component-base v0.28.9
k8s.io/component-base v0.0.0-20250717172125-4e07767df717 k8s.io/component-helpers v0.28.9
k8s.io/component-helpers v0.0.0-20250717172249-5095859f5100 k8s.io/klog/v2 v2.100.1
k8s.io/klog/v2 v2.130.1 k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b k8s.io/metrics v0.28.9
k8s.io/metrics v0.0.0-20250717174355-244095fcc1c1 k8s.io/utils v0.0.0-20230406110748-d93618cff8a2
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 sigs.k8s.io/kustomize/kustomize/v5 v5.0.4-0.20230601165947-6ce0bf390ce3
sigs.k8s.io/kustomize/kustomize/v5 v5.6.0 sigs.k8s.io/kustomize/kyaml v0.14.3-0.20230601165947-6ce0bf390ce3
sigs.k8s.io/kustomize/kyaml v0.19.0 sigs.k8s.io/structured-merge-diff/v4 v4.2.3
sigs.k8s.io/randfill v1.0.0 sigs.k8s.io/yaml v1.3.0
sigs.k8s.io/structured-merge-diff/v6 v6.3.0
sigs.k8s.io/yaml v1.5.0
) )
require ( require (
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/emicklei/go-restful/v3 v3.12.2 // indirect github.com/emicklei/go-restful/v3 v3.9.0 // indirect
github.com/fxamacker/cbor/v2 v2.8.0 // indirect
github.com/go-errors/errors v1.4.2 // 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.2.4 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/swag v0.23.0 // indirect github.com/go-openapi/swag v0.22.3 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/gogo/protobuf v1.3.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/btree v1.1.3 // indirect github.com/golang/protobuf v1.5.4 // indirect
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db // indirect github.com/google/btree v1.0.1 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.3.0 // indirect
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect github.com/imdario/mergo v0.3.6 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/josharian/intern v1.0.0 // indirect github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/mailru/easyjson v0.7.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect
github.com/moby/spdystream v0.5.0 // indirect github.com/moby/spdystream v0.2.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/peterbourgon/diskv v2.0.1+incompatible // 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/pmezard/go-difflib v1.0.0 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/xlab/treeprint v1.2.0 // indirect github.com/xlab/treeprint v1.2.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect go.starlark.net v0.0.0-20230525235612-a134d8f9ddca // indirect
golang.org/x/net v0.38.0 // indirect golang.org/x/net v0.23.0 // indirect
golang.org/x/oauth2 v0.27.0 // indirect golang.org/x/oauth2 v0.8.0 // indirect
golang.org/x/sync v0.12.0 // indirect golang.org/x/sync v0.5.0 // indirect
golang.org/x/term v0.30.0 // indirect golang.org/x/term v0.18.0 // indirect
golang.org/x/text v0.23.0 // indirect golang.org/x/text v0.14.0 // indirect
golang.org/x/time v0.9.0 // indirect golang.org/x/time v0.3.0 // indirect
golang.org/x/tools v0.26.0 // indirect golang.org/x/tools v0.16.1 // indirect
google.golang.org/protobuf v1.36.5 // indirect google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v3 v3.0.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.13.5-0.20230601165947-6ce0bf390ce3 // indirect
) )
replace k8s.io/code-generator => k8s.io/code-generator v0.0.0-20250718051115-9eb96548a40e

315
go.sum
View File

@ -1,67 +1,103 @@
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
github.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk= github.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk=
github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/daviddengcn/go-colortext v1.0.0 h1:ANqDyC0ys6qCSvuEK7l3g5RaehL/Xck9EX8ATG8oKsE=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/daviddengcn/go-colortext v1.0.0/go.mod h1:zDqEI5NVUop5QPpVJUxE9UO10hRnmkD5G4Pmri9+m4c=
github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8=
github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4= github.com/emicklei/go-restful/v3 v3.9.0 h1:XwGDlfxEnQZzuopoqxwSEllNcCOM9DhhFyhFIIGKwxE=
github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc= github.com/emicklei/go-restful/v3 v3.9.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84=
github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d h1:105gxyaGwCFad8crR9dcMQWvV9Hvulu6hwUh4tWPJnM=
github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4=
github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8= 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/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc=
github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU= github.com/fvbommel/sortorder v1.1.0 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQmYw=
github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0=
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= 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-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.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE=
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g=
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golangplus/bytes v0.0.0-20160111154220-45c989fe5450/go.mod h1:Bk6SMAONeMXrxql8uvOKuAZSu8aM5RUGv+1C6IJaEho=
github.com/golangplus/bytes v1.0.0/go.mod h1:AdRaCFwmc/00ZzELMWb01soso6W1R/++O1XL80yAn+A=
github.com/golangplus/fmt v1.0.0/go.mod h1:zpM0OfbMCjPtd2qkTD/jX2MgiFCqklhSUFyDW44gVQE=
github.com/golangplus/testing v1.0.0 h1:+ZeeiKZENNOMkTTELoSySazi+XaEhVO0mb+eanrSEUQ=
github.com/golangplus/testing v1.0.0/go.mod h1:ZDreixUV3YzhoVraIDyOzHrr76p6NUh6k/pPg/Q3gYA=
github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I=
github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 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/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec=
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= 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/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.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM=
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28=
github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I= github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ=
github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60= github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
@ -70,7 +106,6 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@ -83,26 +118,25 @@ github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU= github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8=
github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c=
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/moby/term v0.0.0-20221205130635-1aeaba878587 h1:HfkjXDfhgVaN5rmueG8cL8KKeFNecRCXFhaJ2qZ5SKA=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/moby/term v0.0.0-20221205130635-1aeaba878587/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0=
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= github.com/onsi/ginkgo/v2 v2.9.4 h1:xR7vG4IXt5RWx6FfIjyAtsoMAtnc3C/rFXBBd2AjZwE=
github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= github.com/onsi/ginkgo/v2 v2.9.4/go.mod h1:gCQYp2Q+kSoIj7ykSVb9nskRSsR6PUj4AiLywzIhbKM=
github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/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 h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI=
@ -111,126 +145,159 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ=
github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.starlark.net v0.0.0-20230525235612-a134d8f9ddca h1:VdD38733bfYv5tUZwEIskMM93VanwNIi5bIKnDrJdEY=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.starlark.net v0.0.0-20230525235612-a134d8f9ddca/go.mod h1:jxU+3+j+71eXOW14274+SmmuW82qJzl6iZSeqEtTGds=
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-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-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-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-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.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8=
golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 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-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.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.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA=
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-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-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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 h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
k8s.io/api v0.0.0-20250718010531-33ab3a26f4b3 h1:UnuyCQyBmdFlYypApF2w6Ld0R0kAt8b+0Lt9dYAr23I= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
k8s.io/api v0.0.0-20250718010531-33ab3a26f4b3/go.mod h1:K8dwhtttsRR0RHeSRF8XQ77gfMgyAj3q78/TkxEXhoc= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
k8s.io/apimachinery v0.0.0-20250717210244-b92abb2d8139 h1:jWBClrBPuk+GEA9pJzMa9IvxncSBbw7fmvey15nVm0w= k8s.io/api v0.28.9 h1:E7VEXXCAlSrp+08zq4zgd+ko6Ttu0Mw+XoXlIkDTVW0=
k8s.io/apimachinery v0.0.0-20250717210244-b92abb2d8139/go.mod h1:v1p1Jsze3IHLy5gU17yVqR2qLO7jgYeX6mw3HZy2AEU= k8s.io/api v0.28.9/go.mod h1:AnCsDYf3SHjfa8mPG5LGYf+iF4mie+3peLQR51MMCgw=
k8s.io/cli-runtime v0.0.0-20250717174531-64776d0a280f h1:E/GB1lzzKbz3HPJ6Zu1bJYrey6oDAIAA+RMEozCpPpU= k8s.io/apimachinery v0.28.9 h1:aXz4Zxsw+Pk4KhBerAtKRxNN1uSMWKfciL/iOdBfXvA=
k8s.io/cli-runtime v0.0.0-20250717174531-64776d0a280f/go.mod h1:SybB6wdHGt8FXxaHyNQqsUAhWcZKIDPurWPB5mfFLD0= k8s.io/apimachinery v0.28.9/go.mod h1:zUG757HaKs6Dc3iGtKjzIpBfqTM4yiRsEe3/E7NX15o=
k8s.io/client-go v0.0.0-20250718010928-be36413bbca7 h1:LNOJkn+3JlAEzdZzYheQM97gq6kKQfkrBN0GikI5nbc= k8s.io/cli-runtime v0.28.9 h1:TfEV/UgCiXewliUHOHsUMZ1bfENhqcqKkA/hqQ/HwvQ=
k8s.io/client-go v0.0.0-20250718010928-be36413bbca7/go.mod h1:a14VvgYhux7oUSE9mWdzBuFKDZSGtperboMjQ1JtVgc= k8s.io/cli-runtime v0.28.9/go.mod h1:PgxW97xCDbtWgsuo2nahMc2/MxcSDgscdwm8XZ7973A=
k8s.io/component-base v0.0.0-20250717172125-4e07767df717 h1:07oqkM0FzuGUw/bJw2rJubzccG7ShpGcTJ7SBDGp5Fc= k8s.io/client-go v0.28.9 h1:mmMvejwc/KDjMLmDpyaxkWNzlWRCJ6ht7Qsbsnwn39Y=
k8s.io/component-base v0.0.0-20250717172125-4e07767df717/go.mod h1:/ehREU84M2OxVgU8WfxuUIi4/c5XsT6rIsEGQfhgxEQ= k8s.io/client-go v0.28.9/go.mod h1:GFDy3rUNId++WGrr0hRaBrs+y1eZz5JtVZODEalhRMo=
k8s.io/component-helpers v0.0.0-20250717172249-5095859f5100 h1:XEHmjwZgMNRuVgpqaRH/RR+n4BU0evfitU0RpWGPMUM= k8s.io/component-base v0.28.9 h1:ySM2PR8Z/xaUSG1Akd3yM6dqUezTltI7S5aV41MMuuc=
k8s.io/component-helpers v0.0.0-20250717172249-5095859f5100/go.mod h1:yxuY+YMknW7H9Bj7B29INyMOacJBa6oEG7gi7IKUzEQ= k8s.io/component-base v0.28.9/go.mod h1:QtWzscEhCKRfHV24/S+11BwWjVxhC6fd3RYoEgZcWFU=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/component-helpers v0.28.9 h1:knX9F2nRoxF4wplgXO4C5tE4/k7HGszK3177Tm4+CUc=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/component-helpers v0.28.9/go.mod h1:TdAkLbywEDE2CB5h8LbM/W03T3k8wvqAaoPcEZrr6Z4=
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA= k8s.io/klog/v2 v2.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg=
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts= k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
k8s.io/metrics v0.0.0-20250717174355-244095fcc1c1 h1:V4I6U/hfhZYYmDotL7ukG0nua1luMQSox5QtveZaSv0= k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 h1:LyMgNKD2P8Wn1iAwQU5OhxCKlKJy0sHc+PcDwFB24dQ=
k8s.io/metrics v0.0.0-20250717174355-244095fcc1c1/go.mod h1:TkHVkU+vMKy7qppbMybraSCK8Y+LLpoqk/6Jl+M8EoU= k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9/go.mod h1:wZK2AVp1uHCp4VamDVgBP2COHZjqD1T68Rf0CM3YjSM=
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y= k8s.io/metrics v0.28.9 h1:3TAJhF1GzYK89bE1RLqDinTXAlCnI8UgciwfpKHzKfg=
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= k8s.io/metrics v0.28.9/go.mod h1:7Hn16jtdxc2Q6Vm73QK7nF7HiLJvomLgN7lEQs8SONs=
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 h1:qY1Ad8PODbnymg2pRbkyMT/ylpTrCM8P2RJ0yroCyIk=
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= k8s.io/utils v0.0.0-20230406110748-d93618cff8a2/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
sigs.k8s.io/kustomize/api v0.19.0 h1:F+2HB2mU1MSiR9Hp1NEgoU2q9ItNOaBJl0I4Dlus5SQ= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
sigs.k8s.io/kustomize/api v0.19.0/go.mod h1:/BbwnivGVcBh1r+8m3tH1VNxJmHSk1PzP5fkP6lbL1o= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
sigs.k8s.io/kustomize/kustomize/v5 v5.6.0 h1:MWtRRDWCwQEeW2rnJTqJMuV6Agy56P53SkbVoJpN7wA= sigs.k8s.io/kustomize/api v0.13.5-0.20230601165947-6ce0bf390ce3 h1:XX3Ajgzov2RKUdc5jW3t5jwY7Bo7dcRm+tFxT+NfgY0=
sigs.k8s.io/kustomize/kustomize/v5 v5.6.0/go.mod h1:XuuZiQF7WdcvZzEYyNww9A0p3LazCKeJmCjeycN8e1I= sigs.k8s.io/kustomize/api v0.13.5-0.20230601165947-6ce0bf390ce3/go.mod h1:9n16EZKMhXBNSiUC5kSdFQJkdH3zbxS/JoO619G1VAY=
sigs.k8s.io/kustomize/kyaml v0.19.0 h1:RFge5qsO1uHhwJsu3ipV7RNolC7Uozc0jUBC/61XSlA= sigs.k8s.io/kustomize/kustomize/v5 v5.0.4-0.20230601165947-6ce0bf390ce3 h1:vq2TtoDcQomhy7OxXLUOzSbHMuMYq0Bjn93cDtJEdKw=
sigs.k8s.io/kustomize/kyaml v0.19.0/go.mod h1:FeKD5jEOH+FbZPpqUghBP8mrLjJ3+zD3/rf9NNu1cwY= sigs.k8s.io/kustomize/kustomize/v5 v5.0.4-0.20230601165947-6ce0bf390ce3/go.mod h1:/d88dHCvoy7d0AKFT0yytezSGZKjsZBVs9YTkBHSGFk=
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/kustomize/kyaml v0.14.3-0.20230601165947-6ce0bf390ce3 h1:W6cLQc5pnqM7vh3b7HvGNfXrJ/xL6BDMS0v1V/HHg5U=
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= sigs.k8s.io/kustomize/kyaml v0.14.3-0.20230601165947-6ce0bf390ce3/go.mod h1:JWP1Fj0VWGHyw3YUPjXSQnRnrwezrZSrApfX5S0nIag=
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE=
sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E=
sigs.k8s.io/yaml v1.5.0 h1:M10b2U7aEUY6hRtU870n2VTPgR5RZiL/I6Lcc2F4NUQ= sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
sigs.k8s.io/yaml v1.5.0/go.mod h1:wZs27Rbxoai4C0f8/9urLZtZtF3avA3gKvGyPdDqTO4= sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=

View File

@ -21,8 +21,8 @@ import (
"fmt" "fmt"
"io" "io"
jsonpatch "github.com/evanphx/json-patch"
"github.com/spf13/cobra" "github.com/spf13/cobra"
jsonpatch "gopkg.in/evanphx/json-patch.v4"
"k8s.io/klog/v2" "k8s.io/klog/v2"
"k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/api/meta"
@ -32,8 +32,6 @@ import (
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/json" "k8s.io/apimachinery/pkg/util/json"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/genericiooptions"
"k8s.io/cli-runtime/pkg/printers" "k8s.io/cli-runtime/pkg/printers"
@ -229,7 +227,7 @@ func (flags *AnnotateFlags) ToOptions(f cmdutil.Factory, cmd *cobra.Command, arg
} }
options.namespace, options.enforceNamespace, err = f.ToRawKubeConfigLoader().Namespace() options.namespace, options.enforceNamespace, err = f.ToRawKubeConfigLoader().Namespace()
if err != nil && !(options.local && clientcmd.IsEmptyConfig(err)) { if err != nil {
return nil, err return nil, err
} }
options.builder = f.NewBuilder() options.builder = f.NewBuilder()

View File

@ -123,11 +123,11 @@ func NewCmdAPIResources(restClientGetter genericclioptions.RESTClientGetter, ioS
// Validate checks to the APIResourceOptions to see if there is sufficient information run the command // Validate checks to the APIResourceOptions to see if there is sufficient information run the command
func (o *APIResourceOptions) Validate() error { func (o *APIResourceOptions) Validate() error {
supportedOutputTypes := sets.New[string]("", "wide", "name") supportedOutputTypes := sets.NewString("", "wide", "name")
if !supportedOutputTypes.Has(o.Output) { if !supportedOutputTypes.Has(o.Output) {
return fmt.Errorf("--output %v is not available", o.Output) return fmt.Errorf("--output %v is not available", o.Output)
} }
supportedSortTypes := sets.New[string]("", "name", "kind") supportedSortTypes := sets.NewString("", "name", "kind")
if len(o.SortBy) > 0 { if len(o.SortBy) > 0 {
if !supportedSortTypes.Has(o.SortBy) { if !supportedSortTypes.Has(o.SortBy) {
return fmt.Errorf("--sort-by accepts only name or kind") return fmt.Errorf("--sort-by accepts only name or kind")
@ -193,11 +193,11 @@ func (o *APIResourceOptions) RunAPIResources() error {
continue continue
} }
// filter to resources that support the specified verbs // filter to resources that support the specified verbs
if len(o.Verbs) > 0 && !sets.New[string](resource.Verbs...).HasAll(o.Verbs...) { if len(o.Verbs) > 0 && !sets.NewString(resource.Verbs...).HasAll(o.Verbs...) {
continue continue
} }
// filter to resources that belong to the specified categories // filter to resources that belong to the specified categories
if len(o.Categories) > 0 && !sets.New[string](resource.Categories...).HasAll(o.Categories...) { if len(o.Categories) > 0 && !sets.NewString(resource.Categories...).HasAll(o.Categories...) {
continue continue
} }
resources = append(resources, groupResource{ resources = append(resources, groupResource{

View File

@ -23,7 +23,7 @@ import (
"net/http" "net/http"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"sigs.k8s.io/structured-merge-diff/v6/fieldpath" "sigs.k8s.io/structured-merge-diff/v4/fieldpath"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/errors"
@ -39,17 +39,17 @@ import (
"k8s.io/cli-runtime/pkg/printers" "k8s.io/cli-runtime/pkg/printers"
"k8s.io/cli-runtime/pkg/resource" "k8s.io/cli-runtime/pkg/resource"
"k8s.io/client-go/dynamic" "k8s.io/client-go/dynamic"
"k8s.io/client-go/openapi3"
"k8s.io/client-go/util/csaupgrade" "k8s.io/client-go/util/csaupgrade"
"k8s.io/component-base/version" "k8s.io/component-base/version"
"k8s.io/klog/v2" "k8s.io/klog/v2"
cmddelete "k8s.io/kubectl/pkg/cmd/delete" "k8s.io/kubectl/pkg/cmd/delete"
cmdutil "k8s.io/kubectl/pkg/cmd/util" cmdutil "k8s.io/kubectl/pkg/cmd/util"
"k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/scheme"
"k8s.io/kubectl/pkg/util" "k8s.io/kubectl/pkg/util"
"k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/i18n"
"k8s.io/kubectl/pkg/util/openapi" "k8s.io/kubectl/pkg/util/openapi"
"k8s.io/kubectl/pkg/util/prune" "k8s.io/kubectl/pkg/util/prune"
"k8s.io/kubectl/pkg/util/slice"
"k8s.io/kubectl/pkg/util/templates" "k8s.io/kubectl/pkg/util/templates"
"k8s.io/kubectl/pkg/validation" "k8s.io/kubectl/pkg/validation"
) )
@ -61,7 +61,7 @@ type ApplyFlags struct {
RecordFlags *genericclioptions.RecordFlags RecordFlags *genericclioptions.RecordFlags
PrintFlags *genericclioptions.PrintFlags PrintFlags *genericclioptions.PrintFlags
DeleteFlags *cmddelete.DeleteFlags DeleteFlags *delete.DeleteFlags
FieldManager string FieldManager string
Selector string Selector string
@ -71,8 +71,9 @@ type ApplyFlags struct {
All bool All bool
Overwrite bool Overwrite bool
OpenAPIPatch bool OpenAPIPatch bool
Subresource string
// DEPRECATED: Use PruneAllowlist instead
PruneWhitelist []string // TODO: Remove this in kubectl 1.28 or later
PruneAllowlist []string PruneAllowlist []string
genericiooptions.IOStreams genericiooptions.IOStreams
@ -85,7 +86,7 @@ type ApplyOptions struct {
PrintFlags *genericclioptions.PrintFlags PrintFlags *genericclioptions.PrintFlags
ToPrinter func(string) (printers.ResourcePrinter, error) ToPrinter func(string) (printers.ResourcePrinter, error)
DeleteOptions *cmddelete.DeleteOptions DeleteOptions *delete.DeleteOptions
ServerSideApply bool ServerSideApply bool
ForceConflicts bool ForceConflicts bool
@ -98,15 +99,13 @@ type ApplyOptions struct {
All bool All bool
Overwrite bool Overwrite bool
OpenAPIPatch bool OpenAPIPatch bool
Subresource string
ValidationDirective string ValidationDirective string
Validator validation.Schema Validator validation.Schema
Builder *resource.Builder Builder *resource.Builder
Mapper meta.RESTMapper Mapper meta.RESTMapper
DynamicClient dynamic.Interface DynamicClient dynamic.Interface
OpenAPIGetter openapi.OpenAPIResourcesGetter OpenAPISchema openapi.Resources
OpenAPIV3Root openapi3.Root
Namespace string Namespace string
EnforceNamespace bool EnforceNamespace bool
@ -184,7 +183,7 @@ var ApplySetToolVersion = version.Get().GitVersion
func NewApplyFlags(streams genericiooptions.IOStreams) *ApplyFlags { func NewApplyFlags(streams genericiooptions.IOStreams) *ApplyFlags {
return &ApplyFlags{ return &ApplyFlags{
RecordFlags: genericclioptions.NewRecordFlags(), RecordFlags: genericclioptions.NewRecordFlags(),
DeleteFlags: cmddelete.NewDeleteFlags("The files that contain the configurations to apply."), DeleteFlags: delete.NewDeleteFlags("The files that contain the configurations to apply."),
PrintFlags: genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme), PrintFlags: genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme),
Overwrite: true, Overwrite: true,
@ -234,10 +233,10 @@ func (flags *ApplyFlags) AddFlags(cmd *cobra.Command) {
cmdutil.AddServerSideApplyFlags(cmd) cmdutil.AddServerSideApplyFlags(cmd)
cmdutil.AddFieldManagerFlagVar(cmd, &flags.FieldManager, FieldManagerClientSideApply) cmdutil.AddFieldManagerFlagVar(cmd, &flags.FieldManager, FieldManagerClientSideApply)
cmdutil.AddLabelSelectorFlagVar(cmd, &flags.Selector) cmdutil.AddLabelSelectorFlagVar(cmd, &flags.Selector)
cmdutil.AddPruningFlags(cmd, &flags.Prune, &flags.PruneAllowlist, &flags.All, &flags.ApplySetRef) cmdutil.AddPruningFlags(cmd, &flags.Prune, &flags.PruneAllowlist, &flags.PruneWhitelist, &flags.All, &flags.ApplySetRef)
cmd.Flags().BoolVar(&flags.Overwrite, "overwrite", flags.Overwrite, "Automatically resolve conflicts between the modified and live configuration by using values from the modified configuration") cmd.Flags().BoolVar(&flags.Overwrite, "overwrite", flags.Overwrite, "Automatically resolve conflicts between the modified and live configuration by using values from the modified configuration")
cmd.Flags().BoolVar(&flags.OpenAPIPatch, "openapi-patch", flags.OpenAPIPatch, "If true, use openapi to calculate diff when the openapi presents and the resource can be found in the openapi spec. Otherwise, fall back to use baked-in types.") cmd.Flags().BoolVar(&flags.OpenAPIPatch, "openapi-patch", flags.OpenAPIPatch, "If true, use openapi to calculate diff when the openapi presents and the resource can be found in the openapi spec. Otherwise, fall back to use baked-in types.")
cmdutil.AddSubresourceFlags(cmd, &flags.Subresource, "If specified, apply will operate on the subresource of the requested object. Only allowed when using --server-side.")
} }
// ToOptions converts from CLI inputs to runtime inputs // ToOptions converts from CLI inputs to runtime inputs
@ -283,15 +282,7 @@ func (flags *ApplyFlags) ToOptions(f cmdutil.Factory, cmd *cobra.Command, baseNa
return nil, err return nil, err
} }
var openAPIV3Root openapi3.Root openAPISchema, _ := f.OpenAPISchema()
if !cmdutil.OpenAPIV3Patch.IsDisabled() {
openAPIV3Client, err := f.OpenAPIV3Client()
if err == nil {
openAPIV3Root = openapi3.NewRoot(openAPIV3Client)
} else {
klog.V(4).Infof("warning: OpenAPI V3 Patch is enabled but is unable to be loaded. Will fall back to OpenAPI V2")
}
}
validationDirective, err := cmdutil.GetValidationDirective(cmd) validationDirective, err := cmdutil.GetValidationDirective(cmd)
if err != nil { if err != nil {
@ -334,7 +325,8 @@ func (flags *ApplyFlags) ToOptions(f cmdutil.Factory, cmd *cobra.Command, baseNa
applySet = NewApplySet(parent, tooling, mapper, restClient) applySet = NewApplySet(parent, tooling, mapper, restClient)
} }
if flags.Prune { if flags.Prune {
flags.PruneResources, err = prune.ParseResources(mapper, flags.PruneAllowlist) pruneAllowlist := slice.ToSet(flags.PruneAllowlist, flags.PruneWhitelist)
flags.PruneResources, err = prune.ParseResources(mapper, pruneAllowlist)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -359,7 +351,6 @@ func (flags *ApplyFlags) ToOptions(f cmdutil.Factory, cmd *cobra.Command, baseNa
All: flags.All, All: flags.All,
Overwrite: flags.Overwrite, Overwrite: flags.Overwrite,
OpenAPIPatch: flags.OpenAPIPatch, OpenAPIPatch: flags.OpenAPIPatch,
Subresource: flags.Subresource,
Recorder: recorder, Recorder: recorder,
Namespace: namespace, Namespace: namespace,
@ -369,8 +360,7 @@ func (flags *ApplyFlags) ToOptions(f cmdutil.Factory, cmd *cobra.Command, baseNa
Builder: builder, Builder: builder,
Mapper: mapper, Mapper: mapper,
DynamicClient: dynamicClient, DynamicClient: dynamicClient,
OpenAPIGetter: f, OpenAPISchema: openAPISchema,
OpenAPIV3Root: openAPIV3Root,
IOStreams: flags.IOStreams, IOStreams: flags.IOStreams,
@ -442,9 +432,6 @@ func (o *ApplyOptions) Validate() error {
} }
} }
} }
if len(o.Subresource) > 0 && !o.ServerSideApply {
return fmt.Errorf("--subresource can only be specified for --server-side")
}
return nil return nil
} }
@ -584,15 +571,13 @@ func (o *ApplyOptions) applyOneObject(info *resource.Info) error {
options := metav1.PatchOptions{ options := metav1.PatchOptions{
Force: &o.ForceConflicts, Force: &o.ForceConflicts,
} }
obj, err := helper. obj, err := helper.Patch(
WithSubresource(o.Subresource). info.Namespace,
Patch( info.Name,
info.Namespace, types.ApplyPatchType,
info.Name, data,
types.ApplyPatchType, &options,
data, )
&options,
)
if err != nil { if err != nil {
if isIncompatibleServerError(err) { if isIncompatibleServerError(err) {
err = fmt.Errorf("Server-side apply not available on the server: (%v)", err) err = fmt.Errorf("Server-side apply not available on the server: (%v)", err)
@ -690,12 +675,6 @@ See https://kubernetes.io/docs/reference/using-api/server-side-apply/#conflicts`
return cmdutil.AddSourceToErr("creating", info.Source, err) return cmdutil.AddSourceToErr("creating", info.Source, err)
} }
// prune nulls when client-side apply does a create to match what will happen when client-side applying an update.
// do this after CreateApplyAnnotation so the annotation matches what will be persisted on an update apply of the same manifest.
if u, ok := info.Object.(runtime.Unstructured); ok {
pruneNullsFromMap(u.UnstructuredContent())
}
if o.DryRunStrategy != cmdutil.DryRunClient { if o.DryRunStrategy != cmdutil.DryRunClient {
// Then create the resource and skip the three-way merge // Then create the resource and skip the three-way merge
obj, err := helper.Create(info.Namespace, true, info.Object) obj, err := helper.Create(info.Namespace, true, info.Object)
@ -774,29 +753,6 @@ See https://kubernetes.io/docs/reference/using-api/server-side-apply/#conflicts`
return nil return nil
} }
func pruneNullsFromMap(data map[string]interface{}) {
for k, v := range data {
if v == nil {
delete(data, k)
} else {
pruneNulls(v)
}
}
}
func pruneNullsFromSlice(data []interface{}) {
for _, v := range data {
pruneNulls(v)
}
}
func pruneNulls(v interface{}) {
switch v := v.(type) {
case map[string]interface{}:
pruneNullsFromMap(v)
case []interface{}:
pruneNullsFromSlice(v)
}
}
// Saves the last-applied-configuration annotation in a separate SSA field manager // Saves the last-applied-configuration annotation in a separate SSA field manager
// to prevent it from being dropped by users who have transitioned to SSA. // to prevent it from being dropped by users who have transitioned to SSA.
// //

File diff suppressed because it is too large Load Diff

View File

@ -54,22 +54,13 @@ const (
// Example value: "kube-system,ns1,ns2". // Example value: "kube-system,ns1,ns2".
ApplySetAdditionalNamespacesAnnotation = "applyset.kubernetes.io/additional-namespaces" ApplySetAdditionalNamespacesAnnotation = "applyset.kubernetes.io/additional-namespaces"
// Deprecated: ApplySetGRsAnnotation is a list of group-resources used to optimize listing of ApplySet member objects. // ApplySetGRsAnnotation is a list of group-resources used to optimize listing of ApplySet member objects.
// It is optional in the ApplySet specification, as tools can perform discovery or use a different optimization.
// However, it is currently required in kubectl.
// When present, the value of this annotation must be a comma separated list of the group-resources,
// in the fully-qualified name format, i.e. <resourcename>.<group>.
// Example value: "certificates.cert-manager.io,configmaps,deployments.apps,secrets,services"
// Deprecated and replaced by ApplySetGKsAnnotation, support for this can be removed in applyset beta or GA.
DeprecatedApplySetGRsAnnotation = "applyset.kubernetes.io/contains-group-resources"
// ApplySetGKsAnnotation is a list of group-kinds used to optimize listing of ApplySet member objects.
// It is optional in the ApplySet specification, as tools can perform discovery or use a different optimization. // It is optional in the ApplySet specification, as tools can perform discovery or use a different optimization.
// However, it is currently required in kubectl. // However, it is currently required in kubectl.
// When present, the value of this annotation must be a comma separated list of the group-kinds, // When present, the value of this annotation must be a comma separated list of the group-kinds,
// in the fully-qualified name format, i.e. <kind>.<group>. // in the fully-qualified name format, i.e. <resourcename>.<group>.
// Example value: "Certificate.cert-manager.io,ConfigMap,deployments.apps,Secret,Service" // Example value: "certificates.cert-manager.io,configmaps,deployments.apps,secrets,services"
ApplySetGKsAnnotation = "applyset.kubernetes.io/contains-group-kinds" ApplySetGRsAnnotation = "applyset.kubernetes.io/contains-group-resources"
// ApplySetParentIDLabel is the key of the label that makes object an ApplySet parent object. // ApplySetParentIDLabel is the key of the label that makes object an ApplySet parent object.
// Its value MUST use the format specified in V1ApplySetIdFormat below // Its value MUST use the format specified in V1ApplySetIdFormat below
@ -101,13 +92,13 @@ type ApplySet struct {
toolingID ApplySetTooling toolingID ApplySetTooling
// currentResources is the set of resources that are part of the sever-side set as of when the current operation started. // currentResources is the set of resources that are part of the sever-side set as of when the current operation started.
currentResources map[schema.GroupKind]*kindInfo currentResources map[schema.GroupVersionResource]*meta.RESTMapping
// currentNamespaces is the set of namespaces that contain objects in this applyset as of when the current operation started. // currentNamespaces is the set of namespaces that contain objects in this applyset as of when the current operation started.
currentNamespaces sets.Set[string] currentNamespaces sets.Set[string]
// updatedResources is the set of resources that will be part of the set as of when the current operation completes. // updatedResources is the set of resources that will be part of the set as of when the current operation completes.
updatedResources map[schema.GroupKind]*kindInfo updatedResources map[schema.GroupVersionResource]*meta.RESTMapping
// updatedNamespaces is the set of namespaces that will contain objects in this applyset as of when the current operation completes. // updatedNamespaces is the set of namespaces that will contain objects in this applyset as of when the current operation completes.
updatedNamespaces sets.Set[string] updatedNamespaces sets.Set[string]
@ -152,9 +143,9 @@ func (t ApplySetTooling) String() string {
// NewApplySet creates a new ApplySet object tracked by the given parent object. // NewApplySet creates a new ApplySet object tracked by the given parent object.
func NewApplySet(parent *ApplySetParentRef, tooling ApplySetTooling, mapper meta.RESTMapper, client resource.RESTClient) *ApplySet { func NewApplySet(parent *ApplySetParentRef, tooling ApplySetTooling, mapper meta.RESTMapper, client resource.RESTClient) *ApplySet {
return &ApplySet{ return &ApplySet{
currentResources: make(map[schema.GroupKind]*kindInfo), currentResources: make(map[schema.GroupVersionResource]*meta.RESTMapping),
currentNamespaces: make(sets.Set[string]), currentNamespaces: make(sets.Set[string]),
updatedResources: make(map[schema.GroupKind]*kindInfo), updatedResources: make(map[schema.GroupVersionResource]*meta.RESTMapping),
updatedNamespaces: make(sets.Set[string]), updatedNamespaces: make(sets.Set[string]),
parentRef: parent, parentRef: parent,
toolingID: tooling, toolingID: tooling,
@ -293,7 +284,7 @@ func (a *ApplySet) fetchParent() error {
return fmt.Errorf("ApplySet parent object %q exists and has incorrect value for label %q (got: %s, want: %s)", a.parentRef, ApplySetParentIDLabel, idLabel, a.ID()) return fmt.Errorf("ApplySet parent object %q exists and has incorrect value for label %q (got: %s, want: %s)", a.parentRef, ApplySetParentIDLabel, idLabel, a.ID())
} }
if a.currentResources, err = parseKindAnnotation(annotations, a.restMapper); err != nil { if a.currentResources, err = parseResourcesAnnotation(annotations, a.restMapper); err != nil {
// TODO: handle GVRs for now-deleted CRDs // TODO: handle GVRs for now-deleted CRDs
return fmt.Errorf("parsing ApplySet annotation on %q: %w", a.parentRef, err) return fmt.Errorf("parsing ApplySet annotation on %q: %w", a.parentRef, err)
} }
@ -311,8 +302,8 @@ func (a *ApplySet) LabelSelectorForMembers() string {
// AllPrunableResources returns the list of all resources that should be considered for pruning. // AllPrunableResources returns the list of all resources that should be considered for pruning.
// This is potentially a superset of the resources types that actually contain resources. // This is potentially a superset of the resources types that actually contain resources.
func (a *ApplySet) AllPrunableResources() []*kindInfo { func (a *ApplySet) AllPrunableResources() []*meta.RESTMapping {
var ret []*kindInfo var ret []*meta.RESTMapping
for _, m := range a.currentResources { for _, m := range a.currentResources {
ret = append(ret, m) ret = append(ret, m)
} }
@ -345,43 +336,14 @@ func toolingBaseName(toolAnnotation string) string {
return toolAnnotation return toolAnnotation
} }
// kindInfo holds type information about a particular resource type. func parseResourcesAnnotation(annotations map[string]string, mapper meta.RESTMapper) (map[schema.GroupVersionResource]*meta.RESTMapping, error) {
type kindInfo struct { annotation, ok := annotations[ApplySetGRsAnnotation]
restMapping *meta.RESTMapping
}
func parseKindAnnotation(annotations map[string]string, mapper meta.RESTMapper) (map[schema.GroupKind]*kindInfo, error) {
annotation, ok := annotations[ApplySetGKsAnnotation]
if !ok { if !ok {
if annotations[DeprecatedApplySetGRsAnnotation] != "" {
return parseDeprecatedResourceAnnotation(annotations[DeprecatedApplySetGRsAnnotation], mapper)
}
// The spec does not require this annotation. However, 'missing' means 'perform discovery'. // The spec does not require this annotation. However, 'missing' means 'perform discovery'.
// We return an error because we do not currently support dynamic discovery in kubectl apply. // We return an error because we do not currently support dynamic discovery in kubectl apply.
return nil, fmt.Errorf("kubectl requires the %q annotation to be set on all ApplySet parent objects", ApplySetGKsAnnotation) return nil, fmt.Errorf("kubectl requires the %q annotation to be set on all ApplySet parent objects", ApplySetGRsAnnotation)
} }
mappings := make(map[schema.GroupKind]*kindInfo) mappings := make(map[schema.GroupVersionResource]*meta.RESTMapping)
// Annotation present but empty means that this is currently an empty set.
if annotation == "" {
return mappings, nil
}
for _, gkString := range strings.Split(annotation, ",") {
gk := schema.ParseGroupKind(gkString)
restMapping, err := mapper.RESTMapping(gk)
if err != nil {
return nil, fmt.Errorf("could not find mapping for kind in %q annotation: %w", ApplySetGKsAnnotation, err)
}
mappings[gk] = &kindInfo{
restMapping: restMapping,
}
}
return mappings, nil
}
func parseDeprecatedResourceAnnotation(annotation string, mapper meta.RESTMapper) (map[schema.GroupKind]*kindInfo, error) {
mappings := make(map[schema.GroupKind]*kindInfo)
// Annotation present but empty means that this is currently an empty set. // Annotation present but empty means that this is currently an empty set.
if annotation == "" { if annotation == "" {
return mappings, nil return mappings, nil
@ -390,15 +352,13 @@ func parseDeprecatedResourceAnnotation(annotation string, mapper meta.RESTMapper
gr := schema.ParseGroupResource(grString) gr := schema.ParseGroupResource(grString)
gvk, err := mapper.KindFor(gr.WithVersion("")) gvk, err := mapper.KindFor(gr.WithVersion(""))
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid group resource in %q annotation: %w", DeprecatedApplySetGRsAnnotation, err) return nil, fmt.Errorf("invalid group resource in %q annotation: %w", ApplySetGRsAnnotation, err)
} }
restMapping, err := mapper.RESTMapping(gvk.GroupKind()) mapping, err := mapper.RESTMapping(gvk.GroupKind())
if err != nil { if err != nil {
return nil, fmt.Errorf("could not find kind for resource in %q annotation: %w", DeprecatedApplySetGRsAnnotation, err) return nil, fmt.Errorf("could not find kind for resource in %q annotation: %w", ApplySetGRsAnnotation, err)
}
mappings[gvk.GroupKind()] = &kindInfo{
restMapping: restMapping,
} }
mappings[mapping.Resource] = mapping
} }
return mappings, nil return mappings, nil
} }
@ -417,14 +377,9 @@ func parseNamespacesAnnotation(annotations map[string]string) sets.Set[string] {
// addResource registers the given resource and namespace as being part of the updated set of // addResource registers the given resource and namespace as being part of the updated set of
// resources being applied by the current operation. // resources being applied by the current operation.
func (a *ApplySet) addResource(restMapping *meta.RESTMapping, namespace string) { func (a *ApplySet) addResource(resource *meta.RESTMapping, namespace string) {
gk := restMapping.GroupVersionKind.GroupKind() a.updatedResources[resource.Resource] = resource
if _, found := a.updatedResources[gk]; !found { if resource.Scope == meta.RESTScopeNamespace && namespace != "" {
a.updatedResources[gk] = &kindInfo{
restMapping: restMapping,
}
}
if restMapping.Scope == meta.RESTScopeNamespace && namespace != "" {
a.updatedNamespaces.Insert(namespace) a.updatedNamespaces.Insert(namespace)
} }
} }
@ -439,8 +394,6 @@ func (a *ApplySet) updateParent(mode ApplySetUpdateMode, dryRun cmdutil.DryRunSt
if err != nil { if err != nil {
return fmt.Errorf("failed to encode patch for ApplySet parent: %w", err) return fmt.Errorf("failed to encode patch for ApplySet parent: %w", err)
} }
// Note that because we are using SSA, we will remove any annotations we don't specify,
// which is how we remove the deprecated contains-group-resources annotation.
err = serverSideApplyRequest(a, data, dryRun, validation, false) err = serverSideApplyRequest(a, data, dryRun, validation, false)
if err != nil && errors.IsConflict(err) { if err != nil && errors.IsConflict(err) {
// Try again with conflicts forced // Try again with conflicts forced
@ -476,17 +429,17 @@ func serverSideApplyRequest(a *ApplySet, data []byte, dryRun cmdutil.DryRunStrat
} }
func (a *ApplySet) buildParentPatch(mode ApplySetUpdateMode) *metav1.PartialObjectMetadata { func (a *ApplySet) buildParentPatch(mode ApplySetUpdateMode) *metav1.PartialObjectMetadata {
var newGKsAnnotation, newNsAnnotation string var newGRsAnnotation, newNsAnnotation string
switch mode { switch mode {
case updateToSuperset: case updateToSuperset:
// If the apply succeeded but pruning failed, the set of group resources that // If the apply succeeded but pruning failed, the set of group resources that
// the ApplySet should track is the superset of the previous and current resources. // the ApplySet should track is the superset of the previous and current resources.
// This ensures that the resources that failed to be pruned are not orphaned from the set. // This ensures that the resources that failed to be pruned are not orphaned from the set.
grSuperset := sets.KeySet(a.currentResources).Union(sets.KeySet(a.updatedResources)) grSuperset := sets.KeySet(a.currentResources).Union(sets.KeySet(a.updatedResources))
newGKsAnnotation = generateKindsAnnotation(grSuperset) newGRsAnnotation = generateResourcesAnnotation(grSuperset)
newNsAnnotation = generateNamespacesAnnotation(a.currentNamespaces.Union(a.updatedNamespaces), a.parentRef.Namespace) newNsAnnotation = generateNamespacesAnnotation(a.currentNamespaces.Union(a.updatedNamespaces), a.parentRef.Namespace)
case updateToLatestSet: case updateToLatestSet:
newGKsAnnotation = generateKindsAnnotation(sets.KeySet(a.updatedResources)) newGRsAnnotation = generateResourcesAnnotation(sets.KeySet(a.updatedResources))
newNsAnnotation = generateNamespacesAnnotation(a.updatedNamespaces, a.parentRef.Namespace) newNsAnnotation = generateNamespacesAnnotation(a.updatedNamespaces, a.parentRef.Namespace)
} }
@ -500,7 +453,7 @@ func (a *ApplySet) buildParentPatch(mode ApplySetUpdateMode) *metav1.PartialObje
Namespace: a.parentRef.Namespace, Namespace: a.parentRef.Namespace,
Annotations: map[string]string{ Annotations: map[string]string{
ApplySetToolingAnnotation: a.toolingID.String(), ApplySetToolingAnnotation: a.toolingID.String(),
ApplySetGKsAnnotation: newGKsAnnotation, ApplySetGRsAnnotation: newGRsAnnotation,
ApplySetAdditionalNamespacesAnnotation: newNsAnnotation, ApplySetAdditionalNamespacesAnnotation: newNsAnnotation,
}, },
Labels: map[string]string{ Labels: map[string]string{
@ -516,13 +469,13 @@ func generateNamespacesAnnotation(namespaces sets.Set[string], skip string) stri
return strings.Join(nsList, ",") return strings.Join(nsList, ",")
} }
func generateKindsAnnotation(resources sets.Set[schema.GroupKind]) string { func generateResourcesAnnotation(resources sets.Set[schema.GroupVersionResource]) string {
var gks []string var grs []string
for gk := range resources { for gvr := range resources {
gks = append(gks, gk.String()) grs = append(grs, gvr.GroupResource().String())
} }
sort.Strings(gks) sort.Strings(grs)
return strings.Join(gks, ",") return strings.Join(grs, ",")
} }
func (a ApplySet) FieldManager() string { func (a ApplySet) FieldManager() string {

View File

@ -77,29 +77,27 @@ func (a *ApplySet) FindAllObjectsToPrune(ctx context.Context, dynamicClient dyna
// We run discovery in parallel, in as many goroutines as priority and fairness will allow // We run discovery in parallel, in as many goroutines as priority and fairness will allow
// (We don't expect many requests in real-world scenarios - maybe tens, unlikely to be hundreds) // (We don't expect many requests in real-world scenarios - maybe tens, unlikely to be hundreds)
for gvk, resource := range a.AllPrunableResources() { for _, restMapping := range a.AllPrunableResources() {
scope := resource.restMapping.Scope switch restMapping.Scope.Name() {
switch scope.Name() {
case meta.RESTScopeNameNamespace: case meta.RESTScopeNameNamespace:
for _, namespace := range a.AllPrunableNamespaces() { for _, namespace := range a.AllPrunableNamespaces() {
if namespace == "" { if namespace == "" {
// Just double-check because otherwise we get cryptic error messages // Just double-check because otherwise we get cryptic error messages
return nil, fmt.Errorf("unexpectedly encountered empty namespace during prune of namespace-scoped resource %v", gvk) return nil, fmt.Errorf("unexpectedly encountered empty namespace during prune of namespace-scoped resource %v", restMapping.GroupVersionKind)
} }
tasks = append(tasks, &task{ tasks = append(tasks, &task{
namespace: namespace, namespace: namespace,
restMapping: resource.restMapping, restMapping: restMapping,
}) })
} }
case meta.RESTScopeNameRoot: case meta.RESTScopeNameRoot:
tasks = append(tasks, &task{ tasks = append(tasks, &task{
restMapping: resource.restMapping, restMapping: restMapping,
}) })
default: default:
return nil, fmt.Errorf("unhandled scope %q", scope.Name()) return nil, fmt.Errorf("unhandled scope %q", restMapping.Scope.Name())
} }
} }

View File

@ -17,12 +17,13 @@ limitations under the License.
package apply package apply
import ( import (
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"time" "time"
"github.com/pkg/errors"
"github.com/jonboulle/clockwork" "github.com/jonboulle/clockwork"
apierrors "k8s.io/apimachinery/pkg/api/errors" apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/api/meta"
@ -36,9 +37,6 @@ import (
"k8s.io/apimachinery/pkg/util/strategicpatch" "k8s.io/apimachinery/pkg/util/strategicpatch"
"k8s.io/apimachinery/pkg/util/wait" "k8s.io/apimachinery/pkg/util/wait"
"k8s.io/cli-runtime/pkg/resource" "k8s.io/cli-runtime/pkg/resource"
"k8s.io/client-go/openapi3"
"k8s.io/klog/v2"
"k8s.io/kube-openapi/pkg/validation/spec"
cmdutil "k8s.io/kubectl/pkg/cmd/util" cmdutil "k8s.io/kubectl/pkg/cmd/util"
"k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/scheme"
"k8s.io/kubectl/pkg/util" "k8s.io/kubectl/pkg/util"
@ -48,18 +46,13 @@ import (
const ( const (
// maxPatchRetry is the maximum number of conflicts retry for during a patch operation before returning failure // maxPatchRetry is the maximum number of conflicts retry for during a patch operation before returning failure
maxPatchRetry = 5 maxPatchRetry = 5
// backOffPeriod is the period to back off when apply patch results in error.
backOffPeriod = 1 * time.Second
// how many times we can retry before back off // how many times we can retry before back off
triesBeforeBackOff = 1 triesBeforeBackOff = 1
// groupVersionKindExtensionKey is the key used to lookup the
// GroupVersionKind value for an object definition from the
// definition's "extensions" map.
groupVersionKindExtensionKey = "x-kubernetes-group-version-kind"
) )
// patchRetryBackOffPeriod is the period to back off when apply patch results in error. var createPatchErrFormat = "creating patch with:\noriginal:\n%s\nmodified:\n%s\ncurrent:\n%s\nfor:"
var patchRetryBackOffPeriod = 1 * time.Second
var createPatchErrFormat = "creating patch with:\noriginal:\n%s\nmodified:\n%s\ncurrent:\n%s\nfor: %w"
// Patcher defines options to patch OpenAPI objects. // Patcher defines options to patch OpenAPI objects.
type Patcher struct { type Patcher struct {
@ -80,17 +73,13 @@ type Patcher struct {
// Number of retries to make if the patch fails with conflict // Number of retries to make if the patch fails with conflict
Retries int Retries int
OpenAPIGetter openapi.OpenAPIResourcesGetter OpenapiSchema openapi.Resources
OpenAPIV3Root openapi3.Root
} }
func newPatcher(o *ApplyOptions, info *resource.Info, helper *resource.Helper) (*Patcher, error) { func newPatcher(o *ApplyOptions, info *resource.Info, helper *resource.Helper) (*Patcher, error) {
var openAPIGetter openapi.OpenAPIResourcesGetter var openapiSchema openapi.Resources
var openAPIV3Root openapi3.Root
if o.OpenAPIPatch { if o.OpenAPIPatch {
openAPIGetter = o.OpenAPIGetter openapiSchema = o.OpenAPISchema
openAPIV3Root = o.OpenAPIV3Root
} }
return &Patcher{ return &Patcher{
@ -102,8 +91,7 @@ func newPatcher(o *ApplyOptions, info *resource.Info, helper *resource.Helper) (
CascadingStrategy: o.DeleteOptions.CascadingStrategy, CascadingStrategy: o.DeleteOptions.CascadingStrategy,
Timeout: o.DeleteOptions.Timeout, Timeout: o.DeleteOptions.Timeout,
GracePeriod: o.DeleteOptions.GracePeriod, GracePeriod: o.DeleteOptions.GracePeriod,
OpenAPIGetter: openAPIGetter, OpenapiSchema: openapiSchema,
OpenAPIV3Root: openAPIV3Root,
Retries: maxPatchRetry, Retries: maxPatchRetry,
}, nil }, nil
} }
@ -118,54 +106,26 @@ func (p *Patcher) patchSimple(obj runtime.Object, modified []byte, namespace, na
// Serialize the current configuration of the object from the server. // Serialize the current configuration of the object from the server.
current, err := runtime.Encode(unstructured.UnstructuredJSONScheme, obj) current, err := runtime.Encode(unstructured.UnstructuredJSONScheme, obj)
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("serializing current configuration from:\n%v\nfor: %w", obj, err) return nil, nil, errors.Wrapf(err, "serializing current configuration from:\n%v\nfor:", obj)
} }
// Retrieve the original configuration of the object from the annotation. // Retrieve the original configuration of the object from the annotation.
original, err := util.GetOriginalConfiguration(obj) original, err := util.GetOriginalConfiguration(obj)
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("retrieving original configuration from:\n%v\nfor: %w", obj, err) return nil, nil, errors.Wrapf(err, "retrieving original configuration from:\n%v\nfor:", obj)
} }
var patchType types.PatchType var patchType types.PatchType
var patch []byte var patch []byte
if p.OpenAPIV3Root != nil { if p.OpenapiSchema != nil {
gvkSupported, err := p.gvkSupportsPatchOpenAPIV3(p.Mapping.GroupVersionKind) // if openapischema is used, we'll try to get required patch type for this GVK from Open API.
if err != nil { // if it fails or could not find any patch type, fall back to baked-in patch type determination.
// Realistically this error logging is not needed (not present in V2), if patchType, err = p.getPatchTypeFromOpenAPI(p.Mapping.GroupVersionKind); err == nil && patchType == types.StrategicMergePatchType {
// but would help us in debugging if users encounter a problem patch, err = p.buildStrategicMergeFromOpenAPI(original, modified, current)
// with OpenAPI V3 not present in V2.
klog.V(5).Infof("warning: OpenAPI V3 path does not exist - group: %s, version %s, kind %s\n",
p.Mapping.GroupVersionKind.Group, p.Mapping.GroupVersionKind.Version, p.Mapping.GroupVersionKind.Kind)
} else if gvkSupported {
patch, err = p.buildStrategicMergePatchFromOpenAPIV3(original, modified, current)
if err != nil { if err != nil {
// Fall back to OpenAPI V2 if there is a problem // Warn user about problem and continue strategic merge patching using builtin types.
// We should remove the fallback in the future, fmt.Fprintf(errOut, "warning: error calculating patch from openapi spec: %v\n", err)
// but for the first release it might be beneficial
// to fall back to OpenAPI V2 while logging the error
// and seeing if we get any bug reports.
fmt.Fprintf(errOut, "warning: error calculating patch from openapi v3 spec: %v\n", err)
} else {
patchType = types.StrategicMergePatchType
}
} else {
klog.V(5).Infof("warning: OpenAPI V3 path does not support strategic merge patch - group: %s, version %s, kind %s\n",
p.Mapping.GroupVersionKind.Group, p.Mapping.GroupVersionKind.Version, p.Mapping.GroupVersionKind.Kind)
}
}
if patch == nil && p.OpenAPIGetter != nil {
if openAPISchema, err := p.OpenAPIGetter.OpenAPISchema(); err == nil && openAPISchema != nil {
// if openapischema is used, we'll try to get required patch type for this GVK from Open API.
// if it fails or could not find any patch type, fall back to baked-in patch type determination.
if patchType, err = p.getPatchTypeFromOpenAPI(openAPISchema, p.Mapping.GroupVersionKind); err == nil && patchType == types.StrategicMergePatchType {
patch, err = p.buildStrategicMergeFromOpenAPI(openAPISchema, original, modified, current)
if err != nil {
// Warn user about problem and continue strategic merge patching using builtin types.
fmt.Fprintf(errOut, "warning: error calculating patch from openapi spec: %v\n", err)
}
} }
} }
} }
@ -176,17 +136,17 @@ func (p *Patcher) patchSimple(obj runtime.Object, modified []byte, namespace, na
patchType = types.StrategicMergePatchType patchType = types.StrategicMergePatchType
patch, err = p.buildStrategicMergeFromBuiltins(versionedObj, original, modified, current) patch, err = p.buildStrategicMergeFromBuiltins(versionedObj, original, modified, current)
if err != nil { if err != nil {
return nil, nil, fmt.Errorf(createPatchErrFormat, original, modified, current, err) return nil, nil, errors.Wrapf(err, createPatchErrFormat, original, modified, current)
} }
} else { } else {
if !runtime.IsNotRegisteredError(err) { if !runtime.IsNotRegisteredError(err) {
return nil, nil, fmt.Errorf("getting instance of versioned object for %v: %w", p.Mapping.GroupVersionKind, err) return nil, nil, errors.Wrapf(err, "getting instance of versioned object for %v:", p.Mapping.GroupVersionKind)
} }
patchType = types.MergePatchType patchType = types.MergePatchType
patch, err = p.buildMergePatch(original, modified, current) patch, err = p.buildMergePatch(original, modified, current)
if err != nil { if err != nil {
return nil, nil, fmt.Errorf(createPatchErrFormat, original, modified, current, err) return nil, nil, errors.Wrapf(err, createPatchErrFormat, original, modified, current)
} }
} }
} }
@ -198,7 +158,7 @@ func (p *Patcher) patchSimple(obj runtime.Object, modified []byte, namespace, na
if p.ResourceVersion != nil { if p.ResourceVersion != nil {
patch, err = addResourceVersion(patch, *p.ResourceVersion) patch, err = addResourceVersion(patch, *p.ResourceVersion)
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("failed to insert resourceVersion in patch: %w", err) return nil, nil, errors.Wrap(err, "Failed to insert resourceVersion in patch")
} }
} }
@ -222,94 +182,10 @@ func (p *Patcher) buildMergePatch(original, modified, current []byte) ([]byte, e
return patch, nil return patch, nil
} }
// gvkSupportsPatchOpenAPIV3 checks if a particular GVK supports the patch operation.
// It returns an error if the OpenAPI V3 could not be downloaded.
func (p *Patcher) gvkSupportsPatchOpenAPIV3(gvk schema.GroupVersionKind) (bool, error) {
gvSpec, err := p.OpenAPIV3Root.GVSpec(schema.GroupVersion{
Group: p.Mapping.GroupVersionKind.Group,
Version: p.Mapping.GroupVersionKind.Version,
})
if err != nil {
return false, err
}
if gvSpec == nil || gvSpec.Paths == nil || gvSpec.Paths.Paths == nil {
return false, fmt.Errorf("gvk group: %s, version: %s, kind: %s does not exist for OpenAPI V3", gvk.Group, gvk.Version, gvk.Kind)
}
for _, path := range gvSpec.Paths.Paths {
if path.Patch != nil {
if gvkMatchesSingle(p.Mapping.GroupVersionKind, path.Patch.Extensions) {
if path.Patch.RequestBody == nil || path.Patch.RequestBody.Content == nil {
// GVK exists but does not support requestBody. Indication of malformed OpenAPI.
return false, nil
}
if _, ok := path.Patch.RequestBody.Content["application/strategic-merge-patch+json"]; ok {
return true, nil
}
// GVK exists but strategic-merge-patch is not supported. Likely to be a CRD or aggregated resource.
return false, nil
}
}
}
return false, nil
}
func gvkMatchesArray(targetGVK schema.GroupVersionKind, ext spec.Extensions) bool {
var gvkList []map[string]string
err := ext.GetObject(groupVersionKindExtensionKey, &gvkList)
if err != nil {
return false
}
for _, gvkMap := range gvkList {
if gvkMap["group"] == targetGVK.Group &&
gvkMap["version"] == targetGVK.Version &&
gvkMap["kind"] == targetGVK.Kind {
return true
}
}
return false
}
func gvkMatchesSingle(targetGVK schema.GroupVersionKind, ext spec.Extensions) bool {
var gvkMap map[string]string
err := ext.GetObject(groupVersionKindExtensionKey, &gvkMap)
if err != nil {
return false
}
return gvkMap["group"] == targetGVK.Group &&
gvkMap["version"] == targetGVK.Version &&
gvkMap["kind"] == targetGVK.Kind
}
func (p *Patcher) buildStrategicMergePatchFromOpenAPIV3(original, modified, current []byte) ([]byte, error) {
gvSpec, err := p.OpenAPIV3Root.GVSpec(schema.GroupVersion{
Group: p.Mapping.GroupVersionKind.Group,
Version: p.Mapping.GroupVersionKind.Version,
})
if err != nil {
return nil, err
}
if gvSpec == nil || gvSpec.Components == nil {
return nil, fmt.Errorf("OpenAPI V3 Components is nil")
}
for _, c := range gvSpec.Components.Schemas {
if !gvkMatchesArray(p.Mapping.GroupVersionKind, c.Extensions) {
continue
}
lookupPatchMeta := strategicpatch.PatchMetaFromOpenAPIV3{Schema: c, SchemaList: gvSpec.Components.Schemas}
if openapiv3Patch, err := strategicpatch.CreateThreeWayMergePatch(original, modified, current, lookupPatchMeta, p.Overwrite); err != nil {
return nil, err
} else {
return openapiv3Patch, nil
}
}
return nil, nil
}
// buildStrategicMergeFromOpenAPI builds patch from OpenAPI if it is enabled. // buildStrategicMergeFromOpenAPI builds patch from OpenAPI if it is enabled.
// This is used for core types which is published in openapi. // This is used for core types which is published in openapi.
func (p *Patcher) buildStrategicMergeFromOpenAPI(openAPISchema openapi.Resources, original, modified, current []byte) ([]byte, error) { func (p *Patcher) buildStrategicMergeFromOpenAPI(original, modified, current []byte) ([]byte, error) {
schema := openAPISchema.LookupResource(p.Mapping.GroupVersionKind) schema := p.OpenapiSchema.LookupResource(p.Mapping.GroupVersionKind)
if schema == nil { if schema == nil {
// Missing schema returns nil patch; also no error. // Missing schema returns nil patch; also no error.
return nil, nil return nil, nil
@ -323,8 +199,8 @@ func (p *Patcher) buildStrategicMergeFromOpenAPI(openAPISchema openapi.Resources
} }
// getPatchTypeFromOpenAPI looks up patch types supported by given GroupVersionKind in Open API. // getPatchTypeFromOpenAPI looks up patch types supported by given GroupVersionKind in Open API.
func (p *Patcher) getPatchTypeFromOpenAPI(openAPISchema openapi.Resources, gvk schema.GroupVersionKind) (types.PatchType, error) { func (p *Patcher) getPatchTypeFromOpenAPI(gvk schema.GroupVersionKind) (types.PatchType, error) {
if pc := openAPISchema.GetConsumes(p.Mapping.GroupVersionKind, "PATCH"); pc != nil { if pc := p.OpenapiSchema.GetConsumes(p.Mapping.GroupVersionKind, "PATCH"); pc != nil {
for _, c := range pc { for _, c := range pc {
if c == string(types.StrategicMergePatchType) { if c == string(types.StrategicMergePatchType) {
return types.StrategicMergePatchType, nil return types.StrategicMergePatchType, nil
@ -363,7 +239,7 @@ func (p *Patcher) Patch(current runtime.Object, modified []byte, source, namespa
} }
for i := 1; i <= p.Retries && apierrors.IsConflict(err); i++ { for i := 1; i <= p.Retries && apierrors.IsConflict(err); i++ {
if i > triesBeforeBackOff { if i > triesBeforeBackOff {
p.BackOff.Sleep(patchRetryBackOffPeriod) p.BackOff.Sleep(backOffPeriod)
} }
current, getErr = p.Helper.Get(namespace, name) current, getErr = p.Helper.Get(namespace, name)
if getErr != nil { if getErr != nil {
@ -386,7 +262,7 @@ func (p *Patcher) deleteAndCreate(original runtime.Object, modified []byte, name
return modified, nil, err return modified, nil, err
} }
// TODO: use wait // TODO: use wait
if err := wait.PollUntilContextTimeout(context.Background(), 1*time.Second, p.Timeout, true, func(ctx context.Context) (bool, error) { if err := wait.PollImmediate(1*time.Second, p.Timeout, func() (bool, error) {
if _, err := p.Helper.Get(namespace, name); !apierrors.IsNotFound(err) { if _, err := p.Helper.Get(namespace, name); !apierrors.IsNotFound(err) {
return false, err return false, err
} }

View File

@ -28,7 +28,6 @@ import (
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/httpstream"
"k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/genericiooptions"
"k8s.io/cli-runtime/pkg/resource" "k8s.io/cli-runtime/pkg/resource"
@ -126,7 +125,7 @@ func NewCmdAttach(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.
// RemoteAttach defines the interface accepted by the Attach command - provided for test stubbing // RemoteAttach defines the interface accepted by the Attach command - provided for test stubbing
type RemoteAttach interface { type RemoteAttach interface {
Attach(url *url.URL, config *restclient.Config, stdin io.Reader, stdout, stderr io.Writer, tty bool, terminalSizeQueue remotecommand.TerminalSizeQueue) error Attach(method string, url *url.URL, config *restclient.Config, stdin io.Reader, stdout, stderr io.Writer, tty bool, terminalSizeQueue remotecommand.TerminalSizeQueue) error
} }
// DefaultAttachFunc is the default AttachFunc used // DefaultAttachFunc is the default AttachFunc used
@ -149,7 +148,7 @@ func DefaultAttachFunc(o *AttachOptions, containerToAttach *corev1.Container, ra
TTY: raw, TTY: raw,
}, scheme.ParameterCodec) }, scheme.ParameterCodec)
return o.Attach.Attach(req.URL(), o.Config, o.In, o.Out, o.ErrOut, raw, sizeQueue) return o.Attach.Attach("POST", req.URL(), o.Config, o.In, o.Out, o.ErrOut, raw, sizeQueue)
} }
} }
@ -157,8 +156,8 @@ func DefaultAttachFunc(o *AttachOptions, containerToAttach *corev1.Container, ra
type DefaultRemoteAttach struct{} type DefaultRemoteAttach struct{}
// Attach executes attach to a running container // Attach executes attach to a running container
func (*DefaultRemoteAttach) Attach(url *url.URL, config *restclient.Config, stdin io.Reader, stdout, stderr io.Writer, tty bool, terminalSizeQueue remotecommand.TerminalSizeQueue) error { func (*DefaultRemoteAttach) Attach(method string, url *url.URL, config *restclient.Config, stdin io.Reader, stdout, stderr io.Writer, tty bool, terminalSizeQueue remotecommand.TerminalSizeQueue) error {
exec, err := createExecutor(url, config) exec, err := remotecommand.NewSPDYExecutor(config, method, url)
if err != nil { if err != nil {
return err return err
} }
@ -171,29 +170,6 @@ func (*DefaultRemoteAttach) Attach(url *url.URL, config *restclient.Config, stdi
}) })
} }
// createExecutor returns the Executor or an error if one occurred.
func createExecutor(url *url.URL, config *restclient.Config) (remotecommand.Executor, error) {
exec, err := remotecommand.NewSPDYExecutor(config, "POST", url)
if err != nil {
return nil, err
}
// Fallback executor is default, unless feature flag is explicitly disabled.
if !cmdutil.RemoteCommandWebsockets.IsDisabled() {
// WebSocketExecutor must be "GET" method as described in RFC 6455 Sec. 4.1 (page 17).
websocketExec, err := remotecommand.NewWebSocketExecutor(config, "GET", url.String())
if err != nil {
return nil, err
}
exec, err = remotecommand.NewFallbackExecutor(websocketExec, exec, func(err error) bool {
return httpstream.IsUpgradeFailure(err) || httpstream.IsHTTPSProxyError(err)
})
if err != nil {
return nil, err
}
}
return exec, nil
}
// Complete verifies command line arguments and loads data from the command environment // Complete verifies command line arguments and loads data from the command environment
func (o *AttachOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { func (o *AttachOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error {
var err error var err error
@ -206,7 +182,7 @@ func (o *AttachOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []s
o.GetPodTimeout, err = cmdutil.GetPodRunningTimeoutFlag(cmd) o.GetPodTimeout, err = cmdutil.GetPodRunningTimeoutFlag(cmd)
if err != nil { if err != nil {
return cmdutil.UsageErrorf(cmd, "%s", err.Error()) return cmdutil.UsageErrorf(cmd, err.Error())
} }
o.Builder = f.NewBuilder o.Builder = f.NewBuilder
@ -307,7 +283,6 @@ func (o *AttachOptions) Run() error {
} }
if !o.Quiet { if !o.Quiet {
_, _ = fmt.Fprintln(o.ErrOut, "All commands and output from this session will be recorded in container logs, including credentials and sensitive information passed through the command prompt.")
fmt.Fprintln(o.ErrOut, "If you don't see a command prompt, try pressing enter.") fmt.Fprintln(o.ErrOut, "If you don't see a command prompt, try pressing enter.")
} }
if err := t.Safe(o.AttachFunc(o, containerToAttach, t.Raw, sizeQueue)); err != nil { if err := t.Safe(o.AttachFunc(o, containerToAttach, t.Raw, sizeQueue)); err != nil {

View File

@ -37,18 +37,19 @@ import (
"k8s.io/client-go/tools/remotecommand" "k8s.io/client-go/tools/remotecommand"
"k8s.io/kubectl/pkg/cmd/exec" "k8s.io/kubectl/pkg/cmd/exec"
cmdtesting "k8s.io/kubectl/pkg/cmd/testing" cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
cmdutil "k8s.io/kubectl/pkg/cmd/util"
"k8s.io/kubectl/pkg/cmd/util/podcmd" "k8s.io/kubectl/pkg/cmd/util/podcmd"
"k8s.io/kubectl/pkg/polymorphichelpers" "k8s.io/kubectl/pkg/polymorphichelpers"
"k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/scheme"
) )
type fakeRemoteAttach struct { type fakeRemoteAttach struct {
url *url.URL method string
err error url *url.URL
err error
} }
func (f *fakeRemoteAttach) Attach(url *url.URL, config *restclient.Config, stdin io.Reader, stdout, stderr io.Writer, tty bool, terminalSizeQueue remotecommand.TerminalSizeQueue) error { func (f *fakeRemoteAttach) Attach(method string, url *url.URL, config *restclient.Config, stdin io.Reader, stdout, stderr io.Writer, tty bool, terminalSizeQueue remotecommand.TerminalSizeQueue) error {
f.method = method
f.url = url f.url = url
return f.err return f.err
} }
@ -242,7 +243,6 @@ func TestAttach(t *testing.T) {
pod *corev1.Pod pod *corev1.Pod
remoteAttachErr bool remoteAttachErr bool
expectedErr string expectedErr string
expectedErrOut []string
}{ }{
{ {
name: "pod attach", name: "pod attach",
@ -252,10 +252,6 @@ func TestAttach(t *testing.T) {
attachPath: "/api/" + version + "/namespaces/test/pods/foo/attach", attachPath: "/api/" + version + "/namespaces/test/pods/foo/attach",
pod: attachPod(), pod: attachPod(),
container: "bar", container: "bar",
expectedErrOut: []string{
"All commands and output from this session will be recorded in container logs, including credentials and sensitive information passed through the command prompt.",
"If you don't see a command prompt, try pressing enter.",
},
}, },
{ {
name: "pod attach error", name: "pod attach error",
@ -310,11 +306,10 @@ func TestAttach(t *testing.T) {
if test.remoteAttachErr { if test.remoteAttachErr {
remoteAttach.err = fmt.Errorf("attach error") remoteAttach.err = fmt.Errorf("attach error")
} }
streams, _, _, errOut := genericiooptions.NewTestIOStreams()
options := &AttachOptions{ options := &AttachOptions{
StreamOptions: exec.StreamOptions{ StreamOptions: exec.StreamOptions{
ContainerName: test.container, ContainerName: test.container,
IOStreams: streams, IOStreams: genericiooptions.NewTestIOStreamsDiscard(),
}, },
Attach: remoteAttach, Attach: remoteAttach,
GetPodTimeout: 1000, GetPodTimeout: 1000,
@ -332,7 +327,7 @@ func TestAttach(t *testing.T) {
return err return err
} }
return options.Attach.Attach(u, nil, nil, nil, nil, raw, sizeQueue) return options.Attach.Attach("POST", u, nil, nil, nil, nil, raw, sizeQueue)
} }
} }
@ -352,17 +347,12 @@ func TestAttach(t *testing.T) {
t.Errorf("%s: Did not get expected path for exec request: %q %q", test.name, test.attachPath, remoteAttach.url.Path) t.Errorf("%s: Did not get expected path for exec request: %q %q", test.name, test.attachPath, remoteAttach.url.Path)
return return
} }
if remoteAttach.method != "POST" {
t.Errorf("%s: Did not get method for attach request: %s", test.name, remoteAttach.method)
}
if remoteAttach.url.Query().Get("container") != "bar" { if remoteAttach.url.Query().Get("container") != "bar" {
t.Errorf("%s: Did not have query parameters: %s", test.name, remoteAttach.url.Query()) t.Errorf("%s: Did not have query parameters: %s", test.name, remoteAttach.url.Query())
} }
if test.expectedErrOut != nil {
for _, expect := range test.expectedErrOut {
if !strings.Contains(errOut.String(), expect) {
t.Errorf("%s: expected message %s not found, got: %s", test.name, expect, strings.ReplaceAll(errOut.String(), "\n", ""))
return
}
}
}
}) })
} }
} }
@ -438,7 +428,7 @@ func TestAttachWarnings(t *testing.T) {
return err return err
} }
return options.Attach.Attach(u, nil, nil, nil, nil, raw, sizeQueue) return options.Attach.Attach("POST", u, nil, nil, nil, nil, raw, sizeQueue)
} }
} }
@ -568,37 +558,3 @@ func TestReattachMessage(t *testing.T) {
}) })
} }
} }
func TestCreateExecutor(t *testing.T) {
url, err := url.Parse("http://localhost:8080/index.html")
if err != nil {
t.Fatalf("unable to parse test url: %v", err)
}
config := cmdtesting.DefaultClientConfig()
// First, ensure that no environment variable creates the fallback executor.
executor, err := createExecutor(url, config)
if err != nil {
t.Fatalf("unable to create executor: %v", err)
}
if _, isFallback := executor.(*remotecommand.FallbackExecutor); !isFallback {
t.Errorf("expected fallback executor, got %#v", executor)
}
// Next, check turning on feature flag explicitly also creates fallback executor.
t.Setenv(string(cmdutil.RemoteCommandWebsockets), "true")
executor, err = createExecutor(url, config)
if err != nil {
t.Fatalf("unable to create executor: %v", err)
}
if _, isFallback := executor.(*remotecommand.FallbackExecutor); !isFallback {
t.Errorf("expected fallback executor, got %#v", executor)
}
// Finally, check explicit disabling does NOT create the fallback executor.
t.Setenv(string(cmdutil.RemoteCommandWebsockets), "false")
executor, err = createExecutor(url, config)
if err != nil {
t.Fatalf("unable to create executor: %v", err)
}
if _, isFallback := executor.(*remotecommand.FallbackExecutor); isFallback {
t.Errorf("expected fallback executor, got %#v", executor)
}
}

View File

@ -83,8 +83,9 @@ var (
# Check to see if I can list deployments in my current namespace # Check to see if I can list deployments in my current namespace
kubectl auth can-i list deployments.apps kubectl auth can-i list deployments.apps
# Check to see if service account "foo" of namespace "dev" can list pods in the namespace "prod" # Check to see if service account "foo" of namespace "dev" can list pods
# You must be allowed to use impersonation for the global option "--as" # in the namespace "prod".
# You must be allowed to use impersonation for the global option "--as".
kubectl auth can-i list pods --as=system:serviceaccount:dev:foo -n prod kubectl auth can-i list pods --as=system:serviceaccount:dev:foo -n prod
# Check to see if I can do everything in my current namespace ("*" means all) # Check to see if I can do everything in my current namespace ("*" means all)
@ -99,16 +100,13 @@ var (
# Check to see if I can access the URL /logs/ # Check to see if I can access the URL /logs/
kubectl auth can-i get /logs/ kubectl auth can-i get /logs/
# Check to see if I can approve certificates.k8s.io
kubectl auth can-i approve certificates.k8s.io
# List all allowed actions in namespace "foo" # List all allowed actions in namespace "foo"
kubectl auth can-i --list --namespace=foo`) kubectl auth can-i --list --namespace=foo`)
resourceVerbs = sets.New[string]("get", "list", "watch", "create", "update", "patch", "delete", "deletecollection", "use", "bind", "impersonate", "*", "approve", "sign", "escalate", "attest") resourceVerbs = sets.NewString("get", "list", "watch", "create", "update", "patch", "delete", "deletecollection", "use", "bind", "impersonate", "*")
nonResourceURLVerbs = sets.New[string]("get", "put", "post", "head", "options", "delete", "patch", "*") nonResourceURLVerbs = sets.NewString("get", "put", "post", "head", "options", "delete", "patch", "*")
// holds all the server-supported resources that cannot be discovered by clients. i.e. users and groups for the impersonate verb // holds all the server-supported resources that cannot be discovered by clients. i.e. users and groups for the impersonate verb
nonStandardResourceNames = sets.New[string]("users", "groups") nonStandardResourceNames = sets.NewString("users", "groups")
) )
// NewCmdCanI returns an initialized Command for 'auth can-i' sub command // NewCmdCanI returns an initialized Command for 'auth can-i' sub command
@ -185,7 +183,7 @@ func (o *CanIOptions) Complete(f cmdutil.Factory, args []string) error {
default: default:
errString := "you must specify two arguments: verb resource or verb resource/resourceName." errString := "you must specify two arguments: verb resource or verb resource/resourceName."
usageString := "See 'kubectl auth can-i -h' for help and examples." usageString := "See 'kubectl auth can-i -h' for help and examples."
return fmt.Errorf("%s\n%s", errString, usageString) return errors.New(fmt.Sprintf("%s\n%s", errString, usageString))
} }
} }

View File

@ -161,7 +161,7 @@ func TestRunAccessCheck(t *testing.T) {
test.serverErr test.serverErr
}), }),
} }
tf.ClientConfigVal = &restclient.Config{ContentConfig: restclient.ContentConfig{GroupVersion: &schema.GroupVersion{Group: "", Version: "v1"}, ContentType: runtime.ContentTypeJSON}} tf.ClientConfigVal = &restclient.Config{ContentConfig: restclient.ContentConfig{GroupVersion: &schema.GroupVersion{Group: "", Version: "v1"}}}
if err := test.o.Complete(tf, test.args); err != nil { if err := test.o.Complete(tf, test.args); err != nil {
t.Errorf("%s: %v", test.name, err) t.Errorf("%s: %v", test.name, err)
@ -196,7 +196,6 @@ func TestRunAccessList(t *testing.T) {
" [/version] [] [get]\n" " [/version] [] [get]\n"
tf := cmdtesting.NewTestFactory().WithNamespace("test") tf := cmdtesting.NewTestFactory().WithNamespace("test")
tf.ClientConfigVal.ContentType = runtime.ContentTypeJSON
defer tf.Cleanup() defer tf.Cleanup()
ns := scheme.Codecs.WithoutConversion() ns := scheme.Codecs.WithoutConversion()

View File

@ -122,16 +122,16 @@ var (
whoAmILong = templates.LongDesc(` whoAmILong = templates.LongDesc(`
Experimental: Check who you are and your attributes (groups, extra). Experimental: Check who you are and your attributes (groups, extra).
This command is helpful to get yourself aware of the current user attributes, This command is helpful to get yourself aware of the current user attributes,
especially when dynamic authentication, e.g., token webhook, auth proxy, or OIDC provider, especially when dynamic authentication, e.g., token webhook, auth proxy, or OIDC provider,
is enabled in the Kubernetes cluster. is enabled in the Kubernetes cluster.
`) `)
whoAmIExample = templates.Examples(` whoAmIExample = templates.Examples(`
# Get your subject attributes # Get your subject attributes.
kubectl auth whoami kubectl auth whoami
# Get your subject attributes in JSON format # Get your subject attributes in JSON format.
kubectl auth whoami -o json kubectl auth whoami -o json
`) `)
) )
@ -250,7 +250,7 @@ func printTableSelfSubjectAccessReview(obj runtime.Object, out io.Writer) error
} }
if len(ui.Extra) > 0 { if len(ui.Extra) > 0 {
for _, k := range sets.List(sets.KeySet(ui.Extra)) { for _, k := range sets.StringKeySet(ui.Extra).List() {
v := ui.Extra[k] v := ui.Extra[k]
_, err := fmt.Fprintf(w, "Extra: %s\t%v\n", k, v) _, err := fmt.Fprintf(w, "Extra: %s\t%v\n", k, v)
if err != nil { if err != nil {

View File

@ -75,7 +75,9 @@ func TestWhoAmIRun(t *testing.T) {
`{ `{
"kind": "SelfSubjectReview", "kind": "SelfSubjectReview",
"apiVersion": "authentication.k8s.io/v1", "apiVersion": "authentication.k8s.io/v1",
"metadata": {}, "metadata": {
"creationTimestamp": null
},
"status": { "status": {
"userInfo": { "userInfo": {
"username": "jane.doe", "username": "jane.doe",
@ -129,7 +131,9 @@ func TestWhoAmIRun(t *testing.T) {
`{ `{
"kind": "SelfSubjectReview", "kind": "SelfSubjectReview",
"apiVersion": "authentication.k8s.io/v1beta1", "apiVersion": "authentication.k8s.io/v1beta1",
"metadata": {}, "metadata": {
"creationTimestamp": null
},
"status": { "status": {
"userInfo": { "userInfo": {
"username": "jane.doe", "username": "jane.doe",
@ -182,7 +186,9 @@ func TestWhoAmIRun(t *testing.T) {
`{ `{
"kind": "SelfSubjectReview", "kind": "SelfSubjectReview",
"apiVersion": "authentication.k8s.io/v1", "apiVersion": "authentication.k8s.io/v1",
"metadata": {}, "metadata": {
"creationTimestamp": null
},
"status": { "status": {
"userInfo": { "userInfo": {
"username": "jane.doe", "username": "jane.doe",

View File

@ -19,26 +19,19 @@ package autoscale
import ( import (
"context" "context"
"fmt" "fmt"
"strconv"
"strings"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"k8s.io/klog/v2"
autoscalingv1 "k8s.io/api/autoscaling/v1" autoscalingv1 "k8s.io/api/autoscaling/v1"
autoscalingv2 "k8s.io/api/autoscaling/v2"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/api/meta"
apiresource "k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/genericiooptions"
"k8s.io/cli-runtime/pkg/printers" "k8s.io/cli-runtime/pkg/printers"
"k8s.io/cli-runtime/pkg/resource" "k8s.io/cli-runtime/pkg/resource"
autoscalingv1client "k8s.io/client-go/kubernetes/typed/autoscaling/v1" 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/client-go/scale"
"k8s.io/klog/v2"
cmdutil "k8s.io/kubectl/pkg/cmd/util" cmdutil "k8s.io/kubectl/pkg/cmd/util"
"k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/scheme"
"k8s.io/kubectl/pkg/util" "k8s.io/kubectl/pkg/util"
@ -50,23 +43,16 @@ import (
var ( var (
autoscaleLong = templates.LongDesc(i18n.T(` autoscaleLong = templates.LongDesc(i18n.T(`
Creates an autoscaler that automatically chooses and sets the number of pods that run in a Kubernetes cluster. Creates an autoscaler that automatically chooses and sets the number of pods that run in a Kubernetes cluster.
The command will attempt to use the autoscaling/v2 API first, in case of an error, it will fall back to autoscaling/v1 API.
Looks up a deployment, replica set, stateful set, or replication controller by name and creates an autoscaler that uses the given resource as a reference. Looks up a deployment, replica set, stateful set, or replication controller by name and creates an autoscaler that uses the given resource as a reference.
An autoscaler can automatically increase or decrease number of pods deployed within the system as needed.`)) An autoscaler can automatically increase or decrease number of pods deployed within the system as needed.`))
autoscaleExample = templates.Examples(i18n.T(` 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 # 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% # 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=80% kubectl autoscale rc foo --max=5 --cpu-percent=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 // AutoscaleOptions declares the arguments accepted by the Autoscale command
@ -83,8 +69,6 @@ type AutoscaleOptions struct {
Min int32 Min int32
Max int32 Max int32
CPUPercent int32 CPUPercent int32
CPU string
Memory string
createAnnotation bool createAnnotation bool
args []string args []string
@ -94,8 +78,7 @@ type AutoscaleOptions struct {
builder *resource.Builder builder *resource.Builder
fieldManager string fieldManager string
HPAClientV1 autoscalingv1client.HorizontalPodAutoscalersGetter HPAClient autoscalingv1client.HorizontalPodAutoscalersGetter
HPAClientV2 autoscalingv2client.HorizontalPodAutoscalersGetter
scaleKindResolver scale.ScaleKindResolver scaleKindResolver scale.ScaleKindResolver
genericiooptions.IOStreams genericiooptions.IOStreams
@ -120,7 +103,7 @@ func NewCmdAutoscale(f cmdutil.Factory, ioStreams genericiooptions.IOStreams) *c
validArgs := []string{"deployment", "replicaset", "replicationcontroller", "statefulset"} validArgs := []string{"deployment", "replicaset", "replicationcontroller", "statefulset"}
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "autoscale (-f FILENAME | TYPE NAME | TYPE/NAME) [--min=MINPODS] --max=MAXPODS [--cpu=CPU] [--memory=MEMORY]", Use: "autoscale (-f FILENAME | TYPE NAME | TYPE/NAME) [--min=MINPODS] --max=MAXPODS [--cpu-percent=CPU]",
DisableFlagsInUseLine: true, DisableFlagsInUseLine: true,
Short: i18n.T("Auto-scale a deployment, replica set, stateful set, or replication controller"), Short: i18n.T("Auto-scale a deployment, replica set, stateful set, or replication controller"),
Long: autoscaleLong, Long: autoscaleLong,
@ -140,11 +123,7 @@ 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.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.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().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().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.AddDryRunFlag(cmd)
cmdutil.AddFilenameOptionFlags(cmd, o.FilenameOptions, "identifying the resource to autoscale.") cmdutil.AddFilenameOptionFlags(cmd, o.FilenameOptions, "identifying the resource to autoscale.")
cmdutil.AddApplyAnnotationFlags(cmd) cmdutil.AddApplyAnnotationFlags(cmd)
@ -178,8 +157,7 @@ func (o *AutoscaleOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args
if err != nil { if err != nil {
return err return err
} }
o.HPAClientV2 = kubeClient.AutoscalingV2() o.HPAClient = kubeClient.AutoscalingV1()
o.HPAClientV1 = kubeClient.AutoscalingV1()
o.namespace, o.enforceNamespace, err = f.ToRawKubeConfigLoader().Namespace() o.namespace, o.enforceNamespace, err = f.ToRawKubeConfigLoader().Namespace()
if err != nil { if err != nil {
@ -204,25 +182,11 @@ func (o *AutoscaleOptions) Validate() error {
if o.Max < o.Min { 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) 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 return nil
} }
// Run performs the execution
func (o *AutoscaleOptions) Run() error { func (o *AutoscaleOptions) Run() error {
r := o.builder. r := o.builder.
Unstructured(). Unstructured().
@ -244,31 +208,48 @@ func (o *AutoscaleOptions) Run() error {
mapping := info.ResourceMapping() mapping := info.ResourceMapping()
gvr := mapping.GroupVersionKind.GroupVersion().WithResource(mapping.Resource.Resource) gvr := mapping.GroupVersionKind.GroupVersion().WithResource(mapping.Resource.Resource)
if _, err = o.scaleKindResolver.ScaleForResource(gvr); err != nil { if _, err := o.scaleKindResolver.ScaleForResource(gvr); err != nil {
return fmt.Errorf("cannot autoscale a %s: %w", mapping.GroupVersionKind.Kind, err) return fmt.Errorf("cannot autoscale a %v: %v", mapping.GroupVersionKind.Kind, err)
} }
// handles the creation of HorizontalPodAutoscaler objects for both autoscaling/v2 and autoscaling/v1 APIs. hpa := o.createHorizontalPodAutoscaler(info.Name, mapping)
// If autoscaling/v2 API fails, try to create and handle HorizontalPodAutoscaler using autoscaling/v1 API
var hpaV2 runtime.Object if err := o.Recorder.Record(hpa); err != nil {
hpaV2, err = o.createHorizontalPodAutoscalerV2(info.Name, mapping) klog.V(4).Infof("error recording current command: %v", err)
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. "+ if o.dryRunStrategy == cmdutil.DryRunClient {
"Falling back to try the autoscaling/v1 HorizontalPodAutoscaler", err) count++
// check if the HPA can be created using v1 API.
if ok, err := o.canCreateHPAV1(); !ok { printer, err := o.ToPrinter("created")
return fmt.Errorf("failed to create autoscaling/v2 HPA and the configuration is incompatible with autoscaling/v1: %w", err) if err != nil {
}
hpaV1 := o.createHorizontalPodAutoscalerV1(info.Name, mapping)
if err := o.handleHPA(hpaV1); err != nil {
return err return err
} }
return printer.PrintObj(hpa, o.Out)
} }
if err := util.CreateOrUpdateAnnotation(o.createAnnotation, hpa, scheme.DefaultJSONEncoder()); err != nil {
return err
}
createOptions := metav1.CreateOptions{}
if o.fieldManager != "" {
createOptions.FieldManager = o.fieldManager
}
if o.dryRunStrategy == cmdutil.DryRunServer {
createOptions.DryRun = []string{metav1.DryRunAll}
}
actualHPA, err := o.HPAClient.HorizontalPodAutoscalers(o.namespace).Create(context.TODO(), hpa, createOptions)
if err != nil {
return err
}
count++ count++
return nil printer, err := o.ToPrinter("autoscaled")
if err != nil {
return err
}
return printer.PrintObj(actualHPA, o.Out)
}) })
if err != nil { if err != nil {
return err return err
@ -279,169 +260,7 @@ func (o *AutoscaleOptions) Run() error {
return nil return nil
} }
func (o *AutoscaleOptions) canCreateHPAV1() (bool, error) { func (o *AutoscaleOptions) createHorizontalPodAutoscaler(refName string, mapping *meta.RESTMapping) *autoscalingv1.HorizontalPodAutoscaler {
// Allow fallback to v1 HPA only if:
// 1. CPUPercent is set and Memory is not set.
// 2. Or, Memory is not set and the metric type is UtilizationMetricType.
_, _, metricsType, err := parseResourceInput(o.CPU, corev1.ResourceCPU)
if err != nil {
return false, err
}
return (o.CPUPercent >= 0 && o.Memory == "") ||
(o.Memory == "" && metricsType == autoscalingv2.UtilizationMetricType), nil
}
// handleHPA handles the creation and management of a single HPA object.
func (o *AutoscaleOptions) handleHPA(hpa runtime.Object) error {
if err := o.Recorder.Record(hpa); err != nil {
return fmt.Errorf("error recording current command: %w", err)
}
if o.dryRunStrategy == cmdutil.DryRunClient {
printer, err := o.ToPrinter("created")
if err != nil {
return err
}
return printer.PrintObj(hpa, o.Out)
}
if err := util.CreateOrUpdateAnnotation(o.createAnnotation, hpa, scheme.DefaultJSONEncoder()); err != nil {
return err
}
createOptions := metav1.CreateOptions{}
if o.fieldManager != "" {
createOptions.FieldManager = o.fieldManager
}
if o.dryRunStrategy == cmdutil.DryRunServer {
createOptions.DryRun = []string{metav1.DryRunAll}
}
var actualHPA runtime.Object
var err error
switch typedHPA := hpa.(type) {
case *autoscalingv2.HorizontalPodAutoscaler:
actualHPA, err = o.HPAClientV2.HorizontalPodAutoscalers(o.namespace).Create(context.TODO(), typedHPA, createOptions)
case *autoscalingv1.HorizontalPodAutoscaler:
actualHPA, err = o.HPAClientV1.HorizontalPodAutoscalers(o.namespace).Create(context.TODO(), typedHPA, createOptions)
default:
return fmt.Errorf("unsupported HorizontalPodAutoscaler type %T", hpa)
}
if err != nil {
return err
}
printer, err := o.ToPrinter("autoscaled")
if err != nil {
return err
}
return printer.PrintObj(actualHPA, o.Out)
}
func (o *AutoscaleOptions) createHorizontalPodAutoscalerV2(refName string, mapping *meta.RESTMapping) (*autoscalingv2.HorizontalPodAutoscaler, error) {
name := o.Name
if len(name) == 0 {
name = refName
}
scaler := autoscalingv2.HorizontalPodAutoscaler{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{
APIVersion: mapping.GroupVersionKind.GroupVersion().String(),
Kind: mapping.GroupVersionKind.Kind,
Name: refName,
},
MaxReplicas: o.Max,
},
}
if o.Min > 0 {
scaler.Spec.MinReplicas = &o.Min
}
metrics := []autoscalingv2.MetricSpec{}
// add CPU metric if any of the CPU targets are specified
if o.CPUPercent > 0 {
cpuMetric := autoscalingv2.MetricSpec{
Type: autoscalingv2.ResourceMetricSourceType,
Resource: &autoscalingv2.ResourceMetricSource{
Name: corev1.ResourceCPU,
Target: autoscalingv2.MetricTarget{},
},
}
cpuMetric.Resource.Target.Type = autoscalingv2.UtilizationMetricType
cpuMetric.Resource.Target.AverageUtilization = &o.CPUPercent
metrics = append(metrics, cpuMetric)
}
// add Cpu metric if any of the cpu targets are specified
if o.CPU != "" {
cpuMetric := autoscalingv2.MetricSpec{
Type: autoscalingv2.ResourceMetricSourceType,
Resource: &autoscalingv2.ResourceMetricSource{
Name: corev1.ResourceCPU,
Target: autoscalingv2.MetricTarget{},
},
}
quantity, value, metricsType, err := parseResourceInput(o.CPU, corev1.ResourceCPU)
if err != nil {
return nil, err
}
switch metricsType {
case autoscalingv2.UtilizationMetricType:
cpuMetric.Resource.Target.Type = autoscalingv2.UtilizationMetricType
cpuMetric.Resource.Target.AverageUtilization = &value
case autoscalingv2.AverageValueMetricType:
cpuMetric.Resource.Target.Type = autoscalingv2.AverageValueMetricType
cpuMetric.Resource.Target.AverageValue = &quantity
default:
return nil, fmt.Errorf("unsupported metric type: %v", metricsType)
}
metrics = append(metrics, cpuMetric)
}
// add Memory metric if any of the memory targets are specified
if o.Memory != "" {
memoryMetric := autoscalingv2.MetricSpec{
Type: autoscalingv2.ResourceMetricSourceType,
Resource: &autoscalingv2.ResourceMetricSource{
Name: corev1.ResourceMemory,
Target: autoscalingv2.MetricTarget{},
},
}
quantity, value, metricsType, err := parseResourceInput(o.Memory, corev1.ResourceMemory)
if err != nil {
return nil, err
}
switch metricsType {
case autoscalingv2.UtilizationMetricType:
memoryMetric.Resource.Target.Type = autoscalingv2.UtilizationMetricType
memoryMetric.Resource.Target.AverageUtilization = &value
case autoscalingv2.AverageValueMetricType:
memoryMetric.Resource.Target.Type = autoscalingv2.AverageValueMetricType
memoryMetric.Resource.Target.AverageValue = &quantity
default:
return nil, fmt.Errorf("unsupported metric type: %v", metricsType)
}
metrics = append(metrics, memoryMetric)
}
// Only set Metrics if there are any defined
if len(metrics) > 0 {
scaler.Spec.Metrics = metrics
} else {
scaler.Spec.Metrics = nil
}
return &scaler, nil
}
func (o *AutoscaleOptions) createHorizontalPodAutoscalerV1(refName string, mapping *meta.RESTMapping) *autoscalingv1.HorizontalPodAutoscaler {
name := o.Name name := o.Name
if len(name) == 0 { if len(name) == 0 {
name = refName name = refName
@ -472,72 +291,3 @@ func (o *AutoscaleOptions) createHorizontalPodAutoscalerV1(refName string, mappi
return &scaler return &scaler
} }
// parseResourceInput parses a resource input string into either a utilization percentage or a quantity value.
// It supports:
// - Percentage values (e.g., "70%") for UtilizationMetricType
// - Quantity values with units (e.g., "500m", "2Gi")
// - Bare numbers without units, which are interpreted as:
// - CPU: milliCPU ("500" → "500m")
// - Memory: Mebibytes ("512" → "512Mi")
func parseResourceInput(input string, resourceType corev1.ResourceName) (apiresource.Quantity, int32, autoscalingv2.MetricTargetType, error) {
input = strings.TrimSpace(input)
if input == "" {
return apiresource.Quantity{}, 0, "", fmt.Errorf("empty input")
}
// Case 1: Handle percentage-based metrics like "70%"
percentValue, isPercent, err := parsePercentage(input)
if isPercent {
if err != nil {
return apiresource.Quantity{}, 0, "", err
}
return apiresource.Quantity{}, percentValue, autoscalingv2.UtilizationMetricType, nil
}
// Case 2: Try to interpret input as a bare number (e.g., "500"), and apply default float
valueFloat, err := strconv.ParseFloat(input, 64)
if err == nil {
unit, err := getDefaultUnitForResource(resourceType)
if err != nil {
return apiresource.Quantity{}, 0, "", err
}
inputWithUnit := fmt.Sprintf("%g%s", valueFloat, unit)
quantity, err := apiresource.ParseQuantity(inputWithUnit)
if err != nil {
return apiresource.Quantity{}, 0, "", err
}
return quantity, 0, autoscalingv2.AverageValueMetricType, nil
}
// Case 3: Parse normally if input has a valid unit (e.g., "500m", "2Gi")
quantity, err := apiresource.ParseQuantity(input)
if err != nil {
return apiresource.Quantity{}, 0, "", fmt.Errorf("invalid resource %s value: %s", resourceType, input)
}
return quantity, 0, autoscalingv2.AverageValueMetricType, nil
}
func getDefaultUnitForResource(resourceType corev1.ResourceName) (string, error) {
switch resourceType {
case corev1.ResourceCPU:
return "m", nil
case corev1.ResourceMemory:
return "Mi", nil
default:
return "", fmt.Errorf("unsupported resource type: %v", resourceType)
}
}
func parsePercentage(input string) (int32, bool, error) {
if !strings.HasSuffix(input, "%") {
return 0, false, nil
}
trimmed := strings.TrimSuffix(input, "%")
valueInt64, err := strconv.ParseInt(trimmed, 10, 32)
if err != nil || valueInt64 < 0 {
return 0, true, fmt.Errorf("invalid percentage value: %s", trimmed)
}
return int32(valueInt64), true, nil
}

File diff suppressed because it is too large Load Diff

View File

@ -33,6 +33,7 @@ import (
"k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/i18n"
"k8s.io/kubectl/pkg/util/templates" "k8s.io/kubectl/pkg/util/templates"
ct "github.com/daviddengcn/go-colortext"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -155,8 +156,12 @@ func (o *ClusterInfoOptions) Run() error {
} }
func printService(out io.Writer, name, link string) { func printService(out io.Writer, name, link string) {
ct.ChangeColor(ct.Green, false, ct.None, false)
fmt.Fprint(out, name) fmt.Fprint(out, name)
ct.ResetColor()
fmt.Fprint(out, " is running at ") fmt.Fprint(out, " is running at ")
ct.ChangeColor(ct.Yellow, false, ct.None, false)
fmt.Fprint(out, link) fmt.Fprint(out, link)
ct.ResetColor()
fmt.Fprintln(out, "") fmt.Fprintln(out, "")
} }

View File

@ -22,7 +22,6 @@ import (
"io" "io"
"os" "os"
"path" "path"
"path/filepath"
"time" "time"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -119,8 +118,8 @@ func setupOutputWriter(dir string, defaultWriter io.Writer, filename string, fil
if len(dir) == 0 || dir == "-" { if len(dir) == 0 || dir == "-" {
return defaultWriter return defaultWriter
} }
fullFile := filepath.Join(dir, filename) + fileExtension fullFile := path.Join(dir, filename) + fileExtension
parent := filepath.Dir(fullFile) parent := path.Dir(fullFile)
cmdutil.CheckErr(os.MkdirAll(parent, 0755)) cmdutil.CheckErr(os.MkdirAll(parent, 0755))
file, err := os.Create(fullFile) file, err := os.Create(fullFile)

View File

@ -18,7 +18,7 @@ package clusterinfo
import ( import (
"os" "os"
"path/filepath" "path"
"testing" "testing"
"k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/genericiooptions"
@ -60,7 +60,7 @@ func TestSetupOutputWriterFile(t *testing.T) {
if err != nil { if err != nil {
t.Errorf("unexpected error: %v", err) t.Errorf("unexpected error: %v", err)
} }
fullPath := filepath.Join(dir, file) + extension fullPath := path.Join(dir, file) + extension
defer os.RemoveAll(dir) defer os.RemoveAll(dir)
_, _, buf, _ := genericiooptions.NewTestIOStreams() _, _, buf, _ := genericiooptions.NewTestIOStreams()

View File

@ -73,7 +73,6 @@ import (
cmdutil "k8s.io/kubectl/pkg/cmd/util" cmdutil "k8s.io/kubectl/pkg/cmd/util"
"k8s.io/kubectl/pkg/cmd/version" "k8s.io/kubectl/pkg/cmd/version"
"k8s.io/kubectl/pkg/cmd/wait" "k8s.io/kubectl/pkg/cmd/wait"
"k8s.io/kubectl/pkg/kuberc"
utilcomp "k8s.io/kubectl/pkg/util/completion" utilcomp "k8s.io/kubectl/pkg/util/completion"
"k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/i18n"
"k8s.io/kubectl/pkg/util/templates" "k8s.io/kubectl/pkg/util/templates"
@ -83,6 +82,8 @@ import (
"k8s.io/kubectl/pkg/cmd/kustomize" "k8s.io/kubectl/pkg/cmd/kustomize"
) )
const kubectlCmdHeaders = "KUBECTL_COMMAND_HEADERS"
type KubectlOptions struct { type KubectlOptions struct {
PluginHandler PluginHandler PluginHandler PluginHandler
Arguments []string Arguments []string
@ -91,18 +92,15 @@ type KubectlOptions struct {
genericiooptions.IOStreams genericiooptions.IOStreams
} }
func defaultConfigFlags() *genericclioptions.ConfigFlags { var defaultConfigFlags = genericclioptions.NewConfigFlags(true).WithDeprecatedPasswordFlag().WithDiscoveryBurst(300).WithDiscoveryQPS(50.0)
return genericclioptions.NewConfigFlags(true).WithDeprecatedPasswordFlag().WithDiscoveryBurst(300).WithDiscoveryQPS(50.0)
}
// NewDefaultKubectlCommand creates the `kubectl` command with default arguments // NewDefaultKubectlCommand creates the `kubectl` command with default arguments
func NewDefaultKubectlCommand() *cobra.Command { func NewDefaultKubectlCommand() *cobra.Command {
ioStreams := genericiooptions.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr}
return NewDefaultKubectlCommandWithArgs(KubectlOptions{ return NewDefaultKubectlCommandWithArgs(KubectlOptions{
PluginHandler: NewDefaultPluginHandler(plugin.ValidPluginFilenamePrefixes), PluginHandler: NewDefaultPluginHandler(plugin.ValidPluginFilenamePrefixes),
Arguments: os.Args, Arguments: os.Args,
ConfigFlags: defaultConfigFlags().WithWarningPrinter(ioStreams), ConfigFlags: defaultConfigFlags,
IOStreams: ioStreams, IOStreams: genericiooptions.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr},
}) })
} }
@ -135,29 +133,36 @@ func NewDefaultKubectlCommandWithArgs(o KubectlOptions) *cobra.Command {
case "help", cobra.ShellCompRequestCmd, cobra.ShellCompNoDescRequestCmd: case "help", cobra.ShellCompRequestCmd, cobra.ShellCompNoDescRequestCmd:
// Don't search for a plugin // Don't search for a plugin
default: default:
if err := HandlePluginCommand(o.PluginHandler, cmdPathPieces, 1); err != nil { if err := HandlePluginCommand(o.PluginHandler, cmdPathPieces, false); err != nil {
fmt.Fprintf(o.IOStreams.ErrOut, "Error: %v\n", err) fmt.Fprintf(o.IOStreams.ErrOut, "Error: %v\n", err)
os.Exit(1) os.Exit(1)
} }
} }
} else if err == nil { } else if err == nil {
// Command exists(e.g. kubectl create), but it is not certain that if cmdutil.CmdPluginAsSubcommand.IsEnabled() {
// subcommand also exists (e.g. kubectl create networkpolicy) // Command exists(e.g. kubectl create), but it is not certain that
// we also have to eliminate kubectl create -f // subcommand also exists (e.g. kubectl create networkpolicy)
if IsSubcommandPluginAllowed(foundCmd.Name()) && len(foundArgs) >= 1 && !strings.HasPrefix(foundArgs[0], "-") { if IsSubcommandPluginAllowed(foundCmd.Name()) {
subcommand := foundArgs[0] var subcommand string
builtinSubcmdExist := false for _, arg := range foundArgs { // first "non-flag" argument as subcommand
for _, subcmd := range foundCmd.Commands() { if !strings.HasPrefix(arg, "-") {
if subcmd.Name() == subcommand { subcommand = arg
builtinSubcmdExist = true break
break }
}
builtinSubcmdExist := false
for _, subcmd := range foundCmd.Commands() {
if subcmd.Name() == subcommand {
builtinSubcmdExist = true
break
}
} }
}
if !builtinSubcmdExist { if !builtinSubcmdExist {
if err := HandlePluginCommand(o.PluginHandler, cmdPathPieces, len(cmdPathPieces)-len(foundArgs)+1); err != nil { if err := HandlePluginCommand(o.PluginHandler, cmdPathPieces, true); err != nil {
fmt.Fprintf(o.IOStreams.ErrOut, "Error: %v\n", err) fmt.Fprintf(o.IOStreams.ErrOut, "Error: %v\n", err)
os.Exit(1) os.Exit(1)
}
} }
} }
} }
@ -256,7 +261,7 @@ func (h *DefaultPluginHandler) Execute(executablePath string, cmdArgs, environme
// HandlePluginCommand receives a pluginHandler and command-line arguments and attempts to find // HandlePluginCommand receives a pluginHandler and command-line arguments and attempts to find
// a plugin executable on the PATH that satisfies the given arguments. // a plugin executable on the PATH that satisfies the given arguments.
func HandlePluginCommand(pluginHandler PluginHandler, cmdArgs []string, minArgs int) error { func HandlePluginCommand(pluginHandler PluginHandler, cmdArgs []string, exactMatch bool) error {
var remainingArgs []string // all "non-flag" arguments var remainingArgs []string // all "non-flag" arguments
for _, arg := range cmdArgs { for _, arg := range cmdArgs {
if strings.HasPrefix(arg, "-") { if strings.HasPrefix(arg, "-") {
@ -276,14 +281,13 @@ func HandlePluginCommand(pluginHandler PluginHandler, cmdArgs []string, minArgs
for len(remainingArgs) > 0 { for len(remainingArgs) > 0 {
path, found := pluginHandler.Lookup(strings.Join(remainingArgs, "-")) path, found := pluginHandler.Lookup(strings.Join(remainingArgs, "-"))
if !found { if !found {
remainingArgs = remainingArgs[:len(remainingArgs)-1] if exactMatch {
if len(remainingArgs) < minArgs { // if exactMatch is true, we shouldn't continue searching with shorter names.
// we shouldn't continue searching with shorter names.
// this is especially for not searching kubectl-create plugin // this is especially for not searching kubectl-create plugin
// when kubectl-create-foo plugin is not found. // when kubectl-create-foo plugin is not found.
break break
} }
remainingArgs = remainingArgs[:len(remainingArgs)-1]
continue continue
} }
@ -358,14 +362,9 @@ func NewKubectlCommand(o KubectlOptions) *cobra.Command {
flags.BoolVar(&warningsAsErrors, "warnings-as-errors", warningsAsErrors, "Treat warnings received from the server as errors and exit with a non-zero exit code") flags.BoolVar(&warningsAsErrors, "warnings-as-errors", warningsAsErrors, "Treat warnings received from the server as errors and exit with a non-zero exit code")
pref := kuberc.NewPreferences()
if !cmdutil.KubeRC.IsDisabled() {
pref.AddFlags(flags)
}
kubeConfigFlags := o.ConfigFlags kubeConfigFlags := o.ConfigFlags
if kubeConfigFlags == nil { if kubeConfigFlags == nil {
kubeConfigFlags = defaultConfigFlags().WithWarningPrinter(o.IOStreams) kubeConfigFlags = defaultConfigFlags
} }
kubeConfigFlags.AddFlags(flags) kubeConfigFlags.AddFlags(flags)
matchVersionKubeConfigFlags := cmdutil.NewMatchVersionFlags(kubeConfigFlags) matchVersionKubeConfigFlags := cmdutil.NewMatchVersionFlags(kubeConfigFlags)
@ -385,8 +384,6 @@ func NewKubectlCommand(o KubectlOptions) *cobra.Command {
// Avoid import cycle by setting ValidArgsFunction here instead of in NewCmdGet() // Avoid import cycle by setting ValidArgsFunction here instead of in NewCmdGet()
getCmd := get.NewCmdGet("kubectl", f, o.IOStreams) getCmd := get.NewCmdGet("kubectl", f, o.IOStreams)
getCmd.ValidArgsFunction = utilcomp.ResourceTypeAndNameCompletionFunc(f) getCmd.ValidArgsFunction = utilcomp.ResourceTypeAndNameCompletionFunc(f)
debugCmd := debug.NewCmdDebug(f, o.IOStreams)
debugCmd.ValidArgsFunction = utilcomp.ResourceTypeAndNameCompletionFunc(f)
groups := templates.CommandGroups{ groups := templates.CommandGroups{
{ {
@ -438,7 +435,7 @@ func NewKubectlCommand(o KubectlOptions) *cobra.Command {
proxyCmd, proxyCmd,
cp.NewCmdCp(f, o.IOStreams), cp.NewCmdCp(f, o.IOStreams),
auth.NewCmdAuth(f, o.IOStreams), auth.NewCmdAuth(f, o.IOStreams),
debugCmd, debug.NewCmdDebug(f, o.IOStreams),
events.NewCmdEvents(f, o.IOStreams), events.NewCmdEvents(f, o.IOStreams),
}, },
}, },
@ -472,19 +469,13 @@ func NewKubectlCommand(o KubectlOptions) *cobra.Command {
filters = append(filters, alpha.Name()) filters = append(filters, alpha.Name())
} }
// Add plugin command group to the list of command groups.
// The commands are only injected for the scope of showing help and completion, they are not
// invoked directly.
pluginCommandGroup := plugin.GetPluginCommandGroup(cmds)
groups = append(groups, pluginCommandGroup)
templates.ActsAsRootCommand(cmds, filters, groups...) templates.ActsAsRootCommand(cmds, filters, groups...)
utilcomp.SetFactoryForCompletion(f) utilcomp.SetFactoryForCompletion(f)
registerCompletionFuncForGlobalFlags(cmds, f) registerCompletionFuncForGlobalFlags(cmds, f)
cmds.AddCommand(alpha) cmds.AddCommand(alpha)
cmds.AddCommand(cmdconfig.NewCmdConfig(f, clientcmd.NewDefaultPathOptions(), o.IOStreams)) cmds.AddCommand(cmdconfig.NewCmdConfig(clientcmd.NewDefaultPathOptions(), o.IOStreams))
cmds.AddCommand(plugin.NewCmdPlugin(o.IOStreams)) cmds.AddCommand(plugin.NewCmdPlugin(o.IOStreams))
cmds.AddCommand(version.NewCmdVersion(f, o.IOStreams)) cmds.AddCommand(version.NewCmdVersion(f, o.IOStreams))
cmds.AddCommand(apiresources.NewCmdAPIVersions(f, o.IOStreams)) cmds.AddCommand(apiresources.NewCmdAPIVersions(f, o.IOStreams))
@ -494,15 +485,6 @@ func NewKubectlCommand(o KubectlOptions) *cobra.Command {
// Stop warning about normalization of flags. That makes it possible to // Stop warning about normalization of flags. That makes it possible to
// add the klog flags later. // add the klog flags later.
cmds.SetGlobalNormalizationFunc(cliflag.WordSepNormalizeFunc) cmds.SetGlobalNormalizationFunc(cliflag.WordSepNormalizeFunc)
if !cmdutil.KubeRC.IsDisabled() {
_, err := pref.Apply(cmds, o.Arguments, o.IOStreams.ErrOut)
if err != nil {
fmt.Fprintf(o.IOStreams.ErrOut, "error occurred while applying preferences %v\n", err)
os.Exit(1)
}
}
return cmds return cmds
} }
@ -520,9 +502,12 @@ func NewKubectlCommand(o KubectlOptions) *cobra.Command {
// //
// https://github.com/kubernetes/enhancements/tree/master/keps/sig-cli/859-kubectl-headers // https://github.com/kubernetes/enhancements/tree/master/keps/sig-cli/859-kubectl-headers
func addCmdHeaderHooks(cmds *cobra.Command, kubeConfigFlags *genericclioptions.ConfigFlags) { func addCmdHeaderHooks(cmds *cobra.Command, kubeConfigFlags *genericclioptions.ConfigFlags) {
if cmdutil.CmdHeaders.IsDisabled() { // If the feature gate env var is set to "false", then do no add kubectl command headers.
klog.V(5).Infoln("kubectl command headers turned off") if value, exists := os.LookupEnv(kubectlCmdHeaders); exists {
return if value == "false" || value == "0" {
klog.V(5).Infoln("kubectl command headers turned off")
return
}
} }
klog.V(5).Infoln("kubectl command headers turned on") klog.V(5).Infoln("kubectl command headers turned on")
crt := &genericclioptions.CommandHeaderRoundTripper{} crt := &genericclioptions.CommandHeaderRoundTripper{}
@ -576,29 +561,3 @@ func registerCompletionFuncForGlobalFlags(cmd *cobra.Command, f cmdutil.Factory)
return utilcomp.ListUsersInConfig(toComplete), cobra.ShellCompDirectiveNoFileComp return utilcomp.ListUsersInConfig(toComplete), cobra.ShellCompDirectiveNoFileComp
})) }))
} }
// GetLogVerbosity parses the provided command-line arguments to determine
// the verbosity level for logging. Returns string representing the verbosity
// level, or 0 if no verbosity flag is specified.
func GetLogVerbosity(args []string) string {
for i, arg := range args {
if arg == "--" {
// flags after "--" does not represent any flag of
// the command. We should short cut the iteration in here.
break
}
if arg == "--v" || arg == "-v" {
if i+1 < len(args) {
return args[i+1]
}
} else if strings.Contains(arg, "--v=") || strings.Contains(arg, "-v=") {
parg := strings.Split(arg, "=")
if len(parg) > 1 && parg[1] != "" {
return parg[1]
}
}
}
return "0"
}

View File

@ -29,6 +29,7 @@ import (
"k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/genericiooptions"
"k8s.io/kubectl/pkg/cmd/plugin" "k8s.io/kubectl/pkg/cmd/plugin"
cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
cmdutil "k8s.io/kubectl/pkg/cmd/util" cmdutil "k8s.io/kubectl/pkg/cmd/util"
) )
@ -128,45 +129,47 @@ func TestKubectlSubcommandShadowPlugin(t *testing.T) {
} }
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { cmdtesting.WithAlphaEnvs([]cmdutil.FeatureGate{cmdutil.CmdPluginAsSubcommand}, t, func(t *testing.T) {
pluginsHandler := &testPluginHandler{ t.Run(test.name, func(t *testing.T) {
pluginsDirectory: "plugin/testdata", pluginsHandler := &testPluginHandler{
validPrefixes: plugin.ValidPluginFilenamePrefixes, pluginsDirectory: "plugin/testdata",
} validPrefixes: plugin.ValidPluginFilenamePrefixes,
ioStreams, _, _, _ := genericiooptions.NewTestIOStreams() }
root := NewDefaultKubectlCommandWithArgs(KubectlOptions{PluginHandler: pluginsHandler, Arguments: test.args, IOStreams: ioStreams}) ioStreams, _, _, _ := genericiooptions.NewTestIOStreams()
// original plugin handler (DefaultPluginHandler) is implemented by exec call so no additional actions are expected on the cobra command if we activate the plugin flow root := NewDefaultKubectlCommandWithArgs(KubectlOptions{PluginHandler: pluginsHandler, Arguments: test.args, IOStreams: ioStreams})
if !pluginsHandler.lookedup && !pluginsHandler.executed { // original plugin handler (DefaultPluginHandler) is implemented by exec call so no additional actions are expected on the cobra command if we activate the plugin flow
// args must be set, otherwise Execute will use os.Args (args used for starting the test) and test.args would not be passed if !pluginsHandler.lookedup && !pluginsHandler.executed {
// to the command which might invoke only "kubectl" without any additional args and give false positives // args must be set, otherwise Execute will use os.Args (args used for starting the test) and test.args would not be passed
root.SetArgs(test.args[1:]) // to the command which might invoke only "kubectl" without any additional args and give false positives
// Important note! Incorrect command or command failing validation might just call os.Exit(1) which would interrupt execution of the test root.SetArgs(test.args[1:])
if err := root.Execute(); err != nil { // Important note! Incorrect command or command failing validation might just call os.Exit(1) which would interrupt execution of the test
t.Fatalf("unexpected error: %v", err) if err := root.Execute(); err != nil {
t.Fatalf("unexpected error: %v", err)
}
} }
}
if (pluginsHandler.lookupErr != nil && pluginsHandler.lookupErr.Error() != test.expectLookupError) || if (pluginsHandler.lookupErr != nil && pluginsHandler.lookupErr.Error() != test.expectLookupError) ||
(pluginsHandler.lookupErr == nil && len(test.expectLookupError) > 0) { (pluginsHandler.lookupErr == nil && len(test.expectLookupError) > 0) {
t.Fatalf("unexpected error: expected %q to occur, but got %q", test.expectLookupError, pluginsHandler.lookupErr) t.Fatalf("unexpected error: expected %q to occur, but got %q", test.expectLookupError, pluginsHandler.lookupErr)
} }
if pluginsHandler.lookedup && !pluginsHandler.executed && len(test.expectLookupError) == 0 { if pluginsHandler.lookedup && !pluginsHandler.executed && len(test.expectLookupError) == 0 {
// we have to fail here, because we have found the plugin, but not executed the plugin, nor the command (this would normally result in an error: unknown command) // we have to fail here, because we have found the plugin, but not executed the plugin, nor the command (this would normally result in an error: unknown command)
t.Fatalf("expected plugin execution, but did not occur") t.Fatalf("expected plugin execution, but did not occur")
} }
if pluginsHandler.executedPlugin != test.expectPlugin { if pluginsHandler.executedPlugin != test.expectPlugin {
t.Fatalf("unexpected plugin execution: expected %q, got %q", test.expectPlugin, pluginsHandler.executedPlugin) t.Fatalf("unexpected plugin execution: expected %q, got %q", test.expectPlugin, pluginsHandler.executedPlugin)
} }
if pluginsHandler.executed && len(test.expectPlugin) == 0 { if pluginsHandler.executed && len(test.expectPlugin) == 0 {
t.Fatalf("unexpected plugin execution: expected no plugin, got %q", pluginsHandler.executedPlugin) t.Fatalf("unexpected plugin execution: expected no plugin, got %q", pluginsHandler.executedPlugin)
} }
if !cmp.Equal(pluginsHandler.withArgs, test.expectPluginArgs, cmpopts.EquateEmpty()) { if !cmp.Equal(pluginsHandler.withArgs, test.expectPluginArgs, cmpopts.EquateEmpty()) {
t.Fatalf("unexpected plugin execution args: expected %q, got %q", test.expectPluginArgs, pluginsHandler.withArgs) t.Fatalf("unexpected plugin execution args: expected %q, got %q", test.expectPluginArgs, pluginsHandler.withArgs)
} }
})
}) })
} }
} }
@ -197,18 +200,6 @@ func TestKubectlCommandHandlesPlugins(t *testing.T) {
expectPlugin: "plugin/testdata/kubectl-foo", expectPlugin: "plugin/testdata/kubectl-foo",
expectPluginArgs: []string{"--bar"}, expectPluginArgs: []string{"--bar"},
}, },
{
name: "test that a plugin executable is found based on command args with positional argument",
args: []string{"kubectl", "foo", "positional", "--bar"},
expectPlugin: "plugin/testdata/kubectl-foo",
expectPluginArgs: []string{"positional", "--bar"},
},
{
name: "test that an allowed subcommand plugin executable is found based on command args with positional argument",
args: []string{"kubectl", "create", "foo", "positional", "--bar"},
expectPlugin: "plugin/testdata/kubectl-create-foo",
expectPluginArgs: []string{"positional", "--bar"},
},
{ {
name: "test that a plugin does not execute over an existing command by the same name", name: "test that a plugin does not execute over an existing command by the same name",
args: []string{"kubectl", "version", "--client=true"}, args: []string{"kubectl", "version", "--client=true"},
@ -343,7 +334,6 @@ func (h *testPluginHandler) Lookup(filename string) (string, bool) {
for _, p := range plugins { for _, p := range plugins {
filenameWithSuportedPrefix = fmt.Sprintf("%s-%s", prefix, filename) filenameWithSuportedPrefix = fmt.Sprintf("%s-%s", prefix, filename)
if p.Name() == filenameWithSuportedPrefix { if p.Name() == filenameWithSuportedPrefix {
h.lookupErr = nil
return fmt.Sprintf("%s/%s", h.pluginsDirectory, p.Name()), true return fmt.Sprintf("%s/%s", h.pluginsDirectory, p.Name()), true
} }
} }
@ -382,6 +372,10 @@ func TestKubectlCommandHeadersHooks(t *testing.T) {
envVar: "false", envVar: "false",
addsHooks: false, addsHooks: false,
}, },
"zero env var value; hooks NOT added": {
envVar: "0",
addsHooks: false,
},
} }
for name, testCase := range tests { for name, testCase := range tests {
@ -391,7 +385,7 @@ func TestKubectlCommandHeadersHooks(t *testing.T) {
if kubeConfigFlags.WrapConfigFn != nil { if kubeConfigFlags.WrapConfigFn != nil {
t.Fatal("expected initial nil WrapConfigFn") t.Fatal("expected initial nil WrapConfigFn")
} }
t.Setenv(string(cmdutil.CmdHeaders), testCase.envVar) t.Setenv(kubectlCmdHeaders, testCase.envVar)
addCmdHeaderHooks(cmds, kubeConfigFlags) addCmdHeaderHooks(cmds, kubeConfigFlags)
// Valdidate whether the hooks were added. // Valdidate whether the hooks were added.
if testCase.addsHooks && kubeConfigFlags.WrapConfigFn == nil { if testCase.addsHooks && kubeConfigFlags.WrapConfigFn == nil {

View File

@ -23,7 +23,6 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/genericiooptions"
"k8s.io/client-go/tools/clientcmd" "k8s.io/client-go/tools/clientcmd"
cmdutil "k8s.io/kubectl/pkg/cmd/util" cmdutil "k8s.io/kubectl/pkg/cmd/util"
@ -32,7 +31,7 @@ import (
) )
// NewCmdConfig creates a command object for the "config" action, and adds all child commands to it. // NewCmdConfig creates a command object for the "config" action, and adds all child commands to it.
func NewCmdConfig(restClientGetter genericclioptions.RESTClientGetter, pathOptions *clientcmd.PathOptions, streams genericiooptions.IOStreams) *cobra.Command { func NewCmdConfig(pathOptions *clientcmd.PathOptions, streams genericiooptions.IOStreams) *cobra.Command {
if len(pathOptions.ExplicitFileFlag) == 0 { if len(pathOptions.ExplicitFileFlag) == 0 {
pathOptions.ExplicitFileFlag = clientcmd.RecommendedConfigPathFlag pathOptions.ExplicitFileFlag = clientcmd.RecommendedConfigPathFlag
} }
@ -59,7 +58,7 @@ func NewCmdConfig(restClientGetter genericclioptions.RESTClientGetter, pathOptio
cmd.AddCommand(NewCmdConfigView(streams, pathOptions)) cmd.AddCommand(NewCmdConfigView(streams, pathOptions))
cmd.AddCommand(NewCmdConfigSetCluster(streams.Out, pathOptions)) cmd.AddCommand(NewCmdConfigSetCluster(streams.Out, pathOptions))
cmd.AddCommand(NewCmdConfigSetCredentials(streams.Out, pathOptions)) cmd.AddCommand(NewCmdConfigSetCredentials(streams.Out, pathOptions))
cmd.AddCommand(NewCmdConfigSetContext(restClientGetter, streams.Out, pathOptions)) cmd.AddCommand(NewCmdConfigSetContext(streams.Out, pathOptions))
cmd.AddCommand(NewCmdConfigSet(streams.Out, pathOptions)) cmd.AddCommand(NewCmdConfigSet(streams.Out, pathOptions))
cmd.AddCommand(NewCmdConfigUnset(streams.Out, pathOptions)) cmd.AddCommand(NewCmdConfigUnset(streams.Out, pathOptions))
cmd.AddCommand(NewCmdConfigCurrentContext(streams.Out, pathOptions)) cmd.AddCommand(NewCmdConfigCurrentContext(streams.Out, pathOptions))

View File

@ -19,7 +19,7 @@ package config
import ( import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path"
"reflect" "reflect"
"strings" "strings"
"testing" "testing"
@ -30,7 +30,6 @@ import (
"k8s.io/client-go/tools/clientcmd" "k8s.io/client-go/tools/clientcmd"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api" clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
utiltesting "k8s.io/client-go/util/testing" utiltesting "k8s.io/client-go/util/testing"
cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
cmdutil "k8s.io/kubectl/pkg/cmd/util" cmdutil "k8s.io/kubectl/pkg/cmd/util"
) )
@ -69,6 +68,7 @@ func Example_view() {
// name: federal-context // name: federal-context
// current-context: federal-context // current-context: federal-context
// kind: Config // kind: Config
// preferences: {}
// users: // users:
// - name: red-user // - name: red-user
// user: // user:
@ -279,91 +279,6 @@ func TestEmbedClientCert(t *testing.T) {
test.run(t) test.run(t)
} }
func TestExecPlugin(t *testing.T) {
fakeCertFile, _ := os.CreateTemp(os.TempDir(), "")
defer utiltesting.CloseAndRemove(t, fakeCertFile)
fakeData := []byte("fake-data")
err := os.WriteFile(fakeCertFile.Name(), fakeData, 0600)
if err != nil {
t.Errorf("unexpected error %v", err)
}
expectedConfig := newRedFederalCowHammerConfig()
authInfo := clientcmdapi.NewAuthInfo()
authInfo.Exec = &clientcmdapi.ExecConfig{
Command: "example-client-go-exec-plugin",
Args: []string{"arg1", "arg2"},
Env: []clientcmdapi.ExecEnvVar{
{
Name: "FOO",
Value: "bar",
},
},
APIVersion: "client.authentication.k8s.io/v1",
ProvideClusterInfo: false,
InteractiveMode: "Never",
}
expectedConfig.AuthInfos["cred-exec-user"] = authInfo
test := configCommandTest{
args: []string{
"set-credentials",
"cred-exec-user",
"--exec-api-version=client.authentication.k8s.io/v1",
"--exec-command=example-client-go-exec-plugin",
"--exec-arg=arg1,arg2",
"--exec-env=FOO=bar",
"--exec-interactive-mode=Never",
},
startingConfig: newRedFederalCowHammerConfig(),
expectedConfig: expectedConfig,
}
test.run(t)
}
func TestExecPluginWithProveClusterInfo(t *testing.T) {
fakeCertFile, _ := os.CreateTemp(os.TempDir(), "")
defer utiltesting.CloseAndRemove(t, fakeCertFile)
fakeData := []byte("fake-data")
err := os.WriteFile(fakeCertFile.Name(), fakeData, 0600)
if err != nil {
t.Errorf("unexpected error %v", err)
}
expectedConfig := newRedFederalCowHammerConfig()
authInfo := clientcmdapi.NewAuthInfo()
authInfo.Exec = &clientcmdapi.ExecConfig{
Command: "example-client-go-exec-plugin",
Args: []string{"arg1", "arg2"},
Env: []clientcmdapi.ExecEnvVar{
{
Name: "FOO",
Value: "bar",
},
},
APIVersion: "client.authentication.k8s.io/v1",
ProvideClusterInfo: true,
InteractiveMode: "Always",
}
expectedConfig.AuthInfos["cred-exec-user"] = authInfo
test := configCommandTest{
args: []string{
"set-credentials",
"cred-exec-user",
"--exec-api-version=client.authentication.k8s.io/v1",
"--exec-command=example-client-go-exec-plugin",
"--exec-arg=arg1,arg2",
"--exec-env=FOO=bar",
"--exec-interactive-mode=Always",
"--exec-provide-cluster-info=true",
},
startingConfig: newRedFederalCowHammerConfig(),
expectedConfig: expectedConfig,
}
test.run(t)
}
func TestEmbedClientKey(t *testing.T) { func TestEmbedClientKey(t *testing.T) {
fakeKeyFile, _ := os.CreateTemp(os.TempDir(), "") fakeKeyFile, _ := os.CreateTemp(os.TempDir(), "")
defer utiltesting.CloseAndRemove(t, fakeKeyFile) defer utiltesting.CloseAndRemove(t, fakeKeyFile)
@ -415,7 +330,7 @@ func TestEmptyTokenAndCertAllowed(t *testing.T) {
defer utiltesting.CloseAndRemove(t, fakeCertFile) defer utiltesting.CloseAndRemove(t, fakeCertFile)
expectedConfig := newRedFederalCowHammerConfig() expectedConfig := newRedFederalCowHammerConfig()
authInfo := clientcmdapi.NewAuthInfo() authInfo := clientcmdapi.NewAuthInfo()
authInfo.ClientCertificate = filepath.Base(fakeCertFile.Name()) authInfo.ClientCertificate = path.Base(fakeCertFile.Name())
expectedConfig.AuthInfos["another-user"] = authInfo expectedConfig.AuthInfos["another-user"] = authInfo
test := configCommandTest{ test := configCommandTest{
@ -660,7 +575,7 @@ func TestCAClearsInsecure(t *testing.T) {
clusterInfoWithInsecure.InsecureSkipTLSVerify = true clusterInfoWithInsecure.InsecureSkipTLSVerify = true
clusterInfoWithCA := clientcmdapi.NewCluster() clusterInfoWithCA := clientcmdapi.NewCluster()
clusterInfoWithCA.CertificateAuthority = filepath.Base(fakeCAFile.Name()) clusterInfoWithCA.CertificateAuthority = path.Base(fakeCAFile.Name())
startingConfig := newRedFederalCowHammerConfig() startingConfig := newRedFederalCowHammerConfig()
startingConfig.Clusters["another-cluster"] = clusterInfoWithInsecure startingConfig.Clusters["another-cluster"] = clusterInfoWithInsecure
@ -948,11 +863,8 @@ func testConfigCommand(args []string, startingConfig clientcmdapi.Config, t *tes
argsToUse = append(argsToUse, "--kubeconfig="+fakeKubeFile.Name()) argsToUse = append(argsToUse, "--kubeconfig="+fakeKubeFile.Name())
argsToUse = append(argsToUse, args...) argsToUse = append(argsToUse, args...)
tf := cmdtesting.NewTestFactory().WithNamespace("test")
defer tf.Cleanup()
streams, _, buf, _ := genericiooptions.NewTestIOStreams() streams, _, buf, _ := genericiooptions.NewTestIOStreams()
cmd := NewCmdConfig(tf, clientcmd.NewDefaultPathOptions(), streams) cmd := NewCmdConfig(clientcmd.NewDefaultPathOptions(), streams)
// "context" is a global flag, inherited from base kubectl command in the real world // "context" is a global flag, inherited from base kubectl command in the real world
cmd.PersistentFlags().String("context", "", "The name of the kubeconfig context to use") cmd.PersistentFlags().String("context", "", "The name of the kubeconfig context to use")
cmd.SetArgs(argsToUse) cmd.SetArgs(argsToUse)

View File

@ -89,7 +89,7 @@ func NewCmdConfigGetContexts(streams genericiooptions.IOStreams, configAccess cl
// Complete assigns GetContextsOptions from the args. // Complete assigns GetContextsOptions from the args.
func (o *GetContextsOptions) Complete(cmd *cobra.Command, args []string) error { func (o *GetContextsOptions) Complete(cmd *cobra.Command, args []string) error {
supportedOutputTypes := sets.New[string]("", "name") supportedOutputTypes := sets.NewString("", "name")
if !supportedOutputTypes.Has(o.outputFormat) { if !supportedOutputTypes.Has(o.outputFormat) {
return fmt.Errorf("--output %v is not available in kubectl config get-contexts; resetting to default output format", o.outputFormat) return fmt.Errorf("--output %v is not available in kubectl config get-contexts; resetting to default output format", o.outputFormat)
} }

View File

@ -55,7 +55,7 @@ func newNavigationSteps(path string) (*navigationSteps, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
nextPart := findNameStep(individualParts[currPartIndex:], sets.KeySet(mapValueOptions)) nextPart := findNameStep(individualParts[currPartIndex:], sets.StringKeySet(mapValueOptions))
steps = append(steps, navigationStep{nextPart, mapValueType}) steps = append(steps, navigationStep{nextPart, mapValueType})
currPartIndex += len(strings.Split(nextPart, ".")) currPartIndex += len(strings.Split(nextPart, "."))
@ -98,7 +98,7 @@ func (s *navigationSteps) moreStepsRemaining() bool {
// findNameStep takes the list of parts and a set of valid tags that can be used after the name. It then walks the list of parts // findNameStep takes the list of parts and a set of valid tags that can be used after the name. It then walks the list of parts
// until it find a valid "next" tag or until it reaches the end of the parts and then builds the name back up out of the individual parts // until it find a valid "next" tag or until it reaches the end of the parts and then builds the name back up out of the individual parts
func findNameStep(parts []string, typeOptions sets.Set[string]) string { func findNameStep(parts []string, typeOptions sets.String) string {
if len(parts) == 0 { if len(parts) == 0 {
return "" return ""
} }
@ -136,7 +136,7 @@ func getPotentialTypeValues(typeValue reflect.Type) (map[string]reflect.Type, er
return ret, nil return ret, nil
} }
func findKnownValue(parts []string, valueOptions sets.Set[string]) int { func findKnownValue(parts []string, valueOptions sets.String) int {
for i := range parts { for i := range parts {
if valueOptions.Has(parts[i]) { if valueOptions.Has(parts[i]) {
return i return i

View File

@ -23,7 +23,6 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/client-go/tools/clientcmd" "k8s.io/client-go/tools/clientcmd"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api" clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
cliflag "k8s.io/component-base/cli/flag" cliflag "k8s.io/component-base/cli/flag"
@ -54,7 +53,7 @@ var (
) )
// NewCmdConfigSetContext returns a Command instance for 'config set-context' sub command // NewCmdConfigSetContext returns a Command instance for 'config set-context' sub command
func NewCmdConfigSetContext(restClientGetter genericclioptions.RESTClientGetter, out io.Writer, configAccess clientcmd.ConfigAccess) *cobra.Command { func NewCmdConfigSetContext(out io.Writer, configAccess clientcmd.ConfigAccess) *cobra.Command {
options := &setContextOptions{configAccess: configAccess} options := &setContextOptions{configAccess: configAccess}
cmd := &cobra.Command{ cmd := &cobra.Command{
@ -80,12 +79,6 @@ func NewCmdConfigSetContext(restClientGetter genericclioptions.RESTClientGetter,
cmd.Flags().Var(&options.cluster, clientcmd.FlagClusterName, clientcmd.FlagClusterName+" for the context entry in kubeconfig") cmd.Flags().Var(&options.cluster, clientcmd.FlagClusterName, clientcmd.FlagClusterName+" for the context entry in kubeconfig")
cmd.Flags().Var(&options.authInfo, clientcmd.FlagAuthInfoName, clientcmd.FlagAuthInfoName+" for the context entry in kubeconfig") cmd.Flags().Var(&options.authInfo, clientcmd.FlagAuthInfoName, clientcmd.FlagAuthInfoName+" for the context entry in kubeconfig")
cmd.Flags().Var(&options.namespace, clientcmd.FlagNamespace, clientcmd.FlagNamespace+" for the context entry in kubeconfig") cmd.Flags().Var(&options.namespace, clientcmd.FlagNamespace, clientcmd.FlagNamespace+" for the context entry in kubeconfig")
cmdutil.CheckErr(cmd.RegisterFlagCompletionFunc(
"namespace",
func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return completion.CompGetResource(cmdutil.NewFactory(restClientGetter), "namespace", toComplete), cobra.ShellCompDirectiveNoFileComp
},
))
return cmd return cmd
} }

View File

@ -21,10 +21,10 @@ import (
"os" "os"
"testing" "testing"
utiltesting "k8s.io/client-go/util/testing"
"k8s.io/client-go/tools/clientcmd" "k8s.io/client-go/tools/clientcmd"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api" clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
utiltesting "k8s.io/client-go/util/testing"
cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
) )
type setContextTest struct { type setContextTest struct {
@ -122,11 +122,7 @@ func (test setContextTest) run(t *testing.T) {
pathOptions.GlobalFile = fakeKubeFile.Name() pathOptions.GlobalFile = fakeKubeFile.Name()
pathOptions.EnvVar = "" pathOptions.EnvVar = ""
buf := bytes.NewBuffer([]byte{}) buf := bytes.NewBuffer([]byte{})
cmd := NewCmdConfigSetContext(buf, pathOptions)
tf := cmdtesting.NewTestFactory().WithNamespace("test")
defer tf.Cleanup()
cmd := NewCmdConfigSetContext(tf, buf, pathOptions)
cmd.SetArgs(test.args) cmd.SetArgs(test.args)
cmd.Flags().Parse(test.flags) cmd.Flags().Parse(test.flags)
if err := cmd.Execute(); err != nil { if err := cmd.Execute(); err != nil {

View File

@ -48,25 +48,21 @@ type setCredentialsOptions struct {
authProviderArgs map[string]string authProviderArgs map[string]string
authProviderArgsToRemove []string authProviderArgsToRemove []string
execCommand cliflag.StringFlag execCommand cliflag.StringFlag
execAPIVersion cliflag.StringFlag execAPIVersion cliflag.StringFlag
execInteractiveMode cliflag.StringFlag execArgs []string
execProvideClusterInfo cliflag.Tristate execEnv map[string]string
execArgs []string execEnvToRemove []string
execEnv map[string]string
execEnvToRemove []string
} }
const ( const (
flagAuthProvider = "auth-provider" flagAuthProvider = "auth-provider"
flagAuthProviderArg = "auth-provider-arg" flagAuthProviderArg = "auth-provider-arg"
flagExecCommand = "exec-command" flagExecCommand = "exec-command"
flagExecAPIVersion = "exec-api-version" flagExecAPIVersion = "exec-api-version"
flagExecArg = "exec-arg" flagExecArg = "exec-arg"
flagExecEnv = "exec-env" flagExecEnv = "exec-env"
flagExecInteractiveMode = "exec-interactive-mode"
flagExecProvideClusterInfo = "exec-provide-cluster-info"
) )
var ( var (
@ -109,9 +105,6 @@ var (
# Enable new exec auth plugin for the "cluster-admin" entry # Enable new exec auth plugin for the "cluster-admin" entry
kubectl config set-credentials cluster-admin --exec-command=/path/to/the/executable --exec-api-version=client.authentication.k8s.io/v1beta1 kubectl config set-credentials cluster-admin --exec-command=/path/to/the/executable --exec-api-version=client.authentication.k8s.io/v1beta1
# Enable new exec auth plugin for the "cluster-admin" entry with interactive mode
kubectl config set-credentials cluster-admin --exec-command=/path/to/the/executable --exec-api-version=client.authentication.k8s.io/v1beta1 --exec-interactive-mode=Never
# Define new exec auth plugin arguments for the "cluster-admin" entry # Define new exec auth plugin arguments for the "cluster-admin" entry
kubectl config set-credentials cluster-admin --exec-arg=arg1 --exec-arg=arg2 kubectl config set-credentials cluster-admin --exec-arg=arg1 --exec-arg=arg2
@ -186,9 +179,6 @@ func newCmdConfigSetCredentials(out io.Writer, options *setCredentialsOptions) *
cmd.Flags().StringSlice(flagAuthProviderArg, nil, "'key=value' arguments for the auth provider") cmd.Flags().StringSlice(flagAuthProviderArg, nil, "'key=value' arguments for the auth provider")
cmd.Flags().Var(&options.execCommand, flagExecCommand, "Command for the exec credential plugin for the user entry in kubeconfig") cmd.Flags().Var(&options.execCommand, flagExecCommand, "Command for the exec credential plugin for the user entry in kubeconfig")
cmd.Flags().Var(&options.execAPIVersion, flagExecAPIVersion, "API version of the exec credential plugin for the user entry in kubeconfig") cmd.Flags().Var(&options.execAPIVersion, flagExecAPIVersion, "API version of the exec credential plugin for the user entry in kubeconfig")
cmd.Flags().Var(&options.execInteractiveMode, flagExecInteractiveMode, "InteractiveMode of the exec credentials plugin for the user entry in kubeconfig")
flagClusterInfo := cmd.Flags().VarPF(&options.execProvideClusterInfo, flagExecProvideClusterInfo, "", "ProvideClusterInfo of the exec credentials plugin for the user entry in kubeconfig")
flagClusterInfo.NoOptDefVal = "true"
cmd.Flags().StringSlice(flagExecArg, nil, "New arguments for the exec credential plugin command for the user entry in kubeconfig") cmd.Flags().StringSlice(flagExecArg, nil, "New arguments for the exec credential plugin command for the user entry in kubeconfig")
cmd.Flags().StringArray(flagExecEnv, nil, "'key=value' environment values for the exec credential plugin") cmd.Flags().StringArray(flagExecEnv, nil, "'key=value' environment values for the exec credential plugin")
f := cmd.Flags().VarPF(&options.embedCertData, clientcmd.FlagEmbedCerts, "", "Embed client cert/key for the user entry in kubeconfig") f := cmd.Flags().VarPF(&options.embedCertData, clientcmd.FlagEmbedCerts, "", "Embed client cert/key for the user entry in kubeconfig")
@ -316,14 +306,6 @@ func (o *setCredentialsOptions) modifyAuthInfo(existingAuthInfo clientcmdapi.Aut
modifiedAuthInfo.Exec.Args = o.execArgs modifiedAuthInfo.Exec.Args = o.execArgs
} }
if o.execInteractiveMode.Provided() {
modifiedAuthInfo.Exec.InteractiveMode = clientcmdapi.ExecInteractiveMode(o.execInteractiveMode.Value())
}
if o.execProvideClusterInfo.Provided() {
modifiedAuthInfo.Exec.ProvideClusterInfo = o.execProvideClusterInfo.Value()
}
// iterate over the existing exec env values and remove the specified // iterate over the existing exec env values and remove the specified
if o.execEnvToRemove != nil { if o.execEnvToRemove != nil {
newExecEnv := []clientcmdapi.ExecEnvVar{} newExecEnv := []clientcmdapi.ExecEnvVar{}
@ -455,14 +437,5 @@ func (o setCredentialsOptions) validate() error {
} }
} }
if o.execInteractiveMode.Provided() {
interactiveMode := o.execInteractiveMode.Value()
if interactiveMode != string(clientcmdapi.IfAvailableExecInteractiveMode) &&
interactiveMode != string(clientcmdapi.AlwaysExecInteractiveMode) &&
interactiveMode != string(clientcmdapi.NeverExecInteractiveMode) {
return fmt.Errorf("invalid interactive mode type, can be only IfAvailable, Never, Always")
}
}
return nil return nil
} }

View File

@ -85,6 +85,7 @@ contexts:
name: my-cluster name: my-cluster
current-context: minikube current-context: minikube
kind: Config kind: Config
preferences: {}
users: users:
- name: minikube - name: minikube
user: user:
@ -164,6 +165,7 @@ contexts:
name: my-cluster name: my-cluster
current-context: minikube current-context: minikube
kind: Config kind: Config
preferences: {}
users: users:
- name: minikube - name: minikube
user: user:
@ -245,6 +247,7 @@ contexts:
name: minikube name: minikube
current-context: minikube current-context: minikube
kind: Config kind: Config
preferences: {}
users: users:
- name: minikube - name: minikube
user: user:
@ -269,6 +272,7 @@ contexts:
name: my-cluster name: my-cluster
current-context: my-cluster current-context: my-cluster
kind: Config kind: Config
preferences: {}
users: users:
- name: mu-cluster - name: mu-cluster
user: user:

View File

@ -18,13 +18,12 @@ package cp
import ( import (
"archive/tar" "archive/tar"
"context" "bytes"
"errors" "errors"
"fmt" "fmt"
"io" "io"
"os" "os"
"strings" "strings"
"time"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -268,8 +267,8 @@ func (o *CopyOptions) checkDestinationIsDir(dest fileSpec) error {
options := &exec.ExecOptions{ options := &exec.ExecOptions{
StreamOptions: exec.StreamOptions{ StreamOptions: exec.StreamOptions{
IOStreams: genericiooptions.IOStreams{ IOStreams: genericiooptions.IOStreams{
Out: io.Discard, Out: bytes.NewBuffer([]byte{}),
ErrOut: io.Discard, ErrOut: bytes.NewBuffer([]byte{}),
}, },
Namespace: dest.PodNamespace, Namespace: dest.PodNamespace,
@ -280,21 +279,7 @@ func (o *CopyOptions) checkDestinationIsDir(dest fileSpec) error {
Executor: &exec.DefaultRemoteExecutor{}, Executor: &exec.DefaultRemoteExecutor{},
} }
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) return o.execute(options)
defer cancel()
done := make(chan error)
go func() {
done <- o.execute(options)
}()
select {
case <-ctx.Done():
return ctx.Err()
case err := <-done:
return err
}
} }
func (o *CopyOptions) copyToPod(src, dest fileSpec, options *exec.ExecOptions) error { func (o *CopyOptions) copyToPod(src, dest fileSpec, options *exec.ExecOptions) error {
@ -310,10 +295,6 @@ func (o *CopyOptions) copyToPod(src, dest fileSpec, options *exec.ExecOptions) e
// If no error, dest.File was found to be a directory. // If no error, dest.File was found to be a directory.
// Copy specified src into it // Copy specified src into it
destFile = destFile.Join(srcFile.Base()) destFile = destFile.Join(srcFile.Base())
} else if errors.Is(err, context.DeadlineExceeded) {
// we haven't decided destination is directory or not because context timeout is exceeded.
// That's why, we should shortcut the process in here.
return err
} }
go func(src localPath, dest remotePath, writer io.WriteCloser) { go func(src localPath, dest remotePath, writer io.WriteCloser) {
@ -587,5 +568,8 @@ func (o *CopyOptions) execute(options *exec.ExecOptions) error {
return err return err
} }
return options.Run() if err := options.Run(); err != nil {
return err
}
return nil
} }

View File

@ -988,6 +988,6 @@ func cmpFileData(t *testing.T, filePath, data string) {
type testWriter testing.T type testWriter testing.T
func (t *testWriter) Write(p []byte) (n int, err error) { func (t *testWriter) Write(p []byte) (n int, err error) {
t.Log(string(p)) t.Logf(string(p))
return len(p), nil return len(p), nil
} }

View File

@ -109,6 +109,12 @@ func NewCmdCreate(f cmdutil.Factory, ioStreams genericiooptions.IOStreams) *cobr
Long: createLong, Long: createLong,
Example: createExample, Example: createExample,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
if cmdutil.IsFilenameSliceEmpty(o.FilenameOptions.Filenames, o.FilenameOptions.Kustomize) {
ioStreams.ErrOut.Write([]byte("Error: must specify one of -f and -k\n\n"))
defaultRunFunc := cmdutil.DefaultSubCommandRun(ioStreams.ErrOut)
defaultRunFunc(cmd, args)
return
}
cmdutil.CheckErr(o.Complete(f, cmd, args)) cmdutil.CheckErr(o.Complete(f, cmd, args))
cmdutil.CheckErr(o.Validate()) cmdutil.CheckErr(o.Validate())
cmdutil.CheckErr(o.RunCreate(f, cmd)) cmdutil.CheckErr(o.RunCreate(f, cmd))
@ -153,12 +159,8 @@ func NewCmdCreate(f cmdutil.Factory, ioStreams genericiooptions.IOStreams) *cobr
return cmd return cmd
} }
// Validate makes sure there is no discrepancy in command options // Validate makes sure there is no discrepency in command options
func (o *CreateOptions) Validate() error { func (o *CreateOptions) Validate() error {
if err := o.FilenameOptions.RequireFilenameOrKustomize(); err != nil {
return err
}
if len(o.Raw) > 0 { if len(o.Raw) > 0 {
if o.EditBeforeCreate { if o.EditBeforeCreate {
return fmt.Errorf("--raw and --edit are mutually exclusive") return fmt.Errorf("--raw and --edit are mutually exclusive")

View File

@ -20,7 +20,7 @@ import (
"context" "context"
"fmt" "fmt"
"os" "os"
"path/filepath" "path"
"strings" "strings"
"unicode/utf8" "unicode/utf8"
@ -320,7 +320,7 @@ func handleConfigMapFromFileSources(configMap *corev1.ConfigMap, fileSources []s
return fmt.Errorf("error listing files in %s: %v", filePath, err) return fmt.Errorf("error listing files in %s: %v", filePath, err)
} }
for _, item := range fileList { for _, item := range fileList {
itemPath := filepath.Join(filePath, item.Name()) itemPath := path.Join(filePath, item.Name())
if item.Type().IsRegular() { if item.Type().IsRegular() {
keyName = item.Name() keyName = item.Name()
err = addKeyFromFileToConfigMap(configMap, keyName, itemPath) err = addKeyFromFileToConfigMap(configMap, keyName, itemPath)

View File

@ -53,10 +53,7 @@ var (
kubectl create deployment my-dep --image=nginx --replicas=3 kubectl create deployment my-dep --image=nginx --replicas=3
# Create a deployment named my-dep that runs the busybox image and expose port 5701 # Create a deployment named my-dep that runs the busybox image and expose port 5701
kubectl create deployment my-dep --image=busybox --port=5701 kubectl create deployment my-dep --image=busybox --port=5701`))
# Create a deployment named my-dep that runs multiple containers
kubectl create deployment my-dep --image=busybox:latest --image=ubuntu:latest --image=nginx`))
) )
// CreateDeploymentOptions is returned by NewCmdCreateDeployment // CreateDeploymentOptions is returned by NewCmdCreateDeployment
@ -115,9 +112,9 @@ func NewCmdCreateDeployment(f cmdutil.Factory, ioStreams genericiooptions.IOStre
cmdutil.AddApplyAnnotationFlags(cmd) cmdutil.AddApplyAnnotationFlags(cmd)
cmdutil.AddValidateFlags(cmd) cmdutil.AddValidateFlags(cmd)
cmdutil.AddDryRunFlag(cmd) cmdutil.AddDryRunFlag(cmd)
cmd.Flags().StringSliceVar(&o.Images, "image", o.Images, "Image names to run. A deployment can have multiple images set for multi-container pod.") cmd.Flags().StringSliceVar(&o.Images, "image", o.Images, "Image names to run.")
cmd.MarkFlagRequired("image") cmd.MarkFlagRequired("image")
cmd.Flags().Int32Var(&o.Port, "port", o.Port, "The containerPort that this deployment exposes.") cmd.Flags().Int32Var(&o.Port, "port", o.Port, "The port that this container exposes.")
cmd.Flags().Int32VarP(&o.Replicas, "replicas", "r", o.Replicas, "Number of replicas to create. Default is 1.") cmd.Flags().Int32VarP(&o.Replicas, "replicas", "r", o.Replicas, "Number of replicas to create. Default is 1.")
cmdutil.AddFieldManagerFlagVar(cmd, &o.FieldManager, "kubectl-create") cmdutil.AddFieldManagerFlagVar(cmd, &o.FieldManager, "kubectl-create")

View File

@ -35,7 +35,6 @@ import (
"k8s.io/kubectl/pkg/util" "k8s.io/kubectl/pkg/util"
"k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/i18n"
"k8s.io/kubectl/pkg/util/templates" "k8s.io/kubectl/pkg/util/templates"
"k8s.io/utils/ptr"
) )
var ( var (
@ -262,21 +261,10 @@ func (o *CreateJobOptions) createJobFromCronJob(cronJob *batchv1.CronJob) *batch
// this is ok because we know exactly how we want to be serialized // this is ok because we know exactly how we want to be serialized
TypeMeta: metav1.TypeMeta{APIVersion: batchv1.SchemeGroupVersion.String(), Kind: "Job"}, TypeMeta: metav1.TypeMeta{APIVersion: batchv1.SchemeGroupVersion.String(), Kind: "Job"},
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: o.Name, Name: o.Name,
Annotations: annotations, Annotations: annotations,
Labels: cronJob.Spec.JobTemplate.Labels, Labels: cronJob.Spec.JobTemplate.Labels,
OwnerReferences: []metav1.OwnerReference{ OwnerReferences: []metav1.OwnerReference{*metav1.NewControllerRef(cronJob, batchv1.SchemeGroupVersion.WithKind("CronJob"))},
{
// we are not using metav1.NewControllerRef because it
// sets BlockOwnerDeletion to true which additionally mandates
// cronjobs/finalizer role and not backwards-compatible.
APIVersion: batchv1.SchemeGroupVersion.String(),
Kind: "CronJob",
Name: cronJob.GetName(),
UID: cronJob.GetUID(),
Controller: ptr.To(true),
},
},
}, },
Spec: cronJob.Spec.JobTemplate.Spec, Spec: cronJob.Spec.JobTemplate.Spec,
} }

View File

@ -24,7 +24,6 @@ import (
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
apiequality "k8s.io/apimachinery/pkg/api/equality" apiequality "k8s.io/apimachinery/pkg/api/equality"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/utils/ptr"
) )
func TestCreateJobValidation(t *testing.T) { func TestCreateJobValidation(t *testing.T) {
@ -162,17 +161,9 @@ func TestCreateJobFromCronJob(t *testing.T) {
expected: &batchv1.Job{ expected: &batchv1.Job{
TypeMeta: metav1.TypeMeta{APIVersion: batchv1.SchemeGroupVersion.String(), Kind: "Job"}, TypeMeta: metav1.TypeMeta{APIVersion: batchv1.SchemeGroupVersion.String(), Kind: "Job"},
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: jobName, Name: jobName,
Annotations: map[string]string{"cronjob.kubernetes.io/instantiate": "manual"}, Annotations: map[string]string{"cronjob.kubernetes.io/instantiate": "manual"},
OwnerReferences: []metav1.OwnerReference{ OwnerReferences: []metav1.OwnerReference{*metav1.NewControllerRef(cronJob, batchv1.SchemeGroupVersion.WithKind("CronJob"))},
{
APIVersion: batchv1.SchemeGroupVersion.String(),
Kind: "CronJob",
Name: cronJob.GetName(),
UID: cronJob.GetUID(),
Controller: ptr.To(true),
},
},
}, },
Spec: batchv1.JobSpec{ Spec: batchv1.JobSpec{
Template: corev1.PodTemplateSpec{ Template: corev1.PodTemplateSpec{

View File

@ -116,7 +116,7 @@ var (
func AddSpecialVerb(verb string, gr schema.GroupResource) { func AddSpecialVerb(verb string, gr schema.GroupResource) {
resources, ok := specialVerbs[verb] resources, ok := specialVerbs[verb]
if !ok { if !ok {
resources = make([]schema.GroupResource, 0, 1) resources = make([]schema.GroupResource, 1)
} }
resources = append(resources, gr) resources = append(resources, gr)
specialVerbs[verb] = resources specialVerbs[verb] = resources
@ -425,7 +425,7 @@ func generateResourcePolicyRules(mapper meta.RESTMapper, verbs []string, resourc
// Create separate rule for each of the api group. // Create separate rule for each of the api group.
rules := []rbacv1.PolicyRule{} rules := []rbacv1.PolicyRule{}
for _, g := range sets.List(sets.KeySet(groupResourceMapping)) { for _, g := range sets.StringKeySet(groupResourceMapping).List() {
rule := rbacv1.PolicyRule{} rule := rbacv1.PolicyRule{}
rule.Verbs = verbs rule.Verbs = verbs
rule.Resources = groupResourceMapping[g] rule.Resources = groupResourceMapping[g]

View File

@ -684,17 +684,14 @@ func TestAddSpecialVerb(t *testing.T) {
testCases := map[string]struct { testCases := map[string]struct {
verb string verb string
resource schema.GroupResource resource schema.GroupResource
isNew bool
}{ }{
"existing verb": { "existing verb": {
verb: "use", verb: "use",
resource: schema.GroupResource{Group: "my.custom.io", Resource: "one"}, resource: schema.GroupResource{Group: "my.custom.io", Resource: "one"},
isNew: false,
}, },
"new verb": { "new verb": {
verb: "new", verb: "new",
resource: schema.GroupResource{Group: "my.custom.io", Resource: "two"}, resource: schema.GroupResource{Group: "my.custom.io", Resource: "two"},
isNew: true,
}, },
} }
@ -706,16 +703,6 @@ func TestAddSpecialVerb(t *testing.T) {
t.Errorf("missing expected verb: %s", tc.verb) t.Errorf("missing expected verb: %s", tc.verb)
} }
if tc.isNew {
if len(resources) != 1 {
t.Errorf("new verb should only contain one resource resources:%#v", resources)
}
if !reflect.DeepEqual(tc.resource, resources[0]) {
t.Errorf("miss expected resource:%#v", tc.resource)
}
return
}
for _, res := range resources { for _, res := range resources {
if reflect.DeepEqual(tc.resource, res) { if reflect.DeepEqual(tc.resource, res) {
return return

View File

@ -44,7 +44,7 @@ var (
# Create a role binding for user1, user2, and group1 using the admin cluster role # Create a role binding for user1, user2, and group1 using the admin cluster role
kubectl create rolebinding admin --clusterrole=admin --user=user1 --user=user2 --group=group1 kubectl create rolebinding admin --clusterrole=admin --user=user1 --user=user2 --group=group1
# Create a role binding for service account monitoring:sa-dev using the admin role # Create a role binding for serviceaccount monitoring:sa-dev using the admin role
kubectl create rolebinding admin-binding --role=admin --serviceaccount=monitoring:sa-dev`)) kubectl create rolebinding admin-binding --role=admin --serviceaccount=monitoring:sa-dev`))
) )

View File

@ -20,7 +20,7 @@ import (
"context" "context"
"fmt" "fmt"
"os" "os"
"path/filepath" "path"
"strings" "strings"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -349,7 +349,7 @@ func handleSecretFromFileSources(secret *corev1.Secret, fileSources []string) er
return fmt.Errorf("error listing files in %s: %v", filePath, err) return fmt.Errorf("error listing files in %s: %v", filePath, err)
} }
for _, item := range fileList { for _, item := range fileList {
itemPath := filepath.Join(filePath, item.Name()) itemPath := path.Join(filePath, item.Name())
if item.Type().IsRegular() { if item.Type().IsRegular() {
keyName = item.Name() keyName = item.Name()
if err := addKeyFromFileToSecret(secret, keyName, itemPath); err != nil { if err := addKeyFromFileToSecret(secret, keyName, itemPath); err != nil {

View File

@ -21,7 +21,6 @@ import (
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"fmt" "fmt"
"strings"
"github.com/spf13/cobra" "github.com/spf13/cobra"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
@ -59,7 +58,7 @@ var (
kubectl create secret docker-registry my-secret --docker-server=DOCKER_REGISTRY_SERVER --docker-username=DOCKER_USER --docker-password=DOCKER_PASSWORD --docker-email=DOCKER_EMAIL kubectl create secret docker-registry my-secret --docker-server=DOCKER_REGISTRY_SERVER --docker-username=DOCKER_USER --docker-password=DOCKER_PASSWORD --docker-email=DOCKER_EMAIL
# Create a new secret named my-secret from ~/.docker/config.json # Create a new secret named my-secret from ~/.docker/config.json
kubectl create secret docker-registry my-secret --from-file=path/to/.docker/config.json`)) kubectl create secret docker-registry my-secret --from-file=.dockerconfigjson=path/to/.docker/config.json`))
) )
// DockerConfigJSON represents a local docker auth config file // DockerConfigJSON represents a local docker auth config file
@ -153,11 +152,7 @@ func NewCmdCreateSecretDockerRegistry(f cmdutil.Factory, ioStreams genericioopti
cmd.Flags().StringVar(&o.Email, "docker-email", o.Email, i18n.T("Email for Docker registry")) cmd.Flags().StringVar(&o.Email, "docker-email", o.Email, i18n.T("Email for Docker registry"))
cmd.Flags().StringVar(&o.Server, "docker-server", o.Server, i18n.T("Server location for Docker registry")) cmd.Flags().StringVar(&o.Server, "docker-server", o.Server, i18n.T("Server location for Docker registry"))
cmd.Flags().BoolVar(&o.AppendHash, "append-hash", o.AppendHash, "Append a hash of the secret to its name.") cmd.Flags().BoolVar(&o.AppendHash, "append-hash", o.AppendHash, "Append a hash of the secret to its name.")
cmd.Flags().StringSliceVar(&o.FileSources, "from-file", o.FileSources, "Key files can be specified using their file path, "+ cmd.Flags().StringSliceVar(&o.FileSources, "from-file", o.FileSources, "Key files can be specified using their file path, in which case a default name will be given to them, or optionally with a name and file path, in which case the given name will be used. Specifying a directory will iterate each named file in the directory that is a valid secret key.")
"in which case a default name of "+corev1.DockerConfigJsonKey+" will be given to them, "+
"or optionally with a name and file path, in which case the given name will be used. "+
"Specifying a directory will iterate each named file in the directory that is a valid secret key. "+
"For this command, the key should always be "+corev1.DockerConfigJsonKey+".")
cmdutil.AddFieldManagerFlagVar(cmd, &o.FieldManager, "kubectl-create") cmdutil.AddFieldManagerFlagVar(cmd, &o.FieldManager, "kubectl-create")
@ -209,11 +204,6 @@ func (o *CreateSecretDockerRegistryOptions) Complete(f cmdutil.Factory, cmd *cob
return err return err
} }
for i := range o.FileSources {
if !strings.Contains(o.FileSources[i], "=") {
o.FileSources[i] = corev1.DockerConfigJsonKey + "=" + o.FileSources[i]
}
}
return nil return nil
} }

View File

@ -17,16 +17,11 @@ limitations under the License.
package create package create
import ( import (
"encoding/json"
"fmt"
"os"
"testing" "testing"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
apiequality "k8s.io/apimachinery/pkg/api/equality" apiequality "k8s.io/apimachinery/pkg/api/equality"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/cli-runtime/pkg/genericiooptions"
cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
) )
func TestCreateSecretDockerRegistry(t *testing.T) { func TestCreateSecretDockerRegistry(t *testing.T) {
@ -188,80 +183,3 @@ func TestCreateSecretDockerRegistry(t *testing.T) {
}) })
} }
} }
func TestCreateSecretDockerRegistryFromFile(t *testing.T) {
username, password, email, server := "test-user", "test-password", "test-user@example.org", "https://index.docker.io/v1/"
secretData, err := handleDockerCfgJSONContent(username, password, email, server)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
secret := &corev1.Secret{
TypeMeta: metav1.TypeMeta{
APIVersion: corev1.SchemeGroupVersion.String(),
Kind: "Secret",
},
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
},
Type: corev1.SecretTypeDockerConfigJson,
Data: map[string][]byte{
corev1.DockerConfigJsonKey: secretData,
},
}
tests := map[string]struct {
withKey bool
expected *corev1.Secret
}{
"create_secret_docker_registry_from_file_with_keyname": {
withKey: true,
expected: secret,
},
"create_secret_docker_registry_from_file_without_keyname": {
withKey: false,
expected: secret,
},
}
// Run all the tests
for name, test := range tests {
t.Run(name, func(t *testing.T) {
tmp, _ := os.MkdirTemp("", "input")
defer func() {
err := os.RemoveAll(tmp)
if err != nil {
t.Fatalf("Failed to teardown: %s", err)
}
}()
dockerCfgFile := tmp + "/dockerconfig.json"
err := os.WriteFile(dockerCfgFile, secretData, 0644)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
tf := cmdtesting.NewTestFactory()
defer tf.Cleanup()
ioStreams, _, out, _ := genericiooptions.NewTestIOStreams()
cmd := NewCmdCreateSecretDockerRegistry(tf, ioStreams)
args := []string{"foo", "--dry-run=client", "-ojson"}
if test.withKey {
args = append(args, fmt.Sprintf("--from-file=%s=%s", corev1.DockerConfigJsonKey, dockerCfgFile))
} else {
args = append(args, fmt.Sprintf("--from-file=%s", dockerCfgFile))
}
cmd.SetArgs(args)
err = cmd.Execute()
if err != nil {
t.Errorf("unexpected error: %v", err)
}
got := &corev1.Secret{}
err = json.Unmarshal(out.Bytes(), got)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if !apiequality.Semantic.DeepEqual(got, test.expected) {
t.Errorf("test %s\n expected:\n%#v\ngot:\n%#v", name, test.expected, got)
}
})
}
}

View File

@ -46,7 +46,7 @@ var (
secretForTLSExample = templates.Examples(i18n.T(` secretForTLSExample = templates.Examples(i18n.T(`
# Create a new TLS secret named tls-secret with the given key pair # Create a new TLS secret named tls-secret with the given key pair
kubectl create secret tls tls-secret --cert=path/to/tls.crt --key=path/to/tls.key`)) kubectl create secret tls tls-secret --cert=path/to/tls.cert --key=path/to/tls.key`))
) )
// CreateSecretTLSOptions holds the options for 'create secret tls' sub command // CreateSecretTLSOptions holds the options for 'create secret tls' sub command

View File

@ -18,7 +18,7 @@ package create
import ( import (
"os" "os"
"path/filepath" "path"
"testing" "testing"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
@ -197,8 +197,8 @@ func write(path, contents string, t *testing.T) {
} }
func writeKeyPair(tmpDirPath, key, cert string, t *testing.T) (keyPath, certPath string) { func writeKeyPair(tmpDirPath, key, cert string, t *testing.T) (keyPath, certPath string) {
keyPath = filepath.Join(tmpDirPath, "tls.key") keyPath = path.Join(tmpDirPath, "tls.key")
certPath = filepath.Join(tmpDirPath, "tls.cert") certPath = path.Join(tmpDirPath, "tls.cert")
write(keyPath, key, t) write(keyPath, key, t)
write(certPath, cert, t) write(certPath, cert, t)
return return

View File

@ -18,7 +18,6 @@ package create
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"strconv" "strconv"
"strings" "strings"
@ -400,12 +399,12 @@ func parsePorts(portString string) (int32, intstr.IntOrString, error) {
var targetPort intstr.IntOrString var targetPort intstr.IntOrString
if portNum, err := strconv.Atoi(portStringSlice[1]); err != nil { if portNum, err := strconv.Atoi(portStringSlice[1]); err != nil {
if errs := validation.IsValidPortName(portStringSlice[1]); len(errs) != 0 { if errs := validation.IsValidPortName(portStringSlice[1]); len(errs) != 0 {
return 0, intstr.FromInt32(0), errors.New(strings.Join(errs, ",")) return 0, intstr.FromInt32(0), fmt.Errorf(strings.Join(errs, ","))
} }
targetPort = intstr.FromString(portStringSlice[1]) targetPort = intstr.FromString(portStringSlice[1])
} else { } else {
if errs := validation.IsValidPortNum(portNum); len(errs) != 0 { if errs := validation.IsValidPortNum(portNum); len(errs) != 0 {
return 0, intstr.FromInt32(0), errors.New(strings.Join(errs, ",")) return 0, intstr.FromInt32(0), fmt.Errorf(strings.Join(errs, ","))
} }
targetPort = intstr.FromInt32(int32(portNum)) targetPort = intstr.FromInt32(int32(portNum))
} }

View File

@ -25,7 +25,6 @@ import (
"k8s.io/cli-runtime/pkg/resource" "k8s.io/cli-runtime/pkg/resource"
"k8s.io/client-go/rest/fake" "k8s.io/client-go/rest/fake"
cmdtesting "k8s.io/kubectl/pkg/cmd/testing" cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
cmdutil "k8s.io/kubectl/pkg/cmd/util"
"k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/scheme"
) )
@ -151,35 +150,3 @@ func TestCreateDirectory(t *testing.T) {
t.Errorf("unexpected output: %s", buf.String()) t.Errorf("unexpected output: %s", buf.String())
} }
} }
func TestMissingFilenameError(t *testing.T) {
var errStr string
var exitCode int
cmdutil.BehaviorOnFatal(func(str string, code int) {
if errStr == "" {
errStr = str
exitCode = code
}
})
tf := cmdtesting.NewTestFactory().WithNamespace("test")
defer tf.Cleanup()
ioStreams, _, buf, _ := genericiooptions.NewTestIOStreams()
cmd := NewCmdCreate(tf, ioStreams)
cmd.Run(cmd, []string{})
if buf.Len() > 0 {
t.Errorf("unexpected output: %s", buf.String())
}
if len(errStr) == 0 {
t.Errorf("unexpected non-error")
} else if errStr != "error: must specify one of -f and -k" {
t.Errorf("unexpected error: %s", errStr)
}
if exitCode != 1 {
t.Errorf("unexpected exit code: %d", exitCode)
}
}

View File

@ -23,7 +23,6 @@ import (
"time" "time"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag"
authenticationv1 "k8s.io/api/authentication/v1" authenticationv1 "k8s.io/api/authentication/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -38,7 +37,7 @@ import (
"k8s.io/kubectl/pkg/util/completion" "k8s.io/kubectl/pkg/util/completion"
"k8s.io/kubectl/pkg/util/templates" "k8s.io/kubectl/pkg/util/templates"
"k8s.io/kubectl/pkg/util/term" "k8s.io/kubectl/pkg/util/term"
"k8s.io/utils/ptr" "k8s.io/utils/pointer"
) )
// TokenOptions is the data required to perform a token request operation. // TokenOptions is the data required to perform a token request operation.
@ -47,9 +46,6 @@ type TokenOptions struct {
PrintFlags *genericclioptions.PrintFlags PrintFlags *genericclioptions.PrintFlags
PrintObj func(obj runtime.Object) error PrintObj func(obj runtime.Object) error
// Flags hold the parsed CLI flags.
Flags *pflag.FlagSet
// Name and namespace of service account to create a token for // Name and namespace of service account to create a token for
Name string Name string
Namespace string Namespace string
@ -96,13 +92,12 @@ var (
# Request a token bound to an instance of a Secret object with a specific UID # Request a token bound to an instance of a Secret object with a specific UID
kubectl create token myapp --bound-object-kind Secret --bound-object-name mysecret --bound-object-uid 0d4691ed-659b-4935-a832-355f77ee47cc kubectl create token myapp --bound-object-kind Secret --bound-object-name mysecret --bound-object-uid 0d4691ed-659b-4935-a832-355f77ee47cc
`) `)
)
var boundObjectKinds = map[string]string{ boundObjectKindToAPIVersion = map[string]string{
"Pod": "v1", "Pod": "v1",
"Secret": "v1", "Secret": "v1",
"Node": "v1", }
} )
func NewTokenOpts(ioStreams genericiooptions.IOStreams) *TokenOptions { func NewTokenOpts(ioStreams genericiooptions.IOStreams) *TokenOptions {
return &TokenOptions{ return &TokenOptions{
@ -142,10 +137,10 @@ func NewCmdCreateToken(f cmdutil.Factory, ioStreams genericiooptions.IOStreams)
cmd.Flags().StringArrayVar(&o.Audiences, "audience", o.Audiences, "Audience of the requested token. If unset, defaults to requesting a token for use with the Kubernetes API server. May be repeated to request a token valid for multiple audiences.") cmd.Flags().StringArrayVar(&o.Audiences, "audience", o.Audiences, "Audience of the requested token. If unset, defaults to requesting a token for use with the Kubernetes API server. May be repeated to request a token valid for multiple audiences.")
cmd.Flags().DurationVar(&o.Duration, "duration", o.Duration, "Requested lifetime of the issued token. If not set or if set to 0, the lifetime will be determined by the server automatically. The server may return a token with a longer or shorter lifetime.") cmd.Flags().DurationVar(&o.Duration, "duration", o.Duration, "Requested lifetime of the issued token. The server may return a token with a longer or shorter lifetime.")
cmd.Flags().StringVar(&o.BoundObjectKind, "bound-object-kind", o.BoundObjectKind, "Kind of an object to bind the token to. "+ cmd.Flags().StringVar(&o.BoundObjectKind, "bound-object-kind", o.BoundObjectKind, "Kind of an object to bind the token to. "+
"Supported kinds are "+strings.Join(sets.List(sets.KeySet(boundObjectKinds)), ", ")+". "+ "Supported kinds are "+strings.Join(sets.StringKeySet(boundObjectKindToAPIVersion).List(), ", ")+". "+
"If set, --bound-object-name must be provided.") "If set, --bound-object-name must be provided.")
cmd.Flags().StringVar(&o.BoundObjectName, "bound-object-name", o.BoundObjectName, "Name of an object to bind the token to. "+ cmd.Flags().StringVar(&o.BoundObjectName, "bound-object-name", o.BoundObjectName, "Name of an object to bind the token to. "+
"The token will expire when the object is deleted. "+ "The token will expire when the object is deleted. "+
@ -154,8 +149,6 @@ func NewCmdCreateToken(f cmdutil.Factory, ioStreams genericiooptions.IOStreams)
"Requires --bound-object-kind and --bound-object-name. "+ "Requires --bound-object-kind and --bound-object-name. "+
"If unset, the UID of the existing object is used.") "If unset, the UID of the existing object is used.")
o.Flags = cmd.Flags()
return cmd return cmd
} }
@ -203,7 +196,7 @@ func (o *TokenOptions) Validate() error {
return fmt.Errorf("--namespace is required") return fmt.Errorf("--namespace is required")
} }
if o.Duration < 0 { if o.Duration < 0 {
return fmt.Errorf("--duration must be greater than or equal to 0") return fmt.Errorf("--duration must be positive")
} }
if o.Duration%time.Second != 0 { if o.Duration%time.Second != 0 {
return fmt.Errorf("--duration cannot be expressed in units less than seconds") return fmt.Errorf("--duration cannot be expressed in units less than seconds")
@ -222,8 +215,8 @@ func (o *TokenOptions) Validate() error {
return fmt.Errorf("--bound-object-uid can only be set if --bound-object-kind is provided") return fmt.Errorf("--bound-object-uid can only be set if --bound-object-kind is provided")
} }
} else { } else {
if _, ok := boundObjectKinds[o.BoundObjectKind]; !ok { if _, ok := boundObjectKindToAPIVersion[o.BoundObjectKind]; !ok {
return fmt.Errorf("supported --bound-object-kind values are %s", strings.Join(sets.List(sets.KeySet(boundObjectKinds)), ", ")) return fmt.Errorf("supported --bound-object-kind values are %s", strings.Join(sets.StringKeySet(boundObjectKindToAPIVersion).List(), ", "))
} }
if len(o.BoundObjectName) == 0 { if len(o.BoundObjectName) == 0 {
return fmt.Errorf("--bound-object-name is required if --bound-object-kind is provided") return fmt.Errorf("--bound-object-name is required if --bound-object-kind is provided")
@ -241,12 +234,12 @@ func (o *TokenOptions) Run() error {
}, },
} }
if o.Duration > 0 { if o.Duration > 0 {
request.Spec.ExpirationSeconds = ptr.To(int64(o.Duration / time.Second)) request.Spec.ExpirationSeconds = pointer.Int64(int64(o.Duration / time.Second))
} }
if len(o.BoundObjectKind) > 0 { if len(o.BoundObjectKind) > 0 {
request.Spec.BoundObjectRef = &authenticationv1.BoundObjectReference{ request.Spec.BoundObjectRef = &authenticationv1.BoundObjectReference{
Kind: o.BoundObjectKind, Kind: o.BoundObjectKind,
APIVersion: boundObjectKinds[o.BoundObjectKind], APIVersion: boundObjectKindToAPIVersion[o.BoundObjectKind],
Name: o.BoundObjectName, Name: o.BoundObjectName,
UID: types.UID(o.BoundObjectUID), UID: types.UID(o.BoundObjectUID),
} }

View File

@ -26,6 +26,7 @@ import (
"time" "time"
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
"k8s.io/utils/pointer"
kjson "sigs.k8s.io/json" kjson "sigs.k8s.io/json"
authenticationv1 "k8s.io/api/authentication/v1" authenticationv1 "k8s.io/api/authentication/v1"
@ -37,7 +38,6 @@ import (
cmdtesting "k8s.io/kubectl/pkg/cmd/testing" cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
cmdutil "k8s.io/kubectl/pkg/cmd/util" cmdutil "k8s.io/kubectl/pkg/cmd/util"
"k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/scheme"
"k8s.io/utils/ptr"
) )
func TestCreateToken(t *testing.T) { func TestCreateToken(t *testing.T) {
@ -99,7 +99,8 @@ func TestCreateToken(t *testing.T) {
serverResponseToken: "abc", serverResponseToken: "abc",
expectStdout: `apiVersion: authentication.k8s.io/v1 expectStdout: `apiVersion: authentication.k8s.io/v1
kind: TokenRequest kind: TokenRequest
metadata: {} metadata:
creationTimestamp: null
spec: spec:
audiences: null audiences: null
boundObjectRef: null boundObjectRef: null
@ -114,13 +115,7 @@ status:
test: "bad bound object kind", test: "bad bound object kind",
name: "mysa", name: "mysa",
boundObjectKind: "Foo", boundObjectKind: "Foo",
expectStderr: `error: supported --bound-object-kind values are Node, Pod, Secret`, expectStderr: `error: supported --bound-object-kind values are Pod, Secret`,
},
{
test: "bad bound object kind (node feature enabled)",
name: "mysa",
boundObjectKind: "Foo",
expectStderr: `error: supported --bound-object-kind values are Node, Pod, Secret`,
}, },
{ {
test: "missing bound object name", test: "missing bound object name",
@ -163,29 +158,7 @@ status:
serverResponseToken: "abc", serverResponseToken: "abc",
expectStdout: "abc", expectStdout: "abc",
}, },
{
test: "valid bound object (Node)",
name: "mysa",
boundObjectKind: "Node",
boundObjectName: "mynode",
boundObjectUID: "myuid",
expectRequestPath: "/api/v1/namespaces/test/serviceaccounts/mysa/token",
expectTokenRequest: &authenticationv1.TokenRequest{
TypeMeta: metav1.TypeMeta{APIVersion: "authentication.k8s.io/v1", Kind: "TokenRequest"},
Spec: authenticationv1.TokenRequestSpec{
BoundObjectRef: &authenticationv1.BoundObjectReference{
Kind: "Node",
APIVersion: "v1",
Name: "mynode",
UID: "myuid",
},
},
},
serverResponseToken: "abc",
expectStdout: "abc",
},
{ {
test: "invalid audience", test: "invalid audience",
name: "mysa", name: "mysa",
@ -213,7 +186,7 @@ status:
test: "invalid duration", test: "invalid duration",
name: "mysa", name: "mysa",
duration: -1, duration: -1,
expectStderr: `error: --duration must be greater than or equal to 0`, expectStderr: `error: --duration must be positive`,
}, },
{ {
test: "invalid duration unit", test: "invalid duration unit",
@ -231,28 +204,13 @@ status:
expectTokenRequest: &authenticationv1.TokenRequest{ expectTokenRequest: &authenticationv1.TokenRequest{
TypeMeta: metav1.TypeMeta{APIVersion: "authentication.k8s.io/v1", Kind: "TokenRequest"}, TypeMeta: metav1.TypeMeta{APIVersion: "authentication.k8s.io/v1", Kind: "TokenRequest"},
Spec: authenticationv1.TokenRequestSpec{ Spec: authenticationv1.TokenRequestSpec{
ExpirationSeconds: ptr.To[int64](1000), ExpirationSeconds: pointer.Int64(1000),
}, },
}, },
serverResponseToken: "abc", serverResponseToken: "abc",
expectStdout: "abc", expectStdout: "abc",
}, },
{
test: "zero duration act as default",
name: "mysa",
duration: 0 * time.Second,
expectRequestPath: "/api/v1/namespaces/test/serviceaccounts/mysa/token",
expectTokenRequest: &authenticationv1.TokenRequest{
TypeMeta: metav1.TypeMeta{APIVersion: "authentication.k8s.io/v1", Kind: "TokenRequest"},
Spec: authenticationv1.TokenRequestSpec{
ExpirationSeconds: nil,
},
},
serverResponseToken: "abc",
expectStdout: "abc",
},
{ {
test: "server error", test: "server error",
name: "mysa", name: "mysa",

View File

@ -20,11 +20,12 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"os"
"time" "time"
"github.com/distribution/reference" "github.com/docker/distribution/reference"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"k8s.io/klog/v2"
"k8s.io/utils/pointer"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/errors"
@ -44,7 +45,6 @@ import (
corev1client "k8s.io/client-go/kubernetes/typed/core/v1" corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
"k8s.io/client-go/tools/cache" "k8s.io/client-go/tools/cache"
watchtools "k8s.io/client-go/tools/watch" watchtools "k8s.io/client-go/tools/watch"
"k8s.io/klog/v2"
"k8s.io/kubectl/pkg/cmd/attach" "k8s.io/kubectl/pkg/cmd/attach"
"k8s.io/kubectl/pkg/cmd/exec" "k8s.io/kubectl/pkg/cmd/exec"
"k8s.io/kubectl/pkg/cmd/logs" "k8s.io/kubectl/pkg/cmd/logs"
@ -55,8 +55,6 @@ import (
"k8s.io/kubectl/pkg/util/interrupt" "k8s.io/kubectl/pkg/util/interrupt"
"k8s.io/kubectl/pkg/util/templates" "k8s.io/kubectl/pkg/util/templates"
"k8s.io/kubectl/pkg/util/term" "k8s.io/kubectl/pkg/util/term"
"k8s.io/utils/ptr"
"sigs.k8s.io/yaml"
) )
var ( var (
@ -75,9 +73,6 @@ var (
debugging utilities without restarting the pod. debugging utilities without restarting the pod.
* Node: Create a new pod that runs in the node's host namespaces and can access * Node: Create a new pod that runs in the node's host namespaces and can access
the node's filesystem. the node's filesystem.
Note: When a non-root user is configured for the entire target Pod, some capabilities granted
by debug profile may not work.
`)) `))
debugExample = templates.Examples(i18n.T(` debugExample = templates.Examples(i18n.T(`
@ -111,39 +106,28 @@ var (
var nameSuffixFunc = utilrand.String var nameSuffixFunc = utilrand.String
type DebugAttachFunc func(ctx context.Context, restClientGetter genericclioptions.RESTClientGetter, cmdPath string, ns, podName, containerName string) error
// DebugOptions holds the options for an invocation of kubectl debug. // DebugOptions holds the options for an invocation of kubectl debug.
type DebugOptions struct { type DebugOptions struct {
Args []string Args []string
ArgsOnly bool ArgsOnly bool
Attach bool Attach bool
AttachFunc DebugAttachFunc Container string
Container string CopyTo string
CopyTo string Replace bool
Replace bool Env []corev1.EnvVar
Env []corev1.EnvVar Image string
Image string Interactive bool
Interactive bool Namespace string
KeepLabels bool TargetNames []string
KeepAnnotations bool PullPolicy corev1.PullPolicy
KeepLiveness bool Quiet bool
KeepReadiness bool SameNode bool
KeepStartup bool SetImages map[string]string
KeepInitContainers bool ShareProcesses bool
Namespace string TargetContainer string
TargetNames []string TTY bool
PullPolicy corev1.PullPolicy Profile string
Quiet bool Applier ProfileApplier
SameNode bool
SetImages map[string]string
ShareProcesses bool
TargetContainer string
TTY bool
Profile string
CustomProfileFile string
CustomProfile *corev1.Container
Applier ProfileApplier
explicitNamespace bool explicitNamespace bool
attachChanged bool attachChanged bool
@ -161,11 +145,10 @@ type DebugOptions struct {
// NewDebugOptions returns a DebugOptions initialized with default values. // NewDebugOptions returns a DebugOptions initialized with default values.
func NewDebugOptions(streams genericiooptions.IOStreams) *DebugOptions { func NewDebugOptions(streams genericiooptions.IOStreams) *DebugOptions {
return &DebugOptions{ return &DebugOptions{
Args: []string{}, Args: []string{},
IOStreams: streams, IOStreams: streams,
KeepInitContainers: true, TargetNames: []string{},
TargetNames: []string{}, ShareProcesses: true,
ShareProcesses: true,
} }
} }
@ -200,12 +183,6 @@ func (o *DebugOptions) AddFlags(cmd *cobra.Command) {
cmd.Flags().BoolVar(&o.Replace, "replace", o.Replace, i18n.T("When used with '--copy-to', delete the original Pod.")) cmd.Flags().BoolVar(&o.Replace, "replace", o.Replace, i18n.T("When used with '--copy-to', delete the original Pod."))
cmd.Flags().StringToString("env", nil, i18n.T("Environment variables to set in the container.")) cmd.Flags().StringToString("env", nil, i18n.T("Environment variables to set in the container."))
cmd.Flags().StringVar(&o.Image, "image", o.Image, i18n.T("Container image to use for debug container.")) cmd.Flags().StringVar(&o.Image, "image", o.Image, i18n.T("Container image to use for debug container."))
cmd.Flags().BoolVar(&o.KeepLabels, "keep-labels", o.KeepLabels, i18n.T("If true, keep the original pod labels.(This flag only works when used with '--copy-to')"))
cmd.Flags().BoolVar(&o.KeepAnnotations, "keep-annotations", o.KeepAnnotations, i18n.T("If true, keep the original pod annotations.(This flag only works when used with '--copy-to')"))
cmd.Flags().BoolVar(&o.KeepLiveness, "keep-liveness", o.KeepLiveness, i18n.T("If true, keep the original pod liveness probes.(This flag only works when used with '--copy-to')"))
cmd.Flags().BoolVar(&o.KeepReadiness, "keep-readiness", o.KeepReadiness, i18n.T("If true, keep the original pod readiness probes.(This flag only works when used with '--copy-to')"))
cmd.Flags().BoolVar(&o.KeepStartup, "keep-startup", o.KeepStartup, i18n.T("If true, keep the original startup probes.(This flag only works when used with '--copy-to')"))
cmd.Flags().BoolVar(&o.KeepInitContainers, "keep-init-containers", o.KeepInitContainers, i18n.T("Run the init containers for the pod. Defaults to true.(This flag only works when used with '--copy-to')"))
cmd.Flags().StringToStringVar(&o.SetImages, "set-image", o.SetImages, i18n.T("When used with '--copy-to', a list of name=image pairs for changing container images, similar to how 'kubectl set image' works.")) cmd.Flags().StringToStringVar(&o.SetImages, "set-image", o.SetImages, i18n.T("When used with '--copy-to', a list of name=image pairs for changing container images, similar to how 'kubectl set image' works."))
cmd.Flags().String("image-pull-policy", "", i18n.T("The image pull policy for the container. If left empty, this value will not be specified by the client and defaulted by the server.")) cmd.Flags().String("image-pull-policy", "", i18n.T("The image pull policy for the container. If left empty, this value will not be specified by the client and defaulted by the server."))
cmd.Flags().BoolVarP(&o.Interactive, "stdin", "i", o.Interactive, i18n.T("Keep stdin open on the container(s) in the pod, even if nothing is attached.")) cmd.Flags().BoolVarP(&o.Interactive, "stdin", "i", o.Interactive, i18n.T("Keep stdin open on the container(s) in the pod, even if nothing is attached."))
@ -214,8 +191,7 @@ func (o *DebugOptions) AddFlags(cmd *cobra.Command) {
cmd.Flags().BoolVar(&o.ShareProcesses, "share-processes", o.ShareProcesses, i18n.T("When used with '--copy-to', enable process namespace sharing in the copy.")) cmd.Flags().BoolVar(&o.ShareProcesses, "share-processes", o.ShareProcesses, i18n.T("When used with '--copy-to', enable process namespace sharing in the copy."))
cmd.Flags().StringVar(&o.TargetContainer, "target", "", i18n.T("When using an ephemeral container, target processes in this container name.")) cmd.Flags().StringVar(&o.TargetContainer, "target", "", i18n.T("When using an ephemeral container, target processes in this container name."))
cmd.Flags().BoolVarP(&o.TTY, "tty", "t", o.TTY, i18n.T("Allocate a TTY for the debugging container.")) cmd.Flags().BoolVarP(&o.TTY, "tty", "t", o.TTY, i18n.T("Allocate a TTY for the debugging container."))
cmd.Flags().StringVar(&o.Profile, "profile", ProfileLegacy, i18n.T(`Options are "legacy", "general", "baseline", "netadmin", "restricted" or "sysadmin".`)) cmd.Flags().StringVar(&o.Profile, "profile", ProfileLegacy, i18n.T(`Debugging profile. Options are "legacy", "general", "baseline", "netadmin", or "restricted".`))
cmd.Flags().StringVar(&o.CustomProfileFile, "custom", o.CustomProfileFile, i18n.T("Path to a JSON or YAML file containing a partial container spec to customize built-in debug profiles."))
} }
// Complete finishes run-time initialization of debug.DebugOptions. // Complete finishes run-time initialization of debug.DebugOptions.
@ -238,15 +214,6 @@ func (o *DebugOptions) Complete(restClientGetter genericclioptions.RESTClientGet
o.Attach = true o.Attach = true
} }
// Downstream tools may want to use their own customized
// attach function to do extra work or use attach command
// with different flags instead of the static one defined in
// handleAttachPod. But if this function is not set explicitly,
// we fall back to default.
if o.AttachFunc == nil {
o.AttachFunc = o.handleAttachPod
}
// Environment // Environment
envStrings, err := cmd.Flags().GetStringToString("env") envStrings, err := cmd.Flags().GetStringToString("env")
if err != nil { if err != nil {
@ -272,36 +239,13 @@ func (o *DebugOptions) Complete(restClientGetter genericclioptions.RESTClientGet
} }
if o.Applier == nil { if o.Applier == nil {
kflags := KeepFlags{ applier, err := NewProfileApplier(o.Profile)
Labels: o.KeepLabels,
Annotations: o.KeepAnnotations,
Liveness: o.KeepLiveness,
Readiness: o.KeepReadiness,
Startup: o.KeepStartup,
InitContainers: o.KeepInitContainers,
}
applier, err := NewProfileApplier(o.Profile, kflags)
if err != nil { if err != nil {
return err return err
} }
o.Applier = applier o.Applier = applier
} }
if o.CustomProfileFile != "" {
customProfileBytes, err := os.ReadFile(o.CustomProfileFile)
if err != nil {
return fmt.Errorf("must pass a container spec json file for custom profile: %w", err)
}
err = json.Unmarshal(customProfileBytes, &o.CustomProfile)
if err != nil {
err = yaml.Unmarshal(customProfileBytes, &o.CustomProfile)
if err != nil {
return fmt.Errorf("%s does not contain a valid container spec: %w", o.CustomProfileFile, err)
}
}
}
clientConfig, err := restClientGetter.ToRESTConfig() clientConfig, err := restClientGetter.ToRESTConfig()
if err != nil { if err != nil {
return err return err
@ -394,17 +338,6 @@ func (o *DebugOptions) Validate() error {
return fmt.Errorf("WarningPrinter can not be used without initialization") return fmt.Errorf("WarningPrinter can not be used without initialization")
} }
if o.CustomProfile != nil {
if o.CustomProfile.Name != "" || len(o.CustomProfile.Command) > 0 || o.CustomProfile.Image != "" || o.CustomProfile.Lifecycle != nil || len(o.CustomProfile.VolumeDevices) > 0 {
return fmt.Errorf("name, command, image, lifecycle and volume devices are not modifiable via custom profile")
}
}
// Warning for legacy profile
if o.Profile == ProfileLegacy {
fmt.Fprintln(o.ErrOut, `--profile=legacy is deprecated and will be removed in the future. It is recommended to explicitly specify a profile, for example "--profile=general".`)
}
return nil return nil
} }
@ -444,8 +377,26 @@ func (o *DebugOptions) Run(restClientGetter genericclioptions.RESTClientGetter,
return visitErr return visitErr
} }
if o.Attach && len(containerName) > 0 && o.AttachFunc != nil { if o.Attach && len(containerName) > 0 {
if err := o.AttachFunc(ctx, restClientGetter, cmd.Parent().CommandPath(), debugPod.Namespace, debugPod.Name, containerName); err != nil { opts := &attach.AttachOptions{
StreamOptions: exec.StreamOptions{
IOStreams: o.IOStreams,
Stdin: o.Interactive,
TTY: o.TTY,
Quiet: o.Quiet,
},
CommandName: cmd.Parent().CommandPath() + " attach",
Attach: &attach.DefaultRemoteAttach{},
}
config, err := restClientGetter.ToRESTConfig()
if err != nil {
return err
}
opts.Config = config
opts.AttachFunc = attach.DefaultAttachFunc
if err := o.handleAttachPod(ctx, restClientGetter, debugPod.Namespace, debugPod.Name, containerName, opts); err != nil {
return err return err
} }
} }
@ -498,8 +449,6 @@ func (o *DebugOptions) debugByEphemeralContainer(ctx context.Context, pod *corev
} }
klog.V(2).Infof("new ephemeral container: %#v", debugContainer) klog.V(2).Infof("new ephemeral container: %#v", debugContainer)
o.displayWarning((*corev1.Container)(&debugContainer.EphemeralContainerCommon), pod)
debugJS, err := json.Marshal(debugPod) debugJS, err := json.Marshal(debugPod)
if err != nil { if err != nil {
return nil, "", fmt.Errorf("error creating JSON for debug container: %v", err) return nil, "", fmt.Errorf("error creating JSON for debug container: %v", err)
@ -520,94 +469,51 @@ func (o *DebugOptions) debugByEphemeralContainer(ctx context.Context, pod *corev
return nil, "", fmt.Errorf("ephemeral containers are disabled for this cluster (error from server: %q)", err) return nil, "", fmt.Errorf("ephemeral containers are disabled for this cluster (error from server: %q)", err)
} }
// The Kind used for the /ephemeralcontainers subresource changed in 1.22. When presented with an unexpected
// Kind the api server will respond with a not-registered error. When this happens we can optimistically try
// using the old API.
if runtime.IsNotRegisteredError(err) {
klog.V(1).Infof("Falling back to legacy API because server returned error: %v", err)
return o.debugByEphemeralContainerLegacy(ctx, pod, debugContainer)
}
return nil, "", err return nil, "", err
} }
return result, debugContainer.Name, nil return result, debugContainer.Name, nil
} }
// applyCustomProfile applies given partial container json file on to the profile // debugByEphemeralContainerLegacy adds debugContainer as an ephemeral container using the pre-1.22 /ephemeralcontainers API
// incorporated debug pod. // This may be removed when we no longer wish to support releases prior to 1.22.
func (o *DebugOptions) applyCustomProfile(debugPod *corev1.Pod, containerName string) error { func (o *DebugOptions) debugByEphemeralContainerLegacy(ctx context.Context, pod *corev1.Pod, debugContainer *corev1.EphemeralContainer) (*corev1.Pod, string, error) {
o.CustomProfile.Name = containerName // We no longer have the v1.EphemeralContainers Kind since it was removed in 1.22, but
customJS, err := json.Marshal(o.CustomProfile) // we can present a JSON 6902 patch that the api server will apply.
patch, err := json.Marshal([]map[string]interface{}{{
"op": "add",
"path": "/ephemeralContainers/-",
"value": debugContainer,
}})
if err != nil { if err != nil {
return fmt.Errorf("unable to marshall custom profile: %w", err) return nil, "", fmt.Errorf("error creating JSON 6902 patch for old /ephemeralcontainers API: %s", err)
} }
var index int result := o.podClient.RESTClient().Patch(types.JSONPatchType).
found := false Namespace(pod.Namespace).
for i, val := range debugPod.Spec.Containers { Resource("pods").
if val.Name == containerName { Name(pod.Name).
index = i SubResource("ephemeralcontainers").
found = true Body(patch).
break Do(ctx)
} if err := result.Error(); err != nil {
return nil, "", err
} }
if !found { newPod, err := o.podClient.Pods(pod.Namespace).Get(ctx, pod.Name, metav1.GetOptions{})
return fmt.Errorf("unable to find the %s container in the pod %s", containerName, debugPod.Name)
}
var debugContainerJS []byte
debugContainerJS, err = json.Marshal(debugPod.Spec.Containers[index])
if err != nil { if err != nil {
return fmt.Errorf("unable to marshall container: %w", err) return nil, "", err
} }
patchedContainer, err := strategicpatch.StrategicMergePatch(debugContainerJS, customJS, corev1.Container{}) return newPod, debugContainer.Name, nil
if err != nil {
return fmt.Errorf("error creating three way patch to add debug container: %w", err)
}
err = json.Unmarshal(patchedContainer, &debugPod.Spec.Containers[index])
if err != nil {
return fmt.Errorf("unable to unmarshall patched container to container: %w", err)
}
return nil
}
// applyCustomProfileEphemeral applies given partial container json file on to the profile
// incorporated ephemeral container of the pod.
func (o *DebugOptions) applyCustomProfileEphemeral(debugPod *corev1.Pod, containerName string) error {
o.CustomProfile.Name = containerName
customJS, err := json.Marshal(o.CustomProfile)
if err != nil {
return fmt.Errorf("unable to marshall custom profile: %w", err)
}
var index int
found := false
for i, val := range debugPod.Spec.EphemeralContainers {
if val.Name == containerName {
index = i
found = true
break
}
}
if !found {
return fmt.Errorf("unable to find the %s ephemeral container in the pod %s", containerName, debugPod.Name)
}
var debugContainerJS []byte
debugContainerJS, err = json.Marshal(debugPod.Spec.EphemeralContainers[index])
if err != nil {
return fmt.Errorf("unable to marshall ephemeral container:%w", err)
}
patchedContainer, err := strategicpatch.StrategicMergePatch(debugContainerJS, customJS, corev1.Container{})
if err != nil {
return fmt.Errorf("error creating three way patch to add debug container: %w", err)
}
err = json.Unmarshal(patchedContainer, &debugPod.Spec.EphemeralContainers[index])
if err != nil {
return fmt.Errorf("unable to unmarshall patched container to ephemeral container: %w", err)
}
return nil
} }
// debugByCopy runs a copy of the target Pod with a debug container added or an original container modified // debugByCopy runs a copy of the target Pod with a debug container added or an original container modified
@ -616,16 +522,6 @@ func (o *DebugOptions) debugByCopy(ctx context.Context, pod *corev1.Pod) (*corev
if err != nil { if err != nil {
return nil, "", err return nil, "", err
} }
var debugContainer *corev1.Container
for i := range copied.Spec.Containers {
if copied.Spec.Containers[i].Name == dc {
debugContainer = &copied.Spec.Containers[i]
break
}
}
o.displayWarning(debugContainer, copied)
created, err := o.podClient.Pods(copied.Namespace).Create(ctx, copied, metav1.CreateOptions{}) created, err := o.podClient.Pods(copied.Namespace).Create(ctx, copied, metav1.CreateOptions{})
if err != nil { if err != nil {
return nil, "", err return nil, "", err
@ -639,32 +535,6 @@ func (o *DebugOptions) debugByCopy(ctx context.Context, pod *corev1.Pod) (*corev
return created, dc, nil return created, dc, nil
} }
// Display warning message if some capabilities are set by profile and non-root user is specified in .Spec.SecurityContext.RunAsUser.(#1650)
func (o *DebugOptions) displayWarning(container *corev1.Container, pod *corev1.Pod) {
if container == nil {
return
}
if pod.Spec.SecurityContext.RunAsUser == nil || *pod.Spec.SecurityContext.RunAsUser == 0 {
return
}
if container.SecurityContext == nil {
return
}
if container.SecurityContext.RunAsUser != nil && *container.SecurityContext.RunAsUser == 0 {
return
}
if (container.SecurityContext.Privileged == nil || !*container.SecurityContext.Privileged) &&
(container.SecurityContext.Capabilities == nil || len(container.SecurityContext.Capabilities.Add) == 0) {
return
}
_, _ = fmt.Fprintln(o.ErrOut, `Warning: Non-root user is configured for the entire target Pod, and some capabilities granted by debug profile may not work. Please consider using "--custom" with a custom profile that specifies "securityContext.runAsUser: 0".`)
}
// generateDebugContainer returns a debugging pod and an EphemeralContainer suitable for use as a debug container // generateDebugContainer returns a debugging pod and an EphemeralContainer suitable for use as a debug container
// in the given pod. // in the given pod.
func (o *DebugOptions) generateDebugContainer(pod *corev1.Pod) (*corev1.Pod, *corev1.EphemeralContainer, error) { func (o *DebugOptions) generateDebugContainer(pod *corev1.Pod) (*corev1.Pod, *corev1.EphemeralContainer, error) {
@ -694,13 +564,6 @@ func (o *DebugOptions) generateDebugContainer(pod *corev1.Pod) (*corev1.Pod, *co
return nil, nil, err return nil, nil, err
} }
if o.CustomProfile != nil {
err := o.applyCustomProfileEphemeral(copied, ec.Name)
if err != nil {
return nil, nil, err
}
}
ec = &copied.Spec.EphemeralContainers[len(copied.Spec.EphemeralContainers)-1] ec = &copied.Spec.EphemeralContainers[len(copied.Spec.EphemeralContainers)-1]
return copied, ec, nil return copied, ec, nil
@ -727,9 +590,6 @@ func (o *DebugOptions) generateNodeDebugPod(node *corev1.Node) (*corev1.Pod, err
p := &corev1.Pod{ p := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: pn, Name: pn,
Labels: map[string]string{
"app.kubernetes.io/managed-by": "kubectl-debug",
},
}, },
Spec: corev1.PodSpec{ Spec: corev1.PodSpec{
Containers: []corev1.Container{ Containers: []corev1.Container{
@ -763,13 +623,6 @@ func (o *DebugOptions) generateNodeDebugPod(node *corev1.Node) (*corev1.Pod, err
return nil, err return nil, err
} }
if o.CustomProfile != nil {
err := o.applyCustomProfile(p, cn)
if err != nil {
return nil, err
}
}
return p, nil return p, nil
} }
@ -780,7 +633,6 @@ func (o *DebugOptions) generatePodCopyWithDebugContainer(pod *corev1.Pod) (*core
Name: o.CopyTo, Name: o.CopyTo,
Namespace: pod.Namespace, Namespace: pod.Namespace,
Annotations: pod.Annotations, Annotations: pod.Annotations,
Labels: pod.Labels,
}, },
Spec: *pod.Spec.DeepCopy(), Spec: *pod.Spec.DeepCopy(),
} }
@ -788,7 +640,7 @@ func (o *DebugOptions) generatePodCopyWithDebugContainer(pod *corev1.Pod) (*core
copied.Spec.EphemeralContainers = nil copied.Spec.EphemeralContainers = nil
// change ShareProcessNamespace configuration only when commanded explicitly // change ShareProcessNamespace configuration only when commanded explicitly
if o.shareProcessedChanged { if o.shareProcessedChanged {
copied.Spec.ShareProcessNamespace = ptr.To(o.ShareProcesses) copied.Spec.ShareProcessNamespace = pointer.Bool(o.ShareProcesses)
} }
if !o.SameNode { if !o.SameNode {
copied.Spec.NodeName = "" copied.Spec.NodeName = ""
@ -853,13 +705,6 @@ func (o *DebugOptions) generatePodCopyWithDebugContainer(pod *corev1.Pod) (*core
return nil, "", err return nil, "", err
} }
if o.CustomProfile != nil {
err = o.applyCustomProfile(copied, name)
if err != nil {
return nil, "", err
}
}
return copied, name, nil return copied, name, nil
} }
@ -950,25 +795,7 @@ func (o *DebugOptions) waitForContainer(ctx context.Context, ns, podName, contai
return result, err return result, err
} }
func (o *DebugOptions) handleAttachPod(ctx context.Context, restClientGetter genericclioptions.RESTClientGetter, cmdPath string, ns, podName, containerName string) error { func (o *DebugOptions) handleAttachPod(ctx context.Context, restClientGetter genericclioptions.RESTClientGetter, ns, podName, containerName string, opts *attach.AttachOptions) error {
opts := &attach.AttachOptions{
StreamOptions: exec.StreamOptions{
IOStreams: o.IOStreams,
Stdin: o.Interactive,
TTY: o.TTY,
Quiet: o.Quiet,
},
CommandName: cmdPath + " attach",
Attach: &attach.DefaultRemoteAttach{},
}
config, err := restClientGetter.ToRESTConfig()
if err != nil {
return err
}
opts.Config = config
opts.AttachFunc = attach.DefaultAttachFunc
pod, err := o.waitForContainer(ctx, ns, podName, containerName) pod, err := o.waitForContainer(ctx, ns, podName, containerName)
if err != nil { if err != nil {
return err return err
@ -989,12 +816,12 @@ func (o *DebugOptions) handleAttachPod(ctx context.Context, restClientGetter gen
} }
if status.State.Terminated != nil { if status.State.Terminated != nil {
klog.V(1).Info("Ephemeral container terminated, falling back to logs") klog.V(1).Info("Ephemeral container terminated, falling back to logs")
return logOpts(ctx, restClientGetter, pod, opts) return logOpts(restClientGetter, pod, opts)
} }
if err := opts.Run(); err != nil { if err := opts.Run(); err != nil {
fmt.Fprintf(opts.ErrOut, "warning: couldn't attach to pod/%s, falling back to streaming logs: %v\n", podName, err) fmt.Fprintf(opts.ErrOut, "warning: couldn't attach to pod/%s, falling back to streaming logs: %v\n", podName, err)
return logOpts(ctx, restClientGetter, pod, opts) return logOpts(restClientGetter, pod, opts)
} }
return nil return nil
} }
@ -1012,7 +839,7 @@ func getContainerStatusByName(pod *corev1.Pod, containerName string) *corev1.Con
} }
// logOpts logs output from opts to the pods log. // logOpts logs output from opts to the pods log.
func logOpts(ctx context.Context, restClientGetter genericclioptions.RESTClientGetter, pod *corev1.Pod, opts *attach.AttachOptions) error { func logOpts(restClientGetter genericclioptions.RESTClientGetter, pod *corev1.Pod, opts *attach.AttachOptions) error {
ctrName, err := opts.GetContainerName(pod) ctrName, err := opts.GetContainerName(pod)
if err != nil { if err != nil {
return err return err
@ -1023,7 +850,7 @@ func logOpts(ctx context.Context, restClientGetter genericclioptions.RESTClientG
return err return err
} }
for _, request := range requests { for _, request := range requests {
if err := logs.DefaultConsumeRequest(ctx, request, opts.Out); err != nil { if err := logs.DefaultConsumeRequest(request, opts.Out); err != nil {
return err return err
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -22,7 +22,7 @@ import (
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/kubectl/pkg/util/podutils" "k8s.io/kubectl/pkg/util/podutils"
"k8s.io/utils/ptr" "k8s.io/utils/pointer"
) )
type debugStyle int type debugStyle int
@ -54,8 +54,6 @@ const (
ProfileRestricted = "restricted" ProfileRestricted = "restricted"
// ProfileNetadmin offers elevated privileges for network debugging. // ProfileNetadmin offers elevated privileges for network debugging.
ProfileNetadmin = "netadmin" ProfileNetadmin = "netadmin"
// ProfileSysadmin offers elevated privileges for debugging.
ProfileSysadmin = "sysadmin"
) )
type ProfileApplier interface { type ProfileApplier interface {
@ -64,91 +62,49 @@ type ProfileApplier interface {
} }
// NewProfileApplier returns a new Options for the given profile name. // NewProfileApplier returns a new Options for the given profile name.
func NewProfileApplier(profile string, kflags KeepFlags) (ProfileApplier, error) { func NewProfileApplier(profile string) (ProfileApplier, error) {
switch profile { switch profile {
case ProfileLegacy: case ProfileLegacy:
return &legacyProfile{kflags}, nil return &legacyProfile{}, nil
case ProfileGeneral: case ProfileGeneral:
return &generalProfile{kflags}, nil return &generalProfile{}, nil
case ProfileBaseline: case ProfileBaseline:
return &baselineProfile{kflags}, nil return &baselineProfile{}, nil
case ProfileRestricted: case ProfileRestricted:
return &restrictedProfile{kflags}, nil return &restrictedProfile{}, nil
case ProfileNetadmin: case ProfileNetadmin:
return &netadminProfile{kflags}, nil return &netadminProfile{}, nil
case ProfileSysadmin:
return &sysadminProfile{kflags}, nil
} }
return nil, fmt.Errorf("unknown profile: %s", profile) return nil, fmt.Errorf("unknown profile: %s", profile)
} }
type legacyProfile struct { type legacyProfile struct {
KeepFlags
} }
type generalProfile struct { type generalProfile struct {
KeepFlags
} }
type baselineProfile struct { type baselineProfile struct {
KeepFlags
} }
type restrictedProfile struct { type restrictedProfile struct {
KeepFlags
} }
type netadminProfile struct { type netadminProfile struct {
KeepFlags
} }
type sysadminProfile struct { func (p *legacyProfile) Apply(pod *corev1.Pod, containerName string, target runtime.Object) error {
KeepFlags switch target.(type) {
} case *corev1.Pod:
// do nothing to the copied pod
// KeepFlags holds the flag set that determine which fields to keep in the copy pod. return nil
type KeepFlags struct { case *corev1.Node:
Labels bool mountRootPartition(pod, containerName)
Annotations bool useHostNamespaces(pod)
Liveness bool return nil
Readiness bool default:
Startup bool return fmt.Errorf("the %s profile doesn't support objects of type %T", ProfileLegacy, target)
InitContainers bool
}
// RemoveLabels removes labels from the pod.
func (kflags KeepFlags) RemoveLabels(p *corev1.Pod) {
if !kflags.Labels {
p.Labels = nil
}
}
// RemoveAnnotations remove annotations from the pod.
func (kflags KeepFlags) RemoveAnnotations(p *corev1.Pod) {
if !kflags.Annotations {
p.Annotations = nil
}
}
// RemoveProbes remove probes from all containers of the pod.
func (kflags KeepFlags) RemoveProbes(p *corev1.Pod) {
for i := range p.Spec.Containers {
if !kflags.Liveness {
p.Spec.Containers[i].LivenessProbe = nil
}
if !kflags.Readiness {
p.Spec.Containers[i].ReadinessProbe = nil
}
if !kflags.Startup {
p.Spec.Containers[i].StartupProbe = nil
}
}
}
// RemoveInitContainers remove initContainers from the pod.
func (kflags KeepFlags) RemoveInitContainers(p *corev1.Pod) {
if !kflags.InitContainers {
p.Spec.InitContainers = nil
} }
} }
@ -167,32 +123,10 @@ func getDebugStyle(pod *corev1.Pod, target runtime.Object) (debugStyle, error) {
return unsupported, fmt.Errorf("objects of type %T are not supported", target) return unsupported, fmt.Errorf("objects of type %T are not supported", target)
} }
func (p *legacyProfile) Apply(pod *corev1.Pod, containerName string, target runtime.Object) error {
style, err := getDebugStyle(pod, target)
if err != nil {
return fmt.Errorf("legacy profile: %w", err)
}
switch style {
case node:
mountRootPartition(pod, containerName)
useHostNamespaces(pod)
case podCopy:
p.Labels = false
p.RemoveLabels(pod)
case ephemeral:
// no additional modifications needed
}
return nil
}
func (p *generalProfile) Apply(pod *corev1.Pod, containerName string, target runtime.Object) error { func (p *generalProfile) Apply(pod *corev1.Pod, containerName string, target runtime.Object) error {
style, err := getDebugStyle(pod, target) style, err := getDebugStyle(pod, target)
if err != nil { if err != nil {
return fmt.Errorf("general profile: %w", err) return fmt.Errorf("general profile: %s", err)
} }
switch style { switch style {
@ -202,10 +136,7 @@ func (p *generalProfile) Apply(pod *corev1.Pod, containerName string, target run
useHostNamespaces(pod) useHostNamespaces(pod)
case podCopy: case podCopy:
p.RemoveLabels(pod) removeLabelsAndProbes(pod)
p.RemoveAnnotations(pod)
p.RemoveProbes(pod)
p.RemoveInitContainers(pod)
allowProcessTracing(pod, containerName) allowProcessTracing(pod, containerName)
shareProcessNamespace(pod) shareProcessNamespace(pod)
@ -219,17 +150,14 @@ func (p *generalProfile) Apply(pod *corev1.Pod, containerName string, target run
func (p *baselineProfile) Apply(pod *corev1.Pod, containerName string, target runtime.Object) error { func (p *baselineProfile) Apply(pod *corev1.Pod, containerName string, target runtime.Object) error {
style, err := getDebugStyle(pod, target) style, err := getDebugStyle(pod, target)
if err != nil { if err != nil {
return fmt.Errorf("baseline profile: %w", err) return fmt.Errorf("baseline profile: %s", err)
} }
clearSecurityContext(pod, containerName) clearSecurityContext(pod, containerName)
switch style { switch style {
case podCopy: case podCopy:
p.RemoveLabels(pod) removeLabelsAndProbes(pod)
p.RemoveAnnotations(pod)
p.RemoveProbes(pod)
p.RemoveInitContainers(pod)
shareProcessNamespace(pod) shareProcessNamespace(pod)
case ephemeral, node: case ephemeral, node:
@ -242,7 +170,7 @@ func (p *baselineProfile) Apply(pod *corev1.Pod, containerName string, target ru
func (p *restrictedProfile) Apply(pod *corev1.Pod, containerName string, target runtime.Object) error { func (p *restrictedProfile) Apply(pod *corev1.Pod, containerName string, target runtime.Object) error {
style, err := getDebugStyle(pod, target) style, err := getDebugStyle(pod, target)
if err != nil { if err != nil {
return fmt.Errorf("restricted profile: %w", err) return fmt.Errorf("restricted profile: %s", err)
} }
clearSecurityContext(pod, containerName) clearSecurityContext(pod, containerName)
@ -253,10 +181,6 @@ func (p *restrictedProfile) Apply(pod *corev1.Pod, containerName string, target
switch style { switch style {
case podCopy: case podCopy:
p.RemoveLabels(pod)
p.RemoveAnnotations(pod)
p.RemoveProbes(pod)
p.RemoveInitContainers(pod)
shareProcessNamespace(pod) shareProcessNamespace(pod)
case ephemeral, node: case ephemeral, node:
@ -269,7 +193,7 @@ func (p *restrictedProfile) Apply(pod *corev1.Pod, containerName string, target
func (p *netadminProfile) Apply(pod *corev1.Pod, containerName string, target runtime.Object) error { func (p *netadminProfile) Apply(pod *corev1.Pod, containerName string, target runtime.Object) error {
style, err := getDebugStyle(pod, target) style, err := getDebugStyle(pod, target)
if err != nil { if err != nil {
return fmt.Errorf("netadmin profile: %w", err) return fmt.Errorf("netadmin profile: %s", err)
} }
allowNetadminCapability(pod, containerName) allowNetadminCapability(pod, containerName)
@ -277,47 +201,24 @@ func (p *netadminProfile) Apply(pod *corev1.Pod, containerName string, target ru
switch style { switch style {
case node: case node:
useHostNamespaces(pod) useHostNamespaces(pod)
setPrivileged(pod, containerName)
case podCopy: case podCopy, ephemeral:
p.RemoveLabels(pod)
p.RemoveAnnotations(pod)
p.RemoveProbes(pod)
p.RemoveInitContainers(pod)
shareProcessNamespace(pod)
case ephemeral:
// no additional modifications needed // no additional modifications needed
} }
return nil return nil
} }
func (p *sysadminProfile) Apply(pod *corev1.Pod, containerName string, target runtime.Object) error { // removeLabelsAndProbes removes labels from the pod and remove probes
style, err := getDebugStyle(pod, target) // from all containers of the pod.
if err != nil { func removeLabelsAndProbes(p *corev1.Pod) {
return fmt.Errorf("sysadmin profile: %w", err) p.Labels = nil
for i := range p.Spec.Containers {
p.Spec.Containers[i].LivenessProbe = nil
p.Spec.Containers[i].ReadinessProbe = nil
p.Spec.Containers[i].StartupProbe = nil
} }
setPrivileged(pod, containerName)
switch style {
case node:
useHostNamespaces(pod)
mountRootPartition(pod, containerName)
case podCopy:
// to mimic general, default and baseline
p.RemoveLabels(pod)
p.RemoveAnnotations(pod)
p.RemoveProbes(pod)
p.RemoveInitContainers(pod)
shareProcessNamespace(pod)
case ephemeral:
// no additional modifications needed
}
return nil
} }
// mountRootPartition mounts the host's root path at "/host" in the container. // mountRootPartition mounts the host's root path at "/host" in the container.
@ -353,7 +254,7 @@ func useHostNamespaces(p *corev1.Pod) {
// process namespace. // process namespace.
func shareProcessNamespace(p *corev1.Pod) { func shareProcessNamespace(p *corev1.Pod) {
if p.Spec.ShareProcessNamespace == nil { if p.Spec.ShareProcessNamespace == nil {
p.Spec.ShareProcessNamespace = ptr.To(true) p.Spec.ShareProcessNamespace = pointer.Bool(true)
} }
} }
@ -377,7 +278,7 @@ func setPrivileged(p *corev1.Pod, containerName string) {
if c.SecurityContext == nil { if c.SecurityContext == nil {
c.SecurityContext = &corev1.SecurityContext{} c.SecurityContext = &corev1.SecurityContext{}
} }
c.SecurityContext.Privileged = ptr.To(true) c.SecurityContext.Privileged = pointer.Bool(true)
return false return false
}) })
} }
@ -391,7 +292,7 @@ func disallowRoot(p *corev1.Pod, containerName string) {
if c.SecurityContext == nil { if c.SecurityContext == nil {
c.SecurityContext = &corev1.SecurityContext{} c.SecurityContext = &corev1.SecurityContext{}
} }
c.SecurityContext.RunAsNonRoot = ptr.To(true) c.SecurityContext.RunAsNonRoot = pointer.Bool(true)
return false return false
}) })
} }
@ -425,14 +326,13 @@ func allowProcessTracing(p *corev1.Pod, containerName string) {
}) })
} }
// allowNetadminCapability grants NET_ADMIN and NET_RAW capability to the container. // allowNetadminCapability grants NET_ADMIN capability to the container.
func allowNetadminCapability(p *corev1.Pod, containerName string) { func allowNetadminCapability(p *corev1.Pod, containerName string) {
podutils.VisitContainers(&p.Spec, podutils.AllContainers, func(c *corev1.Container, _ podutils.ContainerType) bool { podutils.VisitContainers(&p.Spec, podutils.AllContainers, func(c *corev1.Container, _ podutils.ContainerType) bool {
if c.Name != containerName { if c.Name != containerName {
return true return true
} }
addCapability(c, "NET_ADMIN") addCapability(c, "NET_ADMIN")
addCapability(c, "NET_RAW")
return false return false
}) })
} }
@ -456,7 +356,7 @@ func disallowPrivilegeEscalation(p *corev1.Pod, containerName string) {
if c.SecurityContext == nil { if c.SecurityContext == nil {
c.SecurityContext = &corev1.SecurityContext{} c.SecurityContext = &corev1.SecurityContext{}
} }
c.SecurityContext.AllowPrivilegeEscalation = ptr.To(false) c.SecurityContext.AllowPrivilegeEscalation = pointer.Bool(false)
return false return false
}) })
} }

View File

@ -25,7 +25,7 @@ import (
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/utils/ptr" "k8s.io/utils/pointer"
) )
var testNode = &corev1.Node{ var testNode = &corev1.Node{
@ -34,203 +34,6 @@ var testNode = &corev1.Node{
}, },
} }
func TestLegacyProfile(t *testing.T) {
pod := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "pod"},
Spec: corev1.PodSpec{EphemeralContainers: []corev1.EphemeralContainer{
{
EphemeralContainerCommon: corev1.EphemeralContainerCommon{
Name: "dbg", Image: "dbgimage",
},
},
}},
}
tests := map[string]struct {
pod *corev1.Pod
containerName string
target runtime.Object
expectPod *corev1.Pod
expectErr bool
}{
"bad inputs results in error": {
pod: nil,
containerName: "dbg",
target: runtime.Object(nil),
expectErr: true,
},
"debug by ephemeral container": {
pod: pod,
containerName: "dbg",
target: pod,
expectPod: &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "pod"},
Spec: corev1.PodSpec{EphemeralContainers: []corev1.EphemeralContainer{
{
EphemeralContainerCommon: corev1.EphemeralContainerCommon{Name: "dbg", Image: "dbgimage"},
},
}},
},
},
"debug by pod copy": {
pod: &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "podcopy",
Labels: map[string]string{
"app": "podcopy",
},
Annotations: map[string]string{
"test": "test",
},
},
Spec: corev1.PodSpec{
InitContainers: []corev1.Container{{Name: "init-container"}},
Containers: []corev1.Container{
{
Name: "app",
Image: "appimage",
LivenessProbe: &corev1.Probe{},
ReadinessProbe: &corev1.Probe{},
StartupProbe: &corev1.Probe{},
},
{
Name: "dbg",
Image: "dbgimage",
SecurityContext: &corev1.SecurityContext{
Capabilities: &corev1.Capabilities{
Add: []corev1.Capability{"NET_ADMIN"},
},
},
},
},
},
},
containerName: "dbg",
target: &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "podcopy",
Labels: map[string]string{
"app": "podcopy",
},
Annotations: map[string]string{
"test": "test",
},
},
Spec: corev1.PodSpec{
InitContainers: []corev1.Container{{Name: "init-container"}},
Containers: []corev1.Container{
{
Name: "app",
Image: "appimage",
LivenessProbe: &corev1.Probe{},
ReadinessProbe: &corev1.Probe{},
StartupProbe: &corev1.Probe{},
},
},
},
},
expectPod: &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "podcopy",
Annotations: map[string]string{
"test": "test",
},
},
Spec: corev1.PodSpec{
InitContainers: []corev1.Container{{Name: "init-container"}},
Containers: []corev1.Container{
{
Name: "app",
Image: "appimage",
LivenessProbe: &corev1.Probe{},
ReadinessProbe: &corev1.Probe{},
StartupProbe: &corev1.Probe{},
},
{
Name: "dbg",
Image: "dbgimage",
SecurityContext: &corev1.SecurityContext{
Capabilities: &corev1.Capabilities{
Add: []corev1.Capability{"NET_ADMIN"},
},
},
},
},
},
},
},
"debug by node": {
pod: &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "pod"},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "dbg",
Image: "dbgimage",
SecurityContext: &corev1.SecurityContext{
Capabilities: &corev1.Capabilities{
Add: []corev1.Capability{"NET_ADMIN"},
},
},
},
},
},
},
containerName: "dbg",
target: testNode,
expectPod: &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "pod"},
Spec: corev1.PodSpec{
HostNetwork: true,
HostPID: true,
HostIPC: true,
Containers: []corev1.Container{
{
Name: "dbg",
Image: "dbgimage",
SecurityContext: &corev1.SecurityContext{
Capabilities: &corev1.Capabilities{
Add: []corev1.Capability{"NET_ADMIN"},
},
},
VolumeMounts: []corev1.VolumeMount{
{
MountPath: "/host",
Name: "host-root",
},
},
},
},
Volumes: []corev1.Volume{
{
Name: "host-root",
VolumeSource: corev1.VolumeSource{
HostPath: &corev1.HostPathVolumeSource{Path: "/"},
},
},
},
},
},
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
applier := &legacyProfile{KeepFlags{InitContainers: true}}
err := applier.Apply(test.pod, test.containerName, test.target)
if (err != nil) != test.expectErr {
t.Fatalf("expect error: %v, got error: %v", test.expectErr, (err != nil))
}
if err != nil {
return
}
if diff := cmp.Diff(test.expectPod, test.pod); diff != "" {
t.Error("unexpected diff in generated object: (-want +got):\n", diff)
}
})
}
}
func TestGeneralProfile(t *testing.T) { func TestGeneralProfile(t *testing.T) {
pod := &corev1.Pod{ pod := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "pod"}, ObjectMeta: metav1.ObjectMeta{Name: "pod"},
@ -278,25 +81,10 @@ func TestGeneralProfile(t *testing.T) {
}, },
"debug by pod copy": { "debug by pod copy": {
pod: &corev1.Pod{ pod: &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{Name: "podcopy"},
Name: "podcopy",
Labels: map[string]string{
"app": "podcopy",
},
Annotations: map[string]string{
"test": "test",
},
},
Spec: corev1.PodSpec{ Spec: corev1.PodSpec{
InitContainers: []corev1.Container{{Name: "init-container"}},
Containers: []corev1.Container{ Containers: []corev1.Container{
{ {Name: "app", Image: "appimage"},
Name: "app",
Image: "appimage",
LivenessProbe: &corev1.Probe{},
ReadinessProbe: &corev1.Probe{},
StartupProbe: &corev1.Probe{},
},
{ {
Name: "dbg", Name: "dbg",
Image: "dbgimage", Image: "dbgimage",
@ -311,32 +99,16 @@ func TestGeneralProfile(t *testing.T) {
}, },
containerName: "dbg", containerName: "dbg",
target: &corev1.Pod{ target: &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{Name: "podcopy"},
Name: "podcopy",
Labels: map[string]string{
"app": "podcopy",
},
Annotations: map[string]string{
"test": "test",
},
},
Spec: corev1.PodSpec{ Spec: corev1.PodSpec{
InitContainers: []corev1.Container{{Name: "init-container"}},
Containers: []corev1.Container{ Containers: []corev1.Container{
{ {Name: "app", Image: "appimage"},
Name: "app",
Image: "appimage",
LivenessProbe: &corev1.Probe{},
ReadinessProbe: &corev1.Probe{},
StartupProbe: &corev1.Probe{},
},
}, },
}, },
}, },
expectPod: &corev1.Pod{ expectPod: &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "podcopy"}, ObjectMeta: metav1.ObjectMeta{Name: "podcopy"},
Spec: corev1.PodSpec{ Spec: corev1.PodSpec{
InitContainers: []corev1.Container{{Name: "init-container"}},
Containers: []corev1.Container{ Containers: []corev1.Container{
{Name: "app", Image: "appimage"}, {Name: "app", Image: "appimage"},
{ {
@ -349,7 +121,7 @@ func TestGeneralProfile(t *testing.T) {
}, },
}, },
}, },
ShareProcessNamespace: ptr.To(true), ShareProcessNamespace: pointer.Bool(true),
}, },
}, },
}, },
@ -397,8 +169,7 @@ func TestGeneralProfile(t *testing.T) {
for name, test := range tests { for name, test := range tests {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
applier := &generalProfile{KeepFlags{InitContainers: true}} err := (&generalProfile{}).Apply(test.pod, test.containerName, test.target)
err := applier.Apply(test.pod, test.containerName, test.target)
if (err != nil) != test.expectErr { if (err != nil) != test.expectErr {
t.Fatalf("expect error: %v, got error: %v", test.expectErr, (err != nil)) t.Fatalf("expect error: %v, got error: %v", test.expectErr, (err != nil))
} }
@ -459,58 +230,27 @@ func TestBaselineProfile(t *testing.T) {
}, },
"debug by pod copy": { "debug by pod copy": {
pod: &corev1.Pod{ pod: &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{Name: "podcopy"},
Name: "podcopy",
Labels: map[string]string{
"app": "podcopy",
},
Annotations: map[string]string{
"test": "test",
},
},
Spec: corev1.PodSpec{ Spec: corev1.PodSpec{
InitContainers: []corev1.Container{{Name: "init-container"}},
Containers: []corev1.Container{ Containers: []corev1.Container{
{ {Name: "app", Image: "appimage"},
Name: "app",
Image: "appimage",
LivenessProbe: &corev1.Probe{},
ReadinessProbe: &corev1.Probe{},
StartupProbe: &corev1.Probe{},
},
{Name: "dbg", Image: "dbgimage"}, {Name: "dbg", Image: "dbgimage"},
}, },
}, },
}, },
containerName: "dbg", containerName: "dbg",
target: &corev1.Pod{ target: &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{Name: "podcopy"},
Name: "podcopy",
Labels: map[string]string{
"app": "podcopy",
},
Annotations: map[string]string{
"test": "test",
},
},
Spec: corev1.PodSpec{ Spec: corev1.PodSpec{
InitContainers: []corev1.Container{{Name: "init-container"}},
Containers: []corev1.Container{ Containers: []corev1.Container{
{ {Name: "app", Image: "appimage"},
Name: "app",
Image: "appimage",
LivenessProbe: &corev1.Probe{},
ReadinessProbe: &corev1.Probe{},
StartupProbe: &corev1.Probe{},
},
}, },
}, },
}, },
expectPod: &corev1.Pod{ expectPod: &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "podcopy"}, ObjectMeta: metav1.ObjectMeta{Name: "podcopy"},
Spec: corev1.PodSpec{ Spec: corev1.PodSpec{
ShareProcessNamespace: ptr.To(true), ShareProcessNamespace: pointer.Bool(true),
InitContainers: []corev1.Container{{Name: "init-container"}},
Containers: []corev1.Container{ Containers: []corev1.Container{
{Name: "app", Image: "appimage"}, {Name: "app", Image: "appimage"},
{ {
@ -548,8 +288,7 @@ func TestBaselineProfile(t *testing.T) {
for name, test := range tests { for name, test := range tests {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
applier := &baselineProfile{KeepFlags{InitContainers: true}} err := (&baselineProfile{}).Apply(test.pod, test.containerName, test.target)
err := applier.Apply(test.pod, test.containerName, test.target)
if (err != nil) != test.expectErr { if (err != nil) != test.expectErr {
t.Fatalf("expect error: %v, got error: %v", test.expectErr, (err != nil)) t.Fatalf("expect error: %v, got error: %v", test.expectErr, (err != nil))
} }
@ -604,11 +343,11 @@ func TestRestrictedProfile(t *testing.T) {
EphemeralContainerCommon: corev1.EphemeralContainerCommon{ EphemeralContainerCommon: corev1.EphemeralContainerCommon{
Name: "dbg", Image: "dbgimage", Name: "dbg", Image: "dbgimage",
SecurityContext: &corev1.SecurityContext{ SecurityContext: &corev1.SecurityContext{
RunAsNonRoot: ptr.To(true), RunAsNonRoot: pointer.Bool(true),
Capabilities: &corev1.Capabilities{ Capabilities: &corev1.Capabilities{
Drop: []corev1.Capability{"ALL"}, Drop: []corev1.Capability{"ALL"},
}, },
AllowPrivilegeEscalation: ptr.To(false), AllowPrivilegeEscalation: pointer.Bool(false),
SeccompProfile: &corev1.SeccompProfile{Type: "RuntimeDefault"}, SeccompProfile: &corev1.SeccompProfile{Type: "RuntimeDefault"},
}, },
}, },
@ -618,69 +357,38 @@ func TestRestrictedProfile(t *testing.T) {
}, },
"debug by pod copy": { "debug by pod copy": {
pod: &corev1.Pod{ pod: &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{Name: "podcopy"},
Name: "podcopy",
Labels: map[string]string{
"app": "podcopy",
},
Annotations: map[string]string{
"test": "test",
},
},
Spec: corev1.PodSpec{ Spec: corev1.PodSpec{
InitContainers: []corev1.Container{{Name: "init-container"}},
Containers: []corev1.Container{ Containers: []corev1.Container{
{ {Name: "app", Image: "appimage"},
Name: "app",
Image: "appimage",
LivenessProbe: &corev1.Probe{},
ReadinessProbe: &corev1.Probe{},
StartupProbe: &corev1.Probe{},
},
{Name: "dbg", Image: "dbgimage"}, {Name: "dbg", Image: "dbgimage"},
}, },
}, },
}, },
containerName: "dbg", containerName: "dbg",
target: &corev1.Pod{ target: &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{Name: "podcopy"},
Name: "podcopy",
Labels: map[string]string{
"app": "podcopy",
},
Annotations: map[string]string{
"test": "test",
},
},
Spec: corev1.PodSpec{ Spec: corev1.PodSpec{
InitContainers: []corev1.Container{{Name: "init-container"}},
Containers: []corev1.Container{ Containers: []corev1.Container{
{ {Name: "app", Image: "appimage"},
Name: "app",
Image: "appimage",
LivenessProbe: &corev1.Probe{},
ReadinessProbe: &corev1.Probe{},
StartupProbe: &corev1.Probe{},
},
}, },
}, },
}, },
expectPod: &corev1.Pod{ expectPod: &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "podcopy"}, ObjectMeta: metav1.ObjectMeta{Name: "podcopy"},
Spec: corev1.PodSpec{ Spec: corev1.PodSpec{
ShareProcessNamespace: ptr.To(true), ShareProcessNamespace: pointer.Bool(true),
InitContainers: []corev1.Container{{Name: "init-container"}},
Containers: []corev1.Container{ Containers: []corev1.Container{
{Name: "app", Image: "appimage"}, {Name: "app", Image: "appimage"},
{ {
Name: "dbg", Name: "dbg",
Image: "dbgimage", Image: "dbgimage",
SecurityContext: &corev1.SecurityContext{ SecurityContext: &corev1.SecurityContext{
RunAsNonRoot: ptr.To(true), RunAsNonRoot: pointer.Bool(true),
Capabilities: &corev1.Capabilities{ Capabilities: &corev1.Capabilities{
Drop: []corev1.Capability{"ALL"}, Drop: []corev1.Capability{"ALL"},
}, },
AllowPrivilegeEscalation: ptr.To(false), AllowPrivilegeEscalation: pointer.Bool(false),
SeccompProfile: &corev1.SeccompProfile{Type: "RuntimeDefault"}, SeccompProfile: &corev1.SeccompProfile{Type: "RuntimeDefault"},
}, },
}, },
@ -700,7 +408,7 @@ func TestRestrictedProfile(t *testing.T) {
Capabilities: &corev1.Capabilities{ Capabilities: &corev1.Capabilities{
Add: []corev1.Capability{"ALL"}, Add: []corev1.Capability{"ALL"},
}, },
AllowPrivilegeEscalation: ptr.To(false), AllowPrivilegeEscalation: pointer.Bool(false),
SeccompProfile: &corev1.SeccompProfile{Type: "RuntimeDefault"}, SeccompProfile: &corev1.SeccompProfile{Type: "RuntimeDefault"},
}, },
}, },
@ -717,11 +425,11 @@ func TestRestrictedProfile(t *testing.T) {
Name: "dbg", Name: "dbg",
Image: "dbgimage", Image: "dbgimage",
SecurityContext: &corev1.SecurityContext{ SecurityContext: &corev1.SecurityContext{
RunAsNonRoot: ptr.To(true), RunAsNonRoot: pointer.Bool(true),
Capabilities: &corev1.Capabilities{ Capabilities: &corev1.Capabilities{
Drop: []corev1.Capability{"ALL"}, Drop: []corev1.Capability{"ALL"},
}, },
AllowPrivilegeEscalation: ptr.To(false), AllowPrivilegeEscalation: pointer.Bool(false),
SeccompProfile: &corev1.SeccompProfile{Type: "RuntimeDefault"}, SeccompProfile: &corev1.SeccompProfile{Type: "RuntimeDefault"},
}, },
}, },
@ -733,8 +441,7 @@ func TestRestrictedProfile(t *testing.T) {
for name, test := range tests { for name, test := range tests {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
applier := &restrictedProfile{KeepFlags{InitContainers: true}} err := (&restrictedProfile{}).Apply(test.pod, test.containerName, test.target)
err := applier.Apply(test.pod, test.containerName, test.target)
if (err != nil) != test.expectErr { if (err != nil) != test.expectErr {
t.Fatalf("expect error: %v, got error: %v", test.expectErr, (err != nil)) t.Fatalf("expect error: %v, got error: %v", test.expectErr, (err != nil))
} }
@ -788,7 +495,7 @@ func TestNetAdminProfile(t *testing.T) {
Name: "dbg", Image: "dbgimage", Name: "dbg", Image: "dbgimage",
SecurityContext: &corev1.SecurityContext{ SecurityContext: &corev1.SecurityContext{
Capabilities: &corev1.Capabilities{ Capabilities: &corev1.Capabilities{
Add: []corev1.Capability{"NET_ADMIN", "NET_RAW"}, Add: []corev1.Capability{"NET_ADMIN"},
}, },
}, },
}, },
@ -799,58 +506,26 @@ func TestNetAdminProfile(t *testing.T) {
{ {
name: "debug by pod copy", name: "debug by pod copy",
pod: &corev1.Pod{ pod: &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{Name: "podcopy"},
Name: "podcopy",
Labels: map[string]string{
"app": "podcopy",
},
Annotations: map[string]string{
"test": "test",
},
},
Spec: corev1.PodSpec{ Spec: corev1.PodSpec{
InitContainers: []corev1.Container{{Name: "init-container"}},
Containers: []corev1.Container{ Containers: []corev1.Container{
{ {Name: "app", Image: "appimage"},
Name: "app",
Image: "appimage",
LivenessProbe: &corev1.Probe{},
ReadinessProbe: &corev1.Probe{},
StartupProbe: &corev1.Probe{},
},
{Name: "dbg", Image: "dbgimage"}, {Name: "dbg", Image: "dbgimage"},
}, },
}, },
}, },
containerName: "dbg", containerName: "dbg",
target: &corev1.Pod{ target: &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{Name: "podcopy"},
Name: "podcopy",
Labels: map[string]string{
"app": "podcopy",
},
Annotations: map[string]string{
"test": "test",
},
},
Spec: corev1.PodSpec{ Spec: corev1.PodSpec{
InitContainers: []corev1.Container{{Name: "init-container"}},
Containers: []corev1.Container{ Containers: []corev1.Container{
{ {Name: "app", Image: "appimage"},
Name: "app",
Image: "appimage",
LivenessProbe: &corev1.Probe{},
ReadinessProbe: &corev1.Probe{},
StartupProbe: &corev1.Probe{},
},
}, },
}, },
}, },
expectPod: &corev1.Pod{ expectPod: &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "podcopy"}, ObjectMeta: metav1.ObjectMeta{Name: "podcopy"},
Spec: corev1.PodSpec{ Spec: corev1.PodSpec{
ShareProcessNamespace: ptr.To(true),
InitContainers: []corev1.Container{{Name: "init-container"}},
Containers: []corev1.Container{ Containers: []corev1.Container{
{Name: "app", Image: "appimage"}, {Name: "app", Image: "appimage"},
{ {
@ -858,7 +533,7 @@ func TestNetAdminProfile(t *testing.T) {
Image: "dbgimage", Image: "dbgimage",
SecurityContext: &corev1.SecurityContext{ SecurityContext: &corev1.SecurityContext{
Capabilities: &corev1.Capabilities{ Capabilities: &corev1.Capabilities{
Add: []corev1.Capability{"NET_ADMIN", "NET_RAW"}, Add: []corev1.Capability{"NET_ADMIN"},
}, },
}, },
}, },
@ -872,13 +547,7 @@ func TestNetAdminProfile(t *testing.T) {
ObjectMeta: metav1.ObjectMeta{Name: "podcopy"}, ObjectMeta: metav1.ObjectMeta{Name: "podcopy"},
Spec: corev1.PodSpec{ Spec: corev1.PodSpec{
Containers: []corev1.Container{ Containers: []corev1.Container{
{ {Name: "app", Image: "appimage"},
Name: "app",
Image: "appimage",
LivenessProbe: &corev1.Probe{},
ReadinessProbe: &corev1.Probe{},
StartupProbe: &corev1.Probe{},
},
{ {
Name: "dbg", Name: "dbg",
Image: "dbgimage", Image: "dbgimage",
@ -896,20 +565,13 @@ func TestNetAdminProfile(t *testing.T) {
ObjectMeta: metav1.ObjectMeta{Name: "podcopy"}, ObjectMeta: metav1.ObjectMeta{Name: "podcopy"},
Spec: corev1.PodSpec{ Spec: corev1.PodSpec{
Containers: []corev1.Container{ Containers: []corev1.Container{
{ {Name: "app", Image: "appimage"},
Name: "app",
Image: "appimage",
LivenessProbe: &corev1.Probe{},
ReadinessProbe: &corev1.Probe{},
StartupProbe: &corev1.Probe{},
},
}, },
}, },
}, },
expectPod: &corev1.Pod{ expectPod: &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "podcopy"}, ObjectMeta: metav1.ObjectMeta{Name: "podcopy"},
Spec: corev1.PodSpec{ Spec: corev1.PodSpec{
ShareProcessNamespace: ptr.To(true),
Containers: []corev1.Container{ Containers: []corev1.Container{
{Name: "app", Image: "appimage"}, {Name: "app", Image: "appimage"},
{ {
@ -917,7 +579,7 @@ func TestNetAdminProfile(t *testing.T) {
Image: "dbgimage", Image: "dbgimage",
SecurityContext: &corev1.SecurityContext{ SecurityContext: &corev1.SecurityContext{
Capabilities: &corev1.Capabilities{ Capabilities: &corev1.Capabilities{
Add: []corev1.Capability{"SYS_PTRACE", "NET_ADMIN", "NET_RAW"}, Add: []corev1.Capability{"SYS_PTRACE", "NET_ADMIN"},
}, },
}, },
}, },
@ -948,8 +610,9 @@ func TestNetAdminProfile(t *testing.T) {
Name: "dbg", Name: "dbg",
Image: "dbgimage", Image: "dbgimage",
SecurityContext: &corev1.SecurityContext{ SecurityContext: &corev1.SecurityContext{
Privileged: pointer.BoolPtr(true),
Capabilities: &corev1.Capabilities{ Capabilities: &corev1.Capabilities{
Add: []corev1.Capability{"NET_ADMIN", "NET_RAW"}, Add: []corev1.Capability{"NET_ADMIN"},
}, },
}, },
}, },
@ -967,6 +630,7 @@ func TestNetAdminProfile(t *testing.T) {
Name: "dbg", Name: "dbg",
Image: "dbgimage", Image: "dbgimage",
SecurityContext: &corev1.SecurityContext{ SecurityContext: &corev1.SecurityContext{
Privileged: pointer.BoolPtr(true),
Capabilities: &corev1.Capabilities{ Capabilities: &corev1.Capabilities{
Add: []corev1.Capability{"SYS_PTRACE"}, Add: []corev1.Capability{"SYS_PTRACE"},
}, },
@ -988,8 +652,9 @@ func TestNetAdminProfile(t *testing.T) {
Name: "dbg", Name: "dbg",
Image: "dbgimage", Image: "dbgimage",
SecurityContext: &corev1.SecurityContext{ SecurityContext: &corev1.SecurityContext{
Privileged: pointer.BoolPtr(true),
Capabilities: &corev1.Capabilities{ Capabilities: &corev1.Capabilities{
Add: []corev1.Capability{"SYS_PTRACE", "NET_ADMIN", "NET_RAW"}, Add: []corev1.Capability{"SYS_PTRACE", "NET_ADMIN"},
}, },
}, },
}, },
@ -1001,286 +666,7 @@ func TestNetAdminProfile(t *testing.T) {
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
applier := &netadminProfile{KeepFlags{InitContainers: true}} err := (&netadminProfile{}).Apply(test.pod, test.containerName, test.target)
err := applier.Apply(test.pod, test.containerName, test.target)
if (err == nil) != (test.expectErr == nil) || (err != nil && test.expectErr != nil && err.Error() != test.expectErr.Error()) {
t.Fatalf("expect error: %v, got error: %v", test.expectErr, err)
}
if err != nil {
return
}
if diff := cmp.Diff(test.expectPod, test.pod); diff != "" {
t.Error("unexpected diff in generated object: (-want +got):\n", diff)
}
})
}
}
func TestSysAdminProfile(t *testing.T) {
pod := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "pod"},
Spec: corev1.PodSpec{EphemeralContainers: []corev1.EphemeralContainer{
{
EphemeralContainerCommon: corev1.EphemeralContainerCommon{
Name: "dbg", Image: "dbgimage",
},
},
}},
}
tests := []struct {
name string
pod *corev1.Pod
containerName string
target runtime.Object
expectPod *corev1.Pod
expectErr error
}{
{
name: "nil target",
pod: pod,
containerName: "dbg",
target: nil,
expectErr: fmt.Errorf("sysadmin profile: objects of type <nil> are not supported"),
},
{
name: "debug by ephemeral container",
pod: pod,
containerName: "dbg",
target: pod,
expectPod: &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "pod"},
Spec: corev1.PodSpec{EphemeralContainers: []corev1.EphemeralContainer{
{
EphemeralContainerCommon: corev1.EphemeralContainerCommon{
Name: "dbg", Image: "dbgimage",
SecurityContext: &corev1.SecurityContext{
Privileged: ptr.To(true),
},
},
},
}},
},
},
{
name: "debug by pod copy",
pod: &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "podcopy",
Labels: map[string]string{
"app": "podcopy",
},
Annotations: map[string]string{
"test": "test",
},
},
Spec: corev1.PodSpec{
InitContainers: []corev1.Container{{Name: "init-container"}},
Containers: []corev1.Container{
{
Name: "app",
Image: "appimage",
LivenessProbe: &corev1.Probe{},
ReadinessProbe: &corev1.Probe{},
StartupProbe: &corev1.Probe{},
},
{Name: "dbg", Image: "dbgimage"},
},
},
},
containerName: "dbg",
target: &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "podcopy",
Labels: map[string]string{
"app": "podcopy",
},
Annotations: map[string]string{
"test": "test",
},
},
Spec: corev1.PodSpec{
InitContainers: []corev1.Container{{Name: "init-container"}},
Containers: []corev1.Container{
{
Name: "app",
Image: "appimage",
LivenessProbe: &corev1.Probe{},
ReadinessProbe: &corev1.Probe{},
StartupProbe: &corev1.Probe{},
},
},
},
},
expectPod: &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "podcopy"},
Spec: corev1.PodSpec{
InitContainers: []corev1.Container{{Name: "init-container"}},
Containers: []corev1.Container{
{Name: "app", Image: "appimage"},
{
Name: "dbg",
Image: "dbgimage",
SecurityContext: &corev1.SecurityContext{
Privileged: ptr.To(true),
},
},
},
ShareProcessNamespace: ptr.To(true),
},
},
},
{
name: "debug by pod copy preserve existing capability",
pod: &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "podcopy"},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "app",
Image: "appimage",
LivenessProbe: &corev1.Probe{},
ReadinessProbe: &corev1.Probe{},
StartupProbe: &corev1.Probe{},
},
{
Name: "dbg",
Image: "dbgimage",
SecurityContext: &corev1.SecurityContext{
Capabilities: &corev1.Capabilities{
Add: []corev1.Capability{"SYS_PTRACE"},
},
},
},
},
},
},
containerName: "dbg",
target: &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "podcopy"},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "app",
Image: "appimage",
LivenessProbe: &corev1.Probe{},
ReadinessProbe: &corev1.Probe{},
StartupProbe: &corev1.Probe{},
},
},
},
},
expectPod: &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "podcopy"},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{Name: "app", Image: "appimage"},
{
Name: "dbg",
Image: "dbgimage",
SecurityContext: &corev1.SecurityContext{
Privileged: ptr.To(true),
Capabilities: &corev1.Capabilities{
Add: []corev1.Capability{"SYS_PTRACE"},
},
},
},
},
ShareProcessNamespace: ptr.To(true),
},
},
},
{
name: "debug by node",
pod: &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "pod"},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{Name: "dbg", Image: "dbgimage"},
},
},
},
containerName: "dbg",
target: testNode,
expectPod: &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "pod"},
Spec: corev1.PodSpec{
HostNetwork: true,
HostPID: true,
HostIPC: true,
Volumes: []corev1.Volume{
{
Name: "host-root",
VolumeSource: corev1.VolumeSource{HostPath: &corev1.HostPathVolumeSource{Path: "/"}},
},
},
Containers: []corev1.Container{
{
Name: "dbg",
Image: "dbgimage",
SecurityContext: &corev1.SecurityContext{
Privileged: ptr.To(true),
},
VolumeMounts: []corev1.VolumeMount{{Name: "host-root", MountPath: "/host"}},
},
},
},
},
},
{
name: "debug by node preserve existing capability",
pod: &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "pod"},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "dbg",
Image: "dbgimage",
SecurityContext: &corev1.SecurityContext{
Capabilities: &corev1.Capabilities{
Add: []corev1.Capability{"SYS_PTRACE"},
},
},
},
},
},
},
containerName: "dbg",
target: testNode,
expectPod: &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "pod"},
Spec: corev1.PodSpec{
HostNetwork: true,
HostPID: true,
HostIPC: true,
Volumes: []corev1.Volume{
{
Name: "host-root",
VolumeSource: corev1.VolumeSource{HostPath: &corev1.HostPathVolumeSource{Path: "/"}},
},
},
Containers: []corev1.Container{
{
Name: "dbg",
Image: "dbgimage",
SecurityContext: &corev1.SecurityContext{
Privileged: ptr.To(true),
Capabilities: &corev1.Capabilities{
Add: []corev1.Capability{"SYS_PTRACE"},
},
},
VolumeMounts: []corev1.VolumeMount{{Name: "host-root", MountPath: "/host"}},
},
},
},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
applier := &sysadminProfile{KeepFlags{InitContainers: true}}
err := applier.Apply(test.pod, test.containerName, test.target)
if (err == nil) != (test.expectErr == nil) || (err != nil && test.expectErr != nil && err.Error() != test.expectErr.Error()) { if (err == nil) != (test.expectErr == nil) || (err != nil && test.expectErr != nil && err.Error() != test.expectErr.Error()) {
t.Fatalf("expect error: %v, got error: %v", test.expectErr, err) t.Fatalf("expect error: %v, got error: %v", test.expectErr, err)
} }

View File

@ -103,10 +103,7 @@ var (
kubectl delete pod foo --force kubectl delete pod foo --force
# Delete all pods # Delete all pods
kubectl delete pods --all kubectl delete pods --all`))
# Delete all pods only if the user confirms the deletion
kubectl delete pods --all --interactive`))
) )
type DeleteOptions struct { type DeleteOptions struct {
@ -506,10 +503,6 @@ func (o *DeleteOptions) PrintObj(info *resource.Info) {
operation = "force deleted" operation = "force deleted"
} }
if info.Namespaced() {
operation = fmt.Sprintf("%s from %s namespace", operation, info.Namespace)
}
switch o.DryRunStrategy { switch o.DryRunStrategy {
case cmdutil.DryRunClient: case cmdutil.DryRunClient:
operation = fmt.Sprintf("%s (dry run)", operation) operation = fmt.Sprintf("%s (dry run)", operation)
@ -528,7 +521,7 @@ func (o *DeleteOptions) PrintObj(info *resource.Info) {
} }
func (o *DeleteOptions) confirmation(infos []*resource.Info) bool { func (o *DeleteOptions) confirmation(infos []*resource.Info) bool {
fmt.Fprintf(o.Out, i18n.T("You are about to delete the following %d resource(s):\n"), len(infos)) //nolint:errcheck fmt.Fprintf(o.Out, i18n.T("You are about to delete the following %d resource(s):\n"), len(infos))
for _, info := range infos { for _, info := range infos {
groupKind := info.Mapping.GroupVersionKind groupKind := info.Mapping.GroupVersionKind
kindString := fmt.Sprintf("%s.%s", strings.ToLower(groupKind.Kind), groupKind.Group) kindString := fmt.Sprintf("%s.%s", strings.ToLower(groupKind.Kind), groupKind.Group)
@ -536,11 +529,11 @@ func (o *DeleteOptions) confirmation(infos []*resource.Info) bool {
kindString = strings.ToLower(groupKind.Kind) kindString = strings.ToLower(groupKind.Kind)
} }
fmt.Fprintf(o.Out, "%s/%s\n", kindString, info.Name) //nolint:errcheck fmt.Fprintf(o.Out, "%s/%s\n", kindString, info.Name)
} }
fmt.Fprint(o.Out, i18n.T("Do you want to continue?")+" (y/N): ") //nolint:errcheck fmt.Fprintf(o.Out, i18n.T("Do you want to continue?")+" (y/n): ")
var input string var input string
_, err := fmt.Fscanln(o.In, &input) _, err := fmt.Fscan(o.In, &input)
if err != nil { if err != nil {
return false return false
} }

View File

@ -160,8 +160,10 @@ func (f *DeleteFlags) AddFlags(cmd *cobra.Command) {
if f.Raw != nil { if f.Raw != nil {
cmd.Flags().StringVar(f.Raw, "raw", *f.Raw, "Raw URI to DELETE to the server. Uses the transport specified by the kubeconfig file.") cmd.Flags().StringVar(f.Raw, "raw", *f.Raw, "Raw URI to DELETE to the server. Uses the transport specified by the kubeconfig file.")
} }
if f.Interactive != nil { if cmdutil.InteractiveDelete.IsEnabled() {
cmd.Flags().BoolVarP(f.Interactive, "interactive", "i", *f.Interactive, "If true, delete resource only when user confirms.") if f.Interactive != nil {
cmd.Flags().BoolVarP(f.Interactive, "interactive", "i", *f.Interactive, "If true, delete resource only when user confirms. This flag is in Alpha.")
}
} }
} }

View File

@ -36,7 +36,7 @@ import (
cmdtesting "k8s.io/kubectl/pkg/cmd/testing" cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
cmdutil "k8s.io/kubectl/pkg/cmd/util" cmdutil "k8s.io/kubectl/pkg/cmd/util"
"k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/scheme"
"k8s.io/utils/ptr" "k8s.io/utils/pointer"
) )
func fakecmd() *cobra.Command { func fakecmd() *cobra.Command {
@ -54,37 +54,41 @@ func TestDeleteFlagValidation(t *testing.T) {
defer f.Cleanup() defer f.Cleanup()
tests := []struct { tests := []struct {
flags DeleteFlags flags DeleteFlags
args [][]string enableAlphas []cmdutil.FeatureGate
expectedErr string args [][]string
expectedErr string
}{ }{
{ {
flags: DeleteFlags{ flags: DeleteFlags{
Raw: ptr.To("test"), Raw: pointer.String("test"),
Interactive: ptr.To(true), Interactive: pointer.Bool(true),
}, },
expectedErr: "--interactive can not be used with --raw", enableAlphas: []cmdutil.FeatureGate{cmdutil.InteractiveDelete},
expectedErr: "--interactive can not be used with --raw",
}, },
} }
for _, test := range tests { for _, test := range tests {
cmd := fakecmd() cmd := fakecmd()
deleteOptions, err := test.flags.ToOptions(nil, genericiooptions.NewTestIOStreamsDiscard()) cmdtesting.WithAlphaEnvs(test.enableAlphas, t, func(t *testing.T) {
if err != nil { deleteOptions, err := test.flags.ToOptions(nil, genericiooptions.NewTestIOStreamsDiscard())
t.Fatalf("unexpected error creating delete options: %s", err) if err != nil {
} t.Fatalf("unexpected error creating delete options: %s", err)
deleteOptions.Filenames = []string{"../../../testdata/redis-master-controller.yaml"} }
err = deleteOptions.Complete(f, nil, cmd) deleteOptions.Filenames = []string{"../../../testdata/redis-master-controller.yaml"}
if err != nil { err = deleteOptions.Complete(f, nil, cmd)
t.Fatalf("unexpected error creating delete options: %s", err) if err != nil {
} t.Fatalf("unexpected error creating delete options: %s", err)
err = deleteOptions.Validate() }
if err == nil { err = deleteOptions.Validate()
t.Fatalf("missing expected error") if err == nil {
} t.Fatalf("missing expected error")
if test.expectedErr != err.Error() { }
t.Errorf("expected error %s, got %s", test.expectedErr, err) if test.expectedErr != err.Error() {
} t.Errorf("expected error %s, got %s", test.expectedErr, err)
}
})
} }
} }
@ -306,7 +310,7 @@ func TestDeleteObject(t *testing.T) {
func TestPreviewResultEqualToResult(t *testing.T) { func TestPreviewResultEqualToResult(t *testing.T) {
deleteFlags := NewDeleteCommandFlags("") deleteFlags := NewDeleteCommandFlags("")
deleteFlags.Interactive = ptr.To(true) deleteFlags.Interactive = pointer.Bool(true)
tf := cmdtesting.NewTestFactory().WithNamespace("test") tf := cmdtesting.NewTestFactory().WithNamespace("test")
defer tf.Cleanup() defer tf.Cleanup()
@ -358,50 +362,34 @@ func TestDeleteObjectWithInteractive(t *testing.T) {
}), }),
} }
streams, in, buf, _ := genericiooptions.NewTestIOStreams() cmdtesting.WithAlphaEnvs([]cmdutil.FeatureGate{cmdutil.InteractiveDelete}, t, func(t *testing.T) {
fmt.Fprint(in, "y") streams, in, buf, _ := genericiooptions.NewTestIOStreams()
cmd := NewCmdDelete(tf, streams) fmt.Fprint(in, "y")
err := cmd.Flags().Set("filename", "../../../testdata/redis-master-controller.yaml") cmd := NewCmdDelete(tf, streams)
if err != nil { cmd.Flags().Set("filename", "../../../testdata/redis-master-controller.yaml")
t.Errorf("unexpected error %v", err) cmd.Flags().Set("output", "name")
} cmd.Flags().Set("interactive", "true")
err = cmd.Flags().Set("output", "name") cmd.Run(cmd, []string{})
if err != nil {
t.Errorf("unexpected error %v", err)
}
err = cmd.Flags().Set("interactive", "true")
if err != nil {
t.Errorf("unexpected error %v", err)
}
cmd.Run(cmd, []string{})
if buf.String() != "You are about to delete the following 1 resource(s):\nreplicationcontroller/redis-master\nDo you want to continue? (y/N): replicationcontroller/redis-master\n" { if buf.String() != "You are about to delete the following 1 resource(s):\nreplicationcontroller/redis-master\nDo you want to continue? (y/n): replicationcontroller/redis-master\n" {
t.Errorf("unexpected output: %s", buf.String()) t.Errorf("unexpected output: %s", buf.String())
} }
streams, in, buf, _ = genericiooptions.NewTestIOStreams() streams, in, buf, _ = genericiooptions.NewTestIOStreams()
fmt.Fprint(in, "n") fmt.Fprint(in, "n")
cmd = NewCmdDelete(tf, streams) cmd = NewCmdDelete(tf, streams)
err = cmd.Flags().Set("filename", "../../../testdata/redis-master-controller.yaml") cmd.Flags().Set("filename", "../../../testdata/redis-master-controller.yaml")
if err != nil { cmd.Flags().Set("output", "name")
t.Errorf("unexpected error %v", err) cmd.Flags().Set("interactive", "true")
} cmd.Run(cmd, []string{})
err = cmd.Flags().Set("output", "name")
if err != nil {
t.Errorf("unexpected error %v", err)
}
err = cmd.Flags().Set("interactive", "true")
if err != nil {
t.Errorf("unexpected error %v", err)
}
cmd.Run(cmd, []string{})
if buf.String() != "You are about to delete the following 1 resource(s):\nreplicationcontroller/redis-master\nDo you want to continue? (y/N): deletion is cancelled\n" { if buf.String() != "You are about to delete the following 1 resource(s):\nreplicationcontroller/redis-master\nDo you want to continue? (y/n): deletion is cancelled\n" {
t.Errorf("unexpected output: %s", buf.String()) t.Errorf("unexpected output: %s", buf.String())
} }
if buf.String() == ": replicationcontroller/redis-master\n" { if buf.String() == ": replicationcontroller/redis-master\n" {
t.Errorf("unexpected output: %s", buf.String()) t.Errorf("unexpected output: %s", buf.String())
} }
})
} }
func TestGracePeriodScenarios(t *testing.T) { func TestGracePeriodScenarios(t *testing.T) {
@ -972,39 +960,3 @@ func TestResourceErrors(t *testing.T) {
}) })
} }
} }
func TestDeleteMessageOutput(t *testing.T) {
cmdtesting.InitTestErrorHandler(t)
_, _, rc := cmdtesting.TestData()
tf := cmdtesting.NewTestFactory().WithNamespace("test-specific")
defer tf.Cleanup()
codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
tf.UnstructuredClient = &fake.RESTClient{
NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
switch p, m := req.URL.Path, req.Method; {
case p == "/namespaces/test-specific/replicationcontrollers/redis-master" && m == "DELETE":
return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &rc.Items[0])}, nil
default:
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
return nil, nil
}
}),
}
streams, _, buf, _ := genericiooptions.NewTestIOStreams()
cmd := NewCmdDelete(tf, streams)
err := cmd.Flags().Set("filename", "../../../testdata/redis-master-controller.yaml")
if err != nil {
t.Errorf("unexpected error: %v", err)
}
cmd.Run(cmd, []string{})
if buf.String() != "replicationcontroller \"redis-master\" deleted from test-specific namespace\n" {
t.Errorf("unexpected output: %s", buf.String())
}
}

View File

@ -193,7 +193,7 @@ func (o *DescribeOptions) Run() error {
allErrs = append(allErrs, err) allErrs = append(allErrs, err)
} }
errs := sets.New[string]() errs := sets.NewString()
first := true first := true
for _, info := range infos { for _, info := range infos {
mapping := info.ResourceMapping() mapping := info.ResourceMapping()

View File

@ -35,7 +35,6 @@ import (
"k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/genericiooptions"
"k8s.io/cli-runtime/pkg/resource" "k8s.io/cli-runtime/pkg/resource"
"k8s.io/client-go/dynamic" "k8s.io/client-go/dynamic"
"k8s.io/client-go/openapi3"
"k8s.io/klog/v2" "k8s.io/klog/v2"
"k8s.io/kubectl/pkg/cmd/apply" "k8s.io/kubectl/pkg/cmd/apply"
cmdutil "k8s.io/kubectl/pkg/cmd/util" cmdutil "k8s.io/kubectl/pkg/cmd/util"
@ -110,8 +109,7 @@ type DiffOptions struct {
Concurrency int Concurrency int
Selector string Selector string
OpenAPIGetter openapi.OpenAPIResourcesGetter OpenAPISchema openapi.Resources
OpenAPIV3Root openapi3.Root
DynamicClient dynamic.Interface DynamicClient dynamic.Interface
CmdNamespace string CmdNamespace string
EnforceNamespace bool EnforceNamespace bool
@ -161,12 +159,12 @@ func NewCmdDiff(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Co
// command it means changes were found. // command it means changes were found.
// Thus, it should return status code greater than 1. // Thus, it should return status code greater than 1.
cmd.SetFlagErrorFunc(func(command *cobra.Command, err error) error { cmd.SetFlagErrorFunc(func(command *cobra.Command, err error) error {
cmdutil.CheckDiffErr(cmdutil.UsageErrorf(cmd, "%s", err.Error())) cmdutil.CheckDiffErr(cmdutil.UsageErrorf(cmd, err.Error()))
return nil return nil
}) })
usage := "contains the configuration to diff" usage := "contains the configuration to diff"
cmd.Flags().StringArray("prune-allowlist", []string{}, "Overwrite the default allowlist with <group/version/kind> for --prune") cmd.Flags().StringArray("prune-allowlist", []string{}, "Overwrite the default whitelist with <group/version/kind> for --prune")
cmd.Flags().Bool("prune", false, "Include resources that would be deleted by pruning. Can be used with -l and default shows all resources would be pruned") cmd.Flags().Bool("prune", false, "Include resources that would be deleted by pruning. Can be used with -l and default shows all resources would be pruned")
cmd.Flags().BoolVar(&options.ShowManagedFields, "show-managed-fields", options.ShowManagedFields, "If true, include managed fields in the diff.") cmd.Flags().BoolVar(&options.ShowManagedFields, "show-managed-fields", options.ShowManagedFields, "If true, include managed fields in the diff.")
cmd.Flags().IntVar(&options.Concurrency, "concurrency", 1, "Number of objects to process in parallel when diffing against the live version. Larger number = faster, but more memory, I/O and CPU over that shorter period of time.") cmd.Flags().IntVar(&options.Concurrency, "concurrency", 1, "Number of objects to process in parallel when diffing against the live version. Larger number = faster, but more memory, I/O and CPU over that shorter period of time.")
@ -325,8 +323,7 @@ type InfoObject struct {
LocalObj runtime.Object LocalObj runtime.Object
Info *resource.Info Info *resource.Info
Encoder runtime.Encoder Encoder runtime.Encoder
OpenAPIGetter openapi.OpenAPIResourcesGetter OpenAPI openapi.Resources
OpenAPIV3Root openapi3.Root
Force bool Force bool
ServerSideApply bool ServerSideApply bool
FieldManager string FieldManager string
@ -398,8 +395,7 @@ func (obj InfoObject) Merged() (runtime.Object, error) {
Helper: helper, Helper: helper,
Overwrite: true, Overwrite: true,
BackOff: clockwork.NewRealClock(), BackOff: clockwork.NewRealClock(),
OpenAPIGetter: obj.OpenAPIGetter, OpenapiSchema: obj.OpenAPI,
OpenAPIV3Root: obj.OpenAPIV3Root,
ResourceVersion: resourceVersion, ResourceVersion: resourceVersion,
} }
@ -641,14 +637,9 @@ func (o *DiffOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []str
} }
if !o.ServerSideApply { if !o.ServerSideApply {
o.OpenAPIGetter = f o.OpenAPISchema, err = f.OpenAPISchema()
if !cmdutil.OpenAPIV3Patch.IsDisabled() { if err != nil {
openAPIV3Client, err := f.OpenAPIV3Client() return err
if err == nil {
o.OpenAPIV3Root = openapi3.NewRoot(openAPIV3Client)
} else {
klog.V(4).Infof("warning: OpenAPI V3 Patch is enabled but is unable to be loaded. Will fall back to OpenAPI V2")
}
} }
} }
@ -730,8 +721,7 @@ func (o *DiffOptions) Run() error {
LocalObj: local, LocalObj: local,
Info: info, Info: info,
Encoder: scheme.DefaultJSONEncoder(), Encoder: scheme.DefaultJSONEncoder(),
OpenAPIGetter: o.OpenAPIGetter, OpenAPI: o.OpenAPISchema,
OpenAPIV3Root: o.OpenAPIV3Root,
Force: force, Force: force,
ServerSideApply: o.ServerSideApply, ServerSideApply: o.ServerSideApply,
FieldManager: o.FieldManager, FieldManager: o.FieldManager,

View File

@ -20,6 +20,7 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"os" "os"
"path"
"path/filepath" "path/filepath"
"strings" "strings"
"testing" "testing"
@ -64,7 +65,6 @@ func TestDiffProgram(t *testing.T) {
externalDiffCommands := [3]string{"diff", "diff -ruN", "diff --report-identical-files"} externalDiffCommands := [3]string{"diff", "diff -ruN", "diff --report-identical-files"}
t.Setenv("LANG", "C") t.Setenv("LANG", "C")
t.Setenv("LANGUAGE", "en_US")
for i, c := range externalDiffCommands { for i, c := range externalDiffCommands {
t.Setenv("KUBECTL_EXTERNAL_DIFF", c) t.Setenv("KUBECTL_EXTERNAL_DIFF", c)
@ -130,7 +130,7 @@ func TestDiffVersion(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
fcontent, err := os.ReadFile(filepath.Join(diff.Dir.Name, obj.Name())) fcontent, err := os.ReadFile(path.Join(diff.Dir.Name, obj.Name()))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -201,7 +201,7 @@ func TestDiffer(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
fcontent, err := os.ReadFile(filepath.Join(diff.From.Dir.Name, obj.Name())) fcontent, err := os.ReadFile(path.Join(diff.From.Dir.Name, obj.Name()))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -210,7 +210,7 @@ func TestDiffer(t *testing.T) {
t.Fatalf("File has %q, expected %q", string(fcontent), econtent) t.Fatalf("File has %q, expected %q", string(fcontent), econtent)
} }
fcontent, err = os.ReadFile(filepath.Join(diff.To.Dir.Name, obj.Name())) fcontent, err = os.ReadFile(path.Join(diff.To.Dir.Name, obj.Name()))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -286,12 +286,12 @@ metadata:
t.Fatal(err) t.Fatal(err)
} }
actualFromContent, _ := os.ReadFile(filepath.Join(diff.From.Dir.Name, obj.Name())) actualFromContent, _ := os.ReadFile(path.Join(diff.From.Dir.Name, obj.Name()))
if string(actualFromContent) != tc.expectedFromContent { if string(actualFromContent) != tc.expectedFromContent {
t.Fatalf("File has %q, expected %q", string(actualFromContent), tc.expectedFromContent) t.Fatalf("File has %q, expected %q", string(actualFromContent), tc.expectedFromContent)
} }
actualToContent, _ := os.ReadFile(filepath.Join(diff.To.Dir.Name, obj.Name())) actualToContent, _ := os.ReadFile(path.Join(diff.To.Dir.Name, obj.Name()))
if string(actualToContent) != tc.expectedToContent { if string(actualToContent) != tc.expectedToContent {
t.Fatalf("File has %q, expected %q", string(actualToContent), tc.expectedToContent) t.Fatalf("File has %q, expected %q", string(actualToContent), tc.expectedToContent)
} }

View File

@ -31,7 +31,6 @@ import (
"k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/genericiooptions"
"k8s.io/cli-runtime/pkg/printers" "k8s.io/cli-runtime/pkg/printers"
"k8s.io/cli-runtime/pkg/resource" "k8s.io/cli-runtime/pkg/resource"
"k8s.io/klog/v2"
cmdutil "k8s.io/kubectl/pkg/cmd/util" cmdutil "k8s.io/kubectl/pkg/cmd/util"
"k8s.io/kubectl/pkg/drain" "k8s.io/kubectl/pkg/drain"
"k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/scheme"
@ -157,53 +156,24 @@ func NewDrainCmdOptions(f cmdutil.Factory, ioStreams genericiooptions.IOStreams)
ChunkSize: cmdutil.DefaultChunkSize, ChunkSize: cmdutil.DefaultChunkSize,
}, },
} }
o.drainer.OnPodDeletionOrEvictionFinished = o.onPodDeletionOrEvictionFinished o.drainer.OnPodDeletedOrEvicted = o.onPodDeletedOrEvicted
o.drainer.OnPodDeletionOrEvictionStarted = o.onPodDeletionOrEvictionStarted
return o return o
} }
// onPodDeletionOrEvictionFinished is called by drain.Helper, when eviction/deletetion of the pod is finished // onPodDeletedOrEvicted is called by drain.Helper, when the pod has been deleted or evicted
func (o *DrainCmdOptions) onPodDeletionOrEvictionFinished(pod *corev1.Pod, usingEviction bool, err error) { func (o *DrainCmdOptions) onPodDeletedOrEvicted(pod *corev1.Pod, usingEviction bool) {
var verbStr string var verbStr string
if usingEviction { if usingEviction {
if err != nil { verbStr = "evicted"
verbStr = "eviction failed"
} else {
verbStr = "evicted"
}
} else { } else {
if err != nil { verbStr = "deleted"
verbStr = "deletion failed"
} else {
verbStr = "deleted"
}
} }
printObj, err := o.ToPrinter(verbStr) printObj, err := o.ToPrinter(verbStr)
if err != nil { if err != nil {
fmt.Fprintf(o.ErrOut, "error building printer: %v\n", err) fmt.Fprintf(o.ErrOut, "error building printer: %v\n", err)
fmt.Fprintf(o.Out, "pod %s/%s %s\n", pod.Namespace, pod.Name, verbStr) fmt.Fprintf(o.Out, "pod %s/%s %s\n", pod.Namespace, pod.Name, verbStr)
} else { } else {
_ = printObj(pod, o.Out) printObj(pod, o.Out)
}
}
// onPodDeletionOrEvictionStarted is called by drain.Helper, when eviction/deletion of the pod is started
func (o *DrainCmdOptions) onPodDeletionOrEvictionStarted(pod *corev1.Pod, usingEviction bool) {
if !klog.V(2).Enabled() {
return
}
var verbStr string
if usingEviction {
verbStr = "eviction started"
} else {
verbStr = "deletion started"
}
printObj, err := o.ToPrinter(verbStr)
if err != nil {
fmt.Fprintf(o.ErrOut, "error building printer: %v\n", err)
fmt.Fprintf(o.Out, "pod %s/%s %s\n", pod.Namespace, pod.Name, verbStr)
} else {
_ = printObj(pod, o.Out)
} }
} }
@ -224,6 +194,8 @@ func NewCmdDrain(f cmdutil.Factory, ioStreams genericiooptions.IOStreams) *cobra
} }
cmd.Flags().BoolVar(&o.drainer.Force, "force", o.drainer.Force, "Continue even if there are pods that do not declare a controller.") cmd.Flags().BoolVar(&o.drainer.Force, "force", o.drainer.Force, "Continue even if there are pods that do not declare a controller.")
cmd.Flags().BoolVar(&o.drainer.IgnoreAllDaemonSets, "ignore-daemonsets", o.drainer.IgnoreAllDaemonSets, "Ignore DaemonSet-managed pods.") cmd.Flags().BoolVar(&o.drainer.IgnoreAllDaemonSets, "ignore-daemonsets", o.drainer.IgnoreAllDaemonSets, "Ignore DaemonSet-managed pods.")
cmd.Flags().BoolVar(&o.drainer.DeleteEmptyDirData, "delete-local-data", o.drainer.DeleteEmptyDirData, "Continue even if there are pods using emptyDir (local data that will be deleted when the node is drained).")
cmd.Flags().MarkDeprecated("delete-local-data", "This option is deprecated and will be deleted. Use --delete-emptydir-data.")
cmd.Flags().BoolVar(&o.drainer.DeleteEmptyDirData, "delete-emptydir-data", o.drainer.DeleteEmptyDirData, "Continue even if there are pods using emptyDir (local data that will be deleted when the node is drained).") cmd.Flags().BoolVar(&o.drainer.DeleteEmptyDirData, "delete-emptydir-data", o.drainer.DeleteEmptyDirData, "Continue even if there are pods using emptyDir (local data that will be deleted when the node is drained).")
cmd.Flags().IntVar(&o.drainer.GracePeriodSeconds, "grace-period", o.drainer.GracePeriodSeconds, "Period of time in seconds given to each pod to terminate gracefully. If negative, the default value specified in the pod will be used.") cmd.Flags().IntVar(&o.drainer.GracePeriodSeconds, "grace-period", o.drainer.GracePeriodSeconds, "Period of time in seconds given to each pod to terminate gracefully. If negative, the default value specified in the pod will be used.")
cmd.Flags().DurationVar(&o.drainer.Timeout, "timeout", o.drainer.Timeout, "The length of time to wait before giving up, zero means infinite") cmd.Flags().DurationVar(&o.drainer.Timeout, "timeout", o.drainer.Timeout, "The length of time to wait before giving up, zero means infinite")
@ -243,7 +215,7 @@ func (o *DrainCmdOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args [
var err error var err error
if len(args) == 0 && !cmd.Flags().Changed("selector") { if len(args) == 0 && !cmd.Flags().Changed("selector") {
return cmdutil.UsageErrorf(cmd, "USAGE: %s [flags]", cmd.Use) return cmdutil.UsageErrorf(cmd, fmt.Sprintf("USAGE: %s [flags]", cmd.Use))
} }
if len(args) > 0 && len(o.drainer.Selector) > 0 { if len(args) > 0 && len(o.drainer.Selector) > 0 {
return cmdutil.UsageErrorf(cmd, "error: cannot specify both a node name and a --selector option") return cmdutil.UsageErrorf(cmd, "error: cannot specify both a node name and a --selector option")
@ -326,7 +298,7 @@ func (o *DrainCmdOptions) RunDrain() error {
return err return err
} }
drainedNodes := sets.New[string]() drainedNodes := sets.NewString()
var fatal []error var fatal []error
remainingNodes := []string{} remainingNodes := []string{}
@ -341,7 +313,7 @@ func (o *DrainCmdOptions) RunDrain() error {
printObj(info.Object, o.Out) printObj(info.Object, o.Out)
} else { } else {
fmt.Fprintf(o.ErrOut, "error: unable to drain node %q due to error: %s, continuing command...\n", info.Name, err) fmt.Fprintf(o.ErrOut, "error: unable to drain node %q due to error:%s, continuing command...\n", info.Name, err)
if !drainedNodes.Has(info.Name) { if !drainedNodes.Has(info.Name) {
fatal = append(fatal, err) fatal = append(fatal, err)

View File

@ -42,7 +42,7 @@ import (
cmdutil "k8s.io/kubectl/pkg/cmd/util" cmdutil "k8s.io/kubectl/pkg/cmd/util"
"k8s.io/kubectl/pkg/drain" "k8s.io/kubectl/pkg/drain"
"k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/scheme"
"k8s.io/utils/ptr" utilpointer "k8s.io/utils/pointer"
) )
const ( const (
@ -277,8 +277,8 @@ func TestDrain(t *testing.T) {
Kind: "ReplicationController", Kind: "ReplicationController",
Name: "rc", Name: "rc",
UID: "123", UID: "123",
BlockOwnerDeletion: ptr.To(true), BlockOwnerDeletion: utilpointer.BoolPtr(true),
Controller: ptr.To(true), Controller: utilpointer.BoolPtr(true),
}, },
}, },
}, },
@ -309,8 +309,8 @@ func TestDrain(t *testing.T) {
APIVersion: "apps/v1", APIVersion: "apps/v1",
Kind: "DaemonSet", Kind: "DaemonSet",
Name: "ds", Name: "ds",
BlockOwnerDeletion: ptr.To(true), BlockOwnerDeletion: utilpointer.BoolPtr(true),
Controller: ptr.To(true), Controller: utilpointer.BoolPtr(true),
}, },
}, },
}, },
@ -330,8 +330,8 @@ func TestDrain(t *testing.T) {
APIVersion: "apps/v1", APIVersion: "apps/v1",
Kind: "DaemonSet", Kind: "DaemonSet",
Name: "ds", Name: "ds",
BlockOwnerDeletion: ptr.To(true), BlockOwnerDeletion: utilpointer.BoolPtr(true),
Controller: ptr.To(true), Controller: utilpointer.BoolPtr(true),
}, },
}, },
}, },
@ -354,8 +354,8 @@ func TestDrain(t *testing.T) {
APIVersion: "apps/v1", APIVersion: "apps/v1",
Kind: "DaemonSet", Kind: "DaemonSet",
Name: "ds", Name: "ds",
BlockOwnerDeletion: ptr.To(true), BlockOwnerDeletion: utilpointer.BoolPtr(true),
Controller: ptr.To(true), Controller: utilpointer.BoolPtr(true),
}, },
}, },
}, },
@ -404,8 +404,8 @@ func TestDrain(t *testing.T) {
APIVersion: "v1", APIVersion: "v1",
Kind: "Job", Kind: "Job",
Name: "job", Name: "job",
BlockOwnerDeletion: ptr.To(true), BlockOwnerDeletion: utilpointer.BoolPtr(true),
Controller: ptr.To(true), Controller: utilpointer.BoolPtr(true),
}, },
}, },
}, },
@ -431,8 +431,8 @@ func TestDrain(t *testing.T) {
APIVersion: "v1", APIVersion: "v1",
Kind: "Job", Kind: "Job",
Name: "job", Name: "job",
BlockOwnerDeletion: ptr.To(true), BlockOwnerDeletion: utilpointer.BoolPtr(true),
Controller: ptr.To(true), Controller: utilpointer.BoolPtr(true),
}, },
}, },
}, },
@ -473,8 +473,8 @@ func TestDrain(t *testing.T) {
APIVersion: "v1", APIVersion: "v1",
Kind: "ReplicaSet", Kind: "ReplicaSet",
Name: "rs", Name: "rs",
BlockOwnerDeletion: ptr.To(true), BlockOwnerDeletion: utilpointer.BoolPtr(true),
Controller: ptr.To(true), Controller: utilpointer.BoolPtr(true),
}, },
}, },
}, },
@ -635,6 +635,17 @@ func TestDrain(t *testing.T) {
expectDelete: true, expectDelete: true,
expectOutputToContain: "node/node drained", expectOutputToContain: "node/node drained",
}, },
{
description: "Ensure compatibility for --delete-local-data until fully deprecated",
node: node,
expected: cordonedNode,
pods: []corev1.Pod{jobPod},
rcs: []corev1.ReplicationController{rc},
args: []string{"node", "--force", "--delete-local-data=true"},
expectFatal: false,
expectDelete: true,
expectOutputToContain: "node/node drained",
},
{ {
description: "Job-managed terminated pod", description: "Job-managed terminated pod",
node: node, node: node,

View File

@ -103,6 +103,6 @@ func NewCmdEdit(f cmdutil.Factory, ioStreams genericiooptions.IOStreams) *cobra.
"Defaults to the line ending native to your platform.") "Defaults to the line ending native to your platform.")
cmdutil.AddFieldManagerFlagVar(cmd, &o.FieldManager, "kubectl-edit") cmdutil.AddFieldManagerFlagVar(cmd, &o.FieldManager, "kubectl-edit")
cmdutil.AddApplyAnnotationVarFlags(cmd, &o.ApplyAnnotation) cmdutil.AddApplyAnnotationVarFlags(cmd, &o.ApplyAnnotation)
cmdutil.AddSubresourceFlags(cmd, &o.Subresource, "If specified, edit will operate on the subresource of the requested object.") cmdutil.AddSubresourceFlags(cmd, &o.Subresource, "If specified, edit will operate on the subresource of the requested object.", editor.SupportedSubresources...)
return cmd return cmd
} }

View File

@ -30,7 +30,8 @@ import (
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
"github.com/spf13/cobra" "github.com/spf13/cobra"
yaml "go.yaml.in/yaml/v2" yaml "gopkg.in/yaml.v2"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/sets"
"k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/genericiooptions"
@ -172,7 +173,7 @@ func TestEdit(t *testing.T) {
t.Setenv("KUBE_EDITOR", "testdata/test_editor.sh") t.Setenv("KUBE_EDITOR", "testdata/test_editor.sh")
t.Setenv("KUBE_EDITOR_CALLBACK", server.URL+"/callback") t.Setenv("KUBE_EDITOR_CALLBACK", server.URL+"/callback")
testcases := sets.New[string]() testcases := sets.NewString()
filepath.Walk("testdata", func(path string, info os.FileInfo, err error) error { filepath.Walk("testdata", func(path string, info os.FileInfo, err error) error {
if err != nil { if err != nil {
return err return err
@ -194,7 +195,7 @@ func TestEdit(t *testing.T) {
t.Fatalf("Error locating edit testcases") t.Fatalf("Error locating edit testcases")
} }
for _, testcaseName := range testcases.UnsortedList() { for _, testcaseName := range testcases.List() {
t.Run(testcaseName, func(t *testing.T) { t.Run(testcaseName, func(t *testing.T) {
i = 0 i = 0
name = testcaseName name = testcaseName

View File

@ -25,7 +25,7 @@ import (
"os" "os"
"strings" "strings"
yaml "go.yaml.in/yaml/v2" yaml "gopkg.in/yaml.v2"
) )
type EditTestCase struct { type EditTestCase struct {

View File

@ -3,7 +3,7 @@
"kind": "Service", "kind": "Service",
"metadata": { "metadata": {
"annotations": { "annotations": {
"kubectl.kubernetes.io/last-applied-configuration": "{\"kind\":\"Service\",\"apiVersion\":\"v1\",\"metadata\":{\"name\":\"svc1\",\"labels\":{\"app\":\"svc1\"}},\"spec\":{\"ports\":[{\"name\":\"80\",\"protocol\":\"TCP\",\"port\":80,\"targetPort\":80}],\"selector\":{\"app\":\"svc1\"},\"type\":\"ClusterIP\"},\"status\":{\"loadBalancer\":{}}}\n" "kubectl.kubernetes.io/last-applied-configuration": "{\"kind\":\"Service\",\"apiVersion\":\"v1\",\"metadata\":{\"name\":\"svc1\",\"creationTimestamp\":null,\"labels\":{\"app\":\"svc1\"}},\"spec\":{\"ports\":[{\"name\":\"80\",\"protocol\":\"TCP\",\"port\":80,\"targetPort\":80}],\"selector\":{\"app\":\"svc1\"},\"type\":\"ClusterIP\"},\"status\":{\"loadBalancer\":{}}}\n"
}, },
"creationTimestamp": "2017-02-27T19:40:53Z", "creationTimestamp": "2017-02-27T19:40:53Z",
"labels": { "labels": {

View File

@ -7,7 +7,7 @@ kind: Service
metadata: metadata:
annotations: annotations:
kubectl.kubernetes.io/last-applied-configuration: | kubectl.kubernetes.io/last-applied-configuration: |
{"kind":"Service","apiVersion":"v1","metadata":{"name":"svc1","labels":{"app":"svc1"}},"spec":{"ports":[{"name":"80","protocol":"TCP","port":80,"targetPort":80}],"selector":{"app":"svc1"},"type":"ClusterIP"},"status":{"loadBalancer":{}}} {"kind":"Service","apiVersion":"v1","metadata":{"name":"svc1","creationTimestamp":null,"labels":{"app":"svc1"}},"spec":{"ports":[{"name":"80","protocol":"TCP","port":80,"targetPort":80}],"selector":{"app":"svc1"},"type":"ClusterIP"},"status":{"loadBalancer":{}}}
creationTimestamp: "2017-02-27T19:40:53Z" creationTimestamp: "2017-02-27T19:40:53Z"
labels: labels:
app: svc1 app: svc1

View File

@ -7,7 +7,7 @@ kind: Service
metadata: metadata:
annotations: annotations:
kubectl.kubernetes.io/last-applied-configuration: | kubectl.kubernetes.io/last-applied-configuration: |
{"kind":"Service","apiVersion":"v1","metadata":{"name":"svc1","labels":{"app":"svc1"}},"spec":{"ports":[{"name":"80","protocol":"TCP","port":80,"targetPort":80}],"selector":{"app":"svc1"},"type":"ClusterIP"},"status":{"loadBalancer":{}}} {"kind":"Service","apiVersion":"v1","metadata":{"name":"svc1","creationTimestamp":null,"labels":{"app":"svc1"}},"spec":{"ports":[{"name":"80","protocol":"TCP","port":80,"targetPort":80}],"selector":{"app":"svc1"},"type":"ClusterIP"},"status":{"loadBalancer":{}}}
creationTimestamp: "2017-02-27T19:40:53Z" creationTimestamp: "2017-02-27T19:40:53Z"
labels: labels:
app: svc1 app: svc1

View File

@ -33,6 +33,7 @@
}, },
"template": { "template": {
"metadata": { "metadata": {
"creationTimestamp": null,
"labels": { "labels": {
"app": "nginx" "app": "nginx"
} }

View File

@ -29,6 +29,7 @@ spec:
type: RollingUpdate type: RollingUpdate
template: template:
metadata: metadata:
creationTimestamp: null
labels: labels:
app: nginx app: nginx
spec: spec:

View File

@ -29,6 +29,7 @@ spec:
type: RollingUpdate type: RollingUpdate
template: template:
metadata: metadata:
creationTimestamp: null
labels: labels:
app: nginx app: nginx
spec: spec:

View File

@ -33,6 +33,7 @@
}, },
"template": { "template": {
"metadata": { "metadata": {
"creationTimestamp": null,
"labels": { "labels": {
"app": "nginx" "app": "nginx"
} }

View File

@ -3,7 +3,7 @@
"kind": "Service", "kind": "Service",
"metadata": { "metadata": {
"annotations": { "annotations": {
"kubectl.kubernetes.io/last-applied-configuration": "{\"kind\":\"Service\",\"apiVersion\":\"v1\",\"metadata\":{\"name\":\"svc1\",\"labels\":{\"app\":\"svc1\"}},\"spec\":{\"ports\":[{\"name\":\"80\",\"protocol\":\"TCP\",\"port\":80,\"targetPort\":80}],\"selector\":{\"app\":\"svc1\"},\"type\":\"ClusterIP\"},\"status\":{\"loadBalancer\":{}}}\n" "kubectl.kubernetes.io/last-applied-configuration": "{\"kind\":\"Service\",\"apiVersion\":\"v1\",\"metadata\":{\"name\":\"svc1\",\"creationTimestamp\":null,\"labels\":{\"app\":\"svc1\"}},\"spec\":{\"ports\":[{\"name\":\"80\",\"protocol\":\"TCP\",\"port\":80,\"targetPort\":80}],\"selector\":{\"app\":\"svc1\"},\"type\":\"ClusterIP\"},\"status\":{\"loadBalancer\":{}}}\n"
}, },
"creationTimestamp": "2017-02-27T19:40:53Z", "creationTimestamp": "2017-02-27T19:40:53Z",
"labels": { "labels": {

View File

@ -7,7 +7,7 @@ kind: Service
metadata: metadata:
annotations: annotations:
kubectl.kubernetes.io/last-applied-configuration: | kubectl.kubernetes.io/last-applied-configuration: |
{"kind":"Service","apiVersion":"v1","metadata":{"name":"svc1","labels":{"app":"svc1"}},"spec":{"ports":[{"name":"80","protocol":"TCP","port":80,"targetPort":80}],"selector":{"app":"svc1"},"type":"ClusterIP"},"status":{"loadBalancer":{}}} {"kind":"Service","apiVersion":"v1","metadata":{"name":"svc1","creationTimestamp":null,"labels":{"app":"svc1"}},"spec":{"ports":[{"name":"80","protocol":"TCP","port":80,"targetPort":80}],"selector":{"app":"svc1"},"type":"ClusterIP"},"status":{"loadBalancer":{}}}
creationTimestamp: "2017-02-27T19:40:53Z" creationTimestamp: "2017-02-27T19:40:53Z"
labels: labels:
app: svc1 app: svc1

View File

@ -7,7 +7,7 @@ kind: Service
metadata: metadata:
annotations: annotations:
kubectl.kubernetes.io/last-applied-configuration: | kubectl.kubernetes.io/last-applied-configuration: |
{"kind":"Service","apiVersion":"v1","metadata":{"name":"svc1","labels":{"app":"svc1"}},"spec":{"ports":[{"name":"80","protocol":"TCP","port":80,"targetPort":80}],"selector":{"app":"svc1"},"type":"ClusterIP"},"status":{"loadBalancer":{}}} {"kind":"Service","apiVersion":"v1","metadata":{"name":"svc1","creationTimestamp":null,"labels":{"app":"svc1"}},"spec":{"ports":[{"name":"80","protocol":"TCP","port":80,"targetPort":80}],"selector":{"app":"svc1"},"type":"ClusterIP"},"status":{"loadBalancer":{}}}
creationTimestamp: "2017-02-27T19:40:53Z" creationTimestamp: "2017-02-27T19:40:53Z"
labels: labels:
app: svc1 app: svc1

View File

@ -3,7 +3,7 @@
"kind": "Service", "kind": "Service",
"metadata": { "metadata": {
"annotations": { "annotations": {
"kubectl.kubernetes.io/last-applied-configuration": "{\"kind\":\"Service\",\"apiVersion\":\"v1\",\"metadata\":{\"name\":\"svc1\",\"labels\":{\"app\":\"svc1\"}},\"spec\":{\"ports\":[{\"name\":\"80\",\"protocol\":\"TCP\",\"port\":80,\"targetPort\":80}],\"selector\":{\"app\":\"svc1\"},\"type\":\"ClusterIP\"},\"status\":{\"loadBalancer\":{}}}\n" "kubectl.kubernetes.io/last-applied-configuration": "{\"kind\":\"Service\",\"apiVersion\":\"v1\",\"metadata\":{\"name\":\"svc1\",\"creationTimestamp\":null,\"labels\":{\"app\":\"svc1\"}},\"spec\":{\"ports\":[{\"name\":\"80\",\"protocol\":\"TCP\",\"port\":80,\"targetPort\":80}],\"selector\":{\"app\":\"svc1\"},\"type\":\"ClusterIP\"},\"status\":{\"loadBalancer\":{}}}\n"
}, },
"creationTimestamp": "2017-02-27T19:40:53Z", "creationTimestamp": "2017-02-27T19:40:53Z",
"labels": { "labels": {

View File

@ -7,7 +7,7 @@ kind: Service
metadata: metadata:
annotations: annotations:
kubectl.kubernetes.io/last-applied-configuration: | kubectl.kubernetes.io/last-applied-configuration: |
{"kind":"Service","apiVersion":"v1","metadata":{"name":"svc1","labels":{"app":"svc1"}},"spec":{"ports":[{"name":"80","protocol":"TCP","port":80,"targetPort":80}],"selector":{"app":"svc1"},"type":"ClusterIP"},"status":{"loadBalancer":{}}} {"kind":"Service","apiVersion":"v1","metadata":{"name":"svc1","creationTimestamp":null,"labels":{"app":"svc1"}},"spec":{"ports":[{"name":"80","protocol":"TCP","port":80,"targetPort":80}],"selector":{"app":"svc1"},"type":"ClusterIP"},"status":{"loadBalancer":{}}}
creationTimestamp: "2017-02-27T19:40:53Z" creationTimestamp: "2017-02-27T19:40:53Z"
labels: labels:
app: svc1 app: svc1

View File

@ -7,7 +7,7 @@ kind: Service
metadata: metadata:
annotations: annotations:
kubectl.kubernetes.io/last-applied-configuration: | kubectl.kubernetes.io/last-applied-configuration: |
{"kind":"Service","apiVersion":"v1","metadata":{"name":"svc1","labels":{"app":"svc1"}},"spec":{"ports":[{"name":"80","protocol":"TCP","port":80,"targetPort":80}],"selector":{"app":"svc1"},"type":"ClusterIP"},"status":{"loadBalancer":{}}} {"kind":"Service","apiVersion":"v1","metadata":{"name":"svc1","creationTimestamp":null,"labels":{"app":"svc1"}},"spec":{"ports":[{"name":"80","protocol":"TCP","port":80,"targetPort":80}],"selector":{"app":"svc1"},"type":"ClusterIP"},"status":{"loadBalancer":{}}}
creationTimestamp: "2017-02-27T19:40:53Z" creationTimestamp: "2017-02-27T19:40:53Z"
labels: labels:
app: svc1 app: svc1

View File

@ -18,12 +18,11 @@ package events
import ( import (
"bytes" "bytes"
"testing"
"time"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"testing"
"time"
) )
func TestPrintObj(t *testing.T) { func TestPrintObj(t *testing.T) {
@ -51,7 +50,7 @@ func TestPrintObj(t *testing.T) {
}, },
Type: corev1.EventTypeNormal, Type: corev1.EventTypeNormal,
Reason: "ScalingReplicaSet", Reason: "ScalingReplicaSet",
Message: "Scaled up replica set bar-002 from 0 to 1", Message: "Scaled up replica set bar-002 to 1",
ReportingController: "deployment-controller", ReportingController: "deployment-controller",
EventTime: metav1.NewMicroTime(time.Now().Add(-20 * time.Minute)), EventTime: metav1.NewMicroTime(time.Now().Add(-20 * time.Minute)),
Series: &corev1.EventSeries{ Series: &corev1.EventSeries{
@ -60,7 +59,7 @@ func TestPrintObj(t *testing.T) {
}, },
}, },
expected: `LAST SEEN TYPE REASON OBJECT MESSAGE expected: `LAST SEEN TYPE REASON OBJECT MESSAGE
12m (x3 over 20m) Normal ScalingReplicaSet Deployment/bar Scaled up replica set bar-002 from 0 to 1 12m (x3 over 20m) Normal ScalingReplicaSet Deployment/bar Scaled up replica set bar-002 to 1
`, `,
}, },
{ {
@ -84,7 +83,7 @@ func TestPrintObj(t *testing.T) {
}, },
Type: corev1.EventTypeNormal, Type: corev1.EventTypeNormal,
Reason: "ScalingReplicaSet", Reason: "ScalingReplicaSet",
Message: "Scaled up replica set bar-002 from 0 to 1", Message: "Scaled up replica set bar-002 to 1",
ReportingController: "deployment-controller", ReportingController: "deployment-controller",
EventTime: metav1.NewMicroTime(time.Now().Add(-20 * time.Minute)), EventTime: metav1.NewMicroTime(time.Now().Add(-20 * time.Minute)),
Series: &corev1.EventSeries{ Series: &corev1.EventSeries{
@ -106,7 +105,7 @@ func TestPrintObj(t *testing.T) {
}, },
Type: corev1.EventTypeNormal, Type: corev1.EventTypeNormal,
Reason: "ScalingReplicaSet", Reason: "ScalingReplicaSet",
Message: "Scaled up replica set bar-002 from 0 to 1", Message: "Scaled up replica set bar-002 to 1",
ReportingController: "deployment-controller", ReportingController: "deployment-controller",
EventTime: metav1.NewMicroTime(time.Now().Add(-15 * time.Minute)), EventTime: metav1.NewMicroTime(time.Now().Add(-15 * time.Minute)),
Series: &corev1.EventSeries{ Series: &corev1.EventSeries{
@ -117,8 +116,8 @@ func TestPrintObj(t *testing.T) {
}, },
}, },
expected: `NAMESPACE LAST SEEN TYPE REASON OBJECT MESSAGE expected: `NAMESPACE LAST SEEN TYPE REASON OBJECT MESSAGE
foo 12m (x3 over 20m) Normal ScalingReplicaSet Deployment/bar Scaled up replica set bar-002 from 0 to 1 foo 12m (x3 over 20m) Normal ScalingReplicaSet Deployment/bar Scaled up replica set bar-002 to 1
bar 11m (x3 over 15m) Normal ScalingReplicaSet Deployment/bar2 Scaled up replica set bar-002 from 0 to 1 bar 11m (x3 over 15m) Normal ScalingReplicaSet Deployment/bar2 Scaled up replica set bar-002 to 1
`, `,
}, },
{ {
@ -140,7 +139,7 @@ bar 11m (x3 over 15m) Normal ScalingReplicaSet Deployment/bar2 Scaled up replica
}, },
Type: corev1.EventTypeNormal, Type: corev1.EventTypeNormal,
Reason: "ScalingReplicaSet", Reason: "ScalingReplicaSet",
Message: "Scaled up replica set bar-002 from 0 to 1", Message: "Scaled up replica set bar-002 to 1",
ReportingController: "deployment-controller", ReportingController: "deployment-controller",
EventTime: metav1.NewMicroTime(time.Now().Add(-20 * time.Minute)), EventTime: metav1.NewMicroTime(time.Now().Add(-20 * time.Minute)),
Series: &corev1.EventSeries{ Series: &corev1.EventSeries{
@ -148,7 +147,7 @@ bar 11m (x3 over 15m) Normal ScalingReplicaSet Deployment/bar2 Scaled up replica
LastObservedTime: metav1.NewMicroTime(time.Now().Add(-12 * time.Minute)), LastObservedTime: metav1.NewMicroTime(time.Now().Add(-12 * time.Minute)),
}, },
}, },
expected: "12m (x3 over 20m) Normal ScalingReplicaSet Deployment/bar Scaled up replica set bar-002 from 0 to 1\n", expected: "12m (x3 over 20m) Normal ScalingReplicaSet Deployment/bar Scaled up replica set bar-002 to 1\n",
}, },
{ {
printer: EventPrinter{ printer: EventPrinter{
@ -169,7 +168,7 @@ bar 11m (x3 over 15m) Normal ScalingReplicaSet Deployment/bar2 Scaled up replica
}, },
Type: corev1.EventTypeNormal, Type: corev1.EventTypeNormal,
Reason: "ScalingReplicaSet", Reason: "ScalingReplicaSet",
Message: "Scaled up replica set bar-002 from 0 to 1", Message: "Scaled up replica set bar-002 to 1",
ReportingController: "deployment-controller", ReportingController: "deployment-controller",
EventTime: metav1.NewMicroTime(time.Now().Add(-20 * time.Minute)), EventTime: metav1.NewMicroTime(time.Now().Add(-20 * time.Minute)),
Series: &corev1.EventSeries{ Series: &corev1.EventSeries{
@ -178,7 +177,7 @@ bar 11m (x3 over 15m) Normal ScalingReplicaSet Deployment/bar2 Scaled up replica
}, },
}, },
expected: `NAMESPACE LAST SEEN TYPE REASON OBJECT MESSAGE expected: `NAMESPACE LAST SEEN TYPE REASON OBJECT MESSAGE
foo 12m (x3 over 20m) Normal ScalingReplicaSet Deployment/bar Scaled up replica set bar-002 from 0 to 1 foo 12m (x3 over 20m) Normal ScalingReplicaSet Deployment/bar Scaled up replica set bar-002 to 1
`, `,
}, },
{ {
@ -200,7 +199,7 @@ foo 12m (x3 over 20m) Normal ScalingReplicaSet Deployment/bar Scaled up replica
}, },
Type: corev1.EventTypeNormal, Type: corev1.EventTypeNormal,
Reason: "ScalingReplicaSet", Reason: "ScalingReplicaSet",
Message: "Scaled up replica set bar-002 from 0 to 1", Message: "Scaled up replica set bar-002 to 1",
ReportingController: "deployment-controller", ReportingController: "deployment-controller",
EventTime: metav1.NewMicroTime(time.Now().Add(-20 * time.Minute)), EventTime: metav1.NewMicroTime(time.Now().Add(-20 * time.Minute)),
Series: &corev1.EventSeries{ Series: &corev1.EventSeries{
@ -208,7 +207,7 @@ foo 12m (x3 over 20m) Normal ScalingReplicaSet Deployment/bar Scaled up replica
LastObservedTime: metav1.NewMicroTime(time.Now().Add(-12 * time.Minute)), LastObservedTime: metav1.NewMicroTime(time.Now().Add(-12 * time.Minute)),
}, },
}, },
expected: `foo 12m (x3 over 20m) Normal ScalingReplicaSet Deployment/bar Scaled up replica set bar-002 from 0 to 1 expected: `foo 12m (x3 over 20m) Normal ScalingReplicaSet Deployment/bar Scaled up replica set bar-002 to 1
`, `,
}, },
{ {

View File

@ -121,7 +121,7 @@ func NewCmdEvents(restClientGetter genericclioptions.RESTClientGetter, streams g
flags := NewEventsFlags(restClientGetter, streams) flags := NewEventsFlags(restClientGetter, streams)
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: fmt.Sprintf("events [(-o|--output=)%s] [--for TYPE/NAME] [--watch] [--types=Normal,Warning]", strings.Join(flags.PrintFlags.AllowedFormats(), "|")), Use: fmt.Sprintf("events [(-o|--output=)%s] [--for TYPE/NAME] [--watch] [--event=Normal,Warning]", strings.Join(flags.PrintFlags.AllowedFormats(), "|")),
DisableFlagsInUseLine: true, DisableFlagsInUseLine: true,
Short: i18n.T("List events"), Short: i18n.T("List events"),
Long: eventsLong, Long: eventsLong,
@ -188,7 +188,7 @@ func (flags *EventsFlags) ToOptions() (*EventsOptions, error) {
} }
if len(o.FilterTypes) > 0 { if len(o.FilterTypes) > 0 {
o.FilterTypes = sets.List(sets.New[string](o.FilterTypes...)) o.FilterTypes = sets.NewString(o.FilterTypes...).List()
} }
var printer printers.ResourcePrinter var printer printers.ResourcePrinter
@ -229,7 +229,6 @@ func (o *EventsOptions) Run() error {
if o.forName != "" { if o.forName != "" {
listOptions.FieldSelector = fields.AndSelectors( listOptions.FieldSelector = fields.AndSelectors(
fields.OneTermEqualSelector("involvedObject.kind", o.forGVK.Kind), fields.OneTermEqualSelector("involvedObject.kind", o.forGVK.Kind),
fields.OneTermEqualSelector("involvedObject.apiVersion", o.forGVK.GroupVersion().String()),
fields.OneTermEqualSelector("involvedObject.name", o.forName)).String() fields.OneTermEqualSelector("involvedObject.name", o.forName)).String()
} }
if o.Watch { if o.Watch {

View File

@ -50,7 +50,7 @@ func getFakeEvents() *corev1.EventList {
}, },
Type: corev1.EventTypeNormal, Type: corev1.EventTypeNormal,
Reason: "ScalingReplicaSet", Reason: "ScalingReplicaSet",
Message: "Scaled up replica set bar-002 from 0 to 1", Message: "Scaled up replica set bar-002 to 1",
ReportingController: "deployment-controller", ReportingController: "deployment-controller",
EventTime: metav1.NewMicroTime(time.Now().Add(-30 * time.Minute)), EventTime: metav1.NewMicroTime(time.Now().Add(-30 * time.Minute)),
Series: &corev1.EventSeries{ Series: &corev1.EventSeries{
@ -72,7 +72,7 @@ func getFakeEvents() *corev1.EventList {
}, },
Type: corev1.EventTypeWarning, Type: corev1.EventTypeWarning,
Reason: "ScalingReplicaSet", Reason: "ScalingReplicaSet",
Message: "Scaled up replica set bar-002 from 0 to 1", Message: "Scaled up replica set bar-002 to 1",
ReportingController: "deployment-controller", ReportingController: "deployment-controller",
EventTime: metav1.NewMicroTime(time.Now().Add(-28 * time.Minute)), EventTime: metav1.NewMicroTime(time.Now().Add(-28 * time.Minute)),
Series: &corev1.EventSeries{ Series: &corev1.EventSeries{
@ -94,7 +94,7 @@ func getFakeEvents() *corev1.EventList {
}, },
Type: corev1.EventTypeNormal, Type: corev1.EventTypeNormal,
Reason: "ScalingReplicaSet", Reason: "ScalingReplicaSet",
Message: "Scaled up replica set bar-002 from 0 to 1", Message: "Scaled up replica set bar-002 to 1",
ReportingController: "deployment-controller", ReportingController: "deployment-controller",
EventTime: metav1.NewMicroTime(time.Now().Add(-25 * time.Minute)), EventTime: metav1.NewMicroTime(time.Now().Add(-25 * time.Minute)),
Series: &corev1.EventSeries{ Series: &corev1.EventSeries{
@ -110,7 +110,7 @@ func TestEventIsSorted(t *testing.T) {
codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
streams, _, buf, _ := genericiooptions.NewTestIOStreams() streams, _, buf, _ := genericiooptions.NewTestIOStreams()
clientset, err := kubernetes.NewForConfig(cmdtesting.DefaultClientConfig()) clientset, err := kubernetes.NewForConfig(cmdtesting.DefaultClientConfig())
if err != nil { if err != err {
t.Fatal(err) t.Fatal(err)
} }
@ -135,9 +135,9 @@ func TestEventIsSorted(t *testing.T) {
} }
expected := `NAMESPACE LAST SEEN TYPE REASON OBJECT MESSAGE expected := `NAMESPACE LAST SEEN TYPE REASON OBJECT MESSAGE
foo 20m (x3 over 30m) Normal ScalingReplicaSet Deployment/bar Scaled up replica set bar-002 from 0 to 1 foo 20m (x3 over 30m) Normal ScalingReplicaSet Deployment/bar Scaled up replica set bar-002 to 1
foo 18m (x3 over 28m) Warning ScalingReplicaSet Deployment/bar Scaled up replica set bar-002 from 0 to 1 foo 18m (x3 over 28m) Warning ScalingReplicaSet Deployment/bar Scaled up replica set bar-002 to 1
otherfoo 15m (x3 over 25m) Normal ScalingReplicaSet Deployment/bar Scaled up replica set bar-002 from 0 to 1 otherfoo 15m (x3 over 25m) Normal ScalingReplicaSet Deployment/bar Scaled up replica set bar-002 to 1
` `
if e, a := expected, buf.String(); e != a { if e, a := expected, buf.String(); e != a {
t.Errorf("expected\n%v\ngot\n%v", e, a) t.Errorf("expected\n%v\ngot\n%v", e, a)
@ -148,7 +148,7 @@ func TestEventNoHeaders(t *testing.T) {
codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
streams, _, buf, _ := genericiooptions.NewTestIOStreams() streams, _, buf, _ := genericiooptions.NewTestIOStreams()
clientset, err := kubernetes.NewForConfig(cmdtesting.DefaultClientConfig()) clientset, err := kubernetes.NewForConfig(cmdtesting.DefaultClientConfig())
if err != nil { if err != err {
t.Fatal(err) t.Fatal(err)
} }
@ -172,9 +172,9 @@ func TestEventNoHeaders(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
expected := `foo 20m (x3 over 30m) Normal ScalingReplicaSet Deployment/bar Scaled up replica set bar-002 from 0 to 1 expected := `foo 20m (x3 over 30m) Normal ScalingReplicaSet Deployment/bar Scaled up replica set bar-002 to 1
foo 18m (x3 over 28m) Warning ScalingReplicaSet Deployment/bar Scaled up replica set bar-002 from 0 to 1 foo 18m (x3 over 28m) Warning ScalingReplicaSet Deployment/bar Scaled up replica set bar-002 to 1
otherfoo 15m (x3 over 25m) Normal ScalingReplicaSet Deployment/bar Scaled up replica set bar-002 from 0 to 1 otherfoo 15m (x3 over 25m) Normal ScalingReplicaSet Deployment/bar Scaled up replica set bar-002 to 1
` `
if e, a := expected, buf.String(); e != a { if e, a := expected, buf.String(); e != a {
t.Errorf("expected\n%v\ngot\n%v", e, a) t.Errorf("expected\n%v\ngot\n%v", e, a)
@ -185,7 +185,7 @@ func TestEventFiltered(t *testing.T) {
codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
streams, _, buf, _ := genericiooptions.NewTestIOStreams() streams, _, buf, _ := genericiooptions.NewTestIOStreams()
clientset, err := kubernetes.NewForConfig(cmdtesting.DefaultClientConfig()) clientset, err := kubernetes.NewForConfig(cmdtesting.DefaultClientConfig())
if err != nil { if err != err {
t.Fatal(err) t.Fatal(err)
} }
@ -211,7 +211,7 @@ func TestEventFiltered(t *testing.T) {
} }
expected := `NAMESPACE LAST SEEN TYPE REASON OBJECT MESSAGE expected := `NAMESPACE LAST SEEN TYPE REASON OBJECT MESSAGE
foo 18m (x3 over 28m) Warning ScalingReplicaSet Deployment/bar Scaled up replica set bar-002 from 0 to 1 foo 18m (x3 over 28m) Warning ScalingReplicaSet Deployment/bar Scaled up replica set bar-002 to 1
` `
if e, a := expected, buf.String(); e != a { if e, a := expected, buf.String(); e != a {
t.Errorf("expected\n%v\ngot\n%v", e, a) t.Errorf("expected\n%v\ngot\n%v", e, a)

View File

@ -27,7 +27,6 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/httpstream"
"k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/genericiooptions"
"k8s.io/cli-runtime/pkg/resource" "k8s.io/cli-runtime/pkg/resource"
@ -114,26 +113,18 @@ func NewCmdExec(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Co
// RemoteExecutor defines the interface accepted by the Exec command - provided for test stubbing // RemoteExecutor defines the interface accepted by the Exec command - provided for test stubbing
type RemoteExecutor interface { type RemoteExecutor interface {
// Execute supports executing remote command in a pod. Execute(method string, url *url.URL, config *restclient.Config, stdin io.Reader, stdout, stderr io.Writer, tty bool, terminalSizeQueue remotecommand.TerminalSizeQueue) error
Execute(url *url.URL, config *restclient.Config, stdin io.Reader, stdout, stderr io.Writer, tty bool, terminalSizeQueue remotecommand.TerminalSizeQueue) error
// ExecuteWithContext, in contrast to Execute, supports stopping the remote command via context cancellation.
ExecuteWithContext(ctx context.Context, url *url.URL, config *restclient.Config, stdin io.Reader, stdout, stderr io.Writer, tty bool, terminalSizeQueue remotecommand.TerminalSizeQueue) error
} }
// DefaultRemoteExecutor is the standard implementation of remote command execution // DefaultRemoteExecutor is the standard implementation of remote command execution
type DefaultRemoteExecutor struct{} type DefaultRemoteExecutor struct{}
func (d *DefaultRemoteExecutor) Execute(url *url.URL, config *restclient.Config, stdin io.Reader, stdout, stderr io.Writer, tty bool, terminalSizeQueue remotecommand.TerminalSizeQueue) error { func (*DefaultRemoteExecutor) Execute(method string, url *url.URL, config *restclient.Config, stdin io.Reader, stdout, stderr io.Writer, tty bool, terminalSizeQueue remotecommand.TerminalSizeQueue) error {
return d.ExecuteWithContext(context.Background(), url, config, stdin, stdout, stderr, tty, terminalSizeQueue) exec, err := remotecommand.NewSPDYExecutor(config, method, url)
}
func (*DefaultRemoteExecutor) ExecuteWithContext(ctx context.Context, url *url.URL, config *restclient.Config, stdin io.Reader, stdout, stderr io.Writer, tty bool, terminalSizeQueue remotecommand.TerminalSizeQueue) error {
exec, err := createExecutor(url, config)
if err != nil { if err != nil {
return err return err
} }
return exec.StreamWithContext(ctx, remotecommand.StreamOptions{ return exec.StreamWithContext(context.Background(), remotecommand.StreamOptions{
Stdin: stdin, Stdin: stdin,
Stdout: stdout, Stdout: stdout,
Stderr: stderr, Stderr: stderr,
@ -142,29 +133,6 @@ func (*DefaultRemoteExecutor) ExecuteWithContext(ctx context.Context, url *url.U
}) })
} }
// createExecutor returns the Executor or an error if one occurred.
func createExecutor(url *url.URL, config *restclient.Config) (remotecommand.Executor, error) {
exec, err := remotecommand.NewSPDYExecutor(config, "POST", url)
if err != nil {
return nil, err
}
// Fallback executor is default, unless feature flag is explicitly disabled.
if !cmdutil.RemoteCommandWebsockets.IsDisabled() {
// WebSocketExecutor must be "GET" method as described in RFC 6455 Sec. 4.1 (page 17).
websocketExec, err := remotecommand.NewWebSocketExecutor(config, "GET", url.String())
if err != nil {
return nil, err
}
exec, err = remotecommand.NewFallbackExecutor(websocketExec, exec, func(err error) bool {
return httpstream.IsUpgradeFailure(err) || httpstream.IsHTTPSProxyError(err)
})
if err != nil {
return nil, err
}
}
return exec, nil
}
type StreamOptions struct { type StreamOptions struct {
Namespace string Namespace string
PodName string PodName string
@ -210,8 +178,17 @@ func (p *ExecOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, argsIn []s
} }
if argsLenAtDash > -1 { if argsLenAtDash > -1 {
p.Command = argsIn[argsLenAtDash:] p.Command = argsIn[argsLenAtDash:]
} else if len(argsIn) > 1 || (len(argsIn) > 0 && len(p.FilenameOptions.Filenames) != 0) { } else if len(argsIn) > 1 {
return cmdutil.UsageErrorf(cmd, "exec [POD] [COMMAND] is not supported anymore. Use exec [POD] -- [COMMAND] instead") if !p.Quiet {
fmt.Fprint(p.ErrOut, "kubectl exec [POD] [COMMAND] is DEPRECATED and will be removed in a future version. Use kubectl exec [POD] -- [COMMAND] instead.\n")
}
p.Command = argsIn[1:]
} else if len(argsIn) > 0 && len(p.FilenameOptions.Filenames) != 0 {
if !p.Quiet {
fmt.Fprint(p.ErrOut, "kubectl exec [POD] [COMMAND] is DEPRECATED and will be removed in a future version. Use kubectl exec [POD] -- [COMMAND] instead.\n")
}
p.Command = argsIn[0:]
p.ResourceName = ""
} }
var err error var err error
@ -224,7 +201,7 @@ func (p *ExecOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, argsIn []s
p.GetPodTimeout, err = cmdutil.GetPodRunningTimeoutFlag(cmd) p.GetPodTimeout, err = cmdutil.GetPodRunningTimeoutFlag(cmd)
if err != nil { if err != nil {
return cmdutil.UsageErrorf(cmd, "%s", err.Error()) return cmdutil.UsageErrorf(cmd, err.Error())
} }
p.Builder = f.NewBuilder p.Builder = f.NewBuilder
@ -394,7 +371,7 @@ func (p *ExecOptions) Run() error {
TTY: t.Raw, TTY: t.Raw,
}, scheme.ParameterCodec) }, scheme.ParameterCodec)
return p.Executor.Execute(req.URL(), p.Config, p.In, p.Out, p.ErrOut, t.Raw, sizeQueue) return p.Executor.Execute("POST", req.URL(), p.Config, p.In, p.Out, p.ErrOut, t.Raw, sizeQueue)
} }
if err := t.Safe(fn); err != nil { if err := t.Safe(fn); err != nil {

View File

@ -18,7 +18,6 @@ package exec
import ( import (
"bytes" "bytes"
"context"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
@ -34,22 +33,20 @@ import (
restclient "k8s.io/client-go/rest" restclient "k8s.io/client-go/rest"
"k8s.io/client-go/rest/fake" "k8s.io/client-go/rest/fake"
"k8s.io/client-go/tools/remotecommand" "k8s.io/client-go/tools/remotecommand"
cmdtesting "k8s.io/kubectl/pkg/cmd/testing" cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
cmdutil "k8s.io/kubectl/pkg/cmd/util"
"k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/scheme"
"k8s.io/kubectl/pkg/util/term" "k8s.io/kubectl/pkg/util/term"
) )
type fakeRemoteExecutor struct { type fakeRemoteExecutor struct {
method string
url *url.URL url *url.URL
execErr error execErr error
} }
func (f *fakeRemoteExecutor) Execute(url *url.URL, config *restclient.Config, stdin io.Reader, stdout, stderr io.Writer, tty bool, terminalSizeQueue remotecommand.TerminalSizeQueue) error { func (f *fakeRemoteExecutor) Execute(method string, url *url.URL, config *restclient.Config, stdin io.Reader, stdout, stderr io.Writer, tty bool, terminalSizeQueue remotecommand.TerminalSizeQueue) error {
return f.ExecuteWithContext(context.Background(), url, config, stdin, stdout, stderr, tty, terminalSizeQueue) f.method = method
}
func (f *fakeRemoteExecutor) ExecuteWithContext(ctx context.Context, url *url.URL, config *restclient.Config, stdin io.Reader, stdout, stderr io.Writer, tty bool, terminalSizeQueue remotecommand.TerminalSizeQueue) error {
f.url = url f.url = url
return f.execErr return f.execErr
} }
@ -115,17 +112,20 @@ func TestPodAndContainer(t *testing.T) {
p: &ExecOptions{}, p: &ExecOptions{},
args: []string{"foo", "cmd"}, args: []string{"foo", "cmd"},
argsLenAtDash: -1, argsLenAtDash: -1,
expectError: true, expectedPod: "foo",
expectedArgs: []string{"cmd"},
name: "cmd, cmd is behind dash", name: "cmd, cmd is behind dash",
obj: execPod(), obj: execPod(),
}, },
{ {
p: &ExecOptions{StreamOptions: StreamOptions{ContainerName: "bar"}}, p: &ExecOptions{StreamOptions: StreamOptions{ContainerName: "bar"}},
args: []string{"foo", "cmd"}, args: []string{"foo", "cmd"},
argsLenAtDash: -1, argsLenAtDash: -1,
expectError: true, expectedPod: "foo",
name: "cmd, container in flag", expectedContainer: "bar",
obj: execPod(), expectedArgs: []string{"cmd"},
name: "cmd, container in flag",
obj: execPod(),
}, },
} }
for _, test := range tests { for _, test := range tests {
@ -240,8 +240,8 @@ func TestExec(t *testing.T) {
Executor: ex, Executor: ex,
} }
cmd := NewCmdExec(tf, genericiooptions.NewTestIOStreamsDiscard()) cmd := NewCmdExec(tf, genericiooptions.NewTestIOStreamsDiscard())
args := []string{"pod/foo", "--", "command"} args := []string{"pod/foo", "command"}
if err := params.Complete(tf, cmd, args, 1); err != nil { if err := params.Complete(tf, cmd, args, -1); err != nil {
t.Fatal(err) t.Fatal(err)
} }
err := params.Run() err := params.Run()
@ -264,6 +264,9 @@ func TestExec(t *testing.T) {
t.Errorf("%s: Did not get expected container query param for exec request", test.name) t.Errorf("%s: Did not get expected container query param for exec request", test.name)
return return
} }
if ex.method != "POST" {
t.Errorf("%s: Did not get method for exec request: %s", test.name, ex.method)
}
}) })
} }
} }
@ -404,37 +407,3 @@ func TestSetupTTY(t *testing.T) {
t.Errorf("attach stdin, TTY, is a terminal: tty.Out should equal o.Out") t.Errorf("attach stdin, TTY, is a terminal: tty.Out should equal o.Out")
} }
} }
func TestCreateExecutor(t *testing.T) {
url, err := url.Parse("http://localhost:8080/index.html")
if err != nil {
t.Fatalf("unable to parse test url: %v", err)
}
config := cmdtesting.DefaultClientConfig()
// First, ensure that no environment variable creates the fallback executor.
executor, err := createExecutor(url, config)
if err != nil {
t.Fatalf("unable to create executor: %v", err)
}
if _, isFallback := executor.(*remotecommand.FallbackExecutor); !isFallback {
t.Errorf("expected fallback executor, got %#v", executor)
}
// Next, check turning on feature flag explicitly also creates fallback executor.
t.Setenv(string(cmdutil.RemoteCommandWebsockets), "true")
executor, err = createExecutor(url, config)
if err != nil {
t.Fatalf("unable to create executor: %v", err)
}
if _, isFallback := executor.(*remotecommand.FallbackExecutor); !isFallback {
t.Errorf("expected fallback executor, got %#v", executor)
}
// Finally, check explicit disabling does NOT create the fallback executor.
t.Setenv(string(cmdutil.RemoteCommandWebsockets), "false")
executor, err = createExecutor(url, config)
if err != nil {
t.Fatalf("unable to create executor: %v", err)
}
if _, isFallback := executor.(*remotecommand.FallbackExecutor); isFallback {
t.Errorf("expected fallback executor, got %#v", executor)
}
}

View File

@ -56,113 +56,86 @@ var (
# Get the documentation of a specific field of a resource # Get the documentation of a specific field of a resource
kubectl explain pods.spec.containers kubectl explain pods.spec.containers
# Get the documentation of resources in different format # Get the documentation of resources in different format
kubectl explain deployment --output=plaintext-openapiv2`)) kubectl explain deployment --output=plaintext-openapiv2`))
)
const (
plaintextTemplateName = "plaintext" plaintextTemplateName = "plaintext"
plaintextOpenAPIV2TemplateName = "plaintext-openapiv2" plaintextOpenAPIV2TemplateName = "plaintext-openapiv2"
) )
// ExplainFlags directly reflect the information that CLI is gathering via flags. type ExplainOptions struct {
// They will be converted to Options, which reflect the runtime requirements for
// the command.
type ExplainFlags struct {
APIVersion string
OutputFormat string
Recursive bool
genericiooptions.IOStreams genericiooptions.IOStreams
CmdParent string
APIVersion string
Recursive bool
args []string
Mapper meta.RESTMapper
Schema openapi.Resources
// Name of the template to use with the openapiv3 template renderer.
OutputFormat string
// Client capable of fetching openapi documents from the user's cluster
OpenAPIV3Client openapiclient.Client
} }
// NewExplainFlags returns a default ExplainFlags func NewExplainOptions(parent string, streams genericiooptions.IOStreams) *ExplainOptions {
func NewExplainFlags(streams genericiooptions.IOStreams) *ExplainFlags { return &ExplainOptions{
return &ExplainFlags{
OutputFormat: plaintextTemplateName,
IOStreams: streams, IOStreams: streams,
CmdParent: parent,
OutputFormat: plaintextTemplateName,
} }
} }
// AddFlags registers flags for a cli
func (flags *ExplainFlags) AddFlags(cmd *cobra.Command) {
cmd.Flags().BoolVar(&flags.Recursive, "recursive", flags.Recursive, "Print the fields of fields (Currently only 1 level deep)")
cmd.Flags().StringVar(&flags.APIVersion, "api-version", flags.APIVersion, "Get different explanations for particular API version (API group/version)")
cmd.Flags().StringVarP(&flags.OutputFormat, "output", "o", plaintextTemplateName, "Format in which to render the schema (plaintext, plaintext-openapiv2)")
}
// ToOptions converts from CLI inputs to runtime input
func (flags *ExplainFlags) ToOptions(f cmdutil.Factory, parent string, args []string) (*ExplainOptions, error) {
mapper, err := f.ToRESTMapper()
if err != nil {
return nil, err
}
// Only openapi v3 needs the discovery client.
openAPIV3Client, err := f.OpenAPIV3Client()
if err != nil {
return nil, err
}
o := &ExplainOptions{
IOStreams: flags.IOStreams,
Recursive: flags.Recursive,
APIVersion: flags.APIVersion,
OutputFormat: flags.OutputFormat,
CmdParent: parent,
args: args,
Mapper: mapper,
openAPIGetter: f,
OpenAPIV3Client: openAPIV3Client,
}
return o, nil
}
// NewCmdExplain returns a cobra command for swagger docs // NewCmdExplain returns a cobra command for swagger docs
func NewCmdExplain(parent string, f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { func NewCmdExplain(parent string, f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command {
flags := NewExplainFlags(streams) o := NewExplainOptions(parent, streams)
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "explain TYPE [--recursive=FALSE|TRUE] [--api-version=api-version-group] [-o|--output=plaintext|plaintext-openapiv2]", Use: "explain TYPE [--recursive=FALSE|TRUE] [--api-version=api-version-group] [--output=plaintext|plaintext-openapiv2]",
DisableFlagsInUseLine: true, DisableFlagsInUseLine: true,
Short: i18n.T("Get documentation for a resource"), Short: i18n.T("Get documentation for a resource"),
Long: explainLong + "\n\n" + cmdutil.SuggestAPIResources(parent), Long: explainLong + "\n\n" + cmdutil.SuggestAPIResources(parent),
Example: explainExamples, Example: explainExamples,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
o, err := flags.ToOptions(f, parent, args) cmdutil.CheckErr(o.Complete(f, cmd, args))
cmdutil.CheckErr(err)
cmdutil.CheckErr(o.Validate()) cmdutil.CheckErr(o.Validate())
cmdutil.CheckErr(o.Run()) cmdutil.CheckErr(o.Run())
}, },
} }
cmd.Flags().BoolVar(&o.Recursive, "recursive", o.Recursive, "When true, print the name of all the fields recursively. Otherwise, print the available fields with their description.")
cmd.Flags().StringVar(&o.APIVersion, "api-version", o.APIVersion, "Use given api-version (group/version) of the resource.")
flags.AddFlags(cmd) // Only enable --output as a valid flag if the feature is enabled
cmd.Flags().StringVar(&o.OutputFormat, "output", plaintextTemplateName, "Format in which to render the schema. Valid values are: (plaintext, plaintext-openapiv2).")
return cmd return cmd
} }
type ExplainOptions struct { func (o *ExplainOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error {
genericiooptions.IOStreams var err error
o.Mapper, err = f.ToRESTMapper()
if err != nil {
return err
}
Recursive bool o.Schema, err = f.OpenAPISchema()
APIVersion string if err != nil {
// Name of the template to use with the openapiv3 template renderer. return err
OutputFormat string }
CmdParent string // Only openapi v3 needs the discovery client.
args []string o.OpenAPIV3Client, err = f.OpenAPIV3Client()
if err != nil {
return err
}
Mapper meta.RESTMapper o.args = args
openAPIGetter openapi.OpenAPIResourcesGetter return nil
// Client capable of fetching openapi documents from the user's cluster
OpenAPIV3Client openapiclient.Client
} }
func (o *ExplainOptions) Validate() error { func (o *ExplainOptions) Validate() error {
@ -251,11 +224,7 @@ func (o *ExplainOptions) renderOpenAPIV2(
gvk = apiVersion.WithKind(gvk.Kind) gvk = apiVersion.WithKind(gvk.Kind)
} }
resources, err := o.openAPIGetter.OpenAPISchema() schema := o.Schema.LookupResource(gvk)
if err != nil {
return err
}
schema := resources.LookupResource(gvk)
if schema == nil { if schema == nil {
return fmt.Errorf("couldn't find resource for %q", gvk) return fmt.Errorf("couldn't find resource for %q", gvk)
} }

View File

@ -57,9 +57,9 @@ func TestExplainInvalidArgs(t *testing.T) {
tf := cmdtesting.NewTestFactory() tf := cmdtesting.NewTestFactory()
defer tf.Cleanup() defer tf.Cleanup()
flags := explain.NewExplainFlags(genericiooptions.NewTestIOStreamsDiscard()) opts := explain.NewExplainOptions("kubectl", genericiooptions.NewTestIOStreamsDiscard())
cmd := explain.NewCmdExplain("kubectl", tf, genericiooptions.NewTestIOStreamsDiscard())
opts, err := flags.ToOptions(tf, "kubectl", []string{}) err := opts.Complete(tf, cmd, []string{})
if err != nil { if err != nil {
t.Fatalf("unexpected error %v", err) t.Fatalf("unexpected error %v", err)
} }
@ -69,7 +69,7 @@ func TestExplainInvalidArgs(t *testing.T) {
t.Error("unexpected non-error") t.Error("unexpected non-error")
} }
opts, err = flags.ToOptions(tf, "kubectl", []string{"resource1", "resource2"}) err = opts.Complete(tf, cmd, []string{"resource1", "resource2"})
if err != nil { if err != nil {
t.Fatalf("unexpected error %v", err) t.Fatalf("unexpected error %v", err)
} }
@ -84,9 +84,9 @@ func TestExplainNotExistResource(t *testing.T) {
tf := cmdtesting.NewTestFactory() tf := cmdtesting.NewTestFactory()
defer tf.Cleanup() defer tf.Cleanup()
flags := explain.NewExplainFlags(genericiooptions.NewTestIOStreamsDiscard()) opts := explain.NewExplainOptions("kubectl", genericiooptions.NewTestIOStreamsDiscard())
cmd := explain.NewCmdExplain("kubectl", tf, genericiooptions.NewTestIOStreamsDiscard())
opts, err := flags.ToOptions(tf, "kubectl", []string{"foo"}) err := opts.Complete(tf, cmd, []string{"foo"})
if err != nil { if err != nil {
t.Fatalf("unexpected error %v", err) t.Fatalf("unexpected error %v", err)
} }
@ -275,44 +275,3 @@ func runExplainTestCases(t *testing.T, cases []explainTestCase) {
buf.Reset() buf.Reset()
} }
} }
// OpenAPI V2 specifications retrieval -- should never be called.
func panicOpenAPISchemaFn() (openapi.Resources, error) {
panic("should never be called")
}
// OpenAPI V3 specifications retrieval does *not* retrieve V2 specifications.
func TestExplainOpenAPIV3DoesNotLoadOpenAPIV2Specs(t *testing.T) {
// Set up OpenAPI V3 specifications endpoint for explain.
fakeServer, err := clienttestutil.NewFakeOpenAPIV3Server(filepath.Join(testDataPath, "openapi", "v3"))
if err != nil {
t.Fatalf("error starting fake openapi server: %v", err.Error())
}
defer fakeServer.HttpServer.Close()
tf := cmdtesting.NewTestFactory()
defer tf.Cleanup()
tf.OpenAPIV3ClientFunc = func() (openapiclient.Client, error) {
fakeDiscoveryClient := discovery.NewDiscoveryClientForConfigOrDie(&rest.Config{Host: fakeServer.HttpServer.URL})
return fakeDiscoveryClient.OpenAPIV3(), nil
}
// OpenAPI V2 specifications retrieval will panic if called.
tf.OpenAPISchemaFunc = panicOpenAPISchemaFn
// Explain the following resources, validating the command does not panic.
cmd := explain.NewCmdExplain("kubectl", tf, genericiooptions.NewTestIOStreamsDiscard())
resources := []string{"pods", "services", "endpoints", "configmaps"}
for _, resource := range resources {
cmd.Run(cmd, []string{resource})
}
// Verify retrieving OpenAPI V2 specifications will panic.
defer func() {
if panicErr := recover(); panicErr == nil {
t.Fatal("expecting panic for openapi v2 retrieval")
}
}()
// Set OpenAPI V2 output flag for explain.
if err := cmd.Flags().Set("output", "plaintext-openapiv2"); err != nil {
t.Fatal(err)
}
cmd.Run(cmd, []string{"pods"})
}

View File

@ -225,12 +225,12 @@ func (flags *ExposeServiceFlags) AddFlags(cmd *cobra.Command) {
} }
func (flags *ExposeServiceFlags) ToOptions(cmd *cobra.Command, args []string) (*ExposeServiceOptions, error) { func (flags *ExposeServiceFlags) ToOptions(cmd *cobra.Command, args []string) (*ExposeServiceOptions, error) {
dryRunStrategy, err := cmdutil.GetDryRunStrategy(cmd) dryRunStratergy, err := cmdutil.GetDryRunStrategy(cmd)
if err != nil { if err != nil {
return nil, err return nil, err
} }
cmdutil.PrintFlagsWithDryRunStrategy(flags.PrintFlags, dryRunStrategy) cmdutil.PrintFlagsWithDryRunStrategy(flags.PrintFlags, dryRunStratergy)
printer, err := flags.PrintFlags.ToPrinter() printer, err := flags.PrintFlags.ToPrinter()
if err != nil { if err != nil {
return nil, err return nil, err
@ -243,7 +243,7 @@ func (flags *ExposeServiceFlags) ToOptions(cmd *cobra.Command, args []string) (*
} }
e := &ExposeServiceOptions{ e := &ExposeServiceOptions{
DryRunStrategy: dryRunStrategy, DryRunStrategy: dryRunStratergy,
PrintObj: printer.PrintObj, PrintObj: printer.PrintObj,
Recorder: recorder, Recorder: recorder,
IOStreams: flags.IOStreams, IOStreams: flags.IOStreams,
@ -532,7 +532,12 @@ func (o *ExposeServiceOptions) createService() (*corev1.Service, error) {
} }
targetPortString := o.TargetPort targetPortString := o.TargetPort
if len(targetPortString) > 0 { if len(targetPortString) > 0 {
targetPort := intstr.Parse(targetPortString) var targetPort intstr.IntOrString
if portNum, err := strconv.Atoi(targetPortString); err != nil {
targetPort = intstr.FromString(targetPortString)
} else {
targetPort = intstr.FromInt(portNum)
}
// Use the same target-port for every port // Use the same target-port for every port
for i := range service.Spec.Ports { for i := range service.Spec.Ports {
service.Spec.Ports[i].TargetPort = targetPort service.Spec.Ports[i].TargetPort = targetPort

View File

@ -694,6 +694,7 @@ func TestExposeOverride(t *testing.T) {
expected: `apiVersion: v1 expected: `apiVersion: v1
kind: Service kind: Service
metadata: metadata:
creationTimestamp: null
labels: labels:
svc: test svc: test
name: foo name: foo
@ -716,6 +717,7 @@ status:
expected: `apiVersion: v1 expected: `apiVersion: v1
kind: Service kind: Service
metadata: metadata:
creationTimestamp: null
labels: labels:
svc: test svc: test
name: foo name: foo
@ -743,6 +745,7 @@ status:
expected: `apiVersion: v1 expected: `apiVersion: v1
kind: Service kind: Service
metadata: metadata:
creationTimestamp: null
labels: labels:
svc: test svc: test
name: foo name: foo
@ -770,6 +773,7 @@ status:
expected: `apiVersion: v1 expected: `apiVersion: v1
kind: Service kind: Service
metadata: metadata:
creationTimestamp: null
labels: labels:
svc: test svc: test
name: foo name: foo
@ -1705,13 +1709,13 @@ func TestGenerateService(t *testing.T) {
Name: "port-1-tcp", Name: "port-1-tcp",
Port: 53, Port: 53,
Protocol: corev1.ProtocolTCP, Protocol: corev1.ProtocolTCP,
TargetPort: intstr.FromInt32(53), TargetPort: intstr.FromInt(53),
}, },
{ {
Name: "port-1-udp", Name: "port-1-udp",
Port: 53, Port: 53,
Protocol: corev1.ProtocolUDP, Protocol: corev1.ProtocolUDP,
TargetPort: intstr.FromInt32(53), TargetPort: intstr.FromInt(53),
}, },
}, },
ClusterIP: corev1.ClusterIPNone, ClusterIP: corev1.ClusterIPNone,

View File

@ -19,7 +19,6 @@ package get
import ( import (
"bufio" "bufio"
"bytes" "bytes"
"errors"
"fmt" "fmt"
"io" "io"
"reflect" "reflect"
@ -162,7 +161,7 @@ func (s *CustomColumnsPrinter) PrintObj(obj runtime.Object, out io.Writer) error
// we need an actual value in order to retrieve the package path for an object. // we need an actual value in order to retrieve the package path for an object.
// using reflect.Indirect indiscriminately is valid here, as all runtime.Objects are supposed to be pointers. // using reflect.Indirect indiscriminately is valid here, as all runtime.Objects are supposed to be pointers.
if printers.InternalObjectPreventer.IsForbidden(reflect.Indirect(reflect.ValueOf(obj)).Type().PkgPath()) { if printers.InternalObjectPreventer.IsForbidden(reflect.Indirect(reflect.ValueOf(obj)).Type().PkgPath()) {
return errors.New(printers.InternalObjectPrinterErr) return fmt.Errorf(printers.InternalObjectPrinterErr)
} }
if _, found := out.(*tabwriter.Writer); !found { if _, found := out.(*tabwriter.Writer); !found {
@ -211,7 +210,7 @@ func (s *CustomColumnsPrinter) printOneObject(obj runtime.Object, parsers []*jso
switch u := obj.(type) { switch u := obj.(type) {
case *metav1.WatchEvent: case *metav1.WatchEvent:
if printers.InternalObjectPreventer.IsForbidden(reflect.Indirect(reflect.ValueOf(u.Object.Object)).Type().PkgPath()) { if printers.InternalObjectPreventer.IsForbidden(reflect.Indirect(reflect.ValueOf(u.Object.Object)).Type().PkgPath()) {
return errors.New(printers.InternalObjectPrinterErr) return fmt.Errorf(printers.InternalObjectPrinterErr)
} }
unstructuredObject, err := runtime.DefaultUnstructuredConverter.ToUnstructured(u.Object.Object) unstructuredObject, err := runtime.DefaultUnstructuredConverter.ToUnstructured(u.Object.Object)
if err != nil { if err != nil {

View File

@ -47,8 +47,9 @@ import (
"k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/scheme"
"k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/i18n"
"k8s.io/kubectl/pkg/util/interrupt" "k8s.io/kubectl/pkg/util/interrupt"
"k8s.io/kubectl/pkg/util/slice"
"k8s.io/kubectl/pkg/util/templates" "k8s.io/kubectl/pkg/util/templates"
"k8s.io/utils/ptr" utilpointer "k8s.io/utils/pointer"
) )
// GetOptions contains the input to the get command. // GetOptions contains the input to the get command.
@ -90,8 +91,8 @@ var (
Prints a table of the most important information about the specified resources. Prints a table of the most important information about the specified resources.
You can filter the list using a label selector and the --selector flag. If the You can filter the list using a label selector and the --selector flag. If the
desired resource type is namespaced you will only see results in the current desired resource type is namespaced you will only see results in your current
namespace if you don't specify any namespace. namespace unless you pass --all-namespaces.
By specifying the output as 'template' and providing a Go template as the value By specifying the output as 'template' and providing a Go template as the value
of the --template flag, you can filter the attributes of the fetched resources.`)) of the --template flag, you can filter the attributes of the fetched resources.`))
@ -131,19 +132,15 @@ var (
kubectl get rc/web service/frontend pods/web-pod-13je7 kubectl get rc/web service/frontend pods/web-pod-13je7
# List the 'status' subresource for a single pod # List the 'status' subresource for a single pod
kubectl get pod web-pod-13je7 --subresource status kubectl get pod web-pod-13je7 --subresource status`))
# List all deployments in namespace 'backend'
kubectl get deployments.apps --namespace backend
# List all pods existing in all namespaces
kubectl get pods --all-namespaces`))
) )
const ( const (
useServerPrintColumns = "server-print" useServerPrintColumns = "server-print"
) )
var supportedSubresources = []string{"status", "scale"}
// NewGetOptions returns a GetOptions with default chunk size 500. // NewGetOptions returns a GetOptions with default chunk size 500.
func NewGetOptions(parent string, streams genericiooptions.IOStreams) *GetOptions { func NewGetOptions(parent string, streams genericiooptions.IOStreams) *GetOptions {
return &GetOptions{ return &GetOptions{
@ -182,14 +179,14 @@ func NewCmdGet(parent string, f cmdutil.Factory, streams genericiooptions.IOStre
cmd.Flags().BoolVarP(&o.Watch, "watch", "w", o.Watch, "After listing/getting the requested object, watch for changes.") cmd.Flags().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.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.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 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().BoolVar(&o.IgnoreNotFound, "ignore-not-found", o.IgnoreNotFound, "If the requested object does not exist the command will return exit code 0.")
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().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.") 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) addServerPrintColumnFlags(cmd, o)
cmdutil.AddFilenameOptionFlags(cmd, &o.FilenameOptions, "identifying the resource to get from a server.") cmdutil.AddFilenameOptionFlags(cmd, &o.FilenameOptions, "identifying the resource to get from a server.")
cmdutil.AddChunkSizeFlag(cmd, &o.ChunkSize) cmdutil.AddChunkSizeFlag(cmd, &o.ChunkSize)
cmdutil.AddLabelSelectorFlagVar(cmd, &o.LabelSelector) cmdutil.AddLabelSelectorFlagVar(cmd, &o.LabelSelector)
cmdutil.AddSubresourceFlags(cmd, &o.Subresource, "If specified, gets the subresource of the requested object.") cmdutil.AddSubresourceFlags(cmd, &o.Subresource, "If specified, gets the subresource of the requested object.", supportedSubresources...)
return cmd return cmd
} }
@ -270,13 +267,9 @@ func (o *GetOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []stri
} }
switch { switch {
case o.Watch: case o.Watch || o.WatchOnly:
if len(o.SortBy) > 0 { if len(o.SortBy) > 0 {
fmt.Fprintf(o.IOStreams.ErrOut, "warning: --watch requested, --sort-by will be ignored for watch events received\n") fmt.Fprintf(o.IOStreams.ErrOut, "warning: --watch or --watch-only requested, --sort-by will be ignored\n")
}
case o.WatchOnly:
if len(o.SortBy) > 0 {
fmt.Fprintf(o.IOStreams.ErrOut, "warning: --watch-only requested, --sort-by will be ignored\n")
} }
default: default:
if len(args) == 0 && cmdutil.IsFilenameSliceEmpty(o.Filenames, o.Kustomize) { if len(args) == 0 && cmdutil.IsFilenameSliceEmpty(o.Filenames, o.Kustomize) {
@ -287,7 +280,7 @@ func (o *GetOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []stri
usageString = fmt.Sprintf("%s\nUse \"%s explain <resource>\" for a detailed description of that resource (e.g. %[2]s explain pods).", usageString, fullCmdName) usageString = fmt.Sprintf("%s\nUse \"%s explain <resource>\" for a detailed description of that resource (e.g. %[2]s explain pods).", usageString, fullCmdName)
} }
return cmdutil.UsageErrorf(cmd, "%s", usageString) return cmdutil.UsageErrorf(cmd, usageString)
} }
} }
@ -316,6 +309,9 @@ func (o *GetOptions) Validate() error {
if o.OutputWatchEvents && !(o.Watch || o.WatchOnly) { if o.OutputWatchEvents && !(o.Watch || o.WatchOnly) {
return fmt.Errorf("--output-watch-events option can only be used with --watch or --watch-only") return fmt.Errorf("--output-watch-events option can only be used with --watch or --watch-only")
} }
if len(o.Subresource) > 0 && !slice.ContainsString(supportedSubresources, o.Subresource, nil) {
return fmt.Errorf("invalid subresource value: %q. Must be one of %v", o.Subresource, supportedSubresources)
}
return nil return nil
} }
@ -486,7 +482,7 @@ func (o *GetOptions) Run(f cmdutil.Factory, args []string) error {
} }
allErrs := []error{} allErrs := []error{}
errs := sets.New[string]() errs := sets.NewString()
infos, err := r.Infos() infos, err := r.Infos()
if err != nil { if err != nil {
allErrs = append(allErrs, err) allErrs = append(allErrs, err)
@ -623,10 +619,6 @@ func (o *GetOptions) watch(f cmdutil.Factory, args []string) error {
} }
infos, err := r.Infos() infos, err := r.Infos()
if err != nil { if err != nil {
// Ignore "NotFound" error when ignore-not-found is set to true
if apierrors.IsNotFound(err) && o.IgnoreNotFound {
return nil
}
return err return err
} }
if multipleGVKsRequested(infos) { if multipleGVKsRequested(infos) {
@ -635,7 +627,7 @@ func (o *GetOptions) watch(f cmdutil.Factory, args []string) error {
info := infos[0] info := infos[0]
mapping := info.ResourceMapping() mapping := info.ResourceMapping()
outputObjects := ptr.To(!o.WatchOnly) outputObjects := utilpointer.BoolPtr(!o.WatchOnly)
printer, err := o.ToPrinter(mapping, outputObjects, o.AllNamespaces, false) printer, err := o.ToPrinter(mapping, outputObjects, o.AllNamespaces, false)
if err != nil { if err != nil {
return err return err

View File

@ -21,7 +21,6 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
cmdutil "k8s.io/kubectl/pkg/cmd/util"
"net/http" "net/http"
"reflect" "reflect"
"strings" "strings"
@ -712,7 +711,7 @@ func TestGetEmptyTable(t *testing.T) {
} }
} }
func TestGetNonExistObject(t *testing.T) { func TestGetObjectIgnoreNotFound(t *testing.T) {
cmdtesting.InitTestErrorHandler(t) cmdtesting.InitTestErrorHandler(t)
ns := &corev1.NamespaceList{ ns := &corev1.NamespaceList{
@ -746,63 +745,6 @@ func TestGetNonExistObject(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() streams, _, buf, _ := genericiooptions.NewTestIOStreams()
cmd := NewCmdGet("kubectl", tf, streams) cmd := NewCmdGet("kubectl", tf, streams)
cmd.SetOut(buf) cmd.SetOut(buf)
@ -1496,6 +1438,7 @@ func TestGetMultipleTypeObjectsAsList(t *testing.T) {
"apiVersion": "v1", "apiVersion": "v1",
"kind": "Pod", "kind": "Pod",
"metadata": { "metadata": {
"creationTimestamp": null,
"name": "foo", "name": "foo",
"namespace": "test", "namespace": "test",
"resourceVersion": "10" "resourceVersion": "10"
@ -1514,6 +1457,7 @@ func TestGetMultipleTypeObjectsAsList(t *testing.T) {
"apiVersion": "v1", "apiVersion": "v1",
"kind": "Pod", "kind": "Pod",
"metadata": { "metadata": {
"creationTimestamp": null,
"name": "bar", "name": "bar",
"namespace": "test", "namespace": "test",
"resourceVersion": "11" "resourceVersion": "11"
@ -1532,6 +1476,7 @@ func TestGetMultipleTypeObjectsAsList(t *testing.T) {
"apiVersion": "v1", "apiVersion": "v1",
"kind": "Service", "kind": "Service",
"metadata": { "metadata": {
"creationTimestamp": null,
"name": "baz", "name": "baz",
"namespace": "test", "namespace": "test",
"resourceVersion": "12" "resourceVersion": "12"
@ -2184,93 +2129,6 @@ 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) { func TestWatchStatus(t *testing.T) {
pods, events := watchTestData() pods, events := watchTestData()
events = append(events, watch.Event{Type: "ERROR", Object: &metav1.Status{Status: "Failure", Reason: "InternalServerError", Message: "Something happened"}}) events = append(events, watch.Event{Type: "ERROR", Object: &metav1.Status{Status: "Failure", Reason: "InternalServerError", Message: "Something happened"}})
@ -2509,10 +2367,10 @@ DELETED test pod/foo 0/0 0 <unknown> <none>
}, },
{ {
format: "json", format: "json",
expected: `{"type":"ADDED","object":{"apiVersion":"v1","kind":"Pod","metadata":{"name":"bar","namespace":"test","resourceVersion":"9"},"spec":{"containers":null,"dnsPolicy":"ClusterFirst","enableServiceLinks":true,"restartPolicy":"Always","securityContext":{},"terminationGracePeriodSeconds":30},"status":{}}} expected: `{"type":"ADDED","object":{"apiVersion":"v1","kind":"Pod","metadata":{"creationTimestamp":null,"name":"bar","namespace":"test","resourceVersion":"9"},"spec":{"containers":null,"dnsPolicy":"ClusterFirst","enableServiceLinks":true,"restartPolicy":"Always","securityContext":{},"terminationGracePeriodSeconds":30},"status":{}}}
{"type":"ADDED","object":{"apiVersion":"v1","kind":"Pod","metadata":{"name":"foo","namespace":"test","resourceVersion":"10"},"spec":{"containers":null,"dnsPolicy":"ClusterFirst","enableServiceLinks":true,"restartPolicy":"Always","securityContext":{},"terminationGracePeriodSeconds":30},"status":{}}} {"type":"ADDED","object":{"apiVersion":"v1","kind":"Pod","metadata":{"creationTimestamp":null,"name":"foo","namespace":"test","resourceVersion":"10"},"spec":{"containers":null,"dnsPolicy":"ClusterFirst","enableServiceLinks":true,"restartPolicy":"Always","securityContext":{},"terminationGracePeriodSeconds":30},"status":{}}}
{"type":"MODIFIED","object":{"apiVersion":"v1","kind":"Pod","metadata":{"name":"foo","namespace":"test","resourceVersion":"11"},"spec":{"containers":null,"dnsPolicy":"ClusterFirst","enableServiceLinks":true,"restartPolicy":"Always","securityContext":{},"terminationGracePeriodSeconds":30},"status":{}}} {"type":"MODIFIED","object":{"apiVersion":"v1","kind":"Pod","metadata":{"creationTimestamp":null,"name":"foo","namespace":"test","resourceVersion":"11"},"spec":{"containers":null,"dnsPolicy":"ClusterFirst","enableServiceLinks":true,"restartPolicy":"Always","securityContext":{},"terminationGracePeriodSeconds":30},"status":{}}}
{"type":"DELETED","object":{"apiVersion":"v1","kind":"Pod","metadata":{"name":"foo","namespace":"test","resourceVersion":"12"},"spec":{"containers":null,"dnsPolicy":"ClusterFirst","enableServiceLinks":true,"restartPolicy":"Always","securityContext":{},"terminationGracePeriodSeconds":30},"status":{}}} {"type":"DELETED","object":{"apiVersion":"v1","kind":"Pod","metadata":{"creationTimestamp":null,"name":"foo","namespace":"test","resourceVersion":"12"},"spec":{"containers":null,"dnsPolicy":"ClusterFirst","enableServiceLinks":true,"restartPolicy":"Always","securityContext":{},"terminationGracePeriodSeconds":30},"status":{}}}
`, `,
}, },
{ {
@ -2521,6 +2379,7 @@ DELETED test pod/foo 0/0 0 <unknown> <none>
apiVersion: v1 apiVersion: v1
kind: Pod kind: Pod
metadata: metadata:
creationTimestamp: null
name: bar name: bar
namespace: test namespace: test
resourceVersion: "9" resourceVersion: "9"
@ -2538,6 +2397,7 @@ object:
apiVersion: v1 apiVersion: v1
kind: Pod kind: Pod
metadata: metadata:
creationTimestamp: null
name: foo name: foo
namespace: test namespace: test
resourceVersion: "10" resourceVersion: "10"
@ -2555,6 +2415,7 @@ object:
apiVersion: v1 apiVersion: v1
kind: Pod kind: Pod
metadata: metadata:
creationTimestamp: null
name: foo name: foo
namespace: test namespace: test
resourceVersion: "11" resourceVersion: "11"
@ -2572,6 +2433,7 @@ object:
apiVersion: v1 apiVersion: v1
kind: Pod kind: Pod
metadata: metadata:
creationTimestamp: null
name: foo name: foo
namespace: test namespace: test
resourceVersion: "12" resourceVersion: "12"

View File

@ -32,6 +32,9 @@ import (
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/cli-runtime/pkg/printers" "k8s.io/cli-runtime/pkg/printers"
"k8s.io/client-go/util/jsonpath" "k8s.io/client-go/util/jsonpath"
"k8s.io/utils/integer"
"github.com/fvbommel/sortorder"
) )
// SortingPrinter sorts list types before delegating to another printer. // SortingPrinter sorts list types before delegating to another printer.
@ -178,7 +181,7 @@ func isLess(i, j reflect.Value) (bool, error) {
case reflect.Float32, reflect.Float64: case reflect.Float32, reflect.Float64:
return i.Float() < j.Float(), nil return i.Float() < j.Float(), nil
case reflect.String: case reflect.String:
return i.String() < j.String(), nil return sortorder.NaturalLess(i.String(), j.String()), nil
case reflect.Pointer: case reflect.Pointer:
return isLess(i.Elem(), j.Elem()) return isLess(i.Elem(), j.Elem())
case reflect.Struct: case reflect.Struct:
@ -203,7 +206,7 @@ func isLess(i, j reflect.Value) (bool, error) {
return true, nil return true, nil
case reflect.Array, reflect.Slice: case reflect.Array, reflect.Slice:
// note: the length of i and j may be different // note: the length of i and j may be different
for idx := 0; idx < min(i.Len(), j.Len()); idx++ { for idx := 0; idx < integer.IntMin(i.Len(), j.Len()); idx++ {
less, err := isLess(i.Index(idx), j.Index(idx)) less, err := isLess(i.Index(idx), j.Index(idx))
if err != nil || !less { if err != nil || !less {
return less, err return less, err
@ -272,11 +275,11 @@ func isLess(i, j reflect.Value) (bool, error) {
// check if it's a Quantity // check if it's a Quantity
itypeQuantity, err := resource.ParseQuantity(itype) itypeQuantity, err := resource.ParseQuantity(itype)
if err != nil { if err != nil {
return itype < jtype, nil return sortorder.NaturalLess(itype, jtype), nil
} }
jtypeQuantity, err := resource.ParseQuantity(jtype) jtypeQuantity, err := resource.ParseQuantity(jtype)
if err != nil { if err != nil {
return itype < jtype, nil return sortorder.NaturalLess(itype, jtype), nil
} }
// Both strings are quantity // Both strings are quantity
return itypeQuantity.Cmp(jtypeQuantity) < 0, nil return itypeQuantity.Cmp(jtypeQuantity) < 0, nil

View File

@ -31,7 +31,6 @@ import (
metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/scheme"
"k8s.io/utils/ptr"
) )
func toUnstructuredOrDie(data []byte) *unstructured.Unstructured { func toUnstructuredOrDie(data []byte) *unstructured.Unstructured {
@ -104,6 +103,8 @@ func createUnstructuredPodResource(t *testing.T, memReq, memLimit, cpuReq, cpuLi
} }
func TestSortingPrinter(t *testing.T) { func TestSortingPrinter(t *testing.T) {
intPtr := func(val int32) *int32 { return &val }
a := &corev1.Pod{ a := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: "a", Name: "a",
@ -271,17 +272,17 @@ func TestSortingPrinter(t *testing.T) {
Items: []corev1.ReplicationController{ Items: []corev1.ReplicationController{
{ {
Spec: corev1.ReplicationControllerSpec{ Spec: corev1.ReplicationControllerSpec{
Replicas: ptr.To[int32](5), Replicas: intPtr(5),
}, },
}, },
{ {
Spec: corev1.ReplicationControllerSpec{ Spec: corev1.ReplicationControllerSpec{
Replicas: ptr.To[int32](1), Replicas: intPtr(1),
}, },
}, },
{ {
Spec: corev1.ReplicationControllerSpec{ Spec: corev1.ReplicationControllerSpec{
Replicas: ptr.To[int32](9), Replicas: intPtr(9),
}, },
}, },
}, },
@ -290,17 +291,17 @@ func TestSortingPrinter(t *testing.T) {
Items: []corev1.ReplicationController{ Items: []corev1.ReplicationController{
{ {
Spec: corev1.ReplicationControllerSpec{ Spec: corev1.ReplicationControllerSpec{
Replicas: ptr.To[int32](1), Replicas: intPtr(1),
}, },
}, },
{ {
Spec: corev1.ReplicationControllerSpec{ Spec: corev1.ReplicationControllerSpec{
Replicas: ptr.To[int32](5), Replicas: intPtr(5),
}, },
}, },
{ {
Spec: corev1.ReplicationControllerSpec{ Spec: corev1.ReplicationControllerSpec{
Replicas: ptr.To[int32](9), Replicas: intPtr(9),
}, },
}, },
}, },

View File

@ -21,8 +21,8 @@ import (
"reflect" "reflect"
"strings" "strings"
jsonpatch "github.com/evanphx/json-patch"
"github.com/spf13/cobra" "github.com/spf13/cobra"
jsonpatch "gopkg.in/evanphx/json-patch.v4"
"k8s.io/klog/v2" "k8s.io/klog/v2"
"k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/api/meta"
@ -38,7 +38,6 @@ import (
"k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/genericiooptions"
"k8s.io/cli-runtime/pkg/printers" "k8s.io/cli-runtime/pkg/printers"
"k8s.io/cli-runtime/pkg/resource" "k8s.io/cli-runtime/pkg/resource"
"k8s.io/client-go/tools/clientcmd"
cmdutil "k8s.io/kubectl/pkg/cmd/util" cmdutil "k8s.io/kubectl/pkg/cmd/util"
"k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/scheme"
"k8s.io/kubectl/pkg/util/completion" "k8s.io/kubectl/pkg/util/completion"
@ -205,7 +204,7 @@ func (o *LabelOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []st
} }
o.namespace, o.enforceNamespace, err = f.ToRawKubeConfigLoader().Namespace() o.namespace, o.enforceNamespace, err = f.ToRawKubeConfigLoader().Namespace()
if err != nil && !(o.local && clientcmd.IsEmptyConfig(err)) { if err != nil {
return err return err
} }
o.builder = f.NewBuilder() o.builder = f.NewBuilder()

View File

@ -29,7 +29,6 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericclioptions"
@ -41,7 +40,6 @@ import (
"k8s.io/kubectl/pkg/util" "k8s.io/kubectl/pkg/util"
"k8s.io/kubectl/pkg/util/completion" "k8s.io/kubectl/pkg/util/completion"
"k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/i18n"
"k8s.io/kubectl/pkg/util/interrupt"
"k8s.io/kubectl/pkg/util/templates" "k8s.io/kubectl/pkg/util/templates"
) )
@ -58,33 +56,15 @@ var (
# Return snapshot logs from pod nginx with only one container # Return snapshot logs from pod nginx with only one container
kubectl logs nginx kubectl logs nginx
# Return snapshot logs from pod nginx, prefixing each line with the source pod and container name
kubectl logs nginx --prefix
# Return snapshot logs from pod nginx, limiting output to 500 bytes
kubectl logs nginx --limit-bytes=500
# Return snapshot logs from pod nginx, waiting up to 20 seconds for it to start running.
kubectl logs nginx --pod-running-timeout=20s
# Return snapshot logs from pod nginx with multi containers # Return snapshot logs from pod nginx with multi containers
kubectl logs nginx --all-containers=true kubectl logs nginx --all-containers=true
# Return snapshot logs from all pods in the deployment nginx
kubectl logs deployment/nginx --all-pods=true
# Return snapshot logs from all containers in pods defined by label app=nginx # Return snapshot logs from all containers in pods defined by label app=nginx
kubectl logs -l app=nginx --all-containers=true kubectl logs -l app=nginx --all-containers=true
# Return snapshot logs from all pods defined by label app=nginx, limiting concurrent log requests to 10 pods
kubectl logs -l app=nginx --max-log-requests=10
# Return snapshot of previous terminated ruby container logs from pod web-1 # Return snapshot of previous terminated ruby container logs from pod web-1
kubectl logs -p -c ruby web-1 kubectl logs -p -c ruby web-1
# Begin streaming the logs from pod nginx, continuing even if errors occur
kubectl logs nginx -f --ignore-errors=true
# Begin streaming the logs of the ruby container in pod web-1 # Begin streaming the logs of the ruby container in pod web-1
kubectl logs -f -c ruby web-1 kubectl logs -f -c ruby web-1
@ -96,9 +76,6 @@ var (
# Show all logs from pod nginx written in the last hour # Show all logs from pod nginx written in the last hour
kubectl logs --since=1h nginx kubectl logs --since=1h nginx
# Show all logs with timestamps from pod nginx starting from August 30, 2024, at 06:00:00 UTC
kubectl logs nginx --since-time=2024-08-30T06:00:00Z --timestamps=true
# Show logs from a kubelet with an expired serving certificate # Show logs from a kubelet with an expired serving certificate
kubectl logs --insecure-skip-tls-verify-backend nginx kubectl logs --insecure-skip-tls-verify-backend nginx
@ -121,11 +98,10 @@ type LogsOptions struct {
Namespace string Namespace string
ResourceArg string ResourceArg string
AllContainers bool AllContainers bool
AllPods bool
Options runtime.Object Options runtime.Object
Resources []string Resources []string
ConsumeRequestFn func(context.Context, rest.ResponseWrapper, io.Writer) error ConsumeRequestFn func(rest.ResponseWrapper, io.Writer) error
// PodLogOptions // PodLogOptions
SinceTime string SinceTime string
@ -145,11 +121,10 @@ type LogsOptions struct {
MaxFollowConcurrency int MaxFollowConcurrency int
Prefix bool Prefix bool
Object runtime.Object Object runtime.Object
GetPodTimeout time.Duration GetPodTimeout time.Duration
RESTClientGetter genericclioptions.RESTClientGetter RESTClientGetter genericclioptions.RESTClientGetter
LogsForObject polymorphichelpers.LogsForObjectFunc LogsForObject polymorphichelpers.LogsForObjectFunc
AllPodLogsForObject polymorphichelpers.AllPodLogsForObjectFunc
genericiooptions.IOStreams genericiooptions.IOStreams
@ -158,9 +133,10 @@ type LogsOptions struct {
containerNameFromRefSpecRegexp *regexp.Regexp containerNameFromRefSpecRegexp *regexp.Regexp
} }
func NewLogsOptions(streams genericiooptions.IOStreams) *LogsOptions { func NewLogsOptions(streams genericiooptions.IOStreams, allContainers bool) *LogsOptions {
return &LogsOptions{ return &LogsOptions{
IOStreams: streams, IOStreams: streams,
AllContainers: allContainers,
Tail: -1, Tail: -1,
MaxFollowConcurrency: 5, MaxFollowConcurrency: 5,
@ -170,7 +146,7 @@ func NewLogsOptions(streams genericiooptions.IOStreams) *LogsOptions {
// NewCmdLogs creates a new pod logs command // NewCmdLogs creates a new pod logs command
func NewCmdLogs(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { func NewCmdLogs(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command {
o := NewLogsOptions(streams) o := NewLogsOptions(streams, false)
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: logsUsageStr, Use: logsUsageStr,
@ -190,7 +166,6 @@ func NewCmdLogs(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Co
} }
func (o *LogsOptions) AddFlags(cmd *cobra.Command) { func (o *LogsOptions) AddFlags(cmd *cobra.Command) {
cmd.Flags().BoolVar(&o.AllPods, "all-pods", o.AllPods, "Get logs from all pod(s). Sets prefix to true.")
cmd.Flags().BoolVar(&o.AllContainers, "all-containers", o.AllContainers, "Get all containers' logs in the pod(s).") cmd.Flags().BoolVar(&o.AllContainers, "all-containers", o.AllContainers, "Get all containers' logs in the pod(s).")
cmd.Flags().BoolVarP(&o.Follow, "follow", "f", o.Follow, "Specify if the logs should be streamed.") cmd.Flags().BoolVarP(&o.Follow, "follow", "f", o.Follow, "Specify if the logs should be streamed.")
cmd.Flags().BoolVar(&o.Timestamps, "timestamps", o.Timestamps, "Include timestamps on each line in the log output") cmd.Flags().BoolVar(&o.Timestamps, "timestamps", o.Timestamps, "Include timestamps on each line in the log output")
@ -267,11 +242,6 @@ func (o *LogsOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []str
default: default:
return cmdutil.UsageErrorf(cmd, "%s", logsUsageErrStr) return cmdutil.UsageErrorf(cmd, "%s", logsUsageErrStr)
} }
if o.AllPods {
o.Prefix = true
}
var err error var err error
o.Namespace, _, err = f.ToRawKubeConfigLoader().Namespace() o.Namespace, _, err = f.ToRawKubeConfigLoader().Namespace()
if err != nil { if err != nil {
@ -292,7 +262,6 @@ func (o *LogsOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []str
o.RESTClientGetter = f o.RESTClientGetter = f
o.LogsForObject = polymorphichelpers.LogsForObjectFn o.LogsForObject = polymorphichelpers.LogsForObjectFn
o.AllPodLogsForObject = polymorphichelpers.AllPodLogsForObjectFn
if o.Object == nil { if o.Object == nil {
builder := f.NewBuilder(). builder := f.NewBuilder().
@ -307,9 +276,6 @@ func (o *LogsOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []str
} }
infos, err := builder.Do().Infos() infos, err := builder.Do().Infos()
if err != nil { if err != nil {
if apierrors.IsNotFound(err) {
err = fmt.Errorf("error from server (NotFound): %w in namespace %q", err, o.Namespace)
}
return err return err
} }
if o.Selector == "" && len(infos) != 1 { if o.Selector == "" && len(infos) != 1 {
@ -358,13 +324,7 @@ func (o LogsOptions) Validate() error {
// RunLogs retrieves a pod log // RunLogs retrieves a pod log
func (o LogsOptions) RunLogs() error { func (o LogsOptions) RunLogs() error {
var requests map[corev1.ObjectReference]rest.ResponseWrapper requests, err := o.LogsForObject(o.RESTClientGetter, o.Object, o.Options, o.GetPodTimeout, o.AllContainers)
var err error
if o.AllPods {
requests, err = o.AllPodLogsForObject(o.RESTClientGetter, o.Object, o.Options, o.GetPodTimeout, o.AllContainers)
} else {
requests, err = o.LogsForObject(o.RESTClientGetter, o.Object, o.Options, o.GetPodTimeout, o.AllContainers)
}
if err != nil { if err != nil {
return err return err
} }
@ -376,21 +336,14 @@ func (o LogsOptions) RunLogs() error {
len(requests), o.MaxFollowConcurrency, len(requests), o.MaxFollowConcurrency,
) )
} }
return o.parallelConsumeRequest(requests)
} }
ctx, cancel := context.WithCancel(context.Background()) return o.sequentialConsumeRequest(requests)
defer cancel()
intr := interrupt.New(nil, cancel)
return intr.Run(func() error {
if o.Follow && len(requests) > 1 {
return o.parallelConsumeRequest(ctx, requests)
}
return o.sequentialConsumeRequest(ctx, requests)
})
} }
func (o LogsOptions) parallelConsumeRequest(ctx context.Context, requests map[corev1.ObjectReference]rest.ResponseWrapper) error { func (o LogsOptions) parallelConsumeRequest(requests map[corev1.ObjectReference]rest.ResponseWrapper) error {
reader, writer := io.Pipe() reader, writer := io.Pipe()
wg := &sync.WaitGroup{} wg := &sync.WaitGroup{}
wg.Add(len(requests)) wg.Add(len(requests))
@ -398,7 +351,7 @@ func (o LogsOptions) parallelConsumeRequest(ctx context.Context, requests map[co
go func(objRef corev1.ObjectReference, request rest.ResponseWrapper) { go func(objRef corev1.ObjectReference, request rest.ResponseWrapper) {
defer wg.Done() defer wg.Done()
out := o.addPrefixIfNeeded(objRef, writer) out := o.addPrefixIfNeeded(objRef, writer)
if err := o.ConsumeRequestFn(ctx, request, out); err != nil { if err := o.ConsumeRequestFn(request, out); err != nil {
if !o.IgnoreLogErrors { if !o.IgnoreLogErrors {
writer.CloseWithError(err) writer.CloseWithError(err)
@ -421,10 +374,10 @@ func (o LogsOptions) parallelConsumeRequest(ctx context.Context, requests map[co
return err return err
} }
func (o LogsOptions) sequentialConsumeRequest(ctx context.Context, requests map[corev1.ObjectReference]rest.ResponseWrapper) error { func (o LogsOptions) sequentialConsumeRequest(requests map[corev1.ObjectReference]rest.ResponseWrapper) error {
for objRef, request := range requests { for objRef, request := range requests {
out := o.addPrefixIfNeeded(objRef, o.Out) out := o.addPrefixIfNeeded(objRef, o.Out)
if err := o.ConsumeRequestFn(ctx, request, out); err != nil { if err := o.ConsumeRequestFn(request, out); err != nil {
if !o.IgnoreLogErrors { if !o.IgnoreLogErrors {
return err return err
} }
@ -465,8 +418,8 @@ func (o LogsOptions) addPrefixIfNeeded(ref corev1.ObjectReference, writer io.Wri
// A successful read returns err == nil, not err == io.EOF. // A successful read returns err == nil, not err == io.EOF.
// Because the function is defined to read from request until io.EOF, it does // Because the function is defined to read from request until io.EOF, it does
// not treat an io.EOF as an error to be reported. // not treat an io.EOF as an error to be reported.
func DefaultConsumeRequest(ctx context.Context, request rest.ResponseWrapper, out io.Writer) error { func DefaultConsumeRequest(request rest.ResponseWrapper, out io.Writer) error {
readCloser, err := request.Stream(ctx) readCloser, err := request.Stream(context.TODO())
if err != nil { if err != nil {
return err return err
} }

View File

@ -29,12 +29,9 @@ import (
"testing/iotest" "testing/iotest"
"time" "time"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/genericiooptions"
restclient "k8s.io/client-go/rest" restclient "k8s.io/client-go/rest"
@ -63,7 +60,7 @@ func TestLog(t *testing.T) {
}, },
} }
o := NewLogsOptions(streams) o := NewLogsOptions(streams, false)
o.LogsForObject = mock.mockLogsForObject o.LogsForObject = mock.mockLogsForObject
o.ConsumeRequestFn = mock.mockConsumeRequest o.ConsumeRequestFn = mock.mockConsumeRequest
@ -84,7 +81,7 @@ func TestLog(t *testing.T) {
}, },
} }
o := NewLogsOptions(streams) o := NewLogsOptions(streams, false)
o.LogsForObject = mock.mockLogsForObject o.LogsForObject = mock.mockLogsForObject
o.ConsumeRequestFn = mock.mockConsumeRequest o.ConsumeRequestFn = mock.mockConsumeRequest
o.Prefix = true o.Prefix = true
@ -93,35 +90,6 @@ func TestLog(t *testing.T) {
}, },
expectedOutSubstrings: []string{"[pod/test-pod/test-container] test log content\n"}, expectedOutSubstrings: []string{"[pod/test-pod/test-container] test log content\n"},
}, },
{
name: "stateful set logs with all pods",
opts: func(streams genericiooptions.IOStreams) *LogsOptions {
mock := &logTestMock{
logsForObjectRequests: map[corev1.ObjectReference]restclient.ResponseWrapper{
{
Kind: "Pod",
Name: "test-sts-0",
FieldPath: "spec.containers{test-container}",
}: &responseWrapperMock{data: strings.NewReader("test log content for pod test-sts-0\n")},
{
Kind: "Pod",
Name: "test-sts-1",
FieldPath: "spec.containers{test-container}",
}: &responseWrapperMock{data: strings.NewReader("test log content for pod test-sts-1\n")},
},
}
o := NewLogsOptions(streams)
o.LogsForObject = mock.mockLogsForObject
o.ConsumeRequestFn = mock.mockConsumeRequest
o.Prefix = true
return o
},
expectedOutSubstrings: []string{
"[pod/test-sts-0/test-container] test log content for pod test-sts-0\n",
"[pod/test-sts-1/test-container] test log content for pod test-sts-1\n",
},
},
{ {
name: "pod logs with prefix: init container", name: "pod logs with prefix: init container",
opts: func(streams genericiooptions.IOStreams) *LogsOptions { opts: func(streams genericiooptions.IOStreams) *LogsOptions {
@ -135,7 +103,7 @@ func TestLog(t *testing.T) {
}, },
} }
o := NewLogsOptions(streams) o := NewLogsOptions(streams, false)
o.LogsForObject = mock.mockLogsForObject o.LogsForObject = mock.mockLogsForObject
o.ConsumeRequestFn = mock.mockConsumeRequest o.ConsumeRequestFn = mock.mockConsumeRequest
o.Prefix = true o.Prefix = true
@ -157,7 +125,7 @@ func TestLog(t *testing.T) {
}, },
} }
o := NewLogsOptions(streams) o := NewLogsOptions(streams, false)
o.LogsForObject = mock.mockLogsForObject o.LogsForObject = mock.mockLogsForObject
o.ConsumeRequestFn = mock.mockConsumeRequest o.ConsumeRequestFn = mock.mockConsumeRequest
o.Prefix = true o.Prefix = true
@ -189,7 +157,7 @@ func TestLog(t *testing.T) {
}, },
} }
o := NewLogsOptions(streams) o := NewLogsOptions(streams, false)
o.LogsForObject = mock.mockLogsForObject o.LogsForObject = mock.mockLogsForObject
o.ConsumeRequestFn = mock.mockConsumeRequest o.ConsumeRequestFn = mock.mockConsumeRequest
return o return o
@ -226,7 +194,7 @@ func TestLog(t *testing.T) {
} }
wg.Add(3) wg.Add(3)
o := NewLogsOptions(streams) o := NewLogsOptions(streams, false)
o.LogsForObject = mock.mockLogsForObject o.LogsForObject = mock.mockLogsForObject
o.ConsumeRequestFn = mock.mockConsumeRequest o.ConsumeRequestFn = mock.mockConsumeRequest
o.Follow = true o.Follow = true
@ -264,7 +232,7 @@ func TestLog(t *testing.T) {
} }
wg.Add(3) wg.Add(3)
o := NewLogsOptions(streams) o := NewLogsOptions(streams, false)
o.LogsForObject = mock.mockLogsForObject o.LogsForObject = mock.mockLogsForObject
o.ConsumeRequestFn = mock.mockConsumeRequest o.ConsumeRequestFn = mock.mockConsumeRequest
o.MaxFollowConcurrency = 2 o.MaxFollowConcurrency = 2
@ -276,7 +244,7 @@ func TestLog(t *testing.T) {
{ {
name: "fail if LogsForObject fails", name: "fail if LogsForObject fails",
opts: func(streams genericiooptions.IOStreams) *LogsOptions { opts: func(streams genericiooptions.IOStreams) *LogsOptions {
o := NewLogsOptions(streams) o := NewLogsOptions(streams, false)
o.LogsForObject = func(restClientGetter genericclioptions.RESTClientGetter, object, options runtime.Object, timeout time.Duration, allContainers bool) (map[corev1.ObjectReference]restclient.ResponseWrapper, error) { o.LogsForObject = func(restClientGetter genericclioptions.RESTClientGetter, object, options runtime.Object, timeout time.Duration, allContainers bool) (map[corev1.ObjectReference]restclient.ResponseWrapper, error) {
return nil, errors.New("Error from the LogsForObject") return nil, errors.New("Error from the LogsForObject")
} }
@ -302,9 +270,9 @@ func TestLog(t *testing.T) {
}, },
} }
o := NewLogsOptions(streams) o := NewLogsOptions(streams, false)
o.LogsForObject = mock.mockLogsForObject o.LogsForObject = mock.mockLogsForObject
o.ConsumeRequestFn = func(ctx context.Context, req restclient.ResponseWrapper, out io.Writer) error { o.ConsumeRequestFn = func(req restclient.ResponseWrapper, out io.Writer) error {
return errors.New("Error from the ConsumeRequestFn") return errors.New("Error from the ConsumeRequestFn")
} }
return o return o
@ -337,7 +305,7 @@ func TestLog(t *testing.T) {
} }
wg.Add(3) wg.Add(3)
o := NewLogsOptions(streams) o := NewLogsOptions(streams, false)
o.LogsForObject = mock.mockLogsForObject o.LogsForObject = mock.mockLogsForObject
o.ConsumeRequestFn = mock.mockConsumeRequest o.ConsumeRequestFn = mock.mockConsumeRequest
o.Follow = true o.Follow = true
@ -376,9 +344,9 @@ func TestLog(t *testing.T) {
} }
wg.Add(3) wg.Add(3)
o := NewLogsOptions(streams) o := NewLogsOptions(streams, false)
o.LogsForObject = mock.mockLogsForObject o.LogsForObject = mock.mockLogsForObject
o.ConsumeRequestFn = func(ctx context.Context, req restclient.ResponseWrapper, out io.Writer) error { o.ConsumeRequestFn = func(req restclient.ResponseWrapper, out io.Writer) error {
return errors.New("Error from the ConsumeRequestFn") return errors.New("Error from the ConsumeRequestFn")
} }
o.Follow = true o.Follow = true
@ -399,9 +367,9 @@ func TestLog(t *testing.T) {
}, },
} }
o := NewLogsOptions(streams) o := NewLogsOptions(streams, false)
o.LogsForObject = mock.mockLogsForObject o.LogsForObject = mock.mockLogsForObject
o.ConsumeRequestFn = func(ctx context.Context, req restclient.ResponseWrapper, out io.Writer) error { o.ConsumeRequestFn = func(req restclient.ResponseWrapper, out io.Writer) error {
return errors.New("Error from the ConsumeRequestFn") return errors.New("Error from the ConsumeRequestFn")
} }
o.Follow = true o.Follow = true
@ -432,7 +400,7 @@ func TestLog(t *testing.T) {
}, },
} }
o := NewLogsOptions(streams) o := NewLogsOptions(streams, false)
o.LogsForObject = mock.mockLogsForObject o.LogsForObject = mock.mockLogsForObject
o.ConsumeRequestFn = mock.mockConsumeRequest o.ConsumeRequestFn = mock.mockConsumeRequest
o.IgnoreLogErrors = true o.IgnoreLogErrors = true
@ -462,7 +430,7 @@ func TestLog(t *testing.T) {
}, },
} }
o := NewLogsOptions(streams) o := NewLogsOptions(streams, false)
o.LogsForObject = mock.mockLogsForObject o.LogsForObject = mock.mockLogsForObject
o.ConsumeRequestFn = mock.mockConsumeRequest o.ConsumeRequestFn = mock.mockConsumeRequest
return o return o
@ -492,7 +460,7 @@ func TestLog(t *testing.T) {
}, },
} }
o := NewLogsOptions(streams) o := NewLogsOptions(streams, false)
o.LogsForObject = mock.mockLogsForObject o.LogsForObject = mock.mockLogsForObject
o.ConsumeRequestFn = mock.mockConsumeRequest o.ConsumeRequestFn = mock.mockConsumeRequest
o.IgnoreLogErrors = true o.IgnoreLogErrors = true
@ -523,7 +491,7 @@ func TestLog(t *testing.T) {
}, },
} }
o := NewLogsOptions(streams) o := NewLogsOptions(streams, false)
o.LogsForObject = mock.mockLogsForObject o.LogsForObject = mock.mockLogsForObject
o.ConsumeRequestFn = mock.mockConsumeRequest o.ConsumeRequestFn = mock.mockConsumeRequest
o.Follow = true o.Follow = true
@ -594,7 +562,7 @@ func TestValidateLogOptions(t *testing.T) {
{ {
name: "since & since-time", name: "since & since-time",
opts: func(streams genericiooptions.IOStreams) *LogsOptions { opts: func(streams genericiooptions.IOStreams) *LogsOptions {
o := NewLogsOptions(streams) o := NewLogsOptions(streams, false)
o.SinceSeconds = time.Hour o.SinceSeconds = time.Hour
o.SinceTime = "2006-01-02T15:04:05Z" o.SinceTime = "2006-01-02T15:04:05Z"
@ -612,7 +580,7 @@ func TestValidateLogOptions(t *testing.T) {
{ {
name: "negative since-time", name: "negative since-time",
opts: func(streams genericiooptions.IOStreams) *LogsOptions { opts: func(streams genericiooptions.IOStreams) *LogsOptions {
o := NewLogsOptions(streams) o := NewLogsOptions(streams, false)
o.SinceSeconds = -1 * time.Second o.SinceSeconds = -1 * time.Second
var err error var err error
@ -629,7 +597,7 @@ func TestValidateLogOptions(t *testing.T) {
{ {
name: "negative limit-bytes", name: "negative limit-bytes",
opts: func(streams genericiooptions.IOStreams) *LogsOptions { opts: func(streams genericiooptions.IOStreams) *LogsOptions {
o := NewLogsOptions(streams) o := NewLogsOptions(streams, false)
o.LimitBytes = -100 o.LimitBytes = -100
var err error var err error
@ -646,7 +614,7 @@ func TestValidateLogOptions(t *testing.T) {
{ {
name: "negative tail", name: "negative tail",
opts: func(streams genericiooptions.IOStreams) *LogsOptions { opts: func(streams genericiooptions.IOStreams) *LogsOptions {
o := NewLogsOptions(streams) o := NewLogsOptions(streams, false)
o.Tail = -100 o.Tail = -100
var err error var err error
@ -663,8 +631,7 @@ func TestValidateLogOptions(t *testing.T) {
{ {
name: "container name combined with --all-containers", name: "container name combined with --all-containers",
opts: func(streams genericiooptions.IOStreams) *LogsOptions { opts: func(streams genericiooptions.IOStreams) *LogsOptions {
o := NewLogsOptions(streams) o := NewLogsOptions(streams, true)
o.AllContainers = true
o.Container = "my-container" o.Container = "my-container"
var err error var err error
@ -681,7 +648,7 @@ func TestValidateLogOptions(t *testing.T) {
{ {
name: "container name combined with second argument", name: "container name combined with second argument",
opts: func(streams genericiooptions.IOStreams) *LogsOptions { opts: func(streams genericiooptions.IOStreams) *LogsOptions {
o := NewLogsOptions(streams) o := NewLogsOptions(streams, false)
o.Container = "my-container" o.Container = "my-container"
o.ContainerNameSpecified = true o.ContainerNameSpecified = true
@ -728,7 +695,7 @@ func TestLogComplete(t *testing.T) {
name: "One args case", name: "One args case",
args: []string{"foo"}, args: []string{"foo"},
opts: func(streams genericiooptions.IOStreams) *LogsOptions { opts: func(streams genericiooptions.IOStreams) *LogsOptions {
o := NewLogsOptions(streams) o := NewLogsOptions(streams, false)
o.Selector = "foo" o.Selector = "foo"
return o return o
}, },
@ -808,7 +775,7 @@ func TestDefaultConsumeRequest(t *testing.T) {
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
err := DefaultConsumeRequest(context.TODO(), test.request, buf) err := DefaultConsumeRequest(test.request, buf)
if err != nil && !strings.Contains(err.Error(), test.expectedErr) { if err != nil && !strings.Contains(err.Error(), test.expectedErr) {
t.Errorf("%s: expected to find:\n\t%s\nfound:\n\t%s\n", test.name, test.expectedErr, err.Error()) t.Errorf("%s: expected to find:\n\t%s\nfound:\n\t%s\n", test.name, test.expectedErr, err.Error())
@ -847,7 +814,7 @@ func TestNoResourceFoundMessage(t *testing.T) {
streams, _, buf, errbuf := genericiooptions.NewTestIOStreams() streams, _, buf, errbuf := genericiooptions.NewTestIOStreams()
cmd := NewCmdLogs(tf, streams) cmd := NewCmdLogs(tf, streams)
o := NewLogsOptions(streams) o := NewLogsOptions(streams, false)
o.Selector = "foo" o.Selector = "foo"
err := o.Complete(tf, cmd, []string{}) err := o.Complete(tf, cmd, []string{})
@ -866,48 +833,6 @@ func TestNoResourceFoundMessage(t *testing.T) {
} }
} }
func TestNoPodInNamespaceFoundMessage(t *testing.T) {
namespace, podName := "test", "bar"
tf := cmdtesting.NewTestFactory().WithNamespace(namespace)
defer tf.Cleanup()
ns := scheme.Codecs.WithoutConversion()
codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
errStatus := apierrors.NewNotFound(schema.GroupResource{Resource: "pods"}, podName).Status()
tf.UnstructuredClient = &fake.RESTClient{
NegotiatedSerializer: ns,
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
switch req.URL.Path {
case fmt.Sprintf("/namespaces/%s/pods/%s", namespace, podName):
fallthrough
case fmt.Sprintf("/namespaces/%s/pods", namespace):
fallthrough
case fmt.Sprintf("/api/v1/namespaces/%s", namespace):
return &http.Response{StatusCode: http.StatusNotFound, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &errStatus)}, nil
default:
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
return nil, nil
}
}),
}
streams, _, _, _ := genericiooptions.NewTestIOStreams()
cmd := NewCmdLogs(tf, streams)
o := NewLogsOptions(streams)
err := o.Complete(tf, cmd, []string{podName})
if err == nil {
t.Fatal("Expected NotFound error, got nil")
}
expected := fmt.Sprintf("error from server (NotFound): pods %q not found in namespace %q", podName, namespace)
if e, a := expected, err.Error(); e != a {
t.Errorf("expected to find:\n\t%s\nfound:\n\t%s\n", e, a)
}
}
type responseWrapperMock struct { type responseWrapperMock struct {
data io.Reader data io.Reader
err error err error
@ -932,8 +857,8 @@ type logTestMock struct {
wg *sync.WaitGroup wg *sync.WaitGroup
} }
func (l *logTestMock) mockConsumeRequest(ctx context.Context, request restclient.ResponseWrapper, out io.Writer) error { func (l *logTestMock) mockConsumeRequest(request restclient.ResponseWrapper, out io.Writer) error {
readCloser, err := request.Stream(ctx) readCloser, err := request.Stream(context.Background())
if err != nil { if err != nil {
return err return err
} }
@ -950,13 +875,6 @@ func (l *logTestMock) mockConsumeRequest(ctx context.Context, request restclient
func (l *logTestMock) mockLogsForObject(restClientGetter genericclioptions.RESTClientGetter, object, options runtime.Object, timeout time.Duration, allContainers bool) (map[corev1.ObjectReference]restclient.ResponseWrapper, error) { func (l *logTestMock) mockLogsForObject(restClientGetter genericclioptions.RESTClientGetter, object, options runtime.Object, timeout time.Duration, allContainers bool) (map[corev1.ObjectReference]restclient.ResponseWrapper, error) {
switch object.(type) { switch object.(type) {
case *appsv1.Deployment:
_, ok := options.(*corev1.PodLogOptions)
if !ok {
return nil, errors.New("provided options object is not a PodLogOptions")
}
return l.logsForObjectRequests, nil
case *corev1.Pod: case *corev1.Pod:
_, ok := options.(*corev1.PodLogOptions) _, ok := options.(*corev1.PodLogOptions)
if !ok { if !ok {

View File

@ -22,8 +22,9 @@ import (
"reflect" "reflect"
"strings" "strings"
jsonpatch "github.com/evanphx/json-patch"
"github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
jsonpatch "gopkg.in/evanphx/json-patch.v4"
"k8s.io/klog/v2" "k8s.io/klog/v2"
apierrors "k8s.io/apimachinery/pkg/api/errors" apierrors "k8s.io/apimachinery/pkg/api/errors"
@ -39,11 +40,11 @@ import (
"k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/genericiooptions"
"k8s.io/cli-runtime/pkg/printers" "k8s.io/cli-runtime/pkg/printers"
"k8s.io/cli-runtime/pkg/resource" "k8s.io/cli-runtime/pkg/resource"
"k8s.io/client-go/tools/clientcmd"
cmdutil "k8s.io/kubectl/pkg/cmd/util" cmdutil "k8s.io/kubectl/pkg/cmd/util"
"k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/scheme"
"k8s.io/kubectl/pkg/util/completion" "k8s.io/kubectl/pkg/util/completion"
"k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/i18n"
"k8s.io/kubectl/pkg/util/slice"
"k8s.io/kubectl/pkg/util/templates" "k8s.io/kubectl/pkg/util/templates"
) )
@ -105,6 +106,8 @@ var (
kubectl patch deployment nginx-deployment --subresource='scale' --type='merge' -p '{"spec":{"replicas":2}}'`)) kubectl patch deployment nginx-deployment --subresource='scale' --type='merge' -p '{"spec":{"replicas":2}}'`))
) )
var supportedSubresources = []string{"status", "scale"}
func NewPatchOptions(ioStreams genericiooptions.IOStreams) *PatchOptions { func NewPatchOptions(ioStreams genericiooptions.IOStreams) *PatchOptions {
return &PatchOptions{ return &PatchOptions{
RecordFlags: genericclioptions.NewRecordFlags(), RecordFlags: genericclioptions.NewRecordFlags(),
@ -136,12 +139,12 @@ func NewCmdPatch(f cmdutil.Factory, ioStreams genericiooptions.IOStreams) *cobra
cmd.Flags().StringVarP(&o.Patch, "patch", "p", "", "The patch to be applied to the resource JSON file.") cmd.Flags().StringVarP(&o.Patch, "patch", "p", "", "The patch to be applied to the resource JSON file.")
cmd.Flags().StringVar(&o.PatchFile, "patch-file", "", "A file containing a patch to be applied to the resource.") cmd.Flags().StringVar(&o.PatchFile, "patch-file", "", "A file containing a patch to be applied to the resource.")
cmd.Flags().StringVar(&o.PatchType, "type", "strategic", fmt.Sprintf("The type of patch being provided; one of %v", sets.List(sets.KeySet(patchTypes)))) cmd.Flags().StringVar(&o.PatchType, "type", "strategic", fmt.Sprintf("The type of patch being provided; one of %v", sets.StringKeySet(patchTypes).List()))
cmdutil.AddDryRunFlag(cmd) cmdutil.AddDryRunFlag(cmd)
cmdutil.AddFilenameOptionFlags(cmd, &o.FilenameOptions, "identifying the resource to update") cmdutil.AddFilenameOptionFlags(cmd, &o.FilenameOptions, "identifying the resource to update")
cmd.Flags().BoolVar(&o.Local, "local", o.Local, "If true, patch will operate on the content of the file, not the server-side resource.") cmd.Flags().BoolVar(&o.Local, "local", o.Local, "If true, patch will operate on the content of the file, not the server-side resource.")
cmdutil.AddFieldManagerFlagVar(cmd, &o.fieldManager, "kubectl-patch") cmdutil.AddFieldManagerFlagVar(cmd, &o.fieldManager, "kubectl-patch")
cmdutil.AddSubresourceFlags(cmd, &o.Subresource, "If specified, patch will operate on the subresource of the requested object.") cmdutil.AddSubresourceFlags(cmd, &o.Subresource, "If specified, patch will operate on the subresource of the requested object.", supportedSubresources...)
return cmd return cmd
} }
@ -168,7 +171,7 @@ func (o *PatchOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []st
} }
o.namespace, o.enforceNamespace, err = f.ToRawKubeConfigLoader().Namespace() o.namespace, o.enforceNamespace, err = f.ToRawKubeConfigLoader().Namespace()
if err != nil && !(o.Local && clientcmd.IsEmptyConfig(err)) { if err != nil {
return err return err
} }
o.args = args o.args = args
@ -193,9 +196,12 @@ func (o *PatchOptions) Validate() error {
} }
if len(o.PatchType) != 0 { if len(o.PatchType) != 0 {
if _, ok := patchTypes[strings.ToLower(o.PatchType)]; !ok { if _, ok := patchTypes[strings.ToLower(o.PatchType)]; !ok {
return fmt.Errorf("--type must be one of %v, not %q", sets.List(sets.KeySet(patchTypes)), o.PatchType) return fmt.Errorf("--type must be one of %v, not %q", sets.StringKeySet(patchTypes).List(), o.PatchType)
} }
} }
if len(o.Subresource) > 0 && !slice.ContainsString(supportedSubresources, o.Subresource, nil) {
return fmt.Errorf("invalid subresource value: %q. Must be one of %v", o.Subresource, supportedSubresources)
}
return nil return nil
} }
@ -259,7 +265,7 @@ func (o *PatchOptions) RunPatch() error {
patchedObj, err := helper.Patch(namespace, name, patchType, patchBytes, nil) patchedObj, err := helper.Patch(namespace, name, patchType, patchBytes, nil)
if err != nil { if err != nil {
if apierrors.IsUnsupportedMediaType(err) { if apierrors.IsUnsupportedMediaType(err) {
return fmt.Errorf("%s is not supported by %s: %w", patchType, mapping.GroupVersionKind, err) return errors.Wrap(err, fmt.Sprintf("%s is not supported by %s", patchType, mapping.GroupVersionKind))
} }
return err return err
} }

View File

@ -40,19 +40,15 @@ var (
Plugins provide extended functionality that is not part of the major command-line distribution. Plugins provide extended functionality that is not part of the major command-line distribution.
Please refer to the documentation and examples for more information about how write your own plugins. Please refer to the documentation and examples for more information about how write your own plugins.
The easiest way to discover and install plugins is via the kubernetes sub-project krew: [krew.sigs.k8s.io]. The easiest way to discover and install plugins is via the kubernetes sub-project krew.
To install krew, visit https://krew.sigs.k8s.io/docs/user-guide/setup/install`)) To install krew, visit [krew.sigs.k8s.io](https://krew.sigs.k8s.io/docs/user-guide/setup/install/)`))
pluginExample = templates.Examples(i18n.T(` pluginExample = templates.Examples(i18n.T(`
# List all available plugins # List all available plugins
kubectl plugin list kubectl plugin list`))
# List only binary names of available plugins without paths
kubectl plugin list --name-only`))
pluginListLong = templates.LongDesc(i18n.T(` pluginListLong = templates.LongDesc(i18n.T(`
List all available plugin files on a user's PATH. List all available plugin files on a user's PATH.
To see plugins binary names without the full path use --name-only flag.
Available plugin files are those that are: Available plugin files are those that are:
- executable - executable
@ -69,7 +65,6 @@ func NewCmdPlugin(streams genericiooptions.IOStreams) *cobra.Command {
DisableFlagsInUseLine: true, DisableFlagsInUseLine: true,
Short: i18n.T("Provides utilities for interacting with plugins"), Short: i18n.T("Provides utilities for interacting with plugins"),
Long: pluginLong, Long: pluginLong,
Example: pluginExample,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
cmdutil.DefaultSubCommandRun(streams.ErrOut)(cmd, args) cmdutil.DefaultSubCommandRun(streams.ErrOut)(cmd, args)
}, },

View File

@ -27,24 +27,14 @@ import (
"strings" "strings"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/kubectl/pkg/util/i18n"
"k8s.io/kubectl/pkg/util/templates"
)
func GetPluginCommandGroup(kubectl *cobra.Command) templates.CommandGroup { "k8s.io/cli-runtime/pkg/genericiooptions"
// Find root level )
return templates.CommandGroup{
Message: i18n.T("Subcommands provided by plugins:"),
Commands: registerPluginCommands(kubectl, false),
}
}
// SetupPluginCompletion adds a Cobra command to the command tree for each // SetupPluginCompletion adds a Cobra command to the command tree for each
// plugin. This is only done when performing shell completion that relate // plugin. This is only done when performing shell completion that relate
// to plugins. // to plugins.
func SetupPluginCompletion(cmd *cobra.Command, args []string) { func SetupPluginCompletion(cmd *cobra.Command, args []string) {
kubectl := cmd.Root()
if len(args) > 0 { if len(args) > 0 {
if strings.HasPrefix(args[0], "-") { if strings.HasPrefix(args[0], "-") {
// Plugins are not supported if the first argument is a flag, // Plugins are not supported if the first argument is a flag,
@ -55,7 +45,7 @@ func SetupPluginCompletion(cmd *cobra.Command, args []string) {
if len(args) == 1 { if len(args) == 1 {
// We are completing a subcommand at the first level so // We are completing a subcommand at the first level so
// we should include all plugins names. // we should include all plugins names.
registerPluginCommands(kubectl, true) addPluginCommands(cmd)
return return
} }
@ -64,7 +54,7 @@ func SetupPluginCompletion(cmd *cobra.Command, args []string) {
// If we don't it could be a plugin and we'll need to add // If we don't it could be a plugin and we'll need to add
// the plugin commands for completion to work. // the plugin commands for completion to work.
found := false found := false
for _, subCmd := range kubectl.Commands() { for _, subCmd := range cmd.Root().Commands() {
if args[0] == subCmd.Name() { if args[0] == subCmd.Name() {
found = true found = true
break break
@ -80,20 +70,19 @@ func SetupPluginCompletion(cmd *cobra.Command, args []string) {
// to avoid them being included in the completion choices. // to avoid them being included in the completion choices.
// This must be done *before* adding the plugin commands so that // This must be done *before* adding the plugin commands so that
// when creating those plugin commands, the flags don't exist. // when creating those plugin commands, the flags don't exist.
kubectl.ResetFlags() cmd.Root().ResetFlags()
cobra.CompDebugln("Cleared global flags for plugin completion", true) cobra.CompDebugln("Cleared global flags for plugin completion", true)
registerPluginCommands(kubectl, true) addPluginCommands(cmd)
} }
} }
} }
// registerPluginCommand allows adding Cobra command to the command tree or extracting them for usage in // addPluginCommand adds a Cobra command to the command tree
// e.g. the help function or for registering the completion function // for each plugin so that the completion logic knows about the plugins
func registerPluginCommands(kubectl *cobra.Command, list bool) (cmds []*cobra.Command) { func addPluginCommands(cmd *cobra.Command) {
userDefinedCommands := []*cobra.Command{} kubectl := cmd.Root()
streams := genericiooptions.IOStreams{
streams := genericclioptions.IOStreams{
In: &bytes.Buffer{}, In: &bytes.Buffer{},
Out: io.Discard, Out: io.Discard,
ErrOut: io.Discard, ErrOut: io.Discard,
@ -109,18 +98,10 @@ func registerPluginCommands(kubectl *cobra.Command, list bool) (cmds []*cobra.Co
// Plugins are named "kubectl-<name>" or with more - such as // Plugins are named "kubectl-<name>" or with more - such as
// "kubectl-<name>-<subcmd1>..." // "kubectl-<name>-<subcmd1>..."
rawPluginArgs := strings.Split(plugin, "-")[1:] for _, arg := range strings.Split(plugin, "-")[1:] {
pluginArgs := rawPluginArgs[:1]
if list {
pluginArgs = rawPluginArgs
}
// Iterate through all segments, for kubectl-my_plugin-sub_cmd, we will end up with
// two iterations: one for my_plugin and one for sub_cmd.
for _, arg := range pluginArgs {
// Underscores (_) in plugin's filename are replaced with dashes(-) // Underscores (_) in plugin's filename are replaced with dashes(-)
// e.g. foo_bar -> foo-bar // e.g. foo_bar -> foo-bar
args = append(args, strings.ReplaceAll(arg, "_", "-")) args = append(args, strings.Replace(arg, "_", "-", -1))
} }
// In order to avoid that the same plugin command is added more than once, // In order to avoid that the same plugin command is added more than once,
@ -136,24 +117,17 @@ func registerPluginCommands(kubectl *cobra.Command, list bool) (cmds []*cobra.Co
// Add a description that will be shown with completion choices. // Add a description that will be shown with completion choices.
// Make each one different by including the plugin name to avoid // Make each one different by including the plugin name to avoid
// all plugins being grouped in a single line during completion for zsh. // all plugins being grouped in a single line during completion for zsh.
Short: fmt.Sprintf(i18n.T("The command %s is a plugin installed by the user"), remainingArg), Short: fmt.Sprintf("The command %s is a plugin installed by the user", remainingArg),
DisableFlagParsing: true, DisableFlagParsing: true,
// Allow plugins to provide their own completion choices // Allow plugins to provide their own completion choices
ValidArgsFunction: pluginCompletion, ValidArgsFunction: pluginCompletion,
// A Run is required for it to be a valid command // A Run is required for it to be a valid command
Run: func(cmd *cobra.Command, args []string) {}, Run: func(cmd *cobra.Command, args []string) {},
} }
// Add the plugin command to the list of user defined commands parentCmd.AddCommand(cmd)
userDefinedCommands = append(userDefinedCommands, cmd) parentCmd = cmd
if list {
parentCmd.AddCommand(cmd)
parentCmd = cmd
}
} }
} }
return userDefinedCommands
} }
// pluginCompletion deals with shell completion beyond the plugin name, it allows to complete // pluginCompletion deals with shell completion beyond the plugin name, it allows to complete
@ -187,7 +161,7 @@ func registerPluginCommands(kubectl *cobra.Command, list bool) (cmds []*cobra.Co
// executable must have executable permissions set on it and must be on $PATH. // executable must have executable permissions set on it and must be on $PATH.
func pluginCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { func pluginCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
// Recreate the plugin name from the commandPath // Recreate the plugin name from the commandPath
pluginName := strings.ReplaceAll(strings.ReplaceAll(cmd.CommandPath(), "-", "_"), " ", "-") pluginName := strings.Replace(strings.Replace(cmd.CommandPath(), "-", "_", -1), " ", "-", -1)
path, found := lookupCompletionExec(pluginName) path, found := lookupCompletionExec(pluginName)
if !found { if !found {

Some files were not shown because too many files have changed in this diff Show More