mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-23 03:41:45 +00:00
Upgrade ConversionReview e2e test image to also support v1
This commit is contained in:
parent
d21822a02a
commit
3f519b0ee2
@ -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",
|
||||
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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),
|
||||
|
@ -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",
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
raw := ar.Request.Object.Raw
|
||||
crd := apiextensionsv1beta1.CustomResourceDefinition{}
|
||||
deserializer := codecs.UniversalDeserializer()
|
||||
if _, _, err := deserializer.Decode(raw, nil, &crd); err != nil {
|
||||
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
|
||||
|
||||
if v, ok := crd.Labels["webhook-e2e-test"]; ok {
|
||||
raw := ar.Request.Object.Raw
|
||||
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)
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user