From 1816c85b20ca59f9e16d2ad11479352330792f83 Mon Sep 17 00:00:00 2001 From: Jason Smith Date: Wed, 11 Jul 2018 13:04:55 +0200 Subject: [PATCH] moved some code around added comments and renamed some variables to make the code easier to understand migrated to new image_util build system improved tests updated copyright headers to 2018 updated webhook version --- test/images/webhook/BUILD | 7 +- test/images/webhook/VERSION | 2 +- test/images/webhook/alwaysdeny.go | 32 +++ test/images/webhook/config.go | 31 ++- test/images/webhook/configmap.go | 92 ++++++++ test/images/webhook/crd.go | 57 +++++ test/images/webhook/customresource.go | 90 ++++++++ test/images/webhook/main.go | 296 +++----------------------- test/images/webhook/patch_test.go | 108 +++++++--- test/images/webhook/pods.go | 103 +++++++++ test/images/webhook/scheme.go | 2 +- test/utils/image/manifest.go | 2 +- 12 files changed, 500 insertions(+), 322 deletions(-) create mode 100644 test/images/webhook/alwaysdeny.go create mode 100644 test/images/webhook/configmap.go create mode 100644 test/images/webhook/crd.go create mode 100644 test/images/webhook/customresource.go create mode 100644 test/images/webhook/pods.go diff --git a/test/images/webhook/BUILD b/test/images/webhook/BUILD index c3f2e3033a9..ac9bc7669f6 100644 --- a/test/images/webhook/BUILD +++ b/test/images/webhook/BUILD @@ -3,8 +3,13 @@ load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library", "go_test") go_library( name = "go_default_library", srcs = [ + "alwaysdeny.go", "config.go", + "configmap.go", + "crd.go", + "customresource.go", "main.go", + "pods.go", "scheme.go", ], importpath = "k8s.io/kubernetes/test/images/webhook", @@ -18,8 +23,6 @@ go_library( "//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/runtime/serializer:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/runtime:go_default_library", - "//staging/src/k8s.io/client-go/kubernetes:go_default_library", - "//staging/src/k8s.io/client-go/rest:go_default_library", "//vendor/github.com/golang/glog:go_default_library", ], ) diff --git a/test/images/webhook/VERSION b/test/images/webhook/VERSION index f766b5eefe5..c4247db8f77 100644 --- a/test/images/webhook/VERSION +++ b/test/images/webhook/VERSION @@ -1 +1 @@ -1.10v2 +1.12v1 diff --git a/test/images/webhook/alwaysdeny.go b/test/images/webhook/alwaysdeny.go new file mode 100644 index 00000000000..8c8796e18e1 --- /dev/null +++ b/test/images/webhook/alwaysdeny.go @@ -0,0 +1,32 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "github.com/golang/glog" + "k8s.io/api/admission/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// alwaysDeny all requests made to this function. +func alwaysDeny(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse { + glog.V(2).Info("calling always-deny") + reviewResponse := v1beta1.AdmissionResponse{} + reviewResponse.Allowed = false + reviewResponse.Result = &metav1.Status{Message: "this webhook denies all requests"} + return &reviewResponse +} diff --git a/test/images/webhook/config.go b/test/images/webhook/config.go index c3f736eaa06..5c84a2b8ec7 100644 --- a/test/images/webhook/config.go +++ b/test/images/webhook/config.go @@ -1,5 +1,5 @@ /* -Copyright 2017 The Kubernetes Authors. +Copyright 2018 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -18,27 +18,26 @@ package main import ( "crypto/tls" - - "k8s.io/client-go/kubernetes" - "k8s.io/client-go/rest" + "flag" "github.com/golang/glog" ) -// Get a clientset with in-cluster config. -func getClient() *kubernetes.Clientset { - config, err := rest.InClusterConfig() - if err != nil { - glog.Fatal(err) - } - clientset, err := kubernetes.NewForConfig(config) - if err != nil { - glog.Fatal(err) - } - return clientset +// Config contains the server (the webhook) cert and key. +type Config struct { + CertFile string + KeyFile string } -func configTLS(config Config, clientset *kubernetes.Clientset) *tls.Config { +func (c *Config) addFlags() { + flag.StringVar(&c.CertFile, "tls-cert-file", c.CertFile, ""+ + "File containing the default x509 Certificate for HTTPS. (CA cert, if any, concatenated "+ + "after server cert).") + flag.StringVar(&c.KeyFile, "tls-private-key-file", c.KeyFile, ""+ + "File containing the default x509 private key matching --tls-cert-file.") +} + +func configTLS(config Config) *tls.Config { sCert, err := tls.LoadX509KeyPair(config.CertFile, config.KeyFile) if err != nil { glog.Fatal(err) diff --git a/test/images/webhook/configmap.go b/test/images/webhook/configmap.go new file mode 100644 index 00000000000..147c6e74854 --- /dev/null +++ b/test/images/webhook/configmap.go @@ -0,0 +1,92 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "github.com/golang/glog" + "k8s.io/api/admission/v1beta1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + configMapPatch1 string = `[ + { "op": "add", "path": "/data/mutation-stage-1", "value": "yes" } + ]` + configMapPatch2 string = `[ + { "op": "add", "path": "/data/mutation-stage-2", "value": "yes" } + ]` +) + +// deny configmaps with specific key-value pair. +func admitConfigMaps(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse { + glog.V(2).Info("admitting configmaps") + configMapResource := metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "configmaps"} + if ar.Request.Resource != configMapResource { + glog.Errorf("expect resource to be %s", configMapResource) + return nil + } + + raw := ar.Request.Object.Raw + configmap := corev1.ConfigMap{} + deserializer := codecs.UniversalDeserializer() + if _, _, err := deserializer.Decode(raw, nil, &configmap); err != nil { + glog.Error(err) + return toAdmissionResponse(err) + } + reviewResponse := v1beta1.AdmissionResponse{} + reviewResponse.Allowed = true + for k, v := range configmap.Data { + if k == "webhook-e2e-test" && v == "webhook-disallow" { + reviewResponse.Allowed = false + reviewResponse.Result = &metav1.Status{ + Reason: "the configmap contains unwanted key and value", + } + } + } + return &reviewResponse +} + +func mutateConfigmaps(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse { + glog.V(2).Info("mutating configmaps") + configMapResource := metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "configmaps"} + if ar.Request.Resource != configMapResource { + glog.Errorf("expect resource to be %s", configMapResource) + return nil + } + + raw := ar.Request.Object.Raw + configmap := corev1.ConfigMap{} + deserializer := codecs.UniversalDeserializer() + if _, _, err := deserializer.Decode(raw, nil, &configmap); err != nil { + glog.Error(err) + return toAdmissionResponse(err) + } + reviewResponse := v1beta1.AdmissionResponse{} + reviewResponse.Allowed = true + if configmap.Data["mutation-start"] == "yes" { + reviewResponse.Patch = []byte(configMapPatch1) + } + if configmap.Data["mutation-stage-1"] == "yes" { + reviewResponse.Patch = []byte(configMapPatch2) + } + + pt := v1beta1.PatchTypeJSONPatch + reviewResponse.PatchType = &pt + + return &reviewResponse +} diff --git a/test/images/webhook/crd.go b/test/images/webhook/crd.go new file mode 100644 index 00000000000..1114058c5e2 --- /dev/null +++ b/test/images/webhook/crd.go @@ -0,0 +1,57 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "fmt" + + apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/golang/glog" + "k8s.io/api/admission/v1beta1" +) + +// This function expects all CRDs submitted to it to be apiextensions.k8s.io/v1beta1 +// TODO: When apiextensions.k8s.io/v1 is added we will need to update this function. +func admitCRD(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse { + glog.V(2).Info("admitting crd") + crdResource := metav1.GroupVersionResource{Group: "apiextensions.k8s.io", Version: "v1beta1", Resource: "customresourcedefinitions"} + if ar.Request.Resource != crdResource { + err := fmt.Errorf("expect resource to be %s", crdResource) + glog.Error(err) + return toAdmissionResponse(err) + } + + raw := ar.Request.Object.Raw + crd := apiextensionsv1beta1.CustomResourceDefinition{} + deserializer := codecs.UniversalDeserializer() + if _, _, err := deserializer.Decode(raw, nil, &crd); err != nil { + glog.Error(err) + return toAdmissionResponse(err) + } + reviewResponse := v1beta1.AdmissionResponse{} + reviewResponse.Allowed = true + + if v, ok := crd.Labels["webhook-e2e-test"]; ok { + if v == "webhook-disallow" { + reviewResponse.Allowed = false + reviewResponse.Result = &metav1.Status{Message: "the crd contains unwanted label"} + } + } + return &reviewResponse +} diff --git a/test/images/webhook/customresource.go b/test/images/webhook/customresource.go new file mode 100644 index 00000000000..273ae4f08e6 --- /dev/null +++ b/test/images/webhook/customresource.go @@ -0,0 +1,90 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "encoding/json" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/golang/glog" + "k8s.io/api/admission/v1beta1" +) + +const ( + customResourcePatch1 string = `[ + { "op": "add", "path": "/data/mutation-stage-1", "value": "yes" } + ]` + customResourcePatch2 string = `[ + { "op": "add", "path": "/data/mutation-stage-2", "value": "yes" } + ]` +) + +func mutateCustomResource(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse { + glog.V(2).Info("mutating custom resource") + cr := struct { + metav1.ObjectMeta + Data map[string]string + }{} + + raw := ar.Request.Object.Raw + err := json.Unmarshal(raw, &cr) + if err != nil { + glog.Error(err) + return toAdmissionResponse(err) + } + + reviewResponse := v1beta1.AdmissionResponse{} + reviewResponse.Allowed = true + + if cr.Data["mutation-start"] == "yes" { + reviewResponse.Patch = []byte(customResourcePatch1) + } + if cr.Data["mutation-stage-1"] == "yes" { + reviewResponse.Patch = []byte(customResourcePatch2) + } + pt := v1beta1.PatchTypeJSONPatch + reviewResponse.PatchType = &pt + return &reviewResponse +} + +func admitCustomResource(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse { + glog.V(2).Info("admitting custom resource") + cr := struct { + metav1.ObjectMeta + Data map[string]string + }{} + + raw := ar.Request.Object.Raw + err := json.Unmarshal(raw, &cr) + if err != nil { + glog.Error(err) + return toAdmissionResponse(err) + } + + reviewResponse := v1beta1.AdmissionResponse{} + reviewResponse.Allowed = true + for k, v := range cr.Data { + if k == "webhook-e2e-test" && v == "webhook-disallow" { + reviewResponse.Allowed = false + reviewResponse.Result = &metav1.Status{ + Reason: "the custom resource contains unwanted data", + } + } + } + return &reviewResponse +} diff --git a/test/images/webhook/main.go b/test/images/webhook/main.go index 0b715cd60bf..d453932ed4b 100644 --- a/test/images/webhook/main.go +++ b/test/images/webhook/main.go @@ -1,5 +1,5 @@ /* -Copyright 2017 The Kubernetes Authors. +Copyright 2018 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -22,44 +22,16 @@ import ( "fmt" "io/ioutil" "net/http" - "strings" "github.com/golang/glog" "k8s.io/api/admission/v1beta1" - corev1 "k8s.io/api/core/v1" - apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" // TODO: try this library to see if it generates correct json patch // https://github.com/mattbaird/jsonpatch ) -const ( - patch1 string = `[ - { "op": "add", "path": "/data/mutation-stage-1", "value": "yes" } - ]` - 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. -type Config struct { - CertFile string - KeyFile string -} - -func (c *Config) addFlags() { - flag.StringVar(&c.CertFile, "tls-cert-file", c.CertFile, ""+ - "File containing the default x509 Certificate for HTTPS. (CA cert, if any, concatenated "+ - "after server cert).") - flag.StringVar(&c.KeyFile, "tls-private-key-file", c.KeyFile, ""+ - "File containing the default x509 private key matching --tls-cert-file.") -} - +// toAdmissionResponse is a helper function to create an AdmissionResponse +// with an embedded error func toAdmissionResponse(err error) *v1beta1.AdmissionResponse { return &v1beta1.AdmissionResponse{ Result: &metav1.Status{ @@ -68,231 +40,11 @@ func toAdmissionResponse(err error) *v1beta1.AdmissionResponse { } } -// Deny all requests made to this function. -func alwaysDeny(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse { - glog.V(2).Info("calling always-deny") - reviewResponse := v1beta1.AdmissionResponse{} - reviewResponse.Allowed = false - reviewResponse.Result = &metav1.Status{Message: "this webhook denies all requests"} - return &reviewResponse -} - -// only allow pods to pull images from specific registry. -func admitPods(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse { - glog.V(2).Info("admitting pods") - podResource := metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"} - if ar.Request.Resource != podResource { - err := fmt.Errorf("expect resource to be %s", podResource) - glog.Error(err) - return toAdmissionResponse(err) - } - - 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 - - var msg string - if v, ok := pod.Labels["webhook-e2e-test"]; ok { - if v == "webhook-disallow" { - reviewResponse.Allowed = false - msg = msg + "the pod contains unwanted label; " - } - if v == "wait-forever" { - reviewResponse.Allowed = false - msg = msg + "the pod response should not be sent; " - <-make(chan int) // Sleep forever - no one sends to this channel - } - } - for _, container := range pod.Spec.Containers { - if strings.Contains(container.Name, "webhook-disallow") { - reviewResponse.Allowed = false - msg = msg + "the pod contains unwanted container name; " - } - } - if !reviewResponse.Allowed { - reviewResponse.Result = &metav1.Status{Message: strings.TrimSpace(msg)} - } - 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") - configMapResource := metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "configmaps"} - if ar.Request.Resource != configMapResource { - glog.Errorf("expect resource to be %s", configMapResource) - return nil - } - - raw := ar.Request.Object.Raw - configmap := corev1.ConfigMap{} - deserializer := codecs.UniversalDeserializer() - if _, _, err := deserializer.Decode(raw, nil, &configmap); err != nil { - glog.Error(err) - return toAdmissionResponse(err) - } - reviewResponse := v1beta1.AdmissionResponse{} - reviewResponse.Allowed = true - for k, v := range configmap.Data { - if k == "webhook-e2e-test" && v == "webhook-disallow" { - reviewResponse.Allowed = false - reviewResponse.Result = &metav1.Status{ - Reason: "the configmap contains unwanted key and value", - } - } - } - return &reviewResponse -} - -func mutateConfigmaps(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse { - glog.V(2).Info("mutating configmaps") - configMapResource := metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "configmaps"} - if ar.Request.Resource != configMapResource { - glog.Errorf("expect resource to be %s", configMapResource) - return nil - } - - raw := ar.Request.Object.Raw - configmap := corev1.ConfigMap{} - deserializer := codecs.UniversalDeserializer() - if _, _, err := deserializer.Decode(raw, nil, &configmap); err != nil { - glog.Error(err) - return toAdmissionResponse(err) - } - reviewResponse := v1beta1.AdmissionResponse{} - reviewResponse.Allowed = true - if configmap.Data["mutation-start"] == "yes" { - reviewResponse.Patch = []byte(patch1) - } - if configmap.Data["mutation-stage-1"] == "yes" { - reviewResponse.Patch = []byte(patch2) - } - - pt := v1beta1.PatchTypeJSONPatch - reviewResponse.PatchType = &pt - - return &reviewResponse -} - -func mutateCustomResource(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse { - glog.V(2).Info("mutating custom resource") - cr := struct { - metav1.ObjectMeta - Data map[string]string - }{} - - raw := ar.Request.Object.Raw - err := json.Unmarshal(raw, &cr) - if err != nil { - glog.Error(err) - return toAdmissionResponse(err) - } - - reviewResponse := v1beta1.AdmissionResponse{} - reviewResponse.Allowed = true - - if cr.Data["mutation-start"] == "yes" { - reviewResponse.Patch = []byte(patch1) - } - if cr.Data["mutation-stage-1"] == "yes" { - reviewResponse.Patch = []byte(patch2) - } - pt := v1beta1.PatchTypeJSONPatch - reviewResponse.PatchType = &pt - return &reviewResponse -} - -func admitCustomResource(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse { - glog.V(2).Info("admitting custom resource") - cr := struct { - metav1.ObjectMeta - Data map[string]string - }{} - - raw := ar.Request.Object.Raw - err := json.Unmarshal(raw, &cr) - if err != nil { - glog.Error(err) - return toAdmissionResponse(err) - } - - reviewResponse := v1beta1.AdmissionResponse{} - reviewResponse.Allowed = true - for k, v := range cr.Data { - if k == "webhook-e2e-test" && v == "webhook-disallow" { - reviewResponse.Allowed = false - reviewResponse.Result = &metav1.Status{ - Reason: "the custom resource contains unwanted data", - } - } - } - return &reviewResponse -} - -// Deny all crds with the label "webhook-e2e-test":"webhook-disallow" -// This function expects all CRDs submitted to it to be apiextensions.k8s.io/v1beta1 -// TODO: When apiextensions.k8s.io/v1 is added we will need to update this function. -func admitCRD(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse { - glog.V(2).Info("admitting crd") - crdResource := metav1.GroupVersionResource{Group: "apiextensions.k8s.io", Version: "v1beta1", Resource: "customresourcedefinitions"} - if ar.Request.Resource != crdResource { - err := fmt.Errorf("expect resource to be %s", crdResource) - glog.Error(err) - return toAdmissionResponse(err) - } - - raw := ar.Request.Object.Raw - crd := apiextensionsv1beta1.CustomResourceDefinition{} - deserializer := codecs.UniversalDeserializer() - if _, _, err := deserializer.Decode(raw, nil, &crd); err != nil { - glog.Error(err) - return toAdmissionResponse(err) - } - reviewResponse := v1beta1.AdmissionResponse{} - reviewResponse.Allowed = true - - if v, ok := crd.Labels["webhook-e2e-test"]; ok { - if v == "webhook-disallow" { - reviewResponse.Allowed = false - reviewResponse.Result = &metav1.Status{Message: "the crd contains unwanted label"} - } - } - return &reviewResponse -} - +// admitFunc is the type we use for all of our validators and mutators type admitFunc func(v1beta1.AdmissionReview) *v1beta1.AdmissionResponse +// serve handles the http portion of a request prior to handing to an admit +// function func serve(w http.ResponseWriter, r *http.Request, admit admitFunc) { var body []byte if r.Body != nil { @@ -309,31 +61,32 @@ func serve(w http.ResponseWriter, r *http.Request, admit admitFunc) { } glog.V(2).Info(fmt.Sprintf("handling request: %v", body)) - var reviewResponse *v1beta1.AdmissionResponse - ar := v1beta1.AdmissionReview{} + + // The AdmissionReview that was sent to the webhook + requestedAdmissionReview := v1beta1.AdmissionReview{} + + // The AdmissionReview that will be returned + responseAdmissionReview := v1beta1.AdmissionReview{} + deserializer := codecs.UniversalDeserializer() - if _, _, err := deserializer.Decode(body, nil, &ar); err != nil { + if _, _, err := deserializer.Decode(body, nil, &requestedAdmissionReview); err != nil { glog.Error(err) - reviewResponse = toAdmissionResponse(err) + responseAdmissionReview.Response = toAdmissionResponse(err) } else { - reviewResponse = admit(ar) + // pass to admitFunc + responseAdmissionReview.Response = admit(requestedAdmissionReview) } - glog.V(2).Info(fmt.Sprintf("sending response: %v", reviewResponse)) - response := v1beta1.AdmissionReview{} - if reviewResponse != nil { - response.Response = reviewResponse - response.Response.UID = ar.Request.UID - } - // reset the Object and OldObject, they are not needed in a response. - ar.Request.Object = runtime.RawExtension{} - ar.Request.OldObject = runtime.RawExtension{} + // Return the same UID + responseAdmissionReview.Response.UID = requestedAdmissionReview.Request.UID - resp, err := json.Marshal(response) + glog.V(2).Info(fmt.Sprintf("sending response: %v", responseAdmissionReview.Response)) + + respBytes, err := json.Marshal(responseAdmissionReview) if err != nil { glog.Error(err) } - if _, err := w.Write(resp); err != nil { + if _, err := w.Write(respBytes); err != nil { glog.Error(err) } } @@ -383,10 +136,9 @@ func main() { http.HandleFunc("/custom-resource", serveCustomResource) http.HandleFunc("/mutating-custom-resource", serveMutateCustomResource) http.HandleFunc("/crd", serveCRD) - clientset := getClient() server := &http.Server{ Addr: ":443", - TLSConfig: configTLS(config, clientset), + TLSConfig: configTLS(config), } server.ListenAndServeTLS("", "") } diff --git a/test/images/webhook/patch_test.go b/test/images/webhook/patch_test.go index 9b11e10e2bb..6916e1f4dfd 100644 --- a/test/images/webhook/patch_test.go +++ b/test/images/webhook/patch_test.go @@ -1,5 +1,5 @@ /* -Copyright 2017 The Kubernetes Authors. +Copyright 2018 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -26,37 +26,87 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) -func TestJSONPatchForConfigMap(t *testing.T) { - cm := corev1.ConfigMap{ - Data: map[string]string{ - "mutation-start": "yes", +func TestPatches(t *testing.T) { + testCases := []struct { + patch string + initial interface{} + expected interface{} + toTest interface{} + }{ + { + patch: configMapPatch1, + initial: corev1.ConfigMap{ + Data: map[string]string{ + "mutation-start": "yes", + }, + }, + expected: &corev1.ConfigMap{ + Data: map[string]string{ + "mutation-start": "yes", + "mutation-stage-1": "yes", + }, + }, + }, + { + patch: configMapPatch2, + initial: corev1.ConfigMap{ + Data: map[string]string{ + "mutation-start": "yes", + }, + }, + expected: &corev1.ConfigMap{ + Data: map[string]string{ + "mutation-start": "yes", + "mutation-stage-2": "yes", + }, + }, + }, + + { + patch: podsInitContainerPatch, + initial: corev1.Pod{ + Spec: corev1.PodSpec{ + InitContainers: []corev1.Container{}, + }, + }, + expected: &corev1.Pod{ + Spec: corev1.PodSpec{ + InitContainers: []corev1.Container{ + { + Image: "webhook-added-image", + Name: "webhook-added-init-container", + Resources: corev1.ResourceRequirements{}, + }, + }, + }, + }, }, } - cmJS, err := json.Marshal(cm) - if err != nil { - t.Fatal(err) + for _, testcase := range testCases { + objJS, err := json.Marshal(testcase.initial) + if err != nil { + t.Fatal(err) + } + patchObj, err := jsonpatch.DecodePatch([]byte(testcase.patch)) + if err != nil { + t.Fatal(err) + } + + patchedJS, err := patchObj.Apply(objJS) + if err != nil { + t.Fatal(err) + } + objType := reflect.TypeOf(testcase.initial) + objTest := reflect.New(objType).Interface() + err = json.Unmarshal(patchedJS, objTest) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(objTest, testcase.expected) { + t.Errorf("\nexpected %#v\n, got %#v", testcase.expected, objTest) + } } - patchObj, err := jsonpatch.DecodePatch([]byte(patch1)) - if err != nil { - t.Fatal(err) - } - patchedJS, err := patchObj.Apply(cmJS) - patchedObj := corev1.ConfigMap{} - err = json.Unmarshal(patchedJS, &patchedObj) - if err != nil { - t.Fatal(err) - } - expected := corev1.ConfigMap{ - Data: map[string]string{ - "mutation-start": "yes", - "mutation-stage-1": "yes", - }, - } - - if !reflect.DeepEqual(patchedObj, expected) { - t.Errorf("\nexpected %#v\n, got %#v", expected, patchedObj) - } } func TestJSONPatchForUnstructured(t *testing.T) { @@ -74,7 +124,7 @@ func TestJSONPatchForUnstructured(t *testing.T) { t.Fatal(err) } - patchObj, err := jsonpatch.DecodePatch([]byte(patch1)) + patchObj, err := jsonpatch.DecodePatch([]byte(configMapPatch1)) if err != nil { t.Fatal(err) } diff --git a/test/images/webhook/pods.go b/test/images/webhook/pods.go new file mode 100644 index 00000000000..2a818501822 --- /dev/null +++ b/test/images/webhook/pods.go @@ -0,0 +1,103 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "fmt" + "strings" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/golang/glog" + "k8s.io/api/admission/v1beta1" +) + +const ( + podsInitContainerPatch string = `[ + {"op":"add","path":"/spec/initContainers","value":[{"image":"webhook-added-image","name":"webhook-added-init-container","resources":{}}]} + ]` +) + +// only allow pods to pull images from specific registry. +func admitPods(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse { + glog.V(2).Info("admitting pods") + podResource := metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"} + if ar.Request.Resource != podResource { + err := fmt.Errorf("expect resource to be %s", podResource) + glog.Error(err) + return toAdmissionResponse(err) + } + + 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 + + var msg string + if v, ok := pod.Labels["webhook-e2e-test"]; ok { + if v == "webhook-disallow" { + reviewResponse.Allowed = false + msg = msg + "the pod contains unwanted label; " + } + if v == "wait-forever" { + reviewResponse.Allowed = false + msg = msg + "the pod response should not be sent; " + <-make(chan int) // Sleep forever - no one sends to this channel + } + } + for _, container := range pod.Spec.Containers { + if strings.Contains(container.Name, "webhook-disallow") { + reviewResponse.Allowed = false + msg = msg + "the pod contains unwanted container name; " + } + } + if !reviewResponse.Allowed { + reviewResponse.Result = &metav1.Status{Message: strings.TrimSpace(msg)} + } + 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(podsInitContainerPatch) + pt := v1beta1.PatchTypeJSONPatch + reviewResponse.PatchType = &pt + } + return &reviewResponse +} diff --git a/test/images/webhook/scheme.go b/test/images/webhook/scheme.go index 80445b481a0..fc0bb290bc2 100644 --- a/test/images/webhook/scheme.go +++ b/test/images/webhook/scheme.go @@ -1,5 +1,5 @@ /* -Copyright 2017 The Kubernetes Authors. +Copyright 2018 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/test/utils/image/manifest.go b/test/utils/image/manifest.go index 63ac8ffd6dc..41a859c2729 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, "webhook", "1.10v2", false} + AdmissionWebhook = ImageConfig{e2eRegistry, "webhook", "1.12v1", true} APIServer = ImageConfig{e2eRegistry, "sample-apiserver", "1.0", false} AppArmorLoader = ImageConfig{gcRegistry, "apparmor-loader", "0.1", false} BusyBox = ImageConfig{gcRegistry, "busybox", "1.24", false}