mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-11 04:52:08 +00:00
ConversionReview v1
This commit is contained in:
parent
fb64b76036
commit
ce769a5ede
@ -311,15 +311,14 @@ func validateEnumStrings(fldPath *field.Path, value string, accepted []string, r
|
|||||||
return field.ErrorList{field.NotSupported(fldPath, value, accepted)}
|
return field.ErrorList{field.NotSupported(fldPath, value, accepted)}
|
||||||
}
|
}
|
||||||
|
|
||||||
var acceptedConversionReviewVersion = []string{v1beta1.SchemeGroupVersion.Version}
|
// AcceptedConversionReviewVersions contains the list of ConversionReview versions the *prior* version of the API server understands.
|
||||||
|
// 1.15: server understands v1beta1; accepted versions are ["v1beta1"]
|
||||||
|
// 1.16: server understands v1, v1beta1; accepted versions are ["v1beta1"]
|
||||||
|
// TODO(liggitt): 1.17: server understands v1, v1beta1; accepted versions are ["v1","v1beta1"]
|
||||||
|
var acceptedConversionReviewVersions = sets.NewString(v1beta1.SchemeGroupVersion.Version)
|
||||||
|
|
||||||
func isAcceptedConversionReviewVersion(v string) bool {
|
func isAcceptedConversionReviewVersion(v string) bool {
|
||||||
for _, version := range acceptedConversionReviewVersion {
|
return acceptedConversionReviewVersions.Has(v)
|
||||||
if v == version {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateConversionReviewVersions(versions []string, requireRecognizedVersion bool, fldPath *field.Path) field.ErrorList {
|
func validateConversionReviewVersions(versions []string, requireRecognizedVersion bool, fldPath *field.Path) field.ErrorList {
|
||||||
@ -346,7 +345,7 @@ func validateConversionReviewVersions(versions []string, requireRecognizedVersio
|
|||||||
allErrs = append(allErrs, field.Invalid(
|
allErrs = append(allErrs, field.Invalid(
|
||||||
fldPath, versions,
|
fldPath, versions,
|
||||||
fmt.Sprintf("must include at least one of %v",
|
fmt.Sprintf("must include at least one of %v",
|
||||||
strings.Join(acceptedConversionReviewVersion, ", "))))
|
strings.Join(acceptedConversionReviewVersions.List(), ", "))))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return allErrs
|
return allErrs
|
||||||
|
@ -14,6 +14,7 @@ go_library(
|
|||||||
deps = [
|
deps = [
|
||||||
"//staging/src/k8s.io/api/autoscaling/v1:go_default_library",
|
"//staging/src/k8s.io/api/autoscaling/v1:go_default_library",
|
||||||
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions:go_default_library",
|
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions: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/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1:go_default_library",
|
||||||
"//staging/src/k8s.io/apiextensions-apiserver/pkg/features:go_default_library",
|
"//staging/src/k8s.io/apiextensions-apiserver/pkg/features:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/api/validation:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/api/validation:go_default_library",
|
||||||
@ -22,6 +23,7 @@ go_library(
|
|||||||
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/validation:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/validation:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
||||||
|
"//staging/src/k8s.io/apimachinery/pkg/types:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/util/uuid:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/util/uuid:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/util/validation/field:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/util/validation/field:go_default_library",
|
||||||
"//staging/src/k8s.io/apiserver/pkg/util/feature:go_default_library",
|
"//staging/src/k8s.io/apiserver/pkg/util/feature:go_default_library",
|
||||||
@ -56,11 +58,15 @@ go_test(
|
|||||||
embed = [":go_default_library"],
|
embed = [":go_default_library"],
|
||||||
deps = [
|
deps = [
|
||||||
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions:go_default_library",
|
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions: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/apis/meta/v1/unstructured: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:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/util/diff:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/util/diff:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/util/validation:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/util/validation:go_default_library",
|
||||||
"//staging/src/k8s.io/apiserver/pkg/util/webhook:go_default_library",
|
"//staging/src/k8s.io/apiserver/pkg/util/webhook:go_default_library",
|
||||||
|
"//vendor/github.com/google/go-cmp/cmp:go_default_library",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@ -18,22 +18,24 @@ package conversion
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
internal "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
||||||
|
v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||||
|
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
|
||||||
apivalidation "k8s.io/apimachinery/pkg/api/validation"
|
apivalidation "k8s.io/apimachinery/pkg/api/validation"
|
||||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
metav1validation "k8s.io/apimachinery/pkg/apis/meta/v1/validation"
|
metav1validation "k8s.io/apimachinery/pkg/apis/meta/v1/validation"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
"k8s.io/apimachinery/pkg/types"
|
||||||
"k8s.io/apimachinery/pkg/util/uuid"
|
"k8s.io/apimachinery/pkg/util/uuid"
|
||||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||||
"k8s.io/apiserver/pkg/util/webhook"
|
"k8s.io/apiserver/pkg/util/webhook"
|
||||||
"k8s.io/client-go/rest"
|
"k8s.io/client-go/rest"
|
||||||
|
|
||||||
internal "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
|
||||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
|
|
||||||
utiltrace "k8s.io/utils/trace"
|
utiltrace "k8s.io/utils/trace"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -42,7 +44,11 @@ type webhookConverterFactory struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func newWebhookConverterFactory(serviceResolver webhook.ServiceResolver, authResolverWrapper webhook.AuthenticationInfoResolverWrapper) (*webhookConverterFactory, error) {
|
func newWebhookConverterFactory(serviceResolver webhook.ServiceResolver, authResolverWrapper webhook.AuthenticationInfoResolverWrapper) (*webhookConverterFactory, error) {
|
||||||
clientManager, err := webhook.NewClientManager([]schema.GroupVersion{v1beta1.SchemeGroupVersion}, v1beta1.AddToScheme)
|
clientManager, err := webhook.NewClientManager(
|
||||||
|
[]schema.GroupVersion{v1.SchemeGroupVersion, v1beta1.SchemeGroupVersion},
|
||||||
|
v1beta1.AddToScheme,
|
||||||
|
v1.AddToScheme,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -116,7 +122,11 @@ func (c *webhookConverter) hasConversionReviewVersion(v string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func createConversionReview(obj runtime.Object, apiVersion string) *v1beta1.ConversionReview {
|
// getObjectsToConvert returns a list of objects requiring conversion.
|
||||||
|
// if obj is a list, getObjectsToConvert returns a (potentially empty) list of the items that are not already in the desired version.
|
||||||
|
// if obj is not a list, and is already in the desired version, getObjectsToConvert returns an empty list.
|
||||||
|
// if obj is not a list, and is not already in the desired version, getObjectsToConvert returns a list containing only obj.
|
||||||
|
func getObjectsToConvert(obj runtime.Object, apiVersion string) []runtime.RawExtension {
|
||||||
listObj, isList := obj.(*unstructured.UnstructuredList)
|
listObj, isList := obj.(*unstructured.UnstructuredList)
|
||||||
var objects []runtime.RawExtension
|
var objects []runtime.RawExtension
|
||||||
if isList {
|
if isList {
|
||||||
@ -131,15 +141,35 @@ func createConversionReview(obj runtime.Object, apiVersion string) *v1beta1.Conv
|
|||||||
objects = []runtime.RawExtension{{Object: obj}}
|
objects = []runtime.RawExtension{{Object: obj}}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return objects
|
||||||
|
}
|
||||||
|
|
||||||
|
// createConversionReviewObjects returns ConversionReview request and response objects for the first supported version found in conversionReviewVersions.
|
||||||
|
func createConversionReviewObjects(conversionReviewVersions []string, objects []runtime.RawExtension, apiVersion string, requestUID types.UID) (request, response runtime.Object, err error) {
|
||||||
|
for _, version := range conversionReviewVersions {
|
||||||
|
switch version {
|
||||||
|
case v1beta1.SchemeGroupVersion.Version:
|
||||||
return &v1beta1.ConversionReview{
|
return &v1beta1.ConversionReview{
|
||||||
Request: &v1beta1.ConversionRequest{
|
Request: &v1beta1.ConversionRequest{
|
||||||
Objects: objects,
|
Objects: objects,
|
||||||
DesiredAPIVersion: apiVersion,
|
DesiredAPIVersion: apiVersion,
|
||||||
UID: uuid.NewUUID(),
|
UID: requestUID,
|
||||||
},
|
},
|
||||||
Response: &v1beta1.ConversionResponse{},
|
Response: &v1beta1.ConversionResponse{},
|
||||||
|
}, &v1beta1.ConversionReview{}, nil
|
||||||
|
case v1.SchemeGroupVersion.Version:
|
||||||
|
return &v1.ConversionReview{
|
||||||
|
Request: &v1.ConversionRequest{
|
||||||
|
Objects: objects,
|
||||||
|
DesiredAPIVersion: apiVersion,
|
||||||
|
UID: requestUID,
|
||||||
|
},
|
||||||
|
Response: &v1.ConversionResponse{},
|
||||||
|
}, &v1.ConversionReview{}, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return nil, nil, fmt.Errorf("no supported conversion review versions")
|
||||||
|
}
|
||||||
|
|
||||||
func getRawExtensionObject(rx runtime.RawExtension) (runtime.Object, error) {
|
func getRawExtensionObject(rx runtime.RawExtension) (runtime.Object, error) {
|
||||||
if rx.Object != nil {
|
if rx.Object != nil {
|
||||||
@ -153,6 +183,59 @@ func getRawExtensionObject(rx runtime.RawExtension) (runtime.Object, error) {
|
|||||||
return &u, nil
|
return &u, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getConvertedObjectsFromResponse validates the response, and returns the converted objects.
|
||||||
|
// if the response is malformed, an error is returned instead.
|
||||||
|
// if the response does not indicate success, the error message is returned instead.
|
||||||
|
func getConvertedObjectsFromResponse(expectedUID types.UID, response runtime.Object) (convertedObjects []runtime.RawExtension, err error) {
|
||||||
|
switch response := response.(type) {
|
||||||
|
case *v1.ConversionReview:
|
||||||
|
// Verify GVK to make sure we decoded what we intended to
|
||||||
|
v1GVK := v1.SchemeGroupVersion.WithKind("ConversionReview")
|
||||||
|
if response.GroupVersionKind() != v1GVK {
|
||||||
|
return nil, fmt.Errorf("expected webhook response of %v, got %v", v1GVK.String(), response.GroupVersionKind().String())
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.Response == nil {
|
||||||
|
return nil, fmt.Errorf("no response provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify UID to make sure this response was actually meant for the request we sent
|
||||||
|
if response.Response.UID != expectedUID {
|
||||||
|
return nil, fmt.Errorf("expected response.uid=%q, got %q", expectedUID, response.Response.UID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.Response.Result.Status != metav1.StatusSuccess {
|
||||||
|
// TODO: Return a webhook specific error to be able to convert it to meta.Status
|
||||||
|
if len(response.Response.Result.Message) > 0 {
|
||||||
|
return nil, errors.New(response.Response.Result.Message)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("response.result.status was '%s', not 'Success'", response.Response.Result.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.Response.ConvertedObjects, nil
|
||||||
|
|
||||||
|
case *v1beta1.ConversionReview:
|
||||||
|
// v1beta1 processing did not verify GVK or UID, so skip those for compatibility
|
||||||
|
|
||||||
|
if response.Response == nil {
|
||||||
|
return nil, fmt.Errorf("no response provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.Response.Result.Status != metav1.StatusSuccess {
|
||||||
|
// TODO: Return a webhook specific error to be able to convert it to meta.Status
|
||||||
|
if len(response.Response.Result.Message) > 0 {
|
||||||
|
return nil, errors.New(response.Response.Result.Message)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("response.result.status was '%s', not 'Success'", response.Response.Result.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.Response.ConvertedObjects, nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unrecognized response type: %T", response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (c *webhookConverter) Convert(in runtime.Object, toGV schema.GroupVersion) (runtime.Object, error) {
|
func (c *webhookConverter) Convert(in runtime.Object, toGV schema.GroupVersion) (runtime.Object, error) {
|
||||||
// In general, the webhook should not do any defaulting or validation. A special case of that is an empty object
|
// In general, the webhook should not do any defaulting or validation. A special case of that is an empty object
|
||||||
// conversion that must result an empty object and practically is the same as nopConverter.
|
// conversion that must result an empty object and practically is the same as nopConverter.
|
||||||
@ -165,18 +248,22 @@ func (c *webhookConverter) Convert(in runtime.Object, toGV schema.GroupVersion)
|
|||||||
|
|
||||||
listObj, isList := in.(*unstructured.UnstructuredList)
|
listObj, isList := in.(*unstructured.UnstructuredList)
|
||||||
|
|
||||||
// Currently converter only supports `v1beta1` ConversionReview
|
requestUID := uuid.NewUUID()
|
||||||
// TODO: Make CRD webhooks caller capable of sending/receiving multiple ConversionReview versions
|
desiredAPIVersion := toGV.String()
|
||||||
if !c.hasConversionReviewVersion(v1beta1.SchemeGroupVersion.Version) {
|
objectsToConvert := getObjectsToConvert(in, desiredAPIVersion)
|
||||||
return nil, fmt.Errorf("webhook does not accept v1beta1 ConversionReview")
|
request, response, err := createConversionReviewObjects(c.conversionReviewVersions, objectsToConvert, desiredAPIVersion, requestUID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
request := createConversionReview(in, toGV.String())
|
objCount := len(objectsToConvert)
|
||||||
objCount := len(request.Request.Objects)
|
|
||||||
if objCount == 0 {
|
if objCount == 0 {
|
||||||
|
// no objects needed conversion
|
||||||
if !isList {
|
if !isList {
|
||||||
|
// for a single item, return as-is
|
||||||
return in, nil
|
return in, nil
|
||||||
}
|
}
|
||||||
|
// for a list, set the version of the top-level list object (all individual objects are already in the correct version)
|
||||||
out := listObj.DeepCopy()
|
out := listObj.DeepCopy()
|
||||||
out.SetAPIVersion(toGV.String())
|
out.SetAPIVersion(toGV.String())
|
||||||
return out, nil
|
return out, nil
|
||||||
@ -184,35 +271,30 @@ func (c *webhookConverter) Convert(in runtime.Object, toGV schema.GroupVersion)
|
|||||||
|
|
||||||
trace := utiltrace.New("Call conversion webhook",
|
trace := utiltrace.New("Call conversion webhook",
|
||||||
utiltrace.Field{"custom-resource-definition", c.name},
|
utiltrace.Field{"custom-resource-definition", c.name},
|
||||||
utiltrace.Field{"desired-api-version", request.Request.DesiredAPIVersion},
|
utiltrace.Field{"desired-api-version", desiredAPIVersion},
|
||||||
utiltrace.Field{"object-count", objCount},
|
utiltrace.Field{"object-count", objCount},
|
||||||
utiltrace.Field{"UID", request.Request.UID})
|
utiltrace.Field{"UID", requestUID})
|
||||||
// Only log conversion webhook traces that exceed a 8ms per object limit plus a 50ms request overhead allowance.
|
// Only log conversion webhook traces that exceed a 8ms per object limit plus a 50ms request overhead allowance.
|
||||||
// The per object limit uses the SLO for conversion webhooks (~4ms per object) plus time to serialize/deserialize
|
// The per object limit uses the SLO for conversion webhooks (~4ms per object) plus time to serialize/deserialize
|
||||||
// the conversion request on the apiserver side (~4ms per object).
|
// the conversion request on the apiserver side (~4ms per object).
|
||||||
defer trace.LogIfLong(time.Duration(50+8*objCount) * time.Millisecond)
|
defer trace.LogIfLong(time.Duration(50+8*objCount) * time.Millisecond)
|
||||||
|
|
||||||
response := &v1beta1.ConversionReview{}
|
|
||||||
// TODO: Figure out if adding one second timeout make sense here.
|
// TODO: Figure out if adding one second timeout make sense here.
|
||||||
ctx := context.TODO()
|
ctx := context.TODO()
|
||||||
r := c.restClient.Post().Context(ctx).Body(request).Do()
|
r := c.restClient.Post().Context(ctx).Body(request).Do()
|
||||||
if err := r.Into(response); err != nil {
|
if err := r.Into(response); err != nil {
|
||||||
// TODO: Return a webhook specific error to be able to convert it to meta.Status
|
// TODO: Return a webhook specific error to be able to convert it to meta.Status
|
||||||
return nil, fmt.Errorf("conversion webhook for %v failed: %v", in.GetObjectKind(), err)
|
return nil, fmt.Errorf("conversion webhook for %v failed: %v", in.GetObjectKind().GroupVersionKind(), err)
|
||||||
}
|
}
|
||||||
trace.Step("Request completed")
|
trace.Step("Request completed")
|
||||||
|
|
||||||
if response.Response == nil {
|
convertedObjects, err := getConvertedObjectsFromResponse(requestUID, response)
|
||||||
// TODO: Return a webhook specific error to be able to convert it to meta.Status
|
if err != nil {
|
||||||
return nil, fmt.Errorf("conversion webhook for %v lacked response", in.GetObjectKind())
|
return nil, fmt.Errorf("conversion webhook for %v failed: %v", in.GetObjectKind().GroupVersionKind(), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if response.Response.Result.Status != v1.StatusSuccess {
|
if len(convertedObjects) != len(objectsToConvert) {
|
||||||
return nil, fmt.Errorf("conversion webhook for %v failed: %v", in.GetObjectKind(), response.Response.Result.Message)
|
return nil, fmt.Errorf("conversion webhook for %v returned %d objects, expected %d", in.GetObjectKind().GroupVersionKind(), len(convertedObjects), len(objectsToConvert))
|
||||||
}
|
|
||||||
|
|
||||||
if len(response.Response.ConvertedObjects) != len(request.Request.Objects) {
|
|
||||||
return nil, fmt.Errorf("conversion webhook for %v returned %d objects, expected %d", in.GetObjectKind(), len(response.Response.ConvertedObjects), len(request.Request.Objects))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if isList {
|
if isList {
|
||||||
@ -227,27 +309,27 @@ func (c *webhookConverter) Convert(in runtime.Object, toGV schema.GroupVersion)
|
|||||||
// convertedList has the right item already.
|
// convertedList has the right item already.
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
converted, err := getRawExtensionObject(response.Response.ConvertedObjects[convertedIndex])
|
converted, err := getRawExtensionObject(convertedObjects[convertedIndex])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("conversion webhook for %v returned invalid converted object at index %v: %v", in.GetObjectKind(), convertedIndex, err)
|
return nil, fmt.Errorf("conversion webhook for %v returned invalid converted object at index %v: %v", in.GetObjectKind().GroupVersionKind(), convertedIndex, err)
|
||||||
}
|
}
|
||||||
convertedIndex++
|
convertedIndex++
|
||||||
if expected, got := toGV, converted.GetObjectKind().GroupVersionKind().GroupVersion(); expected != got {
|
if expected, got := toGV, converted.GetObjectKind().GroupVersionKind().GroupVersion(); expected != got {
|
||||||
return nil, fmt.Errorf("conversion webhook for %v returned invalid converted object at index %v: invalid groupVersion, expected=%v, got=%v", in.GetObjectKind(), convertedIndex, expected, got)
|
return nil, fmt.Errorf("conversion webhook for %v returned invalid converted object at index %v: invalid groupVersion (expected %v, received %v)", in.GetObjectKind().GroupVersionKind(), convertedIndex, expected, got)
|
||||||
}
|
}
|
||||||
if expected, got := original.GetObjectKind().GroupVersionKind().Kind, converted.GetObjectKind().GroupVersionKind().Kind; expected != got {
|
if expected, got := original.GetObjectKind().GroupVersionKind().Kind, converted.GetObjectKind().GroupVersionKind().Kind; expected != got {
|
||||||
return nil, fmt.Errorf("conversion webhook for %v returned invalid converted object at index %v: invalid kind, expected=%v, got=%v", in.GetObjectKind(), convertedIndex, expected, got)
|
return nil, fmt.Errorf("conversion webhook for %v returned invalid converted object at index %v: invalid kind (expected %v, received %v)", in.GetObjectKind().GroupVersionKind(), convertedIndex, expected, got)
|
||||||
}
|
}
|
||||||
unstructConverted, ok := converted.(*unstructured.Unstructured)
|
unstructConverted, ok := converted.(*unstructured.Unstructured)
|
||||||
if !ok {
|
if !ok {
|
||||||
// this should not happened
|
// this should not happened
|
||||||
return nil, fmt.Errorf("conversion webhook for %v returned invalid converted object at index %v: invalid type, expected=Unstructured, got=%T", in.GetObjectKind(), convertedIndex, converted)
|
return nil, fmt.Errorf("conversion webhook for %v returned invalid converted object at index %v: invalid type, expected=Unstructured, got=%T", in.GetObjectKind().GroupVersionKind(), convertedIndex, converted)
|
||||||
}
|
}
|
||||||
if err := validateConvertedObject(original, unstructConverted); err != nil {
|
if err := validateConvertedObject(original, unstructConverted); err != nil {
|
||||||
return nil, fmt.Errorf("conversion webhook for %v returned invalid converted object at index %v: %v", in.GetObjectKind(), convertedIndex, err)
|
return nil, fmt.Errorf("conversion webhook for %v returned invalid converted object at index %v: %v", in.GetObjectKind().GroupVersionKind(), convertedIndex, err)
|
||||||
}
|
}
|
||||||
if err := restoreObjectMeta(original, unstructConverted); err != nil {
|
if err := restoreObjectMeta(original, unstructConverted); err != nil {
|
||||||
return nil, fmt.Errorf("conversion webhook for %v returned invalid metadata in object at index %v: %v", in.GetObjectKind(), convertedIndex, err)
|
return nil, fmt.Errorf("conversion webhook for %v returned invalid metadata in object at index %v: %v", in.GetObjectKind().GroupVersionKind(), convertedIndex, err)
|
||||||
}
|
}
|
||||||
convertedList.Items[i] = *unstructConverted
|
convertedList.Items[i] = *unstructConverted
|
||||||
}
|
}
|
||||||
@ -255,35 +337,35 @@ func (c *webhookConverter) Convert(in runtime.Object, toGV schema.GroupVersion)
|
|||||||
return convertedList, nil
|
return convertedList, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(response.Response.ConvertedObjects) != 1 {
|
if len(convertedObjects) != 1 {
|
||||||
// This should not happened
|
// This should not happened
|
||||||
return nil, fmt.Errorf("conversion webhook for %v failed", in.GetObjectKind())
|
return nil, fmt.Errorf("conversion webhook for %v failed, no objects returned", in.GetObjectKind())
|
||||||
}
|
}
|
||||||
converted, err := getRawExtensionObject(response.Response.ConvertedObjects[0])
|
converted, err := getRawExtensionObject(convertedObjects[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if e, a := toGV, converted.GetObjectKind().GroupVersionKind().GroupVersion(); e != a {
|
if e, a := toGV, converted.GetObjectKind().GroupVersionKind().GroupVersion(); e != a {
|
||||||
return nil, fmt.Errorf("conversion webhook for %v returned invalid object: invalid groupVersion, e=%v, a=%v", in.GetObjectKind(), e, a)
|
return nil, fmt.Errorf("conversion webhook for %v returned invalid object at index 0: invalid groupVersion (expected %v, received %v)", in.GetObjectKind().GroupVersionKind(), e, a)
|
||||||
}
|
}
|
||||||
if e, a := in.GetObjectKind().GroupVersionKind().Kind, converted.GetObjectKind().GroupVersionKind().Kind; e != a {
|
if e, a := in.GetObjectKind().GroupVersionKind().Kind, converted.GetObjectKind().GroupVersionKind().Kind; e != a {
|
||||||
return nil, fmt.Errorf("conversion webhook for %v returned invalid object: invalid kind, e=%v, a=%v", in.GetObjectKind(), e, a)
|
return nil, fmt.Errorf("conversion webhook for %v returned invalid object at index 0: invalid kind (expected %v, received %v)", in.GetObjectKind().GroupVersionKind(), e, a)
|
||||||
}
|
}
|
||||||
unstructConverted, ok := converted.(*unstructured.Unstructured)
|
unstructConverted, ok := converted.(*unstructured.Unstructured)
|
||||||
if !ok {
|
if !ok {
|
||||||
// this should not happened
|
// this should not happened
|
||||||
return nil, fmt.Errorf("conversion webhook for %v failed", in.GetObjectKind())
|
return nil, fmt.Errorf("conversion webhook for %v failed, unexpected type %T at index 0", in.GetObjectKind().GroupVersionKind(), converted)
|
||||||
}
|
}
|
||||||
unstructIn, ok := in.(*unstructured.Unstructured)
|
unstructIn, ok := in.(*unstructured.Unstructured)
|
||||||
if !ok {
|
if !ok {
|
||||||
// this should not happened
|
// this should not happened
|
||||||
return nil, fmt.Errorf("conversion webhook for %v failed", in.GetObjectKind())
|
return nil, fmt.Errorf("conversion webhook for %v failed unexpected input type %T", in.GetObjectKind().GroupVersionKind(), in)
|
||||||
}
|
}
|
||||||
if err := validateConvertedObject(unstructIn, unstructConverted); err != nil {
|
if err := validateConvertedObject(unstructIn, unstructConverted); err != nil {
|
||||||
return nil, fmt.Errorf("conversion webhook for %v returned invalid object: %v", in.GetObjectKind(), err)
|
return nil, fmt.Errorf("conversion webhook for %v returned invalid object: %v", in.GetObjectKind().GroupVersionKind(), err)
|
||||||
}
|
}
|
||||||
if err := restoreObjectMeta(unstructIn, unstructConverted); err != nil {
|
if err := restoreObjectMeta(unstructIn, unstructConverted); err != nil {
|
||||||
return nil, fmt.Errorf("conversion webhook for %v returned invalid metadata: %v", in.GetObjectKind(), err)
|
return nil, fmt.Errorf("conversion webhook for %v returned invalid metadata: %v", in.GetObjectKind().GroupVersionKind(), err)
|
||||||
}
|
}
|
||||||
return converted, nil
|
return converted, nil
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,13 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
|
||||||
|
v1 "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/apis/meta/v1/unstructured"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apimachinery/pkg/util/diff"
|
"k8s.io/apimachinery/pkg/util/diff"
|
||||||
"k8s.io/apimachinery/pkg/util/validation"
|
"k8s.io/apimachinery/pkg/util/validation"
|
||||||
)
|
)
|
||||||
@ -199,3 +205,352 @@ func TestRestoreObjectMeta(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetObjectsToConvert(t *testing.T) {
|
||||||
|
v1Object := &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "foo/v1", "kind": "Widget", "metadata": map[string]interface{}{"name": "myv1"}}}
|
||||||
|
v2Object := &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "foo/v2", "kind": "Widget", "metadata": map[string]interface{}{"name": "myv2"}}}
|
||||||
|
v3Object := &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "foo/v3", "kind": "Widget", "metadata": map[string]interface{}{"name": "myv3"}}}
|
||||||
|
|
||||||
|
testcases := []struct {
|
||||||
|
Name string
|
||||||
|
Object runtime.Object
|
||||||
|
APIVersion string
|
||||||
|
|
||||||
|
ExpectObjects []runtime.RawExtension
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
Name: "empty list",
|
||||||
|
Object: &unstructured.UnstructuredList{},
|
||||||
|
APIVersion: "foo/v1",
|
||||||
|
ExpectObjects: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "one-item list, in desired version",
|
||||||
|
Object: &unstructured.UnstructuredList{
|
||||||
|
Items: []unstructured.Unstructured{*v1Object},
|
||||||
|
},
|
||||||
|
APIVersion: "foo/v1",
|
||||||
|
ExpectObjects: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "one-item list, not in desired version",
|
||||||
|
Object: &unstructured.UnstructuredList{
|
||||||
|
Items: []unstructured.Unstructured{*v2Object},
|
||||||
|
},
|
||||||
|
APIVersion: "foo/v1",
|
||||||
|
ExpectObjects: []runtime.RawExtension{{Object: v2Object}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "multi-item list, in desired version",
|
||||||
|
Object: &unstructured.UnstructuredList{
|
||||||
|
Items: []unstructured.Unstructured{*v1Object, *v1Object, *v1Object},
|
||||||
|
},
|
||||||
|
APIVersion: "foo/v1",
|
||||||
|
ExpectObjects: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "multi-item list, mixed versions",
|
||||||
|
Object: &unstructured.UnstructuredList{
|
||||||
|
Items: []unstructured.Unstructured{*v1Object, *v2Object, *v3Object},
|
||||||
|
},
|
||||||
|
APIVersion: "foo/v1",
|
||||||
|
ExpectObjects: []runtime.RawExtension{{Object: v2Object}, {Object: v3Object}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "single item, in desired version",
|
||||||
|
Object: v1Object,
|
||||||
|
APIVersion: "foo/v1",
|
||||||
|
ExpectObjects: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "single item, not in desired version",
|
||||||
|
Object: v2Object,
|
||||||
|
APIVersion: "foo/v1",
|
||||||
|
ExpectObjects: []runtime.RawExtension{{Object: v2Object}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range testcases {
|
||||||
|
t.Run(tc.Name, func(t *testing.T) {
|
||||||
|
if objects := getObjectsToConvert(tc.Object, tc.APIVersion); !reflect.DeepEqual(objects, tc.ExpectObjects) {
|
||||||
|
t.Errorf("unexpected diff: %s", cmp.Diff(tc.ExpectObjects, objects))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateConversionReviewObjects(t *testing.T) {
|
||||||
|
objects := []runtime.RawExtension{
|
||||||
|
{Object: &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "foo/v2", "Kind": "Widget"}}},
|
||||||
|
}
|
||||||
|
|
||||||
|
testcases := []struct {
|
||||||
|
Name string
|
||||||
|
Versions []string
|
||||||
|
|
||||||
|
ExpectRequest runtime.Object
|
||||||
|
ExpectResponse runtime.Object
|
||||||
|
ExpectErr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
Name: "no supported versions",
|
||||||
|
Versions: []string{"vx"},
|
||||||
|
ExpectErr: "no supported conversion review versions",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "v1",
|
||||||
|
Versions: []string{"v1", "v1beta1", "v2"},
|
||||||
|
ExpectRequest: &v1.ConversionReview{
|
||||||
|
Request: &v1.ConversionRequest{UID: "uid", DesiredAPIVersion: "foo/v1", Objects: objects},
|
||||||
|
Response: &v1.ConversionResponse{},
|
||||||
|
},
|
||||||
|
ExpectResponse: &v1.ConversionReview{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "v1beta1",
|
||||||
|
Versions: []string{"v1beta1", "v1", "v2"},
|
||||||
|
ExpectRequest: &v1beta1.ConversionReview{
|
||||||
|
Request: &v1beta1.ConversionRequest{UID: "uid", DesiredAPIVersion: "foo/v1", Objects: objects},
|
||||||
|
Response: &v1beta1.ConversionResponse{},
|
||||||
|
},
|
||||||
|
ExpectResponse: &v1beta1.ConversionReview{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testcases {
|
||||||
|
t.Run(tc.Name, func(t *testing.T) {
|
||||||
|
request, response, err := createConversionReviewObjects(tc.Versions, objects, "foo/v1", "uid")
|
||||||
|
|
||||||
|
if err == nil && len(tc.ExpectErr) > 0 {
|
||||||
|
t.Errorf("expected error, got none")
|
||||||
|
} else if err != nil && len(tc.ExpectErr) == 0 {
|
||||||
|
t.Errorf("unexpected error %v", err)
|
||||||
|
} else if err != nil && !strings.Contains(err.Error(), tc.ExpectErr) {
|
||||||
|
t.Errorf("expected error containing %q, got %v", tc.ExpectErr, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if e, a := tc.ExpectRequest, request; !reflect.DeepEqual(e, a) {
|
||||||
|
t.Errorf("unexpected diff: %s", cmp.Diff(e, a))
|
||||||
|
}
|
||||||
|
if e, a := tc.ExpectResponse, response; !reflect.DeepEqual(e, a) {
|
||||||
|
t.Errorf("unexpected diff: %s", cmp.Diff(e, a))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetConvertedObjectsFromResponse(t *testing.T) {
|
||||||
|
v1Object := &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "foo/v1", "kind": "Widget", "metadata": map[string]interface{}{"name": "myv1"}}}
|
||||||
|
|
||||||
|
testcases := []struct {
|
||||||
|
Name string
|
||||||
|
Response runtime.Object
|
||||||
|
|
||||||
|
ExpectObjects []runtime.RawExtension
|
||||||
|
ExpectErr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
Name: "nil response",
|
||||||
|
Response: nil,
|
||||||
|
ExpectErr: "unrecognized response type",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "unknown type",
|
||||||
|
Response: &unstructured.Unstructured{},
|
||||||
|
ExpectErr: "unrecognized response type",
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
Name: "minimal valid v1beta1",
|
||||||
|
Response: &v1beta1.ConversionReview{
|
||||||
|
// apiVersion/kind were not validated originally, preserve backward compatibility
|
||||||
|
Response: &v1beta1.ConversionResponse{
|
||||||
|
// uid was not validated originally, preserve backward compatibility
|
||||||
|
Result: metav1.Status{Status: metav1.StatusSuccess},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ExpectObjects: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "valid v1beta1 with objects",
|
||||||
|
Response: &v1beta1.ConversionReview{
|
||||||
|
// apiVersion/kind were not validated originally, preserve backward compatibility
|
||||||
|
Response: &v1beta1.ConversionResponse{
|
||||||
|
// uid was not validated originally, preserve backward compatibility
|
||||||
|
Result: metav1.Status{Status: metav1.StatusSuccess},
|
||||||
|
ConvertedObjects: []runtime.RawExtension{{Object: v1Object}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ExpectObjects: []runtime.RawExtension{{Object: v1Object}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "error v1beta1, empty status",
|
||||||
|
Response: &v1beta1.ConversionReview{
|
||||||
|
Response: &v1beta1.ConversionResponse{
|
||||||
|
Result: metav1.Status{Status: ""},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ExpectErr: `response.result.status was '', not 'Success'`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "error v1beta1, failure status",
|
||||||
|
Response: &v1beta1.ConversionReview{
|
||||||
|
Response: &v1beta1.ConversionResponse{
|
||||||
|
Result: metav1.Status{Status: metav1.StatusFailure},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ExpectErr: `response.result.status was 'Failure', not 'Success'`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "error v1beta1, custom status",
|
||||||
|
Response: &v1beta1.ConversionReview{
|
||||||
|
Response: &v1beta1.ConversionResponse{
|
||||||
|
Result: metav1.Status{Status: metav1.StatusFailure, Message: "some failure message"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ExpectErr: `some failure message`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "invalid v1beta1, no response",
|
||||||
|
Response: &v1beta1.ConversionReview{},
|
||||||
|
ExpectErr: "no response provided",
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
Name: "minimal valid v1",
|
||||||
|
Response: &v1.ConversionReview{
|
||||||
|
TypeMeta: metav1.TypeMeta{APIVersion: "apiextensions.k8s.io/v1", Kind: "ConversionReview"},
|
||||||
|
Response: &v1.ConversionResponse{
|
||||||
|
UID: "uid",
|
||||||
|
Result: metav1.Status{Status: metav1.StatusSuccess},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ExpectObjects: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "valid v1 with objects",
|
||||||
|
Response: &v1.ConversionReview{
|
||||||
|
TypeMeta: metav1.TypeMeta{APIVersion: "apiextensions.k8s.io/v1", Kind: "ConversionReview"},
|
||||||
|
Response: &v1.ConversionResponse{
|
||||||
|
UID: "uid",
|
||||||
|
Result: metav1.Status{Status: metav1.StatusSuccess},
|
||||||
|
ConvertedObjects: []runtime.RawExtension{{Object: v1Object}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ExpectObjects: []runtime.RawExtension{{Object: v1Object}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "invalid v1, no uid",
|
||||||
|
Response: &v1.ConversionReview{
|
||||||
|
TypeMeta: metav1.TypeMeta{APIVersion: "apiextensions.k8s.io/v1", Kind: "ConversionReview"},
|
||||||
|
Response: &v1.ConversionResponse{
|
||||||
|
Result: metav1.Status{Status: metav1.StatusSuccess},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ExpectErr: `expected response.uid="uid"`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "invalid v1, no apiVersion",
|
||||||
|
Response: &v1.ConversionReview{
|
||||||
|
TypeMeta: metav1.TypeMeta{Kind: "ConversionReview"},
|
||||||
|
Response: &v1.ConversionResponse{
|
||||||
|
UID: "uid",
|
||||||
|
Result: metav1.Status{Status: metav1.StatusSuccess},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ExpectErr: `expected webhook response of apiextensions.k8s.io/v1, Kind=ConversionReview`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "invalid v1, no kind",
|
||||||
|
Response: &v1.ConversionReview{
|
||||||
|
TypeMeta: metav1.TypeMeta{APIVersion: "apiextensions.k8s.io/v1"},
|
||||||
|
Response: &v1.ConversionResponse{
|
||||||
|
UID: "uid",
|
||||||
|
Result: metav1.Status{Status: metav1.StatusSuccess},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ExpectErr: `expected webhook response of apiextensions.k8s.io/v1, Kind=ConversionReview`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "invalid v1, mismatched apiVersion",
|
||||||
|
Response: &v1.ConversionReview{
|
||||||
|
TypeMeta: metav1.TypeMeta{APIVersion: "apiextensions.k8s.io/v2", Kind: "ConversionReview"},
|
||||||
|
Response: &v1.ConversionResponse{
|
||||||
|
UID: "uid",
|
||||||
|
Result: metav1.Status{Status: metav1.StatusSuccess},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ExpectErr: `expected webhook response of apiextensions.k8s.io/v1, Kind=ConversionReview`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "invalid v1, mismatched kind",
|
||||||
|
Response: &v1.ConversionReview{
|
||||||
|
TypeMeta: metav1.TypeMeta{APIVersion: "apiextensions.k8s.io/v1", Kind: "ConversionReview2"},
|
||||||
|
Response: &v1.ConversionResponse{
|
||||||
|
UID: "uid",
|
||||||
|
Result: metav1.Status{Status: metav1.StatusSuccess},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ExpectErr: `expected webhook response of apiextensions.k8s.io/v1, Kind=ConversionReview`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "error v1, empty status",
|
||||||
|
Response: &v1.ConversionReview{
|
||||||
|
TypeMeta: metav1.TypeMeta{APIVersion: "apiextensions.k8s.io/v1", Kind: "ConversionReview"},
|
||||||
|
Response: &v1.ConversionResponse{
|
||||||
|
UID: "uid",
|
||||||
|
Result: metav1.Status{Status: ""},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ExpectErr: `response.result.status was '', not 'Success'`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "error v1, failure status",
|
||||||
|
Response: &v1.ConversionReview{
|
||||||
|
TypeMeta: metav1.TypeMeta{APIVersion: "apiextensions.k8s.io/v1", Kind: "ConversionReview"},
|
||||||
|
Response: &v1.ConversionResponse{
|
||||||
|
UID: "uid",
|
||||||
|
Result: metav1.Status{Status: metav1.StatusFailure},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ExpectErr: `response.result.status was 'Failure', not 'Success'`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "error v1, custom status",
|
||||||
|
Response: &v1.ConversionReview{
|
||||||
|
TypeMeta: metav1.TypeMeta{APIVersion: "apiextensions.k8s.io/v1", Kind: "ConversionReview"},
|
||||||
|
Response: &v1.ConversionResponse{
|
||||||
|
UID: "uid",
|
||||||
|
Result: metav1.Status{Status: metav1.StatusFailure, Message: "some failure message"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ExpectErr: `some failure message`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "invalid v1, no response",
|
||||||
|
Response: &v1.ConversionReview{
|
||||||
|
TypeMeta: metav1.TypeMeta{APIVersion: "apiextensions.k8s.io/v1", Kind: "ConversionReview"},
|
||||||
|
},
|
||||||
|
ExpectErr: "no response provided",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testcases {
|
||||||
|
t.Run(tc.Name, func(t *testing.T) {
|
||||||
|
|
||||||
|
objects, err := getConvertedObjectsFromResponse("uid", tc.Response)
|
||||||
|
|
||||||
|
if err == nil && len(tc.ExpectErr) > 0 {
|
||||||
|
t.Errorf("expected error, got none")
|
||||||
|
} else if err != nil && len(tc.ExpectErr) == 0 {
|
||||||
|
t.Errorf("unexpected error %v", err)
|
||||||
|
} else if err != nil && !strings.Contains(err.Error(), tc.ExpectErr) {
|
||||||
|
t.Errorf("expected error containing %q, got %v", tc.ExpectErr, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(objects, tc.ExpectObjects) {
|
||||||
|
t.Errorf("unexpected diff: %s", cmp.Diff(tc.ExpectObjects, objects))
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -39,6 +39,7 @@ go_library(
|
|||||||
importpath = "k8s.io/apiextensions-apiserver/test/integration/conversion",
|
importpath = "k8s.io/apiextensions-apiserver/test/integration/conversion",
|
||||||
visibility = ["//visibility:public"],
|
visibility = ["//visibility:public"],
|
||||||
deps = [
|
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/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:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||||
|
@ -73,38 +73,93 @@ func testWebhookConverter(t *testing.T, defaulting, watchCache bool) {
|
|||||||
tests := []struct {
|
tests := []struct {
|
||||||
group string
|
group string
|
||||||
handler http.Handler
|
handler http.Handler
|
||||||
|
reviewVersions []string
|
||||||
checks []Checker
|
checks []Checker
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
group: "noop-converter",
|
group: "noop-converter-v1",
|
||||||
handler: NewObjectConverterWebhookHandler(t, noopConverter),
|
handler: NewObjectConverterWebhookHandler(t, noopConverter),
|
||||||
|
reviewVersions: []string{"v1", "v1beta1"},
|
||||||
checks: checks(validateStorageVersion, validateServed, validateMixedStorageVersions("v1alpha1", "v1beta1")), // no v1beta2 as the schema differs
|
checks: checks(validateStorageVersion, validateServed, validateMixedStorageVersions("v1alpha1", "v1beta1")), // no v1beta2 as the schema differs
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
group: "nontrivial-converter",
|
group: "noop-converter-v1beta1",
|
||||||
|
handler: NewObjectConverterWebhookHandler(t, noopConverter),
|
||||||
|
reviewVersions: []string{"v1beta1", "v1"},
|
||||||
|
checks: checks(validateStorageVersion, validateServed, validateMixedStorageVersions("v1alpha1", "v1beta1")), // no v1beta2 as the schema differs
|
||||||
|
},
|
||||||
|
{
|
||||||
|
group: "nontrivial-converter-v1",
|
||||||
handler: NewObjectConverterWebhookHandler(t, nontrivialConverter),
|
handler: NewObjectConverterWebhookHandler(t, nontrivialConverter),
|
||||||
|
reviewVersions: []string{"v1", "v1beta1"},
|
||||||
checks: checks(validateStorageVersion, validateServed, validateMixedStorageVersions("v1alpha1", "v1beta1", "v1beta2"), validateNonTrivialConverted, validateNonTrivialConvertedList, validateStoragePruning, validateDefaulting),
|
checks: checks(validateStorageVersion, validateServed, validateMixedStorageVersions("v1alpha1", "v1beta1", "v1beta2"), validateNonTrivialConverted, validateNonTrivialConvertedList, validateStoragePruning, validateDefaulting),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
group: "metadata-mutating-converter",
|
group: "nontrivial-converter-v1beta1",
|
||||||
|
handler: NewObjectConverterWebhookHandler(t, nontrivialConverter),
|
||||||
|
reviewVersions: []string{"v1beta1", "v1"},
|
||||||
|
checks: checks(validateStorageVersion, validateServed, validateMixedStorageVersions("v1alpha1", "v1beta1", "v1beta2"), validateNonTrivialConverted, validateNonTrivialConvertedList, validateStoragePruning, validateDefaulting),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
group: "metadata-mutating-v1",
|
||||||
handler: NewObjectConverterWebhookHandler(t, metadataMutatingConverter),
|
handler: NewObjectConverterWebhookHandler(t, metadataMutatingConverter),
|
||||||
|
reviewVersions: []string{"v1", "v1beta1"},
|
||||||
checks: checks(validateObjectMetaMutation),
|
checks: checks(validateObjectMetaMutation),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
group: "metadata-uid-mutating-converter",
|
group: "metadata-mutating-v1beta1",
|
||||||
|
handler: NewObjectConverterWebhookHandler(t, metadataMutatingConverter),
|
||||||
|
reviewVersions: []string{"v1beta1", "v1"},
|
||||||
|
checks: checks(validateObjectMetaMutation),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
group: "metadata-uid-mutating-v1",
|
||||||
handler: NewObjectConverterWebhookHandler(t, uidMutatingConverter),
|
handler: NewObjectConverterWebhookHandler(t, uidMutatingConverter),
|
||||||
|
reviewVersions: []string{"v1", "v1beta1"},
|
||||||
checks: checks(validateUIDMutation),
|
checks: checks(validateUIDMutation),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
group: "empty-response",
|
group: "metadata-uid-mutating-v1beta1",
|
||||||
handler: NewReviewWebhookHandler(t, emptyResponseConverter),
|
handler: NewObjectConverterWebhookHandler(t, uidMutatingConverter),
|
||||||
|
reviewVersions: []string{"v1beta1", "v1"},
|
||||||
|
checks: checks(validateUIDMutation),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
group: "empty-response-v1",
|
||||||
|
handler: NewReviewWebhookHandler(t, nil, emptyV1ResponseConverter),
|
||||||
|
reviewVersions: []string{"v1", "v1beta1"},
|
||||||
checks: checks(expectConversionFailureMessage("empty-response", "returned 0 objects, expected 1")),
|
checks: checks(expectConversionFailureMessage("empty-response", "returned 0 objects, expected 1")),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
group: "failure-message",
|
group: "empty-response-v1beta1",
|
||||||
handler: NewReviewWebhookHandler(t, failureResponseConverter("custom webhook conversion error")),
|
handler: NewReviewWebhookHandler(t, emptyV1Beta1ResponseConverter, nil),
|
||||||
|
reviewVersions: []string{"v1beta1", "v1"},
|
||||||
|
checks: checks(expectConversionFailureMessage("empty-response", "returned 0 objects, expected 1")),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
group: "failure-message-v1",
|
||||||
|
handler: NewReviewWebhookHandler(t, nil, failureV1ResponseConverter("custom webhook conversion error")),
|
||||||
|
reviewVersions: []string{"v1", "v1beta1"},
|
||||||
checks: checks(expectConversionFailureMessage("failure-message", "custom webhook conversion error")),
|
checks: checks(expectConversionFailureMessage("failure-message", "custom webhook conversion error")),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
group: "failure-message-v1beta1",
|
||||||
|
handler: NewReviewWebhookHandler(t, failureV1Beta1ResponseConverter("custom webhook conversion error"), nil),
|
||||||
|
reviewVersions: []string{"v1beta1", "v1"},
|
||||||
|
checks: checks(expectConversionFailureMessage("failure-message", "custom webhook conversion error")),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
group: "unhandled-v1",
|
||||||
|
handler: NewReviewWebhookHandler(t, nil, nil),
|
||||||
|
reviewVersions: []string{"v1", "v1beta1"},
|
||||||
|
checks: checks(expectConversionFailureMessage("server-error", "the server rejected our request")),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
group: "unhandled-v1beta1",
|
||||||
|
handler: NewReviewWebhookHandler(t, nil, nil),
|
||||||
|
reviewVersions: []string{"v1beta1", "v1"},
|
||||||
|
checks: checks(expectConversionFailureMessage("server-error", "the server rejected our request")),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Added for integration testing of conversion webhooks, where decode errors due to conversion webhook failures need to be tested.
|
// TODO: Added for integration testing of conversion webhooks, where decode errors due to conversion webhook failures need to be tested.
|
||||||
@ -174,7 +229,7 @@ func testWebhookConverter(t *testing.T, defaulting, watchCache bool) {
|
|||||||
}
|
}
|
||||||
defer tearDown()
|
defer tearDown()
|
||||||
|
|
||||||
ctc.setConversionWebhook(t, webhookClientConfig)
|
ctc.setConversionWebhook(t, webhookClientConfig, test.reviewVersions)
|
||||||
defer ctc.removeConversionWebhook(t)
|
defer ctc.removeConversionWebhook(t)
|
||||||
|
|
||||||
// wait until new webhook is called the first time
|
// wait until new webhook is called the first time
|
||||||
@ -711,7 +766,15 @@ func noopConverter(desiredAPIVersion string, obj runtime.RawExtension) (runtime.
|
|||||||
return runtime.RawExtension{Raw: raw}, nil
|
return runtime.RawExtension{Raw: raw}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func emptyResponseConverter(review apiextensionsv1beta1.ConversionReview) (apiextensionsv1beta1.ConversionReview, error) {
|
func emptyV1ResponseConverter(review *apiextensionsv1.ConversionReview) (*apiextensionsv1.ConversionReview, error) {
|
||||||
|
review.Response = &apiextensionsv1.ConversionResponse{
|
||||||
|
UID: review.Request.UID,
|
||||||
|
ConvertedObjects: []runtime.RawExtension{},
|
||||||
|
Result: metav1.Status{Status: "Success"},
|
||||||
|
}
|
||||||
|
return review, nil
|
||||||
|
}
|
||||||
|
func emptyV1Beta1ResponseConverter(review *apiextensionsv1beta1.ConversionReview) (*apiextensionsv1beta1.ConversionReview, error) {
|
||||||
review.Response = &apiextensionsv1beta1.ConversionResponse{
|
review.Response = &apiextensionsv1beta1.ConversionResponse{
|
||||||
UID: review.Request.UID,
|
UID: review.Request.UID,
|
||||||
ConvertedObjects: []runtime.RawExtension{},
|
ConvertedObjects: []runtime.RawExtension{},
|
||||||
@ -720,8 +783,19 @@ func emptyResponseConverter(review apiextensionsv1beta1.ConversionReview) (apiex
|
|||||||
return review, nil
|
return review, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func failureResponseConverter(message string) func(review apiextensionsv1beta1.ConversionReview) (apiextensionsv1beta1.ConversionReview, error) {
|
func failureV1ResponseConverter(message string) func(review *apiextensionsv1.ConversionReview) (*apiextensionsv1.ConversionReview, error) {
|
||||||
return func(review apiextensionsv1beta1.ConversionReview) (apiextensionsv1beta1.ConversionReview, error) {
|
return func(review *apiextensionsv1.ConversionReview) (*apiextensionsv1.ConversionReview, error) {
|
||||||
|
review.Response = &apiextensionsv1.ConversionResponse{
|
||||||
|
UID: review.Request.UID,
|
||||||
|
ConvertedObjects: []runtime.RawExtension{},
|
||||||
|
Result: metav1.Status{Message: message, Status: "Failure"},
|
||||||
|
}
|
||||||
|
return review, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func failureV1Beta1ResponseConverter(message string) func(review *apiextensionsv1beta1.ConversionReview) (*apiextensionsv1beta1.ConversionReview, error) {
|
||||||
|
return func(review *apiextensionsv1beta1.ConversionReview) (*apiextensionsv1beta1.ConversionReview, error) {
|
||||||
review.Response = &apiextensionsv1beta1.ConversionResponse{
|
review.Response = &apiextensionsv1beta1.ConversionResponse{
|
||||||
UID: review.Request.UID,
|
UID: review.Request.UID,
|
||||||
ConvertedObjects: []runtime.RawExtension{},
|
ConvertedObjects: []runtime.RawExtension{},
|
||||||
@ -889,7 +963,7 @@ func (c *conversionTestContext) versionedClients(ns string) map[string]dynamic.R
|
|||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *conversionTestContext) setConversionWebhook(t *testing.T, webhookClientConfig *apiextensionsv1beta1.WebhookClientConfig) {
|
func (c *conversionTestContext) setConversionWebhook(t *testing.T, webhookClientConfig *apiextensionsv1beta1.WebhookClientConfig, reviewVersions []string) {
|
||||||
crd, err := c.apiExtensionsClient.ApiextensionsV1beta1().CustomResourceDefinitions().Get(c.crd.Name, metav1.GetOptions{})
|
crd, err := c.apiExtensionsClient.ApiextensionsV1beta1().CustomResourceDefinitions().Get(c.crd.Name, metav1.GetOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@ -897,6 +971,7 @@ func (c *conversionTestContext) setConversionWebhook(t *testing.T, webhookClient
|
|||||||
crd.Spec.Conversion = &apiextensionsv1beta1.CustomResourceConversion{
|
crd.Spec.Conversion = &apiextensionsv1beta1.CustomResourceConversion{
|
||||||
Strategy: apiextensionsv1beta1.WebhookConverter,
|
Strategy: apiextensionsv1beta1.WebhookConverter,
|
||||||
WebhookClientConfig: webhookClientConfig,
|
WebhookClientConfig: webhookClientConfig,
|
||||||
|
ConversionReviewVersions: reviewVersions,
|
||||||
}
|
}
|
||||||
crd, err = c.apiExtensionsClient.ApiextensionsV1beta1().CustomResourceDefinitions().Update(crd)
|
crd, err = c.apiExtensionsClient.ApiextensionsV1beta1().CustomResourceDefinitions().Update(crd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -30,6 +30,7 @@ import (
|
|||||||
|
|
||||||
"k8s.io/apimachinery/pkg/util/wait"
|
"k8s.io/apimachinery/pkg/util/wait"
|
||||||
|
|
||||||
|
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||||
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
|
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
@ -73,11 +74,14 @@ func StartConversionWebhookServer(handler http.Handler) (func(), *apiextensionsv
|
|||||||
return webhookServer.Close, webhookConfig, nil
|
return webhookServer.Close, webhookConfig, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReviewConverterFunc converts an entire ConversionReview.
|
// V1Beta1ReviewConverterFunc converts an entire ConversionReview.
|
||||||
type ReviewConverterFunc func(review apiextensionsv1beta1.ConversionReview) (apiextensionsv1beta1.ConversionReview, error)
|
type V1Beta1ReviewConverterFunc func(review *apiextensionsv1beta1.ConversionReview) (*apiextensionsv1beta1.ConversionReview, error)
|
||||||
|
|
||||||
|
// V1ReviewConverterFunc converts an entire ConversionReview.
|
||||||
|
type V1ReviewConverterFunc func(review *apiextensionsv1.ConversionReview) (*apiextensionsv1.ConversionReview, error)
|
||||||
|
|
||||||
// NewReviewWebhookHandler creates a handler that delegates the review conversion to the provided ReviewConverterFunc.
|
// NewReviewWebhookHandler creates a handler that delegates the review conversion to the provided ReviewConverterFunc.
|
||||||
func NewReviewWebhookHandler(t *testing.T, converterFunc ReviewConverterFunc) http.Handler {
|
func NewReviewWebhookHandler(t *testing.T, v1beta1ConverterFunc V1Beta1ReviewConverterFunc, v1ConverterFunc V1ReviewConverterFunc) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
defer r.Body.Close()
|
defer r.Body.Close()
|
||||||
data, err := ioutil.ReadAll(r.Body)
|
data, err := ioutil.ReadAll(r.Body)
|
||||||
@ -90,21 +94,63 @@ func NewReviewWebhookHandler(t *testing.T, converterFunc ReviewConverterFunc) ht
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
review := apiextensionsv1beta1.ConversionReview{}
|
typeMeta := &metav1.TypeMeta{}
|
||||||
if err := json.Unmarshal(data, &review); err != nil {
|
if err := json.Unmarshal(data, typeMeta); err != nil {
|
||||||
t.Errorf("Fail to deserialize object: %s with error: %v", string(data), err)
|
t.Errorf("Fail to deserialize object: %s with error: %v", string(data), err)
|
||||||
http.Error(w, err.Error(), 400)
|
http.Error(w, err.Error(), 400)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
review, err = converterFunc(review)
|
var response runtime.Object
|
||||||
|
|
||||||
|
switch typeMeta.GroupVersionKind() {
|
||||||
|
case apiextensionsv1.SchemeGroupVersion.WithKind("ConversionReview"):
|
||||||
|
review := &apiextensionsv1.ConversionReview{}
|
||||||
|
if err := json.Unmarshal(data, review); err != nil {
|
||||||
|
t.Errorf("Fail to deserialize object: %s with error: %v", string(data), err)
|
||||||
|
http.Error(w, err.Error(), 400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if v1ConverterFunc == nil {
|
||||||
|
http.Error(w, "Cannot handle v1 ConversionReview", 422)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response, err = v1ConverterFunc(review)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Error converting review: %v", err)
|
t.Errorf("Error converting review: %v", err)
|
||||||
http.Error(w, err.Error(), 500)
|
http.Error(w, err.Error(), 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
case apiextensionsv1beta1.SchemeGroupVersion.WithKind("ConversionReview"):
|
||||||
|
review := &apiextensionsv1beta1.ConversionReview{}
|
||||||
|
if err := json.Unmarshal(data, review); err != nil {
|
||||||
|
t.Errorf("Fail to deserialize object: %s with error: %v", string(data), err)
|
||||||
|
http.Error(w, err.Error(), 400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if v1beta1ConverterFunc == nil {
|
||||||
|
http.Error(w, "Cannot handle v1beta1 ConversionReview", 422)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response, err = v1beta1ConverterFunc(review)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error converting review: %v", err)
|
||||||
|
http.Error(w, err.Error(), 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
err := fmt.Errorf("unrecognized request kind: %v", typeMeta.GroupVersionKind())
|
||||||
|
t.Error(err)
|
||||||
|
http.Error(w, err.Error(), 400)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
if err := json.NewEncoder(w).Encode(review); err != nil {
|
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||||
t.Errorf("Marshal of response failed with error: %v", err)
|
t.Errorf("Marshal of response failed with error: %v", err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -115,7 +161,7 @@ type ObjectConverterFunc func(desiredAPIVersion string, customResource runtime.R
|
|||||||
|
|
||||||
// NewObjectConverterWebhookHandler creates a handler that delegates custom resource conversion to the provided ConverterFunc.
|
// NewObjectConverterWebhookHandler creates a handler that delegates custom resource conversion to the provided ConverterFunc.
|
||||||
func NewObjectConverterWebhookHandler(t *testing.T, converterFunc ObjectConverterFunc) http.Handler {
|
func NewObjectConverterWebhookHandler(t *testing.T, converterFunc ObjectConverterFunc) http.Handler {
|
||||||
return NewReviewWebhookHandler(t, func(review apiextensionsv1beta1.ConversionReview) (apiextensionsv1beta1.ConversionReview, error) {
|
return NewReviewWebhookHandler(t, func(review *apiextensionsv1beta1.ConversionReview) (*apiextensionsv1beta1.ConversionReview, error) {
|
||||||
converted := []runtime.RawExtension{}
|
converted := []runtime.RawExtension{}
|
||||||
errMsgs := []string{}
|
errMsgs := []string{}
|
||||||
for _, obj := range review.Request.Objects {
|
for _, obj := range review.Request.Objects {
|
||||||
@ -137,6 +183,28 @@ func NewObjectConverterWebhookHandler(t *testing.T, converterFunc ObjectConverte
|
|||||||
review.Response.Result = metav1.Status{Status: "Failure", Message: strings.Join(errMsgs, ", ")}
|
review.Response.Result = metav1.Status{Status: "Failure", Message: strings.Join(errMsgs, ", ")}
|
||||||
}
|
}
|
||||||
return review, nil
|
return review, nil
|
||||||
|
}, func(review *apiextensionsv1.ConversionReview) (*apiextensionsv1.ConversionReview, error) {
|
||||||
|
converted := []runtime.RawExtension{}
|
||||||
|
errMsgs := []string{}
|
||||||
|
for _, obj := range review.Request.Objects {
|
||||||
|
convertedObj, err := converterFunc(review.Request.DesiredAPIVersion, obj)
|
||||||
|
if err != nil {
|
||||||
|
errMsgs = append(errMsgs, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
converted = append(converted, convertedObj)
|
||||||
|
}
|
||||||
|
|
||||||
|
review.Response = &apiextensionsv1.ConversionResponse{
|
||||||
|
UID: review.Request.UID,
|
||||||
|
ConvertedObjects: converted,
|
||||||
|
}
|
||||||
|
if len(errMsgs) == 0 {
|
||||||
|
review.Response.Result = metav1.Status{Status: "Success"}
|
||||||
|
} else {
|
||||||
|
review.Response.Result = metav1.Status{Status: "Failure", Message: strings.Join(errMsgs, ", ")}
|
||||||
|
}
|
||||||
|
return review, nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user