diff --git a/test/images/agnhost/crd-conversion-webhook/converter/BUILD b/test/images/agnhost/crd-conversion-webhook/converter/BUILD index e76b458301f0b..4c12b6e9c8516 100644 --- a/test/images/agnhost/crd-conversion-webhook/converter/BUILD +++ b/test/images/agnhost/crd-conversion-webhook/converter/BUILD @@ -9,11 +9,13 @@ go_library( importpath = "k8s.io/kubernetes/test/images/agnhost/crd-conversion-webhook/converter", visibility = ["//visibility:public"], deps = [ + "//staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1:go_default_library", "//staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1: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/json:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/util/runtime:go_default_library", "//vendor/github.com/munnerz/goautoneg:go_default_library", "//vendor/k8s.io/klog:go_default_library", ], @@ -24,7 +26,7 @@ go_test( srcs = ["converter_test.go"], embed = [":go_default_library"], deps = [ - "//staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1:go_default_library", + "//staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1: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", diff --git a/test/images/agnhost/crd-conversion-webhook/converter/converter_test.go b/test/images/agnhost/crd-conversion-webhook/converter/converter_test.go index e8e12391e2b98..b7b9ffa6d4fea 100644 --- a/test/images/agnhost/crd-conversion-webhook/converter/converter_test.go +++ b/test/images/agnhost/crd-conversion-webhook/converter/converter_test.go @@ -17,21 +17,47 @@ limitations under the License. package converter import ( + "bytes" + "fmt" "net/http" "net/http/httptest" "strings" "testing" - "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/serializer/json" ) -func TestConverter(t *testing.T) { - sampleObj := `kind: ConversionReview -apiVersion: apiextensions.k8s.io/v1beta1 +func TestConverterYAML(t *testing.T) { + cases := []struct { + apiVersion string + contentType string + expected400Err string + }{ + { + apiVersion: "apiextensions.k8s.io/v1beta1", + contentType: "application/json", + expected400Err: "json parse error", + }, + { + apiVersion: "apiextensions.k8s.io/v1beta1", + contentType: "application/yaml", + }, + { + apiVersion: "apiextensions.k8s.io/v1", + contentType: "application/json", + expected400Err: "json parse error", + }, + { + apiVersion: "apiextensions.k8s.io/v1", + contentType: "application/yaml", + }, + } + sampleObjTemplate := `kind: ConversionReview +apiVersion: %s request: uid: 0000-0000-0000-0000 desiredAPIVersion: stable.example.com/v2 @@ -45,53 +71,47 @@ request: image: my-awesome-cron-image hostPort: "localhost:7070" ` - // First try json, it should fail as the data is taml - response := httptest.NewRecorder() - request, err := http.NewRequest("POST", "/convert", strings.NewReader(sampleObj)) - if err != nil { - t.Fatal(err) - } - request.Header.Add("Content-Type", "application/json") - ServeExampleConvert(response, request) - convertReview := v1beta1.ConversionReview{} - scheme := runtime.NewScheme() - jsonSerializer := json.NewSerializer(json.DefaultMetaFactory, scheme, scheme, false) - if _, _, err := jsonSerializer.Decode(response.Body.Bytes(), nil, &convertReview); err != nil { - t.Fatal(err) - } - if convertReview.Response.Result.Status != v1.StatusFailure { - t.Fatalf("expected the operation to fail when yaml is provided with json header") - } else if !strings.Contains(convertReview.Response.Result.Message, "json parse error") { - t.Fatalf("expected to fail on json parser, but it failed with: %v", convertReview.Response.Result.Message) - } + for _, tc := range cases { + t.Run(tc.apiVersion+" "+tc.contentType, func(t *testing.T) { + sampleObj := fmt.Sprintf(sampleObjTemplate, tc.apiVersion) + // First try json, it should fail as the data is taml + response := httptest.NewRecorder() + request, err := http.NewRequest("POST", "/convert", strings.NewReader(sampleObj)) + if err != nil { + t.Fatal(err) + } + request.Header.Add("Content-Type", tc.contentType) + ServeExampleConvert(response, request) + convertReview := apiextensionsv1.ConversionReview{} + scheme := runtime.NewScheme() + if len(tc.expected400Err) > 0 { + body := response.Body.Bytes() + if !bytes.Contains(body, []byte(tc.expected400Err)) { + t.Fatalf("expected to fail on '%s', but it failed with: %s", tc.expected400Err, string(body)) + } + return + } - // Now try yaml, and it should successfully convert - response = httptest.NewRecorder() - request, err = http.NewRequest("POST", "/convert", strings.NewReader(sampleObj)) - if err != nil { - t.Fatal(err) - } - request.Header.Add("Content-Type", "application/yaml") - ServeExampleConvert(response, request) - convertReview = v1beta1.ConversionReview{} - yamlSerializer := json.NewYAMLSerializer(json.DefaultMetaFactory, scheme, scheme) - if _, _, err := yamlSerializer.Decode(response.Body.Bytes(), nil, &convertReview); err != nil { - t.Fatalf("cannot decode data: \n %v\n Error: %v", response.Body, err) - } - if convertReview.Response.Result.Status != v1.StatusSuccess { - t.Fatalf("cr conversion failed: %v", convertReview.Response) - } - convertedObj := unstructured.Unstructured{} - if _, _, err := yamlSerializer.Decode(convertReview.Response.ConvertedObjects[0].Raw, nil, &convertedObj); err != nil { - t.Fatal(err) - } - if e, a := "stable.example.com/v2", convertedObj.GetAPIVersion(); e != a { - t.Errorf("expected= %v, actual= %v", e, a) - } - if e, a := "localhost", convertedObj.Object["host"]; e != a { - t.Errorf("expected= %v, actual= %v", e, a) - } - if e, a := "7070", convertedObj.Object["port"]; e != a { - t.Errorf("expected= %v, actual= %v", e, a) + yamlSerializer := json.NewYAMLSerializer(json.DefaultMetaFactory, scheme, scheme) + if _, _, err := yamlSerializer.Decode(response.Body.Bytes(), nil, &convertReview); err != nil { + t.Fatalf("cannot decode data: \n %v\n Error: %v", response.Body, err) + } + if convertReview.Response.Result.Status != v1.StatusSuccess { + t.Fatalf("cr conversion failed: %v", convertReview.Response) + } + convertedObj := unstructured.Unstructured{} + if _, _, err := yamlSerializer.Decode(convertReview.Response.ConvertedObjects[0].Raw, nil, &convertedObj); err != nil { + t.Fatal(err) + } + if e, a := "stable.example.com/v2", convertedObj.GetAPIVersion(); e != a { + t.Errorf("expected= %v, actual= %v", e, a) + } + if e, a := "localhost", convertedObj.Object["host"]; e != a { + t.Errorf("expected= %v, actual= %v", e, a) + } + if e, a := "7070", convertedObj.Object["port"]; e != a { + t.Errorf("expected= %v, actual= %v", e, a) + } + }) } } diff --git a/test/images/agnhost/crd-conversion-webhook/converter/framework.go b/test/images/agnhost/crd-conversion-webhook/converter/framework.go index 180277ff6e447..5d8aa3c68b895 100644 --- a/test/images/agnhost/crd-conversion-webhook/converter/framework.go +++ b/test/images/agnhost/crd-conversion-webhook/converter/framework.go @@ -26,29 +26,19 @@ import ( "k8s.io/klog" + "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/serializer/json" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" ) // convertFunc is the user defined function for any conversion. The code in this file is a // template that can be use for any CR conversion given this function. type convertFunc func(Object *unstructured.Unstructured, version string) (*unstructured.Unstructured, metav1.Status) -// conversionResponseFailureWithMessagef is a helper function to create an AdmissionResponse -// with a formatted embedded error message. -func conversionResponseFailureWithMessagef(msg string, params ...interface{}) *v1beta1.ConversionResponse { - return &v1beta1.ConversionResponse{ - Result: metav1.Status{ - Message: fmt.Sprintf(msg, params...), - Status: metav1.StatusFailure, - }, - } - -} - func statusErrorWithMessage(msg string, params ...interface{}) metav1.Status { return metav1.Status{ Message: fmt.Sprintf(msg, params...), @@ -62,15 +52,20 @@ func statusSucceed() metav1.Status { } } -// doConversion converts the requested object given the conversion function and returns a conversion response. -// failures will be reported as Reason in the conversion response. -func doConversion(convertRequest *v1beta1.ConversionRequest, convert convertFunc) *v1beta1.ConversionResponse { +// doConversionV1beta1 converts the requested objects in the v1beta1 ConversionRequest using the given conversion function and +// returns a conversion response. Failures are reported with the Reason in the conversion response. +func doConversionV1beta1(convertRequest *v1beta1.ConversionRequest, convert convertFunc) *v1beta1.ConversionResponse { var convertedObjects []runtime.RawExtension for _, obj := range convertRequest.Objects { cr := unstructured.Unstructured{} if err := cr.UnmarshalJSON(obj.Raw); err != nil { klog.Error(err) - return conversionResponseFailureWithMessagef("failed to unmarshall object (%v) with error: %v", string(obj.Raw), err) + return &v1beta1.ConversionResponse{ + Result: metav1.Status{ + Message: fmt.Sprintf("failed to unmarshall object (%v) with error: %v", string(obj.Raw), err), + Status: metav1.StatusFailure, + }, + } } convertedCR, status := convert(&cr, convertRequest.DesiredAPIVersion) if status.Status != metav1.StatusSuccess { @@ -88,6 +83,37 @@ func doConversion(convertRequest *v1beta1.ConversionRequest, convert convertFunc } } +// doConversionV1 converts the requested objects in the v1 ConversionRequest using the given conversion function and +// returns a conversion response. Failures are reported with the Reason in the conversion response. +func doConversionV1(convertRequest *v1.ConversionRequest, convert convertFunc) *v1.ConversionResponse { + var convertedObjects []runtime.RawExtension + for _, obj := range convertRequest.Objects { + cr := unstructured.Unstructured{} + if err := cr.UnmarshalJSON(obj.Raw); err != nil { + klog.Error(err) + return &v1.ConversionResponse{ + Result: metav1.Status{ + Message: fmt.Sprintf("failed to unmarshall object (%v) with error: %v", string(obj.Raw), err), + Status: metav1.StatusFailure, + }, + } + } + convertedCR, status := convert(&cr, convertRequest.DesiredAPIVersion) + if status.Status != metav1.StatusSuccess { + klog.Error(status.String()) + return &v1.ConversionResponse{ + Result: status, + } + } + convertedCR.SetAPIVersion(convertRequest.DesiredAPIVersion) + convertedObjects = append(convertedObjects, runtime.RawExtension{Object: convertedCR}) + } + return &v1.ConversionResponse{ + ConvertedObjects: convertedObjects, + Result: statusSucceed(), + } +} + func serve(w http.ResponseWriter, r *http.Request, convert convertFunc) { var body []byte if r.Body != nil { @@ -106,18 +132,52 @@ func serve(w http.ResponseWriter, r *http.Request, convert convertFunc) { } klog.V(2).Infof("handling request: %v", body) - convertReview := v1beta1.ConversionReview{} - if _, _, err := serializer.Decode(body, nil, &convertReview); err != nil { + obj, gvk, err := serializer.Decode(body, nil, nil) + if err != nil { + msg := fmt.Sprintf("failed to deserialize body (%v) with error %v", string(body), err) klog.Error(err) - convertReview.Response = conversionResponseFailureWithMessagef("failed to deserialize body (%v) with error %v", string(body), err) - } else { - convertReview.Response = doConversion(convertReview.Request, convert) - convertReview.Response.UID = convertReview.Request.UID + http.Error(w, msg, http.StatusBadRequest) + return } - klog.V(2).Info(fmt.Sprintf("sending response: %v", convertReview.Response)) - // reset the request, it is not needed in a response. - convertReview.Request = &v1beta1.ConversionRequest{} + var responseObj runtime.Object + switch *gvk { + case v1beta1.SchemeGroupVersion.WithKind("ConversionReview"): + convertReview, ok := obj.(*v1beta1.ConversionReview) + if !ok { + msg := fmt.Sprintf("Expected v1beta1.ConversionReview but got: %T", obj) + klog.Errorf(msg) + http.Error(w, msg, http.StatusBadRequest) + return + } + convertReview.Response = doConversionV1beta1(convertReview.Request, convert) + convertReview.Response.UID = convertReview.Request.UID + klog.V(2).Info(fmt.Sprintf("sending response: %v", convertReview.Response)) + + // reset the request, it is not needed in a response. + convertReview.Request = &v1beta1.ConversionRequest{} + responseObj = convertReview + case v1.SchemeGroupVersion.WithKind("ConversionReview"): + convertReview, ok := obj.(*v1.ConversionReview) + if !ok { + msg := fmt.Sprintf("Expected v1.ConversionReview but got: %T", obj) + klog.Errorf(msg) + http.Error(w, msg, http.StatusBadRequest) + return + } + convertReview.Response = doConversionV1(convertReview.Request, convert) + convertReview.Response.UID = convertReview.Request.UID + klog.V(2).Info(fmt.Sprintf("sending response: %v", convertReview.Response)) + + // reset the request, it is not needed in a response. + convertReview.Request = &v1.ConversionRequest{} + responseObj = convertReview + default: + msg := fmt.Sprintf("Unsupported group version kind: %v", gvk) + klog.Error(err) + http.Error(w, msg, http.StatusBadRequest) + return + } accept := r.Header.Get("Accept") outSerializer := getOutputSerializer(accept) @@ -127,7 +187,7 @@ func serve(w http.ResponseWriter, r *http.Request, convert convertFunc) { http.Error(w, msg, http.StatusBadRequest) return } - err := outSerializer.Encode(&convertReview, w) + err = outSerializer.Encode(responseObj, w) if err != nil { klog.Error(err) http.Error(w, err.Error(), http.StatusInternalServerError) @@ -145,6 +205,16 @@ type mediaType struct { } var scheme = runtime.NewScheme() + +func init() { + addToScheme(scheme) +} + +func addToScheme(scheme *runtime.Scheme) { + utilruntime.Must(v1.AddToScheme(scheme)) + utilruntime.Must(v1beta1.AddToScheme(scheme)) +} + var serializers = map[mediaType]runtime.Serializer{ {"application", "json"}: json.NewSerializer(json.DefaultMetaFactory, scheme, scheme, false), {"application", "yaml"}: json.NewYAMLSerializer(json.DefaultMetaFactory, scheme, scheme), diff --git a/test/images/agnhost/webhook/BUILD b/test/images/agnhost/webhook/BUILD index 7afaefa56a7c9..62566e4c889fc 100644 --- a/test/images/agnhost/webhook/BUILD +++ b/test/images/agnhost/webhook/BUILD @@ -23,6 +23,7 @@ go_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/v1:go_default_library", "//staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library", diff --git a/test/images/agnhost/webhook/crd.go b/test/images/agnhost/webhook/crd.go index 9562489a2bb30..80c59902a7791 100644 --- a/test/images/agnhost/webhook/crd.go +++ b/test/images/agnhost/webhook/crd.go @@ -20,37 +20,55 @@ import ( "fmt" "k8s.io/api/admission/v1" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "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. +// This function expects all CRDs submitted to it to be apiextensions.k8s.io/v1beta1 or apiextensions.k8s.io/v1. 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 toV1AdmissionResponse(err) - } + + resource := "customresourcedefinitions" + v1beta1GVR := metav1.GroupVersionResource{Group: apiextensionsv1beta1.GroupName, Version: "v1beta1", Resource: resource} + v1GVR := metav1.GroupVersionResource{Group: apiextensionsv1.GroupName, Version: "v1", Resource: resource} + + reviewResponse := v1.AdmissionResponse{} + reviewResponse.Allowed = true raw := ar.Request.Object.Raw - crd := apiextensionsv1beta1.CustomResourceDefinition{} - deserializer := codecs.UniversalDeserializer() - if _, _, err := deserializer.Decode(raw, nil, &crd); err != nil { + var labels map[string]string + + switch ar.Request.Resource { + case v1beta1GVR: + crd := apiextensionsv1beta1.CustomResourceDefinition{} + deserializer := codecs.UniversalDeserializer() + if _, _, err := deserializer.Decode(raw, nil, &crd); err != nil { + klog.Error(err) + return toV1AdmissionResponse(err) + } + labels = crd.Labels + case v1GVR: + crd := apiextensionsv1.CustomResourceDefinition{} + deserializer := codecs.UniversalDeserializer() + if _, _, err := deserializer.Decode(raw, nil, &crd); err != nil { + klog.Error(err) + return toV1AdmissionResponse(err) + } + labels = crd.Labels + default: + err := fmt.Errorf("expect resource to be one of [%v, %v] but got %v", v1beta1GVR, v1GVR, ar.Request.Resource) klog.Error(err) return toV1AdmissionResponse(err) } - reviewResponse := v1.AdmissionResponse{} - reviewResponse.Allowed = true - if v, ok := crd.Labels["webhook-e2e-test"]; ok { + if v, ok := labels["webhook-e2e-test"]; ok { if v == "webhook-disallow" { reviewResponse.Allowed = false reviewResponse.Result = &metav1.Status{Message: "the crd contains unwanted label"} } } return &reviewResponse + }