From 5029bb56c434c0099fd1d2e78de7531c69430753 Mon Sep 17 00:00:00 2001 From: Chao Xu Date: Tue, 9 Jan 2018 15:58:18 -0800 Subject: [PATCH] Let mutating webhook defaults the object after applying the patch sent back by the webhook --- .../plugin/webhook/mutating/admission.go | 8 +- test/e2e/apimachinery/webhook.go | 78 +++++++++++++++++++ test/images/webhook/Makefile | 4 +- test/images/webhook/main.go | 33 ++++++++ test/utils/image/manifest.go | 2 +- 5 files changed, 120 insertions(+), 5 deletions(-) diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/mutating/admission.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/mutating/admission.go index f944152770..ec0ae942b6 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/mutating/admission.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/mutating/admission.go @@ -112,6 +112,7 @@ type MutatingWebhook struct { namespaceMatcher namespace.Matcher clientManager config.ClientManager convertor versioned.Convertor + defaulter runtime.ObjectDefaulter jsonSerializer runtime.Serializer } @@ -137,6 +138,7 @@ func (a *MutatingWebhook) SetScheme(scheme *runtime.Scheme) { Serializer: serializer.NewCodecFactory(scheme).LegacyCodec(admissionv1beta1.SchemeGroupVersion), })) a.convertor.Scheme = scheme + a.defaulter = scheme a.jsonSerializer = json.NewSerializer(json.DefaultMetaFactory, scheme, scheme, false) } } @@ -171,6 +173,9 @@ func (a *MutatingWebhook) ValidateInitialization() error { if err := a.convertor.Validate(); err != nil { return fmt.Errorf("MutatingWebhook.convertor is not properly setup: %v", err) } + if a.defaulter == nil { + return fmt.Errorf("MutatingWebhook.defaulter is not properly setup: %v") + } go a.hookSource.Run(wait.NeverStop) return nil } @@ -312,10 +317,9 @@ func (a *MutatingWebhook) callAttrMutatingHook(ctx context.Context, h *v1beta1.W if err != nil { return apierrors.NewInternalError(err) } - // TODO: if we have multiple mutating webhooks, we can remember the json - // instead of encoding and decoding for each one. if _, _, err := a.jsonSerializer.Decode(patchedJS, nil, attr.Object); err != nil { return apierrors.NewInternalError(err) } + a.defaulter.Default(attr.Object) return nil } diff --git a/test/e2e/apimachinery/webhook.go b/test/e2e/apimachinery/webhook.go index 755055c969..63d69315f1 100644 --- a/test/e2e/apimachinery/webhook.go +++ b/test/e2e/apimachinery/webhook.go @@ -133,6 +133,12 @@ var _ = SIGDescribe("AdmissionWebhook", func() { testMutatingConfigMapWebhook(f) }) + It("Should mutate pod and apply defaults after mutation", func() { + registerMutatingWebhookForPod(f, context) + defer client.AdmissionregistrationV1beta1().MutatingWebhookConfigurations().Delete(mutatingWebhookConfigName, nil) + testMutatingPodWebhook(f) + }) + It("Should mutate crd", func() { crdCleanup, dynamicClient := createCRD(f) defer crdCleanup() @@ -423,6 +429,7 @@ func registerMutatingWebhookForConfigMap(f *framework.Framework, context *certCo // The webhook configuration is honored in 1s. time.Sleep(10 * time.Second) } + func testMutatingConfigMapWebhook(f *framework.Framework) { By("create a configmap that should be updated by the webhook") client := f.ClientSet @@ -439,6 +446,77 @@ func testMutatingConfigMapWebhook(f *framework.Framework) { } } +func registerMutatingWebhookForPod(f *framework.Framework, context *certContext) { + client := f.ClientSet + By("Registering the mutating pod webhook via the AdmissionRegistration API") + + namespace := f.Namespace.Name + + _, err := client.AdmissionregistrationV1beta1().MutatingWebhookConfigurations().Create(&v1beta1.MutatingWebhookConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Name: mutatingWebhookConfigName, + }, + 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", mutatingWebhookConfigName, namespace) + + // The webhook configuration is honored in 1s. + time.Sleep(10 * time.Second) +} + +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: framework.GetPauseImageName(f.ClientSet), + }, + }, + }, + } +} + func testWebhook(f *framework.Framework) { By("create a pod that should be denied by the webhook") client := f.ClientSet diff --git a/test/images/webhook/Makefile b/test/images/webhook/Makefile index a201dd5b23..d9ce02940e 100644 --- a/test/images/webhook/Makefile +++ b/test/images/webhook/Makefile @@ -14,7 +14,7 @@ build: CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o webhook . - docker build --no-cache -t gcr.io/kubernetes-e2e-test-images/k8s-sample-admission-webhook-amd64:1.8v7 . + docker build --no-cache -t gcr.io/kubernetes-e2e-test-images/k8s-sample-admission-webhook-amd64:1.9v1 . rm -rf webhook push: - gcloud docker -- push gcr.io/kubernetes-e2e-test-images/k8s-sample-admission-webhook-amd64:1.8v7 + gcloud docker -- push gcr.io/kubernetes-e2e-test-images/k8s-sample-admission-webhook-amd64:1.9v1 diff --git a/test/images/webhook/main.go b/test/images/webhook/main.go index da2e4e9d3f..bdf68ba9c7 100644 --- a/test/images/webhook/main.go +++ b/test/images/webhook/main.go @@ -40,6 +40,9 @@ const ( patch2 string = `[ { "op": "add", "path": "/data/mutation-stage-2", "value": "yes" } ]` + addInitContainerPatch string = `[ + {"op":"add","path":"/spec/initContainers","value":[{"image":"webhook-added-image","name":"webhook-added-init-container","resources":{}}]} + ]` ) // Config contains the server (the webhook) cert and key. @@ -108,6 +111,31 @@ func admitPods(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse { return &reviewResponse } +func mutatePods(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse { + glog.V(2).Info("mutating pods") + podResource := metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"} + if ar.Request.Resource != podResource { + glog.Errorf("expect resource to be %s", podResource) + return nil + } + + raw := ar.Request.Object.Raw + pod := corev1.Pod{} + deserializer := codecs.UniversalDeserializer() + if _, _, err := deserializer.Decode(raw, nil, &pod); err != nil { + glog.Error(err) + return toAdmissionResponse(err) + } + reviewResponse := v1beta1.AdmissionResponse{} + reviewResponse.Allowed = true + if pod.Name == "webhook-to-be-mutated" { + reviewResponse.Patch = []byte(addInitContainerPatch) + pt := v1beta1.PatchTypeJSONPatch + reviewResponse.PatchType = &pt + } + return &reviewResponse +} + // deny configmaps with specific key-value pair. func admitConfigMaps(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse { glog.V(2).Info("admitting configmaps") @@ -271,6 +299,10 @@ func servePods(w http.ResponseWriter, r *http.Request) { serve(w, r, admitPods) } +func serveMutatePods(w http.ResponseWriter, r *http.Request) { + serve(w, r, mutatePods) +} + func serveConfigmaps(w http.ResponseWriter, r *http.Request) { serve(w, r, admitConfigMaps) } @@ -293,6 +325,7 @@ func main() { flag.Parse() http.HandleFunc("/pods", servePods) + http.HandleFunc("/mutating-pods", serveMutatePods) http.HandleFunc("/configmaps", serveConfigmaps) http.HandleFunc("/mutating-configmaps", serveMutateConfigmaps) http.HandleFunc("/crd", serveCRD) diff --git a/test/utils/image/manifest.go b/test/utils/image/manifest.go index 7b19ed0186..bac1716548 100644 --- a/test/utils/image/manifest.go +++ b/test/utils/image/manifest.go @@ -48,7 +48,7 @@ func (i *ImageConfig) SetVersion(version string) { } var ( - AdmissionWebhook = ImageConfig{e2eRegistry, "k8s-sample-admission-webhook", "1.8v7", true} + AdmissionWebhook = ImageConfig{e2eRegistry, "k8s-sample-admission-webhook", "1.9v1", true} APIServer = ImageConfig{e2eRegistry, "k8s-aggregator-sample-apiserver", "1.7v2", true} AppArmorLoader = ImageConfig{gcRegistry, "apparmor-loader", "0.1", false} BusyBox = ImageConfig{gcRegistry, "busybox", "1.24", false}