From 8a39e5381cdf0299ac654d3a35323694c9cd66e0 Mon Sep 17 00:00:00 2001 From: Mehdy Bohlool Date: Mon, 7 May 2018 12:54:26 -0700 Subject: [PATCH] CRD versioning validation and defaulting --- .../pkg/apis/apiextensions/helpers.go | 31 ++ .../apis/apiextensions/v1beta1/defaults.go | 20 ++ .../apiextensions/validation/validation.go | 67 +++- .../validation/validation_test.go | 335 ++++++++++++++++-- 4 files changed, 424 insertions(+), 29 deletions(-) diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/helpers.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/helpers.go index 8dc7f72d660..92cad7d9b73 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/helpers.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/helpers.go @@ -17,6 +17,7 @@ limitations under the License. package apiextensions import ( + "fmt" "time" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -116,3 +117,33 @@ func CRDRemoveFinalizer(crd *CustomResourceDefinition, needle string) { } crd.Finalizers = newFinalizers } + +// HasServedCRDVersion returns true if `version` is in the list of CRD's versions and the Served flag is set. +func HasServedCRDVersion(crd *CustomResourceDefinition, version string) bool { + for _, v := range crd.Spec.Versions { + if v.Name == version { + return v.Served + } + } + return false +} + +// GetCRDStorageVersion returns the storage version for given CRD. +func GetCRDStorageVersion(crd *CustomResourceDefinition) (string, error) { + for _, v := range crd.Spec.Versions { + if v.Storage { + return v.Name, nil + } + } + // This should not happened if crd is valid + return "", fmt.Errorf("invalid CustomResourceDefinition, no storage version") +} + +func IsStoredVersion(crd *CustomResourceDefinition, version string) bool { + for _, v := range crd.Status.StoredVersions { + if version == v { + return true + } + } + return false +} diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1/defaults.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1/defaults.go index edffaed55f0..1984e229778 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1/defaults.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1/defaults.go @@ -31,6 +31,14 @@ func addDefaultingFuncs(scheme *runtime.Scheme) error { func SetDefaults_CustomResourceDefinition(obj *CustomResourceDefinition) { SetDefaults_CustomResourceDefinitionSpec(&obj.Spec) + if len(obj.Status.StoredVersions) == 0 { + for _, v := range obj.Spec.Versions { + if v.Storage { + obj.Status.StoredVersions = append(obj.Status.StoredVersions, v.Name) + break + } + } + } } func SetDefaults_CustomResourceDefinitionSpec(obj *CustomResourceDefinitionSpec) { @@ -43,4 +51,16 @@ func SetDefaults_CustomResourceDefinitionSpec(obj *CustomResourceDefinitionSpec) if len(obj.Names.ListKind) == 0 && len(obj.Names.Kind) > 0 { obj.Names.ListKind = obj.Names.Kind + "List" } + // If there is no list of versions, create on using deprecated Version field. + if len(obj.Versions) == 0 && len(obj.Version) != 0 { + obj.Versions = []CustomResourceDefinitionVersion{{ + Name: obj.Version, + Storage: true, + Served: true, + }} + } + // For backward compatibility set the version field to the first item in versions list. + if len(obj.Version) == 0 && len(obj.Versions) != 0 { + obj.Version = obj.Versions[0].Name + } } diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation/validation.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation/validation.go index 390f75f4426..eb9acc79e8e 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation/validation.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation/validation.go @@ -45,6 +45,7 @@ func ValidateCustomResourceDefinition(obj *apiextensions.CustomResourceDefinitio allErrs := genericvalidation.ValidateObjectMeta(&obj.ObjectMeta, false, nameValidationFn, field.NewPath("metadata")) allErrs = append(allErrs, ValidateCustomResourceDefinitionSpec(&obj.Spec, field.NewPath("spec"))...) allErrs = append(allErrs, ValidateCustomResourceDefinitionStatus(&obj.Status, field.NewPath("status"))...) + allErrs = append(allErrs, ValidateCustomResourceDefinitionStoredVersions(obj.Status.StoredVersions, obj.Spec.Versions, field.NewPath("status").Child("storedVersions"))...) return allErrs } @@ -53,6 +54,34 @@ func ValidateCustomResourceDefinitionUpdate(obj, oldObj *apiextensions.CustomRes allErrs := genericvalidation.ValidateObjectMetaUpdate(&obj.ObjectMeta, &oldObj.ObjectMeta, field.NewPath("metadata")) allErrs = append(allErrs, ValidateCustomResourceDefinitionSpecUpdate(&obj.Spec, &oldObj.Spec, apiextensions.IsCRDConditionTrue(oldObj, apiextensions.Established), field.NewPath("spec"))...) allErrs = append(allErrs, ValidateCustomResourceDefinitionStatus(&obj.Status, field.NewPath("status"))...) + allErrs = append(allErrs, ValidateCustomResourceDefinitionStoredVersions(obj.Status.StoredVersions, obj.Spec.Versions, field.NewPath("status").Child("storedVersions"))...) + return allErrs +} + +// ValidateCustomResourceDefinitionStoredVersions statically validates +func ValidateCustomResourceDefinitionStoredVersions(storedVersions []string, versions []apiextensions.CustomResourceDefinitionVersion, fldPath *field.Path) field.ErrorList { + if len(storedVersions) == 0 { + return field.ErrorList{field.Invalid(fldPath, storedVersions, "must have at least one stored version")} + } + allErrs := field.ErrorList{} + storedVersionsMap := map[string]int{} + for i, v := range storedVersions { + storedVersionsMap[v] = i + } + for _, v := range versions { + _, ok := storedVersionsMap[v.Name] + if v.Storage && !ok { + allErrs = append(allErrs, field.Invalid(fldPath, v, "must have the storage version "+v.Name)) + } + if ok { + delete(storedVersionsMap, v.Name) + } + } + + for v, i := range storedVersionsMap { + allErrs = append(allErrs, field.Invalid(fldPath.Index(i), v, "must appear in spec.versions")) + } + return allErrs } @@ -75,12 +104,6 @@ func ValidateCustomResourceDefinitionSpec(spec *apiextensions.CustomResourceDefi allErrs = append(allErrs, field.Invalid(fldPath.Child("group"), spec.Group, "should be a domain with at least one dot")) } - if len(spec.Version) == 0 { - allErrs = append(allErrs, field.Required(fldPath.Child("version"), "")) - } else if errs := validationutil.IsDNS1035Label(spec.Version); len(errs) > 0 { - allErrs = append(allErrs, field.Invalid(fldPath.Child("version"), spec.Version, strings.Join(errs, ","))) - } - switch spec.Scope { case "": allErrs = append(allErrs, field.Required(fldPath.Child("scope"), "")) @@ -89,6 +112,37 @@ func ValidateCustomResourceDefinitionSpec(spec *apiextensions.CustomResourceDefi allErrs = append(allErrs, field.NotSupported(fldPath.Child("scope"), spec.Scope, []string{string(apiextensions.ClusterScoped), string(apiextensions.NamespaceScoped)})) } + storageFlagCount := 0 + versionsMap := map[string]bool{} + uniqueNames := true + for i, version := range spec.Versions { + if version.Storage { + storageFlagCount++ + } + if versionsMap[version.Name] { + uniqueNames = false + } else { + versionsMap[version.Name] = true + } + if errs := validationutil.IsDNS1035Label(version.Name); len(errs) > 0 { + allErrs = append(allErrs, field.Invalid(fldPath.Child("versions").Index(i).Child("name"), spec.Versions[i].Name, strings.Join(errs, ","))) + } + } + if !uniqueNames { + allErrs = append(allErrs, field.Invalid(fldPath.Child("versions"), spec.Versions, "must contain unique version names")) + } + if storageFlagCount != 1 { + allErrs = append(allErrs, field.Invalid(fldPath.Child("versions"), spec.Versions, "must have exactly one version marked as storage version")) + } + if len(spec.Version) != 0 { + if errs := validationutil.IsDNS1035Label(spec.Version); len(errs) > 0 { + allErrs = append(allErrs, field.Invalid(fldPath.Child("version"), spec.Version, strings.Join(errs, ","))) + } + if len(spec.Versions) >= 1 && spec.Versions[0].Name != spec.Version { + allErrs = append(allErrs, field.Invalid(fldPath.Child("version"), spec.Version, "must match the first version in spec.versions")) + } + } + // in addition to the basic name restrictions, some names are required for spec, but not for status if len(spec.Names.Plural) == 0 { allErrs = append(allErrs, field.Required(fldPath.Child("names", "plural"), "")) @@ -130,7 +184,6 @@ func ValidateCustomResourceDefinitionSpecUpdate(spec, oldSpec *apiextensions.Cus if established { // these effect the storage and cannot be changed therefore - allErrs = append(allErrs, genericvalidation.ValidateImmutableField(spec.Version, oldSpec.Version, fldPath.Child("version"))...) allErrs = append(allErrs, genericvalidation.ValidateImmutableField(spec.Scope, oldSpec.Scope, fldPath.Child("scope"))...) allErrs = append(allErrs, genericvalidation.ValidateImmutableField(spec.Names.Kind, oldSpec.Names.Kind, fldPath.Child("names", "kind"))...) } diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation/validation_test.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation/validation_test.go index 8f71dd9b31d..6bc545c0fd8 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation/validation_test.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation/validation_test.go @@ -19,10 +19,9 @@ package validation import ( "testing" + "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/validation/field" - - "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" ) type validationMatch struct { @@ -51,11 +50,150 @@ func (v validationMatch) matches(err *field.Error) bool { } func TestValidateCustomResourceDefinition(t *testing.T) { + singleVersionList := []apiextensions.CustomResourceDefinitionVersion{ + { + Name: "version", + Served: true, + Storage: true, + }, + } tests := []struct { name string resource *apiextensions.CustomResourceDefinition errors []validationMatch }{ + { + name: "no_storage_version", + resource: &apiextensions.CustomResourceDefinition{ + ObjectMeta: metav1.ObjectMeta{Name: "plural.group.com"}, + Spec: apiextensions.CustomResourceDefinitionSpec{ + Group: "group.com", + Scope: apiextensions.ResourceScope("Cluster"), + Names: apiextensions.CustomResourceDefinitionNames{ + Plural: "plural", + Singular: "singular", + Kind: "Plural", + ListKind: "PluralList", + }, + Versions: []apiextensions.CustomResourceDefinitionVersion{ + { + Name: "version", + Served: true, + Storage: false, + }, + { + Name: "version2", + Served: true, + Storage: false, + }, + }, + }, + Status: apiextensions.CustomResourceDefinitionStatus{ + StoredVersions: []string{"version"}, + }, + }, + errors: []validationMatch{ + invalid("spec", "versions"), + }, + }, + { + name: "multiple_storage_version", + resource: &apiextensions.CustomResourceDefinition{ + ObjectMeta: metav1.ObjectMeta{Name: "plural.group.com"}, + Spec: apiextensions.CustomResourceDefinitionSpec{ + Group: "group.com", + Scope: apiextensions.ResourceScope("Cluster"), + Names: apiextensions.CustomResourceDefinitionNames{ + Plural: "plural", + Singular: "singular", + Kind: "Plural", + ListKind: "PluralList", + }, + Versions: []apiextensions.CustomResourceDefinitionVersion{ + { + Name: "version", + Served: true, + Storage: true, + }, + { + Name: "version2", + Served: true, + Storage: true, + }, + }, + }, + Status: apiextensions.CustomResourceDefinitionStatus{ + StoredVersions: []string{"version"}, + }, + }, + errors: []validationMatch{ + invalid("spec", "versions"), + invalid("status", "storedVersions"), + }, + }, + { + name: "missing_storage_version_in_stored_versions", + resource: &apiextensions.CustomResourceDefinition{ + ObjectMeta: metav1.ObjectMeta{Name: "plural.group.com"}, + Spec: apiextensions.CustomResourceDefinitionSpec{ + Group: "group.com", + Scope: apiextensions.ResourceScope("Cluster"), + Names: apiextensions.CustomResourceDefinitionNames{ + Plural: "plural", + Singular: "singular", + Kind: "Plural", + ListKind: "PluralList", + }, + Versions: []apiextensions.CustomResourceDefinitionVersion{ + { + Name: "version", + Served: true, + Storage: false, + }, + { + Name: "version2", + Served: true, + Storage: true, + }, + }, + }, + Status: apiextensions.CustomResourceDefinitionStatus{ + StoredVersions: []string{"version"}, + }, + }, + errors: []validationMatch{ + invalid("status", "storedVersions"), + }, + }, + { + name: "empty_stored_version", + resource: &apiextensions.CustomResourceDefinition{ + ObjectMeta: metav1.ObjectMeta{Name: "plural.group.com"}, + Spec: apiextensions.CustomResourceDefinitionSpec{ + Group: "group.com", + Scope: apiextensions.ResourceScope("Cluster"), + Names: apiextensions.CustomResourceDefinitionNames{ + Plural: "plural", + Singular: "singular", + Kind: "Plural", + ListKind: "PluralList", + }, + Versions: []apiextensions.CustomResourceDefinitionVersion{ + { + Name: "version", + Served: true, + Storage: true, + }, + }, + }, + Status: apiextensions.CustomResourceDefinitionStatus{ + StoredVersions: []string{}, + }, + }, + errors: []validationMatch{ + invalid("status", "storedVersions"), + }, + }, { name: "mismatched name", resource: &apiextensions.CustomResourceDefinition{ @@ -68,8 +206,9 @@ func TestValidateCustomResourceDefinition(t *testing.T) { }, }, errors: []validationMatch{ + invalid("status", "storedVersions"), invalid("metadata", "name"), - required("spec", "version"), + invalid("spec", "versions"), required("spec", "scope"), required("spec", "names", "singular"), required("spec", "names", "kind"), @@ -82,9 +221,10 @@ func TestValidateCustomResourceDefinition(t *testing.T) { ObjectMeta: metav1.ObjectMeta{Name: "plural.group.com"}, }, errors: []validationMatch{ + invalid("status", "storedVersions"), invalid("metadata", "name"), + invalid("spec", "versions"), required("spec", "group"), - required("spec", "version"), required("spec", "scope"), required("spec", "names", "plural"), required("spec", "names", "singular"), @@ -117,9 +257,9 @@ func TestValidateCustomResourceDefinition(t *testing.T) { }, }, errors: []validationMatch{ + invalid("status", "storedVersions"), invalid("metadata", "name"), invalid("spec", "group"), - invalid("spec", "version"), unsupported("spec", "scope"), invalid("spec", "names", "plural"), invalid("spec", "names", "singular"), @@ -131,6 +271,8 @@ func TestValidateCustomResourceDefinition(t *testing.T) { invalid("status", "acceptedNames", "kind"), invalid("status", "acceptedNames", "listKind"), // invalid format invalid("status", "acceptedNames", "listKind"), // kind == listKind + invalid("spec", "versions"), + invalid("spec", "version"), }, }, { @@ -138,8 +280,9 @@ func TestValidateCustomResourceDefinition(t *testing.T) { resource: &apiextensions.CustomResourceDefinition{ ObjectMeta: metav1.ObjectMeta{Name: "plural.group"}, Spec: apiextensions.CustomResourceDefinitionSpec{ - Group: "group.c(*&om", - Version: "version", + Group: "group.c(*&om", + Version: "version", + Versions: singleVersionList, Names: apiextensions.CustomResourceDefinitionNames{ Plural: "plural", Singular: "singular", @@ -154,6 +297,7 @@ func TestValidateCustomResourceDefinition(t *testing.T) { Kind: "matching", ListKind: "matching", }, + StoredVersions: []string{"version"}, }, }, errors: []validationMatch{ @@ -169,9 +313,10 @@ func TestValidateCustomResourceDefinition(t *testing.T) { resource: &apiextensions.CustomResourceDefinition{ ObjectMeta: metav1.ObjectMeta{Name: "plural.group.com"}, Spec: apiextensions.CustomResourceDefinitionSpec{ - Group: "group.com", - Version: "version", - Scope: apiextensions.NamespaceScoped, + Group: "group.com", + Version: "version", + Versions: singleVersionList, + Scope: apiextensions.NamespaceScoped, Names: apiextensions.CustomResourceDefinitionNames{ Plural: "plural", Singular: "singular", @@ -187,6 +332,9 @@ func TestValidateCustomResourceDefinition(t *testing.T) { }, }, }, + Status: apiextensions.CustomResourceDefinitionStatus{ + StoredVersions: []string{"version"}, + }, }, errors: []validationMatch{ forbidden("spec", "validation", "openAPIV3Schema", "additionalProperties"), @@ -197,9 +345,10 @@ func TestValidateCustomResourceDefinition(t *testing.T) { resource: &apiextensions.CustomResourceDefinition{ ObjectMeta: metav1.ObjectMeta{Name: "plural.group.com"}, Spec: apiextensions.CustomResourceDefinitionSpec{ - Group: "group.com", - Version: "version", - Scope: apiextensions.NamespaceScoped, + Group: "group.com", + Version: "version", + Versions: singleVersionList, + Scope: apiextensions.NamespaceScoped, Names: apiextensions.CustomResourceDefinitionNames{ Plural: "plural", Singular: "singular", @@ -217,6 +366,9 @@ func TestValidateCustomResourceDefinition(t *testing.T) { }, }, }, + Status: apiextensions.CustomResourceDefinitionStatus{ + StoredVersions: []string{"version"}, + }, }, errors: []validationMatch{}, }, @@ -266,7 +418,14 @@ func TestValidateCustomResourceDefinitionUpdate(t *testing.T) { Spec: apiextensions.CustomResourceDefinitionSpec{ Group: "group.com", Version: "version", - Scope: apiextensions.ResourceScope("Cluster"), + Versions: []apiextensions.CustomResourceDefinitionVersion{ + { + Name: "version", + Served: true, + Storage: true, + }, + }, + Scope: apiextensions.ResourceScope("Cluster"), Names: apiextensions.CustomResourceDefinitionNames{ Plural: "plural", Singular: "singular", @@ -291,7 +450,14 @@ func TestValidateCustomResourceDefinitionUpdate(t *testing.T) { Spec: apiextensions.CustomResourceDefinitionSpec{ Group: "group.com", Version: "version", - Scope: apiextensions.ResourceScope("Cluster"), + Versions: []apiextensions.CustomResourceDefinitionVersion{ + { + Name: "version", + Served: true, + Storage: true, + }, + }, + Scope: apiextensions.ResourceScope("Cluster"), Names: apiextensions.CustomResourceDefinitionNames{ Plural: "plural", Singular: "singular", @@ -306,6 +472,7 @@ func TestValidateCustomResourceDefinitionUpdate(t *testing.T) { Kind: "kind", ListKind: "listkind", }, + StoredVersions: []string{"version"}, }, }, errors: []validationMatch{}, @@ -320,7 +487,14 @@ func TestValidateCustomResourceDefinitionUpdate(t *testing.T) { Spec: apiextensions.CustomResourceDefinitionSpec{ Group: "group.com", Version: "version", - Scope: apiextensions.ResourceScope("Cluster"), + Versions: []apiextensions.CustomResourceDefinitionVersion{ + { + Name: "version", + Served: true, + Storage: true, + }, + }, + Scope: apiextensions.ResourceScope("Cluster"), Names: apiextensions.CustomResourceDefinitionNames{ Plural: "plural", Singular: "singular", @@ -348,7 +522,14 @@ func TestValidateCustomResourceDefinitionUpdate(t *testing.T) { Spec: apiextensions.CustomResourceDefinitionSpec{ Group: "group.com", Version: "version", - Scope: apiextensions.ResourceScope("Cluster"), + Versions: []apiextensions.CustomResourceDefinitionVersion{ + { + Name: "version", + Served: true, + Storage: true, + }, + }, + Scope: apiextensions.ResourceScope("Cluster"), Names: apiextensions.CustomResourceDefinitionNames{ Plural: "plural", Singular: "singular", @@ -363,10 +544,91 @@ func TestValidateCustomResourceDefinitionUpdate(t *testing.T) { Kind: "kind", ListKind: "listkind", }, + StoredVersions: []string{"version"}, }, }, errors: []validationMatch{}, }, + { + name: "version-deleted", + old: &apiextensions.CustomResourceDefinition{ + ObjectMeta: metav1.ObjectMeta{ + Name: "plural.group.com", + ResourceVersion: "42", + }, + Spec: apiextensions.CustomResourceDefinitionSpec{ + Group: "group.com", + Version: "version", + Versions: []apiextensions.CustomResourceDefinitionVersion{ + { + Name: "version", + Served: true, + Storage: true, + }, + { + Name: "version2", + Served: true, + Storage: false, + }, + }, + Scope: apiextensions.ResourceScope("Cluster"), + Names: apiextensions.CustomResourceDefinitionNames{ + Plural: "plural", + Singular: "singular", + Kind: "kind", + ListKind: "listkind", + }, + }, + Status: apiextensions.CustomResourceDefinitionStatus{ + AcceptedNames: apiextensions.CustomResourceDefinitionNames{ + Plural: "plural", + Singular: "singular", + Kind: "kind", + ListKind: "listkind", + }, + StoredVersions: []string{"version", "version2"}, + Conditions: []apiextensions.CustomResourceDefinitionCondition{ + {Type: apiextensions.Established, Status: apiextensions.ConditionTrue}, + }, + }, + }, + resource: &apiextensions.CustomResourceDefinition{ + ObjectMeta: metav1.ObjectMeta{ + Name: "plural.group.com", + ResourceVersion: "42", + }, + Spec: apiextensions.CustomResourceDefinitionSpec{ + Group: "group.com", + Version: "version", + Versions: []apiextensions.CustomResourceDefinitionVersion{ + { + Name: "version", + Served: true, + Storage: true, + }, + }, + Scope: apiextensions.ResourceScope("Cluster"), + Names: apiextensions.CustomResourceDefinitionNames{ + Plural: "plural", + Singular: "singular", + Kind: "kind", + ListKind: "listkind", + }, + }, + Status: apiextensions.CustomResourceDefinitionStatus{ + AcceptedNames: apiextensions.CustomResourceDefinitionNames{ + Plural: "plural", + Singular: "singular", + Kind: "kind", + ListKind: "listkind", + }, + StoredVersions: []string{"version", "version2"}, + }, + }, + errors: []validationMatch{ + invalid("status", "storedVersions[1]"), + }, + }, { name: "changes", old: &apiextensions.CustomResourceDefinition{ @@ -377,7 +639,14 @@ func TestValidateCustomResourceDefinitionUpdate(t *testing.T) { Spec: apiextensions.CustomResourceDefinitionSpec{ Group: "group.com", Version: "version", - Scope: apiextensions.ResourceScope("Cluster"), + Versions: []apiextensions.CustomResourceDefinitionVersion{ + { + Name: "version", + Served: true, + Storage: true, + }, + }, + Scope: apiextensions.ResourceScope("Cluster"), Names: apiextensions.CustomResourceDefinitionNames{ Plural: "plural", Singular: "singular", @@ -405,7 +674,14 @@ func TestValidateCustomResourceDefinitionUpdate(t *testing.T) { Spec: apiextensions.CustomResourceDefinitionSpec{ Group: "abc.com", Version: "version2", - Scope: apiextensions.ResourceScope("Namespaced"), + Versions: []apiextensions.CustomResourceDefinitionVersion{ + { + Name: "version2", + Served: true, + Storage: true, + }, + }, + Scope: apiextensions.ResourceScope("Namespaced"), Names: apiextensions.CustomResourceDefinitionNames{ Plural: "plural2", Singular: "singular2", @@ -420,6 +696,7 @@ func TestValidateCustomResourceDefinitionUpdate(t *testing.T) { Kind: "kind2", ListKind: "listkind2", }, + StoredVersions: []string{"version2"}, }, }, errors: []validationMatch{ @@ -437,7 +714,14 @@ func TestValidateCustomResourceDefinitionUpdate(t *testing.T) { Spec: apiextensions.CustomResourceDefinitionSpec{ Group: "group.com", Version: "version", - Scope: apiextensions.ResourceScope("Cluster"), + Versions: []apiextensions.CustomResourceDefinitionVersion{ + { + Name: "version", + Served: true, + Storage: true, + }, + }, + Scope: apiextensions.ResourceScope("Cluster"), Names: apiextensions.CustomResourceDefinitionNames{ Plural: "plural", Singular: "singular", @@ -465,7 +749,14 @@ func TestValidateCustomResourceDefinitionUpdate(t *testing.T) { Spec: apiextensions.CustomResourceDefinitionSpec{ Group: "abc.com", Version: "version2", - Scope: apiextensions.ResourceScope("Namespaced"), + Versions: []apiextensions.CustomResourceDefinitionVersion{ + { + Name: "version2", + Served: true, + Storage: true, + }, + }, + Scope: apiextensions.ResourceScope("Namespaced"), Names: apiextensions.CustomResourceDefinitionNames{ Plural: "plural2", Singular: "singular2", @@ -480,11 +771,11 @@ func TestValidateCustomResourceDefinitionUpdate(t *testing.T) { Kind: "kind2", ListKind: "listkind2", }, + StoredVersions: []string{"version2"}, }, }, errors: []validationMatch{ immutable("spec", "group"), - immutable("spec", "version"), immutable("spec", "scope"), immutable("spec", "names", "kind"), immutable("spec", "names", "plural"),