CRD versioning validation and defaulting

This commit is contained in:
Mehdy Bohlool 2018-05-07 12:54:26 -07:00
parent 10c48ae510
commit 8a39e5381c
4 changed files with 424 additions and 29 deletions

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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"))...)
}

View File

@ -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"),