| /* |
| 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 apimachinery |
| |
| import ( |
| "fmt" |
| "reflect" |
| "strings" |
| "time" |
| |
| "k8s.io/api/admissionregistration/v1beta1" |
| apps "k8s.io/api/apps/v1" |
| "k8s.io/api/core/v1" |
| rbacv1beta1 "k8s.io/api/rbac/v1beta1" |
| apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" |
| crdclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" |
| "k8s.io/apimachinery/pkg/api/errors" |
| metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" |
| "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" |
| "k8s.io/apimachinery/pkg/types" |
| "k8s.io/apimachinery/pkg/util/intstr" |
| utilversion "k8s.io/apimachinery/pkg/util/version" |
| "k8s.io/apimachinery/pkg/util/wait" |
| "k8s.io/client-go/dynamic" |
| clientset "k8s.io/client-go/kubernetes" |
| "k8s.io/kubernetes/test/e2e/framework" |
| imageutils "k8s.io/kubernetes/test/utils/image" |
| |
| . "github.com/onsi/ginkgo" |
| . "github.com/onsi/gomega" |
| _ "github.com/stretchr/testify/assert" |
| ) |
| |
| const ( |
| secretName = "sample-webhook-secret" |
| deploymentName = "sample-webhook-deployment" |
| serviceName = "e2e-test-webhook" |
| roleBindingName = "webhook-auth-reader" |
| |
| // The webhook configuration names should not be reused between test instances. |
| crWebhookConfigName = "e2e-test-webhook-config-cr" |
| webhookConfigName = "e2e-test-webhook-config" |
| attachingPodWebhookConfigName = "e2e-test-webhook-config-attaching-pod" |
| mutatingWebhookConfigName = "e2e-test-mutating-webhook-config" |
| podMutatingWebhookConfigName = "e2e-test-mutating-webhook-pod" |
| crMutatingWebhookConfigName = "e2e-test-mutating-webhook-config-cr" |
| webhookFailClosedConfigName = "e2e-test-webhook-fail-closed" |
| validatingWebhookForWebhooksConfigName = "e2e-test-validating-webhook-for-webhooks-config" |
| mutatingWebhookForWebhooksConfigName = "e2e-test-mutating-webhook-for-webhooks-config" |
| dummyValidatingWebhookConfigName = "e2e-test-dummy-validating-webhook-config" |
| dummyMutatingWebhookConfigName = "e2e-test-dummy-mutating-webhook-config" |
| crdWebhookConfigName = "e2e-test-webhook-config-crd" |
| |
| skipNamespaceLabelKey = "skip-webhook-admission" |
| skipNamespaceLabelValue = "yes" |
| skippedNamespaceName = "exempted-namesapce" |
| disallowedPodName = "disallowed-pod" |
| toBeAttachedPodName = "to-be-attached-pod" |
| hangingPodName = "hanging-pod" |
| disallowedConfigMapName = "disallowed-configmap" |
| allowedConfigMapName = "allowed-configmap" |
| failNamespaceLabelKey = "fail-closed-webhook" |
| failNamespaceLabelValue = "yes" |
| failNamespaceName = "fail-closed-namesapce" |
| addedLabelKey = "added-label" |
| addedLabelValue = "yes" |
| ) |
| |
| var serverWebhookVersion = utilversion.MustParseSemantic("v1.8.0") |
| |
| var _ = SIGDescribe("AdmissionWebhook", func() { |
| var context *certContext |
| f := framework.NewDefaultFramework("webhook") |
| |
| var client clientset.Interface |
| var namespaceName string |
| |
| BeforeEach(func() { |
| client = f.ClientSet |
| namespaceName = f.Namespace.Name |
| |
| // Make sure the relevant provider supports admission webhook |
| framework.SkipUnlessServerVersionGTE(serverWebhookVersion, f.ClientSet.Discovery()) |
| framework.SkipUnlessProviderIs("gce", "gke", "local") |
| |
| _, err := f.ClientSet.AdmissionregistrationV1beta1().ValidatingWebhookConfigurations().List(metav1.ListOptions{}) |
| if errors.IsNotFound(err) { |
| framework.Skipf("dynamic configuration of webhooks requires the admissionregistration.k8s.io group to be enabled") |
| } |
| |
| By("Setting up server cert") |
| context = setupServerCert(namespaceName, serviceName) |
| createAuthReaderRoleBinding(f, namespaceName) |
| |
| // Note that in 1.9 we will have backwards incompatible change to |
| // admission webhooks, so the image will be updated to 1.9 sometime in |
| // the development 1.9 cycle. |
| deployWebhookAndService(f, imageutils.GetE2EImage(imageutils.AdmissionWebhook), context) |
| }) |
| |
| AfterEach(func() { |
| cleanWebhookTest(client, namespaceName) |
| }) |
| |
| It("Should be able to deny pod and configmap creation", func() { |
| webhookCleanup := registerWebhook(f, context) |
| defer webhookCleanup() |
| testWebhook(f) |
| }) |
| |
| It("Should be able to deny attaching pod", func() { |
| webhookCleanup := registerWebhookForAttachingPod(f, context) |
| defer webhookCleanup() |
| testAttachingPodWebhook(f) |
| }) |
| |
| It("Should be able to deny custom resource creation", func() { |
| testcrd, err := framework.CreateTestCRD(f) |
| if err != nil { |
| return |
| } |
| defer testcrd.CleanUp() |
| webhookCleanup := registerWebhookForCustomResource(f, context, testcrd) |
| defer webhookCleanup() |
| testCustomResourceWebhook(f, testcrd.Crd, testcrd.GetV1DynamicClient()) |
| }) |
| |
| It("Should unconditionally reject operations on fail closed webhook", func() { |
| webhookCleanup := registerFailClosedWebhook(f, context) |
| defer webhookCleanup() |
| testFailClosedWebhook(f) |
| }) |
| |
| It("Should mutate configmap", func() { |
| webhookCleanup := registerMutatingWebhookForConfigMap(f, context) |
| defer webhookCleanup() |
| testMutatingConfigMapWebhook(f) |
| }) |
| |
| It("Should mutate pod and apply defaults after mutation", func() { |
| webhookCleanup := registerMutatingWebhookForPod(f, context) |
| defer webhookCleanup() |
| testMutatingPodWebhook(f) |
| }) |
| |
| It("Should not be able to mutate or prevent deletion of webhook configuration objects", func() { |
| validatingWebhookCleanup := registerValidatingWebhookForWebhookConfigurations(f, context) |
| defer validatingWebhookCleanup() |
| mutatingWebhookCleanup := registerMutatingWebhookForWebhookConfigurations(f, context) |
| defer mutatingWebhookCleanup() |
| testWebhooksForWebhookConfigurations(f) |
| }) |
| |
| It("Should mutate custom resource", func() { |
| testcrd, err := framework.CreateTestCRD(f) |
| if err != nil { |
| return |
| } |
| defer testcrd.CleanUp() |
| webhookCleanup := registerMutatingWebhookForCustomResource(f, context, testcrd) |
| defer webhookCleanup() |
| testMutatingCustomResourceWebhook(f, testcrd.Crd, testcrd.GetV1DynamicClient()) |
| }) |
| |
| It("Should deny crd creation", func() { |
| crdWebhookCleanup := registerValidatingWebhookForCRD(f, context) |
| defer crdWebhookCleanup() |
| |
| testCRDDenyWebhook(f) |
| }) |
| |
| // TODO: add more e2e tests for mutating webhooks |
| // 1. mutating webhook that mutates pod |
| // 2. mutating webhook that sends empty patch |
| // 2.1 and sets status.allowed=true |
| // 2.2 and sets status.allowed=false |
| // 3. mutating webhook that sends patch, but also sets status.allowed=false |
| // 4. mtuating webhook that fail-open v.s. fail-closed |
| }) |
| |
| func createAuthReaderRoleBinding(f *framework.Framework, namespace string) { |
| By("Create role binding to let webhook read extension-apiserver-authentication") |
| client := f.ClientSet |
| // Create the role binding to allow the webhook read the extension-apiserver-authentication configmap |
| _, err := client.RbacV1beta1().RoleBindings("kube-system").Create(&rbacv1beta1.RoleBinding{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: roleBindingName, |
| Annotations: map[string]string{ |
| rbacv1beta1.AutoUpdateAnnotationKey: "true", |
| }, |
| }, |
| RoleRef: rbacv1beta1.RoleRef{ |
| APIGroup: "", |
| Kind: "Role", |
| Name: "extension-apiserver-authentication-reader", |
| }, |
| // Webhook uses the default service account. |
| Subjects: []rbacv1beta1.Subject{ |
| { |
| Kind: "ServiceAccount", |
| Name: "default", |
| Namespace: namespace, |
| }, |
| }, |
| }) |
| if err != nil && errors.IsAlreadyExists(err) { |
| framework.Logf("role binding %s already exists", roleBindingName) |
| } else { |
| framework.ExpectNoError(err, "creating role binding %s:webhook to access configMap", namespace) |
| } |
| } |
| |
| func deployWebhookAndService(f *framework.Framework, image string, context *certContext) { |
| By("Deploying the webhook pod") |
| client := f.ClientSet |
| |
| // Creating the secret that contains the webhook's cert. |
| secret := &v1.Secret{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: secretName, |
| }, |
| Type: v1.SecretTypeOpaque, |
| Data: map[string][]byte{ |
| "tls.crt": context.cert, |
| "tls.key": context.key, |
| }, |
| } |
| namespace := f.Namespace.Name |
| _, err := client.CoreV1().Secrets(namespace).Create(secret) |
| framework.ExpectNoError(err, "creating secret %q in namespace %q", secretName, namespace) |
| |
| // Create the deployment of the webhook |
| podLabels := map[string]string{"app": "sample-webhook", "webhook": "true"} |
| replicas := int32(1) |
| zero := int64(0) |
| mounts := []v1.VolumeMount{ |
| { |
| Name: "webhook-certs", |
| ReadOnly: true, |
| MountPath: "/webhook.local.config/certificates", |
| }, |
| } |
| volumes := []v1.Volume{ |
| { |
| Name: "webhook-certs", |
| VolumeSource: v1.VolumeSource{ |
| Secret: &v1.SecretVolumeSource{SecretName: secretName}, |
| }, |
| }, |
| } |
| containers := []v1.Container{ |
| { |
| Name: "sample-webhook", |
| VolumeMounts: mounts, |
| Args: []string{ |
| "--tls-cert-file=/webhook.local.config/certificates/tls.crt", |
| "--tls-private-key-file=/webhook.local.config/certificates/tls.key", |
| "--alsologtostderr", |
| "-v=4", |
| "2>&1", |
| }, |
| Image: image, |
| }, |
| } |
| d := &apps.Deployment{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: deploymentName, |
| Labels: podLabels, |
| }, |
| Spec: apps.DeploymentSpec{ |
| Replicas: &replicas, |
| Selector: &metav1.LabelSelector{ |
| MatchLabels: podLabels, |
| }, |
| Strategy: apps.DeploymentStrategy{ |
| Type: apps.RollingUpdateDeploymentStrategyType, |
| }, |
| Template: v1.PodTemplateSpec{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Labels: podLabels, |
| }, |
| Spec: v1.PodSpec{ |
| TerminationGracePeriodSeconds: &zero, |
| Containers: containers, |
| Volumes: volumes, |
| }, |
| }, |
| }, |
| } |
| deployment, err := client.AppsV1().Deployments(namespace).Create(d) |
| framework.ExpectNoError(err, "creating deployment %s in namespace %s", deploymentName, namespace) |
| By("Wait for the deployment to be ready") |
| err = framework.WaitForDeploymentRevisionAndImage(client, namespace, deploymentName, "1", image) |
| framework.ExpectNoError(err, "waiting for the deployment of image %s in %s in %s to complete", image, deploymentName, namespace) |
| err = framework.WaitForDeploymentComplete(client, deployment) |
| framework.ExpectNoError(err, "waiting for the deployment status valid", image, deploymentName, namespace) |
| |
| By("Deploying the webhook service") |
| |
| serviceLabels := map[string]string{"webhook": "true"} |
| service := &v1.Service{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Namespace: namespace, |
| Name: serviceName, |
| Labels: map[string]string{"test": "webhook"}, |
| }, |
| Spec: v1.ServiceSpec{ |
| Selector: serviceLabels, |
| Ports: []v1.ServicePort{ |
| { |
| Protocol: "TCP", |
| Port: 443, |
| TargetPort: intstr.FromInt(443), |
| }, |
| }, |
| }, |
| } |
| _, err = client.CoreV1().Services(namespace).Create(service) |
| framework.ExpectNoError(err, "creating service %s in namespace %s", serviceName, namespace) |
| |
| By("Verifying the service has paired with the endpoint") |
| err = framework.WaitForServiceEndpointsNum(client, namespace, serviceName, 1, 1*time.Second, 30*time.Second) |
| framework.ExpectNoError(err, "waiting for service %s/%s have %d endpoint", namespace, serviceName, 1) |
| } |
| |
| func strPtr(s string) *string { return &s } |
| |
| func registerWebhook(f *framework.Framework, context *certContext) func() { |
| client := f.ClientSet |
| By("Registering the webhook via the AdmissionRegistration API") |
| |
| namespace := f.Namespace.Name |
| configName := webhookConfigName |
| // A webhook that cannot talk to server, with fail-open policy |
| failOpenHook := failingWebhook(namespace, "fail-open.k8s.io") |
| policyIgnore := v1beta1.Ignore |
| failOpenHook.FailurePolicy = &policyIgnore |
| |
| _, err := client.AdmissionregistrationV1beta1().ValidatingWebhookConfigurations().Create(&v1beta1.ValidatingWebhookConfiguration{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: configName, |
| }, |
| Webhooks: []v1beta1.Webhook{ |
| { |
| Name: "deny-unwanted-pod-container-name-and-label.k8s.io", |
| Rules: []v1beta1.RuleWithOperations{{ |
| Operations: []v1beta1.OperationType{v1beta1.Create}, |
| Rule: v1beta1.Rule{ |
| APIGroups: []string{""}, |
| APIVersions: []string{"v1"}, |
| Resources: []string{"pods"}, |
| }, |
| }}, |
| ClientConfig: v1beta1.WebhookClientConfig{ |
| Service: &v1beta1.ServiceReference{ |
| Namespace: namespace, |
| Name: serviceName, |
| Path: strPtr("/pods"), |
| }, |
| CABundle: context.signingCert, |
| }, |
| }, |
| { |
| Name: "deny-unwanted-configmap-data.k8s.io", |
| Rules: []v1beta1.RuleWithOperations{{ |
| Operations: []v1beta1.OperationType{v1beta1.Create, v1beta1.Update}, |
| Rule: v1beta1.Rule{ |
| APIGroups: []string{""}, |
| APIVersions: []string{"v1"}, |
| Resources: []string{"configmaps"}, |
| }, |
| }}, |
| // The webhook skips the namespace that has label "skip-webhook-admission":"yes" |
| NamespaceSelector: &metav1.LabelSelector{ |
| MatchExpressions: []metav1.LabelSelectorRequirement{ |
| { |
| Key: skipNamespaceLabelKey, |
| Operator: metav1.LabelSelectorOpNotIn, |
| Values: []string{skipNamespaceLabelValue}, |
| }, |
| }, |
| }, |
| ClientConfig: v1beta1.WebhookClientConfig{ |
| Service: &v1beta1.ServiceReference{ |
| Namespace: namespace, |
| Name: serviceName, |
| Path: strPtr("/configmaps"), |
| }, |
| CABundle: context.signingCert, |
| }, |
| }, |
| // Server cannot talk to this webhook, so it always fails. |
| // Because this webhook is configured fail-open, request should be admitted after the call fails. |
| failOpenHook, |
| }, |
| }) |
| framework.ExpectNoError(err, "registering webhook config %s with namespace %s", configName, namespace) |
| |
| // The webhook configuration is honored in 10s. |
| time.Sleep(10 * time.Second) |
| |
| return func() { |
| client.AdmissionregistrationV1beta1().ValidatingWebhookConfigurations().Delete(configName, nil) |
| } |
| } |
| |
| func registerWebhookForAttachingPod(f *framework.Framework, context *certContext) func() { |
| client := f.ClientSet |
| By("Registering the webhook via the AdmissionRegistration API") |
| |
| namespace := f.Namespace.Name |
| configName := attachingPodWebhookConfigName |
| // A webhook that cannot talk to server, with fail-open policy |
| failOpenHook := failingWebhook(namespace, "fail-open.k8s.io") |
| policyIgnore := v1beta1.Ignore |
| failOpenHook.FailurePolicy = &policyIgnore |
| |
| _, err := client.AdmissionregistrationV1beta1().ValidatingWebhookConfigurations().Create(&v1beta1.ValidatingWebhookConfiguration{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: configName, |
| }, |
| Webhooks: []v1beta1.Webhook{ |
| { |
| Name: "deny-attaching-pod.k8s.io", |
| Rules: []v1beta1.RuleWithOperations{{ |
| Operations: []v1beta1.OperationType{v1beta1.Connect}, |
| Rule: v1beta1.Rule{ |
| APIGroups: []string{""}, |
| APIVersions: []string{"v1"}, |
| Resources: []string{"pods/attach"}, |
| }, |
| }}, |
| ClientConfig: v1beta1.WebhookClientConfig{ |
| Service: &v1beta1.ServiceReference{ |
| Namespace: namespace, |
| Name: serviceName, |
| Path: strPtr("/pods/attach"), |
| }, |
| CABundle: context.signingCert, |
| }, |
| }, |
| }, |
| }) |
| framework.ExpectNoError(err, "registering webhook config %s with namespace %s", configName, namespace) |
| |
| // The webhook configuration is honored in 10s. |
| time.Sleep(10 * time.Second) |
| |
| return func() { |
| client.AdmissionregistrationV1beta1().ValidatingWebhookConfigurations().Delete(configName, nil) |
| } |
| } |
| |
| func registerMutatingWebhookForConfigMap(f *framework.Framework, context *certContext) func() { |
| client := f.ClientSet |
| By("Registering the mutating configmap webhook via the AdmissionRegistration API") |
| |
| namespace := f.Namespace.Name |
| configName := mutatingWebhookConfigName |
| |
| _, err := client.AdmissionregistrationV1beta1().MutatingWebhookConfigurations().Create(&v1beta1.MutatingWebhookConfiguration{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: configName, |
| }, |
| Webhooks: []v1beta1.Webhook{ |
| { |
| Name: "adding-configmap-data-stage-1.k8s.io", |
| Rules: []v1beta1.RuleWithOperations{{ |
| Operations: []v1beta1.OperationType{v1beta1.Create}, |
| Rule: v1beta1.Rule{ |
| APIGroups: []string{""}, |
| APIVersions: []string{"v1"}, |
| Resources: []string{"configmaps"}, |
| }, |
| }}, |
| ClientConfig: v1beta1.WebhookClientConfig{ |
| Service: &v1beta1.ServiceReference{ |
| Namespace: namespace, |
| Name: serviceName, |
| Path: strPtr("/mutating-configmaps"), |
| }, |
| CABundle: context.signingCert, |
| }, |
| }, |
| { |
| Name: "adding-configmap-data-stage-2.k8s.io", |
| Rules: []v1beta1.RuleWithOperations{{ |
| Operations: []v1beta1.OperationType{v1beta1.Create}, |
| Rule: v1beta1.Rule{ |
| APIGroups: []string{""}, |
| APIVersions: []string{"v1"}, |
| Resources: []string{"configmaps"}, |
| }, |
| }}, |
| ClientConfig: v1beta1.WebhookClientConfig{ |
| Service: &v1beta1.ServiceReference{ |
| Namespace: namespace, |
| Name: serviceName, |
| Path: strPtr("/mutating-configmaps"), |
| }, |
| CABundle: context.signingCert, |
| }, |
| }, |
| }, |
| }) |
| framework.ExpectNoError(err, "registering mutating webhook config %s with namespace %s", configName, namespace) |
| |
| // The webhook configuration is honored in 10s. |
| time.Sleep(10 * time.Second) |
| return func() { client.AdmissionregistrationV1beta1().MutatingWebhookConfigurations().Delete(configName, nil) } |
| } |
| |
| func testMutatingConfigMapWebhook(f *framework.Framework) { |
| By("create a configmap that should be updated by the webhook") |
| client := f.ClientSet |
| configMap := toBeMutatedConfigMap(f) |
| mutatedConfigMap, err := client.CoreV1().ConfigMaps(f.Namespace.Name).Create(configMap) |
| Expect(err).To(BeNil()) |
| expectedConfigMapData := map[string]string{ |
| "mutation-start": "yes", |
| "mutation-stage-1": "yes", |
| "mutation-stage-2": "yes", |
| } |
| if !reflect.DeepEqual(expectedConfigMapData, mutatedConfigMap.Data) { |
| framework.Failf("\nexpected %#v\n, got %#v\n", expectedConfigMapData, mutatedConfigMap.Data) |
| } |
| } |
| |
| func registerMutatingWebhookForPod(f *framework.Framework, context *certContext) func() { |
| client := f.ClientSet |
| By("Registering the mutating pod webhook via the AdmissionRegistration API") |
| |
| namespace := f.Namespace.Name |
| configName := podMutatingWebhookConfigName |
| |
| _, err := client.AdmissionregistrationV1beta1().MutatingWebhookConfigurations().Create(&v1beta1.MutatingWebhookConfiguration{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: configName, |
| }, |
| Webhooks: []v1beta1.Webhook{ |
| { |
| Name: "adding-init-container.k8s.io", |
| Rules: []v1beta1.RuleWithOperations{{ |
| Operations: []v1beta1.OperationType{v1beta1.Create}, |
| Rule: v1beta1.Rule{ |
| APIGroups: []string{""}, |
| APIVersions: []string{"v1"}, |
| Resources: []string{"pods"}, |
| }, |
| }}, |
| ClientConfig: v1beta1.WebhookClientConfig{ |
| Service: &v1beta1.ServiceReference{ |
| Namespace: namespace, |
| Name: serviceName, |
| Path: strPtr("/mutating-pods"), |
| }, |
| CABundle: context.signingCert, |
| }, |
| }, |
| }, |
| }) |
| framework.ExpectNoError(err, "registering mutating webhook config %s with namespace %s", configName, namespace) |
| |
| // The webhook configuration is honored in 10s. |
| time.Sleep(10 * time.Second) |
| |
| return func() { client.AdmissionregistrationV1beta1().MutatingWebhookConfigurations().Delete(configName, nil) } |
| } |
| |
| func testMutatingPodWebhook(f *framework.Framework) { |
| By("create a pod that should be updated by the webhook") |
| client := f.ClientSet |
| configMap := toBeMutatedPod(f) |
| mutatedPod, err := client.CoreV1().Pods(f.Namespace.Name).Create(configMap) |
| Expect(err).To(BeNil()) |
| if len(mutatedPod.Spec.InitContainers) != 1 { |
| framework.Failf("expect pod to have 1 init container, got %#v", mutatedPod.Spec.InitContainers) |
| } |
| if got, expected := mutatedPod.Spec.InitContainers[0].Name, "webhook-added-init-container"; got != expected { |
| framework.Failf("expect the init container name to be %q, got %q", expected, got) |
| } |
| if got, expected := mutatedPod.Spec.InitContainers[0].TerminationMessagePolicy, v1.TerminationMessageReadFile; got != expected { |
| framework.Failf("expect the init terminationMessagePolicy to be default to %q, got %q", expected, got) |
| } |
| } |
| |
| func toBeMutatedPod(f *framework.Framework) *v1.Pod { |
| return &v1.Pod{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "webhook-to-be-mutated", |
| }, |
| Spec: v1.PodSpec{ |
| Containers: []v1.Container{ |
| { |
| Name: "example", |
| Image: imageutils.GetPauseImageName(), |
| }, |
| }, |
| }, |
| } |
| } |
| |
| func testWebhook(f *framework.Framework) { |
| By("create a pod that should be denied by the webhook") |
| client := f.ClientSet |
| // Creating the pod, the request should be rejected |
| pod := nonCompliantPod(f) |
| _, err := client.CoreV1().Pods(f.Namespace.Name).Create(pod) |
| Expect(err).To(HaveOccurred(), "create pod %s in namespace %s should have been denied by webhook", pod.Name, f.Namespace.Name) |
| expectedErrMsg1 := "the pod contains unwanted container name" |
| if !strings.Contains(err.Error(), expectedErrMsg1) { |
| framework.Failf("expect error contains %q, got %q", expectedErrMsg1, err.Error()) |
| } |
| expectedErrMsg2 := "the pod contains unwanted label" |
| if !strings.Contains(err.Error(), expectedErrMsg2) { |
| framework.Failf("expect error contains %q, got %q", expectedErrMsg2, err.Error()) |
| } |
| |
| By("create a pod that causes the webhook to hang") |
| client = f.ClientSet |
| // Creating the pod, the request should be rejected |
| pod = hangingPod(f) |
| _, err = client.CoreV1().Pods(f.Namespace.Name).Create(pod) |
| Expect(err).To(HaveOccurred(), "create pod %s in namespace %s should have caused webhook to hang", pod.Name, f.Namespace.Name) |
| expectedTimeoutErr := "request did not complete within" |
| if !strings.Contains(err.Error(), expectedTimeoutErr) { |
| framework.Failf("expect timeout error %q, got %q", expectedTimeoutErr, err.Error()) |
| } |
| |
| By("create a configmap that should be denied by the webhook") |
| // Creating the configmap, the request should be rejected |
| configmap := nonCompliantConfigMap(f) |
| _, err = client.CoreV1().ConfigMaps(f.Namespace.Name).Create(configmap) |
| Expect(err).To(HaveOccurred(), "create configmap %s in namespace %s should have been denied by the webhook", configmap.Name, f.Namespace.Name) |
| expectedErrMsg := "the configmap contains unwanted key and value" |
| if !strings.Contains(err.Error(), expectedErrMsg) { |
| framework.Failf("expect error contains %q, got %q", expectedErrMsg, err.Error()) |
| } |
| |
| By("create a configmap that should be admitted by the webhook") |
| // Creating the configmap, the request should be admitted |
| configmap = &v1.ConfigMap{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: allowedConfigMapName, |
| }, |
| Data: map[string]string{ |
| "admit": "this", |
| }, |
| } |
| _, err = client.CoreV1().ConfigMaps(f.Namespace.Name).Create(configmap) |
| Expect(err).NotTo(HaveOccurred(), "failed to create configmap %s in namespace: %s", configmap.Name, f.Namespace.Name) |
| |
| By("update (PUT) the admitted configmap to a non-compliant one should be rejected by the webhook") |
| toNonCompliantFn := func(cm *v1.ConfigMap) { |
| if cm.Data == nil { |
| cm.Data = map[string]string{} |
| } |
| cm.Data["webhook-e2e-test"] = "webhook-disallow" |
| } |
| _, err = updateConfigMap(client, f.Namespace.Name, allowedConfigMapName, toNonCompliantFn) |
| Expect(err).To(HaveOccurred(), "update (PUT) admitted configmap %s in namespace %s to a non-compliant one should be rejected by webhook", allowedConfigMapName, f.Namespace.Name) |
| if !strings.Contains(err.Error(), expectedErrMsg) { |
| framework.Failf("expect error contains %q, got %q", expectedErrMsg, err.Error()) |
| } |
| |
| By("update (PATCH) the admitted configmap to a non-compliant one should be rejected by the webhook") |
| patch := nonCompliantConfigMapPatch() |
| _, err = client.CoreV1().ConfigMaps(f.Namespace.Name).Patch(allowedConfigMapName, types.StrategicMergePatchType, []byte(patch)) |
| Expect(err).To(HaveOccurred(), "update admitted configmap %s in namespace %s by strategic merge patch to a non-compliant one should be rejected by webhook. Patch: %+v", allowedConfigMapName, f.Namespace.Name, patch) |
| if !strings.Contains(err.Error(), expectedErrMsg) { |
| framework.Failf("expect error contains %q, got %q", expectedErrMsg, err.Error()) |
| } |
| |
| By("create a namespace that bypass the webhook") |
| err = createNamespace(f, &v1.Namespace{ObjectMeta: metav1.ObjectMeta{ |
| Name: skippedNamespaceName, |
| Labels: map[string]string{ |
| skipNamespaceLabelKey: skipNamespaceLabelValue, |
| }, |
| }}) |
| framework.ExpectNoError(err, "creating namespace %q", skippedNamespaceName) |
| // clean up the namespace |
| defer client.CoreV1().Namespaces().Delete(skippedNamespaceName, nil) |
| |
| By("create a configmap that violates the webhook policy but is in a whitelisted namespace") |
| configmap = nonCompliantConfigMap(f) |
| _, err = client.CoreV1().ConfigMaps(skippedNamespaceName).Create(configmap) |
| Expect(err).NotTo(HaveOccurred(), "failed to create configmap %s in namespace: %s", configmap.Name, skippedNamespaceName) |
| } |
| |
| func testAttachingPodWebhook(f *framework.Framework) { |
| By("create a pod") |
| client := f.ClientSet |
| pod := toBeAttachedPod(f) |
| _, err := client.CoreV1().Pods(f.Namespace.Name).Create(pod) |
| Expect(err).NotTo(HaveOccurred(), "failed to create pod %s in namespace: %s", pod.Name, f.Namespace.Name) |
| err = framework.WaitForPodNameRunningInNamespace(client, pod.Name, f.Namespace.Name) |
| Expect(err).NotTo(HaveOccurred(), "error while waiting for pod %s to go to Running phase in namespace: %s", pod.Name, f.Namespace.Name) |
| |
| By("'kubectl attach' the pod, should be denied by the webhook") |
| timer := time.NewTimer(30 * time.Second) |
| defer timer.Stop() |
| _, err = framework.NewKubectlCommand("attach", fmt.Sprintf("--namespace=%v", f.Namespace.Name), pod.Name, "-i", "-c=container1").WithTimeout(timer.C).Exec() |
| Expect(err).To(HaveOccurred(), "'kubectl attach' the pod, should be denied by the webhook") |
| if e, a := "attaching to pod 'to-be-attached-pod' is not allowed", err.Error(); !strings.Contains(a, e) { |
| framework.Failf("unexpected 'kubectl attach' error message. expected to contain %q, got %q", e, a) |
| } |
| } |
| |
| // failingWebhook returns a webhook with rule of create configmaps, |
| // but with an invalid client config so that server cannot communicate with it |
| func failingWebhook(namespace, name string) v1beta1.Webhook { |
| return v1beta1.Webhook{ |
| Name: name, |
| Rules: []v1beta1.RuleWithOperations{{ |
| Operations: []v1beta1.OperationType{v1beta1.Create}, |
| Rule: v1beta1.Rule{ |
| APIGroups: []string{""}, |
| APIVersions: []string{"v1"}, |
| Resources: []string{"configmaps"}, |
| }, |
| }}, |
| ClientConfig: v1beta1.WebhookClientConfig{ |
| Service: &v1beta1.ServiceReference{ |
| Namespace: namespace, |
| Name: serviceName, |
| Path: strPtr("/configmaps"), |
| }, |
| // Without CA bundle, the call to webhook always fails |
| CABundle: nil, |
| }, |
| } |
| } |
| |
| func registerFailClosedWebhook(f *framework.Framework, context *certContext) func() { |
| client := f.ClientSet |
| By("Registering a webhook that server cannot talk to, with fail closed policy, via the AdmissionRegistration API") |
| |
| namespace := f.Namespace.Name |
| configName := webhookFailClosedConfigName |
| // A webhook that cannot talk to server, with fail-closed policy |
| policyFail := v1beta1.Fail |
| hook := failingWebhook(namespace, "fail-closed.k8s.io") |
| hook.FailurePolicy = &policyFail |
| hook.NamespaceSelector = &metav1.LabelSelector{ |
| MatchExpressions: []metav1.LabelSelectorRequirement{ |
| { |
| Key: failNamespaceLabelKey, |
| Operator: metav1.LabelSelectorOpIn, |
| Values: []string{failNamespaceLabelValue}, |
| }, |
| }, |
| } |
| |
| _, err := client.AdmissionregistrationV1beta1().ValidatingWebhookConfigurations().Create(&v1beta1.ValidatingWebhookConfiguration{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: configName, |
| }, |
| Webhooks: []v1beta1.Webhook{ |
| // Server cannot talk to this webhook, so it always fails. |
| // Because this webhook is configured fail-closed, request should be rejected after the call fails. |
| hook, |
| }, |
| }) |
| framework.ExpectNoError(err, "registering webhook config %s with namespace %s", configName, namespace) |
| |
| // The webhook configuration is honored in 10s. |
| time.Sleep(10 * time.Second) |
| return func() { |
| f.ClientSet.AdmissionregistrationV1beta1().ValidatingWebhookConfigurations().Delete(configName, nil) |
| } |
| } |
| |
| func testFailClosedWebhook(f *framework.Framework) { |
| client := f.ClientSet |
| By("create a namespace for the webhook") |
| err := createNamespace(f, &v1.Namespace{ObjectMeta: metav1.ObjectMeta{ |
| Name: failNamespaceName, |
| Labels: map[string]string{ |
| failNamespaceLabelKey: failNamespaceLabelValue, |
| }, |
| }}) |
| framework.ExpectNoError(err, "creating namespace %q", failNamespaceName) |
| defer client.CoreV1().Namespaces().Delete(failNamespaceName, nil) |
| |
| By("create a configmap should be unconditionally rejected by the webhook") |
| configmap := &v1.ConfigMap{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "foo", |
| }, |
| } |
| _, err = client.CoreV1().ConfigMaps(failNamespaceName).Create(configmap) |
| Expect(err).To(HaveOccurred(), "create configmap in namespace: %s should be unconditionally rejected by the webhook", failNamespaceName) |
| if !errors.IsInternalError(err) { |
| framework.Failf("expect an internal error, got %#v", err) |
| } |
| } |
| |
| func registerValidatingWebhookForWebhookConfigurations(f *framework.Framework, context *certContext) func() { |
| var err error |
| client := f.ClientSet |
| By("Registering a validating webhook on ValidatingWebhookConfiguration and MutatingWebhookConfiguration objects, via the AdmissionRegistration API") |
| |
| namespace := f.Namespace.Name |
| configName := validatingWebhookForWebhooksConfigName |
| failurePolicy := v1beta1.Fail |
| |
| // This webhook denies all requests to Delete validating webhook configuration and |
| // mutating webhook configuration objects. It should never be called, however, because |
| // dynamic admission webhooks should not be called on requests involving webhook configuration objects. |
| _, err = client.AdmissionregistrationV1beta1().ValidatingWebhookConfigurations().Create(&v1beta1.ValidatingWebhookConfiguration{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: configName, |
| }, |
| Webhooks: []v1beta1.Webhook{ |
| { |
| Name: "deny-webhook-configuration-deletions.k8s.io", |
| Rules: []v1beta1.RuleWithOperations{{ |
| Operations: []v1beta1.OperationType{v1beta1.Delete}, |
| Rule: v1beta1.Rule{ |
| APIGroups: []string{"admissionregistration.k8s.io"}, |
| APIVersions: []string{"*"}, |
| Resources: []string{ |
| "validatingwebhookconfigurations", |
| "mutatingwebhookconfigurations", |
| }, |
| }, |
| }}, |
| ClientConfig: v1beta1.WebhookClientConfig{ |
| Service: &v1beta1.ServiceReference{ |
| Namespace: namespace, |
| Name: serviceName, |
| Path: strPtr("/always-deny"), |
| }, |
| CABundle: context.signingCert, |
| }, |
| FailurePolicy: &failurePolicy, |
| }, |
| }, |
| }) |
| framework.ExpectNoError(err, "registering webhook config %s with namespace %s", configName, namespace) |
| |
| // The webhook configuration is honored in 10s. |
| time.Sleep(10 * time.Second) |
| return func() { |
| err := client.AdmissionregistrationV1beta1().ValidatingWebhookConfigurations().Delete(configName, nil) |
| framework.ExpectNoError(err, "deleting webhook config %s with namespace %s", configName, namespace) |
| } |
| } |
| |
| func registerMutatingWebhookForWebhookConfigurations(f *framework.Framework, context *certContext) func() { |
| var err error |
| client := f.ClientSet |
| By("Registering a mutating webhook on ValidatingWebhookConfiguration and MutatingWebhookConfiguration objects, via the AdmissionRegistration API") |
| |
| namespace := f.Namespace.Name |
| configName := mutatingWebhookForWebhooksConfigName |
| failurePolicy := v1beta1.Fail |
| |
| // This webhook adds a label to all requests create to validating webhook configuration and |
| // mutating webhook configuration objects. It should never be called, however, because |
| // dynamic admission webhooks should not be called on requests involving webhook configuration objects. |
| _, err = client.AdmissionregistrationV1beta1().MutatingWebhookConfigurations().Create(&v1beta1.MutatingWebhookConfiguration{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: configName, |
| }, |
| Webhooks: []v1beta1.Webhook{ |
| { |
| Name: "add-label-to-webhook-configurations.k8s.io", |
| Rules: []v1beta1.RuleWithOperations{{ |
| Operations: []v1beta1.OperationType{v1beta1.Create}, |
| Rule: v1beta1.Rule{ |
| APIGroups: []string{"admissionregistration.k8s.io"}, |
| APIVersions: []string{"*"}, |
| Resources: []string{ |
| "validatingwebhookconfigurations", |
| "mutatingwebhookconfigurations", |
| }, |
| }, |
| }}, |
| ClientConfig: v1beta1.WebhookClientConfig{ |
| Service: &v1beta1.ServiceReference{ |
| Namespace: namespace, |
| Name: serviceName, |
| Path: strPtr("/add-label"), |
| }, |
| CABundle: context.signingCert, |
| }, |
| FailurePolicy: &failurePolicy, |
| }, |
| }, |
| }) |
| framework.ExpectNoError(err, "registering webhook config %s with namespace %s", configName, namespace) |
| |
| // The webhook configuration is honored in 10s. |
| time.Sleep(10 * time.Second) |
| return func() { |
| err := client.AdmissionregistrationV1beta1().MutatingWebhookConfigurations().Delete(configName, nil) |
| framework.ExpectNoError(err, "deleting webhook config %s with namespace %s", configName, namespace) |
| } |
| } |
| |
| // This test assumes that the deletion-rejecting webhook defined in |
| // registerValidatingWebhookForWebhookConfigurations and the webhook-config-mutating |
| // webhook defined in registerMutatingWebhookForWebhookConfigurations already exist. |
| func testWebhooksForWebhookConfigurations(f *framework.Framework) { |
| var err error |
| client := f.ClientSet |
| By("Creating a dummy validating-webhook-configuration object") |
| |
| namespace := f.Namespace.Name |
| failurePolicy := v1beta1.Ignore |
| |
| mutatedValidatingWebhookConfiguration, err := client.AdmissionregistrationV1beta1().ValidatingWebhookConfigurations().Create(&v1beta1.ValidatingWebhookConfiguration{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: dummyValidatingWebhookConfigName, |
| }, |
| Webhooks: []v1beta1.Webhook{ |
| { |
| Name: "dummy-validating-webhook.k8s.io", |
| Rules: []v1beta1.RuleWithOperations{{ |
| Operations: []v1beta1.OperationType{v1beta1.Create}, |
| // This will not match any real resources so this webhook should never be called. |
| Rule: v1beta1.Rule{ |
| APIGroups: []string{""}, |
| APIVersions: []string{"v1"}, |
| Resources: []string{"invalid"}, |
| }, |
| }}, |
| ClientConfig: v1beta1.WebhookClientConfig{ |
| Service: &v1beta1.ServiceReference{ |
| Namespace: namespace, |
| Name: serviceName, |
| // This path not recognized by the webhook service, |
| // so the call to this webhook will always fail, |
| // but because the failure policy is ignore, it will |
| // have no effect on admission requests. |
| Path: strPtr(""), |
| }, |
| CABundle: nil, |
| }, |
| FailurePolicy: &failurePolicy, |
| }, |
| }, |
| }) |
| framework.ExpectNoError(err, "registering webhook config %s with namespace %s", dummyValidatingWebhookConfigName, namespace) |
| if mutatedValidatingWebhookConfiguration.ObjectMeta.Labels != nil && mutatedValidatingWebhookConfiguration.ObjectMeta.Labels[addedLabelKey] == addedLabelValue { |
| framework.Failf("expected %s not to be mutated by mutating webhooks but it was", dummyValidatingWebhookConfigName) |
| } |
| |
| // The webhook configuration is honored in 10s. |
| time.Sleep(10 * time.Second) |
| |
| By("Deleting the validating-webhook-configuration, which should be possible to remove") |
| |
| err = client.AdmissionregistrationV1beta1().ValidatingWebhookConfigurations().Delete(dummyValidatingWebhookConfigName, nil) |
| framework.ExpectNoError(err, "deleting webhook config %s with namespace %s", dummyValidatingWebhookConfigName, namespace) |
| |
| By("Creating a dummy mutating-webhook-configuration object") |
| |
| mutatedMutatingWebhookConfiguration, err := client.AdmissionregistrationV1beta1().MutatingWebhookConfigurations().Create(&v1beta1.MutatingWebhookConfiguration{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: dummyMutatingWebhookConfigName, |
| }, |
| Webhooks: []v1beta1.Webhook{ |
| { |
| Name: "dummy-mutating-webhook.k8s.io", |
| Rules: []v1beta1.RuleWithOperations{{ |
| Operations: []v1beta1.OperationType{v1beta1.Create}, |
| // This will not match any real resources so this webhook should never be called. |
| Rule: v1beta1.Rule{ |
| APIGroups: []string{""}, |
| APIVersions: []string{"v1"}, |
| Resources: []string{"invalid"}, |
| }, |
| }}, |
| ClientConfig: v1beta1.WebhookClientConfig{ |
| Service: &v1beta1.ServiceReference{ |
| Namespace: namespace, |
| Name: serviceName, |
| // This path not recognized by the webhook service, |
| // so the call to this webhook will always fail, |
| // but because the failure policy is ignore, it will |
| // have no effect on admission requests. |
| Path: strPtr(""), |
| }, |
| CABundle: nil, |
| }, |
| FailurePolicy: &failurePolicy, |
| }, |
| }, |
| }) |
| framework.ExpectNoError(err, "registering webhook config %s with namespace %s", dummyMutatingWebhookConfigName, namespace) |
| if mutatedMutatingWebhookConfiguration.ObjectMeta.Labels != nil && mutatedMutatingWebhookConfiguration.ObjectMeta.Labels[addedLabelKey] == addedLabelValue { |
| framework.Failf("expected %s not to be mutated by mutating webhooks but it was", dummyMutatingWebhookConfigName) |
| } |
| |
| // The webhook configuration is honored in 10s. |
| time.Sleep(10 * time.Second) |
| |
| By("Deleting the mutating-webhook-configuration, which should be possible to remove") |
| |
| err = client.AdmissionregistrationV1beta1().MutatingWebhookConfigurations().Delete(dummyMutatingWebhookConfigName, nil) |
| framework.ExpectNoError(err, "deleting webhook config %s with namespace %s", dummyMutatingWebhookConfigName, namespace) |
| } |
| |
| func createNamespace(f *framework.Framework, ns *v1.Namespace) error { |
| return wait.PollImmediate(100*time.Millisecond, 30*time.Second, func() (bool, error) { |
| _, err := f.ClientSet.CoreV1().Namespaces().Create(ns) |
| if err != nil { |
| if strings.HasPrefix(err.Error(), "object is being deleted:") { |
| return false, nil |
| } |
| return false, err |
| } |
| return true, nil |
| }) |
| } |
| |
| func nonCompliantPod(f *framework.Framework) *v1.Pod { |
| return &v1.Pod{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: disallowedPodName, |
| Labels: map[string]string{ |
| "webhook-e2e-test": "webhook-disallow", |
| }, |
| }, |
| Spec: v1.PodSpec{ |
| Containers: []v1.Container{ |
| { |
| Name: "webhook-disallow", |
| Image: imageutils.GetPauseImageName(), |
| }, |
| }, |
| }, |
| } |
| } |
| |
| func hangingPod(f *framework.Framework) *v1.Pod { |
| return &v1.Pod{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: hangingPodName, |
| Labels: map[string]string{ |
| "webhook-e2e-test": "wait-forever", |
| }, |
| }, |
| Spec: v1.PodSpec{ |
| Containers: []v1.Container{ |
| { |
| Name: "wait-forever", |
| Image: imageutils.GetPauseImageName(), |
| }, |
| }, |
| }, |
| } |
| } |
| |
| func toBeAttachedPod(f *framework.Framework) *v1.Pod { |
| return &v1.Pod{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: toBeAttachedPodName, |
| }, |
| Spec: v1.PodSpec{ |
| Containers: []v1.Container{ |
| { |
| Name: "container1", |
| Image: imageutils.GetPauseImageName(), |
| }, |
| }, |
| }, |
| } |
| } |
| |
| func nonCompliantConfigMap(f *framework.Framework) *v1.ConfigMap { |
| return &v1.ConfigMap{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: disallowedConfigMapName, |
| }, |
| Data: map[string]string{ |
| "webhook-e2e-test": "webhook-disallow", |
| }, |
| } |
| } |
| |
| func toBeMutatedConfigMap(f *framework.Framework) *v1.ConfigMap { |
| return &v1.ConfigMap{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "to-be-mutated", |
| }, |
| Data: map[string]string{ |
| "mutation-start": "yes", |
| }, |
| } |
| } |
| |
| func nonCompliantConfigMapPatch() string { |
| return fmt.Sprint(`{"data":{"webhook-e2e-test":"webhook-disallow"}}`) |
| } |
| |
| type updateConfigMapFn func(cm *v1.ConfigMap) |
| |
| func updateConfigMap(c clientset.Interface, ns, name string, update updateConfigMapFn) (*v1.ConfigMap, error) { |
| var cm *v1.ConfigMap |
| pollErr := wait.PollImmediate(2*time.Second, 1*time.Minute, func() (bool, error) { |
| var err error |
| if cm, err = c.CoreV1().ConfigMaps(ns).Get(name, metav1.GetOptions{}); err != nil { |
| return false, err |
| } |
| update(cm) |
| if cm, err = c.CoreV1().ConfigMaps(ns).Update(cm); err == nil { |
| return true, nil |
| } |
| // Only retry update on conflict |
| if !errors.IsConflict(err) { |
| return false, err |
| } |
| return false, nil |
| }) |
| return cm, pollErr |
| } |
| |
| func cleanWebhookTest(client clientset.Interface, namespaceName string) { |
| _ = client.CoreV1().Services(namespaceName).Delete(serviceName, nil) |
| _ = client.AppsV1().Deployments(namespaceName).Delete(deploymentName, nil) |
| _ = client.CoreV1().Secrets(namespaceName).Delete(secretName, nil) |
| _ = client.RbacV1beta1().RoleBindings("kube-system").Delete(roleBindingName, nil) |
| } |
| |
| func registerWebhookForCustomResource(f *framework.Framework, context *certContext, testcrd *framework.TestCrd) func() { |
| client := f.ClientSet |
| By("Registering the custom resource webhook via the AdmissionRegistration API") |
| |
| namespace := f.Namespace.Name |
| configName := crWebhookConfigName |
| _, err := client.AdmissionregistrationV1beta1().ValidatingWebhookConfigurations().Create(&v1beta1.ValidatingWebhookConfiguration{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: configName, |
| }, |
| Webhooks: []v1beta1.Webhook{ |
| { |
| Name: "deny-unwanted-custom-resource-data.k8s.io", |
| Rules: []v1beta1.RuleWithOperations{{ |
| Operations: []v1beta1.OperationType{v1beta1.Create}, |
| Rule: v1beta1.Rule{ |
| APIGroups: []string{testcrd.ApiGroup}, |
| APIVersions: testcrd.GetAPIVersions(), |
| Resources: []string{testcrd.GetPluralName()}, |
| }, |
| }}, |
| ClientConfig: v1beta1.WebhookClientConfig{ |
| Service: &v1beta1.ServiceReference{ |
| Namespace: namespace, |
| Name: serviceName, |
| Path: strPtr("/custom-resource"), |
| }, |
| CABundle: context.signingCert, |
| }, |
| }, |
| }, |
| }) |
| framework.ExpectNoError(err, "registering custom resource webhook config %s with namespace %s", configName, namespace) |
| |
| // The webhook configuration is honored in 10s. |
| time.Sleep(10 * time.Second) |
| return func() { |
| client.AdmissionregistrationV1beta1().ValidatingWebhookConfigurations().Delete(configName, nil) |
| } |
| } |
| |
| func registerMutatingWebhookForCustomResource(f *framework.Framework, context *certContext, testcrd *framework.TestCrd) func() { |
| client := f.ClientSet |
| By("Registering the mutating webhook for a custom resource via the AdmissionRegistration API") |
| |
| namespace := f.Namespace.Name |
| configName := crMutatingWebhookConfigName |
| _, err := client.AdmissionregistrationV1beta1().MutatingWebhookConfigurations().Create(&v1beta1.MutatingWebhookConfiguration{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: configName, |
| }, |
| Webhooks: []v1beta1.Webhook{ |
| { |
| Name: "mutate-custom-resource-data-stage-1.k8s.io", |
| Rules: []v1beta1.RuleWithOperations{{ |
| Operations: []v1beta1.OperationType{v1beta1.Create}, |
| Rule: v1beta1.Rule{ |
| APIGroups: []string{testcrd.ApiGroup}, |
| APIVersions: testcrd.GetAPIVersions(), |
| Resources: []string{testcrd.GetPluralName()}, |
| }, |
| }}, |
| ClientConfig: v1beta1.WebhookClientConfig{ |
| Service: &v1beta1.ServiceReference{ |
| Namespace: namespace, |
| Name: serviceName, |
| Path: strPtr("/mutating-custom-resource"), |
| }, |
| CABundle: context.signingCert, |
| }, |
| }, |
| { |
| Name: "mutate-custom-resource-data-stage-2.k8s.io", |
| Rules: []v1beta1.RuleWithOperations{{ |
| Operations: []v1beta1.OperationType{v1beta1.Create}, |
| Rule: v1beta1.Rule{ |
| APIGroups: []string{testcrd.ApiGroup}, |
| APIVersions: testcrd.GetAPIVersions(), |
| Resources: []string{testcrd.GetPluralName()}, |
| }, |
| }}, |
| ClientConfig: v1beta1.WebhookClientConfig{ |
| Service: &v1beta1.ServiceReference{ |
| Namespace: namespace, |
| Name: serviceName, |
| Path: strPtr("/mutating-custom-resource"), |
| }, |
| CABundle: context.signingCert, |
| }, |
| }, |
| }, |
| }) |
| framework.ExpectNoError(err, "registering custom resource webhook config %s with namespace %s", configName, namespace) |
| |
| // The webhook configuration is honored in 10s. |
| time.Sleep(10 * time.Second) |
| |
| return func() { client.AdmissionregistrationV1beta1().MutatingWebhookConfigurations().Delete(configName, nil) } |
| } |
| |
| func testCustomResourceWebhook(f *framework.Framework, crd *apiextensionsv1beta1.CustomResourceDefinition, customResourceClient dynamic.ResourceInterface) { |
| By("Creating a custom resource that should be denied by the webhook") |
| crInstanceName := "cr-instance-1" |
| crInstance := &unstructured.Unstructured{ |
| Object: map[string]interface{}{ |
| "kind": crd.Spec.Names.Kind, |
| "apiVersion": crd.Spec.Group + "/" + crd.Spec.Version, |
| "metadata": map[string]interface{}{ |
| "name": crInstanceName, |
| "namespace": f.Namespace.Name, |
| }, |
| "data": map[string]interface{}{ |
| "webhook-e2e-test": "webhook-disallow", |
| }, |
| }, |
| } |
| _, err := customResourceClient.Create(crInstance, metav1.CreateOptions{}) |
| Expect(err).To(HaveOccurred(), "create custom resource %s in namespace %s should be denied by webhook", crInstanceName, f.Namespace.Name) |
| expectedErrMsg := "the custom resource contains unwanted data" |
| if !strings.Contains(err.Error(), expectedErrMsg) { |
| framework.Failf("expect error contains %q, got %q", expectedErrMsg, err.Error()) |
| } |
| } |
| |
| func testMutatingCustomResourceWebhook(f *framework.Framework, crd *apiextensionsv1beta1.CustomResourceDefinition, customResourceClient dynamic.ResourceInterface) { |
| By("Creating a custom resource that should be mutated by the webhook") |
| crName := "cr-instance-1" |
| cr := &unstructured.Unstructured{ |
| Object: map[string]interface{}{ |
| "kind": crd.Spec.Names.Kind, |
| "apiVersion": crd.Spec.Group + "/" + crd.Spec.Version, |
| "metadata": map[string]interface{}{ |
| "name": crName, |
| "namespace": f.Namespace.Name, |
| }, |
| "data": map[string]interface{}{ |
| "mutation-start": "yes", |
| }, |
| }, |
| } |
| mutatedCR, err := customResourceClient.Create(cr, metav1.CreateOptions{}) |
| Expect(err).NotTo(HaveOccurred(), "failed to create custom resource %s in namespace: %s", crName, f.Namespace.Name) |
| expectedCRData := map[string]interface{}{ |
| "mutation-start": "yes", |
| "mutation-stage-1": "yes", |
| "mutation-stage-2": "yes", |
| } |
| if !reflect.DeepEqual(expectedCRData, mutatedCR.Object["data"]) { |
| framework.Failf("\nexpected %#v\n, got %#v\n", expectedCRData, mutatedCR.Object["data"]) |
| } |
| } |
| |
| func registerValidatingWebhookForCRD(f *framework.Framework, context *certContext) func() { |
| client := f.ClientSet |
| By("Registering the crd webhook via the AdmissionRegistration API") |
| |
| namespace := f.Namespace.Name |
| configName := crdWebhookConfigName |
| |
| // This webhook will deny the creation of CustomResourceDefinitions which have the |
| // label "webhook-e2e-test":"webhook-disallow" |
| // NOTE: Because tests are run in parallel and in an unpredictable order, it is critical |
| // that no other test attempts to create CRD with that label. |
| _, err := client.AdmissionregistrationV1beta1().ValidatingWebhookConfigurations().Create(&v1beta1.ValidatingWebhookConfiguration{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: configName, |
| }, |
| Webhooks: []v1beta1.Webhook{ |
| { |
| Name: "deny-crd-with-unwanted-label.k8s.io", |
| Rules: []v1beta1.RuleWithOperations{{ |
| Operations: []v1beta1.OperationType{v1beta1.Create}, |
| Rule: v1beta1.Rule{ |
| APIGroups: []string{"apiextensions.k8s.io"}, |
| APIVersions: []string{"*"}, |
| Resources: []string{"customresourcedefinitions"}, |
| }, |
| }}, |
| ClientConfig: v1beta1.WebhookClientConfig{ |
| Service: &v1beta1.ServiceReference{ |
| Namespace: namespace, |
| Name: serviceName, |
| Path: strPtr("/crd"), |
| }, |
| CABundle: context.signingCert, |
| }, |
| }, |
| }, |
| }) |
| framework.ExpectNoError(err, "registering crd webhook config %s with namespace %s", configName, namespace) |
| |
| // The webhook configuration is honored in 10s. |
| time.Sleep(10 * time.Second) |
| return func() { |
| client.AdmissionregistrationV1beta1().ValidatingWebhookConfigurations().Delete(configName, nil) |
| } |
| } |
| |
| func testCRDDenyWebhook(f *framework.Framework) { |
| By("Creating a custom resource definition that should be denied by the webhook") |
| name := fmt.Sprintf("e2e-test-%s-%s-crd", f.BaseName, "deny") |
| kind := fmt.Sprintf("E2e-test-%s-%s-crd", f.BaseName, "deny") |
| group := fmt.Sprintf("%s-crd-test.k8s.io", f.BaseName) |
| apiVersions := []apiextensionsv1beta1.CustomResourceDefinitionVersion{ |
| { |
| Name: "v1", |
| Served: true, |
| Storage: true, |
| }, |
| } |
| testcrd := &framework.TestCrd{ |
| Name: name, |
| Kind: kind, |
| ApiGroup: group, |
| Versions: apiVersions, |
| } |
| |
| // Creating a custom resource definition for use by assorted tests. |
| config, err := framework.LoadConfig() |
| if err != nil { |
| framework.Failf("failed to load config: %v", err) |
| return |
| } |
| apiExtensionClient, err := crdclientset.NewForConfig(config) |
| if err != nil { |
| framework.Failf("failed to initialize apiExtensionClient: %v", err) |
| return |
| } |
| crd := &apiextensionsv1beta1.CustomResourceDefinition{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: testcrd.GetMetaName(), |
| Labels: map[string]string{ |
| "webhook-e2e-test": "webhook-disallow", |
| }, |
| }, |
| Spec: apiextensionsv1beta1.CustomResourceDefinitionSpec{ |
| Group: testcrd.ApiGroup, |
| Versions: testcrd.Versions, |
| Names: apiextensionsv1beta1.CustomResourceDefinitionNames{ |
| Plural: testcrd.GetPluralName(), |
| Singular: testcrd.Name, |
| Kind: testcrd.Kind, |
| ListKind: testcrd.GetListName(), |
| }, |
| Scope: apiextensionsv1beta1.NamespaceScoped, |
| }, |
| } |
| |
| // create CRD |
| _, err = apiExtensionClient.ApiextensionsV1beta1().CustomResourceDefinitions().Create(crd) |
| Expect(err).To(HaveOccurred(), "create custom resource definition %s should be denied by webhook", testcrd.GetMetaName()) |
| expectedErrMsg := "the crd contains unwanted label" |
| if !strings.Contains(err.Error(), expectedErrMsg) { |
| framework.Failf("expect error contains %q, got %q", expectedErrMsg, err.Error()) |
| } |
| } |