diff --git a/cmd/gardener-seed-admission-controller/app/app_suite_test.go b/cmd/gardener-seed-admission-controller/app/app_suite_test.go deleted file mode 100644 index 65af696cab50..000000000000 --- a/cmd/gardener-seed-admission-controller/app/app_suite_test.go +++ /dev/null @@ -1,210 +0,0 @@ -// Copyright (c) 2020 SAP SE or an SAP affiliate company. All rights reserved. This file is licensed under the Apache Software License, v. 2 except as noted otherwise in the LICENSE file -// -// 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 app - -import ( - "context" - "crypto/tls" - "fmt" - "io/ioutil" - "net" - "net/http" - "net/url" - "path/filepath" - "testing" - "time" - - "github.com/emicklei/go-restful" - v1 "k8s.io/api/admissionregistration/v1" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/version" - "k8s.io/apiserver/pkg/admission" - "k8s.io/apiserver/pkg/admission/plugin/webhook/mutating" - webhooktesting "k8s.io/apiserver/pkg/admission/plugin/webhook/testing" - "k8s.io/apiserver/pkg/authentication/user" - "k8s.io/apiserver/pkg/server/routes" - "k8s.io/utils/pointer" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/ginkgo/extensions/table" - . "github.com/onsi/gomega" -) - -func TestWebhook(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Webhook admission tests") -} - -var ( - stopCh = make(chan struct{}) - wh *mutating.Plugin - stopCtx, cancelFn = context.WithCancel(context.Background()) -) - -var _ = BeforeSuite(func(done Done) { - o := Options{ - BindAddress: "127.0.0.1", - Port: 10250, - Kubeconfig: filepath.Join("testdata", "dummy-kubeconfig.yaml"), - ServerCertPath: filepath.Join("testdata", "tls.crt"), - ServerKeyPath: filepath.Join("testdata", "tls.key"), - } - - // create a dummy server to response with K8S version - container := restful.NewContainer() - - routes.Version{Version: &version.Info{ - GitVersion: "v1.18.0", - Major: "1", - Minor: "18", - }}.Install(container) - - dummyK8SServer := http.Server{ - Addr: "127.0.0.1:11234", - Handler: container, - } - - go func() { - defer GinkgoRecover() - err := dummyK8SServer.ListenAndServe() - Expect(err).ToNot(HaveOccurred()) - }() - - go func() { - defer GinkgoRecover() - o.run(stopCtx) - Expect(dummyK8SServer.Shutdown(stopCtx)).ToNot(HaveOccurred(), "shutdown of dummy server succeeds") - }() - - // wait for the webhook server to get ready - dialer := &net.Dialer{Timeout: time.Second} - addrPort := fmt.Sprintf("%s:%d", "localhost", 10250) - Eventually(func() error { - conn, err := tls.DialWithDialer(dialer, "tcp", addrPort, &tls.Config{InsecureSkipVerify: true}) - if err != nil { - return err - } - conn.Close() - - return nil - }).Should(Succeed()) - - webhookServer, err := url.Parse("https://localhost:10250") - Expect(err).ToNot(HaveOccurred(), "failed to parse url") - - caCert, err := ioutil.ReadFile(filepath.Join("testdata", "ca.crt")) - Expect(err).NotTo(HaveOccurred(), "ca.crt can be read") - - wh, err = mutating.NewMutatingWebhook(nil) - Expect(err).ToNot(HaveOccurred(), "failed to create mutating webhook") - - client, informer := webhooktesting.NewFakeMutatingDataSource("foo", []v1.MutatingWebhook{{ - Name: "foo", - NamespaceSelector: &metav1.LabelSelector{}, - ObjectSelector: &metav1.LabelSelector{}, - AdmissionReviewVersions: []string{"v1beta1"}, - Rules: []v1.RuleWithOperations{{ - Operations: []v1.OperationType{v1.OperationAll}, - Rule: v1.Rule{ - APIGroups: []string{"*"}, - APIVersions: []string{"*"}, - Resources: []string{"*/*"}, - }, - }}, - ClientConfig: v1.WebhookClientConfig{ - Service: &v1.ServiceReference{ - Name: "webhook-test", - Namespace: "default", - Path: pointer.StringPtr("/webhooks/default-pod-scheduler-name/gardener-shoot-controlplane-scheduler"), - }, - CABundle: caCert, - }, - }}, nil) - - wh.SetAuthenticationInfoResolverWrapper(webhooktesting.Wrapper(webhooktesting.NewAuthenticationInfoResolver(new(int32)))) - wh.SetServiceResolver(webhooktesting.NewServiceResolver(*webhookServer)) - wh.SetExternalKubeClientSet(client) - wh.SetExternalKubeInformerFactory(informer) - - informer.Start(stopCh) - informer.WaitForCacheSync(stopCh) - - Expect(wh.ValidateInitialization()).ToNot(HaveOccurred(), "failed to validate initialization") - - close(done) -}, 60) - -var _ = AfterSuite(func() { - cancelFn() - close(stopCh) -}) - -var _ = Describe("Pod admission", func() { - var pod, expected *corev1.Pod - - BeforeEach(func() { - pod = &corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Name: "foo", - Namespace: "test", - }, Spec: corev1.PodSpec{ - Containers: []corev1.Container{ - { - Name: "test", - }, - }, - }, - } - expected = pod.DeepCopy() - }) - - DescribeTable("with different operations", func(op admission.Operation, expectedSchedulerName string) { - expected.Spec.SchedulerName = expectedSchedulerName - - Expect(wh.Admit( - context.TODO(), - newPodAttribute(pod, op), - webhooktesting.NewObjectInterfacesForTest(), - )).NotTo(HaveOccurred(), "admission succeeds") - Expect(pod).To(Equal(expected)) - }, - Entry("CREATE adds .spec.schedulerName", admission.Create, "gardener-shoot-controlplane-scheduler"), - Entry("UPDDATE does nothing", admission.Update, ""), - Entry("DELETE does nothing", admission.Delete, ""), - Entry("CONNECT does nothing", admission.Connect, ""), - ) -}) - -func newPodAttribute(p *corev1.Pod, op admission.Operation) admission.Attributes { - return admission.NewAttributesRecord( - p, - nil, - corev1.SchemeGroupVersion.WithKind("Pod"), - "test", - "foo", - corev1.SchemeGroupVersion.WithResource("pods"), - "", - op, - &metav1.CreateOptions{}, - false, - &user.DefaultInfo{ - Name: "webhook-test", - UID: "webhook-test", - Groups: nil, - Extra: nil, - }, - ) -} diff --git a/cmd/gardener-seed-admission-controller/app/gardener_seed_admission_controller.go b/cmd/gardener-seed-admission-controller/app/gardener_seed_admission_controller.go index a7fe62b96e86..2852d8549705 100644 --- a/cmd/gardener-seed-admission-controller/app/gardener_seed_admission_controller.go +++ b/cmd/gardener-seed-admission-controller/app/gardener_seed_admission_controller.go @@ -16,53 +16,98 @@ package app import ( "context" - "errors" + "flag" "fmt" - "net/http" "time" - "github.com/gardener/gardener/pkg/client/kubernetes" - gardenerlogger "github.com/gardener/gardener/pkg/logger" - "github.com/gardener/gardener/pkg/seedadmission" - "github.com/spf13/cobra" "github.com/spf13/pflag" - utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" "k8s.io/component-base/version/verflag" - "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/config" + logf "sigs.k8s.io/controller-runtime/pkg/log" logzap "sigs.k8s.io/controller-runtime/pkg/log/zap" - "sigs.k8s.io/controller-runtime/pkg/runtime/inject" + "sigs.k8s.io/controller-runtime/pkg/manager" "sigs.k8s.io/controller-runtime/pkg/webhook" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + "github.com/gardener/gardener/pkg/client/kubernetes" + "github.com/gardener/gardener/pkg/seedadmission" +) + +const ( + // Name is a const for the name of this component. + Name = "gardener-seed-admission-controller" ) -var logger = gardenerlogger.NewLogger("info") +var ( + gracefulShutdownTimeout = 5 * time.Second +) + +// NewSeedAdmissionControllerCommand creates a new *cobra.Command able to run gardener-seed-admission-controller. +func NewSeedAdmissionControllerCommand() *cobra.Command { + var ( + log = logzap.New(logzap.UseDevMode(false), func(opts *logzap.Options) { + encCfg := zap.NewProductionEncoderConfig() + // overwrite time encoding to human readable format + encCfg.EncodeTime = zapcore.ISO8601TimeEncoder + opts.Encoder = zapcore.NewJSONEncoder(encCfg) + }) + opts = &Options{} + ) + + logf.SetLogger(log) + + cmd := &cobra.Command{ + Use: Name, + Short: "Launch the " + Name, + Long: Name + " serves validating and mutating webhook endpoints for resources in seed clusters.", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + verflag.PrintAndExitIfRequested() + + if err := opts.validate(); err != nil { + return err + } + + cmd.SilenceUsage = true + + log.Info("Starting " + Name + "...") + cmd.Flags().VisitAll(func(flag *pflag.Flag) { + log.Info(fmt.Sprintf("FLAG: --%s=%s", flag.Name, flag.Value)) + }) + + return opts.Run(cmd.Context()) + }, + } + + flags := cmd.Flags() + flags.AddGoFlagSet(flag.CommandLine) + verflag.AddFlags(flags) + opts.AddFlags(flags) + return cmd +} -// Options has all the context and parameters needed to run a Gardener seed admission controller. +// Options has all the context and parameters needed to run gardener-seed-admission-controller. type Options struct { // BindAddress is the address the HTTP server should bind to. BindAddress string // Port is the port that should be opened by the HTTP server. Port int - // ServerCertPath is the path to a server certificate. - ServerCertPath string - // ServerKeyPath is the path to a TLS private key. - ServerKeyPath string - // Kubeconfig is path to a kubeconfig file. If not given it uses the in-cluster config. - Kubeconfig string + // ServerCertDir is the path to server TLS cert and key. + ServerCertDir string } -// AddFlags adds flags for a specific Scheduler to the specified FlagSet. +// AddFlags adds gardener-seed-admission-controller's flags to the specified FlagSet. func (o *Options) AddFlags(fs *pflag.FlagSet) { fs.StringVar(&o.BindAddress, "bind-address", "0.0.0.0", "address to bind to") - fs.IntVar(&o.Port, "port", 9443, "server port") - fs.StringVar(&o.ServerCertPath, "tls-cert-path", "", "path to server certificate") - fs.StringVar(&o.ServerKeyPath, "tls-private-key-path", "", "path to client certificate") - fs.StringVar(&o.Kubeconfig, "kubeconfig", "", "path to a kubeconfig") + fs.IntVar(&o.Port, "port", 9443, "webhook server port") + fs.StringVar(&o.ServerCertDir, "tls-cert-dir", "", "directory with server TLS certificate and key (must contain a tls.crt and tls.key file)") } -// Validate validates all the required options. -func (o *Options) validate(args []string) error { +// validate validates all the required options. +func (o *Options) validate() error { if len(o.BindAddress) == 0 { return fmt.Errorf("missing bind address") } @@ -71,122 +116,53 @@ func (o *Options) validate(args []string) error { return fmt.Errorf("missing port") } - if len(o.ServerCertPath) == 0 { + if len(o.ServerCertDir) == 0 { return fmt.Errorf("missing server tls cert path") } - if len(o.ServerKeyPath) == 0 { - return fmt.Errorf("missing server tls key path") - } - - if len(args) != 0 { - return errors.New("arguments are not supported") - } - return nil } -func (o *Options) run(ctx context.Context) { - run(ctx, o.BindAddress, o.Port, o.ServerCertPath, o.ServerKeyPath, o.Kubeconfig) -} - -// NewCommandStartGardenerSeedAdmissionController creates a *cobra.Command object with default parameters -func NewCommandStartGardenerSeedAdmissionController() *cobra.Command { - opts := &Options{} - - cmd := &cobra.Command{ - Use: "gardener-seed-admission-controller", - Short: "Launch the Gardener seed admission controller", - Long: `The Gardener seed admission controller serves a validation webhook endpoint for resources in the seed clusters.`, - Run: func(cmd *cobra.Command, args []string) { - verflag.PrintAndExitIfRequested() - - utilruntime.Must(opts.validate(args)) - - logger.Infof("Starting Gardener seed admission controller...") - cmd.Flags().VisitAll(func(flag *pflag.Flag) { - logger.Infof("FLAG: --%s=%s", flag.Name, flag.Value) - }) - - opts.run(cmd.Context()) - }, - } - - flags := cmd.Flags() - verflag.AddFlags(flags) - opts.AddFlags(flags) - return cmd -} - -// run runs the Gardener seed admission controller. This should never exit. -func run(ctx context.Context, bindAddress string, port int, certPath, keyPath, kubeconfigPath string) { - var ( - log = logzap.New(logzap.UseDevMode(false)) - mux = http.NewServeMux() - ) +// Run runs gardener-seed-admission-controller using the specified options. +func (o *Options) Run(ctx context.Context) error { + log := logf.Log - k8sClient, err := kubernetes.NewClientFromFile("", kubeconfigPath, kubernetes.WithClientOptions(client.Options{ - Scheme: kubernetes.SeedScheme, - })) + log.Info("getting rest config") + restConfig, err := config.GetConfig() if err != nil { - logger.Errorf("unable to create kubernetes client: %+v", err) - panic(err) + return err } - // prepare an injection func that will inject needed dependencies into webhook and handler. - var setFields inject.Func - setFields = func(i interface{}) error { - if _, err := inject.InjectorInto(setFields, i); err != nil { - return err - } - if _, err := inject.ClientInto(k8sClient.DirectClient(), i); err != nil { - return err - } - if _, err := inject.LoggerInto(log, i); err != nil { - return err - } - // inject scheme into webhook, needed to construct and a decoder for decoding the included objects - // decoder will be inject by webhook into handler - if _, err := inject.SchemeInto(kubernetes.SeedScheme, i); err != nil { - return err - } - return nil + log.Info("setting up manager") + mgr, err := manager.New(restConfig, manager.Options{ + Scheme: kubernetes.SeedScheme, + LeaderElection: false, + MetricsBindAddress: "0", // disable for now, as we don't scrape the component + Host: o.BindAddress, + Port: o.Port, + CertDir: o.ServerCertDir, + GracefulShutdownTimeout: &gracefulShutdownTimeout, + }) + if err != nil { + return err } - extensionDeletionProtection := &webhook.Admission{Handler: seedadmission.NewExtensionDeletionProtection(logger)} - defaultSchedulerName := &webhook.Admission{Handler: admission.HandlerFunc(seedadmission.DefaultShootControlPlanePodsSchedulerName)} - if err := setFields(extensionDeletionProtection); err != nil { - panic(fmt.Errorf("error injecting dependencies into webhook handler: %w", err)) - } - if err := setFields(defaultSchedulerName); err != nil { - panic(fmt.Errorf("error injecting dependencies into webhook handler: %w", err)) - } + log.Info("setting up webhook server") + server := mgr.GetWebhookServer() - mux.Handle("/webhooks/validate-extension-crd-deletion", extensionDeletionProtection) - mux.Handle( + server.Register(seedadmission.ExtensionDeletionProtectionWebhookPath, &webhook.Admission{Handler: &seedadmission.ExtensionDeletionProtection{}}) + server.Register( // in the future we might want to have additional scheduler names // so lets have the handler be of pattern "/webhooks/default-pod-scheduler-name/{scheduler-name}" - fmt.Sprintf(seedadmission.GardenerShootControlPlaneSchedulerWebhookPath), - defaultSchedulerName, + seedadmission.GardenerShootControlPlaneSchedulerWebhookPath, + &webhook.Admission{Handler: admission.HandlerFunc(seedadmission.DefaultShootControlPlanePodsSchedulerName)}, ) - srv := &http.Server{ - Addr: fmt.Sprintf("%s:%d", bindAddress, port), - Handler: mux, + log.Info("starting manager") + if err := mgr.Start(ctx); err != nil { + log.Error(err, "error running manager") + return err } - go func() { - if err := srv.ListenAndServeTLS(certPath, keyPath); err != http.ErrServerClosed { - logger.Errorf("Could not start HTTPS server: %v", err) - panic(err) - } - }() - - <-ctx.Done() - timeoutCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - if err := srv.Shutdown(timeoutCtx); err != nil { - logger.Errorf("Error when shutting down HTTPS server: %v", err) - } - logger.Info("HTTPS servers stopped.") + return nil } diff --git a/cmd/gardener-seed-admission-controller/app/testdata/admission.json b/cmd/gardener-seed-admission-controller/app/testdata/admission.json deleted file mode 100644 index 39844c3c3d57..000000000000 --- a/cmd/gardener-seed-admission-controller/app/testdata/admission.json +++ /dev/null @@ -1,71 +0,0 @@ -{ - "apiVersion": "admission.k8s.io/v1beta1", - "kind": "AdmissionReview", - "request": { - "uid": "705ab4f5-6393-11e8-b7cc-42010a800002", - "kind": { - "group": "", - "version": "v1", - "kind": "Pod" - }, - "resource": { - "group": "", - "version": "v1", - "resource": "pods" - }, - "requestKind": { - "group": "", - "version": "v1", - "kind": "Pod" - }, - "requestResource": { - "group": "", - "version": "v1", - "resource": "pods" - }, - "name": "foo", - "namespace": "my-namespace", - "operation": "CREATE", - "userInfo": { - "username": "admin", - "uid": "014fbff9a07c", - "groups": [ - "system:authenticated", - "my-admin-group" - ], - "extra": { - "some-key": [ - "some-value1", - "some-value2" - ] - } - }, - "object": { - "apiVersion": "v1", - "kind": "Pod", - "metadata": { - "creationTimestamp": null - }, - "spec": { - "initContainers": [ - { - "name": "foo-init", - "resources": {} - } - ], - "containers": [ - { - "name": "foo", - "resources": {} - } - ] - }, - "status": {} - }, - "options": { - "apiVersion": "meta.k8s.io/v1", - "kind": "CreateOptions" - }, - "dryRun": false - } -} diff --git a/cmd/gardener-seed-admission-controller/app/testdata/ca.crt b/cmd/gardener-seed-admission-controller/app/testdata/ca.crt deleted file mode 100644 index 157768320e2d..000000000000 --- a/cmd/gardener-seed-admission-controller/app/testdata/ca.crt +++ /dev/null @@ -1,20 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDPTCCAiWgAwIBAgIJALTyMgMR6YygMA0GCSqGSIb3DQEBCwUAMDQxMjAwBgNV -BAMMKWdlbmVyaWNfd2ViaG9va19hZG1pc3Npb25fcGx1Z2luX3Rlc3RzX2NhMCAX -DTE3MTExNjAwMDUzOVoYDzIyOTEwOTAxMDAwNTM5WjA0MTIwMAYDVQQDDClnZW5l -cmljX3dlYmhvb2tfYWRtaXNzaW9uX3BsdWdpbl90ZXN0c19jYTCCASIwDQYJKoZI -hvcNAQEBBQADggEPADCCAQoCggEBAK6p1uCvlLJj820geyWITdBzbCTSLY8P/O0i -cSIxqlLbQvtrbPmPgXXXEIDtBH8z+vECevh+4rZs3alj036GSYrsvrcF8YBcDB9l -gigeiOQoJ1R3A2nJnT+7zoK9RnMqiFdejVhJ7eocUTX1Z3q3bi1ln1a9P8BmN2o8 -NOhRxMe6C9SX8dZaOFD1Xad/T7eazsMOuhT/HrUWhIctgBxLXx0uC+BsXCS/9J98 -PFCHzKmI0ylvZc7il1Lq8/RxCdzfiTCLaJX/t6ITKf8XIokgbx8xW37v2b9p16lh -nMajppzNS/GS8EOHN2UUP5/bSGnBGcsuvVQEgEHCIzHcCB3FmS8CAwEAAaNQME4w -HQYDVR0OBBYEFBsZKbynr9Iix+ud0FxQvMVIPZqOMB8GA1UdIwQYMBaAFBsZKbyn -r9Iix+ud0FxQvMVIPZqOMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEB -AJZXfcHyAsq5qr1UqhutrNlsW2u7kkAc+Ql5wZEdXIyjKKC+kOllWqKo5IPtmMIi -R5VCm1g3iFCUV6FdXNtfF7tWZqaHV58nkJYlDc2yZxQzaWQeu8U92w+Qr5H1ansL -FOGS6A4+rMj2EDEt+lCmsz+l5UD7nrhnyGMzeumASQ6cXPV1uB2LTc4IzsOYFKs9 -nt9SDH7tF+0bQwZ1YUrfMYJpNp6ETjpPKJVhq8/FGqwT+9egFbgjAhrEpPccFkXo -D7NLhM1JHUiqNQStDq9mDKLHKmp++UScrNc66b6egN1sIPBHyLu8ApzcF2YHuEYC -RGyWZ0sJtjzWjK7IU9RdmLI= ------END CERTIFICATE----- diff --git a/cmd/gardener-seed-admission-controller/app/testdata/dummy-kubeconfig.yaml b/cmd/gardener-seed-admission-controller/app/testdata/dummy-kubeconfig.yaml deleted file mode 100644 index d129aa028f6b..000000000000 --- a/cmd/gardener-seed-admission-controller/app/testdata/dummy-kubeconfig.yaml +++ /dev/null @@ -1,19 +0,0 @@ -apiVersion: v1 -clusters: -- cluster: - insecure-skip-tls-verify: true - server: http://localhost:11234 - name: dummy -contexts: -- context: - cluster: dummy - user: dummy - name: dummy -current-context: dummy -kind: Config -preferences: {} -users: -- name: dummy - user: - username: dummy - password: dummy diff --git a/cmd/gardener-seed-admission-controller/app/testdata/tls.crt b/cmd/gardener-seed-admission-controller/app/testdata/tls.crt deleted file mode 100644 index 50d91c906e72..000000000000 --- a/cmd/gardener-seed-admission-controller/app/testdata/tls.crt +++ /dev/null @@ -1,20 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDQDCCAiigAwIBAgIJANWw74P5KJk2MA0GCSqGSIb3DQEBCwUAMDQxMjAwBgNV -BAMMKWdlbmVyaWNfd2ViaG9va19hZG1pc3Npb25fcGx1Z2luX3Rlc3RzX2NhMCAX -DTE3MTExNjAwMDUzOVoYDzIyOTEwOTAxMDAwNTM5WjAjMSEwHwYDVQQDExh3ZWJo -b29rLXRlc3QuZGVmYXVsdC5zdmMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK -AoIBAQDXd/nQ89a5H8ifEsigmMd01Ib6NVR3bkJjtkvYnTbdfYEBj7UzqOQtHoLa -dIVmefny5uIHvj93WD8WDVPB3jX2JHrXkDTXd/6o6jIXHcsUfFTVLp6/bZ+Anqe0 -r/7hAPkzA2A7APyTWM3ZbEeo1afXogXhOJ1u/wz0DflgcB21gNho4kKTONXO3NHD -XLpspFqSkxfEfKVDJaYAoMnYZJtFNsa2OvsmLnhYF8bjeT3i07lfwrhUZvP+7Gsp -7UgUwc06WuNHjfx1s5e6ySzH0QioMD1rjYneqOvk0pKrMIhuAEWXqq7jlXcDtx1E -j+wnYbVqqVYheHZ8BCJoVAAQGs9/AgMBAAGjZDBiMAkGA1UdEwQCMAAwCwYDVR0P -BAQDAgXgMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDATApBgNVHREEIjAg -hwR/AAABghh3ZWJob29rLXRlc3QuZGVmYXVsdC5zdmMwDQYJKoZIhvcNAQELBQAD -ggEBAD/GKSPNyQuAOw/jsYZesb+RMedbkzs18sSwlxAJQMUrrXwlVdHrA8q5WhE6 -ABLqU1b8lQ8AWun07R8k5tqTmNvCARrAPRUqls/ryER+3Y9YEcxEaTc3jKNZFLbc -T6YtcnkdhxsiO136wtiuatpYL91RgCmuSpR8+7jEHhuFU01iaASu7ypFrUzrKHTF -bKwiLRQi1cMzVcLErq5CDEKiKhUkoDucyARFszrGt9vNIl/YCcBOkcNvM3c05Hn3 -M++C29JwS3Hwbubg6WO3wjFjoEhpCwU6qRYUz3MRp4tHO4kxKXx+oQnUiFnR7vW0 -YkNtGc1RUDHwecCTFpJtPb7Yu/E= ------END CERTIFICATE----- diff --git a/cmd/gardener-seed-admission-controller/app/testdata/tls.key b/cmd/gardener-seed-admission-controller/app/testdata/tls.key deleted file mode 100644 index 18a05b8c7b6c..000000000000 --- a/cmd/gardener-seed-admission-controller/app/testdata/tls.key +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEowIBAAKCAQEA13f50PPWuR/InxLIoJjHdNSG+jVUd25CY7ZL2J023X2BAY+1 -M6jkLR6C2nSFZnn58ubiB74/d1g/Fg1Twd419iR615A013f+qOoyFx3LFHxU1S6e -v22fgJ6ntK/+4QD5MwNgOwD8k1jN2WxHqNWn16IF4Tidbv8M9A35YHAdtYDYaOJC -kzjVztzRw1y6bKRakpMXxHylQyWmAKDJ2GSbRTbGtjr7Ji54WBfG43k94tO5X8K4 -VGbz/uxrKe1IFMHNOlrjR438dbOXusksx9EIqDA9a42J3qjr5NKSqzCIbgBFl6qu -45V3A7cdRI/sJ2G1aqlWIXh2fAQiaFQAEBrPfwIDAQABAoIBAAZbxgWCjJ2d8H+x -QDZtC8XI18redAWqPU9P++ECkrHqmDoBkalanJEwS1BDDATAKL4gTh9IX/sXoZT3 -A7e+5PzEitN9r/GD2wIFF0FTYcDTAnXgEFM52vEivXQ5lV3yd2gn+1kCaHG4typp -ZZv34iIc5+uDjjHOWQWCvA86f8XxX5EfYH+GkjfixTtN2xhWWlfi9vzYeESS4Jbt -tqfH0iEaZ1Bm/qvb8vFgKiuSTOoSpaf+ojAdtPtXDjf1bBtQQG+RSQkP59O/taLM -FCVuRrU8EtdB0+9anwmAP+O2UqjL5izA578lQtdIh13jHtGEgOcnfGNUphK11y9r -Mg5V28ECgYEA9fwI6Xy1Rb9b9irp4bU5Ec99QXa4x2bxld5cDdNOZWJQu9OnaIbg -kw/1SyUkZZCGMmibM/BiWGKWoDf8E+rn/ujGOtd70sR9U0A94XMPqEv7iHxhpZmD -rZuSz4/snYbOWCZQYXFoD/nqOwE7Atnz7yh+Jti0qxBQ9bmkb9o0QW8CgYEA4D3d -okzodg5QQ1y9L0J6jIC6YysoDedveYZMd4Un9bKlZEJev4OwiT4xXmSGBYq/7dzo -OJOvN6qgPfibr27mSB8NkAk6jL/VdJf3thWxNYmjF4E3paLJ24X31aSipN1Ta6K3 -KKQUQRvixVoI1q+8WHAubBDEqvFnNYRHD+AjKvECgYBkekjhpvEcxme4DBtw+OeQ -4OJXJTmhKemwwB12AERboWc88d3GEqIVMEWQJmHRotFOMfCDrMNfOxYv5+5t7FxL -gaXHT1Hi7CQNJ4afWrKgmjjqrXPtguGIvq2fXzjVt8T9uNjIlNxe+kS1SXFjXsgH -ftDY6VgTMB0B4ozKq6UAvQKBgQDER8K5buJHe+3rmMCMHn+Qfpkndr4ftYXQ9Kn4 -MFiy6sV0hdfTgRzEdOjXu9vH/BRVy3iFFVhYvIR42iTEIal2VaAUhM94Je5cmSyd -eE1eFHTqfRPNazmPaqttmSc4cfa0D4CNFVoZR6RupIl6Cect7jvkIaVUD+wMXxWo -osOFsQKBgDLwVhZWoQ13RV/jfQxS3veBUnHJwQJ7gKlL1XZ16mpfEOOVnJF7Es8j -TIIXXYhgSy/XshUbsgXQ+YGliye/rXSCTXHBXvWShOqxEMgeMYMRkcm8ZLp/DH7C -kC2pemkLPUJqgSh1PASGcJbDJIvFGUfP69tUCYpHpk3nHzexuAg3 ------END RSA PRIVATE KEY----- diff --git a/cmd/gardener-seed-admission-controller/main.go b/cmd/gardener-seed-admission-controller/main.go index b0f8573641e4..012440cadb42 100644 --- a/cmd/gardener-seed-admission-controller/main.go +++ b/cmd/gardener-seed-admission-controller/main.go @@ -28,7 +28,7 @@ func main() { utils.DeduplicateWarnings() ctx := signals.SetupSignalHandler() - if err := app.NewCommandStartGardenerSeedAdmissionController().ExecuteContext(ctx); err != nil { + if err := app.NewSeedAdmissionControllerCommand().ExecuteContext(ctx); err != nil { fmt.Println(err) os.Exit(1) } diff --git a/go.mod b/go.mod index eecebc89b502..02f1a13b757d 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,6 @@ require ( github.com/Masterminds/semver v1.5.0 github.com/ahmetb/gen-crd-api-reference-docs v0.2.0 github.com/coreos/go-systemd/v22 v22.1.0 - github.com/emicklei/go-restful v2.9.6+incompatible github.com/envoyproxy/go-control-plane v0.9.7-0.20200730005029-803dd64f0468 github.com/gardener/etcd-druid v0.3.0 github.com/gardener/external-dns-management v0.7.18 diff --git a/hack/local-development/start-seed-admission-controller b/hack/local-development/start-seed-admission-controller index 6779b6dec814..b91cd01c74f8 100755 --- a/hack/local-development/start-seed-admission-controller +++ b/hack/local-development/start-seed-admission-controller @@ -26,5 +26,4 @@ GO111MODULE=on \ -ldflags "$(./hack/get-build-ld-flags.sh)" \ "$(dirname $0)"/../../cmd/gardener-seed-admission-controller/main.go \ --kubeconfig="${KUBECONFIG:-$kubeconfig}" \ - --tls-cert-path="$(dirname $0)/../../example/seed-admission-controller/tls.crt" \ - --tls-private-key-path="$(dirname $0)/../../example/seed-admission-controller/tls.key" + --tls-cert-dir="$(dirname $0)/../../example/seed-admission-controller" diff --git a/pkg/operation/botanist/seedsystemcomponents/seedadmission/seedadmission.go b/pkg/operation/botanist/seedsystemcomponents/seedadmission/seedadmission.go index aa5e1fa1c00f..c0bd29b1ba0b 100644 --- a/pkg/operation/botanist/seedsystemcomponents/seedadmission/seedadmission.go +++ b/pkg/operation/botanist/seedsystemcomponents/seedadmission/seedadmission.go @@ -19,14 +19,9 @@ import ( "fmt" "time" - v1beta1constants "github.com/gardener/gardener/pkg/apis/core/v1beta1/constants" - extensionsv1alpha1 "github.com/gardener/gardener/pkg/apis/extensions/v1alpha1" - "github.com/gardener/gardener/pkg/client/kubernetes" - "github.com/gardener/gardener/pkg/operation/botanist/component" - "github.com/gardener/gardener/pkg/operation/common" - "github.com/gardener/gardener/pkg/utils/managedresources" - "github.com/Masterminds/semver" + admissionv1 "k8s.io/api/admission/v1" + admissionv1beta1 "k8s.io/api/admission/v1beta1" admissionregistrationv1beta1 "k8s.io/api/admissionregistration/v1beta1" appsv1 "k8s.io/api/apps/v1" autoscalingv1 "k8s.io/api/autoscaling/v1" @@ -42,6 +37,14 @@ import ( autoscalingv1beta2 "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/apis/autoscaling.k8s.io/v1beta2" "k8s.io/utils/pointer" "sigs.k8s.io/controller-runtime/pkg/client" + + v1beta1constants "github.com/gardener/gardener/pkg/apis/core/v1beta1/constants" + extensionsv1alpha1 "github.com/gardener/gardener/pkg/apis/extensions/v1alpha1" + "github.com/gardener/gardener/pkg/client/kubernetes" + "github.com/gardener/gardener/pkg/operation/botanist/component" + "github.com/gardener/gardener/pkg/operation/common" + "github.com/gardener/gardener/pkg/seedadmission" + "github.com/gardener/gardener/pkg/utils/managedresources" ) const ( @@ -204,8 +207,7 @@ func (g *gardenerSeedAdmissionController) Deploy(ctx context.Context) error { Command: []string{ "/gardener-seed-admission-controller", fmt.Sprintf("--port=%d", port), - fmt.Sprintf("--tls-cert-path=%s/%s", volumeMountPath, corev1.TLSCertKey), - fmt.Sprintf("--tls-private-key-path=%s/%s", volumeMountPath, corev1.TLSPrivateKeyKey), + fmt.Sprintf("--tls-cert-dir=%s", volumeMountPath), }, Ports: []corev1.ContainerPort{{ ContainerPort: int32(port), @@ -276,9 +278,8 @@ func (g *gardenerSeedAdmissionController) Deploy(ctx context.Context) error { failurePolicy = admissionregistrationv1beta1.Fail validatingWebhookConfiguration = &admissionregistrationv1beta1.ValidatingWebhookConfiguration{ ObjectMeta: metav1.ObjectMeta{ - Name: Name, - Namespace: g.namespace, - Labels: getLabels(), + Name: Name, + Labels: getLabels(), }, Webhooks: []admissionregistrationv1beta1.ValidatingWebhook{ { @@ -298,9 +299,11 @@ func (g *gardenerSeedAdmissionController) Deploy(ctx context.Context) error { Service: &admissionregistrationv1beta1.ServiceReference{ Name: service.Name, Namespace: service.Namespace, - Path: pointer.StringPtr("/webhooks/validate-extension-crd-deletion"), + Path: pointer.StringPtr(seedadmission.ExtensionDeletionProtectionWebhookPath), }, }, + AdmissionReviewVersions: []string{admissionv1beta1.SchemeGroupVersion.Version, admissionv1.SchemeGroupVersion.Version}, + TimeoutSeconds: pointer.Int32Ptr(10), }, { Name: "crs.seed.admission.core.gardener.cloud", @@ -329,9 +332,11 @@ func (g *gardenerSeedAdmissionController) Deploy(ctx context.Context) error { Service: &admissionregistrationv1beta1.ServiceReference{ Name: service.Name, Namespace: service.Namespace, - Path: pointer.StringPtr("/webhooks/validate-extension-crd-deletion"), + Path: pointer.StringPtr(seedadmission.ExtensionDeletionProtectionWebhookPath), }, }, + AdmissionReviewVersions: []string{admissionv1beta1.SchemeGroupVersion.Version, admissionv1.SchemeGroupVersion.Version}, + TimeoutSeconds: pointer.Int32Ptr(10), }, }, } diff --git a/pkg/operation/botanist/seedsystemcomponents/seedadmission/seedadmission_test.go b/pkg/operation/botanist/seedsystemcomponents/seedadmission/seedadmission_test.go index 6845a337fbf0..394edb36c84e 100644 --- a/pkg/operation/botanist/seedsystemcomponents/seedadmission/seedadmission_test.go +++ b/pkg/operation/botanist/seedsystemcomponents/seedadmission/seedadmission_test.go @@ -138,8 +138,7 @@ spec: - command: - /gardener-seed-admission-controller - --port=10250 - - --tls-cert-path=/srv/gardener-seed-admission-controller/tls.crt - - --tls-private-key-path=/srv/gardener-seed-admission-controller/tls.key + - --tls-cert-dir=/srv/gardener-seed-admission-controller image: ` + image + ` imagePullPolicy: IfNotPresent name: gardener-seed-admission-controller @@ -238,9 +237,11 @@ metadata: app: gardener role: seed-admission-controller name: gardener-seed-admission-controller - namespace: shoot--foo--bar webhooks: -- clientConfig: +- admissionReviewVersions: + - v1beta1 + - v1 + clientConfig: caBundle: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUMrakNDQWVLZ0F3SUJBZ0lVVHAzWHZocldPVk04WkdlODZZb1hNVi9VSjdBd0RRWUpLb1pJaHZjTkFRRUwKQlFBd0ZURVRNQkVHQTFVRUF4TUthM1ZpWlhKdVpYUmxjekFlRncweE9UQXlNamN4TlRNME1EQmFGdzB5TkRBeQpNall4TlRNME1EQmFNQlV4RXpBUkJnTlZCQU1UQ210MVltVnlibVYwWlhNd2dnRWlNQTBHQ1NxR1NJYjNEUUVCCkFRVUFBNElCRHdBd2dnRUtBb0lCQVFDeWkwUUdPY3YyYlRmM044T0xOOTdSd3NnSDZRQXI4d1NwQU9ydHRCSmcKRm5mblUyVDFSSGd4bTdxZDE5MFdMOERDaHYwZFpmNzZkNmVTUTRacmpqeUFyVHp1ZmI0RHRQd2crVldxN1h2RgpCTnluKzJoZjRTeVNrd2Q2azdYTGhVVFJ4MDQ4SWJCeUM0ditGRXZtb0xBd3JjMGQwRzE0ZWM2c25EKzdqTzdlCmt5a1EvTmdBT0w3UDZrRHM5ejYrYk9mZ0YwbkdOK2JtZVdRcUplalIwdCtPeVFEQ3g1L0ZNdFVmRVZSNVFYODAKYWVlZmdwM0pGWmI2ZkF3OUtoTHRkUlYzRlAwdHo2aFMrZTRTZzBtd0FBT3FpalpzVjg3a1A1R1l6anRjZkExMgpsRFlsL25iMUd0VnZ2a1FENDlWblY3bURubDZtRzNMQ01OQ05INldsWk52M0FnTUJBQUdqUWpCQU1BNEdBMVVkCkR3RUIvd1FFQXdJQkJqQVBCZ05WSFJNQkFmOEVCVEFEQVFIL01CMEdBMVVkRGdRV0JCU0ZBM0x2Sk0yMWQ4cXMKWlZWQ2U2UnJUVDl3aVRBTkJna3Foa2lHOXcwQkFRc0ZBQU9DQVFFQW5zL0VKM3lLc2p0SVNvdGVRNzE0cjJVbQpCTVB5VVlUVGRSSEQ4TFpNZDNSeWt2c2FjRjJsMnk4OE56NndKY0F1b1VqMWg4YUJEUDVvWFZ0Tm1GVDlqeWJTClRYclJ2V2krYWVZZGI1NTZuRUE1L2E5NGUrY2IrQ2szcXkvMXhnUW9TNDU3QVpRT0Rpc0RaTkJZV2tBRnMyTGMKdWNwY0F0WEp0SXRoVm03RmpvQUhZY3NyWTA0eUFpWUVKTEQwMlRqVURYZzRpR09HTWtWSGRtaGF3QkRCRjNBagplc2ZjcUZ3amk2SnlBS0ZSQUNQb3d5a1FPTkZ3VVNvbTg5dVlFU1NDSkZ2TkNrOU1KbWpKMlB6RFV0NkN5cFI0CmVwRmRkMWZYTHd1d243ZnZQTW1KcUQzSHRMYWxYMUFabVBrK0JJOGV6ZkFpVmNWcW5USlFNWGxZUHBZZTlBPT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQ== service: name: gardener-seed-admission-controller @@ -260,7 +261,11 @@ webhooks: - DELETE resources: - customresourcedefinitions -- clientConfig: + timeoutSeconds: 10 +- admissionReviewVersions: + - v1beta1 + - v1 + clientConfig: caBundle: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUMrakNDQWVLZ0F3SUJBZ0lVVHAzWHZocldPVk04WkdlODZZb1hNVi9VSjdBd0RRWUpLb1pJaHZjTkFRRUwKQlFBd0ZURVRNQkVHQTFVRUF4TUthM1ZpWlhKdVpYUmxjekFlRncweE9UQXlNamN4TlRNME1EQmFGdzB5TkRBeQpNall4TlRNME1EQmFNQlV4RXpBUkJnTlZCQU1UQ210MVltVnlibVYwWlhNd2dnRWlNQTBHQ1NxR1NJYjNEUUVCCkFRVUFBNElCRHdBd2dnRUtBb0lCQVFDeWkwUUdPY3YyYlRmM044T0xOOTdSd3NnSDZRQXI4d1NwQU9ydHRCSmcKRm5mblUyVDFSSGd4bTdxZDE5MFdMOERDaHYwZFpmNzZkNmVTUTRacmpqeUFyVHp1ZmI0RHRQd2crVldxN1h2RgpCTnluKzJoZjRTeVNrd2Q2azdYTGhVVFJ4MDQ4SWJCeUM0ditGRXZtb0xBd3JjMGQwRzE0ZWM2c25EKzdqTzdlCmt5a1EvTmdBT0w3UDZrRHM5ejYrYk9mZ0YwbkdOK2JtZVdRcUplalIwdCtPeVFEQ3g1L0ZNdFVmRVZSNVFYODAKYWVlZmdwM0pGWmI2ZkF3OUtoTHRkUlYzRlAwdHo2aFMrZTRTZzBtd0FBT3FpalpzVjg3a1A1R1l6anRjZkExMgpsRFlsL25iMUd0VnZ2a1FENDlWblY3bURubDZtRzNMQ01OQ05INldsWk52M0FnTUJBQUdqUWpCQU1BNEdBMVVkCkR3RUIvd1FFQXdJQkJqQVBCZ05WSFJNQkFmOEVCVEFEQVFIL01CMEdBMVVkRGdRV0JCU0ZBM0x2Sk0yMWQ4cXMKWlZWQ2U2UnJUVDl3aVRBTkJna3Foa2lHOXcwQkFRc0ZBQU9DQVFFQW5zL0VKM3lLc2p0SVNvdGVRNzE0cjJVbQpCTVB5VVlUVGRSSEQ4TFpNZDNSeWt2c2FjRjJsMnk4OE56NndKY0F1b1VqMWg4YUJEUDVvWFZ0Tm1GVDlqeWJTClRYclJ2V2krYWVZZGI1NTZuRUE1L2E5NGUrY2IrQ2szcXkvMXhnUW9TNDU3QVpRT0Rpc0RaTkJZV2tBRnMyTGMKdWNwY0F0WEp0SXRoVm03RmpvQUhZY3NyWTA0eUFpWUVKTEQwMlRqVURYZzRpR09HTWtWSGRtaGF3QkRCRjNBagplc2ZjcUZ3amk2SnlBS0ZSQUNQb3d5a1FPTkZ3VVNvbTg5dVlFU1NDSkZ2TkNrOU1KbWpKMlB6RFV0NkN5cFI0CmVwRmRkMWZYTHd1d243ZnZQTW1KcUQzSHRMYWxYMUFabVBrK0JJOGV6ZkFpVmNWcW5USlFNWGxZUHBZZTlBPT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQ== service: name: gardener-seed-admission-controller @@ -286,6 +291,7 @@ webhooks: - networks - operatingsystemconfigs - workers + timeoutSeconds: 10 ` validatingWebhookConfigurationYAMLK8sLess115 = validatingWebhookConfigurationTop + validatingWebhookConfigurationBottom validatingWebhookConfigurationYAMLK8sGreaterEqual115 = validatingWebhookConfigurationTop + ` objectSelector: @@ -331,15 +337,15 @@ status: {} }, Type: corev1.SecretTypeOpaque, Data: map[string][]byte{ - "clusterrole____gardener-seed-admission-controller.yaml": []byte(clusterRoleYAML), - "clusterrolebinding____gardener-seed-admission-controller.yaml": []byte(clusterRoleBindingYAML), - "deployment__shoot--foo--bar__gardener-seed-admission-controller.yaml": []byte(deploymentYAML), - "poddisruptionbudget__shoot--foo--bar__gardener-seed-admission-controller.yaml": []byte(pdbYAML), - "secret__shoot--foo--bar__gardener-seed-admission-controller-tls.yaml": []byte(secretYAML), - "service__shoot--foo--bar__gardener-seed-admission-controller.yaml": []byte(serviceYAML), - "serviceaccount__shoot--foo--bar__gardener-seed-admission-controller.yaml": []byte(serviceAccountYAML), - "validatingwebhookconfiguration__shoot--foo--bar__gardener-seed-admission-controller.yaml": []byte(validatingWebhookConfigurationYAMLK8sLess115), - "verticalpodautoscaler__shoot--foo--bar__gardener-seed-admission-controller-vpa.yaml": []byte(vpaYAML), + "clusterrole____gardener-seed-admission-controller.yaml": []byte(clusterRoleYAML), + "clusterrolebinding____gardener-seed-admission-controller.yaml": []byte(clusterRoleBindingYAML), + "deployment__shoot--foo--bar__gardener-seed-admission-controller.yaml": []byte(deploymentYAML), + "poddisruptionbudget__shoot--foo--bar__gardener-seed-admission-controller.yaml": []byte(pdbYAML), + "secret__shoot--foo--bar__gardener-seed-admission-controller-tls.yaml": []byte(secretYAML), + "service__shoot--foo--bar__gardener-seed-admission-controller.yaml": []byte(serviceYAML), + "serviceaccount__shoot--foo--bar__gardener-seed-admission-controller.yaml": []byte(serviceAccountYAML), + "validatingwebhookconfiguration____gardener-seed-admission-controller.yaml": []byte(validatingWebhookConfigurationYAMLK8sLess115), + "verticalpodautoscaler__shoot--foo--bar__gardener-seed-admission-controller-vpa.yaml": []byte(vpaYAML), }, } managedResource = &resourcesv1alpha1.ManagedResource{ @@ -399,7 +405,7 @@ status: {} It("should successfully deploy all resources (k8s >= 1.15)", func() { seedAdmission = New(c, namespace, image, semver.MustParse("1.15.5")) - managedResourceSecret.Data["validatingwebhookconfiguration__shoot--foo--bar__gardener-seed-admission-controller.yaml"] = []byte(validatingWebhookConfigurationYAMLK8sGreaterEqual115) + managedResourceSecret.Data["validatingwebhookconfiguration____gardener-seed-admission-controller.yaml"] = []byte(validatingWebhookConfigurationYAMLK8sGreaterEqual115) gomock.InOrder( c.EXPECT().Get(ctx, kutil.Key(namespace, managedResourceSecretName), gomock.AssignableToTypeOf(&corev1.Secret{})), diff --git a/pkg/operation/seed/scheduler/gardener_kube_scheduler.go b/pkg/operation/seed/scheduler/gardener_kube_scheduler.go index e041e117124f..b9989a31ddc7 100644 --- a/pkg/operation/seed/scheduler/gardener_kube_scheduler.go +++ b/pkg/operation/seed/scheduler/gardener_kube_scheduler.go @@ -19,17 +19,9 @@ import ( "fmt" "time" - v1beta1constants "github.com/gardener/gardener/pkg/apis/core/v1beta1/constants" - "github.com/gardener/gardener/pkg/client/kubernetes" - "github.com/gardener/gardener/pkg/operation/botanist/component" - "github.com/gardener/gardener/pkg/operation/common" - "github.com/gardener/gardener/pkg/operation/seed/scheduler/configurator" - "github.com/gardener/gardener/pkg/utils" - "github.com/gardener/gardener/pkg/utils/imagevector" - kutil "github.com/gardener/gardener/pkg/utils/kubernetes" - "github.com/gardener/gardener/pkg/utils/managedresources" - "github.com/pkg/errors" + admissionv1 "k8s.io/api/admission/v1" + admissionv1beta1 "k8s.io/api/admission/v1beta1" admissionregistrationv1beta1 "k8s.io/api/admissionregistration/v1beta1" appsv1 "k8s.io/api/apps/v1" autoscalingv1 "k8s.io/api/autoscaling/v1" @@ -44,6 +36,16 @@ import ( "k8s.io/utils/pointer" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + v1beta1constants "github.com/gardener/gardener/pkg/apis/core/v1beta1/constants" + "github.com/gardener/gardener/pkg/client/kubernetes" + "github.com/gardener/gardener/pkg/operation/botanist/component" + "github.com/gardener/gardener/pkg/operation/common" + "github.com/gardener/gardener/pkg/operation/seed/scheduler/configurator" + "github.com/gardener/gardener/pkg/utils" + "github.com/gardener/gardener/pkg/utils/imagevector" + kutil "github.com/gardener/gardener/pkg/utils/kubernetes" + "github.com/gardener/gardener/pkg/utils/managedresources" ) const ( @@ -355,7 +357,7 @@ func (k *kubeScheduler) Deploy(ctx context.Context) error { ObjectSelector: &metav1.LabelSelector{}, SideEffects: &sideEffects, TimeoutSeconds: &timeout, - AdmissionReviewVersions: []string{admissionregistrationv1beta1.SchemeGroupVersion.Version}, + AdmissionReviewVersions: []string{admissionv1beta1.SchemeGroupVersion.Version, admissionv1.SchemeGroupVersion.Version}, ReinvocationPolicy: &revocationPolicy, }}, } diff --git a/pkg/operation/seed/scheduler/gardener_kube_scheduler_test.go b/pkg/operation/seed/scheduler/gardener_kube_scheduler_test.go index a612b6fffac4..bd265318839a 100644 --- a/pkg/operation/seed/scheduler/gardener_kube_scheduler_test.go +++ b/pkg/operation/seed/scheduler/gardener_kube_scheduler_test.go @@ -18,12 +18,10 @@ import ( "context" "fmt" - "github.com/gardener/gardener/pkg/operation/botanist/component" - . "github.com/gardener/gardener/pkg/operation/seed/scheduler" - "github.com/gardener/gardener/pkg/utils/imagevector" - . "github.com/gardener/gardener/pkg/utils/test/matchers" - resourcesv1alpha1 "github.com/gardener/gardener-resource-manager/pkg/apis/resources/v1alpha1" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + admissionv1 "k8s.io/api/admission/v1" admissionregistrationv1beta1 "k8s.io/api/admissionregistration/v1beta1" appsv1 "k8s.io/api/apps/v1" autoscalingv1 "k8s.io/api/autoscaling/v1" @@ -41,8 +39,10 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" + "github.com/gardener/gardener/pkg/operation/botanist/component" + . "github.com/gardener/gardener/pkg/operation/seed/scheduler" + "github.com/gardener/gardener/pkg/utils/imagevector" + . "github.com/gardener/gardener/pkg/utils/test/matchers" ) var _ = Describe("New", func() { @@ -372,7 +372,7 @@ var _ = Describe("New", func() { }, Webhooks: []admissionregistrationv1beta1.MutatingWebhook{{ Name: "kube-scheduler.scheduling.gardener.cloud", - AdmissionReviewVersions: []string{admissionregistrationv1beta1.SchemeGroupVersion.Version}, + AdmissionReviewVersions: []string{admissionregistrationv1beta1.SchemeGroupVersion.Version, admissionv1.SchemeGroupVersion.Version}, ClientConfig: *expectedWebhookClientConfig, FailurePolicy: &failurePolicy, MatchPolicy: &matchPolicy, diff --git a/pkg/seedadmission/constants.go b/pkg/seedadmission/constants.go index c02ada58d267..88007ca3b706 100644 --- a/pkg/seedadmission/constants.go +++ b/pkg/seedadmission/constants.go @@ -21,4 +21,8 @@ const ( // GardenerShootControlPlaneSchedulerWebhookPath is the path of the webhook server // that sets the "gardener-shoot-controlplane-scheduler" schedulerName for Pods. GardenerShootControlPlaneSchedulerWebhookPath = "/webhooks/default-pod-scheduler-name/" + GardenerShootControlPlaneSchedulerName + // ExtensionDeletionProtectionWebhookPath is the path of the webhook endpoint + // that validates DELETE requests for extension CRDs and extension + // resources, that are marked for deletion protection (`gardener.cloud/deletion-protected`). + ExtensionDeletionProtectionWebhookPath = "/webhooks/validate-extension-crd-deletion" ) diff --git a/pkg/seedadmission/extension_crds.go b/pkg/seedadmission/extension_crds.go index c9ef27670100..f192dcacb584 100644 --- a/pkg/seedadmission/extension_crds.go +++ b/pkg/seedadmission/extension_crds.go @@ -22,7 +22,7 @@ import ( "strings" "time" - "github.com/sirupsen/logrus" + "github.com/go-logr/logr" admissionv1 "k8s.io/api/admission/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" @@ -37,36 +37,37 @@ import ( "github.com/gardener/gardener/pkg/operation/common" ) -// NewExtensionDeletionProtection creates a new handler validating DELETE requests for extension CRDs and extension +var _ admission.Handler = &ExtensionDeletionProtection{} + +// ExtensionDeletionProtection is a webhook handler validating DELETE requests for extension CRDs and extension // resources, that are marked for deletion protection (`gardener.cloud/deletion-protected`). -// TODO: remove this constructor in favor of InjectLogger once we have switched to logr. -func NewExtensionDeletionProtection(logger logrus.FieldLogger) admission.Handler { - return &extensionDeletionProtection{ - logger: logger, - } +type ExtensionDeletionProtection struct { + logger logr.Logger + reader client.Reader + decoder *admission.Decoder } -type extensionDeletionProtection struct { - logger logrus.FieldLogger - client client.Client - decoder *admission.Decoder +// InjectLogger inject a logger into the handler. +func (h *ExtensionDeletionProtection) InjectLogger(l logr.Logger) error { + h.logger = l + return nil } -// InjectClient injects a client. -func (h *extensionDeletionProtection) InjectClient(c client.Client) error { - h.client = c +// InjectAPIReader injects a reader into the handler. +func (h *ExtensionDeletionProtection) InjectAPIReader(reader client.Reader) error { + h.reader = reader return nil } // InjectDecoder injects a decoder capable of decoding objects included in admission requests. -func (h *extensionDeletionProtection) InjectDecoder(d *admission.Decoder) error { +func (h *ExtensionDeletionProtection) InjectDecoder(d *admission.Decoder) error { h.decoder = d return nil } // Handle implements the webhook handler for extension deletion protection. -func (h *extensionDeletionProtection) Handle(ctx context.Context, request admission.Request) admission.Response { - ctx, cancel := context.WithTimeout(ctx, 30*time.Second) +func (h *ExtensionDeletionProtection) Handle(ctx context.Context, request admission.Request) admission.Response { + ctx, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() // If the request does not indicate the correct operation (DELETE) we allow the review without further doing. @@ -93,7 +94,7 @@ func (h *extensionDeletionProtection) Handle(ctx context.Context, request admiss return admission.Allowed("resource is not deletion-protected") } - obj, err := getRequestObject(ctx, h.client, h.decoder, request) + obj, err := getRequestObject(ctx, h.reader, h.decoder, request) if apierrors.IsNotFound(err) { return admission.Allowed("object was not found") } @@ -108,14 +109,14 @@ func (h *extensionDeletionProtection) Handle(ctx context.Context, request admiss operation = "DELETE" } - entryLogger := h.logger. - WithField("resource", fmt.Sprintf("%s/%s/%s", request.Kind.Group, request.Kind.Version, request.Kind.Kind)). - WithField("operation", operation). - WithField("namespace", request.Namespace) + log := h.logger. + WithValues("resource", request.Resource). + WithValues("operation", operation). + WithValues("namespace", request.Namespace) - entryLogger.Info("Handling request") + log.Info("Handling request") - if err := admitObjectDeletion(entryLogger, obj, request.Kind.Kind); err != nil { + if err := admitObjectDeletion(log, obj, request.Kind.Kind); err != nil { return admission.Denied(err.Error()) } return admission.Allowed("") @@ -123,49 +124,49 @@ func (h *extensionDeletionProtection) Handle(ctx context.Context, request admiss // admitObjectDeletion checks if the object deletion is confirmed. If the given object is a list of objects then it // performs the check for every single object. -func admitObjectDeletion(logger logrus.FieldLogger, obj runtime.Object, kind string) error { +func admitObjectDeletion(log logr.Logger, obj runtime.Object, kind string) error { if strings.HasSuffix(obj.GetObjectKind().GroupVersionKind().Kind, "List") { return meta.EachListItem(obj, func(o runtime.Object) error { - return checkIfObjectDeletionIsConfirmed(logger, o, kind) + return checkIfObjectDeletionIsConfirmed(log, o, kind) }) } - return checkIfObjectDeletionIsConfirmed(logger, obj, kind) + return checkIfObjectDeletionIsConfirmed(log, obj, kind) } // checkIfObjectDeletionIsConfirmed checks if the object was annotated with the deletion confirmation. If it is a custom // resource definition then it is only considered if the CRD has the deletion protection label. -func checkIfObjectDeletionIsConfirmed(logger logrus.FieldLogger, object runtime.Object, kind string) error { +func checkIfObjectDeletionIsConfirmed(log logr.Logger, object runtime.Object, kind string) error { obj, ok := object.(client.Object) if !ok { return fmt.Errorf("%T does not implement client.Object", object) } - logger = logger.WithField("name", obj.GetName()) + log = log.WithValues("name", obj.GetName()) - if kind == "CustomResourceDefinition" && !crdMustBeConsidered(logger, obj.GetLabels()) { + if kind == "CustomResourceDefinition" && !crdMustBeConsidered(log, obj.GetLabels()) { return nil } if err := common.CheckIfDeletionIsConfirmed(obj); err != nil { - logger.Info("Deletion is not confirmed - preventing deletion") + log.Info("Deletion is not confirmed - preventing deletion") return err } - logger.Info("Deletion is confirmed - allowing deletion") + log.Info("Deletion is confirmed - allowing deletion") return nil } // TODO: This function can be removed once the minimum seed Kubernetes version is bumped to >= 1.15. In 1.15, webhook // configurations may use object selectors, i.e., we can get rid of this custom filtering. -func crdMustBeConsidered(logger logrus.FieldLogger, labels map[string]string) bool { +func crdMustBeConsidered(log logr.Logger, labels map[string]string) bool { val, ok := labels[common.GardenerDeletionProtected] if !ok { - logger.Infof("Ignoring CRD because it has no %s label - allowing deletion", common.GardenerDeletionProtected) + log.Info("Ignoring CRD because it has no " + common.GardenerDeletionProtected + " label - allowing deletion") return false } if ok, _ := strconv.ParseBool(val); !ok { - logger.Infof("Admitting CRD deletion because %s label value is not true - allowing deletion", common.GardenerDeletionProtected) + log.Info("Admitting CRD deletion because " + common.GardenerDeletionProtected + " label value is not true - allowing deletion") return false } diff --git a/pkg/seedadmission/extension_crds_test.go b/pkg/seedadmission/extension_crds_test.go index a32e5753523b..52e8dd6b2730 100644 --- a/pkg/seedadmission/extension_crds_test.go +++ b/pkg/seedadmission/extension_crds_test.go @@ -20,36 +20,39 @@ import ( "errors" "fmt" "net/http" + "path/filepath" + "time" "github.com/golang/mock/gomock" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" admissionv1 "k8s.io/api/admission/v1" + corev1 "k8s.io/api/core/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" 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" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/runtime/inject" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" "github.com/gardener/gardener/pkg/apis/core" extensionsv1alpha1 "github.com/gardener/gardener/pkg/apis/extensions/v1alpha1" "github.com/gardener/gardener/pkg/client/kubernetes" - gardenlogger "github.com/gardener/gardener/pkg/logger" mockclient "github.com/gardener/gardener/pkg/mock/controller-runtime/client" "github.com/gardener/gardener/pkg/operation/common" . "github.com/gardener/gardener/pkg/seedadmission" kutil "github.com/gardener/gardener/pkg/utils/kubernetes" + "github.com/gardener/gardener/pkg/utils/test" ) -var _ = Describe("Extension CRDs", func() { +var _ = Describe("Extension Deletion Protection", func() { Describe("#ValidateExtensionDeletion", func() { var ( - ctx = context.Background() - logger = gardenlogger.NewNopLogger() request admission.Request decoder *admission.Decoder @@ -70,8 +73,9 @@ var _ = Describe("Extension CRDs", func() { decoder, err = admission.NewDecoder(kubernetes.SeedScheme) Expect(err).NotTo(HaveOccurred()) - validator = NewExtensionDeletionProtection(logger) - Expect(inject.ClientInto(c, validator)).To(BeTrue()) + validator = &ExtensionDeletionProtection{} + Expect(inject.LoggerInto(logger, validator)).To(BeTrue()) + Expect(inject.APIReaderInto(c, validator)).To(BeTrue()) Expect(admission.InjectDecoderInto(decoder, validator)).To(BeTrue()) }) @@ -103,6 +107,10 @@ var _ = Describe("Extension CRDs", func() { deletionConfirmedAnnotations = map[string]string{common.ConfirmationDeletion: "true"} ) + resourceToId := func(resource metav1.GroupVersionResource) string { + return fmt.Sprintf("%s/%s/%s", resource.Group, resource.Version, resource.Resource) + } + testDeletionUnconfirmed := func(ctx context.Context, request admission.Request, resource metav1.GroupVersionResource) { request.Resource = resource expectDenied(validator.Handle(ctx, request), ContainSubstring("annotation to delete"), resourceToId(resource)) @@ -113,6 +121,44 @@ var _ = Describe("Extension CRDs", func() { expectAllowed(validator.Handle(ctx, request), Equal(""), resourceToId(resource)) } + type unstructuredInterface interface { + SetAPIVersion(string) + SetKind(string) + } + + prepareRequestAndObjectWithResource := func(request *admission.Request, obj unstructuredInterface, resource metav1.GroupVersionResource) { + request.Kind.Group = resource.Group + request.Kind.Version = resource.Version + obj.SetAPIVersion(request.Kind.Group + "/" + request.Kind.Version) + obj.SetKind(request.Kind.Kind) + } + + prepareObjectWithLabelsAnnotations := func(obj runtime.Object, resource metav1.GroupVersionResource, labels, annotations map[string]string) { + switch obj := obj.(type) { + case *unstructured.Unstructured: + obj.SetAPIVersion(fmt.Sprintf("%s/%s", resource.Group, resource.Version)) + obj.SetKind(resource.Resource) + obj.SetLabels(labels) + obj.SetAnnotations(annotations) + case *unstructured.UnstructuredList: + o := &unstructured.Unstructured{} + o.SetAPIVersion(fmt.Sprintf("%s/%s", resource.Group, resource.Version)) + o.SetKind(resource.Resource) + o.SetLabels(labels) + o.SetAnnotations(annotations) + obj.Items = []unstructured.Unstructured{*o} + } + } + + getObjectJSONWithLabelsAnnotations := func(obj *unstructured.Unstructured, resource metav1.GroupVersionResource, labels, annotations map[string]string) []byte { + prepareObjectWithLabelsAnnotations(obj, resource, labels, annotations) + + objJSON, err := json.Marshal(obj) + Expect(err).NotTo(HaveOccurred()) + + return objJSON + } + Context("ignored requests", func() { It("should ignore other operations than DELETE", func() { request.Operation = admissionv1.Create @@ -529,46 +575,170 @@ var _ = Describe("Extension CRDs", func() { }) }) }) -}) -func resourceToId(resource metav1.GroupVersionResource) string { - return fmt.Sprintf("%s/%s/%s", resource.Group, resource.Version, resource.Resource) -} - -type unstructuredInterface interface { - SetAPIVersion(string) - SetKind(string) -} - -func prepareRequestAndObjectWithResource(request *admission.Request, obj unstructuredInterface, resource metav1.GroupVersionResource) { - request.Kind.Group = resource.Group - request.Kind.Version = resource.Version - obj.SetAPIVersion(request.Kind.Group + "/" + request.Kind.Version) - obj.SetKind(request.Kind.Kind) -} - -func prepareObjectWithLabelsAnnotations(obj runtime.Object, resource metav1.GroupVersionResource, labels, annotations map[string]string) { - switch obj := obj.(type) { - case *unstructured.Unstructured: - obj.SetAPIVersion(fmt.Sprintf("%s/%s", resource.Group, resource.Version)) - obj.SetKind(resource.Resource) - obj.SetLabels(labels) - obj.SetAnnotations(annotations) - case *unstructured.UnstructuredList: - o := &unstructured.Unstructured{} - o.SetAPIVersion(fmt.Sprintf("%s/%s", resource.Group, resource.Version)) - o.SetKind(resource.Resource) - o.SetLabels(labels) - o.SetAnnotations(annotations) - obj.Items = []unstructured.Unstructured{*o} - } -} - -func getObjectJSONWithLabelsAnnotations(obj *unstructured.Unstructured, resource metav1.GroupVersionResource, labels, annotations map[string]string) []byte { - prepareObjectWithLabelsAnnotations(obj, resource, labels, annotations) - - objJSON, err := json.Marshal(obj) - Expect(err).NotTo(HaveOccurred()) - - return objJSON -} + Describe("Integration Test", func() { + var ( + c client.Client + namespace = "shoot--foo--bar" + + crdObjects = []client.Object{ + &apiextensionsv1.CustomResourceDefinition{ObjectMeta: metav1.ObjectMeta{Name: "backupbuckets.extensions.gardener.cloud"}}, + &apiextensionsv1.CustomResourceDefinition{ObjectMeta: metav1.ObjectMeta{Name: "backupentries.extensions.gardener.cloud"}}, + &apiextensionsv1.CustomResourceDefinition{ObjectMeta: metav1.ObjectMeta{Name: "containerruntimes.extensions.gardener.cloud"}}, + &apiextensionsv1.CustomResourceDefinition{ObjectMeta: metav1.ObjectMeta{Name: "controlplanes.extensions.gardener.cloud"}}, + &apiextensionsv1.CustomResourceDefinition{ObjectMeta: metav1.ObjectMeta{Name: "extensions.extensions.gardener.cloud"}}, + &apiextensionsv1.CustomResourceDefinition{ObjectMeta: metav1.ObjectMeta{Name: "infrastructures.extensions.gardener.cloud"}}, + &apiextensionsv1.CustomResourceDefinition{ObjectMeta: metav1.ObjectMeta{Name: "networks.extensions.gardener.cloud"}}, + &apiextensionsv1.CustomResourceDefinition{ObjectMeta: metav1.ObjectMeta{Name: "operatingsystemconfigs.extensions.gardener.cloud"}}, + &apiextensionsv1.CustomResourceDefinition{ObjectMeta: metav1.ObjectMeta{Name: "workers.extensions.gardener.cloud"}}, + } + extensionObjects = []client.Object{ + &extensionsv1alpha1.BackupBucket{ObjectMeta: metav1.ObjectMeta{Name: "foo"}}, + &extensionsv1alpha1.BackupEntry{ObjectMeta: metav1.ObjectMeta{Name: namespace}}, + &extensionsv1alpha1.ContainerRuntime{ObjectMeta: metav1.ObjectMeta{Namespace: namespace, Name: "foo"}}, + &extensionsv1alpha1.ControlPlane{ObjectMeta: metav1.ObjectMeta{Namespace: namespace, Name: "foo"}}, + &extensionsv1alpha1.Extension{ObjectMeta: metav1.ObjectMeta{Namespace: namespace, Name: "foo"}}, + &extensionsv1alpha1.Infrastructure{ObjectMeta: metav1.ObjectMeta{Namespace: namespace, Name: "foo"}}, + &extensionsv1alpha1.Network{ObjectMeta: metav1.ObjectMeta{Namespace: namespace, Name: "foo"}}, + &extensionsv1alpha1.OperatingSystemConfig{ObjectMeta: metav1.ObjectMeta{Namespace: namespace, Name: "foo"}}, + &extensionsv1alpha1.Worker{ObjectMeta: metav1.ObjectMeta{Namespace: namespace, Name: "foo"}}, + } + podObject = &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: namespace}, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Name: "foo", + Image: "foo:latest", + }}, + }, + } + + deletionUnprotectedLabels = map[string]string{common.GardenerDeletionProtected: "false"} + deletionConfirmedAnnotations = map[string]string{common.ConfirmationDeletion: "true"} + ) + + BeforeEach(func() { + c, err = client.New(restConfig, client.Options{Scheme: kubernetes.SeedScheme}) + Expect(err).NotTo(HaveOccurred()) + + ns := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}} + if err := c.Create(ctx, ns); err != nil && !apierrors.IsAlreadyExists(err) { + Expect(err).NotTo(HaveOccurred()) + } + + By("applying CRDs") + applier, err := kubernetes.NewChartApplierForConfig(restConfig) + Expect(err).NotTo(HaveOccurred()) + repoRoot := filepath.Join("..", "..") + Expect(applier.Apply(ctx, filepath.Join(repoRoot, "charts", "seed-bootstrap", "charts", "extensions"), "extensions", "")).To(Succeed()) + + Eventually(func() bool { + for _, object := range extensionObjects { + err := c.Get(ctx, client.ObjectKeyFromObject(object), object) + if meta.IsNoMatchError(err) { + return false + } + } + return true + }, 1*time.Second, 50*time.Millisecond).Should(BeTrue()) + }) + + objectID := func(obj client.Object) string { + return fmt.Sprintf("%T/%s", obj, client.ObjectKeyFromObject(obj)) + } + + testDeletionUnconfirmed := func(ctx context.Context, obj client.Object) { + Eventually(func() string { + err := c.Delete(ctx, obj) + return string(apierrors.ReasonForError(err)) + }, 1*time.Second, 50*time.Millisecond).Should(ContainSubstring("annotation to delete"), objectID(obj)) + } + + testDeletionConfirmed := func(ctx context.Context, obj client.Object) { + Eventually(func() error { + return c.Delete(ctx, obj) + }, 1*time.Second, 50*time.Millisecond).ShouldNot(HaveOccurred(), objectID(obj)) + Eventually(func() bool { + err := c.Get(ctx, client.ObjectKeyFromObject(obj), obj) + return apierrors.IsNotFound(err) || meta.IsNoMatchError(err) + }, 1*time.Second, 50*time.Millisecond).Should(BeTrue(), objectID(obj)) + } + + Context("custom resource definitions", func() { + It("should admit the deletion because CRD has no protection label", func() { + for _, obj := range crdObjects { + // patch out default gardener.cloud/deletion-protected=true label + _, err := controllerutil.CreateOrPatch(ctx, c, obj, func() error { + obj.SetLabels(nil) + return nil + }) + Expect(err).NotTo(HaveOccurred(), objectID(obj)) + testDeletionConfirmed(ctx, obj) + } + }) + + It("should admit the deletion because CRD's protection label is not true", func() { + for _, obj := range crdObjects { + // override default gardener.cloud/deletion-protected=true label + _, err := controllerutil.CreateOrPatch(ctx, c, obj, func() error { + obj.SetLabels(deletionUnprotectedLabels) + return nil + }) + Expect(err).NotTo(HaveOccurred(), objectID(obj)) + testDeletionConfirmed(ctx, obj) + } + }) + + It("should prevent the deletion because CRD's protection label is true but deletion is not confirmed", func() { + // CRDs in seed-bootstrap chart should have gardener.cloud/deletion-protected=true label by default + for _, obj := range crdObjects { + testDeletionUnconfirmed(ctx, obj) + } + }) + + It("should admit the deletion because CRD's protection label is true and deletion is confirmed", func() { + // CRDs in seed-bootstrap chart should have gardener.cloud/deletion-protected=true label by default + for _, obj := range crdObjects { + _, err := controllerutil.CreateOrPatch(ctx, c, obj, func() error { + obj.SetAnnotations(deletionConfirmedAnnotations) + return nil + }) + Expect(err).NotTo(HaveOccurred(), objectID(obj)) + testDeletionConfirmed(ctx, obj) + } + }) + }) + + Context("extension resources", func() { + BeforeEach(func() { + By("creating extension test objects") + _, err := test.EnsureTestResources(ctx, c, "testdata") + Expect(err).NotTo(HaveOccurred()) + }) + + It("should prevent the deletion because deletion is not confirmed", func() { + for _, obj := range extensionObjects { + testDeletionUnconfirmed(ctx, obj) + } + }) + + It("should admit the deletion because deletion is confirmed", func() { + for _, obj := range extensionObjects { + _, err := controllerutil.CreateOrPatch(ctx, c, obj, func() error { + obj.SetAnnotations(deletionConfirmedAnnotations) + return nil + }) + Expect(err).NotTo(HaveOccurred(), objectID(obj)) + testDeletionConfirmed(ctx, obj) + } + }) + }) + + Context("other resources", func() { + It("should not block deletion of other resources", func() { + Expect(c.Create(ctx, podObject)).To(Succeed()) + testDeletionConfirmed(ctx, podObject) + }) + }) + }) +}) diff --git a/pkg/seedadmission/pod_scheduler_name_test.go b/pkg/seedadmission/pod_scheduler_name_test.go index 8b406eda4c33..83d12e44500b 100644 --- a/pkg/seedadmission/pod_scheduler_name_test.go +++ b/pkg/seedadmission/pod_scheduler_name_test.go @@ -21,7 +21,10 @@ import ( . "github.com/onsi/gomega" "gomodules.xyz/jsonpatch/v2" admissionv1 "k8s.io/api/admission/v1" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" . "github.com/gardener/gardener/pkg/seedadmission" @@ -69,4 +72,66 @@ var _ = Describe("Pod Scheduler Name", func() { }) }) }) + + Describe("Integration Test", func() { + var ( + c client.Client + + namespace = "shoot--foo--bar" + pod *corev1.Pod + ) + + BeforeEach(func() { + c, err = client.New(restConfig, client.Options{}) + Expect(err).NotTo(HaveOccurred()) + + ns := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}} + _, err = controllerutil.CreateOrPatch(ctx, c, ns, func() error { + ns.SetLabels(map[string]string{ + "gardener.cloud/role": "shoot", + }) + return nil + }) + Expect(err).NotTo(HaveOccurred()) + + pod = &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + Namespace: namespace, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Name: "test", + Image: "foo:latest", + }}, + }, + } + }) + + AfterEach(func() { + Expect(client.IgnoreNotFound(c.Delete(ctx, pod))).To(Succeed()) + }) + + Describe("ignored requests", func() { + BeforeEach(func() { + Expect(c.Create(ctx, pod)).To(Succeed()) + }) + + It("Update does nothing", func() { + expected := &corev1.Pod{} + Expect(c.Get(ctx, client.ObjectKeyFromObject(pod), expected)).To(Succeed()) + Expect(c.Update(ctx, pod)).To(Succeed()) + Expect(pod).To(Equal(expected)) + }) + It("Delete does nothing", func() { + Expect(c.Delete(ctx, pod)).To(Succeed()) + }) + }) + + It("should set .spec.schedulerName", func() { + Expect(c.Create(ctx, pod)).To(Succeed()) + Expect(c.Get(ctx, client.ObjectKeyFromObject(pod), pod)).To(Succeed()) + Expect(pod.Spec.SchedulerName).To(Equal("gardener-shoot-controlplane-scheduler")) + }) + }) }) diff --git a/pkg/seedadmission/seedadmission_suite_test.go b/pkg/seedadmission/seedadmission_suite_test.go index bd5c3ceb39fc..983fb2be37d2 100644 --- a/pkg/seedadmission/seedadmission_suite_test.go +++ b/pkg/seedadmission/seedadmission_suite_test.go @@ -15,14 +15,37 @@ package seedadmission_test import ( + "context" "net/http" "testing" + "github.com/go-logr/logr" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" gomegatypes "github.com/onsi/gomega/types" + "go.uber.org/zap/zapcore" "gomodules.xyz/jsonpatch/v2" + admissionv1beta1 "k8s.io/api/admission/v1beta1" + admissionregistrationv1beta1 "k8s.io/api/admissionregistration/v1beta1" + corev1 "k8s.io/api/core/v1" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/rest" + "k8s.io/utils/pointer" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + logf "sigs.k8s.io/controller-runtime/pkg/log" + logzap "sigs.k8s.io/controller-runtime/pkg/log/zap" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/webhook" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + "github.com/gardener/gardener/cmd/utils" + v1beta1constants "github.com/gardener/gardener/pkg/apis/core/v1beta1/constants" + extensionsv1alpha1 "github.com/gardener/gardener/pkg/apis/extensions/v1alpha1" + "github.com/gardener/gardener/pkg/seedadmission" + "github.com/gardener/gardener/test/framework" ) func TestSeedadmission(t *testing.T) { @@ -30,6 +53,156 @@ func TestSeedadmission(t *testing.T) { RunSpecs(t, "Seed Admission Suite") } +var ( + ctx context.Context + ctxCancel context.CancelFunc + err error + logger logr.Logger + testEnv *envtest.Environment + restConfig *rest.Config +) + +var _ = BeforeSuite(func() { + utils.DeduplicateWarnings() + ctx, ctxCancel = context.WithCancel(context.Background()) + + logger = logzap.New(logzap.UseDevMode(true), logzap.WriteTo(GinkgoWriter), logzap.Level(zapcore.Level(1))) + // enable manager and envtest logs + logf.SetLogger(logger) + + By("starting test environment") + testEnv = &envtest.Environment{ + WebhookInstallOptions: envtest.WebhookInstallOptions{ + ValidatingWebhooks: []client.Object{getValidatingWebhookConfig()}, + MutatingWebhooks: []client.Object{getMutatingWebhookConfig()}, + }, + } + restConfig, err = testEnv.Start() + Expect(err).ToNot(HaveOccurred()) + Expect(restConfig).ToNot(BeNil()) + + By("setting up manager") + // setup manager in order to leverage dependency injection + mgr, err := manager.New(restConfig, manager.Options{ + Port: testEnv.WebhookInstallOptions.LocalServingPort, + Host: testEnv.WebhookInstallOptions.LocalServingHost, + CertDir: testEnv.WebhookInstallOptions.LocalServingCertDir, + }) + Expect(err).NotTo(HaveOccurred()) + + By("setting up webhook server") + server := mgr.GetWebhookServer() + server.Register(seedadmission.ExtensionDeletionProtectionWebhookPath, &webhook.Admission{Handler: &seedadmission.ExtensionDeletionProtection{}}) + server.Register(seedadmission.GardenerShootControlPlaneSchedulerWebhookPath, &webhook.Admission{Handler: admission.HandlerFunc(seedadmission.DefaultShootControlPlanePodsSchedulerName)}) + + go func() { + Expect(server.Start(ctx)).To(Succeed()) + }() +}) + +var _ = AfterSuite(func() { + By("running cleanup actions") + framework.RunCleanupActions() + + By("stopping manager") + ctxCancel() + + By("stopping test environment") + Expect(testEnv.Stop()).To(Succeed()) +}) + +func getValidatingWebhookConfig() *admissionregistrationv1beta1.ValidatingWebhookConfiguration { + return &admissionregistrationv1beta1.ValidatingWebhookConfiguration{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "admissionregistration.k8s.io/v1beta1", + Kind: "ValidatingWebhookConfiguration", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "gardener-seed-admission-controller", + }, + Webhooks: []admissionregistrationv1beta1.ValidatingWebhook{{ + Name: "crds.seed.admission.core.gardener.cloud", + Rules: []admissionregistrationv1beta1.RuleWithOperations{{ + Rule: admissionregistrationv1beta1.Rule{ + APIGroups: []string{apiextensionsv1.GroupName}, + APIVersions: []string{apiextensionsv1beta1.SchemeGroupVersion.Version, apiextensionsv1.SchemeGroupVersion.Version}, + Resources: []string{"customresourcedefinitions"}, + }, + Operations: []admissionregistrationv1beta1.OperationType{admissionregistrationv1beta1.Delete}, + }}, + ClientConfig: admissionregistrationv1beta1.WebhookClientConfig{ + Service: &admissionregistrationv1beta1.ServiceReference{ + Path: pointer.StringPtr(seedadmission.ExtensionDeletionProtectionWebhookPath), + }, + }, + AdmissionReviewVersions: []string{admissionv1beta1.SchemeGroupVersion.Version}, + }, { + Name: "crs.seed.admission.core.gardener.cloud", + Rules: []admissionregistrationv1beta1.RuleWithOperations{{ + Rule: admissionregistrationv1beta1.Rule{ + APIGroups: []string{extensionsv1alpha1.SchemeGroupVersion.Group}, + APIVersions: []string{extensionsv1alpha1.SchemeGroupVersion.Version}, + Resources: []string{ + "backupbuckets", + "backupentries", + "containerruntimes", + "controlplanes", + "extensions", + "infrastructures", + "networks", + "operatingsystemconfigs", + "workers", + }, + }, + Operations: []admissionregistrationv1beta1.OperationType{admissionregistrationv1beta1.Delete}, + }}, + ClientConfig: admissionregistrationv1beta1.WebhookClientConfig{ + Service: &admissionregistrationv1beta1.ServiceReference{ + Path: pointer.StringPtr(seedadmission.ExtensionDeletionProtectionWebhookPath), + }, + }, + AdmissionReviewVersions: []string{admissionv1beta1.SchemeGroupVersion.Version}, + }}, + } +} + +func getMutatingWebhookConfig() *admissionregistrationv1beta1.MutatingWebhookConfiguration { + scope := admissionregistrationv1beta1.NamespacedScope + + return &admissionregistrationv1beta1.MutatingWebhookConfiguration{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "admissionregistration.k8s.io/v1beta1", + Kind: "MutatingWebhookConfiguration", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "gardener-seed-admission-controller", + }, + Webhooks: []admissionregistrationv1beta1.MutatingWebhook{{ + Name: "kube-scheduler.scheduling.gardener.cloud", + Rules: []admissionregistrationv1beta1.RuleWithOperations{{ + Operations: []admissionregistrationv1beta1.OperationType{admissionregistrationv1beta1.Create}, + Rule: admissionregistrationv1beta1.Rule{ + APIGroups: []string{corev1.GroupName}, + APIVersions: []string{corev1.SchemeGroupVersion.Version}, + Scope: &scope, + Resources: []string{"pods"}, + }, + }}, + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + v1beta1constants.GardenRole: v1beta1constants.GardenRoleShoot, + }, + }, + ClientConfig: admissionregistrationv1beta1.WebhookClientConfig{ + Service: &admissionregistrationv1beta1.ServiceReference{ + Path: pointer.StringPtr(seedadmission.GardenerShootControlPlaneSchedulerWebhookPath), + }, + }, + AdmissionReviewVersions: []string{admissionv1beta1.SchemeGroupVersion.Version}, + }}, + } +} + func expectAllowed(response admission.Response, reason gomegatypes.GomegaMatcher, optionalDescription ...interface{}) { Expect(response.Allowed).To(BeTrue(), optionalDescription...) Expect(string(response.Result.Reason)).To(reason, optionalDescription...) diff --git a/pkg/seedadmission/testdata/backupbucket.yaml b/pkg/seedadmission/testdata/backupbucket.yaml new file mode 100644 index 000000000000..7c1f7990a77a --- /dev/null +++ b/pkg/seedadmission/testdata/backupbucket.yaml @@ -0,0 +1,10 @@ +apiVersion: extensions.gardener.cloud/v1alpha1 +kind: BackupBucket +metadata: + name: foo +spec: + region: eu-west1 + secretRef: + name: bucket-foo + namespace: garden + type: gcp diff --git a/pkg/seedadmission/testdata/backupentry.yaml b/pkg/seedadmission/testdata/backupentry.yaml new file mode 100644 index 000000000000..d82d44429c5b --- /dev/null +++ b/pkg/seedadmission/testdata/backupentry.yaml @@ -0,0 +1,11 @@ +apiVersion: extensions.gardener.cloud/v1alpha1 +kind: BackupEntry +metadata: + name: shoot--foo--bar +spec: + bucketName: foo + region: eu-west1 + secretRef: + name: entry-shoot--foo--bar + namespace: garden + type: gcp diff --git a/pkg/seedadmission/testdata/containerruntime.yaml b/pkg/seedadmission/testdata/containerruntime.yaml new file mode 100644 index 000000000000..71755e57f482 --- /dev/null +++ b/pkg/seedadmission/testdata/containerruntime.yaml @@ -0,0 +1,13 @@ +apiVersion: extensions.gardener.cloud/v1alpha1 +kind: ContainerRuntime +metadata: + name: foo + namespace: shoot--foo--bar +spec: + binaryPath: /var/bin/containerruntimes + type: gvisor + workerPool: + name: worker-cpu + selector: + matchLabels: + worker.gardener.cloud/pool: worker-cpu diff --git a/pkg/seedadmission/testdata/controlplane.yaml b/pkg/seedadmission/testdata/controlplane.yaml new file mode 100644 index 000000000000..963eaa5f3992 --- /dev/null +++ b/pkg/seedadmission/testdata/controlplane.yaml @@ -0,0 +1,28 @@ +apiVersion: extensions.gardener.cloud/v1alpha1 +kind: ControlPlane +metadata: + name: foo + namespace: shoot--foo--bar +spec: + infrastructureProviderStatus: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: InfrastructureStatus + networks: + subnets: + - name: shoot--foo--bar-nodes + purpose: nodes + vpc: + cloudRouter: + name: shoot--foo--bar-cloud-router + name: shoot--foo--bar + serviceAccountEmail: shoot--foo--bar@my-gardener-project.iam.gserviceaccount.com + providerConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: ControlPlaneConfig + zone: europe-west1-b + purpose: normal + region: europe-west1 + secretRef: + name: cloudprovider + namespace: shoot--foo--bar + type: gcp diff --git a/pkg/seedadmission/testdata/extension.yaml b/pkg/seedadmission/testdata/extension.yaml new file mode 100644 index 000000000000..38c91bb62750 --- /dev/null +++ b/pkg/seedadmission/testdata/extension.yaml @@ -0,0 +1,7 @@ +apiVersion: extensions.gardener.cloud/v1alpha1 +kind: Extension +metadata: + name: foo + namespace: shoot--foo--bar +spec: + type: shoot-dns-service diff --git a/pkg/seedadmission/testdata/infra.yaml b/pkg/seedadmission/testdata/infra.yaml new file mode 100644 index 000000000000..497cc12b742a --- /dev/null +++ b/pkg/seedadmission/testdata/infra.yaml @@ -0,0 +1,17 @@ +apiVersion: extensions.gardener.cloud/v1alpha1 +kind: Infrastructure +metadata: + name: foo + namespace: shoot--foo--bar +spec: + providerConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: InfrastructureConfig + networks: + worker: 10.250.0.0/19 + region: europe-west1 + secretRef: + name: cloudprovider + namespace: shoot--foo--bar + sshPublicKey: MTIzNDU2Nzg= + type: gcp diff --git a/pkg/seedadmission/testdata/network.yaml b/pkg/seedadmission/testdata/network.yaml new file mode 100644 index 000000000000..cf2fa0e07ef7 --- /dev/null +++ b/pkg/seedadmission/testdata/network.yaml @@ -0,0 +1,9 @@ +apiVersion: extensions.gardener.cloud/v1alpha1 +kind: Network +metadata: + name: foo + namespace: shoot--foo--bar +spec: + podCIDR: 100.96.0.0/11 + serviceCIDR: 100.64.0.0/13 + type: calico diff --git a/pkg/seedadmission/testdata/osc.yaml b/pkg/seedadmission/testdata/osc.yaml new file mode 100644 index 000000000000..24f7709298a1 --- /dev/null +++ b/pkg/seedadmission/testdata/osc.yaml @@ -0,0 +1,8 @@ +apiVersion: extensions.gardener.cloud/v1alpha1 +kind: OperatingSystemConfig +metadata: + name: foo + namespace: shoot--foo--bar +spec: + purpose: provision + type: gardenlinux diff --git a/pkg/seedadmission/testdata/worker.yaml b/pkg/seedadmission/testdata/worker.yaml new file mode 100644 index 000000000000..4cd64b87a244 --- /dev/null +++ b/pkg/seedadmission/testdata/worker.yaml @@ -0,0 +1,45 @@ +apiVersion: extensions.gardener.cloud/v1alpha1 +kind: Worker +metadata: + name: foo + namespace: shoot--foo--bar +spec: + infrastructureProviderStatus: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: InfrastructureStatus + networks: + subnets: + - name: shoot--foo--bar-nodes + purpose: nodes + vpc: + cloudRouter: + name: shoot--foo--bar-cloud-router + name: shoot--foo--bar + serviceAccountEmail: shoot--foo--bar@my-gardener-project.iam.gserviceaccount.com + pools: + - labels: + node.kubernetes.io/role: node + worker.garden.sapcloud.io/group: cpu-worker + worker.gardener.cloud/pool: cpu-worker + worker.gardener.cloud/system-components: "true" + machineImage: + name: gardenlinux + version: 184.0.0 + machineType: n1-standard-2 + maxSurge: 1 + maxUnavailable: 0 + maximum: 4 + minimum: 1 + name: cpu-worker + userData: MTIzNDU2Nzg= + volume: + size: 20Gi + type: pd-standard + zones: + - europe-west1-b + region: europe-west1 + secretRef: + name: cloudprovider + namespace: shoot--foo--bar + sshPublicKey: MTIzNDU2Nzg= + type: gcp diff --git a/pkg/seedadmission/utils.go b/pkg/seedadmission/utils.go index f06a7126727b..0d281810b547 100644 --- a/pkg/seedadmission/utils.go +++ b/pkg/seedadmission/utils.go @@ -25,7 +25,7 @@ import ( kutil "github.com/gardener/gardener/pkg/utils/kubernetes" ) -func getRequestObject(ctx context.Context, c client.Client, decoder *admission.Decoder, request admission.Request) (runtime.Object, error) { +func getRequestObject(ctx context.Context, reader client.Reader, decoder *admission.Decoder, request admission.Request) (runtime.Object, error) { var ( obj runtime.Object err error @@ -45,7 +45,7 @@ func getRequestObject(ctx context.Context, c client.Client, decoder *admission.D o := &unstructured.UnstructuredList{} o.SetAPIVersion(request.Kind.Group + "/" + request.Kind.Version) o.SetKind(request.Kind.Kind + "List") - err = c.List(ctx, o, client.InNamespace(request.Namespace)) + err = reader.List(ctx, o, client.InNamespace(request.Namespace)) obj = o // Older Kubernetes versions don't provide the object neither in OldObject nor in the Object field. In this case @@ -54,7 +54,7 @@ func getRequestObject(ctx context.Context, c client.Client, decoder *admission.D o := &unstructured.Unstructured{} o.SetAPIVersion(request.Kind.Group + "/" + request.Kind.Version) o.SetKind(request.Kind.Kind) - err = c.Get(ctx, kutil.Key(request.Namespace, request.Name), o) + err = reader.Get(ctx, kutil.Key(request.Namespace, request.Name), o) obj = o } diff --git a/pkg/utils/test/test_resources.go b/pkg/utils/test/test_resources.go new file mode 100644 index 000000000000..165321d1941c --- /dev/null +++ b/pkg/utils/test/test_resources.go @@ -0,0 +1,141 @@ +// Copyright (c) 2021 SAP SE or an SAP affiliate company. All rights reserved. This file is licensed under the Apache Software License, v. 2 except as noted otherwise in the LICENSE file +// +// 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 test + +import ( + "bufio" + "bytes" + "context" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/serializer" + "k8s.io/apimachinery/pkg/util/sets" + k8syaml "k8s.io/apimachinery/pkg/util/yaml" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// EnsureTestResources reads test resources from path, applies them using the given client and returns the created +// objects. +func EnsureTestResources(ctx context.Context, c client.Client, path string) ([]client.Object, error) { + objects, err := ReadTestResources(c.Scheme(), path) + if err != nil { + return nil, fmt.Errorf("error decoding resources: %w", err) + } + + for _, obj := range objects { + current := obj.DeepCopyObject().(client.Object) + if err := c.Get(ctx, client.ObjectKeyFromObject(current), current); err != nil { + if !apierrors.IsNotFound(err) { + return nil, err + } + + // object doesn't exists, create it + if err := c.Create(ctx, obj); err != nil { + return nil, err + } + } else { + // object already exists, update it + if err := c.Patch(ctx, obj, client.MergeFromWithOptions(current, client.MergeFromWithOptimisticLock{})); err != nil { + return nil, err + } + } + } + return objects, nil +} + +// ReadTestResources reads test resources from path, decodes them using the given scheme and returns the parsed objects. +// Objects are values of the proper API types, if registered in the given scheme, and *unstructured.Unstructured +// otherwise. +func ReadTestResources(scheme *runtime.Scheme, path string) ([]client.Object, error) { + decoder := serializer.NewCodecFactory(scheme).UniversalDeserializer() + + var files []os.FileInfo + var err error + info, err := os.Stat(path) + if err != nil { + return nil, err + } + if !info.IsDir() { + path, files = filepath.Dir(path), []os.FileInfo{info} + } else { + if files, err = ioutil.ReadDir(path); err != nil { + return nil, err + } + } + + // file extensions that may contain Webhooks + resourceExtensions := sets.NewString(".json", ".yaml", ".yml") + + var objects []client.Object + for _, file := range files { + // Only parse allowlisted file types + if !resourceExtensions.Has(filepath.Ext(file.Name())) { + continue + } + + // Unmarshal Webhooks from file into structs + docs, err := readDocuments(filepath.Join(path, file.Name())) + if err != nil { + return nil, err + } + + for _, doc := range docs { + obj, err := runtime.Decode(decoder, doc) + if err != nil { + return nil, err + } + clientObj, ok := obj.(client.Object) + if !ok { + return nil, fmt.Errorf("%T does not implement client.Object", obj) + } + + objects = append(objects, clientObj) + } + } + return objects, nil + +} + +// readDocuments reads documents from file +func readDocuments(fp string) ([][]byte, error) { + b, err := ioutil.ReadFile(fp) + if err != nil { + return nil, err + } + + var docs [][]byte + reader := k8syaml.NewYAMLReader(bufio.NewReader(bytes.NewReader(b))) + for { + // Read document + doc, err := reader.Read() + if err != nil { + if err == io.EOF { + break + } + + return nil, err + } + + docs = append(docs, doc) + } + + return docs, nil +} diff --git a/vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/testcerts/certs.go b/vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/testcerts/certs.go deleted file mode 100644 index bb7b81f6ae72..000000000000 --- a/vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/testcerts/certs.go +++ /dev/null @@ -1,216 +0,0 @@ -/* -Copyright 2017 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -// This file was generated using openssl by the gencerts.sh script -// and holds raw certificates for the webhook tests. - -package testcerts - -var CAKey = []byte(`-----BEGIN RSA PRIVATE KEY----- -MIIEogIBAAKCAQEArqnW4K+UsmPzbSB7JYhN0HNsJNItjw/87SJxIjGqUttC+2ts -+Y+BddcQgO0EfzP68QJ6+H7itmzdqWPTfoZJiuy+twXxgFwMH2WCKB6I5CgnVHcD -acmdP7vOgr1GcyqIV16NWEnt6hxRNfVnerduLWWfVr0/wGY3ajw06FHEx7oL1Jfx -1lo4UPVdp39Pt5rOww66FP8etRaEhy2AHEtfHS4L4GxcJL/0n3w8UIfMqYjTKW9l -zuKXUurz9HEJ3N+JMItolf+3ohMp/xciiSBvHzFbfu/Zv2nXqWGcxqOmnM1L8ZLw -Q4c3ZRQ/n9tIacEZyy69VASAQcIjMdwIHcWZLwIDAQABAoIBADybeJGMu6dPIYfr -bm/upTnA43S/bcmnvZc3jVRVMYoAxXRiqXTLhBu03egu1pGhIuGAf9U8ikTM7/m4 -RwovZNONJPxzVoK47gfy/EAZoFyzRjp79bY+nI8iBx28ufZ6esb+a0OIm8LRwqhb -mGWvws6D5c9+aeHEVlRJwf4faY34ASEbw19QhOLfPCGp0wOy4MX3aIMaCfZ+iHYc -GAVaf44rWmTYKHEkLMABky9jGIXXJXROY/ggKWC1zXdPVChO40ECd1b2XGry6Ta/ -j+quFXDgI7b/ju/7jLnDCQjuC5G6E7X3n8KLZtzJrReiwpyeFo86GWc5E99umOyB -tPjwNikCgYEA1mbNAVg3mBOFcEAN0j5QKEy1hv16nVlGRwBye/0nG3mUHCs+UzC2 -fQyDfPcfXZERyDb4QJCJ4Y6VSVoo3Jn1okFPbz7y2eKdjo1N65T9HrK7G+QZXinH -/72LktfAWphdz2JKuSnrlJS8YupSx7pS+lpz+W+rcDpdYVJXoD6L6SMCgYEA0I1E -4h3MH5O46GJvMoGW4PH5FIi5nbWj69Y4/nRJCACNlLPJ+k3lcf0OTYcy9pllv5Ya -EV5n0qHAH7ACudKoB6YqvDsrZxfv8tlmWLBTFp5QQpBdlMWjgGSbJLbkxvt3rUfF -x/eQebvzSqp69R0/XqJ9fxWXvdtZoZYXNJxVPoUCgYAgA7W077FNegzA2C+4Jync -+qdYgt0eRchircRqk0CVr6/YDPT/gxSc05OGw3fhhtn65YpoSaztC1drXpUfa7Xs -BoiP+fxVYKtaL+tktBifztx1q7fGAcMlgu4mfSTx4jKP1wOFZqcQxqzisE6wGDhv -vbX3lx8oYO60q5D+EpjdtQKBgDM/A3YsrEP2ILG5vmlCvrh3vST2k+XVBHqnIUol -eOymdiPcKf1/tqnT7PfQCQ3fk8kIMU+jSw/O/07KCWFwCioXAtlOENQ8ZZHfKe8R -JNmh/UbeAqDUD+E014qmBoF+uWGzCT6h7rZ7IMVwLtacYT33366it67Hf7bdEsay -w5+hAoGABSgjlf9WsC7WzY6sZwZG25aBMGFw6vr3jawLiuNk3Hr+pGV/H7PEzSh+ -vBpvC0Vkp5Dg32asmME40LbYpMu2BV4E1wK17i+DZVUMezNO0mABykWecyPYdmxL -bJtLu4yaP84W433T5E6G7Im+x+KjXI7TRzpQZFQnVadmmpuurUY= ------END RSA PRIVATE KEY-----`) - -var CACert = []byte(`-----BEGIN CERTIFICATE----- -MIIDPTCCAiWgAwIBAgIJALTyMgMR6YygMA0GCSqGSIb3DQEBCwUAMDQxMjAwBgNV -BAMMKWdlbmVyaWNfd2ViaG9va19hZG1pc3Npb25fcGx1Z2luX3Rlc3RzX2NhMCAX -DTE3MTExNjAwMDUzOVoYDzIyOTEwOTAxMDAwNTM5WjA0MTIwMAYDVQQDDClnZW5l -cmljX3dlYmhvb2tfYWRtaXNzaW9uX3BsdWdpbl90ZXN0c19jYTCCASIwDQYJKoZI -hvcNAQEBBQADggEPADCCAQoCggEBAK6p1uCvlLJj820geyWITdBzbCTSLY8P/O0i -cSIxqlLbQvtrbPmPgXXXEIDtBH8z+vECevh+4rZs3alj036GSYrsvrcF8YBcDB9l -gigeiOQoJ1R3A2nJnT+7zoK9RnMqiFdejVhJ7eocUTX1Z3q3bi1ln1a9P8BmN2o8 -NOhRxMe6C9SX8dZaOFD1Xad/T7eazsMOuhT/HrUWhIctgBxLXx0uC+BsXCS/9J98 -PFCHzKmI0ylvZc7il1Lq8/RxCdzfiTCLaJX/t6ITKf8XIokgbx8xW37v2b9p16lh -nMajppzNS/GS8EOHN2UUP5/bSGnBGcsuvVQEgEHCIzHcCB3FmS8CAwEAAaNQME4w -HQYDVR0OBBYEFBsZKbynr9Iix+ud0FxQvMVIPZqOMB8GA1UdIwQYMBaAFBsZKbyn -r9Iix+ud0FxQvMVIPZqOMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEB -AJZXfcHyAsq5qr1UqhutrNlsW2u7kkAc+Ql5wZEdXIyjKKC+kOllWqKo5IPtmMIi -R5VCm1g3iFCUV6FdXNtfF7tWZqaHV58nkJYlDc2yZxQzaWQeu8U92w+Qr5H1ansL -FOGS6A4+rMj2EDEt+lCmsz+l5UD7nrhnyGMzeumASQ6cXPV1uB2LTc4IzsOYFKs9 -nt9SDH7tF+0bQwZ1YUrfMYJpNp6ETjpPKJVhq8/FGqwT+9egFbgjAhrEpPccFkXo -D7NLhM1JHUiqNQStDq9mDKLHKmp++UScrNc66b6egN1sIPBHyLu8ApzcF2YHuEYC -RGyWZ0sJtjzWjK7IU9RdmLI= ------END CERTIFICATE-----`) - -var BadCAKey = []byte(`-----BEGIN RSA PRIVATE KEY----- -MIIEpAIBAAKCAQEArgYoZAUDj1/dQ2G+dMib+b1m39ACK6wBt4zDjl8x5iUZZjDy -O1l/Hj6n3NUHphFsYZ5070ds8GwXANvYGFi3F6GFiRW/R5lcChmhxhY5JJtWquDp -amonNEjiAuiFf6u6CKeBvm8CgcH/Cbzc4XP1oJZiSaGOpmqGiHlLeL25y6ZR3X7D -jOPalSi9WHQN0m9DS7EMvyalySAwzJKlcOpSeBPdMTp1Ay1HShJiiNj8sdCRLpq7 -rwHypKS6mVNItsAnx+jc40d162dmFQg8FBm3M6d5QTrJBAfuvoLrlNCtssl11hOi -XDYJshTBVI9HAR/lzQiF+coZOHJwYuYXp7xYOwIDAQABAoIBAQChzSvkwxyqQ9Gw -AsNYReVv8IAj/HzoKgd2p7RzPWNhvoC9GSk/sViVwF/G3XM9HtoMcY37pAdQCs/g -hoeHK4UgvZcw/D1azuZapbZaPPNoa93LB08/F+/XlyQ83ACz0fEodsYVT5WfG8aL -QUSFgpGQfAJqv4GojUcEwPJBEvYat8I028fzYMlLJ9m45pQFzFsGKU36vs6esPkL -MKbVO1qI6CEVDtnLkIo2bE5vpo8w5C22HseFO+E+1VNaHIRK5TBBGGWVJVfUZ3bq -7LWngkaN9gspCauKkTozj2bl611lFRI7wbA32WV1eIYgNT+8jTESo/oHpsnpRSI7 -4UMp5GExAoGBAN7wkIS4zWGF2hijsorcPLBx3YOsBxW26Qhl/ck6a3lVGZHrDijc -u8hDhOWNDxSSqUwQiL7UAVcE7npw6XpZ37Obc//t+Hm/gGUOIGlB/Pl3g4h7pF7T -s2pXIMKvF0dfQGpmqgCytUz7Oho4LLbkywky7jykMc8IlVuTZEdKbdZHAoGBAMfU -nR+79gT8yIBruEX3VI71Vbce0Pn+3+G+PO12uUN6XqMb9YA+f5aS6AwP5EupERwn -YvkMkCNSYYkV+GU5b+N9Pn7xt33dnEqhrGPUrOLoIAl6qJ7jc78GZy67SCuIKrZZ -AN5qFQlRCENv28C+0Ne6rMX+8/JL1Mxo+0J/6QRtAoGAbNhs5q/Hbm7IfbEmkY9X -fhoJuai6yMpF2hjZoG6KXHHFCy4E+sRSVkNI1j5Zd4TnbUDBUtH1WYQJ3vPTui24 -/1rNds27u81YpX4RKvLRzQahzHf5V2bquOeTEhokNm915rz7EV4vEEe0JWr5wc3Q -p0wbbrYHr3oUWeKLWhcnqy8CgYEAh9XiHMFDIe7HSGxw7baLl0Xzxy++dEGp5CTR -+8VZeCIFlLCbuFpDlpI0BIcE891wEQhBAfRlQm1seagimoRpp2Tqh5Y92eQ7qout -yIq4HuIVbPwhBSit9Gsg1qZeD6FXD27+5TGNLTEVAepWofXTtuFhMpH1N34OoAi4 -y2Jxfh0CgYB4IrPUeBAZKiC6Lo6nwxo0rrsiHLJwXJojLwsUt0zG5vh3mI25KAB1 -a3ARRbiRKU/IX5I9ToclJ3h0a1nVr1kzV/E5f+5FgQ9swkTNEbM8gBsc5X9ZayjD -Hfv6+p7TH3bReXDKtpOUgso0dIy2anN6Ppu1wODtrFnUOJ9wkO4OSg== ------END RSA PRIVATE KEY-----`) - -var BadCACert = []byte(`-----BEGIN CERTIFICATE----- -MIIDPTCCAiWgAwIBAgIJAIaoBDrksTyaMA0GCSqGSIb3DQEBCwUAMDQxMjAwBgNV -BAMMKWdlbmVyaWNfd2ViaG9va19hZG1pc3Npb25fcGx1Z2luX3Rlc3RzX2NhMCAX -DTE3MTExNjAwMDUzOVoYDzIyOTEwOTAxMDAwNTM5WjA0MTIwMAYDVQQDDClnZW5l -cmljX3dlYmhvb2tfYWRtaXNzaW9uX3BsdWdpbl90ZXN0c19jYTCCASIwDQYJKoZI -hvcNAQEBBQADggEPADCCAQoCggEBAK4GKGQFA49f3UNhvnTIm/m9Zt/QAiusAbeM -w45fMeYlGWYw8jtZfx4+p9zVB6YRbGGedO9HbPBsFwDb2BhYtxehhYkVv0eZXAoZ -ocYWOSSbVqrg6WpqJzRI4gLohX+rugingb5vAoHB/wm83OFz9aCWYkmhjqZqhoh5 -S3i9ucumUd1+w4zj2pUovVh0DdJvQ0uxDL8mpckgMMySpXDqUngT3TE6dQMtR0oS -YojY/LHQkS6au68B8qSkuplTSLbAJ8fo3ONHdetnZhUIPBQZtzOneUE6yQQH7r6C -65TQrbLJddYTolw2CbIUwVSPRwEf5c0IhfnKGThycGLmF6e8WDsCAwEAAaNQME4w -HQYDVR0OBBYEFFFthspVCOb5fSkQ2BFCykech3RVMB8GA1UdIwQYMBaAFFFthspV -COb5fSkQ2BFCykech3RVMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEB -AEgGbcx1qhdi4lFNC0YRHJxjn3JPW6tr4qgDiusqMj9TF9/RohKOvLblq2kSB0x3 -pyDMkVv2rd5U4qtKruEQ1OgY3cB7hy6mt/ZhldF540Lli8j9N63LMRXwIu068j2W -WSiWV416LOZEcuid7mZjAsbG4xvaDg/yW1RBpA3XnwMSmr7Y+T6XkjzgT3WWiwOf -4ANc3ecsl53x/beb9YF+TjqmjmtGSgUW78UTAsGFFKmjJ/cStQUaMCEvS9Gun7hH -eLarZIVV5Ia/FziGHoi7Q44C66pXD437xmkR1ueExoKwXbBt4c5GeH1rJjUVnlyk -pMokZBC57nXx8krZVEu1SRA= ------END CERTIFICATE-----`) - -var ServerKey = []byte(`-----BEGIN RSA PRIVATE KEY----- -MIIEowIBAAKCAQEA13f50PPWuR/InxLIoJjHdNSG+jVUd25CY7ZL2J023X2BAY+1 -M6jkLR6C2nSFZnn58ubiB74/d1g/Fg1Twd419iR615A013f+qOoyFx3LFHxU1S6e -v22fgJ6ntK/+4QD5MwNgOwD8k1jN2WxHqNWn16IF4Tidbv8M9A35YHAdtYDYaOJC -kzjVztzRw1y6bKRakpMXxHylQyWmAKDJ2GSbRTbGtjr7Ji54WBfG43k94tO5X8K4 -VGbz/uxrKe1IFMHNOlrjR438dbOXusksx9EIqDA9a42J3qjr5NKSqzCIbgBFl6qu -45V3A7cdRI/sJ2G1aqlWIXh2fAQiaFQAEBrPfwIDAQABAoIBAAZbxgWCjJ2d8H+x -QDZtC8XI18redAWqPU9P++ECkrHqmDoBkalanJEwS1BDDATAKL4gTh9IX/sXoZT3 -A7e+5PzEitN9r/GD2wIFF0FTYcDTAnXgEFM52vEivXQ5lV3yd2gn+1kCaHG4typp -ZZv34iIc5+uDjjHOWQWCvA86f8XxX5EfYH+GkjfixTtN2xhWWlfi9vzYeESS4Jbt -tqfH0iEaZ1Bm/qvb8vFgKiuSTOoSpaf+ojAdtPtXDjf1bBtQQG+RSQkP59O/taLM -FCVuRrU8EtdB0+9anwmAP+O2UqjL5izA578lQtdIh13jHtGEgOcnfGNUphK11y9r -Mg5V28ECgYEA9fwI6Xy1Rb9b9irp4bU5Ec99QXa4x2bxld5cDdNOZWJQu9OnaIbg -kw/1SyUkZZCGMmibM/BiWGKWoDf8E+rn/ujGOtd70sR9U0A94XMPqEv7iHxhpZmD -rZuSz4/snYbOWCZQYXFoD/nqOwE7Atnz7yh+Jti0qxBQ9bmkb9o0QW8CgYEA4D3d -okzodg5QQ1y9L0J6jIC6YysoDedveYZMd4Un9bKlZEJev4OwiT4xXmSGBYq/7dzo -OJOvN6qgPfibr27mSB8NkAk6jL/VdJf3thWxNYmjF4E3paLJ24X31aSipN1Ta6K3 -KKQUQRvixVoI1q+8WHAubBDEqvFnNYRHD+AjKvECgYBkekjhpvEcxme4DBtw+OeQ -4OJXJTmhKemwwB12AERboWc88d3GEqIVMEWQJmHRotFOMfCDrMNfOxYv5+5t7FxL -gaXHT1Hi7CQNJ4afWrKgmjjqrXPtguGIvq2fXzjVt8T9uNjIlNxe+kS1SXFjXsgH -ftDY6VgTMB0B4ozKq6UAvQKBgQDER8K5buJHe+3rmMCMHn+Qfpkndr4ftYXQ9Kn4 -MFiy6sV0hdfTgRzEdOjXu9vH/BRVy3iFFVhYvIR42iTEIal2VaAUhM94Je5cmSyd -eE1eFHTqfRPNazmPaqttmSc4cfa0D4CNFVoZR6RupIl6Cect7jvkIaVUD+wMXxWo -osOFsQKBgDLwVhZWoQ13RV/jfQxS3veBUnHJwQJ7gKlL1XZ16mpfEOOVnJF7Es8j -TIIXXYhgSy/XshUbsgXQ+YGliye/rXSCTXHBXvWShOqxEMgeMYMRkcm8ZLp/DH7C -kC2pemkLPUJqgSh1PASGcJbDJIvFGUfP69tUCYpHpk3nHzexuAg3 ------END RSA PRIVATE KEY-----`) - -var ServerCert = []byte(`-----BEGIN CERTIFICATE----- -MIIDQDCCAiigAwIBAgIJANWw74P5KJk2MA0GCSqGSIb3DQEBCwUAMDQxMjAwBgNV -BAMMKWdlbmVyaWNfd2ViaG9va19hZG1pc3Npb25fcGx1Z2luX3Rlc3RzX2NhMCAX -DTE3MTExNjAwMDUzOVoYDzIyOTEwOTAxMDAwNTM5WjAjMSEwHwYDVQQDExh3ZWJo -b29rLXRlc3QuZGVmYXVsdC5zdmMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK -AoIBAQDXd/nQ89a5H8ifEsigmMd01Ib6NVR3bkJjtkvYnTbdfYEBj7UzqOQtHoLa -dIVmefny5uIHvj93WD8WDVPB3jX2JHrXkDTXd/6o6jIXHcsUfFTVLp6/bZ+Anqe0 -r/7hAPkzA2A7APyTWM3ZbEeo1afXogXhOJ1u/wz0DflgcB21gNho4kKTONXO3NHD -XLpspFqSkxfEfKVDJaYAoMnYZJtFNsa2OvsmLnhYF8bjeT3i07lfwrhUZvP+7Gsp -7UgUwc06WuNHjfx1s5e6ySzH0QioMD1rjYneqOvk0pKrMIhuAEWXqq7jlXcDtx1E -j+wnYbVqqVYheHZ8BCJoVAAQGs9/AgMBAAGjZDBiMAkGA1UdEwQCMAAwCwYDVR0P -BAQDAgXgMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDATApBgNVHREEIjAg -hwR/AAABghh3ZWJob29rLXRlc3QuZGVmYXVsdC5zdmMwDQYJKoZIhvcNAQELBQAD -ggEBAD/GKSPNyQuAOw/jsYZesb+RMedbkzs18sSwlxAJQMUrrXwlVdHrA8q5WhE6 -ABLqU1b8lQ8AWun07R8k5tqTmNvCARrAPRUqls/ryER+3Y9YEcxEaTc3jKNZFLbc -T6YtcnkdhxsiO136wtiuatpYL91RgCmuSpR8+7jEHhuFU01iaASu7ypFrUzrKHTF -bKwiLRQi1cMzVcLErq5CDEKiKhUkoDucyARFszrGt9vNIl/YCcBOkcNvM3c05Hn3 -M++C29JwS3Hwbubg6WO3wjFjoEhpCwU6qRYUz3MRp4tHO4kxKXx+oQnUiFnR7vW0 -YkNtGc1RUDHwecCTFpJtPb7Yu/E= ------END CERTIFICATE-----`) - -var ClientKey = []byte(`-----BEGIN RSA PRIVATE KEY----- -MIIEpQIBAAKCAQEAo3q//YITCuCWOSLPAdXSGUU+KvydADr63dy5CDTW5wBovnh9 -9Lb0r7iZUkSklk0nMbqVILMGt87MuuW2sdge2paWMlhMlh1R5gWuKSQUahF6pHrD -O9fOeUEHxpK0hI1l/gGBKP5DjoGNALu9m2AEkUG02BXZJ0AVUpbtgXDtf6AbdSlt -ZrlCiETkMuzuYZ8xHDS/AhnR6d8TMQ0hh1dj2UpR0jrMmMg5Im3+D7r35NuiCSoy -LCYSecBLCQ4TZnylLHMLzhgOhReqCdwaKeunliDPascgGvtcabEak7grPzyD2nUV -Zt1yysTXnsV87OF+b1tuwXFFo6W3KGqTtwik5QIDAQABAoIBAQChJ5dts7VL6ruM -FXlViM/1Y2H2hFHM8VduMHEi2tvimm+nHCamf1jUhLh39fz9wY7aoeDyfCkqNy1x -LJQd2zwHJZ1ogcz1ym96vqzCF7QcH6Dz1aTyMDp1I5sjsGlNpgoeDKOjoos8Rw+V -4nz2VwAJpWk9/sOzwqOCaBA3ovgs7zdpPcFhMMle0v79TOUBQ/aa86X0xtRDxwuH -hT8Z2t5hPjSLAjLO9cT0i5bYVVVnvq1ZTGXETXEwi7mMI2HPLELdcTSwesxTTRpt -ACIJOuwHPK5KxxC2HFHTyS0THaCDCR9Hqk8lwKEa+CBmjeCc2MwXuYsVGKsm0FaZ -viS+fGBBAoGBANcglIBOb8WA+BR+F7Bi7jU3nVtalU4DAHKbhSYg8EAxMIuWMq14 -UK+h2Qz2RrT1ezegVAm/NEqLX28vhYm1yz2RHDCUqAughKvhwNJ2mkLLREsJNATw -AMXDS2KhDPIsbJMKY66Gci17+q2FhyXiW10dxpTReVqnOiT1qeUilPkRAoGBAMKK -HG5EGaiF3brr8swsmPaqq33aXqo1k40/pd97xuauPIC/HBLeu+g6w3z8QecOKEYk -+Z5R/o/rsjCIpG4uF19lyAKZ9IgGpHX0rbEfWEyl5WARDOegXHGVfj1DNGhZEtO+ -kSq1i5LteQSfRXvarbhbV7bKgvJYtLK5960XaM6VAoGBALyIPfzQQN5LL57t/p7D -pNWYvtwf37d1o//M0fzfYw4uzceXQySJy9SQN+NHNiJC/NB8PwonupEV4fZUJGjS -nKKBOL5OmZNPAtaLy2vnKzwcXeaQ0zj8iQDILZnrYKggTKr0sPVzuD6qZ7+IxS9r -V/ycKrujdQIAilF3xoQcMYixAoGAfx2NvENFXMez/brFGMKfZLZafk7dAm0lr+sB -8MjJS9xX7mxx5Kajs/gJ2rZePaMTj9oDPX8oTlRdR7dRcikt3oj8Ky78CJIGjojF -ofHwWY0hFyes/gDbxuA+77rlGLXzRmbEJlsgC26eX/XOikJ2tvsAkpE7BS4PTKWV -gAXG1w0CgYEAq4rhFKVi37qKI8kVHO5O3lvRfKOmiMs2j2X+3T2QSTGcGRZD31EO -ImRWsYCAaX97313xYhjTT4jJzNU4fdnJ5hFte+Nt7BK1h/ze4+0XGJBK7wnDqaqg -kL0SB6nxr/Gqnhwx+wEaLkfhiy7Gx0E0IoSGEELsW/MMgvzzAo1/jaM= ------END RSA PRIVATE KEY-----`) - -var ClientCert = []byte(`-----BEGIN CERTIFICATE----- -MIIDVTCCAj2gAwIBAgIJANWw74P5KJk3MA0GCSqGSIb3DQEBCwUAMDQxMjAwBgNV -BAMMKWdlbmVyaWNfd2ViaG9va19hZG1pc3Npb25fcGx1Z2luX3Rlc3RzX2NhMCAX -DTE3MTExNjAwMDUzOVoYDzIyOTEwOTAxMDAwNTM5WjA4MTYwNAYDVQQDFC1nZW5l -cmljX3dlYmhvb2tfYWRtaXNzaW9uX3BsdWdpbl90ZXN0c19jbGllbnQwggEiMA0G -CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCjer/9ghMK4JY5Is8B1dIZRT4q/J0A -Ovrd3LkINNbnAGi+eH30tvSvuJlSRKSWTScxupUgswa3zsy65bax2B7alpYyWEyW -HVHmBa4pJBRqEXqkesM71855QQfGkrSEjWX+AYEo/kOOgY0Au72bYASRQbTYFdkn -QBVSlu2BcO1/oBt1KW1muUKIROQy7O5hnzEcNL8CGdHp3xMxDSGHV2PZSlHSOsyY -yDkibf4Puvfk26IJKjIsJhJ5wEsJDhNmfKUscwvOGA6FF6oJ3Bop66eWIM9qxyAa -+1xpsRqTuCs/PIPadRVm3XLKxNeexXzs4X5vW27BcUWjpbcoapO3CKTlAgMBAAGj -ZDBiMAkGA1UdEwQCMAAwCwYDVR0PBAQDAgXgMB0GA1UdJQQWMBQGCCsGAQUFBwMC -BggrBgEFBQcDATApBgNVHREEIjAghwR/AAABghh3ZWJob29rLXRlc3QuZGVmYXVs -dC5zdmMwDQYJKoZIhvcNAQELBQADggEBACDF/OlwaoxLu4h4bvyNJnuQdsw3O2Zz -xEADJOkeqM389hYmTlfyPFFhHocFW79ObUxa+73haBXTI6wFP0wSr2jaSQ86j85/ -V99S8WP/D4jmVqXXTe43o3WvvKFUHfJ7BO4OEHED0orRe11IcSkP8emSHHehqXxg -V0P3s1cZao7pPplRSZjcOC5dimEfKnx7ibBh22a8wjq2vPbGxTDf56nkeq4/fbc5 -MaAAeVpyFlN6ueREaz7ixy0r3yLMhC9xr4E6p8VvWsYBkQHWyukiUzbwVUwpK+Rw -Hy80c9+1z7X9/eKr9N/fzwbfrGjb3rbi7o1UHEEwiLaq1a+Df6dP92o= ------END CERTIFICATE-----`) diff --git a/vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/testcerts/doc.go b/vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/testcerts/doc.go deleted file mode 100644 index a06fe3a6f8fd..000000000000 --- a/vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/testcerts/doc.go +++ /dev/null @@ -1,19 +0,0 @@ -/* -Copyright 2017 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -// Package testcerts contains generated key pairs used by the unit tests of -// mutating and validating webhooks. They are for testing only. -package testcerts // import "k8s.io/apiserver/pkg/admission/plugin/webhook/testcerts" diff --git a/vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/testcerts/gencerts.sh b/vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/testcerts/gencerts.sh deleted file mode 100644 index 34493b966d54..000000000000 --- a/vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/testcerts/gencerts.sh +++ /dev/null @@ -1,109 +0,0 @@ -#!/usr/bin/env bash - -# Copyright 2017 The Kubernetes Authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -set -e - -# gencerts.sh generates the certificates for the generic webhook admission plugin tests. -# -# It is not expected to be run often (there is no go generate rule), and mainly -# exists for documentation purposes. - -CN_BASE="generic_webhook_admission_plugin_tests" - -cat > server.conf << EOF -[req] -req_extensions = v3_req -distinguished_name = req_distinguished_name -[req_distinguished_name] -[ v3_req ] -basicConstraints = CA:FALSE -keyUsage = nonRepudiation, digitalSignature, keyEncipherment -extendedKeyUsage = clientAuth, serverAuth -subjectAltName = @alt_names -[alt_names] -IP.1 = 127.0.0.1 -DNS.1 = webhook-test.default.svc -EOF - -cat > client.conf << EOF -[req] -req_extensions = v3_req -distinguished_name = req_distinguished_name -[req_distinguished_name] -[ v3_req ] -basicConstraints = CA:FALSE -keyUsage = nonRepudiation, digitalSignature, keyEncipherment -extendedKeyUsage = clientAuth, serverAuth -subjectAltName = @alt_names -[alt_names] -IP.1 = 127.0.0.1 -DNS.1 = webhook-test.default.svc -EOF - -# Create a certificate authority -openssl genrsa -out CAKey.pem 2048 -openssl req -x509 -new -nodes -key CAKey.pem -days 100000 -out CACert.pem -subj "/CN=${CN_BASE}_ca" - -# Create a second certificate authority -openssl genrsa -out BadCAKey.pem 2048 -openssl req -x509 -new -nodes -key BadCAKey.pem -days 100000 -out BadCACert.pem -subj "/CN=${CN_BASE}_ca" - -# Create a server certiticate -openssl genrsa -out ServerKey.pem 2048 -openssl req -new -key ServerKey.pem -out server.csr -subj "/CN=webhook-test.default.svc" -config server.conf -openssl x509 -req -in server.csr -CA CACert.pem -CAkey CAKey.pem -CAcreateserial -out ServerCert.pem -days 100000 -extensions v3_req -extfile server.conf - -# Create a client certiticate -openssl genrsa -out ClientKey.pem 2048 -openssl req -new -key ClientKey.pem -out client.csr -subj "/CN=${CN_BASE}_client" -config client.conf -openssl x509 -req -in client.csr -CA CACert.pem -CAkey CAKey.pem -CAcreateserial -out ClientCert.pem -days 100000 -extensions v3_req -extfile client.conf - -outfile=certs.go - -cat > $outfile << EOF -/* -Copyright 2017 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -// This file was generated using openssl by the gencerts.sh script -// and holds raw certificates for the webhook tests. - -package testcerts -EOF - -for file in CAKey CACert BadCAKey BadCACert ServerKey ServerCert ClientKey ClientCert; do - data=$(cat ${file}.pem) - echo "" >> $outfile - echo "var $file = []byte(\`$data\`)" >> $outfile -done - -# Clean up after we're done. -rm ./*.pem -rm ./*.csr -rm ./*.srl -rm ./*.conf diff --git a/vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/testing/authentication_info_resolver.go b/vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/testing/authentication_info_resolver.go deleted file mode 100644 index c06f6d826a8a..000000000000 --- a/vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/testing/authentication_info_resolver.go +++ /dev/null @@ -1,63 +0,0 @@ -/* -Copyright 2018 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package testing - -import ( - "sync/atomic" - - "k8s.io/apiserver/pkg/admission/plugin/webhook/testcerts" - "k8s.io/apiserver/pkg/util/webhook" - "k8s.io/client-go/rest" -) - -// Wrapper turns an AuthenticationInfoResolver into a AuthenticationInfoResolverWrapper that unconditionally -// returns the given AuthenticationInfoResolver. -func Wrapper(r webhook.AuthenticationInfoResolver) func(webhook.AuthenticationInfoResolver) webhook.AuthenticationInfoResolver { - return func(webhook.AuthenticationInfoResolver) webhook.AuthenticationInfoResolver { - return r - } -} - -// NewAuthenticationInfoResolver creates a fake AuthenticationInfoResolver that counts cache misses on -// every call to its methods. -func NewAuthenticationInfoResolver(cacheMisses *int32) webhook.AuthenticationInfoResolver { - return &authenticationInfoResolver{ - restConfig: &rest.Config{ - TLSClientConfig: rest.TLSClientConfig{ - CAData: testcerts.CACert, - CertData: testcerts.ClientCert, - KeyData: testcerts.ClientKey, - }, - }, - cacheMisses: cacheMisses, - } -} - -type authenticationInfoResolver struct { - restConfig *rest.Config - cacheMisses *int32 -} - -func (a *authenticationInfoResolver) ClientConfigFor(hostPort string) (*rest.Config, error) { - atomic.AddInt32(a.cacheMisses, 1) - return a.restConfig, nil -} - -func (a *authenticationInfoResolver) ClientConfigForService(serviceName, serviceNamespace string, servicePort int) (*rest.Config, error) { - atomic.AddInt32(a.cacheMisses, 1) - return a.restConfig, nil -} diff --git a/vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/testing/service_resolver.go b/vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/testing/service_resolver.go deleted file mode 100644 index 97c2e9a521f2..000000000000 --- a/vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/testing/service_resolver.go +++ /dev/null @@ -1,42 +0,0 @@ -/* -Copyright 2018 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package testing - -import ( - "fmt" - "net/url" - - "k8s.io/apiserver/pkg/util/webhook" -) - -type serviceResolver struct { - base url.URL -} - -// NewServiceResolver returns a static service resolve that return the given URL or -// an error for the failResolve namespace. -func NewServiceResolver(base url.URL) webhook.ServiceResolver { - return &serviceResolver{base} -} - -func (f serviceResolver) ResolveEndpoint(namespace, name string, port int32) (*url.URL, error) { - if namespace == "failResolve" { - return nil, fmt.Errorf("couldn't resolve service location") - } - u := f.base - return &u, nil -} diff --git a/vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/testing/testcase.go b/vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/testing/testcase.go deleted file mode 100644 index 14cd8a9a1194..000000000000 --- a/vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/testing/testcase.go +++ /dev/null @@ -1,1053 +0,0 @@ -/* -Copyright 2018 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package testing - -import ( - "fmt" - "net/http" - "net/url" - "reflect" - "strings" - "sync" - - registrationv1 "k8s.io/api/admissionregistration/v1" - corev1 "k8s.io/api/core/v1" - 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/apiserver/pkg/admission" - "k8s.io/apiserver/pkg/admission/plugin/webhook/testcerts" - auditinternal "k8s.io/apiserver/pkg/apis/audit" - "k8s.io/apiserver/pkg/authentication/user" - "k8s.io/client-go/informers" - "k8s.io/client-go/kubernetes" - fakeclientset "k8s.io/client-go/kubernetes/fake" -) - -var matchEverythingRules = []registrationv1.RuleWithOperations{{ - Operations: []registrationv1.OperationType{registrationv1.OperationAll}, - Rule: registrationv1.Rule{ - APIGroups: []string{"*"}, - APIVersions: []string{"*"}, - Resources: []string{"*/*"}, - }, -}} - -var sideEffectsUnknown = registrationv1.SideEffectClassUnknown -var sideEffectsNone = registrationv1.SideEffectClassNone -var sideEffectsSome = registrationv1.SideEffectClassSome -var sideEffectsNoneOnDryRun = registrationv1.SideEffectClassNoneOnDryRun - -var reinvokeNever = registrationv1.NeverReinvocationPolicy -var reinvokeIfNeeded = registrationv1.IfNeededReinvocationPolicy - -// NewFakeValidatingDataSource returns a mock client and informer returning the given webhooks. -func NewFakeValidatingDataSource(name string, webhooks []registrationv1.ValidatingWebhook, stopCh <-chan struct{}) (clientset kubernetes.Interface, factory informers.SharedInformerFactory) { - var objs = []runtime.Object{ - &corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Labels: map[string]string{ - "runlevel": "0", - }, - }, - }, - } - objs = append(objs, ®istrationv1.ValidatingWebhookConfiguration{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-webhooks", - }, - Webhooks: webhooks, - }) - - client := fakeclientset.NewSimpleClientset(objs...) - informerFactory := informers.NewSharedInformerFactory(client, 0) - - return client, informerFactory -} - -// NewFakeMutatingDataSource returns a mock client and informer returning the given webhooks. -func NewFakeMutatingDataSource(name string, webhooks []registrationv1.MutatingWebhook, stopCh <-chan struct{}) (clientset kubernetes.Interface, factory informers.SharedInformerFactory) { - var objs = []runtime.Object{ - &corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Labels: map[string]string{ - "runlevel": "0", - }, - }, - }, - } - objs = append(objs, ®istrationv1.MutatingWebhookConfiguration{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-webhooks", - }, - Webhooks: webhooks, - }) - - client := fakeclientset.NewSimpleClientset(objs...) - informerFactory := informers.NewSharedInformerFactory(client, 0) - - return client, informerFactory -} - -func newAttributesRecord(object metav1.Object, oldObject metav1.Object, kind schema.GroupVersionKind, namespace string, name string, resource string, labels map[string]string, dryRun bool) admission.Attributes { - object.SetName(name) - object.SetNamespace(namespace) - objectLabels := map[string]string{resource + ".name": name} - for k, v := range labels { - objectLabels[k] = v - } - object.SetLabels(objectLabels) - - oldObject.SetName(name) - oldObject.SetNamespace(namespace) - - gvr := kind.GroupVersion().WithResource(resource) - subResource := "" - userInfo := user.DefaultInfo{ - Name: "webhook-test", - UID: "webhook-test", - } - options := &metav1.UpdateOptions{} - - return &FakeAttributes{ - Attributes: admission.NewAttributesRecord(object.(runtime.Object), oldObject.(runtime.Object), kind, namespace, name, gvr, subResource, admission.Update, options, dryRun, &userInfo), - } -} - -// FakeAttributes decorate admission.Attributes. It's used to trace the added annotations. -type FakeAttributes struct { - admission.Attributes - annotations map[string]string - mutex sync.Mutex -} - -// AddAnnotation adds an annotation key value pair to FakeAttributes -func (f *FakeAttributes) AddAnnotation(k, v string) error { - return f.AddAnnotationWithLevel(k, v, auditinternal.LevelMetadata) -} - -// AddAnnotationWithLevel adds an annotation key value pair to FakeAttributes -func (f *FakeAttributes) AddAnnotationWithLevel(k, v string, _ auditinternal.Level) error { - f.mutex.Lock() - defer f.mutex.Unlock() - if err := f.Attributes.AddAnnotation(k, v); err != nil { - return err - } - if f.annotations == nil { - f.annotations = make(map[string]string) - } - f.annotations[k] = v - return nil -} - -// GetAnnotations reads annotations from FakeAttributes -func (f *FakeAttributes) GetAnnotations(level auditinternal.Level) map[string]string { - f.mutex.Lock() - defer f.mutex.Unlock() - return f.annotations -} - -// NewAttribute returns static admission Attributes for testing. -func NewAttribute(namespace string, labels map[string]string, dryRun bool) admission.Attributes { - // Set up a test object for the call - object := corev1.Pod{ - TypeMeta: metav1.TypeMeta{ - APIVersion: "v1", - Kind: "Pod", - }, - } - oldObject := corev1.Pod{} - kind := corev1.SchemeGroupVersion.WithKind("Pod") - name := "my-pod" - - return newAttributesRecord(&object, &oldObject, kind, namespace, name, "pod", labels, dryRun) -} - -// NewAttributeUnstructured returns static admission Attributes for testing with custom resources. -func NewAttributeUnstructured(namespace string, labels map[string]string, dryRun bool) admission.Attributes { - // Set up a test object for the call - object := unstructured.Unstructured{} - object.SetKind("TestCRD") - object.SetAPIVersion("custom.resource/v1") - oldObject := unstructured.Unstructured{} - oldObject.SetKind("TestCRD") - oldObject.SetAPIVersion("custom.resource/v1") - kind := object.GroupVersionKind() - name := "my-test-crd" - - return newAttributesRecord(&object, &oldObject, kind, namespace, name, "crd", labels, dryRun) -} - -type urlConfigGenerator struct { - baseURL *url.URL -} - -func (c urlConfigGenerator) ccfgURL(urlPath string) registrationv1.WebhookClientConfig { - u2 := *c.baseURL - u2.Path = urlPath - urlString := u2.String() - return registrationv1.WebhookClientConfig{ - URL: &urlString, - CABundle: testcerts.CACert, - } -} - -// ValidatingTest is a validating webhook test case. -type ValidatingTest struct { - Name string - Webhooks []registrationv1.ValidatingWebhook - Path string - IsCRD bool - IsDryRun bool - AdditionalLabels map[string]string - SkipBenchmark bool - ExpectLabels map[string]string - ExpectAllow bool - ErrorContains string - ExpectAnnotations map[string]string - ExpectStatusCode int32 - ExpectReinvokeWebhooks map[string]bool -} - -// MutatingTest is a mutating webhook test case. -type MutatingTest struct { - Name string - Webhooks []registrationv1.MutatingWebhook - Path string - IsCRD bool - IsDryRun bool - AdditionalLabels map[string]string - SkipBenchmark bool - ExpectLabels map[string]string - ExpectAllow bool - ErrorContains string - ExpectAnnotations map[string]string - ExpectStatusCode int32 - ExpectReinvokeWebhooks map[string]bool -} - -// ConvertToMutatingTestCases converts a validating test case to a mutating one for test purposes. -func ConvertToMutatingTestCases(tests []ValidatingTest, configurationName string) []MutatingTest { - r := make([]MutatingTest, len(tests)) - for i, t := range tests { - for idx, hook := range t.Webhooks { - if t.ExpectAnnotations == nil { - t.ExpectAnnotations = map[string]string{} - } - // Add expected annotation if the converted webhook is intended to match - if reflect.DeepEqual(hook.NamespaceSelector, &metav1.LabelSelector{}) && - reflect.DeepEqual(hook.ObjectSelector, &metav1.LabelSelector{}) && - reflect.DeepEqual(hook.Rules, matchEverythingRules) { - key := fmt.Sprintf("mutation.webhook.admission.k8s.io/round_0_index_%d", idx) - value := mutationAnnotationValue(configurationName, hook.Name, false) - t.ExpectAnnotations[key] = value - } - // Break if the converted webhook is intended to fail close - if strings.Contains(hook.Name, "internalErr") && (hook.FailurePolicy == nil || *hook.FailurePolicy == registrationv1.Fail) { - break - } - } - r[i] = MutatingTest{t.Name, ConvertToMutatingWebhooks(t.Webhooks), t.Path, t.IsCRD, t.IsDryRun, t.AdditionalLabels, t.SkipBenchmark, t.ExpectLabels, t.ExpectAllow, t.ErrorContains, t.ExpectAnnotations, t.ExpectStatusCode, t.ExpectReinvokeWebhooks} - } - return r -} - -// ConvertToMutatingWebhooks converts a validating webhook to a mutating one for test purposes. -func ConvertToMutatingWebhooks(webhooks []registrationv1.ValidatingWebhook) []registrationv1.MutatingWebhook { - mutating := make([]registrationv1.MutatingWebhook, len(webhooks)) - for i, h := range webhooks { - mutating[i] = registrationv1.MutatingWebhook{h.Name, h.ClientConfig, h.Rules, h.FailurePolicy, h.MatchPolicy, h.NamespaceSelector, h.ObjectSelector, h.SideEffects, h.TimeoutSeconds, h.AdmissionReviewVersions, nil} - } - return mutating -} - -// NewNonMutatingTestCases returns test cases with a given base url. -// All test cases in NewNonMutatingTestCases have no Patch set in -// AdmissionResponse. The test cases are used by both MutatingAdmissionWebhook -// and ValidatingAdmissionWebhook. -func NewNonMutatingTestCases(url *url.URL) []ValidatingTest { - policyFail := registrationv1.Fail - policyIgnore := registrationv1.Ignore - ccfgURL := urlConfigGenerator{url}.ccfgURL - - return []ValidatingTest{ - { - Name: "no match", - Webhooks: []registrationv1.ValidatingWebhook{{ - Name: "nomatch", - ClientConfig: ccfgSVC("disallow"), - Rules: []registrationv1.RuleWithOperations{{ - Operations: []registrationv1.OperationType{registrationv1.Create}, - }}, - NamespaceSelector: &metav1.LabelSelector{}, - ObjectSelector: &metav1.LabelSelector{}, - AdmissionReviewVersions: []string{"v1beta1"}, - }}, - ExpectAllow: true, - }, - { - Name: "match & allow", - Webhooks: []registrationv1.ValidatingWebhook{{ - Name: "allow.example.com", - ClientConfig: ccfgSVC("allow"), - Rules: matchEverythingRules, - NamespaceSelector: &metav1.LabelSelector{}, - ObjectSelector: &metav1.LabelSelector{}, - AdmissionReviewVersions: []string{"v1beta1"}, - }}, - ExpectAllow: true, - ExpectAnnotations: map[string]string{"allow.example.com/key1": "value1"}, - }, - { - Name: "match & disallow", - Webhooks: []registrationv1.ValidatingWebhook{{ - Name: "disallow", - ClientConfig: ccfgSVC("disallow"), - Rules: matchEverythingRules, - NamespaceSelector: &metav1.LabelSelector{}, - ObjectSelector: &metav1.LabelSelector{}, - AdmissionReviewVersions: []string{"v1beta1"}, - }}, - ExpectStatusCode: http.StatusForbidden, - ErrorContains: "without explanation", - }, - { - Name: "match & disallow ii", - Webhooks: []registrationv1.ValidatingWebhook{{ - Name: "disallowReason", - ClientConfig: ccfgSVC("disallowReason"), - Rules: matchEverythingRules, - NamespaceSelector: &metav1.LabelSelector{}, - ObjectSelector: &metav1.LabelSelector{}, - AdmissionReviewVersions: []string{"v1beta1"}, - }}, - ExpectStatusCode: http.StatusForbidden, - ErrorContains: "you shall not pass", - }, - { - Name: "match & disallow & but allowed because namespaceSelector exempt the ns", - Webhooks: []registrationv1.ValidatingWebhook{{ - Name: "disallow", - ClientConfig: ccfgSVC("disallow"), - Rules: newMatchEverythingRules(), - NamespaceSelector: &metav1.LabelSelector{ - MatchExpressions: []metav1.LabelSelectorRequirement{{ - Key: "runlevel", - Values: []string{"1"}, - Operator: metav1.LabelSelectorOpIn, - }}, - }, - ObjectSelector: &metav1.LabelSelector{}, - AdmissionReviewVersions: []string{"v1beta1"}, - }}, - - ExpectAllow: true, - }, - { - Name: "match & disallow & but allowed because namespaceSelector exempt the ns ii", - Webhooks: []registrationv1.ValidatingWebhook{{ - Name: "disallow", - ClientConfig: ccfgSVC("disallow"), - Rules: newMatchEverythingRules(), - NamespaceSelector: &metav1.LabelSelector{ - MatchExpressions: []metav1.LabelSelectorRequirement{{ - Key: "runlevel", - Values: []string{"0"}, - Operator: metav1.LabelSelectorOpNotIn, - }}, - }, - ObjectSelector: &metav1.LabelSelector{}, - AdmissionReviewVersions: []string{"v1beta1"}, - }}, - ExpectAllow: true, - }, - { - Name: "match & fail (but allow because fail open)", - Webhooks: []registrationv1.ValidatingWebhook{{ - Name: "internalErr A", - ClientConfig: ccfgSVC("internalErr"), - Rules: matchEverythingRules, - NamespaceSelector: &metav1.LabelSelector{}, - ObjectSelector: &metav1.LabelSelector{}, - FailurePolicy: &policyIgnore, - AdmissionReviewVersions: []string{"v1beta1"}, - }, { - Name: "internalErr B", - ClientConfig: ccfgSVC("internalErr"), - Rules: matchEverythingRules, - NamespaceSelector: &metav1.LabelSelector{}, - ObjectSelector: &metav1.LabelSelector{}, - FailurePolicy: &policyIgnore, - AdmissionReviewVersions: []string{"v1beta1"}, - }, { - Name: "internalErr C", - ClientConfig: ccfgSVC("internalErr"), - Rules: matchEverythingRules, - NamespaceSelector: &metav1.LabelSelector{}, - ObjectSelector: &metav1.LabelSelector{}, - FailurePolicy: &policyIgnore, - AdmissionReviewVersions: []string{"v1beta1"}, - }}, - - SkipBenchmark: true, - ExpectAllow: true, - }, - { - Name: "match & fail (but disallow because fail close on nil FailurePolicy)", - Webhooks: []registrationv1.ValidatingWebhook{{ - Name: "internalErr A", - ClientConfig: ccfgSVC("internalErr"), - NamespaceSelector: &metav1.LabelSelector{}, - ObjectSelector: &metav1.LabelSelector{}, - Rules: matchEverythingRules, - AdmissionReviewVersions: []string{"v1beta1"}, - }, { - Name: "internalErr B", - ClientConfig: ccfgSVC("internalErr"), - NamespaceSelector: &metav1.LabelSelector{}, - ObjectSelector: &metav1.LabelSelector{}, - Rules: matchEverythingRules, - AdmissionReviewVersions: []string{"v1beta1"}, - }, { - Name: "internalErr C", - ClientConfig: ccfgSVC("internalErr"), - NamespaceSelector: &metav1.LabelSelector{}, - ObjectSelector: &metav1.LabelSelector{}, - Rules: matchEverythingRules, - AdmissionReviewVersions: []string{"v1beta1"}, - }}, - ExpectStatusCode: http.StatusInternalServerError, - ExpectAllow: false, - }, - { - Name: "match & fail (but fail because fail closed)", - Webhooks: []registrationv1.ValidatingWebhook{{ - Name: "internalErr A", - ClientConfig: ccfgSVC("internalErr"), - Rules: matchEverythingRules, - NamespaceSelector: &metav1.LabelSelector{}, - ObjectSelector: &metav1.LabelSelector{}, - FailurePolicy: &policyFail, - AdmissionReviewVersions: []string{"v1beta1"}, - }, { - Name: "internalErr B", - ClientConfig: ccfgSVC("internalErr"), - Rules: matchEverythingRules, - NamespaceSelector: &metav1.LabelSelector{}, - ObjectSelector: &metav1.LabelSelector{}, - FailurePolicy: &policyFail, - AdmissionReviewVersions: []string{"v1beta1"}, - }, { - Name: "internalErr C", - ClientConfig: ccfgSVC("internalErr"), - Rules: matchEverythingRules, - NamespaceSelector: &metav1.LabelSelector{}, - ObjectSelector: &metav1.LabelSelector{}, - FailurePolicy: &policyFail, - AdmissionReviewVersions: []string{"v1beta1"}, - }}, - ExpectStatusCode: http.StatusInternalServerError, - ExpectAllow: false, - }, - { - Name: "match & allow (url)", - Webhooks: []registrationv1.ValidatingWebhook{{ - Name: "allow.example.com", - ClientConfig: ccfgURL("allow"), - Rules: matchEverythingRules, - NamespaceSelector: &metav1.LabelSelector{}, - ObjectSelector: &metav1.LabelSelector{}, - AdmissionReviewVersions: []string{"v1beta1"}, - }}, - ExpectAllow: true, - ExpectAnnotations: map[string]string{"allow.example.com/key1": "value1"}, - }, - { - Name: "match & disallow (url)", - Webhooks: []registrationv1.ValidatingWebhook{{ - Name: "disallow", - ClientConfig: ccfgURL("disallow"), - Rules: matchEverythingRules, - NamespaceSelector: &metav1.LabelSelector{}, - ObjectSelector: &metav1.LabelSelector{}, - AdmissionReviewVersions: []string{"v1beta1"}, - }}, - ExpectStatusCode: http.StatusForbidden, - ErrorContains: "without explanation", - }, { - Name: "absent response and fail open", - Webhooks: []registrationv1.ValidatingWebhook{{ - Name: "nilResponse", - ClientConfig: ccfgURL("nilResponse"), - FailurePolicy: &policyIgnore, - Rules: matchEverythingRules, - NamespaceSelector: &metav1.LabelSelector{}, - ObjectSelector: &metav1.LabelSelector{}, - AdmissionReviewVersions: []string{"v1beta1"}, - }}, - SkipBenchmark: true, - ExpectAllow: true, - }, - { - Name: "absent response and fail closed", - Webhooks: []registrationv1.ValidatingWebhook{{ - Name: "nilResponse", - ClientConfig: ccfgURL("nilResponse"), - FailurePolicy: &policyFail, - Rules: matchEverythingRules, - NamespaceSelector: &metav1.LabelSelector{}, - ObjectSelector: &metav1.LabelSelector{}, - AdmissionReviewVersions: []string{"v1beta1"}, - }}, - ExpectStatusCode: http.StatusInternalServerError, - ErrorContains: "webhook response was absent", - }, - { - Name: "no match dry run", - Webhooks: []registrationv1.ValidatingWebhook{{ - Name: "nomatch", - ClientConfig: ccfgSVC("allow"), - Rules: []registrationv1.RuleWithOperations{{ - Operations: []registrationv1.OperationType{registrationv1.Create}, - }}, - NamespaceSelector: &metav1.LabelSelector{}, - ObjectSelector: &metav1.LabelSelector{}, - SideEffects: &sideEffectsSome, - AdmissionReviewVersions: []string{"v1beta1"}, - }}, - IsDryRun: true, - ExpectAllow: true, - }, - { - Name: "match dry run side effects Unknown", - Webhooks: []registrationv1.ValidatingWebhook{{ - Name: "allow", - ClientConfig: ccfgSVC("allow"), - Rules: matchEverythingRules, - NamespaceSelector: &metav1.LabelSelector{}, - ObjectSelector: &metav1.LabelSelector{}, - SideEffects: &sideEffectsUnknown, - AdmissionReviewVersions: []string{"v1beta1"}, - }}, - IsDryRun: true, - ExpectStatusCode: http.StatusBadRequest, - ErrorContains: "does not support dry run", - }, - { - Name: "match dry run side effects None", - Webhooks: []registrationv1.ValidatingWebhook{{ - Name: "allow", - ClientConfig: ccfgSVC("allow"), - Rules: matchEverythingRules, - NamespaceSelector: &metav1.LabelSelector{}, - ObjectSelector: &metav1.LabelSelector{}, - SideEffects: &sideEffectsNone, - AdmissionReviewVersions: []string{"v1beta1"}, - }}, - IsDryRun: true, - ExpectAllow: true, - ExpectAnnotations: map[string]string{"allow/key1": "value1"}, - }, - { - Name: "match dry run side effects Some", - Webhooks: []registrationv1.ValidatingWebhook{{ - Name: "allow", - ClientConfig: ccfgSVC("allow"), - Rules: matchEverythingRules, - NamespaceSelector: &metav1.LabelSelector{}, - ObjectSelector: &metav1.LabelSelector{}, - SideEffects: &sideEffectsSome, - AdmissionReviewVersions: []string{"v1beta1"}, - }}, - IsDryRun: true, - ExpectStatusCode: http.StatusBadRequest, - ErrorContains: "does not support dry run", - }, - { - Name: "match dry run side effects NoneOnDryRun", - Webhooks: []registrationv1.ValidatingWebhook{{ - Name: "allow", - ClientConfig: ccfgSVC("allow"), - Rules: matchEverythingRules, - NamespaceSelector: &metav1.LabelSelector{}, - ObjectSelector: &metav1.LabelSelector{}, - SideEffects: &sideEffectsNoneOnDryRun, - AdmissionReviewVersions: []string{"v1beta1"}, - }}, - IsDryRun: true, - ExpectAllow: true, - ExpectAnnotations: map[string]string{"allow/key1": "value1"}, - }, - { - Name: "illegal annotation format", - Webhooks: []registrationv1.ValidatingWebhook{{ - Name: "invalidAnnotation", - ClientConfig: ccfgURL("invalidAnnotation"), - Rules: matchEverythingRules, - NamespaceSelector: &metav1.LabelSelector{}, - ObjectSelector: &metav1.LabelSelector{}, - AdmissionReviewVersions: []string{"v1beta1"}, - }}, - ExpectAllow: true, - }, - { - Name: "skip webhook whose objectSelector does not match", - Webhooks: []registrationv1.ValidatingWebhook{{ - Name: "allow.example.com", - ClientConfig: ccfgSVC("allow"), - Rules: matchEverythingRules, - NamespaceSelector: &metav1.LabelSelector{}, - ObjectSelector: &metav1.LabelSelector{}, - AdmissionReviewVersions: []string{"v1beta1"}, - }, { - Name: "shouldNotBeCalled", - ClientConfig: ccfgSVC("shouldNotBeCalled"), - NamespaceSelector: &metav1.LabelSelector{}, - ObjectSelector: &metav1.LabelSelector{ - MatchLabels: map[string]string{ - "label": "nonexistent", - }, - }, - Rules: matchEverythingRules, - AdmissionReviewVersions: []string{"v1beta1"}, - }}, - ExpectAllow: true, - ExpectAnnotations: map[string]string{"allow.example.com/key1": "value1"}, - }, - { - Name: "skip webhook whose objectSelector does not match CRD's labels", - Webhooks: []registrationv1.ValidatingWebhook{{ - Name: "allow.example.com", - ClientConfig: ccfgSVC("allow"), - Rules: matchEverythingRules, - NamespaceSelector: &metav1.LabelSelector{}, - ObjectSelector: &metav1.LabelSelector{}, - AdmissionReviewVersions: []string{"v1beta1"}, - }, { - Name: "shouldNotBeCalled", - ClientConfig: ccfgSVC("shouldNotBeCalled"), - NamespaceSelector: &metav1.LabelSelector{}, - ObjectSelector: &metav1.LabelSelector{ - MatchLabels: map[string]string{ - "label": "nonexistent", - }, - }, - Rules: matchEverythingRules, - AdmissionReviewVersions: []string{"v1beta1"}, - }}, - IsCRD: true, - ExpectAllow: true, - ExpectAnnotations: map[string]string{"allow.example.com/key1": "value1"}, - }, - // No need to test everything with the url case, since only the - // connection is different. - } -} - -func mutationAnnotationValue(configuration, webhook string, mutated bool) string { - return fmt.Sprintf(`{"configuration":"%s","webhook":"%s","mutated":%t}`, configuration, webhook, mutated) -} - -func patchAnnotationValue(configuration, webhook string, patch string) string { - return strings.Replace(fmt.Sprintf(`{"configuration": "%s", "webhook": "%s", "patch": %s, "patchType": "JSONPatch"}`, configuration, webhook, patch), " ", "", -1) -} - -// NewMutatingTestCases returns test cases with a given base url. -// All test cases in NewMutatingTestCases have Patch set in -// AdmissionResponse. The test cases are only used by both MutatingAdmissionWebhook. -func NewMutatingTestCases(url *url.URL, configurationName string) []MutatingTest { - return []MutatingTest{ - { - Name: "match & remove label", - Webhooks: []registrationv1.MutatingWebhook{{ - Name: "removelabel.example.com", - ClientConfig: ccfgSVC("removeLabel"), - Rules: matchEverythingRules, - NamespaceSelector: &metav1.LabelSelector{}, - ObjectSelector: &metav1.LabelSelector{}, - AdmissionReviewVersions: []string{"v1beta1"}, - }}, - ExpectAllow: true, - AdditionalLabels: map[string]string{"remove": "me"}, - ExpectLabels: map[string]string{"pod.name": "my-pod"}, - ExpectAnnotations: map[string]string{ - "removelabel.example.com/key1": "value1", - "mutation.webhook.admission.k8s.io/round_0_index_0": mutationAnnotationValue(configurationName, "removelabel.example.com", true), - "patch.webhook.admission.k8s.io/round_0_index_0": patchAnnotationValue(configurationName, "removelabel.example.com", `[{"op": "remove", "path": "/metadata/labels/remove"}]`), - }, - }, - { - Name: "match & add label", - Webhooks: []registrationv1.MutatingWebhook{{ - Name: "addLabel", - ClientConfig: ccfgSVC("addLabel"), - Rules: matchEverythingRules, - NamespaceSelector: &metav1.LabelSelector{}, - ObjectSelector: &metav1.LabelSelector{}, - AdmissionReviewVersions: []string{"v1beta1"}, - }}, - ExpectAllow: true, - ExpectLabels: map[string]string{"pod.name": "my-pod", "added": "test"}, - ExpectAnnotations: map[string]string{ - "mutation.webhook.admission.k8s.io/round_0_index_0": mutationAnnotationValue(configurationName, "addLabel", true), - "patch.webhook.admission.k8s.io/round_0_index_0": patchAnnotationValue(configurationName, "addLabel", `[{"op": "add", "path": "/metadata/labels/added", "value": "test"}]`), - }, - }, - { - Name: "match CRD & add label", - Webhooks: []registrationv1.MutatingWebhook{{ - Name: "addLabel", - ClientConfig: ccfgSVC("addLabel"), - Rules: matchEverythingRules, - NamespaceSelector: &metav1.LabelSelector{}, - ObjectSelector: &metav1.LabelSelector{}, - AdmissionReviewVersions: []string{"v1beta1"}, - }}, - IsCRD: true, - ExpectAllow: true, - ExpectLabels: map[string]string{"crd.name": "my-test-crd", "added": "test"}, - ExpectAnnotations: map[string]string{ - "mutation.webhook.admission.k8s.io/round_0_index_0": mutationAnnotationValue(configurationName, "addLabel", true), - "patch.webhook.admission.k8s.io/round_0_index_0": patchAnnotationValue(configurationName, "addLabel", `[{"op": "add", "path": "/metadata/labels/added", "value": "test"}]`), - }, - }, - { - Name: "match CRD & remove label", - Webhooks: []registrationv1.MutatingWebhook{{ - Name: "removelabel.example.com", - ClientConfig: ccfgSVC("removeLabel"), - Rules: matchEverythingRules, - NamespaceSelector: &metav1.LabelSelector{}, - ObjectSelector: &metav1.LabelSelector{}, - AdmissionReviewVersions: []string{"v1beta1"}, - }}, - IsCRD: true, - ExpectAllow: true, - AdditionalLabels: map[string]string{"remove": "me"}, - ExpectLabels: map[string]string{"crd.name": "my-test-crd"}, - ExpectAnnotations: map[string]string{ - "removelabel.example.com/key1": "value1", - "mutation.webhook.admission.k8s.io/round_0_index_0": mutationAnnotationValue(configurationName, "removelabel.example.com", true), - "patch.webhook.admission.k8s.io/round_0_index_0": patchAnnotationValue(configurationName, "removelabel.example.com", `[{"op": "remove", "path": "/metadata/labels/remove"}]`), - }, - }, - { - Name: "match & invalid mutation", - Webhooks: []registrationv1.MutatingWebhook{{ - Name: "invalidMutation", - ClientConfig: ccfgSVC("invalidMutation"), - Rules: matchEverythingRules, - NamespaceSelector: &metav1.LabelSelector{}, - ObjectSelector: &metav1.LabelSelector{}, - AdmissionReviewVersions: []string{"v1beta1"}, - }}, - ExpectStatusCode: http.StatusInternalServerError, - ErrorContains: "invalid character", - ExpectAnnotations: map[string]string{ - "mutation.webhook.admission.k8s.io/round_0_index_0": mutationAnnotationValue(configurationName, "invalidMutation", false), - }, - }, - { - Name: "match & remove label dry run unsupported", - Webhooks: []registrationv1.MutatingWebhook{{ - Name: "removeLabel", - ClientConfig: ccfgSVC("removeLabel"), - Rules: matchEverythingRules, - NamespaceSelector: &metav1.LabelSelector{}, - ObjectSelector: &metav1.LabelSelector{}, - SideEffects: &sideEffectsUnknown, - AdmissionReviewVersions: []string{"v1beta1"}, - }}, - IsDryRun: true, - ExpectStatusCode: http.StatusBadRequest, - ErrorContains: "does not support dry run", - ExpectAnnotations: map[string]string{ - "mutation.webhook.admission.k8s.io/round_0_index_0": mutationAnnotationValue(configurationName, "removeLabel", false), - }, - }, - { - Name: "first webhook remove labels, second webhook shouldn't be called", - Webhooks: []registrationv1.MutatingWebhook{{ - Name: "removelabel.example.com", - ClientConfig: ccfgSVC("removeLabel"), - Rules: matchEverythingRules, - NamespaceSelector: &metav1.LabelSelector{}, - ObjectSelector: &metav1.LabelSelector{ - MatchLabels: map[string]string{ - "remove": "me", - }, - }, - AdmissionReviewVersions: []string{"v1beta1"}, - }, { - Name: "shouldNotBeCalled", - ClientConfig: ccfgSVC("shouldNotBeCalled"), - NamespaceSelector: &metav1.LabelSelector{}, - ObjectSelector: &metav1.LabelSelector{ - MatchLabels: map[string]string{ - "remove": "me", - }, - }, - Rules: matchEverythingRules, - AdmissionReviewVersions: []string{"v1beta1"}, - }}, - ExpectAllow: true, - AdditionalLabels: map[string]string{"remove": "me"}, - ExpectLabels: map[string]string{"pod.name": "my-pod"}, - ExpectAnnotations: map[string]string{ - "removelabel.example.com/key1": "value1", - "mutation.webhook.admission.k8s.io/round_0_index_0": mutationAnnotationValue(configurationName, "removelabel.example.com", true), - "patch.webhook.admission.k8s.io/round_0_index_0": patchAnnotationValue(configurationName, "removelabel.example.com", `[{"op": "remove", "path": "/metadata/labels/remove"}]`), - }, - }, - { - Name: "first webhook remove labels from CRD, second webhook shouldn't be called", - Webhooks: []registrationv1.MutatingWebhook{{ - Name: "removelabel.example.com", - ClientConfig: ccfgSVC("removeLabel"), - Rules: matchEverythingRules, - NamespaceSelector: &metav1.LabelSelector{}, - ObjectSelector: &metav1.LabelSelector{ - MatchLabels: map[string]string{ - "remove": "me", - }, - }, - AdmissionReviewVersions: []string{"v1beta1"}, - }, { - Name: "shouldNotBeCalled", - ClientConfig: ccfgSVC("shouldNotBeCalled"), - NamespaceSelector: &metav1.LabelSelector{}, - ObjectSelector: &metav1.LabelSelector{ - MatchLabels: map[string]string{ - "remove": "me", - }, - }, - Rules: matchEverythingRules, - AdmissionReviewVersions: []string{"v1beta1"}, - }}, - IsCRD: true, - ExpectAllow: true, - AdditionalLabels: map[string]string{"remove": "me"}, - ExpectLabels: map[string]string{"crd.name": "my-test-crd"}, - ExpectAnnotations: map[string]string{ - "removelabel.example.com/key1": "value1", - "mutation.webhook.admission.k8s.io/round_0_index_0": mutationAnnotationValue(configurationName, "removelabel.example.com", true), - "patch.webhook.admission.k8s.io/round_0_index_0": patchAnnotationValue(configurationName, "removelabel.example.com", `[{"op": "remove", "path": "/metadata/labels/remove"}]`), - }, - }, - // No need to test everything with the url case, since only the - // connection is different. - { - Name: "match & reinvoke if needed policy", - Webhooks: []registrationv1.MutatingWebhook{{ - Name: "addLabel", - ClientConfig: ccfgSVC("addLabel"), - Rules: matchEverythingRules, - NamespaceSelector: &metav1.LabelSelector{}, - ObjectSelector: &metav1.LabelSelector{}, - AdmissionReviewVersions: []string{"v1beta1"}, - ReinvocationPolicy: &reinvokeIfNeeded, - }, { - Name: "removeLabel", - ClientConfig: ccfgSVC("removeLabel"), - Rules: matchEverythingRules, - NamespaceSelector: &metav1.LabelSelector{}, - ObjectSelector: &metav1.LabelSelector{}, - AdmissionReviewVersions: []string{"v1beta1"}, - ReinvocationPolicy: &reinvokeIfNeeded, - }}, - AdditionalLabels: map[string]string{"remove": "me"}, - ExpectAllow: true, - ExpectReinvokeWebhooks: map[string]bool{"addLabel": true}, - ExpectAnnotations: map[string]string{ - "mutation.webhook.admission.k8s.io/round_0_index_0": mutationAnnotationValue(configurationName, "addLabel", true), - "mutation.webhook.admission.k8s.io/round_0_index_1": mutationAnnotationValue(configurationName, "removeLabel", true), - "patch.webhook.admission.k8s.io/round_0_index_0": patchAnnotationValue(configurationName, "addLabel", `[{"op": "add", "path": "/metadata/labels/added", "value": "test"}]`), - "patch.webhook.admission.k8s.io/round_0_index_1": patchAnnotationValue(configurationName, "removeLabel", `[{"op": "remove", "path": "/metadata/labels/remove"}]`), - }, - }, - { - Name: "match & never reinvoke policy", - Webhooks: []registrationv1.MutatingWebhook{{ - Name: "addLabel", - ClientConfig: ccfgSVC("addLabel"), - Rules: matchEverythingRules, - NamespaceSelector: &metav1.LabelSelector{}, - ObjectSelector: &metav1.LabelSelector{}, - AdmissionReviewVersions: []string{"v1beta1"}, - ReinvocationPolicy: &reinvokeNever, - }}, - ExpectAllow: true, - ExpectReinvokeWebhooks: map[string]bool{"addLabel": false}, - ExpectAnnotations: map[string]string{ - "mutation.webhook.admission.k8s.io/round_0_index_0": mutationAnnotationValue(configurationName, "addLabel", true), - "patch.webhook.admission.k8s.io/round_0_index_0": patchAnnotationValue(configurationName, "addLabel", `[{"op": "add", "path": "/metadata/labels/added", "value": "test"}]`), - }, - }, - { - Name: "match & never reinvoke policy (by default)", - Webhooks: []registrationv1.MutatingWebhook{{ - Name: "addLabel", - ClientConfig: ccfgSVC("addLabel"), - Rules: matchEverythingRules, - NamespaceSelector: &metav1.LabelSelector{}, - ObjectSelector: &metav1.LabelSelector{}, - AdmissionReviewVersions: []string{"v1beta1"}, - }}, - ExpectAllow: true, - ExpectReinvokeWebhooks: map[string]bool{"addLabel": false}, - ExpectAnnotations: map[string]string{ - "mutation.webhook.admission.k8s.io/round_0_index_0": mutationAnnotationValue(configurationName, "addLabel", true), - "patch.webhook.admission.k8s.io/round_0_index_0": patchAnnotationValue(configurationName, "addLabel", `[{"op": "add", "path": "/metadata/labels/added", "value": "test"}]`), - }, - }, - { - Name: "match & no reinvoke", - Webhooks: []registrationv1.MutatingWebhook{{ - Name: "noop", - ClientConfig: ccfgSVC("noop"), - Rules: matchEverythingRules, - NamespaceSelector: &metav1.LabelSelector{}, - ObjectSelector: &metav1.LabelSelector{}, - AdmissionReviewVersions: []string{"v1beta1"}, - }}, - ExpectAllow: true, - ExpectAnnotations: map[string]string{ - "mutation.webhook.admission.k8s.io/round_0_index_0": mutationAnnotationValue(configurationName, "noop", false), - }, - }, - } -} - -// CachedTest is a test case for the client manager. -type CachedTest struct { - Name string - Webhooks []registrationv1.ValidatingWebhook - ExpectAllow bool - ExpectCacheMiss bool -} - -// NewCachedClientTestcases returns a set of client manager test cases. -func NewCachedClientTestcases(url *url.URL) []CachedTest { - policyIgnore := registrationv1.Ignore - ccfgURL := urlConfigGenerator{url}.ccfgURL - - return []CachedTest{ - { - Name: "uncached: service webhook, path 'allow'", - Webhooks: []registrationv1.ValidatingWebhook{{ - Name: "cache1", - ClientConfig: ccfgSVC("allow"), - Rules: newMatchEverythingRules(), - NamespaceSelector: &metav1.LabelSelector{}, - ObjectSelector: &metav1.LabelSelector{}, - FailurePolicy: &policyIgnore, - AdmissionReviewVersions: []string{"v1beta1"}, - }}, - ExpectAllow: true, - ExpectCacheMiss: true, - }, - { - Name: "uncached: service webhook, path 'internalErr'", - Webhooks: []registrationv1.ValidatingWebhook{{ - Name: "cache2", - ClientConfig: ccfgSVC("internalErr"), - Rules: newMatchEverythingRules(), - NamespaceSelector: &metav1.LabelSelector{}, - ObjectSelector: &metav1.LabelSelector{}, - FailurePolicy: &policyIgnore, - AdmissionReviewVersions: []string{"v1beta1"}, - }}, - ExpectAllow: true, - ExpectCacheMiss: true, - }, - { - Name: "cached: service webhook, path 'allow'", - Webhooks: []registrationv1.ValidatingWebhook{{ - Name: "cache3", - ClientConfig: ccfgSVC("allow"), - Rules: newMatchEverythingRules(), - NamespaceSelector: &metav1.LabelSelector{}, - ObjectSelector: &metav1.LabelSelector{}, - FailurePolicy: &policyIgnore, - AdmissionReviewVersions: []string{"v1beta1"}, - }}, - ExpectAllow: true, - ExpectCacheMiss: false, - }, - { - Name: "uncached: url webhook, path 'allow'", - Webhooks: []registrationv1.ValidatingWebhook{{ - Name: "cache4", - ClientConfig: ccfgURL("allow"), - Rules: newMatchEverythingRules(), - NamespaceSelector: &metav1.LabelSelector{}, - ObjectSelector: &metav1.LabelSelector{}, - FailurePolicy: &policyIgnore, - AdmissionReviewVersions: []string{"v1beta1"}, - }}, - ExpectAllow: true, - ExpectCacheMiss: true, - }, - { - Name: "cached: service webhook, path 'allow'", - Webhooks: []registrationv1.ValidatingWebhook{{ - Name: "cache5", - ClientConfig: ccfgURL("allow"), - Rules: newMatchEverythingRules(), - NamespaceSelector: &metav1.LabelSelector{}, - ObjectSelector: &metav1.LabelSelector{}, - FailurePolicy: &policyIgnore, - AdmissionReviewVersions: []string{"v1beta1"}, - }}, - ExpectAllow: true, - ExpectCacheMiss: false, - }, - } -} - -// ccfgSVC returns a client config using the service reference mechanism. -func ccfgSVC(urlPath string) registrationv1.WebhookClientConfig { - return registrationv1.WebhookClientConfig{ - Service: ®istrationv1.ServiceReference{ - Name: "webhook-test", - Namespace: "default", - Path: &urlPath, - }, - CABundle: testcerts.CACert, - } -} - -func newMatchEverythingRules() []registrationv1.RuleWithOperations { - return []registrationv1.RuleWithOperations{{ - Operations: []registrationv1.OperationType{registrationv1.OperationAll}, - Rule: registrationv1.Rule{ - APIGroups: []string{"*"}, - APIVersions: []string{"*"}, - Resources: []string{"*/*"}, - }, - }} -} - -// NewObjectInterfacesForTest returns an ObjectInterfaces appropriate for test cases in this file. -func NewObjectInterfacesForTest() admission.ObjectInterfaces { - scheme := runtime.NewScheme() - corev1.AddToScheme(scheme) - return admission.NewObjectInterfacesFromScheme(scheme) -} diff --git a/vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/testing/webhook_server.go b/vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/testing/webhook_server.go deleted file mode 100644 index b6d446005b02..000000000000 --- a/vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/testing/webhook_server.go +++ /dev/null @@ -1,162 +0,0 @@ -/* -Copyright 2018 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package testing - -import ( - "crypto/tls" - "crypto/x509" - "encoding/json" - "net/http" - "net/http/httptest" - "testing" - - "k8s.io/api/admission/v1beta1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apiserver/pkg/admission/plugin/webhook/testcerts" -) - -// NewTestServer returns a webhook test HTTPS server with fixed webhook test certs. -func NewTestServer(t testing.TB) *httptest.Server { - // Create the test webhook server - sCert, err := tls.X509KeyPair(testcerts.ServerCert, testcerts.ServerKey) - if err != nil { - t.Error(err) - t.FailNow() - } - rootCAs := x509.NewCertPool() - rootCAs.AppendCertsFromPEM(testcerts.CACert) - testServer := httptest.NewUnstartedServer(http.HandlerFunc(webhookHandler)) - testServer.TLS = &tls.Config{ - Certificates: []tls.Certificate{sCert}, - ClientCAs: rootCAs, - ClientAuth: tls.RequireAndVerifyClientCert, - } - return testServer -} - -func webhookHandler(w http.ResponseWriter, r *http.Request) { - // fmt.Printf("got req: %v\n", r.URL.Path) - switch r.URL.Path { - case "/internalErr": - http.Error(w, "webhook internal server error", http.StatusInternalServerError) - return - case "/invalidReq": - w.WriteHeader(http.StatusSwitchingProtocols) - w.Write([]byte("webhook invalid request")) - return - case "/invalidResp": - w.Header().Set("Content-Type", "application/json") - w.Write([]byte("webhook invalid response")) - case "/disallow": - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(&v1beta1.AdmissionReview{ - Response: &v1beta1.AdmissionResponse{ - Allowed: false, - Result: &metav1.Status{ - Code: http.StatusForbidden, - }, - }, - }) - case "/disallowReason": - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(&v1beta1.AdmissionReview{ - Response: &v1beta1.AdmissionResponse{ - Allowed: false, - Result: &metav1.Status{ - Message: "you shall not pass", - Code: http.StatusForbidden, - }, - }, - }) - case "/shouldNotBeCalled": - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(&v1beta1.AdmissionReview{ - Response: &v1beta1.AdmissionResponse{ - Allowed: false, - Result: &metav1.Status{ - Message: "doesn't expect labels to match object selector", - Code: http.StatusForbidden, - }, - }, - }) - case "/allow": - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(&v1beta1.AdmissionReview{ - Response: &v1beta1.AdmissionResponse{ - Allowed: true, - AuditAnnotations: map[string]string{ - "key1": "value1", - }, - }, - }) - case "/removeLabel": - w.Header().Set("Content-Type", "application/json") - pt := v1beta1.PatchTypeJSONPatch - json.NewEncoder(w).Encode(&v1beta1.AdmissionReview{ - Response: &v1beta1.AdmissionResponse{ - Allowed: true, - PatchType: &pt, - Patch: []byte(`[{"op": "remove", "path": "/metadata/labels/remove"}]`), - AuditAnnotations: map[string]string{ - "key1": "value1", - }, - }, - }) - case "/addLabel": - w.Header().Set("Content-Type", "application/json") - pt := v1beta1.PatchTypeJSONPatch - json.NewEncoder(w).Encode(&v1beta1.AdmissionReview{ - Response: &v1beta1.AdmissionResponse{ - Allowed: true, - PatchType: &pt, - Patch: []byte(`[{"op": "add", "path": "/metadata/labels/added", "value": "test"}]`), - }, - }) - case "/invalidMutation": - w.Header().Set("Content-Type", "application/json") - pt := v1beta1.PatchTypeJSONPatch - json.NewEncoder(w).Encode(&v1beta1.AdmissionReview{ - Response: &v1beta1.AdmissionResponse{ - Allowed: true, - PatchType: &pt, - Patch: []byte(`[{"op": "add", "CORRUPTED_KEY":}]`), - }, - }) - case "/nilResponse": - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(&v1beta1.AdmissionReview{}) - case "/invalidAnnotation": - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(&v1beta1.AdmissionReview{ - Response: &v1beta1.AdmissionResponse{ - Allowed: true, - AuditAnnotations: map[string]string{ - "invalid*key": "value1", - }, - }, - }) - case "/noop": - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(&v1beta1.AdmissionReview{ - Response: &v1beta1.AdmissionResponse{ - Allowed: true, - }, - }) - default: - http.NotFound(w, r) - } -} diff --git a/vendor/modules.txt b/vendor/modules.txt index 8e833228e9ad..8d17847acdb9 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -52,7 +52,6 @@ github.com/dsnet/compress/internal github.com/dsnet/compress/internal/errors github.com/dsnet/compress/internal/prefix # github.com/emicklei/go-restful v2.9.6+incompatible => github.com/emicklei/go-restful v2.9.5+incompatible -## explicit github.com/emicklei/go-restful github.com/emicklei/go-restful/log # github.com/envoyproxy/go-control-plane v0.9.7-0.20200730005029-803dd64f0468 => github.com/envoyproxy/go-control-plane v0.9.4 @@ -786,8 +785,6 @@ k8s.io/apiserver/pkg/admission/plugin/webhook/namespace k8s.io/apiserver/pkg/admission/plugin/webhook/object k8s.io/apiserver/pkg/admission/plugin/webhook/request k8s.io/apiserver/pkg/admission/plugin/webhook/rules -k8s.io/apiserver/pkg/admission/plugin/webhook/testcerts -k8s.io/apiserver/pkg/admission/plugin/webhook/testing k8s.io/apiserver/pkg/admission/plugin/webhook/validating k8s.io/apiserver/pkg/apis/apiserver k8s.io/apiserver/pkg/apis/apiserver/install