diff --git a/pkg/apis/admission/fuzzer/BUILD b/pkg/apis/admission/fuzzer/BUILD index 8fd84dccad3..1d0b0955b42 100644 --- a/pkg/apis/admission/fuzzer/BUILD +++ b/pkg/apis/admission/fuzzer/BUILD @@ -9,7 +9,12 @@ go_library( name = "go_default_library", srcs = ["fuzzer.go"], importpath = "k8s.io/kubernetes/pkg/apis/admission/fuzzer", - deps = ["//staging/src/k8s.io/apimachinery/pkg/runtime/serializer:go_default_library"], + deps = [ + "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/runtime/serializer:go_default_library", + "//vendor/github.com/google/gofuzz:go_default_library", + ], ) filegroup( diff --git a/pkg/apis/admission/fuzzer/fuzzer.go b/pkg/apis/admission/fuzzer/fuzzer.go index 6b4aea2de13..73908f667ba 100644 --- a/pkg/apis/admission/fuzzer/fuzzer.go +++ b/pkg/apis/admission/fuzzer/fuzzer.go @@ -17,10 +17,23 @@ limitations under the License. package fuzzer import ( + fuzz "github.com/google/gofuzz" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" runtimeserializer "k8s.io/apimachinery/pkg/runtime/serializer" ) // Funcs returns the fuzzer functions for the admission api group. var Funcs = func(codecs runtimeserializer.CodecFactory) []interface{} { - return []interface{}{} + return []interface{}{ + func(s *runtime.RawExtension, c fuzz.Continue) { + u := &unstructured.Unstructured{Object: map[string]interface{}{ + "apiVersion": "unknown.group/unknown", + "kind": "Something", + "somekey": "somevalue", + }} + s.Object = u + }, + } } diff --git a/test/images/agnhost/VERSION b/test/images/agnhost/VERSION index 6b4950e3de2..95e3ba81920 100644 --- a/test/images/agnhost/VERSION +++ b/test/images/agnhost/VERSION @@ -1 +1 @@ -2.4 +2.5 diff --git a/test/images/agnhost/agnhost b/test/images/agnhost/agnhost new file mode 100755 index 00000000000..40261770537 Binary files /dev/null and b/test/images/agnhost/agnhost differ diff --git a/test/images/agnhost/crd-conversion-webhook/main.go b/test/images/agnhost/crd-conversion-webhook/main.go index ac5096ddb9b..3d26e589a13 100644 --- a/test/images/agnhost/crd-conversion-webhook/main.go +++ b/test/images/agnhost/crd-conversion-webhook/main.go @@ -17,6 +17,7 @@ limitations under the License. package crdconvwebhook import ( + "fmt" "net/http" "github.com/spf13/cobra" @@ -27,6 +28,7 @@ import ( var ( certFile string keyFile string + port int ) // CmdCrdConversionWebhook is used by agnhost Cobra. @@ -48,6 +50,8 @@ func init() { "after server cert.") CmdCrdConversionWebhook.Flags().StringVar(&keyFile, "tls-private-key-file", "", "File containing the default x509 private key matching --tls-cert-file.") + CmdCrdConversionWebhook.Flags().IntVar(&port, "port", 443, + "Secure port that the webhook listens on") } // Config contains the server (the webhook) cert and key. @@ -62,8 +66,11 @@ func main(cmd *cobra.Command, args []string) { http.HandleFunc("/crdconvert", converter.ServeExampleConvert) clientset := getClient() server := &http.Server{ - Addr: ":443", + Addr: fmt.Sprintf(":%d", port), TLSConfig: configTLS(config, clientset), } - server.ListenAndServeTLS("", "") + err := server.ListenAndServeTLS("", "") + if err != nil { + panic(err) + } } diff --git a/test/images/agnhost/webhook/BUILD b/test/images/agnhost/webhook/BUILD index 84959a2d627..7afaefa56a7 100644 --- a/test/images/agnhost/webhook/BUILD +++ b/test/images/agnhost/webhook/BUILD @@ -8,6 +8,7 @@ go_library( "alwaysdeny.go", "config.go", "configmap.go", + "convert.go", "crd.go", "customresource.go", "main.go", @@ -17,7 +18,9 @@ go_library( importpath = "k8s.io/kubernetes/test/images/agnhost/webhook", visibility = ["//visibility:public"], deps = [ + "//staging/src/k8s.io/api/admission/v1:go_default_library", "//staging/src/k8s.io/api/admission/v1beta1:go_default_library", + "//staging/src/k8s.io/api/admissionregistration/v1:go_default_library", "//staging/src/k8s.io/api/admissionregistration/v1beta1:go_default_library", "//staging/src/k8s.io/api/core/v1:go_default_library", "//staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1:go_default_library", @@ -48,15 +51,22 @@ go_test( name = "go_default_test", srcs = [ "addlabel_test.go", + "convert_test.go", "patch_test.go", ], embed = [":go_default_library"], deps = [ + "//pkg/apis/admission/fuzzer:go_default_library", + "//staging/src/k8s.io/api/admission/v1:go_default_library", "//staging/src/k8s.io/api/admission/v1beta1:go_default_library", "//staging/src/k8s.io/api/core/v1:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/api/apitesting/fuzzer:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_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/diff:go_default_library", "//vendor/github.com/evanphx/json-patch:go_default_library", + "//vendor/github.com/google/gofuzz:go_default_library", ], ) diff --git a/test/images/agnhost/webhook/addlabel.go b/test/images/agnhost/webhook/addlabel.go index 6e61625a96c..402e2b5cfa1 100644 --- a/test/images/agnhost/webhook/addlabel.go +++ b/test/images/agnhost/webhook/addlabel.go @@ -19,7 +19,7 @@ package webhook import ( "encoding/json" - "k8s.io/api/admission/v1beta1" + "k8s.io/api/admission/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/klog" ) @@ -37,7 +37,7 @@ const ( ) // Add a label {"added-label": "yes"} to the object -func addLabel(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse { +func addLabel(ar v1.AdmissionReview) *v1.AdmissionResponse { klog.V(2).Info("calling add-label") obj := struct { metav1.ObjectMeta `json:"metadata,omitempty"` @@ -46,13 +46,13 @@ func addLabel(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse { err := json.Unmarshal(raw, &obj) if err != nil { klog.Error(err) - return toAdmissionResponse(err) + return toV1AdmissionResponse(err) } - reviewResponse := v1beta1.AdmissionResponse{} + reviewResponse := v1.AdmissionResponse{} reviewResponse.Allowed = true - pt := v1beta1.PatchTypeJSONPatch + pt := v1.PatchTypeJSONPatch labelValue, hasLabel := obj.ObjectMeta.Labels["added-label"] switch { case len(obj.ObjectMeta.Labels) == 0: diff --git a/test/images/agnhost/webhook/addlabel_test.go b/test/images/agnhost/webhook/addlabel_test.go index 08a6dbdabc9..b603dad84e7 100644 --- a/test/images/agnhost/webhook/addlabel_test.go +++ b/test/images/agnhost/webhook/addlabel_test.go @@ -22,7 +22,7 @@ import ( "testing" jsonpatch "github.com/evanphx/json-patch" - "k8s.io/api/admission/v1beta1" + "k8s.io/api/admission/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -58,7 +58,7 @@ func TestAddLabel(t *testing.T) { if err != nil { t.Fatal(err) } - review := v1beta1.AdmissionReview{Request: &v1beta1.AdmissionRequest{Object: runtime.RawExtension{Raw: raw}}} + review := v1.AdmissionReview{Request: &v1.AdmissionRequest{Object: runtime.RawExtension{Raw: raw}}} response := addLabel(review) if response.Patch != nil { patchObj, err := jsonpatch.DecodePatch([]byte(response.Patch)) diff --git a/test/images/agnhost/webhook/alwaysallow.go b/test/images/agnhost/webhook/alwaysallow.go index f7999b6c068..ce40d201925 100644 --- a/test/images/agnhost/webhook/alwaysallow.go +++ b/test/images/agnhost/webhook/alwaysallow.go @@ -19,17 +19,17 @@ package webhook import ( "time" - "k8s.io/api/admission/v1beta1" + "k8s.io/api/admission/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/klog" ) // alwaysAllowDelayFiveSeconds sleeps for five seconds and allows all requests made to this function. -func alwaysAllowDelayFiveSeconds(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse { +func alwaysAllowDelayFiveSeconds(ar v1.AdmissionReview) *v1.AdmissionResponse { klog.V(2).Info("always-allow-with-delay sleeping for 5 seconds") time.Sleep(5 * time.Second) klog.V(2).Info("calling always-allow") - reviewResponse := v1beta1.AdmissionResponse{} + reviewResponse := v1.AdmissionResponse{} reviewResponse.Allowed = true reviewResponse.Result = &metav1.Status{Message: "this webhook allows all requests"} return &reviewResponse diff --git a/test/images/agnhost/webhook/alwaysdeny.go b/test/images/agnhost/webhook/alwaysdeny.go index e0643c5879f..33ea6adcdec 100644 --- a/test/images/agnhost/webhook/alwaysdeny.go +++ b/test/images/agnhost/webhook/alwaysdeny.go @@ -17,15 +17,15 @@ limitations under the License. package webhook import ( - "k8s.io/api/admission/v1beta1" + "k8s.io/api/admission/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/klog" ) // alwaysDeny all requests made to this function. -func alwaysDeny(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse { +func alwaysDeny(ar v1.AdmissionReview) *v1.AdmissionResponse { klog.V(2).Info("calling always-deny") - reviewResponse := v1beta1.AdmissionResponse{} + reviewResponse := v1.AdmissionResponse{} reviewResponse.Allowed = false reviewResponse.Result = &metav1.Status{Message: "this webhook denies all requests"} return &reviewResponse diff --git a/test/images/agnhost/webhook/configmap.go b/test/images/agnhost/webhook/configmap.go index 881c474f2a3..85fbb1db63f 100644 --- a/test/images/agnhost/webhook/configmap.go +++ b/test/images/agnhost/webhook/configmap.go @@ -17,7 +17,7 @@ limitations under the License. package webhook import ( - "k8s.io/api/admission/v1beta1" + v1 "k8s.io/api/admission/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/klog" @@ -33,7 +33,7 @@ const ( ) // deny configmaps with specific key-value pair. -func admitConfigMaps(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse { +func admitConfigMaps(ar v1.AdmissionReview) *v1.AdmissionResponse { klog.V(2).Info("admitting configmaps") configMapResource := metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "configmaps"} if ar.Request.Resource != configMapResource { @@ -42,7 +42,7 @@ func admitConfigMaps(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse { } var raw []byte - if ar.Request.Operation == v1beta1.Delete { + if ar.Request.Operation == v1.Delete { raw = ar.Request.OldObject.Raw } else { raw = ar.Request.Object.Raw @@ -51,19 +51,19 @@ func admitConfigMaps(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse { deserializer := codecs.UniversalDeserializer() if _, _, err := deserializer.Decode(raw, nil, &configmap); err != nil { klog.Error(err) - return toAdmissionResponse(err) + return toV1AdmissionResponse(err) } - reviewResponse := v1beta1.AdmissionResponse{} + reviewResponse := v1.AdmissionResponse{} reviewResponse.Allowed = true for k, v := range configmap.Data { if k == "webhook-e2e-test" && v == "webhook-disallow" && - (ar.Request.Operation == v1beta1.Create || ar.Request.Operation == v1beta1.Update) { + (ar.Request.Operation == v1.Create || ar.Request.Operation == v1.Update) { reviewResponse.Allowed = false reviewResponse.Result = &metav1.Status{ Reason: "the configmap contains unwanted key and value", } } - if k == "webhook-e2e-test" && v == "webhook-nondeletable" && ar.Request.Operation == v1beta1.Delete { + if k == "webhook-e2e-test" && v == "webhook-nondeletable" && ar.Request.Operation == v1.Delete { reviewResponse.Allowed = false reviewResponse.Result = &metav1.Status{ Reason: "the configmap cannot be deleted because it contains unwanted key and value", @@ -73,7 +73,7 @@ func admitConfigMaps(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse { return &reviewResponse } -func mutateConfigmaps(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse { +func mutateConfigmaps(ar v1.AdmissionReview) *v1.AdmissionResponse { klog.V(2).Info("mutating configmaps") configMapResource := metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "configmaps"} if ar.Request.Resource != configMapResource { @@ -86,9 +86,9 @@ func mutateConfigmaps(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse { deserializer := codecs.UniversalDeserializer() if _, _, err := deserializer.Decode(raw, nil, &configmap); err != nil { klog.Error(err) - return toAdmissionResponse(err) + return toV1AdmissionResponse(err) } - reviewResponse := v1beta1.AdmissionResponse{} + reviewResponse := v1.AdmissionResponse{} reviewResponse.Allowed = true if configmap.Data["mutation-start"] == "yes" { reviewResponse.Patch = []byte(configMapPatch1) @@ -97,7 +97,7 @@ func mutateConfigmaps(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse { reviewResponse.Patch = []byte(configMapPatch2) } - pt := v1beta1.PatchTypeJSONPatch + pt := v1.PatchTypeJSONPatch reviewResponse.PatchType = &pt return &reviewResponse diff --git a/test/images/agnhost/webhook/convert.go b/test/images/agnhost/webhook/convert.go new file mode 100644 index 00000000000..96695f77b52 --- /dev/null +++ b/test/images/agnhost/webhook/convert.go @@ -0,0 +1,103 @@ +/* +Copyright 2019 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 webhook + +import ( + v1 "k8s.io/api/admission/v1" + "k8s.io/api/admission/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func convertAdmissionRequestToV1(r *v1beta1.AdmissionRequest) *v1.AdmissionRequest { + return &v1.AdmissionRequest{ + Kind: r.Kind, + Namespace: r.Namespace, + Name: r.Name, + Object: r.Object, + Resource: r.Resource, + Operation: v1.Operation(r.Operation), + UID: r.UID, + DryRun: r.DryRun, + OldObject: r.OldObject, + Options: r.Options, + RequestKind: r.RequestKind, + RequestResource: r.RequestResource, + RequestSubResource: r.RequestSubResource, + SubResource: r.SubResource, + UserInfo: r.UserInfo, + } +} + +func convertAdmissionRequestToV1beta1(r *v1.AdmissionRequest) *v1beta1.AdmissionRequest { + return &v1beta1.AdmissionRequest{ + Kind: r.Kind, + Namespace: r.Namespace, + Name: r.Name, + Object: r.Object, + Resource: r.Resource, + Operation: v1beta1.Operation(r.Operation), + UID: r.UID, + DryRun: r.DryRun, + OldObject: r.OldObject, + Options: r.Options, + RequestKind: r.RequestKind, + RequestResource: r.RequestResource, + RequestSubResource: r.RequestSubResource, + SubResource: r.SubResource, + UserInfo: r.UserInfo, + } +} + +func convertAdmissionResponseToV1(r *v1beta1.AdmissionResponse) *v1.AdmissionResponse { + var pt *v1.PatchType + if r.PatchType != nil { + t := v1.PatchType(*r.PatchType) + pt = &t + } + return &v1.AdmissionResponse{ + UID: r.UID, + Allowed: r.Allowed, + AuditAnnotations: r.AuditAnnotations, + Patch: r.Patch, + PatchType: pt, + Result: r.Result, + } +} + +func convertAdmissionResponseToV1beta1(r *v1.AdmissionResponse) *v1beta1.AdmissionResponse { + var pt *v1beta1.PatchType + if r.PatchType != nil { + t := v1beta1.PatchType(*r.PatchType) + pt = &t + } + return &v1beta1.AdmissionResponse{ + UID: r.UID, + Allowed: r.Allowed, + AuditAnnotations: r.AuditAnnotations, + Patch: r.Patch, + PatchType: pt, + Result: r.Result, + } +} + +func toV1AdmissionResponse(err error) *v1.AdmissionResponse { + return &v1.AdmissionResponse{ + Result: &metav1.Status{ + Message: err.Error(), + }, + } +} diff --git a/test/images/agnhost/webhook/convert_test.go b/test/images/agnhost/webhook/convert_test.go new file mode 100644 index 00000000000..d1b9dba55d3 --- /dev/null +++ b/test/images/agnhost/webhook/convert_test.go @@ -0,0 +1,64 @@ +/* +Copyright 2019 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 webhook + +import ( + "fmt" + "math/rand" + "reflect" + "testing" + + fuzz "github.com/google/gofuzz" + + v1 "k8s.io/api/admission/v1" + "k8s.io/api/admission/v1beta1" + "k8s.io/apimachinery/pkg/api/apitesting/fuzzer" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/serializer" + "k8s.io/apimachinery/pkg/util/diff" + admissionfuzzer "k8s.io/kubernetes/pkg/apis/admission/fuzzer" +) + +func TestConvertAdmissionRequestToV1(t *testing.T) { + f := fuzzer.FuzzerFor(admissionfuzzer.Funcs, rand.NewSource(rand.Int63()), serializer.NewCodecFactory(runtime.NewScheme())) + for i := 0; i < 100; i++ { + t.Run(fmt.Sprintf("Run %d/100", i), func(t *testing.T) { + orig := &v1beta1.AdmissionRequest{} + f.Fuzz(orig) + converted := convertAdmissionRequestToV1(orig) + rt := convertAdmissionRequestToV1beta1(converted) + if !reflect.DeepEqual(orig, rt) { + t.Errorf("expected all request fields to be in converted object but found unaccounted for differences, diff:\n%s", diff.ObjectReflectDiff(orig, converted)) + } + }) + } +} + +func TestConvertAdmissionResponseToV1beta1(t *testing.T) { + f := fuzz.New() + for i := 0; i < 100; i++ { + t.Run(fmt.Sprintf("Run %d/100", i), func(t *testing.T) { + orig := &v1.AdmissionResponse{} + f.Fuzz(orig) + converted := convertAdmissionResponseToV1beta1(orig) + rt := convertAdmissionResponseToV1(converted) + if !reflect.DeepEqual(orig, rt) { + t.Errorf("expected all fields to be in converted object but found unaccounted for differences, diff:\n%s", diff.ObjectReflectDiff(orig, converted)) + } + }) + } +} diff --git a/test/images/agnhost/webhook/crd.go b/test/images/agnhost/webhook/crd.go index a82fc7469c2..9562489a2bb 100644 --- a/test/images/agnhost/webhook/crd.go +++ b/test/images/agnhost/webhook/crd.go @@ -19,22 +19,21 @@ package webhook import ( "fmt" + "k8s.io/api/admission/v1" apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - "k8s.io/api/admission/v1beta1" "k8s.io/klog" ) // 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 { +func admitCRD(ar v1.AdmissionReview) *v1.AdmissionResponse { klog.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) klog.Error(err) - return toAdmissionResponse(err) + return toV1AdmissionResponse(err) } raw := ar.Request.Object.Raw @@ -42,9 +41,9 @@ func admitCRD(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse { deserializer := codecs.UniversalDeserializer() if _, _, err := deserializer.Decode(raw, nil, &crd); err != nil { klog.Error(err) - return toAdmissionResponse(err) + return toV1AdmissionResponse(err) } - reviewResponse := v1beta1.AdmissionResponse{} + reviewResponse := v1.AdmissionResponse{} reviewResponse.Allowed = true if v, ok := crd.Labels["webhook-e2e-test"]; ok { diff --git a/test/images/agnhost/webhook/customresource.go b/test/images/agnhost/webhook/customresource.go index 6bb44a3f3c1..a1452d6454e 100644 --- a/test/images/agnhost/webhook/customresource.go +++ b/test/images/agnhost/webhook/customresource.go @@ -19,9 +19,8 @@ package webhook import ( "encoding/json" + v1 "k8s.io/api/admission/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - "k8s.io/api/admission/v1beta1" "k8s.io/klog" ) @@ -34,7 +33,7 @@ const ( ]` ) -func mutateCustomResource(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse { +func mutateCustomResource(ar v1.AdmissionReview) *v1.AdmissionResponse { klog.V(2).Info("mutating custom resource") cr := struct { metav1.ObjectMeta @@ -45,10 +44,10 @@ func mutateCustomResource(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse err := json.Unmarshal(raw, &cr) if err != nil { klog.Error(err) - return toAdmissionResponse(err) + return toV1AdmissionResponse(err) } - reviewResponse := v1beta1.AdmissionResponse{} + reviewResponse := v1.AdmissionResponse{} reviewResponse.Allowed = true if cr.Data["mutation-start"] == "yes" { @@ -57,12 +56,12 @@ func mutateCustomResource(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse if cr.Data["mutation-stage-1"] == "yes" { reviewResponse.Patch = []byte(customResourcePatch2) } - pt := v1beta1.PatchTypeJSONPatch + pt := v1.PatchTypeJSONPatch reviewResponse.PatchType = &pt return &reviewResponse } -func admitCustomResource(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse { +func admitCustomResource(ar v1.AdmissionReview) *v1.AdmissionResponse { klog.V(2).Info("admitting custom resource") cr := struct { metav1.ObjectMeta @@ -70,7 +69,7 @@ func admitCustomResource(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse }{} var raw []byte - if ar.Request.Operation == v1beta1.Delete { + if ar.Request.Operation == v1.Delete { raw = ar.Request.OldObject.Raw } else { raw = ar.Request.Object.Raw @@ -78,20 +77,20 @@ func admitCustomResource(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse err := json.Unmarshal(raw, &cr) if err != nil { klog.Error(err) - return toAdmissionResponse(err) + return toV1AdmissionResponse(err) } - reviewResponse := v1beta1.AdmissionResponse{} + reviewResponse := v1.AdmissionResponse{} reviewResponse.Allowed = true for k, v := range cr.Data { if k == "webhook-e2e-test" && v == "webhook-disallow" && - (ar.Request.Operation == v1beta1.Create || ar.Request.Operation == v1beta1.Update) { + (ar.Request.Operation == v1.Create || ar.Request.Operation == v1.Update) { reviewResponse.Allowed = false reviewResponse.Result = &metav1.Status{ Reason: "the custom resource contains unwanted data", } } - if k == "webhook-e2e-test" && v == "webhook-nondeletable" && ar.Request.Operation == v1beta1.Delete { + if k == "webhook-e2e-test" && v == "webhook-nondeletable" && ar.Request.Operation == v1.Delete { reviewResponse.Allowed = false reviewResponse.Result = &metav1.Status{ Reason: "the custom resource cannot be deleted because it contains unwanted key and value", diff --git a/test/images/agnhost/webhook/main.go b/test/images/agnhost/webhook/main.go index c21981e056e..c801df27793 100644 --- a/test/images/agnhost/webhook/main.go +++ b/test/images/agnhost/webhook/main.go @@ -24,8 +24,9 @@ import ( "github.com/spf13/cobra" + v1 "k8s.io/api/admission/v1" "k8s.io/api/admission/v1beta1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/klog" // TODO: try this library to see if it generates correct json patch // https://github.com/mattbaird/jsonpatch @@ -34,6 +35,7 @@ import ( var ( certFile string keyFile string + port int ) // CmdWebhook is used by agnhost Cobra. @@ -52,24 +54,40 @@ func init() { "File containing the default x509 Certificate for HTTPS. (CA cert, if any, concatenated after server cert).") CmdWebhook.Flags().StringVar(&keyFile, "tls-private-key-file", "", "File containing the default x509 private key matching --tls-cert-file.") + CmdWebhook.Flags().IntVar(&port, "port", 443, + "Secure port that the webhook listens on") } -// 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{ - Message: err.Error(), - }, +// admitv1beta1Func handles a v1beta1 admission +type admitv1beta1Func func(v1beta1.AdmissionReview) *v1beta1.AdmissionResponse + +// admitv1beta1Func handles a v1 admission +type admitv1Func func(v1.AdmissionReview) *v1.AdmissionResponse + +// admitHandler is a handler, for both validators and mutators, that supports multiple admission review versions +type admitHandler struct { + v1beta1 admitv1beta1Func + v1 admitv1Func +} + +func newDelegateToV1AdmitHandler(f admitv1Func) admitHandler { + return admitHandler{ + v1beta1: delegateV1beta1AdmitToV1(f), + v1: f, } } -// admitFunc is the type we use for all of our validators and mutators -type admitFunc func(v1beta1.AdmissionReview) *v1beta1.AdmissionResponse +func delegateV1beta1AdmitToV1(f admitv1Func) admitv1beta1Func { + return func(review v1beta1.AdmissionReview) *v1beta1.AdmissionResponse { + in := v1.AdmissionReview{Request: convertAdmissionRequestToV1(review.Request)} + out := f(in) + return convertAdmissionResponseToV1beta1(out) + } +} // 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) { +func serve(w http.ResponseWriter, r *http.Request, admit admitHandler) { var body []byte if r.Body != nil { if data, err := ioutil.ReadAll(r.Body); err == nil { @@ -86,77 +104,101 @@ func serve(w http.ResponseWriter, r *http.Request, admit admitFunc) { klog.V(2).Info(fmt.Sprintf("handling request: %s", body)) - // 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, &requestedAdmissionReview); err != nil { - klog.Error(err) - responseAdmissionReview.Response = toAdmissionResponse(err) - } else { - // pass to admitFunc - responseAdmissionReview.Response = admit(requestedAdmissionReview) + obj, gvk, err := deserializer.Decode(body, nil, nil) + if err != nil { + msg := fmt.Sprintf("Request could not be decoded: %v", err) + klog.Error(msg) + http.Error(w, msg, http.StatusBadRequest) + return } - // Return the same UID - responseAdmissionReview.Response.UID = requestedAdmissionReview.Request.UID + var responseObj runtime.Object + switch *gvk { + case v1beta1.SchemeGroupVersion.WithKind("AdmissionReview"): + requestedAdmissionReview, ok := obj.(*v1beta1.AdmissionReview) + if !ok { + klog.Errorf("Expected v1beta1.AdmissionReview but got: %T", obj) + return + } + responseAdmissionReview := &v1beta1.AdmissionReview{} + responseAdmissionReview.SetGroupVersionKind(*gvk) + responseAdmissionReview.Response = admit.v1beta1(*requestedAdmissionReview) + responseAdmissionReview.Response.UID = requestedAdmissionReview.Request.UID + responseObj = responseAdmissionReview + case v1.SchemeGroupVersion.WithKind("AdmissionReview"): + requestedAdmissionReview, ok := obj.(*v1.AdmissionReview) + if !ok { + klog.Errorf("Expected v1.AdmissionReview but got: %T", obj) + return + } + responseAdmissionReview := &v1.AdmissionReview{} + responseAdmissionReview.SetGroupVersionKind(*gvk) + responseAdmissionReview.Response = admit.v1(*requestedAdmissionReview) + responseAdmissionReview.Response.UID = requestedAdmissionReview.Request.UID + responseObj = responseAdmissionReview + default: + msg := fmt.Sprintf("Unsupported group version kind: %v", gvk) + klog.Error(msg) + http.Error(w, msg, http.StatusBadRequest) + return + } - klog.V(2).Info(fmt.Sprintf("sending response: %v", responseAdmissionReview.Response)) - - respBytes, err := json.Marshal(responseAdmissionReview) + klog.V(2).Info(fmt.Sprintf("sending response: %v", responseObj)) + respBytes, err := json.Marshal(responseObj) if err != nil { klog.Error(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return } + w.Header().Set("Content-Type", "application/json") if _, err := w.Write(respBytes); err != nil { klog.Error(err) } } func serveAlwaysAllowDelayFiveSeconds(w http.ResponseWriter, r *http.Request) { - serve(w, r, alwaysAllowDelayFiveSeconds) + serve(w, r, newDelegateToV1AdmitHandler(alwaysAllowDelayFiveSeconds)) } func serveAlwaysDeny(w http.ResponseWriter, r *http.Request) { - serve(w, r, alwaysDeny) + serve(w, r, newDelegateToV1AdmitHandler(alwaysDeny)) } func serveAddLabel(w http.ResponseWriter, r *http.Request) { - serve(w, r, addLabel) + serve(w, r, newDelegateToV1AdmitHandler(addLabel)) } func servePods(w http.ResponseWriter, r *http.Request) { - serve(w, r, admitPods) + serve(w, r, newDelegateToV1AdmitHandler(admitPods)) } func serveAttachingPods(w http.ResponseWriter, r *http.Request) { - serve(w, r, denySpecificAttachment) + serve(w, r, newDelegateToV1AdmitHandler(denySpecificAttachment)) } func serveMutatePods(w http.ResponseWriter, r *http.Request) { - serve(w, r, mutatePods) + serve(w, r, newDelegateToV1AdmitHandler(mutatePods)) } func serveConfigmaps(w http.ResponseWriter, r *http.Request) { - serve(w, r, admitConfigMaps) + serve(w, r, newDelegateToV1AdmitHandler(admitConfigMaps)) } func serveMutateConfigmaps(w http.ResponseWriter, r *http.Request) { - serve(w, r, mutateConfigmaps) + serve(w, r, newDelegateToV1AdmitHandler(mutateConfigmaps)) } func serveCustomResource(w http.ResponseWriter, r *http.Request) { - serve(w, r, admitCustomResource) + serve(w, r, newDelegateToV1AdmitHandler(admitCustomResource)) } func serveMutateCustomResource(w http.ResponseWriter, r *http.Request) { - serve(w, r, mutateCustomResource) + serve(w, r, newDelegateToV1AdmitHandler(mutateCustomResource)) } func serveCRD(w http.ResponseWriter, r *http.Request) { - serve(w, r, admitCRD) + serve(w, r, newDelegateToV1AdmitHandler(admitCRD)) } func main(cmd *cobra.Command, args []string) { @@ -177,8 +219,11 @@ func main(cmd *cobra.Command, args []string) { http.HandleFunc("/mutating-custom-resource", serveMutateCustomResource) http.HandleFunc("/crd", serveCRD) server := &http.Server{ - Addr: ":443", + Addr: fmt.Sprintf(":%d", port), TLSConfig: configTLS(config), } - server.ListenAndServeTLS("", "") + err := server.ListenAndServeTLS("", "") + if err != nil { + panic(err) + } } diff --git a/test/images/agnhost/webhook/pods.go b/test/images/agnhost/webhook/pods.go index 6d268cfcd06..9d6976d691b 100644 --- a/test/images/agnhost/webhook/pods.go +++ b/test/images/agnhost/webhook/pods.go @@ -20,10 +20,9 @@ import ( "fmt" "strings" + "k8s.io/api/admission/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - "k8s.io/api/admission/v1beta1" "k8s.io/klog" ) @@ -34,13 +33,13 @@ const ( ) // only allow pods to pull images from specific registry. -func admitPods(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse { +func admitPods(ar v1.AdmissionReview) *v1.AdmissionResponse { klog.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) klog.Error(err) - return toAdmissionResponse(err) + return toV1AdmissionResponse(err) } raw := ar.Request.Object.Raw @@ -48,9 +47,9 @@ func admitPods(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse { deserializer := codecs.UniversalDeserializer() if _, _, err := deserializer.Decode(raw, nil, &pod); err != nil { klog.Error(err) - return toAdmissionResponse(err) + return toV1AdmissionResponse(err) } - reviewResponse := v1beta1.AdmissionResponse{} + reviewResponse := v1.AdmissionResponse{} reviewResponse.Allowed = true var msg string @@ -77,7 +76,7 @@ func admitPods(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse { return &reviewResponse } -func mutatePods(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse { +func mutatePods(ar v1.AdmissionReview) *v1.AdmissionResponse { klog.V(2).Info("mutating pods") podResource := metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"} if ar.Request.Resource != podResource { @@ -90,13 +89,13 @@ func mutatePods(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse { deserializer := codecs.UniversalDeserializer() if _, _, err := deserializer.Decode(raw, nil, &pod); err != nil { klog.Error(err) - return toAdmissionResponse(err) + return toV1AdmissionResponse(err) } - reviewResponse := v1beta1.AdmissionResponse{} + reviewResponse := v1.AdmissionResponse{} reviewResponse.Allowed = true if pod.Name == "webhook-to-be-mutated" { reviewResponse.Patch = []byte(podsInitContainerPatch) - pt := v1beta1.PatchTypeJSONPatch + pt := v1.PatchTypeJSONPatch reviewResponse.PatchType = &pt } return &reviewResponse @@ -104,21 +103,21 @@ func mutatePods(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse { // denySpecificAttachment denies `kubectl attach to-be-attached-pod -i -c=container1" // or equivalent client requests. -func denySpecificAttachment(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse { +func denySpecificAttachment(ar v1.AdmissionReview) *v1.AdmissionResponse { klog.V(2).Info("handling attaching pods") if ar.Request.Name != "to-be-attached-pod" { - return &v1beta1.AdmissionResponse{Allowed: true} + return &v1.AdmissionResponse{Allowed: true} } podResource := metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"} if e, a := podResource, ar.Request.Resource; e != a { err := fmt.Errorf("expect resource to be %s, got %s", e, a) klog.Error(err) - return toAdmissionResponse(err) + return toV1AdmissionResponse(err) } if e, a := "attach", ar.Request.SubResource; e != a { err := fmt.Errorf("expect subresource to be %s, got %s", e, a) klog.Error(err) - return toAdmissionResponse(err) + return toV1AdmissionResponse(err) } raw := ar.Request.Object.Raw @@ -126,13 +125,13 @@ func denySpecificAttachment(ar v1beta1.AdmissionReview) *v1beta1.AdmissionRespon deserializer := codecs.UniversalDeserializer() if _, _, err := deserializer.Decode(raw, nil, &podAttachOptions); err != nil { klog.Error(err) - return toAdmissionResponse(err) + return toV1AdmissionResponse(err) } klog.V(2).Info(fmt.Sprintf("podAttachOptions=%#v\n", podAttachOptions)) if !podAttachOptions.Stdin || podAttachOptions.Container != "container1" { - return &v1beta1.AdmissionResponse{Allowed: true} + return &v1.AdmissionResponse{Allowed: true} } - return &v1beta1.AdmissionResponse{ + return &v1.AdmissionResponse{ Allowed: false, Result: &metav1.Status{ Message: "attaching to pod 'to-be-attached-pod' is not allowed", diff --git a/test/images/agnhost/webhook/scheme.go b/test/images/agnhost/webhook/scheme.go index 74d6b32ff16..3c09f990802 100644 --- a/test/images/agnhost/webhook/scheme.go +++ b/test/images/agnhost/webhook/scheme.go @@ -17,7 +17,9 @@ limitations under the License. package webhook import ( + admissionv1 "k8s.io/api/admission/v1" admissionv1beta1 "k8s.io/api/admission/v1beta1" + admissionregistrationv1 "k8s.io/api/admissionregistration/v1" admissionregistrationv1beta1 "k8s.io/api/admissionregistration/v1beta1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" @@ -36,4 +38,6 @@ func addToScheme(scheme *runtime.Scheme) { utilruntime.Must(corev1.AddToScheme(scheme)) utilruntime.Must(admissionv1beta1.AddToScheme(scheme)) utilruntime.Must(admissionregistrationv1beta1.AddToScheme(scheme)) + utilruntime.Must(admissionv1.AddToScheme(scheme)) + utilruntime.Must(admissionregistrationv1.AddToScheme(scheme)) }