From 4236ecd483f925dd6257bef87e7f058715356a46 Mon Sep 17 00:00:00 2001 From: Liang Deng <283304489@qq.com> Date: Wed, 20 Sep 2023 19:27:14 +0800 Subject: [PATCH] feat: add command-line tool admiralctl and implement the migrate subcommand Signed-off-by: Liang Deng <283304489@qq.com> --- cmd/admiralctl/admiralctl.go | 31 + go.mod | 27 +- go.sum | 64 +- pkg/admiralctl/admiralctl.go | 87 +++ pkg/admiralctl/migrate/migrate.go | 694 ++++++++++++++++++ pkg/admiralctl/options/global.go | 34 + pkg/admiralctl/options/options.go | 56 ++ pkg/admiralctl/util/constants.go | 25 + pkg/admiralctl/util/factory.go | 47 ++ pkg/admiralctl/util/names/names.go | 53 ++ pkg/admiralctl/util/names/names_test.go | 52 ++ pkg/admiralctl/util/restmapper/restmapper.go | 158 ++++ .../util/restmapper/restmapper_test.go | 213 ++++++ 13 files changed, 1535 insertions(+), 6 deletions(-) create mode 100755 cmd/admiralctl/admiralctl.go create mode 100644 pkg/admiralctl/admiralctl.go create mode 100644 pkg/admiralctl/migrate/migrate.go create mode 100644 pkg/admiralctl/options/global.go create mode 100644 pkg/admiralctl/options/options.go create mode 100644 pkg/admiralctl/util/constants.go create mode 100644 pkg/admiralctl/util/factory.go create mode 100644 pkg/admiralctl/util/names/names.go create mode 100644 pkg/admiralctl/util/names/names_test.go create mode 100644 pkg/admiralctl/util/restmapper/restmapper.go create mode 100644 pkg/admiralctl/util/restmapper/restmapper_test.go diff --git a/cmd/admiralctl/admiralctl.go b/cmd/admiralctl/admiralctl.go new file mode 100755 index 000000000..e843919b6 --- /dev/null +++ b/cmd/admiralctl/admiralctl.go @@ -0,0 +1,31 @@ +/* +Copyright 2023 The KubeAdmiral Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "k8s.io/component-base/cli" + "k8s.io/kubectl/pkg/cmd/util" + + "github.com/kubewharf/kubeadmiral/pkg/admiralctl" +) + +func main() { + cmd := admiralctl.NewAdmiralCtlCommand("admiralctl", "admiralctl") + if err := cli.RunNoErrOutput(cmd); err != nil { + util.CheckErr(err) + } +} diff --git a/go.mod b/go.mod index 275047a86..1bbfbc920 100644 --- a/go.mod +++ b/go.mod @@ -8,10 +8,12 @@ require ( github.com/evanphx/json-patch/v5 v5.6.0 github.com/go-logr/logr v1.2.4 github.com/google/go-cmp v0.5.9 + github.com/kr/pretty v0.2.0 github.com/onsi/ginkgo/v2 v2.11.0 github.com/onsi/gomega v1.27.8 github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.14.0 + github.com/spf13/cobra v1.7.0 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.8.4 golang.org/x/sync v0.2.0 @@ -19,21 +21,28 @@ require ( k8s.io/api v0.26.6 k8s.io/apiextensions-apiserver v0.26.6 k8s.io/apimachinery v0.26.6 + k8s.io/cli-runtime v0.26.6 k8s.io/client-go v0.26.6 k8s.io/component-base v0.26.6 k8s.io/klog/v2 v2.90.1 + k8s.io/kubectl v0.26.6 k8s.io/utils v0.0.0-20221128185143-99ec85e7a448 sigs.k8s.io/controller-runtime v0.14.1 sigs.k8s.io/kind v0.17.0 ) require ( + github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect github.com/BurntSushi/toml v1.0.0 // indirect + github.com/MakeNowJust/heredoc v1.0.0 // indirect github.com/alessio/shellescape v1.4.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver/v4 v4.0.0 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect + github.com/chai2010/gettext-go v1.0.2 // indirect github.com/emicklei/go-restful/v3 v3.9.0 // indirect + github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect + github.com/go-errors/errors v1.0.1 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonreference v0.20.0 // indirect github.com/go-openapi/swag v0.19.14 // indirect @@ -41,27 +50,39 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.3 // indirect + github.com/google/btree v1.0.1 // indirect github.com/google/gnostic v0.5.7-v3refs // indirect github.com/google/gofuzz v1.1.0 // indirect github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect github.com/google/safetext v0.0.0-20220905092116-b49f7bc46da2 // indirect + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/uuid v1.3.0 // indirect + github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect github.com/imdario/mergo v0.3.6 // indirect - github.com/inconshreveable/mousetrap v1.0.1 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-isatty v0.0.14 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.2 // indirect + github.com/mitchellh/go-wordwrap v1.0.0 // indirect + github.com/moby/spdystream v0.2.0 // indirect + github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pelletier/go-toml v1.9.4 // indirect + github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.3.0 // indirect github.com/prometheus/common v0.37.0 // indirect github.com/prometheus/procfs v0.8.0 // indirect - github.com/spf13/cobra v1.6.1 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/xlab/treeprint v1.1.0 // indirect + go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect golang.org/x/net v0.10.0 // indirect golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b // indirect golang.org/x/sys v0.9.0 // indirect @@ -75,6 +96,8 @@ require ( gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 // indirect sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect + sigs.k8s.io/kustomize/api v0.12.1 // indirect + sigs.k8s.io/kustomize/kyaml v0.13.9 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect sigs.k8s.io/yaml v1.3.0 // indirect ) diff --git a/go.sum b/go.sum index 45f6a5024..f3ffbee5b 100644 --- a/go.sum +++ b/go.sum @@ -31,10 +31,14 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +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/BurntSushi/toml v1.0.0 h1:dtDWrepsVPfW9H/4y7dDgFc2MBUSeJhlaDtK13CxFlU= github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= +github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= @@ -42,6 +46,7 @@ github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRF github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0= github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -52,6 +57,8 @@ github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk= +github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -60,10 +67,13 @@ github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGX github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 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.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= +github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153 h1:yUdfgN0XgIJw7foRItutHYUIhlcKzcSf5vDpdhQAKTc= github.com/emicklei/go-restful/v3 v3.9.0 h1:XwGDlfxEnQZzuopoqxwSEllNcCOM9DhhFyhFIIGKwxE= github.com/emicklei/go-restful/v3 v3.9.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -74,7 +84,11 @@ github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCv github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww= github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= +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/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= +github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -137,6 +151,8 @@ github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +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 v0.5.7-v3refs h1:FhTMOKj2VhjpouxvWJAV1TL304uMlb9zcDqkl6cEI54= github.com/google/gnostic v0.5.7-v3refs/go.mod h1:73MKFl6jIHelAJNaBGFzt3SPtZULs9dYrGFt8OiIsHQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -167,10 +183,15 @@ github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLe github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/safetext v0.0.0-20220905092116-b49f7bc46da2 h1:SJ+NtwL6QaZ21U+IrK7d0gGgpjGGvd2kz+FzTHVzdqI= github.com/google/safetext v0.0.0-20220905092116-b49f7bc46da2/go.mod h1:Tv1PlzqC9t8wNnpPdctvtSUOPUUg4SHeE6vR1Ir2hmg= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= @@ -178,8 +199,8 @@ github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1: 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.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= -github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 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= @@ -199,11 +220,14 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 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.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= @@ -214,6 +238,12 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.2 h1:hAHbPm5IJGijwng3PWk09JkG9WeqChjprR5s9bBZ+OM= github.com/matttproud/golang_protobuf_extensions v1.0.2/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4= +github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= +github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8= +github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= +github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae h1:O4SWKdcHVCvYqyDV+9CJA1fcDN2L11Bule0iFy3YlAI= +github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -221,6 +251,8 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 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/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= @@ -233,6 +265,8 @@ github.com/onsi/gomega v1.27.8 h1:gegWiwZjBsf2DgiSbf5hpokZ98JVDMcWkUiigk6/KXc= github.com/onsi/gomega v1.27.8/go.mod h1:2J8vzI/s+2shY9XHRApDkdgPo1TKT7P2u6fXeJKFnNQ= github.com/pelletier/go-toml v1.9.4 h1:tjENF6MfZAg8e4ZmZTeWaWiT2vXtsoO6+iuOjFhECwM= github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -266,25 +300,32 @@ github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1 github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo= github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g= -github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= -github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= +github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= +github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 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.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/xlab/treeprint v1.1.0 h1:G/1DjNkPpfZCFt9CSh6b5/nY4VimlbHF3Rh4obvtzDk= +github.com/xlab/treeprint v1.1.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -294,6 +335,8 @@ go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 h1:+FNtrFTmVw0YZGpBGX56XDee331t6JAXeK2bcyhLOOc= +go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5/go.mod h1:nmDLcffg48OtT/PSW0Hg7FvpRQsQh5OSqIylirxKC7o= go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= @@ -401,6 +444,7 @@ golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191002063906-3421d5a6bb1c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -425,6 +469,7 @@ golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -459,6 +504,7 @@ golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBn golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -599,6 +645,8 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= +gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= @@ -612,6 +660,8 @@ k8s.io/apiextensions-apiserver v0.26.6 h1:BrrWb5gQlWuwvqGJs1xMV1Qtr+xQS6ri6A1QBT k8s.io/apiextensions-apiserver v0.26.6/go.mod h1:T6zbudRhmwN0sxg9lD51co/3Ah3JuCduz0nbtxyRXrk= k8s.io/apimachinery v0.26.6 h1:OT04J9US8G+AqfqvcJZZ8s3WUQkWbc3t6ePPWieDN6I= k8s.io/apimachinery v0.26.6/go.mod h1:qYzLkrQ9lhrZRh0jNKo2cfvf/R1/kQONnSiyB7NUJU0= +k8s.io/cli-runtime v0.26.6 h1:535Ult64Zp1583D1mewL1LdThwAcuYc4MwM69l3p2VU= +k8s.io/cli-runtime v0.26.6/go.mod h1:m/5RF2eCPVh0fhdBXgSJIyTiZuPOG0BOvkIVZtc1NqE= k8s.io/client-go v0.26.6 h1:CtC0wOxkAwjYyG2URGzdEKo0nLILopSDYn5AmzOkdi4= k8s.io/client-go v0.26.6/go.mod h1:HDjbQGY7XzFYFUWOPAfAsIYhvFXyc9l6Ne0pO0bOQ7o= k8s.io/component-base v0.26.6 h1:/Tm16Z8l/ruLFcw1XbFKTRSuxD6gQULQxxYgmar8PI0= @@ -620,6 +670,8 @@ k8s.io/klog/v2 v2.90.1 h1:m4bYOKall2MmOiRaR1J+We67Do7vm9KiQVlT96lnHUw= k8s.io/klog/v2 v2.90.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 h1:+70TFaan3hfJzs+7VK2o+OGxg8HsuBr/5f6tVAjDu6E= k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280/go.mod h1:+Axhij7bCpeqhklhUTe3xmOn6bWxolyZEeyaFpjGtl4= +k8s.io/kubectl v0.26.6 h1:8w/13HZ+kb7tKFoZ55Ci96L3RvjTFFuLPBEYYSOP0rA= +k8s.io/kubectl v0.26.6/go.mod h1:q9wFF+QoE0tOQnJvPbxCXnjKuot/0v/eFXNBjheEsgY= k8s.io/utils v0.0.0-20221128185143-99ec85e7a448 h1:KTgPnR10d5zhztWptI952TNtt/4u5h3IzDXkdIMuo2Y= k8s.io/utils v0.0.0-20221128185143-99ec85e7a448/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= @@ -631,6 +683,10 @@ sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 h1:iXTIw73aPyC+oRdyqqvVJuloN sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= sigs.k8s.io/kind v0.17.0 h1:CScmGz/wX66puA06Gj8OZb76Wmk7JIjgWf5JDvY7msM= sigs.k8s.io/kind v0.17.0/go.mod h1:Qqp8AiwOlMZmJWs37Hgs31xcbiYXjtXlRBSftcnZXQk= +sigs.k8s.io/kustomize/api v0.12.1 h1:7YM7gW3kYBwtKvoY216ZzY+8hM+lV53LUayghNRJ0vM= +sigs.k8s.io/kustomize/api v0.12.1/go.mod h1:y3JUhimkZkR6sbLNwfJHxvo1TCLwuwm14sCYnkH6S1s= +sigs.k8s.io/kustomize/kyaml v0.13.9 h1:Qz53EAaFFANyNgyOEJbT/yoIHygK40/ZcvU3rgry2Tk= +sigs.k8s.io/kustomize/kyaml v0.13.9/go.mod h1:QsRbD0/KcU+wdk0/L0fIp2KLnohkVzs6fQ85/nOXac4= sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= diff --git a/pkg/admiralctl/admiralctl.go b/pkg/admiralctl/admiralctl.go new file mode 100644 index 000000000..8b5090ec5 --- /dev/null +++ b/pkg/admiralctl/admiralctl.go @@ -0,0 +1,87 @@ +/* +Copyright 2023 The KubeAdmiral Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package admiralctl + +import ( + "flag" + "fmt" + "os" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "k8s.io/cli-runtime/pkg/genericclioptions" + apiserverflag "k8s.io/component-base/cli/flag" + "k8s.io/klog/v2" + "k8s.io/kubectl/pkg/util/templates" + + "github.com/kubewharf/kubeadmiral/pkg/admiralctl/migrate" + "github.com/kubewharf/kubeadmiral/pkg/admiralctl/options" + "github.com/kubewharf/kubeadmiral/pkg/admiralctl/util" +) + +var ( + rootCmdShort = "%s controls a Kubernetes Cluster Federation." + rootCmdLong = "%s controls a Kubernetes Cluster Federation." +) + +// NewAdmiralCtlCommand creates the `admiralctl` command. +func NewAdmiralCtlCommand(cmdUse, parentCommand string) *cobra.Command { + // Parent command to which all sub-commands are added. + rootCmd := &cobra.Command{ + Use: cmdUse, + Short: fmt.Sprintf(rootCmdShort, parentCommand), + Long: fmt.Sprintf(rootCmdLong, parentCommand), + RunE: runHelp, + } + + // Init log flags + klog.InitFlags(flag.CommandLine) + + // Add the command line flags from other dependencies (e.g., klog), but do not + // warn if they contain underscores. + pflag.CommandLine.SetNormalizeFunc(apiserverflag.WordSepNormalizeFunc) + pflag.CommandLine.AddGoFlagSet(flag.CommandLine) + rootCmd.PersistentFlags().AddFlagSet(pflag.CommandLine) + + // From this point and forward we get warnings on flags that contain "_" separators + rootCmd.SetGlobalNormalizationFunc(apiserverflag.WarnWordSepNormalizeFunc) + + // Prevent klog errors about logging before parsing. + _ = flag.CommandLine.Parse(nil) + f := util.NewFactory(options.DefaultConfigFlags) + ioStreams := genericclioptions.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr} + groups := templates.CommandGroups{ + { + Message: "Advanced Commands:", + Commands: []*cobra.Command{ + migrate.NewCmdMigrate(f, parentCommand), + }, + }, + } + groups.Add(rootCmd) + + filters := []string{"options"} + + rootCmd.AddCommand(options.NewCmdOptions(parentCommand, ioStreams.Out)) + templates.ActsAsRootCommand(rootCmd, filters, groups...) + + return rootCmd +} + +func runHelp(cmd *cobra.Command, _ []string) error { + return cmd.Help() +} diff --git a/pkg/admiralctl/migrate/migrate.go b/pkg/admiralctl/migrate/migrate.go new file mode 100644 index 000000000..be53d73a7 --- /dev/null +++ b/pkg/admiralctl/migrate/migrate.go @@ -0,0 +1,694 @@ +/* +Copyright 2023 The KubeAdmiral Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package migrate + +import ( + "context" + "encoding/json" + "fmt" + "os" + "strings" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/cli-runtime/pkg/printers" + "k8s.io/cli-runtime/pkg/resource" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/rest" + cmdutil "k8s.io/kubectl/pkg/cmd/util" + "k8s.io/kubectl/pkg/util/templates" + + "github.com/kubewharf/kubeadmiral/pkg/admiralctl/options" + "github.com/kubewharf/kubeadmiral/pkg/admiralctl/util" + "github.com/kubewharf/kubeadmiral/pkg/admiralctl/util/names" + "github.com/kubewharf/kubeadmiral/pkg/admiralctl/util/restmapper" + fedcorev1a1 "github.com/kubewharf/kubeadmiral/pkg/apis/core/v1alpha1" + fedclient "github.com/kubewharf/kubeadmiral/pkg/client/clientset/versioned" + "github.com/kubewharf/kubeadmiral/pkg/client/generic/scheme" + "github.com/kubewharf/kubeadmiral/pkg/controllers/common" + "github.com/kubewharf/kubeadmiral/pkg/controllers/scheduler" + "github.com/kubewharf/kubeadmiral/pkg/util/adoption" +) + +var ( + migrateLong = templates.LongDesc(` + migrate resources from legacy clusters to Kubeadmiral control plane. + Requires the cluster has been joined or registered. + + If the resource already exists in Kubeadmiral control plane, + please edit PropagationPolicy and OverridePolicy to propagate it. + `) + + migrateExample = templates.Examples(` + # migrate deployment(default/nginx) from cluster1 to Kubeadmiral + %[1]s migrate deployment nginx -n default -C cluster1 --cluster-kubeconfig= + + # migrate deployment(default/nginx) with gvk from cluster1 to Kubeadmiral + %[1]s migrate deployment.v1.apps nginx -n default -C cluster1 --cluster-kubeconfig= + + # Dumps the artifacts but does not deploy them to Kubeadmiral, same as 'dry run' + %[1]s migrate deployment nginx -n default -C cluster1 --cluster-kubeconfig= -o yaml|json + + # migrate secret(default/default-token) from cluster1 to Kubeadmiral + %[1]s migrate secret default-token -n default -C cluster1 --cluster-kubeconfig= + + # Support to use '--cluster-kubeconfig' and '--cluster-context' to specify the configuration of member cluster + %[1]s migrate deployment nginx -n default -C cluster1 --cluster-kubeconfig= --cluster-context=`) +) + +// NewCmdMigrate defines the `migrate` command that migrate resources from legacy clusters +func NewCmdMigrate(f util.Factory, parentCommand string) *cobra.Command { + opts := CommandmigrateOption{} + opts.JSONYamlPrintFlags = genericclioptions.NewJSONYamlPrintFlags() + + cmd := &cobra.Command{ + Use: "migrate -n -C " + + "--cluster-kubeconfig=", + Short: "migrate resources from legacy clusters to Kubeadmiral control plane", + Long: migrateLong, + Example: fmt.Sprintf(migrateExample, parentCommand), + SilenceUsage: true, + DisableFlagsInUseLine: true, + RunE: func(cmd *cobra.Command, args []string) error { + if err := opts.Preflight(f, args); err != nil { + return err + } + if err := opts.Run(f, args); err != nil { + return err + } + return nil + }, + Annotations: map[string]string{ + util.TagCommandGroup: util.GroupAdvancedCommands, + }, + } + + flag := cmd.Flags() + opts.AddFlags(flag) + options.AddKubeConfigFlags(flag) + + return cmd +} + +// CommandmigrateOption holds all command options for migrate +type CommandmigrateOption struct { + // Cluster is the name of legacy cluster + Cluster string + + // Namespace is the namespace of legacy resource + Namespace string + + // ClusterContext is the cluster's context that we are going to join with. + ClusterContext string + + // ClusterKubeConfig is the cluster's kubeconfig path. + ClusterKubeConfig string + + // DryRun tells if run the command in dry-run mode, without making any server requests. + DryRun bool + + // AutoCreatePolicy tells if the migrate command should create the + // PropagationPolicy(or ClusterPropagationPolicy) by default. + // It default to true. + AutoCreatePolicy bool + + // PolicyName is the name of the PropagationPolicy(or ClusterPropagationPolicy), + // It defaults to the promoting resource name with a random hash suffix. + // It will be ignored if AutoCreatePolicy is false. + PolicyName string + + // SchedulingMode determines the mode used by the scheduler when scheduling federated objects. + SchedulingMode string + + resource.FilenameOptions + + JSONYamlPrintFlags *genericclioptions.JSONYamlPrintFlags + OutputFormat string + Printer func(*meta.RESTMapping, *bool, bool, bool) (printers.ResourcePrinterFunc, error) + TemplateFlags *genericclioptions.KubeTemplatePrintFlags + + name string + gvk schema.GroupVersionKind +} + +// AddFlags adds flags to the specified FlagSet. +func (o *CommandmigrateOption) AddFlags(flags *pflag.FlagSet) { + flags.BoolVar(&o.AutoCreatePolicy, "auto-create-policy", true, + "Automatically create a PropagationPolicy for namespace-scoped resources or "+ + "create a ClusterPropagationPolicy for cluster-scoped resources.") + flags.StringVar(&o.PolicyName, "policy-name", "", + "The name of the PropagationPolicy(or ClusterPropagationPolicy) that is automatically created after migration. "+ + "If not specified, the name will be the resource name with a hash suffix that is generated by resource metadata.") + flags.StringVarP(&o.OutputFormat, "output", "o", "", "Output format. One of: json|yaml") + flags.StringVar(&o.SchedulingMode, "scheduling-mode", string(fedcorev1a1.SchedulingModeDuplicate), + "determines the mode used by the scheduler when scheduling federated objects, One of: Duplicate|Divide, default is Duplicate.") + flags.StringVarP(&o.Namespace, "namespace", "n", o.Namespace, "If present, the namespace scope for this CLI request.") + flags.StringVarP(&o.Cluster, "cluster", "C", "", "the name of legacy cluster (eg -C=member1)") + flags.StringVar(&o.ClusterContext, "cluster-context", "", + "Context name of legacy cluster in kubeconfig. Only works when there are multiple contexts in the kubeconfig.") + flags.StringVar(&o.ClusterKubeConfig, "cluster-kubeconfig", "", + "Path of the legacy cluster's kubeconfig.") + flags.BoolVar(&o.DryRun, "dry-run", false, "Run the command in dry-run mode, without making any server requests.") +} + +// Preflight ensures that options are valid and marshals them if necessary +func (o *CommandmigrateOption) Preflight(f util.Factory, args []string) error { + var err error + + if len(args) != 2 { + return fmt.Errorf("incorrect command format, please use correct command format") + } + + o.name = args[1] + + if o.OutputFormat == "yaml" || o.OutputFormat == "json" { + o.Printer = func(mapping *meta.RESTMapping, outputObjects *bool, + withNamespace bool, withKind bool, + ) (printers.ResourcePrinterFunc, error) { + printer, err := o.JSONYamlPrintFlags.ToPrinter(o.OutputFormat) + + if genericclioptions.IsNoCompatiblePrinterError(err) { + return nil, err + } + + printer, err = printers.NewTypeSetter(scheme.Scheme).WrapToPrinter(printer, nil) + if err != nil { + return nil, err + } + + return printer.PrintObj, nil + } + } + + if o.Namespace == "" { + o.Namespace, _, err = f.ToRawKubeConfigLoader().Namespace() + if err != nil { + return fmt.Errorf("failed to get namespace from Factory. error: %w", err) + } + } + + // If '--cluster-context' not specified, take the cluster name as the context. + if len(o.ClusterContext) == 0 { + o.ClusterContext = o.Cluster + } + + if o.Cluster == "" { + return fmt.Errorf("the cluster cannot be empty") + } + + if o.ClusterKubeConfig == "" { + return fmt.Errorf("the clusterKubeConfig cannot be empty") + } + + if o.OutputFormat != "" && o.OutputFormat != "yaml" && o.OutputFormat != "json" { + return fmt.Errorf("output format is only one of json and yaml") + } + + return nil +} + +// Run migrate resources from legacy clusters +func (o *CommandmigrateOption) Run(f util.Factory, args []string) error { + var memberClusterFactory cmdutil.Factory + var err error + + kubeConfigFlags := genericclioptions.NewConfigFlags(true).WithDeprecatedPasswordFlag() + kubeConfigFlags.KubeConfig = &o.ClusterKubeConfig + kubeConfigFlags.Context = &o.ClusterContext + memberClusterFactory = cmdutil.NewFactory(kubeConfigFlags) + + objInfo, err := o.getObjInfo(memberClusterFactory, o.Cluster, args) + if err != nil { + return fmt.Errorf("failed to get resource in cluster(%s). err: %w", o.Cluster, err) + } + + obj := objInfo.Info.Object.(*unstructured.Unstructured) + + o.gvk = obj.GetObjectKind().GroupVersionKind() + + controlPlaneRestConfig, err := f.ToRESTConfig() + if err != nil { + return fmt.Errorf("failed to get control plane rest config. err: %w", err) + } + + mapper, err := restmapper.MapperProvider(controlPlaneRestConfig) + if err != nil { + return fmt.Errorf("failed to create restmapper: %w", err) + } + + gvr, err := restmapper.GetGroupVersionResource(mapper, o.gvk) + if err != nil { + return fmt.Errorf("failed to get gvr from %q: %w", o.gvk, err) + } + + return o.migrate(controlPlaneRestConfig, obj, gvr) +} + +func (o *CommandmigrateOption) migrate(controlPlaneRestConfig *rest.Config, obj *unstructured.Unstructured, + gvr schema.GroupVersionResource, +) error { + if err := preprocessResource(obj); err != nil { + return fmt.Errorf("failed to preprocess resource %q(%s/%s) in control plane: %w", gvr, o.Namespace, o.name, err) + } + + if o.OutputFormat != "" { + // only print the resource template and Policy + err := o.printObjectAndPolicy(obj, gvr) + + return err + } + + if o.DryRun { + return nil + } + + controlPlaneDynamicClient := dynamic.NewForConfigOrDie(controlPlaneRestConfig) + + kubeadmiralClient := fedclient.NewForConfigOrDie(controlPlaneRestConfig) + + if len(obj.GetNamespace()) == 0 { + _, err := controlPlaneDynamicClient.Resource(gvr).Get(context.TODO(), o.name, metav1.GetOptions{}) + if err == nil { + fmt.Printf("Resource %q(%s) already exist in Kubeadmiral control plane, "+ + "you can edit PropagationPolicy and OverridePolicy to propagate it\n", gvr, o.name) + return nil + } + + if !apierrors.IsNotFound(err) { + return fmt.Errorf("failed to get resource %q(%s) in control plane: %w", gvr, o.name, err) + } + + _, err = controlPlaneDynamicClient.Resource(gvr).Create(context.TODO(), obj, metav1.CreateOptions{}) + if err != nil { + return fmt.Errorf("failed to create resource %q(%s) in control plane: %w", gvr, o.name, err) + } + + if o.AutoCreatePolicy { + policyName, err := o.createClusterPropagationPolicy(kubeadmiralClient) + if err != nil { + return err + } + patchData, err := getPatchData(obj, policyName) + if err != nil { + return err + } + _, err = controlPlaneDynamicClient.Resource(gvr).Namespace(o.Namespace). + Patch(context.TODO(), obj.GetName(), types.StrategicMergePatchType, patchData, metav1.PatchOptions{}) + if err != nil { + return fmt.Errorf("failed to update resource %q(%s/%s) in control plane: %w", gvr, obj.GetNamespace(), o.name, err) + } + } + + fmt.Printf("Resource %q(%s) is migrated successfully\n", gvr, o.name) + } else { + _, err := controlPlaneDynamicClient.Resource(gvr).Namespace(o.Namespace).Get(context.TODO(), o.name, metav1.GetOptions{}) + if err == nil { + fmt.Printf("Resource %q(%s/%s) already exist in Kubeadmiral control plane, "+ + "you can edit PropagationPolicy and OverridePolicy to propagate it\n", gvr, o.Namespace, o.name) + return nil + } + + if !apierrors.IsNotFound(err) { + return fmt.Errorf("failed to get resource %q(%s/%s) in control plane: %w", gvr, o.Namespace, o.name, err) + } + + _, err = controlPlaneDynamicClient.Resource(gvr).Namespace(o.Namespace).Create(context.TODO(), obj, metav1.CreateOptions{}) + if err != nil { + return fmt.Errorf("failed to create resource %q(%s/%s) in control plane: %w", gvr, o.Namespace, o.name, err) + } + + if o.AutoCreatePolicy { + policyName, err := o.createPropagationPolicy(kubeadmiralClient) + if err != nil { + return err + } + patchData, err := getPatchData(obj, policyName) + if err != nil { + return err + } + _, err = controlPlaneDynamicClient.Resource(gvr).Namespace(o.Namespace). + Patch(context.TODO(), obj.GetName(), types.StrategicMergePatchType, patchData, metav1.PatchOptions{}) + if err != nil { + return fmt.Errorf("failed to update resource %q(%s/%s) in control plane: %w", gvr, obj.GetNamespace(), o.name, err) + } + } + + fmt.Printf("Resource %q(%s/%s) is migrated successfully\n", gvr, o.Namespace, o.name) + } + + return nil +} + +func getPatchData(obj *unstructured.Unstructured, policyName string) ([]byte, error) { + labels := obj.DeepCopy().GetLabels() + annotations := obj.DeepCopy().GetAnnotations() + if labels == nil { + labels = map[string]string{} + } + if _, exist := labels[scheduler.PropagationPolicyNameLabel]; !exist { + labels[scheduler.PropagationPolicyNameLabel] = policyName + } + if annotations == nil { + annotations = map[string]string{} + } + if _, exist := annotations[adoption.ConflictResolutionAnnotation]; !exist { + annotations[adoption.ConflictResolutionAnnotation] = string(adoption.ConflictResolutionAdopt) + } + patchData := map[string]interface{}{"metadata": map[string]map[string]string{"labels": labels, "annotations": annotations}} + patchDataByte, err := json.Marshal(patchData) + if err != nil { + return nil, fmt.Errorf("failed to marshal patch data: %w", err) + } + return patchDataByte, nil +} + +// Obj cluster info +type Obj struct { + Cluster string + Info *resource.Info +} + +// getObjInfo get obj info in member cluster +func (o *CommandmigrateOption) getObjInfo(f cmdutil.Factory, cluster string, args []string) (*Obj, error) { + r := f.NewBuilder(). + Unstructured(). + NamespaceParam(o.Namespace). + FilenameParam(false, &o.FilenameOptions). + RequestChunksOf(500). + ResourceTypeOrNameArgs(true, args...). + ContinueOnError(). + Latest(). + Flatten(). + Do() + + r.IgnoreErrors(apierrors.IsNotFound) + + infos, err := r.Infos() + if err != nil { + return nil, err + } + + if len(infos) == 0 { + return nil, fmt.Errorf("the %s or %s don't exist in cluster(%s)", args[0], args[1], o.Cluster) + } + + obj := &Obj{ + Cluster: cluster, + Info: infos[0], + } + + return obj, nil +} + +// printObjectAndPolicy print the converted resource +func (o *CommandmigrateOption) printObjectAndPolicy(obj *unstructured.Unstructured, gvr schema.GroupVersionResource) error { + printer, err := o.Printer(nil, nil, false, false) + if err != nil { + return fmt.Errorf("failed to initialize k8s printer. err: %w", err) + } + + if err = printer.PrintObj(obj, os.Stdout); err != nil { + return fmt.Errorf("failed to print the resource template. err: %w", err) + } + if o.AutoCreatePolicy { + var policyName string + if o.PolicyName == "" { + policyName = names.GeneratePolicyName(o.Namespace, o.name, o.gvk.String()) + } else { + policyName = o.PolicyName + } + if len(obj.GetNamespace()) == 0 { + cpp := buildClusterPropagationPolicy(policyName, o.Cluster, o.SchedulingMode) + if err = printer.PrintObj(cpp, os.Stdout); err != nil { + return fmt.Errorf("failed to print the ClusterPropagationPolicy. err: %w", err) + } + } else { + pp := buildPropagationPolicy(policyName, o.Namespace, o.Cluster, o.SchedulingMode) + if err = printer.PrintObj(pp, os.Stdout); err != nil { + return fmt.Errorf("failed to print the PropagationPolicy. err: %w", err) + } + } + } + + return nil +} + +// createPropagationPolicy create PropagationPolicy in Kubeadmiral control plane +func (o *CommandmigrateOption) createPropagationPolicy(kubeadmiralClient *fedclient.Clientset) (string, error) { + var policyName string + if o.PolicyName == "" { + policyName = names.GeneratePolicyName(o.Namespace, o.name, o.gvk.String()) + } else { + policyName = o.PolicyName + } + + _, err := kubeadmiralClient.CoreV1alpha1().PropagationPolicies(o.Namespace).Get(context.TODO(), policyName, metav1.GetOptions{}) + if err != nil && apierrors.IsNotFound(err) { + pp := buildPropagationPolicy(policyName, o.Namespace, o.Cluster, o.SchedulingMode) + _, err = kubeadmiralClient.CoreV1alpha1().PropagationPolicies(o.Namespace).Create(context.TODO(), pp, metav1.CreateOptions{}) + + return policyName, err + } + if err != nil { + return "", fmt.Errorf("failed to get PropagationPolicy(%s/%s) in control plane: %w", o.Namespace, policyName, err) + } + + // PropagationPolicy already exists, not to create it + return "", fmt.Errorf("the PropagationPolicy(%s/%s) already exist, please edit it to propagate resource", o.Namespace, policyName) +} + +// createClusterPropagationPolicy create ClusterPropagationPolicy in Kubeadmiral control plane +func (o *CommandmigrateOption) createClusterPropagationPolicy(kubeadmiralClient *fedclient.Clientset) (string, error) { + var policyName string + if o.PolicyName == "" { + policyName = names.GeneratePolicyName("", o.name, o.gvk.String()) + } else { + policyName = o.PolicyName + } + + _, err := kubeadmiralClient.CoreV1alpha1().ClusterPropagationPolicies().Get(context.TODO(), policyName, metav1.GetOptions{}) + if err != nil && apierrors.IsNotFound(err) { + cpp := buildClusterPropagationPolicy(policyName, o.Cluster, o.SchedulingMode) + _, err = kubeadmiralClient.CoreV1alpha1().ClusterPropagationPolicies().Create(context.TODO(), cpp, metav1.CreateOptions{}) + + return policyName, err + } + if err != nil { + return "", fmt.Errorf("failed to get ClusterPropagationPolicy(%s) in control plane: %w", policyName, err) + } + + // ClusterPropagationPolicy already exists, not to create it + return "", fmt.Errorf("the ClusterPropagationPolicy(%s) already exist, please edit it to propagate resource", policyName) +} + +// preprocessResource delete redundant fields to convert resource as template +func preprocessResource(obj *unstructured.Unstructured) error { + // remove fields that generated by kube-apiserver and no need(or can't) propagate to member clusters. + if err := removeIrrelevantField(obj); err != nil { + return err + } + + return nil +} + +// removeIrrelevantField used to remove fields that generated by kube-apiserver and no need(or can't) propagate to +// member clusters. +func removeIrrelevantField(workload *unstructured.Unstructured, extraHooks ...func(*unstructured.Unstructured)) error { + // populated by the kubernetes. + unstructured.RemoveNestedField(workload.Object, "metadata", "creationTimestamp") + + // populated by the kubernetes. + // The kubernetes will set this fields in case of graceful deletion. This field is read-only and can't propagate to + // member clusters. + unstructured.RemoveNestedField(workload.Object, "metadata", "deletionTimestamp") + + // populated by the kubernetes. + // The kubernetes will set this fields in case of graceful deletion. This field is read-only and can't propagate to + // member clusters. + unstructured.RemoveNestedField(workload.Object, "metadata", "deletionGracePeriodSeconds") + + // populated by the kubernetes. + unstructured.RemoveNestedField(workload.Object, "metadata", "generation") + + // This is mostly for internal housekeeping, and users typically shouldn't need to set or understand this field. + // Remove this field to keep 'Work' clean and tidy. + unstructured.RemoveNestedField(workload.Object, "metadata", "managedFields") + + // populated by the kubernetes. + unstructured.RemoveNestedField(workload.Object, "metadata", "resourceVersion") + + // populated by the kubernetes and has been deprecated by kubernetes. + unstructured.RemoveNestedField(workload.Object, "metadata", "selfLink") + + // populated by the kubernetes. + unstructured.RemoveNestedField(workload.Object, "metadata", "uid") + + unstructured.RemoveNestedField(workload.Object, "metadata", "ownerReferences") + + unstructured.RemoveNestedField(workload.Object, "metadata", "finalizers") + + unstructured.RemoveNestedField(workload.Object, "status") + + if workload.GetKind() == common.ServiceKind { + // In the case spec.clusterIP is set to `None`, means user want a headless service, then it shouldn't be removed. + clusterIP, exist, _ := unstructured.NestedString(workload.Object, "spec", "clusterIP") + if exist && clusterIP != corev1.ClusterIPNone { + unstructured.RemoveNestedField(workload.Object, "spec", "clusterIP") + unstructured.RemoveNestedField(workload.Object, "spec", "clusterIPs") + } + } + + if workload.GetKind() == common.JobKind { + job := &batchv1.Job{} + err := convertToTypedObject(workload, job) + if err != nil { + return err + } + if job.Spec.ManualSelector == nil || !*job.Spec.ManualSelector { + if err = removeGenerateSelectorOfJob(workload); err != nil { + return err + } + } + } + + if workload.GetKind() == common.ServiceAccountKind { + secrets, exist, _ := unstructured.NestedSlice(workload.Object, "secrets") + // If 'secrets' exists in ServiceAccount, remove the automatic generation secrets(e.g. default-token-xxx) + if exist && len(secrets) > 0 { + tokenPrefix := fmt.Sprintf("%s-token-", workload.GetName()) + for idx := 0; idx < len(secrets); idx++ { + if strings.HasPrefix(secrets[idx].(map[string]interface{})["name"].(string), tokenPrefix) { + secrets = append(secrets[:idx], secrets[idx+1:]...) + } + } + _ = unstructured.SetNestedSlice(workload.Object, secrets, "secrets") + } + } + + for i := range extraHooks { + extraHooks[i](workload) + } + return nil +} + +// getLabelValue retrieves the value via 'labelKey' if exist, otherwise returns an empty string. +func getLabelValue(labels map[string]string, labelKey string) string { + if labels == nil { + return "" + } + + return labels[labelKey] +} + +func removeGenerateSelectorOfJob(workload *unstructured.Unstructured) error { + matchLabels, exist, err := unstructured.NestedStringMap(workload.Object, "spec", "selector", "matchLabels") + if err != nil { + return err + } + if exist { + if getLabelValue(matchLabels, "controller-uid") != "" { + delete(matchLabels, "controller-uid") + } + err = unstructured.SetNestedStringMap(workload.Object, matchLabels, "spec", "selector", "matchLabels") + if err != nil { + return err + } + } + + templateLabels, exist, err := unstructured.NestedStringMap(workload.Object, "spec", "template", "metadata", "labels") + if err != nil { + return err + } + if exist { + if getLabelValue(templateLabels, "controller-uid") != "" { + delete(templateLabels, "controller-uid") + } + + if getLabelValue(templateLabels, "job-name") != "" { + delete(templateLabels, "job-name") + } + + err = unstructured.SetNestedStringMap(workload.Object, templateLabels, "spec", "template", "metadata", "labels") + if err != nil { + return err + } + } + return nil +} + +// convertToTypedObject converts an unstructured object to typed. +func convertToTypedObject(in, out interface{}) error { + if in == nil || out == nil { + return fmt.Errorf("convert objects should not be nil") + } + + switch v := in.(type) { + case *unstructured.Unstructured: + return runtime.DefaultUnstructuredConverter.FromUnstructured(v.UnstructuredContent(), out) + case map[string]interface{}: + return runtime.DefaultUnstructuredConverter.FromUnstructured(v, out) + default: + return fmt.Errorf("convert object must be pointer of unstructured or map[string]interface{}") + } +} + +// buildPropagationPolicy build PropagationPolicy according to resource and cluster +func buildPropagationPolicy(policyName, namespace, cluster, schedulingMode string) *fedcorev1a1.PropagationPolicy { + pp := &fedcorev1a1.PropagationPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: policyName, + Namespace: namespace, + }, + Spec: fedcorev1a1.PropagationPolicySpec{ + SchedulingMode: fedcorev1a1.SchedulingMode(schedulingMode), + Placements: []fedcorev1a1.DesiredPlacement{ + { + Cluster: cluster, + }, + }, + }, + } + return pp +} + +// buildClusterPropagationPolicy build ClusterPropagationPolicy according to resource and cluster +func buildClusterPropagationPolicy(policyName, cluster, schedulingMode string) *fedcorev1a1.ClusterPropagationPolicy { + cpp := &fedcorev1a1.ClusterPropagationPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: policyName, + }, + Spec: fedcorev1a1.PropagationPolicySpec{ + SchedulingMode: fedcorev1a1.SchedulingMode(schedulingMode), + Placements: []fedcorev1a1.DesiredPlacement{ + { + Cluster: cluster, + }, + }, + }, + } + return cpp +} diff --git a/pkg/admiralctl/options/global.go b/pkg/admiralctl/options/global.go new file mode 100644 index 000000000..9a4fcb128 --- /dev/null +++ b/pkg/admiralctl/options/global.go @@ -0,0 +1,34 @@ +/* +Copyright 2023 The KubeAdmiral Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package options + +import ( + "github.com/spf13/pflag" + "k8s.io/cli-runtime/pkg/genericclioptions" +) + +// DefaultConfigFlags It composes the set of values necessary for obtaining a REST client config with default values set. +var DefaultConfigFlags = genericclioptions.NewConfigFlags(true). + WithDeprecatedPasswordFlag().WithDiscoveryBurst(300).WithDiscoveryQPS(50.0) + +// AddKubeConfigFlags adds flags to the specified FlagSet. +func AddKubeConfigFlags(flags *pflag.FlagSet) { + flags.StringVar(DefaultConfigFlags.KubeConfig, "kubeconfig", *DefaultConfigFlags.KubeConfig, + "Path to the kubeconfig file to use for CLI requests.") + flags.StringVar(DefaultConfigFlags.Context, "kubeadmiral-context", *DefaultConfigFlags.Context, + "The name of the kubeconfig context to use") +} diff --git a/pkg/admiralctl/options/options.go b/pkg/admiralctl/options/options.go new file mode 100644 index 000000000..503b2c274 --- /dev/null +++ b/pkg/admiralctl/options/options.go @@ -0,0 +1,56 @@ +/* +Copyright 2023 The KubeAdmiral Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package options + +import ( + "fmt" + "io" + + "github.com/spf13/cobra" + "k8s.io/kubectl/pkg/util/templates" +) + +var optionsExample = templates.Examples(` + # Print flags inherited by all commands + %[1]s options`) + +// NewCmdOptions implements the options command +func NewCmdOptions(parentCommand string, out io.Writer) *cobra.Command { + cmd := &cobra.Command{ + Use: "options", + Short: "Print the list of flags inherited by all commands", + Long: "Print the list of flags inherited by all commands", + Example: fmt.Sprintf(optionsExample, parentCommand), + RunE: func(cmd *cobra.Command, args []string) error { + if err := cmd.Usage(); err != nil { + return err + } + return nil + }, + } + + // The `options` command needs write its output to the `out` stream + // (typically stdout). Without calling SetOutput here, the Usage() + // function call will fall back to stderr. + // + // See https://github.com/kubernetes/kubernetes/pull/46394 for details. + cmd.SetOut(out) + cmd.SetErr(out) + + templates.UseOptionsTemplates(cmd) + return cmd +} diff --git a/pkg/admiralctl/util/constants.go b/pkg/admiralctl/util/constants.go new file mode 100644 index 000000000..760b148a2 --- /dev/null +++ b/pkg/admiralctl/util/constants.go @@ -0,0 +1,25 @@ +/* +Copyright 2023 The KubeAdmiral Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +// Define command +const ( + // TagCommandGroup used for tag the group of the command + TagCommandGroup = "commandGroup" + // GroupAdvancedCommands means the command belongs to Group "Advanced Commands" + GroupAdvancedCommands = "Advanced Commands" +) diff --git a/pkg/admiralctl/util/factory.go b/pkg/admiralctl/util/factory.go new file mode 100644 index 000000000..6129cb779 --- /dev/null +++ b/pkg/admiralctl/util/factory.go @@ -0,0 +1,47 @@ +/* +Copyright 2023 The KubeAdmiral Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import ( + "k8s.io/cli-runtime/pkg/genericclioptions" + cmdutil "k8s.io/kubectl/pkg/cmd/util" +) + +type Factory interface { + cmdutil.Factory +} + +var _ Factory = &factoryImpl{} + +// factoryImpl is the implementation of Factory +type factoryImpl struct { + cmdutil.Factory + + // kubeConfigFlags holds all the flags specified by user. + // These flags will be inherited by the member cluster's client. + kubeConfigFlags *genericclioptions.ConfigFlags +} + +// NewFactory returns a new factory +func NewFactory(kubeConfigFlags *genericclioptions.ConfigFlags) Factory { + matchVersionKubeConfigFlags := cmdutil.NewMatchVersionFlags(kubeConfigFlags) + f := &factoryImpl{ + kubeConfigFlags: kubeConfigFlags, + Factory: cmdutil.NewFactory(matchVersionKubeConfigFlags), + } + return f +} diff --git a/pkg/admiralctl/util/names/names.go b/pkg/admiralctl/util/names/names.go new file mode 100644 index 000000000..d9eb9b25f --- /dev/null +++ b/pkg/admiralctl/util/names/names.go @@ -0,0 +1,53 @@ +/* +Copyright 2023 The KubeAdmiral Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package names + +import ( + "fmt" + "hash" + "hash/fnv" + "strings" + + "github.com/kr/pretty" + "k8s.io/apimachinery/pkg/util/rand" +) + +// GeneratePolicyName generates the propagationPolicy name +func GeneratePolicyName(namespace, name, gvk string) string { + hash := fnv.New32a() + deepHashObject(hash, namespace+gvk) + + // The name of resources, like 'Role'/'ClusterRole'/'RoleBinding'/'ClusterRoleBinding', + // may contain symbols(like ':') that are not allowed by CRD resources which require the + // name can be used as a DNS subdomain name. So, we need to replace it. + // These resources may also allow for other characters(like '&','$') that are not allowed + // by CRD resources, we only handle the most common ones now for performance concerns. + // For more information about the DNS subdomain name, please refer to + // https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#dns-subdomain-names. + if strings.Contains(name, ":") { + name = strings.ReplaceAll(name, ":", ".") + } + return strings.ToLower(fmt.Sprintf("%s-%s", name, rand.SafeEncodeString(fmt.Sprint(hash.Sum32())))) +} + +// deepHashObject writes specified object to hash using the pretty library +// which follows pointers and prints actual values of the nested objects +// ensuring the hash does not change when a pointer changes. +func deepHashObject(hasher hash.Hash, objectToWrite interface{}) { + hasher.Reset() + pretty.Fprintf(hasher, "%# v", objectToWrite) +} diff --git a/pkg/admiralctl/util/names/names_test.go b/pkg/admiralctl/util/names/names_test.go new file mode 100644 index 000000000..e95df63dc --- /dev/null +++ b/pkg/admiralctl/util/names/names_test.go @@ -0,0 +1,52 @@ +/* +Copyright 2023 The KubeAdmiral Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package names + +import ( + "testing" +) + +func TestGeneratePolicyName(t *testing.T) { + tests := []struct { + name string + namespace string + resourcename string + gvk string + expected string + }{ + { + name: "generate policy name", + namespace: "ns-foo", + resourcename: "foo", + gvk: "rand", + expected: "foo-b4978784", + }, + { + name: "generate policy name with :", + namespace: "ns-foo", + resourcename: "system:foo", + gvk: "rand", + expected: "system.foo-b4978784", + }, + } + for _, test := range tests { + got := GeneratePolicyName(test.namespace, test.resourcename, test.gvk) + if got != test.expected { + t.Errorf("Test %s failed: expected %v, but got %v", test.name, test.expected, got) + } + } +} diff --git a/pkg/admiralctl/util/restmapper/restmapper.go b/pkg/admiralctl/util/restmapper/restmapper.go new file mode 100644 index 000000000..1b51901a3 --- /dev/null +++ b/pkg/admiralctl/util/restmapper/restmapper.go @@ -0,0 +1,158 @@ +/* +Copyright 2023 The KubeAdmiral Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package restmapper + +import ( + "sync" + + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/discovery" + "k8s.io/client-go/rest" + "k8s.io/client-go/restmapper" +) + +// GetGroupVersionResource is a helper to map GVK(schema.GroupVersionKind) to GVR(schema.GroupVersionResource). +func GetGroupVersionResource(restMapper meta.RESTMapper, gvk schema.GroupVersionKind) (schema.GroupVersionResource, error) { + restMapping, err := restMapper.RESTMapping(gvk.GroupKind(), gvk.Version) + if err != nil { + return schema.GroupVersionResource{}, err + } + return restMapping.Resource, nil +} + +// cachedRESTMapper caches the previous result to accelerate subsequent queries. +type cachedRESTMapper struct { + restMapper meta.RESTMapper + discoveryClient discovery.DiscoveryInterface + gvkToGVR sync.Map + // mu is used to provide thread-safe mapper reloading. + mu sync.RWMutex +} + +func (g *cachedRESTMapper) KindFor(resource schema.GroupVersionResource) (schema.GroupVersionKind, error) { + return g.getMapper().KindFor(resource) +} + +func (g *cachedRESTMapper) KindsFor(resource schema.GroupVersionResource) ([]schema.GroupVersionKind, error) { + return g.getMapper().KindsFor(resource) +} + +func (g *cachedRESTMapper) ResourceFor(input schema.GroupVersionResource) (schema.GroupVersionResource, error) { + return g.getMapper().ResourceFor(input) +} + +func (g *cachedRESTMapper) ResourcesFor(input schema.GroupVersionResource) ([]schema.GroupVersionResource, error) { + return g.getMapper().ResourcesFor(input) +} + +func (g *cachedRESTMapper) RESTMappings(gk schema.GroupKind, versions ...string) ([]*meta.RESTMapping, error) { + return g.getMapper().RESTMappings(gk, versions...) +} + +func (g *cachedRESTMapper) ResourceSingularizer(resource string) (singular string, err error) { + return g.getMapper().ResourceSingularizer(resource) +} + +func (g *cachedRESTMapper) RESTMapping(gk schema.GroupKind, versions ...string) (*meta.RESTMapping, error) { + // in case of multi-versions or no versions, cachedRESTMapper don't know which is the preferred version, + // so just bypass the cache and consult the underlying mapper. + if len(versions) != 1 { + return g.getMapper().RESTMapping(gk, versions...) + } + + gvk := gk.WithVersion(versions[0]) + value, ok := g.gvkToGVR.Load(gvk) + if ok { // hit cache, just return + return value.(*meta.RESTMapping), nil + } + + // consult underlying mapper and then update cache + restMapping, err := g.getMapper().RESTMapping(gk, versions...) + if meta.IsNoMatchError(err) { + // hit here means a resource might be missing from the current rest mapper, + // probably because a new resource(CRD) has been added, we have to reload + // resource and rebuild the rest mapper. + + var groupResources []*restmapper.APIGroupResources + groupResources, err = restmapper.GetAPIGroupResources(g.discoveryClient) + if err != nil { + return nil, err + } + + newMapper := restmapper.NewDiscoveryRESTMapper(groupResources) + restMapping, err = newMapper.RESTMapping(gk, versions...) + if err == nil { + // hit here means after reloading, the new rest mapper can recognize + // the resource, we have to replace the mapper and clear cache. + g.mu.Lock() + g.restMapper = newMapper + g.mu.Unlock() + g.gvkToGVR.Range(func(key, value any) bool { + g.gvkToGVR.Delete(key) + return true + }) + } + } + + if err != nil { + return restMapping, err + } + g.gvkToGVR.Store(gvk, restMapping) + + return restMapping, nil +} + +func (g *cachedRESTMapper) getMapper() meta.RESTMapper { + g.mu.RLock() + defer g.mu.RUnlock() + return g.restMapper +} + +// NewCachedRESTMapper builds a cachedRESTMapper with a customized underlyingMapper. +// If underlyingMapper is nil, defaults to DiscoveryRESTMapper. +func NewCachedRESTMapper(cfg *rest.Config, underlyingMapper meta.RESTMapper) (meta.RESTMapper, error) { + cachedMapper := cachedRESTMapper{} + + // short path, build with customized underlying mapper. + if underlyingMapper != nil { + cachedMapper.restMapper = underlyingMapper + return &cachedMapper, nil + } + + client, err := discovery.NewDiscoveryClientForConfig(cfg) + if err != nil { + return nil, err + } + + // loading current resources for building a base rest mapper. + groupResources, err := restmapper.GetAPIGroupResources(client) + if err != nil { + return nil, err + } + + cachedMapper.restMapper = restmapper.NewDiscoveryRESTMapper(groupResources) + cachedMapper.discoveryClient = client + + return &cachedMapper, nil +} + +// MapperProvider is a wrapper of cachedRESTMapper which is typically used +// to generate customized RESTMapper for controller-runtime framework. +func MapperProvider(c *rest.Config) (meta.RESTMapper, error) { + return NewCachedRESTMapper(c, nil) +} diff --git a/pkg/admiralctl/util/restmapper/restmapper_test.go b/pkg/admiralctl/util/restmapper/restmapper_test.go new file mode 100644 index 000000000..a5482e264 --- /dev/null +++ b/pkg/admiralctl/util/restmapper/restmapper_test.go @@ -0,0 +1,213 @@ +/* +Copyright 2023 The KubeAdmiral Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package restmapper + +import ( + "testing" + + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + discoveryfake "k8s.io/client-go/discovery/fake" + "k8s.io/client-go/rest" + "k8s.io/client-go/restmapper" + coretesting "k8s.io/client-go/testing" + "sigs.k8s.io/controller-runtime/pkg/client/apiutil" +) + +var fakeResources = []*metav1.APIResourceList{ + { + GroupVersion: "apps/v1", + APIResources: []metav1.APIResource{{Name: "deployments", Namespaced: true, Kind: "Deployment"}}, + }, + { + GroupVersion: "v1", + APIResources: []metav1.APIResource{{Name: "pods", Namespaced: true, Kind: "Pod"}}, + }, + { + GroupVersion: "v2", + APIResources: []metav1.APIResource{{Name: "pods", Namespaced: true, Kind: "Pod"}}, + }, + { + GroupVersion: "extensions/v1beta", + APIResources: []metav1.APIResource{{Name: "jobs", Namespaced: true, Kind: "Job"}}, + }, +} + +// getGVRTestCases organizes the test cases for GetGroupVersionResource. +// It can be shared by both benchmark and unit test. +var getGVRTestCases = []struct { + name string + inputGVK schema.GroupVersionKind + expectedGVR schema.GroupVersionResource + expectErr bool +}{ + { + name: "v1,Pod cache miss", + inputGVK: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"}, + expectedGVR: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}, + expectErr: false, + }, + { + name: "v2,Pod cache miss", + inputGVK: schema.GroupVersionKind{Group: "", Version: "v2", Kind: "Pod"}, + expectedGVR: schema.GroupVersionResource{Group: "", Version: "v2", Resource: "pods"}, + expectErr: false, + }, + { + name: "extensions/v1beta,Job cache miss", + inputGVK: schema.GroupVersionKind{Group: "extensions", Version: "v1beta", Kind: "Job"}, + expectedGVR: schema.GroupVersionResource{Group: "extensions", Version: "v1beta", Resource: "jobs"}, + expectErr: false, + }, + { + name: "v1,Pod cache hit once", + inputGVK: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"}, + expectedGVR: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}, + expectErr: false, + }, + { + name: "v1,Pod cache hit twice", + inputGVK: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"}, + expectedGVR: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}, + expectErr: false, + }, + { + name: "v2,Pod cache hit once", + inputGVK: schema.GroupVersionKind{Group: "", Version: "v2", Kind: "Pod"}, + expectedGVR: schema.GroupVersionResource{Group: "", Version: "v2", Resource: "pods"}, + expectErr: false, + }, + { + name: "v2,Pod cache hit twice", + inputGVK: schema.GroupVersionKind{Group: "", Version: "v2", Kind: "Pod"}, + expectedGVR: schema.GroupVersionResource{Group: "", Version: "v2", Resource: "pods"}, + expectErr: false, + }, + { + name: "extensions/v1beta,Job cache hit once", + inputGVK: schema.GroupVersionKind{Group: "extensions", Version: "v1beta", Kind: "Job"}, + expectedGVR: schema.GroupVersionResource{Group: "extensions", Version: "v1beta", Resource: "jobs"}, + expectErr: false, + }, + { + name: "extensions/v1beta,Job cache hit twice", + inputGVK: schema.GroupVersionKind{Group: "extensions", Version: "v1beta", Kind: "Job"}, + expectedGVR: schema.GroupVersionResource{Group: "extensions", Version: "v1beta", Resource: "jobs"}, + expectErr: false, + }, + { + name: "cache miss and invalidate the cache", + inputGVK: schema.GroupVersionKind{Group: "non-existence", Version: "non-existence", Kind: "non-existence"}, + expectErr: true, + }, +} + +var discoveryClient = &discoveryfake.FakeDiscovery{Fake: &coretesting.Fake{Resources: fakeResources}} + +func BenchmarkGetGroupVersionResource(b *testing.B) { + option := apiutil.WithCustomMapper(func() (meta.RESTMapper, error) { + groupResources, err := restmapper.GetAPIGroupResources(discoveryClient) + if err != nil { + return nil, err + } + return restmapper.NewDiscoveryRESTMapper(groupResources), nil + }) + + mapper, err := apiutil.NewDynamicRESTMapper(&rest.Config{}, option) + if err != nil { + b.Error(err) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + for _, tc := range getGVRTestCases { + _, err := GetGroupVersionResource(mapper, tc.inputGVK) + if (err != nil && !tc.expectErr) || (err == nil && tc.expectErr) { + b.Errorf("GetGroupVersionResource For %#v Error: %v, wantErr: %v", tc.inputGVK, err, tc.expectErr) + } + } + } +} + +func BenchmarkGetGroupVersionResourceWithoutCache(b *testing.B) { + groupResources, err := restmapper.GetAPIGroupResources(discoveryClient) + if err != nil { + b.Fatalf("Failed to load resources: %v", err) + } + + mapper := restmapper.NewDiscoveryRESTMapper(groupResources) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + for _, tc := range getGVRTestCases { + _, err := GetGroupVersionResource(mapper, tc.inputGVK) + if (err != nil && !tc.expectErr) || (err == nil && tc.expectErr) { + b.Errorf("GetGroupVersionResource For %#v Error: %v, wantErr: %v", tc.inputGVK, err, tc.expectErr) + } + } + } +} + +func BenchmarkGetGroupVersionResourceWithCache(b *testing.B) { + cachedMapper := &cachedRESTMapper{} + + groupResources, err := restmapper.GetAPIGroupResources(discoveryClient) + if err != nil { + b.Fatalf("Failed to load resources: %v", err) + } + + newMapper := restmapper.NewDiscoveryRESTMapper(groupResources) + cachedMapper.restMapper = newMapper + cachedMapper.discoveryClient = discoveryClient + + b.ResetTimer() + for i := 0; i < b.N; i++ { + for _, tc := range getGVRTestCases { + _, err := GetGroupVersionResource(cachedMapper, tc.inputGVK) + if (err != nil && !tc.expectErr) || (err == nil && tc.expectErr) { + b.Errorf("GetGroupVersionResource For %#v Error: %v, wantErr: %v", tc.inputGVK, err, tc.expectErr) + } + } + } +} + +func TestGetGroupVersionResourceWithCache(t *testing.T) { + cachedMapper := &cachedRESTMapper{} + + groupResources, err := restmapper.GetAPIGroupResources(discoveryClient) + if err != nil { + t.Fatalf("Failed to load resources: %v", err) + } + + newMapper := restmapper.NewDiscoveryRESTMapper(groupResources) + cachedMapper.restMapper = newMapper + cachedMapper.discoveryClient = discoveryClient + + for _, tc := range getGVRTestCases { + t.Run(tc.name, func(t *testing.T) { + got, err := GetGroupVersionResource(cachedMapper, tc.inputGVK) + if (err != nil && !tc.expectErr) || (err == nil && tc.expectErr) { + t.Fatalf("GetGroupVersionResource (%#v) error: %v, wantErr: %v", tc.inputGVK, err, tc.expectErr) + } + + if got != tc.expectedGVR { + t.Fatalf("GetGroupVersionResource(%#v) = %#v, want: %#v", tc.inputGVK, got, tc.expectedGVR) + } + }) + } +}