mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-23 11:50:44 +00:00
CRConverter: change to UnstructuredList
Change the CRConverter interface to use *unstructured.UnstructuredList for input/output to simplify what converter implementers must implement. Signed-off-by: Andy Goldstein <andy.goldstein@redhat.com>
This commit is contained in:
parent
0976267f0f
commit
beb7d6cff1
@ -21,9 +21,12 @@ import (
|
|||||||
|
|
||||||
autoscalingv1 "k8s.io/api/autoscaling/v1"
|
autoscalingv1 "k8s.io/api/autoscaling/v1"
|
||||||
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||||
|
apivalidation "k8s.io/apimachinery/pkg/api/validation"
|
||||||
"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"
|
||||||
"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/util/validation/field"
|
||||||
"k8s.io/apiserver/pkg/util/webhook"
|
"k8s.io/apiserver/pkg/util/webhook"
|
||||||
typedscheme "k8s.io/client-go/kubernetes/scheme"
|
typedscheme "k8s.io/client-go/kubernetes/scheme"
|
||||||
)
|
)
|
||||||
@ -110,7 +113,7 @@ type CRConverter interface {
|
|||||||
// Convert converts in object to the given gvk and returns the converted object.
|
// Convert converts in object to the given gvk and returns the converted object.
|
||||||
// Note that the function may mutate in object and return it. A safe wrapper will make sure
|
// Note that the function may mutate in object and return it. A safe wrapper will make sure
|
||||||
// a safe converter will be returned.
|
// a safe converter will be returned.
|
||||||
Convert(in runtime.Object, targetGVK schema.GroupVersion) (runtime.Object, error)
|
Convert(in *unstructured.UnstructuredList, targetGVK schema.GroupVersion) (*unstructured.UnstructuredList, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// delegatingCRConverter extends the delegate converter with generic CR conversion behaviour. The delegate will implement the
|
// delegatingCRConverter extends the delegate converter with generic CR conversion behaviour. The delegate will implement the
|
||||||
@ -173,13 +176,6 @@ func (c *delegatingCRConverter) Convert(in, out, context interface{}) error {
|
|||||||
// UnstructuredList with the request's GV, populates it from storage, then calls conversion to convert
|
// UnstructuredList with the request's GV, populates it from storage, then calls conversion to convert
|
||||||
// the individual items. This function assumes it never gets a v1.List.
|
// the individual items. This function assumes it never gets a v1.List.
|
||||||
func (c *delegatingCRConverter) ConvertToVersion(in runtime.Object, target runtime.GroupVersioner) (runtime.Object, error) {
|
func (c *delegatingCRConverter) ConvertToVersion(in runtime.Object, target runtime.GroupVersioner) (runtime.Object, error) {
|
||||||
fromGVK := in.GetObjectKind().GroupVersionKind()
|
|
||||||
toGVK, ok := target.KindForGroupVersionKinds([]schema.GroupVersionKind{fromGVK})
|
|
||||||
if !ok {
|
|
||||||
// TODO: should this be a typed error?
|
|
||||||
return nil, fmt.Errorf("%v is unstructured and is not suitable for converting to %q", fromGVK.String(), target)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Special-case typed scale conversion if this custom resource supports a scale endpoint
|
// Special-case typed scale conversion if this custom resource supports a scale endpoint
|
||||||
if c.convertScale {
|
if c.convertScale {
|
||||||
if _, isInScale := in.(*autoscalingv1.Scale); isInScale {
|
if _, isInScale := in.(*autoscalingv1.Scale); isInScale {
|
||||||
@ -187,8 +183,28 @@ func (c *delegatingCRConverter) ConvertToVersion(in runtime.Object, target runti
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fromGVK := in.GetObjectKind().GroupVersionKind()
|
||||||
|
toGVK, ok := target.KindForGroupVersionKinds([]schema.GroupVersionKind{fromGVK})
|
||||||
|
if !ok {
|
||||||
|
// TODO: should this be a typed error?
|
||||||
|
return nil, fmt.Errorf("%v is unstructured and is not suitable for converting to %q", fromGVK.String(), target)
|
||||||
|
}
|
||||||
|
|
||||||
|
isList := false
|
||||||
|
var list *unstructured.UnstructuredList
|
||||||
|
switch t := in.(type) {
|
||||||
|
case *unstructured.Unstructured:
|
||||||
|
list = &unstructured.UnstructuredList{Items: []unstructured.Unstructured{*t}}
|
||||||
|
case *unstructured.UnstructuredList:
|
||||||
|
list = t
|
||||||
|
isList = true
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unexpected type %T", in)
|
||||||
|
}
|
||||||
|
|
||||||
|
desiredAPIVersion := toGVK.GroupVersion().String()
|
||||||
if !c.validVersions[toGVK.GroupVersion()] {
|
if !c.validVersions[toGVK.GroupVersion()] {
|
||||||
return nil, fmt.Errorf("request to convert CR to an invalid group/version: %s", toGVK.GroupVersion().String())
|
return nil, fmt.Errorf("request to convert CR to an invalid group/version: %s", desiredAPIVersion)
|
||||||
}
|
}
|
||||||
// Note that even if the request is for a list, the GV of the request UnstructuredList is what
|
// Note that even if the request is for a list, the GV of the request UnstructuredList is what
|
||||||
// is expected to convert to. As mentioned in the function's document, it is not expected to
|
// is expected to convert to. As mentioned in the function's document, it is not expected to
|
||||||
@ -196,23 +212,101 @@ func (c *delegatingCRConverter) ConvertToVersion(in runtime.Object, target runti
|
|||||||
if !c.validVersions[fromGVK.GroupVersion()] {
|
if !c.validVersions[fromGVK.GroupVersion()] {
|
||||||
return nil, fmt.Errorf("request to convert CR from an invalid group/version: %s", fromGVK.GroupVersion().String())
|
return nil, fmt.Errorf("request to convert CR from an invalid group/version: %s", fromGVK.GroupVersion().String())
|
||||||
}
|
}
|
||||||
// Check list item's apiVersion
|
|
||||||
if list, ok := in.(*unstructured.UnstructuredList); ok {
|
var objectsToConvert []*unstructured.Unstructured
|
||||||
for i := range list.Items {
|
objectsToConvert, err := getObjectsToConvert(list, desiredAPIVersion, c.validVersions)
|
||||||
expectedGV := list.Items[i].GroupVersionKind().GroupVersion()
|
if err != nil {
|
||||||
if !c.validVersions[expectedGV] {
|
return nil, err
|
||||||
return nil, fmt.Errorf("request to convert CR list failed, list index %d has invalid group/version: %s", i, expectedGV.String())
|
}
|
||||||
}
|
|
||||||
|
objCount := len(objectsToConvert)
|
||||||
|
if objCount == 0 {
|
||||||
|
// no objects needed conversion
|
||||||
|
if !isList {
|
||||||
|
// for a single item, return as-is
|
||||||
|
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 := list.DeepCopy()
|
||||||
|
out.SetAPIVersion(desiredAPIVersion)
|
||||||
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// A smoke test in API machinery calls the converter on empty objects during startup. The test is initiated here:
|
// A smoke test in API machinery calls the converter on empty objects during startup. The test is initiated here:
|
||||||
// https://github.com/kubernetes/kubernetes/blob/dbb448bbdcb9e440eee57024ffa5f1698956a054/staging/src/k8s.io/apiserver/pkg/storage/cacher/cacher.go#L201
|
// https://github.com/kubernetes/kubernetes/blob/dbb448bbdcb9e440eee57024ffa5f1698956a054/staging/src/k8s.io/apiserver/pkg/storage/cacher/cacher.go#L201
|
||||||
if isEmptyUnstructuredObject(in) {
|
if isEmptyUnstructuredObject(in) {
|
||||||
return NewNOPConverter().Convert(in, toGVK.GroupVersion())
|
converted, err := NewNOPConverter().Convert(list, toGVK.GroupVersion())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !isList {
|
||||||
|
return &converted.Items[0], nil
|
||||||
|
}
|
||||||
|
return converted, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.converter.Convert(in, toGVK.GroupVersion())
|
convertedObjects, err := c.converter.Convert(list, toGVK.GroupVersion())
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("conversion for %v failed: %w", in.GetObjectKind().GroupVersionKind(), err)
|
||||||
|
}
|
||||||
|
if len(convertedObjects.Items) != len(objectsToConvert) {
|
||||||
|
return nil, fmt.Errorf("conversion for %v returned %d objects, expected %d", in.GetObjectKind().GroupVersionKind(), len(convertedObjects.Items), len(objectsToConvert))
|
||||||
|
}
|
||||||
|
|
||||||
|
// start a deepcopy of the input and fill in the converted objects from the response at the right spots.
|
||||||
|
// The response list might be sparse because objects had the right version already.
|
||||||
|
convertedList := list.DeepCopy()
|
||||||
|
convertedList.SetAPIVersion(desiredAPIVersion)
|
||||||
|
convertedIndex := 0
|
||||||
|
for i := range convertedList.Items {
|
||||||
|
original := &convertedList.Items[i]
|
||||||
|
if original.GetAPIVersion() == desiredAPIVersion {
|
||||||
|
// This item has not been sent for conversion, and therefore does not show up in the response.
|
||||||
|
// convertedList has the right item already.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
converted := &convertedObjects.Items[convertedIndex]
|
||||||
|
convertedIndex++
|
||||||
|
if expected, got := toGVK.GroupVersion(), converted.GetObjectKind().GroupVersionKind().GroupVersion(); expected != got {
|
||||||
|
return nil, fmt.Errorf("conversion 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 {
|
||||||
|
return nil, fmt.Errorf("conversion for %v returned invalid converted object at index %v: invalid kind (expected %v, received %v)", in.GetObjectKind().GroupVersionKind(), convertedIndex, expected, got)
|
||||||
|
}
|
||||||
|
if err := validateConvertedObject(original, converted); err != nil {
|
||||||
|
return nil, fmt.Errorf("conversion for %v returned invalid converted object at index %v: %v", in.GetObjectKind().GroupVersionKind(), convertedIndex, err)
|
||||||
|
}
|
||||||
|
if err := restoreObjectMeta(original, converted); err != nil {
|
||||||
|
return nil, fmt.Errorf("conversion for %v returned invalid metadata in object at index %v: %v", in.GetObjectKind().GroupVersionKind(), convertedIndex, err)
|
||||||
|
}
|
||||||
|
convertedList.Items[i] = *converted
|
||||||
|
}
|
||||||
|
|
||||||
|
if isList {
|
||||||
|
return convertedList, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &convertedList.Items[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getObjectsToConvert(
|
||||||
|
list *unstructured.UnstructuredList,
|
||||||
|
desiredAPIVersion string,
|
||||||
|
validVersions map[schema.GroupVersion]bool,
|
||||||
|
) ([]*unstructured.Unstructured, error) {
|
||||||
|
var objectsToConvert []*unstructured.Unstructured
|
||||||
|
for i := range list.Items {
|
||||||
|
expectedGV := list.Items[i].GroupVersionKind().GroupVersion()
|
||||||
|
if !validVersions[expectedGV] {
|
||||||
|
return nil, fmt.Errorf("request to convert CR list failed, list index %d has invalid group/version: %s", i, expectedGV.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only sent item for conversion, if the apiVersion is different
|
||||||
|
if list.Items[i].GetAPIVersion() != desiredAPIVersion {
|
||||||
|
objectsToConvert = append(objectsToConvert, &list.Items[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return objectsToConvert, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// safeConverterWrapper is a wrapper over an unsafe object converter that makes copy of the input and then delegate to the unsafe converter.
|
// safeConverterWrapper is a wrapper over an unsafe object converter that makes copy of the input and then delegate to the unsafe converter.
|
||||||
@ -259,3 +353,94 @@ func isEmptyUnstructuredObject(in runtime.Object) bool {
|
|||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// validateConvertedObject checks that ObjectMeta fields match, with the exception of
|
||||||
|
// labels and annotations.
|
||||||
|
func validateConvertedObject(in, out *unstructured.Unstructured) error {
|
||||||
|
if e, a := in.GetKind(), out.GetKind(); e != a {
|
||||||
|
return fmt.Errorf("must have the same kind: %v != %v", e, a)
|
||||||
|
}
|
||||||
|
if e, a := in.GetName(), out.GetName(); e != a {
|
||||||
|
return fmt.Errorf("must have the same name: %v != %v", e, a)
|
||||||
|
}
|
||||||
|
if e, a := in.GetNamespace(), out.GetNamespace(); e != a {
|
||||||
|
return fmt.Errorf("must have the same namespace: %v != %v", e, a)
|
||||||
|
}
|
||||||
|
if e, a := in.GetUID(), out.GetUID(); e != a {
|
||||||
|
return fmt.Errorf("must have the same UID: %v != %v", e, a)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// restoreObjectMeta deep-copies metadata from original into converted, while preserving labels and annotations from converted.
|
||||||
|
func restoreObjectMeta(original, converted *unstructured.Unstructured) error {
|
||||||
|
obj, found := converted.Object["metadata"]
|
||||||
|
if !found {
|
||||||
|
return fmt.Errorf("missing metadata in converted object")
|
||||||
|
}
|
||||||
|
responseMetaData, ok := obj.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("invalid metadata of type %T in converted object", obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := original.Object["metadata"]; !ok {
|
||||||
|
// the original will always have metadata. But just to be safe, let's clear in converted
|
||||||
|
// with an empty object instead of nil, to be able to add labels and annotations below.
|
||||||
|
converted.Object["metadata"] = map[string]interface{}{}
|
||||||
|
} else {
|
||||||
|
converted.Object["metadata"] = runtime.DeepCopyJSONValue(original.Object["metadata"])
|
||||||
|
}
|
||||||
|
|
||||||
|
obj = converted.Object["metadata"]
|
||||||
|
convertedMetaData, ok := obj.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("invalid metadata of type %T in input object", obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, fld := range []string{"labels", "annotations"} {
|
||||||
|
obj, found := responseMetaData[fld]
|
||||||
|
if !found || obj == nil {
|
||||||
|
delete(convertedMetaData, fld)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
responseField, ok := obj.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("invalid metadata.%s of type %T in converted object", fld, obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
originalField, ok := convertedMetaData[fld].(map[string]interface{})
|
||||||
|
if !ok && convertedMetaData[fld] != nil {
|
||||||
|
return fmt.Errorf("invalid metadata.%s of type %T in original object", fld, convertedMetaData[fld])
|
||||||
|
}
|
||||||
|
|
||||||
|
somethingChanged := len(originalField) != len(responseField)
|
||||||
|
for k, v := range responseField {
|
||||||
|
if _, ok := v.(string); !ok {
|
||||||
|
return fmt.Errorf("metadata.%s[%s] must be a string, but is %T in converted object", fld, k, v)
|
||||||
|
}
|
||||||
|
if originalField[k] != interface{}(v) {
|
||||||
|
somethingChanged = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if somethingChanged {
|
||||||
|
stringMap := make(map[string]string, len(responseField))
|
||||||
|
for k, v := range responseField {
|
||||||
|
stringMap[k] = v.(string)
|
||||||
|
}
|
||||||
|
var errs field.ErrorList
|
||||||
|
if fld == "labels" {
|
||||||
|
errs = metav1validation.ValidateLabels(stringMap, field.NewPath("metadata", "labels"))
|
||||||
|
} else {
|
||||||
|
errs = apivalidation.ValidateAnnotations(stringMap, field.NewPath("metadata", "annotation"))
|
||||||
|
}
|
||||||
|
if len(errs) > 0 {
|
||||||
|
return errs.ToAggregate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
convertedMetaData[fld] = responseField
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
@ -21,6 +21,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/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/runtime"
|
||||||
@ -188,3 +189,102 @@ func TestConversion(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
|
||||||
|
List *unstructured.UnstructuredList
|
||||||
|
APIVersion string
|
||||||
|
ValidVersions map[schema.GroupVersion]bool
|
||||||
|
|
||||||
|
ExpectObjects []*unstructured.Unstructured
|
||||||
|
ExpectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
Name: "empty list",
|
||||||
|
List: &unstructured.UnstructuredList{},
|
||||||
|
APIVersion: "foo/v1",
|
||||||
|
ValidVersions: map[schema.GroupVersion]bool{
|
||||||
|
{Group: "foo", Version: "v1"}: true,
|
||||||
|
},
|
||||||
|
ExpectObjects: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "one-item list, in desired version",
|
||||||
|
List: &unstructured.UnstructuredList{
|
||||||
|
Items: []unstructured.Unstructured{*v1Object},
|
||||||
|
},
|
||||||
|
ValidVersions: map[schema.GroupVersion]bool{
|
||||||
|
{Group: "foo", Version: "v1"}: true,
|
||||||
|
},
|
||||||
|
APIVersion: "foo/v1",
|
||||||
|
ExpectObjects: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "one-item list, not in desired version",
|
||||||
|
List: &unstructured.UnstructuredList{
|
||||||
|
Items: []unstructured.Unstructured{*v2Object},
|
||||||
|
},
|
||||||
|
ValidVersions: map[schema.GroupVersion]bool{
|
||||||
|
{Group: "foo", Version: "v1"}: true,
|
||||||
|
{Group: "foo", Version: "v2"}: true,
|
||||||
|
},
|
||||||
|
APIVersion: "foo/v1",
|
||||||
|
ExpectObjects: []*unstructured.Unstructured{v2Object},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "multi-item list, in desired version",
|
||||||
|
List: &unstructured.UnstructuredList{
|
||||||
|
Items: []unstructured.Unstructured{*v1Object, *v1Object, *v1Object},
|
||||||
|
},
|
||||||
|
ValidVersions: map[schema.GroupVersion]bool{
|
||||||
|
{Group: "foo", Version: "v1"}: true,
|
||||||
|
{Group: "foo", Version: "v2"}: true,
|
||||||
|
},
|
||||||
|
APIVersion: "foo/v1",
|
||||||
|
ExpectObjects: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "multi-item list, mixed versions",
|
||||||
|
List: &unstructured.UnstructuredList{
|
||||||
|
Items: []unstructured.Unstructured{*v1Object, *v2Object, *v3Object},
|
||||||
|
},
|
||||||
|
ValidVersions: map[schema.GroupVersion]bool{
|
||||||
|
{Group: "foo", Version: "v1"}: true,
|
||||||
|
{Group: "foo", Version: "v2"}: true,
|
||||||
|
{Group: "foo", Version: "v3"}: true,
|
||||||
|
},
|
||||||
|
APIVersion: "foo/v1",
|
||||||
|
ExpectObjects: []*unstructured.Unstructured{v2Object, v3Object},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "multi-item list, invalid versions",
|
||||||
|
List: &unstructured.UnstructuredList{
|
||||||
|
Items: []unstructured.Unstructured{*v1Object, *v2Object, *v3Object},
|
||||||
|
},
|
||||||
|
ValidVersions: map[schema.GroupVersion]bool{
|
||||||
|
{Group: "foo", Version: "v2"}: true,
|
||||||
|
{Group: "foo", Version: "v3"}: true,
|
||||||
|
},
|
||||||
|
APIVersion: "foo/v1",
|
||||||
|
ExpectObjects: nil,
|
||||||
|
ExpectError: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range testcases {
|
||||||
|
t.Run(tc.Name, func(t *testing.T) {
|
||||||
|
objects, err := getObjectsToConvert(tc.List, tc.APIVersion, tc.ValidVersions)
|
||||||
|
gotError := err != nil
|
||||||
|
if e, a := tc.ExpectError, gotError; e != a {
|
||||||
|
t.Fatalf("error: expected %t, got %t", e, a)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(objects, tc.ExpectObjects) {
|
||||||
|
t.Errorf("unexpected diff: %s", cmp.Diff(tc.ExpectObjects, objects))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -21,7 +21,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
"k8s.io/component-base/metrics"
|
"k8s.io/component-base/metrics"
|
||||||
"k8s.io/component-base/metrics/legacyregistry"
|
"k8s.io/component-base/metrics/legacyregistry"
|
||||||
@ -73,7 +73,7 @@ func (c *converterMetricFactory) addMetrics(crdName string, converter CRConverte
|
|||||||
return &converterMetric{latencies: metric, delegate: converter, crdName: crdName}, nil
|
return &converterMetric{latencies: metric, delegate: converter, crdName: crdName}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *converterMetric) Convert(in runtime.Object, targetGV schema.GroupVersion) (runtime.Object, error) {
|
func (m *converterMetric) Convert(in *unstructured.UnstructuredList, targetGV schema.GroupVersion) (*unstructured.UnstructuredList, error) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
obj, err := m.delegate.Convert(in, targetGV)
|
obj, err := m.delegate.Convert(in, targetGV)
|
||||||
fromVersion := in.GetObjectKind().GroupVersionKind().Version
|
fromVersion := in.GetObjectKind().GroupVersionKind().Version
|
||||||
|
@ -18,7 +18,6 @@ package conversion
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"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/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -35,13 +34,9 @@ func NewNOPConverter() *nopConverter {
|
|||||||
var _ CRConverter = &nopConverter{}
|
var _ CRConverter = &nopConverter{}
|
||||||
|
|
||||||
// ConvertToVersion converts in object to the given gv in place and returns the same `in` object.
|
// ConvertToVersion converts in object to the given gv in place and returns the same `in` object.
|
||||||
func (c *nopConverter) Convert(in runtime.Object, targetGV schema.GroupVersion) (runtime.Object, error) {
|
func (c *nopConverter) Convert(list *unstructured.UnstructuredList, targetGV schema.GroupVersion) (*unstructured.UnstructuredList, error) {
|
||||||
// Run the converter on the list items instead of list itself
|
for i := range list.Items {
|
||||||
if list, ok := in.(*unstructured.UnstructuredList); ok {
|
list.Items[i].SetGroupVersionKind(targetGV.WithKind(list.Items[i].GroupVersionKind().Kind))
|
||||||
for i := range list.Items {
|
|
||||||
list.Items[i].SetGroupVersionKind(targetGV.WithKind(list.Items[i].GroupVersionKind().Kind))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
in.GetObjectKind().SetGroupVersionKind(targetGV.WithKind(in.GetObjectKind().GroupVersionKind().Kind))
|
return list, nil
|
||||||
return in, nil
|
|
||||||
}
|
}
|
||||||
|
@ -26,15 +26,12 @@ import (
|
|||||||
|
|
||||||
v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
|
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
|
||||||
apivalidation "k8s.io/apimachinery/pkg/api/validation"
|
|
||||||
metav1 "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"
|
|
||||||
"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/types"
|
||||||
"k8s.io/apimachinery/pkg/util/uuid"
|
"k8s.io/apimachinery/pkg/util/uuid"
|
||||||
"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"
|
||||||
"k8s.io/component-base/tracing"
|
"k8s.io/component-base/tracing"
|
||||||
@ -113,36 +110,21 @@ func (f *webhookConverterFactory) NewWebhookConverter(crd *v1.CustomResourceDefi
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getObjectsToConvert returns a list of objects requiring conversion.
|
// createConversionReviewObjects returns ConversionReview request and response objects for the first supported version found in conversionReviewVersions.
|
||||||
// if obj is a list, getObjectsToConvert returns a (potentially empty) list of the items that are not already in the desired version.
|
func createConversionReviewObjects(conversionReviewVersions []string, objects *unstructured.UnstructuredList, apiVersion string, requestUID types.UID) (request, response runtime.Object, err error) {
|
||||||
// if obj is not a list, and is already in the desired version, getObjectsToConvert returns an empty list.
|
rawObjects := make([]runtime.RawExtension, len(objects.Items))
|
||||||
// if obj is not a list, and is not already in the desired version, getObjectsToConvert returns a list containing only obj.
|
for i := range objects.Items {
|
||||||
func getObjectsToConvert(obj runtime.Object, apiVersion string) []runtime.RawExtension {
|
rawObjects[i] = runtime.RawExtension{
|
||||||
listObj, isList := obj.(*unstructured.UnstructuredList)
|
Object: &objects.Items[i],
|
||||||
var objects []runtime.RawExtension
|
|
||||||
if isList {
|
|
||||||
for i := range listObj.Items {
|
|
||||||
// Only sent item for conversion, if the apiVersion is different
|
|
||||||
if listObj.Items[i].GetAPIVersion() != apiVersion {
|
|
||||||
objects = append(objects, runtime.RawExtension{Object: &listObj.Items[i]})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if obj.GetObjectKind().GroupVersionKind().GroupVersion().String() != apiVersion {
|
|
||||||
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 {
|
for _, version := range conversionReviewVersions {
|
||||||
switch version {
|
switch version {
|
||||||
case v1beta1.SchemeGroupVersion.Version:
|
case v1beta1.SchemeGroupVersion.Version:
|
||||||
return &v1beta1.ConversionReview{
|
return &v1beta1.ConversionReview{
|
||||||
Request: &v1beta1.ConversionRequest{
|
Request: &v1beta1.ConversionRequest{
|
||||||
Objects: objects,
|
Objects: rawObjects,
|
||||||
DesiredAPIVersion: apiVersion,
|
DesiredAPIVersion: apiVersion,
|
||||||
UID: requestUID,
|
UID: requestUID,
|
||||||
},
|
},
|
||||||
@ -151,7 +133,7 @@ func createConversionReviewObjects(conversionReviewVersions []string, objects []
|
|||||||
case v1.SchemeGroupVersion.Version:
|
case v1.SchemeGroupVersion.Version:
|
||||||
return &v1.ConversionReview{
|
return &v1.ConversionReview{
|
||||||
Request: &v1.ConversionRequest{
|
Request: &v1.ConversionRequest{
|
||||||
Objects: objects,
|
Objects: rawObjects,
|
||||||
DesiredAPIVersion: apiVersion,
|
DesiredAPIVersion: apiVersion,
|
||||||
UID: requestUID,
|
UID: requestUID,
|
||||||
},
|
},
|
||||||
@ -162,9 +144,13 @@ func createConversionReviewObjects(conversionReviewVersions []string, objects []
|
|||||||
return nil, nil, fmt.Errorf("no supported conversion review versions")
|
return nil, nil, fmt.Errorf("no supported conversion review versions")
|
||||||
}
|
}
|
||||||
|
|
||||||
func getRawExtensionObject(rx runtime.RawExtension) (runtime.Object, error) {
|
func getRawExtensionObject(rx runtime.RawExtension) (*unstructured.Unstructured, error) {
|
||||||
if rx.Object != nil {
|
if rx.Object != nil {
|
||||||
return rx.Object, nil
|
u, ok := rx.Object.(*unstructured.Unstructured)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("unexpected type %T", rx.Object)
|
||||||
|
}
|
||||||
|
return u, nil
|
||||||
}
|
}
|
||||||
u := unstructured.Unstructured{}
|
u := unstructured.Unstructured{}
|
||||||
err := u.UnmarshalJSON(rx.Raw)
|
err := u.UnmarshalJSON(rx.Raw)
|
||||||
@ -227,30 +213,16 @@ func getConvertedObjectsFromResponse(expectedUID types.UID, response runtime.Obj
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *webhookConverter) Convert(in runtime.Object, toGV schema.GroupVersion) (runtime.Object, error) {
|
func (c *webhookConverter) Convert(in *unstructured.UnstructuredList, toGV schema.GroupVersion) (*unstructured.UnstructuredList, error) {
|
||||||
ctx := context.TODO()
|
ctx := context.TODO()
|
||||||
listObj, isList := in.(*unstructured.UnstructuredList)
|
|
||||||
|
|
||||||
requestUID := uuid.NewUUID()
|
requestUID := uuid.NewUUID()
|
||||||
desiredAPIVersion := toGV.String()
|
desiredAPIVersion := toGV.String()
|
||||||
objectsToConvert := getObjectsToConvert(in, desiredAPIVersion)
|
request, response, err := createConversionReviewObjects(c.conversionReviewVersions, in, desiredAPIVersion, requestUID)
|
||||||
request, response, err := createConversionReviewObjects(c.conversionReviewVersions, objectsToConvert, desiredAPIVersion, requestUID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
objCount := len(objectsToConvert)
|
objCount := len(in.Items)
|
||||||
if objCount == 0 {
|
|
||||||
// no objects needed conversion
|
|
||||||
if !isList {
|
|
||||||
// for a single item, return as-is
|
|
||||||
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.SetAPIVersion(toGV.String())
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, span := tracing.Start(ctx, "Call conversion webhook",
|
ctx, span := tracing.Start(ctx, "Call conversion webhook",
|
||||||
attribute.String("custom-resource-definition", c.name),
|
attribute.String("custom-resource-definition", c.name),
|
||||||
@ -275,170 +247,14 @@ func (c *webhookConverter) Convert(in runtime.Object, toGV schema.GroupVersion)
|
|||||||
return nil, fmt.Errorf("conversion webhook for %v failed: %v", in.GetObjectKind().GroupVersionKind(), err)
|
return nil, fmt.Errorf("conversion webhook for %v failed: %v", in.GetObjectKind().GroupVersionKind(), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(convertedObjects) != len(objectsToConvert) {
|
out := &unstructured.UnstructuredList{}
|
||||||
return nil, fmt.Errorf("conversion webhook for %v returned %d objects, expected %d", in.GetObjectKind().GroupVersionKind(), len(convertedObjects), len(objectsToConvert))
|
out.Items = make([]unstructured.Unstructured, len(convertedObjects))
|
||||||
}
|
for i := range convertedObjects {
|
||||||
|
u, err := getRawExtensionObject(convertedObjects[i])
|
||||||
if isList {
|
if err != nil {
|
||||||
// start a deepcopy of the input and fill in the converted objects from the response at the right spots.
|
return nil, err
|
||||||
// The response list might be sparse because objects had the right version already.
|
|
||||||
convertedList := listObj.DeepCopy()
|
|
||||||
convertedIndex := 0
|
|
||||||
for i := range convertedList.Items {
|
|
||||||
original := &convertedList.Items[i]
|
|
||||||
if original.GetAPIVersion() == toGV.String() {
|
|
||||||
// This item has not been sent for conversion, and therefore does not show up in the response.
|
|
||||||
// convertedList has the right item already.
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
converted, err := getRawExtensionObject(convertedObjects[convertedIndex])
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("conversion webhook for %v returned invalid converted object at index %v: %v", in.GetObjectKind().GroupVersionKind(), convertedIndex, err)
|
|
||||||
}
|
|
||||||
convertedIndex++
|
|
||||||
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, received %v)", in.GetObjectKind().GroupVersionKind(), convertedIndex, 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, received %v)", in.GetObjectKind().GroupVersionKind(), convertedIndex, expected, got)
|
|
||||||
}
|
|
||||||
unstructConverted, ok := converted.(*unstructured.Unstructured)
|
|
||||||
if !ok {
|
|
||||||
// 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().GroupVersionKind(), convertedIndex, converted)
|
|
||||||
}
|
|
||||||
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().GroupVersionKind(), convertedIndex, err)
|
|
||||||
}
|
|
||||||
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().GroupVersionKind(), convertedIndex, err)
|
|
||||||
}
|
|
||||||
convertedList.Items[i] = *unstructConverted
|
|
||||||
}
|
}
|
||||||
convertedList.SetAPIVersion(toGV.String())
|
out.Items[i] = *u
|
||||||
return convertedList, nil
|
|
||||||
}
|
}
|
||||||
|
return out, nil
|
||||||
if len(convertedObjects) != 1 {
|
|
||||||
// This should not happened
|
|
||||||
return nil, fmt.Errorf("conversion webhook for %v failed, no objects returned", in.GetObjectKind())
|
|
||||||
}
|
|
||||||
converted, err := getRawExtensionObject(convertedObjects[0])
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if e, a := toGV, converted.GetObjectKind().GroupVersionKind().GroupVersion(); 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 {
|
|
||||||
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)
|
|
||||||
if !ok {
|
|
||||||
// this should not happened
|
|
||||||
return nil, fmt.Errorf("conversion webhook for %v failed, unexpected type %T at index 0", in.GetObjectKind().GroupVersionKind(), converted)
|
|
||||||
}
|
|
||||||
unstructIn, ok := in.(*unstructured.Unstructured)
|
|
||||||
if !ok {
|
|
||||||
// this should not happened
|
|
||||||
return nil, fmt.Errorf("conversion webhook for %v failed unexpected input type %T", in.GetObjectKind().GroupVersionKind(), in)
|
|
||||||
}
|
|
||||||
if err := validateConvertedObject(unstructIn, unstructConverted); err != nil {
|
|
||||||
return nil, fmt.Errorf("conversion webhook for %v returned invalid object: %v", in.GetObjectKind().GroupVersionKind(), err)
|
|
||||||
}
|
|
||||||
if err := restoreObjectMeta(unstructIn, unstructConverted); err != nil {
|
|
||||||
return nil, fmt.Errorf("conversion webhook for %v returned invalid metadata: %v", in.GetObjectKind().GroupVersionKind(), err)
|
|
||||||
}
|
|
||||||
return converted, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// validateConvertedObject checks that ObjectMeta fields match, with the exception of
|
|
||||||
// labels and annotations.
|
|
||||||
func validateConvertedObject(in, out *unstructured.Unstructured) error {
|
|
||||||
if e, a := in.GetKind(), out.GetKind(); e != a {
|
|
||||||
return fmt.Errorf("must have the same kind: %v != %v", e, a)
|
|
||||||
}
|
|
||||||
if e, a := in.GetName(), out.GetName(); e != a {
|
|
||||||
return fmt.Errorf("must have the same name: %v != %v", e, a)
|
|
||||||
}
|
|
||||||
if e, a := in.GetNamespace(), out.GetNamespace(); e != a {
|
|
||||||
return fmt.Errorf("must have the same namespace: %v != %v", e, a)
|
|
||||||
}
|
|
||||||
if e, a := in.GetUID(), out.GetUID(); e != a {
|
|
||||||
return fmt.Errorf("must have the same UID: %v != %v", e, a)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// restoreObjectMeta deep-copies metadata from original into converted, while preserving labels and annotations from converted.
|
|
||||||
func restoreObjectMeta(original, converted *unstructured.Unstructured) error {
|
|
||||||
obj, found := converted.Object["metadata"]
|
|
||||||
if !found {
|
|
||||||
return fmt.Errorf("missing metadata in converted object")
|
|
||||||
}
|
|
||||||
responseMetaData, ok := obj.(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("invalid metadata of type %T in converted object", obj)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, ok := original.Object["metadata"]; !ok {
|
|
||||||
// the original will always have metadata. But just to be safe, let's clear in converted
|
|
||||||
// with an empty object instead of nil, to be able to add labels and annotations below.
|
|
||||||
converted.Object["metadata"] = map[string]interface{}{}
|
|
||||||
} else {
|
|
||||||
converted.Object["metadata"] = runtime.DeepCopyJSONValue(original.Object["metadata"])
|
|
||||||
}
|
|
||||||
|
|
||||||
obj = converted.Object["metadata"]
|
|
||||||
convertedMetaData, ok := obj.(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("invalid metadata of type %T in input object", obj)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, fld := range []string{"labels", "annotations"} {
|
|
||||||
obj, found := responseMetaData[fld]
|
|
||||||
if !found || obj == nil {
|
|
||||||
delete(convertedMetaData, fld)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
responseField, ok := obj.(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("invalid metadata.%s of type %T in converted object", fld, obj)
|
|
||||||
}
|
|
||||||
|
|
||||||
originalField, ok := convertedMetaData[fld].(map[string]interface{})
|
|
||||||
if !ok && convertedMetaData[fld] != nil {
|
|
||||||
return fmt.Errorf("invalid metadata.%s of type %T in original object", fld, convertedMetaData[fld])
|
|
||||||
}
|
|
||||||
|
|
||||||
somethingChanged := len(originalField) != len(responseField)
|
|
||||||
for k, v := range responseField {
|
|
||||||
if _, ok := v.(string); !ok {
|
|
||||||
return fmt.Errorf("metadata.%s[%s] must be a string, but is %T in converted object", fld, k, v)
|
|
||||||
}
|
|
||||||
if originalField[k] != interface{}(v) {
|
|
||||||
somethingChanged = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if somethingChanged {
|
|
||||||
stringMap := make(map[string]string, len(responseField))
|
|
||||||
for k, v := range responseField {
|
|
||||||
stringMap[k] = v.(string)
|
|
||||||
}
|
|
||||||
var errs field.ErrorList
|
|
||||||
if fld == "labels" {
|
|
||||||
errs = metav1validation.ValidateLabels(stringMap, field.NewPath("metadata", "labels"))
|
|
||||||
} else {
|
|
||||||
errs = apivalidation.ValidateAnnotations(stringMap, field.NewPath("metadata", "annotation"))
|
|
||||||
}
|
|
||||||
if len(errs) > 0 {
|
|
||||||
return errs.ToAggregate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
convertedMetaData[fld] = responseField
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
@ -206,81 +206,19 @@ 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) {
|
func TestCreateConversionReviewObjects(t *testing.T) {
|
||||||
objects := []runtime.RawExtension{
|
objects := &unstructured.UnstructuredList{
|
||||||
{Object: &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "foo/v2", "Kind": "Widget"}}},
|
Items: []unstructured.Unstructured{
|
||||||
|
{
|
||||||
|
Object: map[string]interface{}{"apiVersion": "foo/v2", "Kind": "Widget"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
rawObjects := []runtime.RawExtension{
|
||||||
|
{
|
||||||
|
Object: &objects.Items[0],
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
testcases := []struct {
|
testcases := []struct {
|
||||||
@ -300,7 +238,7 @@ func TestCreateConversionReviewObjects(t *testing.T) {
|
|||||||
Name: "v1",
|
Name: "v1",
|
||||||
Versions: []string{"v1", "v1beta1", "v2"},
|
Versions: []string{"v1", "v1beta1", "v2"},
|
||||||
ExpectRequest: &v1.ConversionReview{
|
ExpectRequest: &v1.ConversionReview{
|
||||||
Request: &v1.ConversionRequest{UID: "uid", DesiredAPIVersion: "foo/v1", Objects: objects},
|
Request: &v1.ConversionRequest{UID: "uid", DesiredAPIVersion: "foo/v1", Objects: rawObjects},
|
||||||
Response: &v1.ConversionResponse{},
|
Response: &v1.ConversionResponse{},
|
||||||
},
|
},
|
||||||
ExpectResponse: &v1.ConversionReview{},
|
ExpectResponse: &v1.ConversionReview{},
|
||||||
@ -309,7 +247,7 @@ func TestCreateConversionReviewObjects(t *testing.T) {
|
|||||||
Name: "v1beta1",
|
Name: "v1beta1",
|
||||||
Versions: []string{"v1beta1", "v1", "v2"},
|
Versions: []string{"v1beta1", "v1", "v2"},
|
||||||
ExpectRequest: &v1beta1.ConversionReview{
|
ExpectRequest: &v1beta1.ConversionReview{
|
||||||
Request: &v1beta1.ConversionRequest{UID: "uid", DesiredAPIVersion: "foo/v1", Objects: objects},
|
Request: &v1beta1.ConversionRequest{UID: "uid", DesiredAPIVersion: "foo/v1", Objects: rawObjects},
|
||||||
Response: &v1beta1.ConversionResponse{},
|
Response: &v1beta1.ConversionResponse{},
|
||||||
},
|
},
|
||||||
ExpectResponse: &v1beta1.ConversionReview{},
|
ExpectResponse: &v1beta1.ConversionReview{},
|
||||||
|
Loading…
Reference in New Issue
Block a user