123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734 |
- /*
- 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"
- 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"
- e2edeploy "k8s.io/kubernetes/test/e2e/framework/deployment"
- e2elog "k8s.io/kubernetes/test/e2e/framework/log"
- "k8s.io/kubernetes/test/utils/crd"
- imageutils "k8s.io/kubernetes/test/utils/image"
- "k8s.io/utils/pointer"
- "github.com/onsi/ginkgo"
- "github.com/onsi/gomega"
- // ensure libs have a chance to initialize
- _ "github.com/stretchr/testify/assert"
- )
- const (
- secretName = "sample-webhook-secret"
- deploymentName = "sample-webhook-deployment"
- serviceName = "e2e-test-webhook"
- servicePort = 8443
- 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"
- 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"
- slowWebhookConfigName = "e2e-test-webhook-config-slow"
- skipNamespaceLabelKey = "skip-webhook-admission"
- skipNamespaceLabelValue = "yes"
- skippedNamespaceName = "exempted-namesapce"
- disallowedPodName = "disallowed-pod"
- toBeAttachedPodName = "to-be-attached-pod"
- hangingPodName = "hanging-pod"
- disallowedConfigMapName = "disallowed-configmap"
- nonDeletableConfigmapName = "nondeletable-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
- ginkgo.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")
- }
- ginkgo.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)
- })
- ginkgo.AfterEach(func() {
- cleanWebhookTest(client, namespaceName)
- })
- ginkgo.It("Should be able to deny pod and configmap creation", func() {
- webhookCleanup := registerWebhook(f, context)
- defer webhookCleanup()
- testWebhook(f)
- })
- ginkgo.It("Should be able to deny attaching pod", func() {
- webhookCleanup := registerWebhookForAttachingPod(f, context)
- defer webhookCleanup()
- testAttachingPodWebhook(f)
- })
- ginkgo.It("Should be able to deny custom resource creation and deletion", func() {
- testcrd, err := crd.CreateTestCRD(f)
- if err != nil {
- return
- }
- defer testcrd.CleanUp()
- webhookCleanup := registerWebhookForCustomResource(f, context, testcrd)
- defer webhookCleanup()
- testCustomResourceWebhook(f, testcrd.Crd, testcrd.DynamicClients["v1"])
- testBlockingCustomResourceDeletion(f, testcrd.Crd, testcrd.DynamicClients["v1"])
- })
- ginkgo.It("Should unconditionally reject operations on fail closed webhook", func() {
- webhookCleanup := registerFailClosedWebhook(f, context)
- defer webhookCleanup()
- testFailClosedWebhook(f)
- })
- ginkgo.It("Should mutate configmap", func() {
- webhookCleanup := registerMutatingWebhookForConfigMap(f, context)
- defer webhookCleanup()
- testMutatingConfigMapWebhook(f)
- })
- ginkgo.It("Should mutate pod and apply defaults after mutation", func() {
- webhookCleanup := registerMutatingWebhookForPod(f, context)
- defer webhookCleanup()
- testMutatingPodWebhook(f)
- })
- ginkgo.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)
- })
- ginkgo.It("Should mutate custom resource", func() {
- testcrd, err := crd.CreateTestCRD(f)
- if err != nil {
- return
- }
- defer testcrd.CleanUp()
- webhookCleanup := registerMutatingWebhookForCustomResource(f, context, testcrd)
- defer webhookCleanup()
- testMutatingCustomResourceWebhook(f, testcrd.Crd, testcrd.DynamicClients["v1"], false)
- })
- ginkgo.It("Should deny crd creation", func() {
- crdWebhookCleanup := registerValidatingWebhookForCRD(f, context)
- defer crdWebhookCleanup()
- testCRDDenyWebhook(f)
- })
- ginkgo.It("Should mutate custom resource with different stored version", func() {
- testcrd, err := createAdmissionWebhookMultiVersionTestCRDWithV1Storage(f)
- if err != nil {
- return
- }
- defer testcrd.CleanUp()
- webhookCleanup := registerMutatingWebhookForCustomResource(f, context, testcrd)
- defer webhookCleanup()
- testMultiVersionCustomResourceWebhook(f, testcrd)
- })
- ginkgo.It("Should mutate custom resource with pruning", func() {
- const prune = true
- testcrd, err := createAdmissionWebhookMultiVersionTestCRDWithV1Storage(f, func(crd *apiextensionsv1beta1.CustomResourceDefinition) {
- crd.Spec.PreserveUnknownFields = pointer.BoolPtr(false)
- crd.Spec.Validation = &apiextensionsv1beta1.CustomResourceValidation{
- OpenAPIV3Schema: &apiextensionsv1beta1.JSONSchemaProps{
- Type: "object",
- Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
- "data": {
- Type: "object",
- Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
- "mutation-start": {Type: "string"},
- "mutation-stage-1": {Type: "string"},
- // mutation-stage-2 is intentionally missing such that it is pruned
- },
- },
- },
- },
- }
- })
- if err != nil {
- return
- }
- defer testcrd.CleanUp()
- webhookCleanup := registerMutatingWebhookForCustomResource(f, context, testcrd)
- defer webhookCleanup()
- testMutatingCustomResourceWebhook(f, testcrd.Crd, testcrd.DynamicClients["v1"], prune)
- })
- ginkgo.It("Should honor timeout", func() {
- policyFail := v1beta1.Fail
- policyIgnore := v1beta1.Ignore
- ginkgo.By("Setting timeout (1s) shorter than webhook latency (5s)")
- slowWebhookCleanup := registerSlowWebhook(f, context, &policyFail, pointer.Int32Ptr(1))
- testSlowWebhookTimeoutFailEarly(f)
- slowWebhookCleanup()
- ginkgo.By("Having no error when timeout is shorter than webhook latency and failure policy is ignore")
- slowWebhookCleanup = registerSlowWebhook(f, context, &policyIgnore, pointer.Int32Ptr(1))
- testSlowWebhookTimeoutNoError(f)
- slowWebhookCleanup()
- ginkgo.By("Having no error when timeout is longer than webhook latency")
- slowWebhookCleanup = registerSlowWebhook(f, context, &policyFail, pointer.Int32Ptr(10))
- testSlowWebhookTimeoutNoError(f)
- slowWebhookCleanup()
- ginkgo.By("Having no error when timeout is empty (defaulted to 10s in v1beta1)")
- slowWebhookCleanup = registerSlowWebhook(f, context, &policyFail, nil)
- testSlowWebhookTimeoutNoError(f)
- slowWebhookCleanup()
- })
- // 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. mutating webhook that fail-open v.s. fail-closed
- })
- func createAuthReaderRoleBinding(f *framework.Framework, namespace string) {
- ginkgo.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) {
- e2elog.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) {
- ginkgo.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)
- ginkgo.By("Wait for the deployment to be ready")
- err = e2edeploy.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 = e2edeploy.WaitForDeploymentComplete(client, deployment)
- framework.ExpectNoError(err, "waiting for the deployment status valid", image, deploymentName, namespace)
- ginkgo.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: servicePort,
- TargetPort: intstr.FromInt(443),
- },
- },
- },
- }
- _, err = client.CoreV1().Services(namespace).Create(service)
- framework.ExpectNoError(err, "creating service %s in namespace %s", serviceName, namespace)
- ginkgo.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
- ginkgo.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.ValidatingWebhook{
- {
- 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"),
- Port: pointer.Int32Ptr(servicePort),
- },
- CABundle: context.signingCert,
- },
- },
- {
- Name: "deny-unwanted-configmap-data.k8s.io",
- Rules: []v1beta1.RuleWithOperations{{
- Operations: []v1beta1.OperationType{v1beta1.Create, v1beta1.Update, v1beta1.Delete},
- 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"),
- Port: pointer.Int32Ptr(servicePort),
- },
- 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
- ginkgo.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.ValidatingWebhook{
- {
- 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"),
- Port: pointer.Int32Ptr(servicePort),
- },
- 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
- ginkgo.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.MutatingWebhook{
- {
- 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"),
- Port: pointer.Int32Ptr(servicePort),
- },
- 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"),
- Port: pointer.Int32Ptr(servicePort),
- },
- 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) {
- ginkgo.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)
- gomega.Expect(err).To(gomega.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
- ginkgo.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.MutatingWebhook{
- {
- 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"),
- Port: pointer.Int32Ptr(servicePort),
- },
- 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) {
- ginkgo.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)
- gomega.Expect(err).To(gomega.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) {
- ginkgo.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)
- framework.ExpectError(err, "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())
- }
- ginkgo.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)
- framework.ExpectError(err, "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())
- }
- ginkgo.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)
- framework.ExpectError(err, "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())
- }
- ginkgo.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)
- framework.ExpectNoError(err, "failed to create configmap %s in namespace: %s", configmap.Name, f.Namespace.Name)
- ginkgo.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)
- framework.ExpectError(err, "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())
- }
- ginkgo.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))
- framework.ExpectError(err, "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())
- }
- ginkgo.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)
- ginkgo.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)
- framework.ExpectNoError(err, "failed to create configmap %s in namespace: %s", configmap.Name, skippedNamespaceName)
- }
- func testBlockingConfigmapDeletion(f *framework.Framework) {
- ginkgo.By("create a configmap that should be denied by the webhook when deleting")
- client := f.ClientSet
- configmap := nonDeletableConfigmap(f)
- _, err := client.CoreV1().ConfigMaps(f.Namespace.Name).Create(configmap)
- framework.ExpectNoError(err, "failed to create configmap %s in namespace: %s", configmap.Name, f.Namespace.Name)
- ginkgo.By("deleting the configmap should be denied by the webhook")
- err = client.CoreV1().ConfigMaps(f.Namespace.Name).Delete(configmap.Name, &metav1.DeleteOptions{})
- framework.ExpectError(err, "deleting configmap %s in namespace: %s should be denied", configmap.Name, f.Namespace.Name)
- expectedErrMsg1 := "the configmap cannot be deleted because it contains unwanted key and value"
- if !strings.Contains(err.Error(), expectedErrMsg1) {
- framework.Failf("expect error contains %q, got %q", expectedErrMsg1, err.Error())
- }
- ginkgo.By("remove the offending key and value from the configmap data")
- toCompliantFn := func(cm *v1.ConfigMap) {
- if cm.Data == nil {
- cm.Data = map[string]string{}
- }
- cm.Data["webhook-e2e-test"] = "webhook-allow"
- }
- _, err = updateConfigMap(client, f.Namespace.Name, configmap.Name, toCompliantFn)
- framework.ExpectNoError(err, "failed to update configmap %s in namespace: %s", configmap.Name, f.Namespace.Name)
- ginkgo.By("deleting the updated configmap should be successful")
- err = client.CoreV1().ConfigMaps(f.Namespace.Name).Delete(configmap.Name, &metav1.DeleteOptions{})
- framework.ExpectNoError(err, "failed to delete configmap %s in namespace: %s", configmap.Name, f.Namespace.Name)
- }
- func testAttachingPodWebhook(f *framework.Framework) {
- ginkgo.By("create a pod")
- client := f.ClientSet
- pod := toBeAttachedPod(f)
- _, err := client.CoreV1().Pods(f.Namespace.Name).Create(pod)
- framework.ExpectNoError(err, "failed to create pod %s in namespace: %s", pod.Name, f.Namespace.Name)
- err = framework.WaitForPodNameRunningInNamespace(client, pod.Name, f.Namespace.Name)
- framework.ExpectNoError(err, "error while waiting for pod %s to go to Running phase in namespace: %s", pod.Name, f.Namespace.Name)
- ginkgo.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()
- framework.ExpectError(err, "'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.ValidatingWebhook {
- return v1beta1.ValidatingWebhook{
- 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"),
- Port: pointer.Int32Ptr(servicePort),
- },
- // Without CA bundle, the call to webhook always fails
- CABundle: nil,
- },
- }
- }
- func registerFailClosedWebhook(f *framework.Framework, context *certContext) func() {
- client := f.ClientSet
- ginkgo.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.ValidatingWebhook{
- // 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
- ginkgo.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)
- ginkgo.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)
- framework.ExpectError(err, "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
- ginkgo.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.ValidatingWebhook{
- {
- 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"),
- Port: pointer.Int32Ptr(servicePort),
- },
- 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
- ginkgo.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.MutatingWebhook{
- {
- 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"),
- Port: pointer.Int32Ptr(servicePort),
- },
- 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
- ginkgo.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.ValidatingWebhook{
- {
- 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(""),
- Port: pointer.Int32Ptr(servicePort),
- },
- 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)
- ginkgo.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)
- ginkgo.By("Creating a dummy mutating-webhook-configuration object")
- mutatedMutatingWebhookConfiguration, err := client.AdmissionregistrationV1beta1().MutatingWebhookConfigurations().Create(&v1beta1.MutatingWebhookConfiguration{
- ObjectMeta: metav1.ObjectMeta{
- Name: dummyMutatingWebhookConfigName,
- },
- Webhooks: []v1beta1.MutatingWebhook{
- {
- 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(""),
- Port: pointer.Int32Ptr(servicePort),
- },
- 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)
- ginkgo.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 nonDeletableConfigmap(f *framework.Framework) *v1.ConfigMap {
- return &v1.ConfigMap{
- ObjectMeta: metav1.ObjectMeta{
- Name: nonDeletableConfigmapName,
- },
- Data: map[string]string{
- "webhook-e2e-test": "webhook-nondeletable",
- },
- }
- }
- 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
- }
- type updateCustomResourceFn func(cm *unstructured.Unstructured)
- func updateCustomResource(c dynamic.ResourceInterface, ns, name string, update updateCustomResourceFn) (*unstructured.Unstructured, error) {
- var cr *unstructured.Unstructured
- pollErr := wait.PollImmediate(2*time.Second, 1*time.Minute, func() (bool, error) {
- var err error
- if cr, err = c.Get(name, metav1.GetOptions{}); err != nil {
- return false, err
- }
- update(cr)
- if cr, err = c.Update(cr, metav1.UpdateOptions{}); err == nil {
- return true, nil
- }
- // Only retry update on conflict
- if !errors.IsConflict(err) {
- return false, err
- }
- return false, nil
- })
- return cr, 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 *crd.TestCrd) func() {
- client := f.ClientSet
- ginkgo.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.ValidatingWebhook{
- {
- Name: "deny-unwanted-custom-resource-data.k8s.io",
- Rules: []v1beta1.RuleWithOperations{{
- Operations: []v1beta1.OperationType{v1beta1.Create, v1beta1.Update, v1beta1.Delete},
- Rule: v1beta1.Rule{
- APIGroups: []string{testcrd.Crd.Spec.Group},
- APIVersions: servedAPIVersions(testcrd.Crd),
- Resources: []string{testcrd.Crd.Spec.Names.Plural},
- },
- }},
- ClientConfig: v1beta1.WebhookClientConfig{
- Service: &v1beta1.ServiceReference{
- Namespace: namespace,
- Name: serviceName,
- Path: strPtr("/custom-resource"),
- Port: pointer.Int32Ptr(servicePort),
- },
- 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 *crd.TestCrd) func() {
- client := f.ClientSet
- ginkgo.By(fmt.Sprintf("Registering the mutating webhook for custom resource %s via the AdmissionRegistration API", testcrd.Crd.Name))
- namespace := f.Namespace.Name
- configName := f.UniqueName
- _, err := client.AdmissionregistrationV1beta1().MutatingWebhookConfigurations().Create(&v1beta1.MutatingWebhookConfiguration{
- ObjectMeta: metav1.ObjectMeta{
- Name: configName,
- },
- Webhooks: []v1beta1.MutatingWebhook{
- {
- Name: "mutate-custom-resource-data-stage-1.k8s.io",
- Rules: []v1beta1.RuleWithOperations{{
- Operations: []v1beta1.OperationType{v1beta1.Create, v1beta1.Update},
- Rule: v1beta1.Rule{
- APIGroups: []string{testcrd.Crd.Spec.Group},
- APIVersions: servedAPIVersions(testcrd.Crd),
- Resources: []string{testcrd.Crd.Spec.Names.Plural},
- },
- }},
- ClientConfig: v1beta1.WebhookClientConfig{
- Service: &v1beta1.ServiceReference{
- Namespace: namespace,
- Name: serviceName,
- Path: strPtr("/mutating-custom-resource"),
- Port: pointer.Int32Ptr(servicePort),
- },
- 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.Crd.Spec.Group},
- APIVersions: servedAPIVersions(testcrd.Crd),
- Resources: []string{testcrd.Crd.Spec.Names.Plural},
- },
- }},
- ClientConfig: v1beta1.WebhookClientConfig{
- Service: &v1beta1.ServiceReference{
- Namespace: namespace,
- Name: serviceName,
- Path: strPtr("/mutating-custom-resource"),
- Port: pointer.Int32Ptr(servicePort),
- },
- 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) {
- ginkgo.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{})
- framework.ExpectError(err, "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 testBlockingCustomResourceDeletion(f *framework.Framework, crd *apiextensionsv1beta1.CustomResourceDefinition, customResourceClient dynamic.ResourceInterface) {
- ginkgo.By("Creating a custom resource whose deletion would be denied by the webhook")
- crInstanceName := "cr-instance-2"
- 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-nondeletable",
- },
- },
- }
- _, err := customResourceClient.Create(crInstance, metav1.CreateOptions{})
- framework.ExpectNoError(err, "failed to create custom resource %s in namespace: %s", crInstanceName, f.Namespace.Name)
- ginkgo.By("Deleting the custom resource should be denied")
- err = customResourceClient.Delete(crInstanceName, &metav1.DeleteOptions{})
- framework.ExpectError(err, "deleting custom resource %s in namespace: %s should be denied", crInstanceName, f.Namespace.Name)
- expectedErrMsg1 := "the custom resource cannot be deleted because it contains unwanted key and value"
- if !strings.Contains(err.Error(), expectedErrMsg1) {
- framework.Failf("expect error contains %q, got %q", expectedErrMsg1, err.Error())
- }
- ginkgo.By("Remove the offending key and value from the custom resource data")
- toCompliantFn := func(cr *unstructured.Unstructured) {
- if _, ok := cr.Object["data"]; !ok {
- cr.Object["data"] = map[string]interface{}{}
- }
- data := cr.Object["data"].(map[string]interface{})
- data["webhook-e2e-test"] = "webhook-allow"
- }
- _, err = updateCustomResource(customResourceClient, f.Namespace.Name, crInstanceName, toCompliantFn)
- framework.ExpectNoError(err, "failed to update custom resource %s in namespace: %s", crInstanceName, f.Namespace.Name)
- ginkgo.By("Deleting the updated custom resource should be successful")
- err = customResourceClient.Delete(crInstanceName, &metav1.DeleteOptions{})
- framework.ExpectNoError(err, "failed to delete custom resource %s in namespace: %s", crInstanceName, f.Namespace.Name)
- }
- func testMutatingCustomResourceWebhook(f *framework.Framework, crd *apiextensionsv1beta1.CustomResourceDefinition, customResourceClient dynamic.ResourceInterface, prune bool) {
- ginkgo.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{})
- framework.ExpectNoError(err, "failed to create custom resource %s in namespace: %s", crName, f.Namespace.Name)
- expectedCRData := map[string]interface{}{
- "mutation-start": "yes",
- "mutation-stage-1": "yes",
- }
- if !prune {
- expectedCRData["mutation-stage-2"] = "yes"
- }
- if !reflect.DeepEqual(expectedCRData, mutatedCR.Object["data"]) {
- framework.Failf("\nexpected %#v\n, got %#v\n", expectedCRData, mutatedCR.Object["data"])
- }
- }
- func testMultiVersionCustomResourceWebhook(f *framework.Framework, testcrd *crd.TestCrd) {
- customResourceClient := testcrd.DynamicClients["v1"]
- ginkgo.By("Creating a custom resource while v1 is storage version")
- crName := "cr-instance-1"
- cr := &unstructured.Unstructured{
- Object: map[string]interface{}{
- "kind": testcrd.Crd.Spec.Names.Kind,
- "apiVersion": testcrd.Crd.Spec.Group + "/" + testcrd.Crd.Spec.Version,
- "metadata": map[string]interface{}{
- "name": crName,
- "namespace": f.Namespace.Name,
- },
- "data": map[string]interface{}{
- "mutation-start": "yes",
- },
- },
- }
- _, err := customResourceClient.Create(cr, metav1.CreateOptions{})
- framework.ExpectNoError(err, "failed to create custom resource %s in namespace: %s", crName, f.Namespace.Name)
- ginkgo.By("Patching Custom Resource Definition to set v2 as storage")
- apiVersionWithV2StoragePatch := fmt.Sprint(`{"spec": {"versions": [{"name": "v1", "storage": false, "served": true},{"name": "v2", "storage": true, "served": true}]}}`)
- _, err = testcrd.APIExtensionClient.ApiextensionsV1beta1().CustomResourceDefinitions().Patch(testcrd.Crd.Name, types.StrategicMergePatchType, []byte(apiVersionWithV2StoragePatch))
- framework.ExpectNoError(err, "failed to patch custom resource definition %s in namespace: %s", testcrd.Crd.Name, f.Namespace.Name)
- ginkgo.By("Patching the custom resource while v2 is storage version")
- crDummyPatch := fmt.Sprint(`[{ "op": "add", "path": "/dummy", "value": "test" }]`)
- _, err = testcrd.DynamicClients["v2"].Patch(crName, types.JSONPatchType, []byte(crDummyPatch), metav1.PatchOptions{})
- framework.ExpectNoError(err, "failed to patch custom resource %s in namespace: %s", crName, f.Namespace.Name)
- }
- func registerValidatingWebhookForCRD(f *framework.Framework, context *certContext) func() {
- client := f.ClientSet
- ginkgo.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.ValidatingWebhook{
- {
- 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"),
- Port: pointer.Int32Ptr(servicePort),
- },
- 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) {
- ginkgo.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,
- },
- }
- // 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: name + "s." + group,
- Labels: map[string]string{
- "webhook-e2e-test": "webhook-disallow",
- },
- },
- Spec: apiextensionsv1beta1.CustomResourceDefinitionSpec{
- Group: group,
- Versions: apiVersions,
- Names: apiextensionsv1beta1.CustomResourceDefinitionNames{
- Singular: name,
- Kind: kind,
- ListKind: kind + "List",
- Plural: name + "s",
- },
- Scope: apiextensionsv1beta1.NamespaceScoped,
- },
- }
- // create CRD
- _, err = apiExtensionClient.ApiextensionsV1beta1().CustomResourceDefinitions().Create(crd)
- framework.ExpectError(err, "create custom resource definition %s should be denied by webhook", crd.Name)
- expectedErrMsg := "the crd contains unwanted label"
- if !strings.Contains(err.Error(), expectedErrMsg) {
- framework.Failf("expect error contains %q, got %q", expectedErrMsg, err.Error())
- }
- }
- func registerSlowWebhook(f *framework.Framework, context *certContext, policy *v1beta1.FailurePolicyType, timeout *int32) func() {
- client := f.ClientSet
- ginkgo.By("Registering slow webhook via the AdmissionRegistration API")
- namespace := f.Namespace.Name
- configName := slowWebhookConfigName
- // Add a unique label to the namespace
- ns, err := client.CoreV1().Namespaces().Get(namespace, metav1.GetOptions{})
- framework.ExpectNoError(err, "error getting namespace %s", namespace)
- if ns.Labels == nil {
- ns.Labels = map[string]string{}
- }
- ns.Labels[slowWebhookConfigName] = namespace
- _, err = client.CoreV1().Namespaces().Update(ns)
- framework.ExpectNoError(err, "error labeling namespace %s", namespace)
- _, err = client.AdmissionregistrationV1beta1().ValidatingWebhookConfigurations().Create(&v1beta1.ValidatingWebhookConfiguration{
- ObjectMeta: metav1.ObjectMeta{
- Name: configName,
- },
- Webhooks: []v1beta1.ValidatingWebhook{
- {
- Name: "allow-configmap-with-delay-webhook.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("/always-allow-delay-5s"),
- Port: pointer.Int32Ptr(servicePort),
- },
- CABundle: context.signingCert,
- },
- // Scope the webhook to just this namespace
- NamespaceSelector: &metav1.LabelSelector{
- MatchLabels: ns.Labels,
- },
- FailurePolicy: policy,
- TimeoutSeconds: timeout,
- },
- },
- })
- framework.ExpectNoError(err, "registering slow 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 testSlowWebhookTimeoutFailEarly(f *framework.Framework) {
- ginkgo.By("Request fails when timeout (1s) is shorter than slow webhook latency (5s)")
- client := f.ClientSet
- name := "e2e-test-slow-webhook-configmap"
- _, err := client.CoreV1().ConfigMaps(f.Namespace.Name).Create(&v1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: name}})
- framework.ExpectError(err, "create configmap in namespace %s should have timed-out reaching slow webhook", f.Namespace.Name)
- expectedErrMsg := `/always-allow-delay-5s?timeout=1s: context deadline exceeded`
- if !strings.Contains(err.Error(), expectedErrMsg) {
- framework.Failf("expect error contains %q, got %q", expectedErrMsg, err.Error())
- }
- }
- func testSlowWebhookTimeoutNoError(f *framework.Framework) {
- client := f.ClientSet
- name := "e2e-test-slow-webhook-configmap"
- _, err := client.CoreV1().ConfigMaps(f.Namespace.Name).Create(&v1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: name}})
- gomega.Expect(err).To(gomega.BeNil())
- err = client.CoreV1().ConfigMaps(f.Namespace.Name).Delete(name, &metav1.DeleteOptions{})
- gomega.Expect(err).To(gomega.BeNil())
- }
- // createAdmissionWebhookMultiVersionTestCRDWithV1Storage creates a new CRD specifically
- // for the admissin webhook calling test.
- func createAdmissionWebhookMultiVersionTestCRDWithV1Storage(f *framework.Framework, opts ...crd.Option) (*crd.TestCrd, error) {
- group := fmt.Sprintf("%s-multiversion-crd-test.k8s.io", f.BaseName)
- return crd.CreateMultiVersionTestCRD(f, group, append([]crd.Option{func(crd *apiextensionsv1beta1.CustomResourceDefinition) {
- crd.Spec.Versions = []apiextensionsv1beta1.CustomResourceDefinitionVersion{
- {
- Name: "v1",
- Served: true,
- Storage: true,
- },
- {
- Name: "v2",
- Served: true,
- Storage: false,
- },
- }
- }}, opts...)...)
- }
- // servedAPIVersions returns the API versions served by the CRD.
- func servedAPIVersions(crd *apiextensionsv1beta1.CustomResourceDefinition) []string {
- ret := []string{}
- for _, v := range crd.Spec.Versions {
- if v.Served {
- ret = append(ret, v.Name)
- }
- }
- return ret
- }
|