Merge pull request #81105 from liggitt/crd-validation-refactor

CRD validation refactor
This commit is contained in:
Kubernetes Prow Robot 2019-08-08 12:35:46 -07:00 committed by GitHub
commit 35cacd44e6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 192 additions and 91 deletions

View File

@ -12,6 +12,7 @@ go_library(
importmap = "k8s.io/kubernetes/vendor/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation", importmap = "k8s.io/kubernetes/vendor/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation",
importpath = "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation", importpath = "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation",
deps = [ deps = [
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apihelpers: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/v1beta1:go_default_library", "//staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1:go_default_library",
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema:go_default_library", "//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema:go_default_library",
@ -22,6 +23,7 @@ go_library(
"//staging/src/k8s.io/apimachinery/pkg/api/equality:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/api/equality: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",
"//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/util/sets:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/sets: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/apimachinery/pkg/util/validation/field:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/validation/field:go_default_library",
@ -44,6 +46,7 @@ go_test(
"//staging/src/k8s.io/apimachinery/pkg/api/apitesting/fuzzer:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/api/apitesting/fuzzer: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",
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/runtime/serializer:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/runtime/serializer:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/json:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/json: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",

View File

@ -25,9 +25,11 @@ import (
govalidate "github.com/go-openapi/validate" govalidate "github.com/go-openapi/validate"
schemaobjectmeta "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/objectmeta" schemaobjectmeta "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/objectmeta"
"k8s.io/apiextensions-apiserver/pkg/apihelpers"
apiequality "k8s.io/apimachinery/pkg/api/equality" apiequality "k8s.io/apimachinery/pkg/api/equality"
genericvalidation "k8s.io/apimachinery/pkg/api/validation" genericvalidation "k8s.io/apimachinery/pkg/api/validation"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/sets"
utilvalidation "k8s.io/apimachinery/pkg/util/validation" utilvalidation "k8s.io/apimachinery/pkg/util/validation"
"k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/apimachinery/pkg/util/validation/field"
@ -48,7 +50,7 @@ var (
) )
// ValidateCustomResourceDefinition statically validates // ValidateCustomResourceDefinition statically validates
func ValidateCustomResourceDefinition(obj *apiextensions.CustomResourceDefinition) field.ErrorList { func ValidateCustomResourceDefinition(obj *apiextensions.CustomResourceDefinition, requestGV schema.GroupVersion) field.ErrorList {
nameValidationFn := func(name string, prefix bool) []string { nameValidationFn := func(name string, prefix bool) []string {
ret := genericvalidation.NameIsDNSSubdomain(name, prefix) ret := genericvalidation.NameIsDNSSubdomain(name, prefix)
requiredName := obj.Spec.Names.Plural + "." + obj.Spec.Group requiredName := obj.Spec.Names.Plural + "." + obj.Spec.Group
@ -58,19 +60,43 @@ func ValidateCustomResourceDefinition(obj *apiextensions.CustomResourceDefinitio
return ret return ret
} }
opts := validationOptions{
allowDefaults: allowDefaults(requestGV),
requireRecognizedConversionReviewVersion: true,
requireImmutableNames: false,
}
allErrs := genericvalidation.ValidateObjectMeta(&obj.ObjectMeta, false, nameValidationFn, field.NewPath("metadata")) allErrs := genericvalidation.ValidateObjectMeta(&obj.ObjectMeta, false, nameValidationFn, field.NewPath("metadata"))
allErrs = append(allErrs, ValidateCustomResourceDefinitionSpec(&obj.Spec, field.NewPath("spec"))...) allErrs = append(allErrs, validateCustomResourceDefinitionSpec(&obj.Spec, opts, field.NewPath("spec"))...)
allErrs = append(allErrs, ValidateCustomResourceDefinitionStatus(&obj.Status, field.NewPath("status"))...) allErrs = append(allErrs, ValidateCustomResourceDefinitionStatus(&obj.Status, field.NewPath("status"))...)
allErrs = append(allErrs, ValidateCustomResourceDefinitionStoredVersions(obj.Status.StoredVersions, obj.Spec.Versions, field.NewPath("status").Child("storedVersions"))...) allErrs = append(allErrs, ValidateCustomResourceDefinitionStoredVersions(obj.Status.StoredVersions, obj.Spec.Versions, field.NewPath("status").Child("storedVersions"))...)
allErrs = append(allErrs, validateAPIApproval(obj, nil, requestGV)...)
return allErrs return allErrs
} }
// validationOptions groups several validation options, to avoid passing multiple bool parameters to methods
type validationOptions struct {
// allowDefaults permits the validation schema to contain default attributes
allowDefaults bool
// requireRecognizedConversionReviewVersion requires accepted webhook conversion versions to contain a recognized version
requireRecognizedConversionReviewVersion bool
// requireImmutableNames disables changing spec.names
requireImmutableNames bool
}
// ValidateCustomResourceDefinitionUpdate statically validates // ValidateCustomResourceDefinitionUpdate statically validates
func ValidateCustomResourceDefinitionUpdate(obj, oldObj *apiextensions.CustomResourceDefinition) field.ErrorList { func ValidateCustomResourceDefinitionUpdate(obj, oldObj *apiextensions.CustomResourceDefinition, requestGV schema.GroupVersion) field.ErrorList {
opts := validationOptions{
allowDefaults: allowDefaults(requestGV) || specHasDefaults(&oldObj.Spec),
requireRecognizedConversionReviewVersion: oldObj.Spec.Conversion == nil || hasValidConversionReviewVersionOrEmpty(oldObj.Spec.Conversion.ConversionReviewVersions),
requireImmutableNames: apiextensions.IsCRDConditionTrue(oldObj, apiextensions.Established),
}
allErrs := genericvalidation.ValidateObjectMetaUpdate(&obj.ObjectMeta, &oldObj.ObjectMeta, field.NewPath("metadata")) 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, validateCustomResourceDefinitionSpecUpdate(&obj.Spec, &oldObj.Spec, opts, field.NewPath("spec"))...)
allErrs = append(allErrs, ValidateCustomResourceDefinitionStatus(&obj.Status, field.NewPath("status"))...) allErrs = append(allErrs, ValidateCustomResourceDefinitionStatus(&obj.Status, field.NewPath("status"))...)
allErrs = append(allErrs, ValidateCustomResourceDefinitionStoredVersions(obj.Status.StoredVersions, obj.Spec.Versions, field.NewPath("status").Child("storedVersions"))...) allErrs = append(allErrs, ValidateCustomResourceDefinitionStoredVersions(obj.Status.StoredVersions, obj.Spec.Versions, field.NewPath("status").Child("storedVersions"))...)
allErrs = append(allErrs, validateAPIApproval(obj, oldObj, requestGV)...)
return allErrs return allErrs
} }
@ -108,10 +134,10 @@ func ValidateUpdateCustomResourceDefinitionStatus(obj, oldObj *apiextensions.Cus
return allErrs return allErrs
} }
// ValidateCustomResourceDefinitionVersion statically validates. // validateCustomResourceDefinitionVersion statically validates.
func ValidateCustomResourceDefinitionVersion(version *apiextensions.CustomResourceDefinitionVersion, fldPath *field.Path, mustBeStructural, statusEnabled, allowDefaults bool) field.ErrorList { func validateCustomResourceDefinitionVersion(version *apiextensions.CustomResourceDefinitionVersion, fldPath *field.Path, mustBeStructural, statusEnabled bool, opts validationOptions) field.ErrorList {
allErrs := field.ErrorList{} allErrs := field.ErrorList{}
allErrs = append(allErrs, ValidateCustomResourceDefinitionValidation(version.Schema, mustBeStructural, statusEnabled, allowDefaults, fldPath.Child("schema"))...) allErrs = append(allErrs, validateCustomResourceDefinitionValidation(version.Schema, mustBeStructural, statusEnabled, opts, fldPath.Child("schema"))...)
allErrs = append(allErrs, ValidateCustomResourceDefinitionSubresources(version.Subresources, fldPath.Child("subresources"))...) allErrs = append(allErrs, ValidateCustomResourceDefinitionSubresources(version.Subresources, fldPath.Child("subresources"))...)
for i := range version.AdditionalPrinterColumns { for i := range version.AdditionalPrinterColumns {
allErrs = append(allErrs, ValidateCustomResourceColumnDefinition(&version.AdditionalPrinterColumns[i], fldPath.Child("additionalPrinterColumns").Index(i))...) allErrs = append(allErrs, ValidateCustomResourceColumnDefinition(&version.AdditionalPrinterColumns[i], fldPath.Child("additionalPrinterColumns").Index(i))...)
@ -119,13 +145,7 @@ func ValidateCustomResourceDefinitionVersion(version *apiextensions.CustomResour
return allErrs return allErrs
} }
// ValidateCustomResourceDefinitionSpec statically validates func validateCustomResourceDefinitionSpec(spec *apiextensions.CustomResourceDefinitionSpec, opts validationOptions, fldPath *field.Path) field.ErrorList {
func ValidateCustomResourceDefinitionSpec(spec *apiextensions.CustomResourceDefinitionSpec, fldPath *field.Path) field.ErrorList {
allowDefaults := utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceDefaulting)
return validateCustomResourceDefinitionSpec(spec, true, allowDefaults, fldPath)
}
func validateCustomResourceDefinitionSpec(spec *apiextensions.CustomResourceDefinitionSpec, requireRecognizedVersion, allowDefaults bool, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{} allErrs := field.ErrorList{}
if len(spec.Group) == 0 { if len(spec.Group) == 0 {
@ -151,7 +171,7 @@ func validateCustomResourceDefinitionSpec(spec *apiextensions.CustomResourceDefi
} }
} }
} }
if allowDefaults && specHasDefaults(spec) { if opts.allowDefaults && specHasDefaults(spec) {
mustBeStructural = true mustBeStructural = true
if spec.PreserveUnknownFields == nil || *spec.PreserveUnknownFields == true { if spec.PreserveUnknownFields == nil || *spec.PreserveUnknownFields == true {
allErrs = append(allErrs, field.Invalid(fldPath.Child("preserveUnknownFields"), true, "must be false in order to use defaults in the schema")) allErrs = append(allErrs, field.Invalid(fldPath.Child("preserveUnknownFields"), true, "must be false in order to use defaults in the schema"))
@ -177,7 +197,7 @@ func validateCustomResourceDefinitionSpec(spec *apiextensions.CustomResourceDefi
allErrs = append(allErrs, field.Invalid(fldPath.Child("versions").Index(i).Child("name"), spec.Versions[i].Name, strings.Join(errs, ","))) allErrs = append(allErrs, field.Invalid(fldPath.Child("versions").Index(i).Child("name"), spec.Versions[i].Name, strings.Join(errs, ",")))
} }
subresources := getSubresourcesForVersion(spec, version.Name) subresources := getSubresourcesForVersion(spec, version.Name)
allErrs = append(allErrs, ValidateCustomResourceDefinitionVersion(&version, fldPath.Child("versions").Index(i), mustBeStructural, hasStatusEnabled(subresources), allowDefaults)...) allErrs = append(allErrs, validateCustomResourceDefinitionVersion(&version, fldPath.Child("versions").Index(i), mustBeStructural, hasStatusEnabled(subresources), opts)...)
} }
// The top-level and per-version fields are mutual exclusive // The top-level and per-version fields are mutual exclusive
@ -232,7 +252,7 @@ func validateCustomResourceDefinitionSpec(spec *apiextensions.CustomResourceDefi
} }
allErrs = append(allErrs, ValidateCustomResourceDefinitionNames(&spec.Names, fldPath.Child("names"))...) allErrs = append(allErrs, ValidateCustomResourceDefinitionNames(&spec.Names, fldPath.Child("names"))...)
allErrs = append(allErrs, ValidateCustomResourceDefinitionValidation(spec.Validation, mustBeStructural, hasAnyStatusEnabled(spec), allowDefaults, fldPath.Child("validation"))...) allErrs = append(allErrs, validateCustomResourceDefinitionValidation(spec.Validation, mustBeStructural, hasAnyStatusEnabled(spec), opts, fldPath.Child("validation"))...)
allErrs = append(allErrs, ValidateCustomResourceDefinitionSubresources(spec.Subresources, fldPath.Child("subresources"))...) allErrs = append(allErrs, ValidateCustomResourceDefinitionSubresources(spec.Subresources, fldPath.Child("subresources"))...)
for i := range spec.AdditionalPrinterColumns { for i := range spec.AdditionalPrinterColumns {
@ -244,7 +264,7 @@ func validateCustomResourceDefinitionSpec(spec *apiextensions.CustomResourceDefi
if (spec.Conversion != nil && spec.Conversion.Strategy != apiextensions.NoneConverter) && (spec.PreserveUnknownFields == nil || *spec.PreserveUnknownFields) { if (spec.Conversion != nil && spec.Conversion.Strategy != apiextensions.NoneConverter) && (spec.PreserveUnknownFields == nil || *spec.PreserveUnknownFields) {
allErrs = append(allErrs, field.Invalid(fldPath.Child("conversion").Child("strategy"), spec.Conversion.Strategy, "must be None if spec.preserveUnknownFields is true")) allErrs = append(allErrs, field.Invalid(fldPath.Child("conversion").Child("strategy"), spec.Conversion.Strategy, "must be None if spec.preserveUnknownFields is true"))
} }
allErrs = append(allErrs, validateCustomResourceConversion(spec.Conversion, requireRecognizedVersion, fldPath.Child("conversion"))...) allErrs = append(allErrs, validateCustomResourceConversion(spec.Conversion, opts.requireRecognizedConversionReviewVersion, fldPath.Child("conversion"))...)
return allErrs return allErrs
} }
@ -359,16 +379,11 @@ func validateCustomResourceConversion(conversion *apiextensions.CustomResourceCo
return allErrs return allErrs
} }
// ValidateCustomResourceDefinitionSpecUpdate statically validates // validateCustomResourceDefinitionSpecUpdate statically validates
func ValidateCustomResourceDefinitionSpecUpdate(spec, oldSpec *apiextensions.CustomResourceDefinitionSpec, established bool, fldPath *field.Path) field.ErrorList { func validateCustomResourceDefinitionSpecUpdate(spec, oldSpec *apiextensions.CustomResourceDefinitionSpec, opts validationOptions, fldPath *field.Path) field.ErrorList {
requireRecognizedVersion := oldSpec.Conversion == nil || hasValidConversionReviewVersionOrEmpty(oldSpec.Conversion.ConversionReviewVersions) allErrs := validateCustomResourceDefinitionSpec(spec, opts, fldPath)
// find out whether any schema had default before. Then we keep allowing it. if opts.requireImmutableNames {
allowDefaults := utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceDefaulting) || specHasDefaults(oldSpec)
allErrs := validateCustomResourceDefinitionSpec(spec, requireRecognizedVersion, allowDefaults, fldPath)
if established {
// these effect the storage and cannot be changed therefore // these effect the storage and cannot be changed therefore
allErrs = append(allErrs, genericvalidation.ValidateImmutableField(spec.Scope, oldSpec.Scope, fldPath.Child("scope"))...) 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"))...) allErrs = append(allErrs, genericvalidation.ValidateImmutableField(spec.Names.Kind, oldSpec.Names.Kind, fldPath.Child("names", "kind"))...)
@ -573,8 +588,8 @@ type specStandardValidator interface {
withInsideResourceMeta() specStandardValidator withInsideResourceMeta() specStandardValidator
} }
// ValidateCustomResourceDefinitionValidation statically validates // validateCustomResourceDefinitionValidation statically validates
func ValidateCustomResourceDefinitionValidation(customResourceValidation *apiextensions.CustomResourceValidation, mustBeStructural, statusSubresourceEnabled, allowDefaults bool, fldPath *field.Path) field.ErrorList { func validateCustomResourceDefinitionValidation(customResourceValidation *apiextensions.CustomResourceValidation, mustBeStructural, statusSubresourceEnabled bool, opts validationOptions, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{} allErrs := field.ErrorList{}
if customResourceValidation == nil { if customResourceValidation == nil {
@ -615,7 +630,7 @@ func ValidateCustomResourceDefinitionValidation(customResourceValidation *apiext
} }
openAPIV3Schema := &specStandardValidatorV3{ openAPIV3Schema := &specStandardValidatorV3{
allowDefaults: allowDefaults, allowDefaults: opts.allowDefaults,
} }
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(schema, fldPath.Child("openAPIV3Schema"), openAPIV3Schema, true)...) allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(schema, fldPath.Child("openAPIV3Schema"), openAPIV3Schema, true)...)
@ -929,6 +944,14 @@ func allowedAtRootSchema(field string) bool {
return false return false
} }
// allowDefaults returns true if the defaulting feature is enabled and the request group version allows adding defaults
func allowDefaults(requestGV schema.GroupVersion) bool {
if !utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceDefaulting) {
return false
}
return true
}
func specHasDefaults(spec *apiextensions.CustomResourceDefinitionSpec) bool { func specHasDefaults(spec *apiextensions.CustomResourceDefinitionSpec) bool {
if spec.Validation != nil && schemaHasDefaults(spec.Validation.OpenAPIV3Schema) { if spec.Validation != nil && schemaHasDefaults(spec.Validation.OpenAPIV3Schema) {
return true return true
@ -1094,3 +1117,44 @@ func schemaHasKubernetesExtensions(s *apiextensions.JSONSchemaProps) bool {
return false return false
} }
// validateAPIApproval returns a list of errors if the API approval annotation isn't valid
func validateAPIApproval(newCRD, oldCRD *apiextensions.CustomResourceDefinition, requestGV schema.GroupVersion) field.ErrorList {
// check to see if we need confirm API approval for kube group.
if requestGV == v1beta1.SchemeGroupVersion {
// no-op for compatibility with v1beta1
return nil
}
if !apihelpers.IsProtectedCommunityGroup(newCRD.Spec.Group) {
// no-op for non-protected groups
return nil
}
// default to a state that allows missing values to continue to be missing
var oldApprovalState *apihelpers.APIApprovalState
if oldCRD != nil {
t, _ := apihelpers.GetAPIApprovalState(oldCRD.Annotations)
oldApprovalState = &t
}
newApprovalState, reason := apihelpers.GetAPIApprovalState(newCRD.Annotations)
// if the approval state hasn't changed, never fail on approval validation
// this is allowed so that a v1 client that is simply updating spec and not mutating this value doesn't get rejected. Imagine a controller controlling a CRD spec.
if oldApprovalState != nil && *oldApprovalState == newApprovalState {
return nil
}
// in v1, we require valid approval strings
switch newApprovalState {
case apihelpers.APIApprovalInvalid:
return field.ErrorList{field.Invalid(field.NewPath("metadata", "annotations").Key(v1beta1.KubeAPIApprovedAnnotation), newCRD.Annotations[v1beta1.KubeAPIApprovedAnnotation], reason)}
case apihelpers.APIApprovalMissing:
return field.ErrorList{field.Required(field.NewPath("metadata", "annotations").Key(v1beta1.KubeAPIApprovedAnnotation), reason)}
case apihelpers.APIApproved, apihelpers.APIApprovalBypassed:
// success
return nil
default:
return field.ErrorList{field.Invalid(field.NewPath("metadata", "annotations").Key(v1beta1.KubeAPIApprovedAnnotation), newCRD.Annotations[v1beta1.KubeAPIApprovedAnnotation], reason)}
}
}

View File

@ -28,6 +28,7 @@ import (
"k8s.io/apimachinery/pkg/api/apitesting/fuzzer" "k8s.io/apimachinery/pkg/api/apitesting/fuzzer"
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"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apimachinery/pkg/util/json" "k8s.io/apimachinery/pkg/util/json"
"k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/apimachinery/pkg/util/validation/field"
@ -78,6 +79,7 @@ func TestValidateCustomResourceDefinition(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
resource *apiextensions.CustomResourceDefinition resource *apiextensions.CustomResourceDefinition
requestGV schema.GroupVersion
errors []validationMatch errors []validationMatch
enabledFeatures []featuregate.Feature enabledFeatures []featuregate.Feature
}{ }{
@ -315,6 +317,7 @@ func TestValidateCustomResourceDefinition(t *testing.T) {
StoredVersions: []string{"version"}, StoredVersions: []string{"version"},
}, },
}, },
requestGV: apiextensionsv1beta1.SchemeGroupVersion,
errors: []validationMatch{ errors: []validationMatch{
forbidden("spec", "conversion", "webhookClientConfig"), forbidden("spec", "conversion", "webhookClientConfig"),
}, },
@ -354,6 +357,7 @@ func TestValidateCustomResourceDefinition(t *testing.T) {
StoredVersions: []string{"version"}, StoredVersions: []string{"version"},
}, },
}, },
requestGV: apiextensionsv1beta1.SchemeGroupVersion,
errors: []validationMatch{ errors: []validationMatch{
forbidden("spec", "conversion", "conversionReviewVersions"), forbidden("spec", "conversion", "conversionReviewVersions"),
}, },
@ -665,7 +669,8 @@ func TestValidateCustomResourceDefinition(t *testing.T) {
StoredVersions: []string{"version1"}, StoredVersions: []string{"version1"},
}, },
}, },
errors: []validationMatch{}, requestGV: apiextensionsv1beta1.SchemeGroupVersion,
errors: []validationMatch{},
}, },
{ {
name: "webhook conversion without preserveUnknownFields=false", name: "webhook conversion without preserveUnknownFields=false",
@ -705,6 +710,7 @@ func TestValidateCustomResourceDefinition(t *testing.T) {
StoredVersions: []string{"version1"}, StoredVersions: []string{"version1"},
}, },
}, },
requestGV: apiextensionsv1beta1.SchemeGroupVersion,
errors: []validationMatch{ errors: []validationMatch{
invalid("spec", "conversion", "strategy"), invalid("spec", "conversion", "strategy"),
}, },
@ -788,6 +794,7 @@ func TestValidateCustomResourceDefinition(t *testing.T) {
StoredVersions: []string{"version"}, StoredVersions: []string{"version"},
}, },
}, },
requestGV: apiextensionsv1beta1.SchemeGroupVersion,
errors: []validationMatch{ errors: []validationMatch{
invalid("spec", "versions"), invalid("spec", "versions"),
}, },
@ -826,6 +833,7 @@ func TestValidateCustomResourceDefinition(t *testing.T) {
StoredVersions: []string{"version"}, StoredVersions: []string{"version"},
}, },
}, },
requestGV: apiextensionsv1beta1.SchemeGroupVersion,
errors: []validationMatch{ errors: []validationMatch{
invalid("spec", "versions"), invalid("spec", "versions"),
invalid("status", "storedVersions"), invalid("status", "storedVersions"),
@ -865,6 +873,7 @@ func TestValidateCustomResourceDefinition(t *testing.T) {
StoredVersions: []string{"version"}, StoredVersions: []string{"version"},
}, },
}, },
requestGV: apiextensionsv1beta1.SchemeGroupVersion,
errors: []validationMatch{ errors: []validationMatch{
invalid("status", "storedVersions"), invalid("status", "storedVersions"),
}, },
@ -898,6 +907,7 @@ func TestValidateCustomResourceDefinition(t *testing.T) {
StoredVersions: []string{}, StoredVersions: []string{},
}, },
}, },
requestGV: apiextensionsv1beta1.SchemeGroupVersion,
errors: []validationMatch{ errors: []validationMatch{
invalid("status", "storedVersions"), invalid("status", "storedVersions"),
}, },
@ -914,6 +924,7 @@ func TestValidateCustomResourceDefinition(t *testing.T) {
PreserveUnknownFields: pointer.BoolPtr(true), PreserveUnknownFields: pointer.BoolPtr(true),
}, },
}, },
requestGV: apiextensionsv1beta1.SchemeGroupVersion,
errors: []validationMatch{ errors: []validationMatch{
invalid("status", "storedVersions"), invalid("status", "storedVersions"),
invalid("metadata", "name"), invalid("metadata", "name"),
@ -966,6 +977,7 @@ func TestValidateCustomResourceDefinition(t *testing.T) {
}, },
}, },
}, },
requestGV: apiextensionsv1beta1.SchemeGroupVersion,
errors: []validationMatch{ errors: []validationMatch{
invalid("status", "storedVersions"), invalid("status", "storedVersions"),
invalid("metadata", "name"), invalid("metadata", "name"),
@ -1014,6 +1026,7 @@ func TestValidateCustomResourceDefinition(t *testing.T) {
StoredVersions: []string{"version"}, StoredVersions: []string{"version"},
}, },
}, },
requestGV: apiextensionsv1beta1.SchemeGroupVersion,
errors: []validationMatch{ errors: []validationMatch{
invalid("metadata", "name"), invalid("metadata", "name"),
invalid("spec", "group"), invalid("spec", "group"),
@ -1054,6 +1067,7 @@ func TestValidateCustomResourceDefinition(t *testing.T) {
StoredVersions: []string{"version"}, StoredVersions: []string{"version"},
}, },
}, },
requestGV: apiextensionsv1beta1.SchemeGroupVersion,
errors: []validationMatch{ errors: []validationMatch{
forbidden("spec", "validation", "openAPIV3Schema", "additionalProperties"), forbidden("spec", "validation", "openAPIV3Schema", "additionalProperties"),
}, },
@ -1092,7 +1106,8 @@ func TestValidateCustomResourceDefinition(t *testing.T) {
StoredVersions: []string{"version"}, StoredVersions: []string{"version"},
}, },
}, },
errors: []validationMatch{}, requestGV: apiextensionsv1beta1.SchemeGroupVersion,
errors: []validationMatch{},
}, },
{ {
name: "per-version fields may not all be set to identical values (top-level field should be used instead)", name: "per-version fields may not all be set to identical values (top-level field should be used instead)",
@ -1136,6 +1151,7 @@ func TestValidateCustomResourceDefinition(t *testing.T) {
StoredVersions: []string{"version"}, StoredVersions: []string{"version"},
}, },
}, },
requestGV: apiextensionsv1beta1.SchemeGroupVersion,
errors: []validationMatch{ errors: []validationMatch{
// Per-version schema/subresources/columns may not all be set to identical values. // Per-version schema/subresources/columns may not all be set to identical values.
// Note that the test will fail if we de-duplicate the expected errors below. // Note that the test will fail if we de-duplicate the expected errors below.
@ -1421,6 +1437,7 @@ func TestValidateCustomResourceDefinition(t *testing.T) {
StoredVersions: []string{"version0"}, StoredVersions: []string{"version0"},
}, },
}, },
requestGV: apiextensionsv1beta1.SchemeGroupVersion,
errors: []validationMatch{ errors: []validationMatch{
invalid("spec", "versions[3]", "subresources", "scale", "labelSelectorPath"), invalid("spec", "versions[3]", "subresources", "scale", "labelSelectorPath"),
}, },
@ -1457,6 +1474,7 @@ func TestValidateCustomResourceDefinition(t *testing.T) {
StoredVersions: []string{"version"}, StoredVersions: []string{"version"},
}, },
}, },
requestGV: apiextensionsv1beta1.SchemeGroupVersion,
errors: []validationMatch{ errors: []validationMatch{
forbidden("spec", "validation", "openAPIV3Schema", "properties[a]", "default"), // disabled feature-gate forbidden("spec", "validation", "openAPIV3Schema", "properties[a]", "default"), // disabled feature-gate
}, },
@ -1491,6 +1509,7 @@ func TestValidateCustomResourceDefinition(t *testing.T) {
StoredVersions: []string{"version"}, StoredVersions: []string{"version"},
}, },
}, },
requestGV: apiextensionsv1beta1.SchemeGroupVersion,
errors: []validationMatch{ errors: []validationMatch{
required("spec", "validation", "openAPIV3Schema", "type"), required("spec", "validation", "openAPIV3Schema", "type"),
}, },
@ -1525,6 +1544,7 @@ func TestValidateCustomResourceDefinition(t *testing.T) {
StoredVersions: []string{"version"}, StoredVersions: []string{"version"},
}, },
}, },
requestGV: apiextensionsv1beta1.SchemeGroupVersion,
errors: []validationMatch{ errors: []validationMatch{
required("spec", "validation", "openAPIV3Schema", "type"), required("spec", "validation", "openAPIV3Schema", "type"),
}, },
@ -1563,6 +1583,7 @@ func TestValidateCustomResourceDefinition(t *testing.T) {
StoredVersions: []string{"version"}, StoredVersions: []string{"version"},
}, },
}, },
requestGV: apiextensionsv1beta1.SchemeGroupVersion,
errors: []validationMatch{ errors: []validationMatch{
required("spec", "validation", "openAPIV3Schema", "type"), required("spec", "validation", "openAPIV3Schema", "type"),
}, },
@ -1626,6 +1647,7 @@ func TestValidateCustomResourceDefinition(t *testing.T) {
StoredVersions: []string{"version"}, StoredVersions: []string{"version"},
}, },
}, },
requestGV: apiextensionsv1beta1.SchemeGroupVersion,
errors: []validationMatch{ errors: []validationMatch{
forbidden("spec", "validation", "openAPIV3Schema", "properties[embedded]", "properties[metadata]", "x-kubernetes-embedded-resource"), forbidden("spec", "validation", "openAPIV3Schema", "properties[embedded]", "properties[metadata]", "x-kubernetes-embedded-resource"),
forbidden("spec", "validation", "openAPIV3Schema", "properties[embedded]", "properties[apiVersion]", "properties[foo]", "x-kubernetes-embedded-resource"), forbidden("spec", "validation", "openAPIV3Schema", "properties[embedded]", "properties[apiVersion]", "properties[foo]", "x-kubernetes-embedded-resource"),
@ -2388,7 +2410,7 @@ func TestValidateCustomResourceDefinition(t *testing.T) {
if tc.resource.Spec.Conversion != nil && tc.resource.Spec.Conversion.Strategy == apiextensions.WebhookConverter && len(tc.resource.Spec.Conversion.ConversionReviewVersions) == 0 { if tc.resource.Spec.Conversion != nil && tc.resource.Spec.Conversion.Strategy == apiextensions.WebhookConverter && len(tc.resource.Spec.Conversion.ConversionReviewVersions) == 0 {
tc.resource.Spec.Conversion.ConversionReviewVersions = []string{"v1beta1"} tc.resource.Spec.Conversion.ConversionReviewVersions = []string{"v1beta1"}
} }
errs := ValidateCustomResourceDefinition(tc.resource) errs := ValidateCustomResourceDefinition(tc.resource, tc.requestGV)
seenErrs := make([]bool, len(errs)) seenErrs := make([]bool, len(errs))
for _, expectedError := range tc.errors { for _, expectedError := range tc.errors {
@ -2420,6 +2442,7 @@ func TestValidateCustomResourceDefinitionUpdate(t *testing.T) {
name string name string
old *apiextensions.CustomResourceDefinition old *apiextensions.CustomResourceDefinition
resource *apiextensions.CustomResourceDefinition resource *apiextensions.CustomResourceDefinition
requestGV schema.GroupVersion
errors []validationMatch errors []validationMatch
enabledFeatures []featuregate.Feature enabledFeatures []featuregate.Feature
}{ }{
@ -3350,7 +3373,8 @@ func TestValidateCustomResourceDefinitionUpdate(t *testing.T) {
StoredVersions: []string{"version"}, StoredVersions: []string{"version"},
}, },
}, },
errors: []validationMatch{}, requestGV: apiextensionsv1beta1.SchemeGroupVersion,
errors: []validationMatch{},
}, },
{ {
name: "setting defaults with enabled feature gate", name: "setting defaults with enabled feature gate",
@ -3528,7 +3552,7 @@ func TestValidateCustomResourceDefinitionUpdate(t *testing.T) {
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, gate, true)() defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, gate, true)()
} }
errs := ValidateCustomResourceDefinitionUpdate(tc.resource, tc.old) errs := ValidateCustomResourceDefinitionUpdate(tc.resource, tc.old, tc.requestGV)
seenErrs := make([]bool, len(errs)) seenErrs := make([]bool, len(errs))
for _, expectedError := range tc.errors { for _, expectedError := range tc.errors {
@ -3561,6 +3585,7 @@ func TestValidateCustomResourceDefinitionValidation(t *testing.T) {
input apiextensions.CustomResourceValidation input apiextensions.CustomResourceValidation
mustBeStructural bool mustBeStructural bool
statusEnabled bool statusEnabled bool
opts validationOptions
wantError bool wantError bool
}{ }{
{ {
@ -3702,7 +3727,7 @@ func TestValidateCustomResourceDefinitionValidation(t *testing.T) {
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
got := ValidateCustomResourceDefinitionValidation(&tt.input, tt.mustBeStructural, tt.statusEnabled, false, field.NewPath("spec", "validation")) got := validateCustomResourceDefinitionValidation(&tt.input, tt.mustBeStructural, tt.statusEnabled, tt.opts, field.NewPath("spec", "validation"))
if !tt.wantError && len(got) > 0 { if !tt.wantError && len(got) > 0 {
t.Errorf("Expected no error, but got: %v", got) t.Errorf("Expected no error, but got: %v", got)
} else if tt.wantError && len(got) == 0 { } else if tt.wantError && len(got) == 0 {

View File

@ -11,9 +11,7 @@ go_library(
importmap = "k8s.io/kubernetes/vendor/k8s.io/apiextensions-apiserver/pkg/registry/customresourcedefinition", importmap = "k8s.io/kubernetes/vendor/k8s.io/apiextensions-apiserver/pkg/registry/customresourcedefinition",
importpath = "k8s.io/apiextensions-apiserver/pkg/registry/customresourcedefinition", importpath = "k8s.io/apiextensions-apiserver/pkg/registry/customresourcedefinition",
deps = [ deps = [
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apihelpers: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/v1beta1:go_default_library",
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation:go_default_library", "//staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation: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/equality:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/api/equality:go_default_library",
@ -22,6 +20,7 @@ go_library(
"//staging/src/k8s.io/apimachinery/pkg/fields:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/fields:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/labels:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/labels: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/util/validation/field:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/validation/field:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/endpoints/request:go_default_library", "//staging/src/k8s.io/apiserver/pkg/endpoints/request:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/registry/generic:go_default_library", "//staging/src/k8s.io/apiserver/pkg/registry/generic:go_default_library",
@ -55,12 +54,14 @@ go_test(
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/v1beta1: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/validation: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/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/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/field:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/validation/field:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/endpoints/request: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",
"//staging/src/k8s.io/component-base/featuregate/testing:go_default_library", "//staging/src/k8s.io/component-base/featuregate/testing:go_default_library",
"//vendor/k8s.io/utils/pointer:go_default_library",
], ],
) )

View File

@ -20,17 +20,16 @@ import (
"context" "context"
"fmt" "fmt"
"k8s.io/apiextensions-apiserver/pkg/apihelpers"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation" "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation"
apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features" apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features"
apiequality "k8s.io/apimachinery/pkg/api/equality" apiequality "k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/apiserver/pkg/endpoints/request" genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/registry/generic" "k8s.io/apiserver/pkg/registry/generic"
"k8s.io/apiserver/pkg/storage" "k8s.io/apiserver/pkg/storage"
"k8s.io/apiserver/pkg/storage/names" "k8s.io/apiserver/pkg/storage/names"
@ -101,8 +100,12 @@ func (strategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) {
// Validate validates a new CustomResourceDefinition. // Validate validates a new CustomResourceDefinition.
func (strategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList { func (strategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList {
fieldErrors := validation.ValidateCustomResourceDefinition(obj.(*apiextensions.CustomResourceDefinition)) var groupVersion schema.GroupVersion
return append(fieldErrors, validateAPIApproval(ctx, obj.(*apiextensions.CustomResourceDefinition), nil)...) if requestInfo, found := genericapirequest.RequestInfoFrom(ctx); found {
groupVersion = schema.GroupVersion{Group: requestInfo.APIGroup, Version: requestInfo.APIVersion}
}
return validation.ValidateCustomResourceDefinition(obj.(*apiextensions.CustomResourceDefinition), groupVersion)
} }
// AllowCreateOnUpdate is false for CustomResourceDefinition; this means a POST is // AllowCreateOnUpdate is false for CustomResourceDefinition; this means a POST is
@ -122,47 +125,12 @@ func (strategy) Canonicalize(obj runtime.Object) {
// ValidateUpdate is the default update validation for an end user updating status. // ValidateUpdate is the default update validation for an end user updating status.
func (strategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList { func (strategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList {
fieldErrors := validation.ValidateCustomResourceDefinitionUpdate(obj.(*apiextensions.CustomResourceDefinition), old.(*apiextensions.CustomResourceDefinition)) var groupVersion schema.GroupVersion
if requestInfo, found := genericapirequest.RequestInfoFrom(ctx); found {
return append(fieldErrors, validateAPIApproval(ctx, obj.(*apiextensions.CustomResourceDefinition), old.(*apiextensions.CustomResourceDefinition))...) groupVersion = schema.GroupVersion{Group: requestInfo.APIGroup, Version: requestInfo.APIVersion}
}
// validateAPIApproval returns a list of errors if the API approval annotation isn't valid
func validateAPIApproval(ctx context.Context, newCRD, oldCRD *apiextensions.CustomResourceDefinition) field.ErrorList {
// check to see if we need confirm API approval for kube group. Do nothing for non-protected groups and do nothing in v1beta1.
if requestInfo, ok := request.RequestInfoFrom(ctx); !ok || requestInfo.APIVersion == "v1beta1" {
return field.ErrorList{}
}
if !apihelpers.IsProtectedCommunityGroup(newCRD.Spec.Group) {
return field.ErrorList{}
} }
// default to a state that allows missing values to continue to be missing return validation.ValidateCustomResourceDefinitionUpdate(obj.(*apiextensions.CustomResourceDefinition), old.(*apiextensions.CustomResourceDefinition), groupVersion)
var oldApprovalState *apihelpers.APIApprovalState
if oldCRD != nil {
t, _ := apihelpers.GetAPIApprovalState(oldCRD.Annotations)
oldApprovalState = &t
}
newApprovalState, reason := apihelpers.GetAPIApprovalState(newCRD.Annotations)
// if the approval state hasn't changed, never fail on approval validation
// this is allowed so that a v1 client that is simply updating spec and not mutating this value doesn't get rejected. Imagine a controller controlling a CRD spec.
if oldApprovalState != nil && *oldApprovalState == newApprovalState {
return field.ErrorList{}
}
// in v1, we require valid approval strings
switch newApprovalState {
case apihelpers.APIApprovalInvalid:
return field.ErrorList{field.Invalid(field.NewPath("metadata", "annotations").Key(v1beta1.KubeAPIApprovedAnnotation), newCRD.Annotations[v1beta1.KubeAPIApprovedAnnotation], reason)}
case apihelpers.APIApprovalMissing:
return field.ErrorList{field.Required(field.NewPath("metadata", "annotations").Key(v1beta1.KubeAPIApprovedAnnotation), reason)}
case apihelpers.APIApproved, apihelpers.APIApprovalBypassed:
// success
return field.ErrorList{}
default:
return field.ErrorList{field.Invalid(field.NewPath("metadata", "annotations").Key(v1beta1.KubeAPIApprovedAnnotation), newCRD.Annotations[v1beta1.KubeAPIApprovedAnnotation], reason)}
}
} }
type statusStrategy struct { type statusStrategy struct {

View File

@ -17,20 +17,21 @@ limitations under the License.
package customresourcedefinition package customresourcedefinition
import ( import (
"context"
"fmt" "fmt"
"reflect" "reflect"
"testing" "testing"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation"
apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features" apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/diff" "k8s.io/apimachinery/pkg/util/diff"
"k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/apiserver/pkg/endpoints/request"
utilfeature "k8s.io/apiserver/pkg/util/feature" utilfeature "k8s.io/apiserver/pkg/util/feature"
featuregatetesting "k8s.io/component-base/featuregate/testing" featuregatetesting "k8s.io/component-base/featuregate/testing"
"k8s.io/utils/pointer"
) )
func TestDropDisableFieldsCustomResourceDefinition(t *testing.T) { func TestDropDisableFieldsCustomResourceDefinition(t *testing.T) {
@ -517,6 +518,9 @@ func TestValidateAPIApproval(t *testing.T) {
annotationValue: "invalid", annotationValue: "invalid",
validateError: func(t *testing.T, errors field.ErrorList) { validateError: func(t *testing.T, errors field.ErrorList) {
t.Helper() t.Helper()
if len(errors) == 0 {
t.Fatal("expected errors, got none")
}
if e, a := `metadata.annotations[api-approved.kubernetes.io]: Invalid value: "invalid": protected groups must have approval annotation "api-approved.kubernetes.io" with either a URL or a reason starting with "unapproved", see https://github.com/kubernetes/enhancements/pull/1111`, errors.ToAggregate().Error(); e != a { if e, a := `metadata.annotations[api-approved.kubernetes.io]: Invalid value: "invalid": protected groups must have approval annotation "api-approved.kubernetes.io" with either a URL or a reason starting with "unapproved", see https://github.com/kubernetes/enhancements/pull/1111`, errors.ToAggregate().Error(); e != a {
t.Fatal(errors) t.Fatal(errors)
} }
@ -538,6 +542,9 @@ func TestValidateAPIApproval(t *testing.T) {
oldAnnotationValue: strPtr("invalid"), oldAnnotationValue: strPtr("invalid"),
validateError: func(t *testing.T, errors field.ErrorList) { validateError: func(t *testing.T, errors field.ErrorList) {
t.Helper() t.Helper()
if len(errors) == 0 {
t.Fatal("expected errors, got none")
}
if e, a := `metadata.annotations[api-approved.kubernetes.io]: Required value: protected groups must have approval annotation "api-approved.kubernetes.io", see https://github.com/kubernetes/enhancements/pull/1111`, errors.ToAggregate().Error(); e != a { if e, a := `metadata.annotations[api-approved.kubernetes.io]: Required value: protected groups must have approval annotation "api-approved.kubernetes.io", see https://github.com/kubernetes/enhancements/pull/1111`, errors.ToAggregate().Error(); e != a {
t.Fatal(errors) t.Fatal(errors)
} }
@ -551,6 +558,9 @@ func TestValidateAPIApproval(t *testing.T) {
oldAnnotationValue: strPtr(""), oldAnnotationValue: strPtr(""),
validateError: func(t *testing.T, errors field.ErrorList) { validateError: func(t *testing.T, errors field.ErrorList) {
t.Helper() t.Helper()
if len(errors) == 0 {
t.Fatal("expected errors, got none")
}
if e, a := `metadata.annotations[api-approved.kubernetes.io]: Invalid value: "invalid": protected groups must have approval annotation "api-approved.kubernetes.io" with either a URL or a reason starting with "unapproved", see https://github.com/kubernetes/enhancements/pull/1111`, errors.ToAggregate().Error(); e != a { if e, a := `metadata.annotations[api-approved.kubernetes.io]: Invalid value: "invalid": protected groups must have approval annotation "api-approved.kubernetes.io" with either a URL or a reason starting with "unapproved", see https://github.com/kubernetes/enhancements/pull/1111`, errors.ToAggregate().Error(); e != a {
t.Fatal(errors) t.Fatal(errors)
} }
@ -563,6 +573,9 @@ func TestValidateAPIApproval(t *testing.T) {
annotationValue: "", annotationValue: "",
validateError: func(t *testing.T, errors field.ErrorList) { validateError: func(t *testing.T, errors field.ErrorList) {
t.Helper() t.Helper()
if len(errors) == 0 {
t.Fatal("expected errors, got none")
}
if e, a := `metadata.annotations[api-approved.kubernetes.io]: Required value: protected groups must have approval annotation "api-approved.kubernetes.io", see https://github.com/kubernetes/enhancements/pull/1111`, errors.ToAggregate().Error(); e != a { if e, a := `metadata.annotations[api-approved.kubernetes.io]: Required value: protected groups must have approval annotation "api-approved.kubernetes.io", see https://github.com/kubernetes/enhancements/pull/1111`, errors.ToAggregate().Error(); e != a {
t.Fatal(errors) t.Fatal(errors)
} }
@ -597,6 +610,9 @@ func TestValidateAPIApproval(t *testing.T) {
annotationValue: "invalid", annotationValue: "invalid",
validateError: func(t *testing.T, errors field.ErrorList) { validateError: func(t *testing.T, errors field.ErrorList) {
t.Helper() t.Helper()
if len(errors) == 0 {
t.Fatal("expected errors, got none")
}
if e, a := `metadata.annotations[api-approved.kubernetes.io]: Invalid value: "invalid": protected groups must have approval annotation "api-approved.kubernetes.io" with either a URL or a reason starting with "unapproved", see https://github.com/kubernetes/enhancements/pull/1111`, errors.ToAggregate().Error(); e != a { if e, a := `metadata.annotations[api-approved.kubernetes.io]: Invalid value: "invalid": protected groups must have approval annotation "api-approved.kubernetes.io" with either a URL or a reason starting with "unapproved", see https://github.com/kubernetes/enhancements/pull/1111`, errors.ToAggregate().Error(); e != a {
t.Fatal(errors) t.Fatal(errors)
} }
@ -606,24 +622,48 @@ func TestValidateAPIApproval(t *testing.T) {
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
ctx := request.WithRequestInfo(context.TODO(), &request.RequestInfo{APIVersion: test.version})
crd := &apiextensions.CustomResourceDefinition{ crd := &apiextensions.CustomResourceDefinition{
ObjectMeta: metav1.ObjectMeta{Name: "foo", Annotations: map[string]string{v1beta1.KubeAPIApprovedAnnotation: test.annotationValue}}, ObjectMeta: metav1.ObjectMeta{Name: "foos." + test.group, Annotations: map[string]string{v1beta1.KubeAPIApprovedAnnotation: test.annotationValue}, ResourceVersion: "1"},
Spec: apiextensions.CustomResourceDefinitionSpec{ Spec: apiextensions.CustomResourceDefinitionSpec{
Group: test.group, Group: test.group,
Scope: apiextensions.NamespaceScoped,
Version: "v1",
Versions: []apiextensions.CustomResourceDefinitionVersion{{Name: "v1", Storage: true, Served: true}},
Names: apiextensions.CustomResourceDefinitionNames{Plural: "foos", Singular: "foo", Kind: "Foo", ListKind: "FooList"},
Validation: &apiextensions.CustomResourceValidation{
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{Type: "object", XPreserveUnknownFields: pointer.BoolPtr(true)},
},
},
Status: apiextensions.CustomResourceDefinitionStatus{
StoredVersions: []string{"v1"},
}, },
} }
var oldCRD *apiextensions.CustomResourceDefinition var oldCRD *apiextensions.CustomResourceDefinition
if test.oldAnnotationValue != nil { if test.oldAnnotationValue != nil {
oldCRD = &apiextensions.CustomResourceDefinition{ oldCRD = &apiextensions.CustomResourceDefinition{
ObjectMeta: metav1.ObjectMeta{Name: "foo", Annotations: map[string]string{v1beta1.KubeAPIApprovedAnnotation: *test.oldAnnotationValue}}, ObjectMeta: metav1.ObjectMeta{Name: "foos." + test.group, Annotations: map[string]string{v1beta1.KubeAPIApprovedAnnotation: *test.oldAnnotationValue}, ResourceVersion: "1"},
Spec: apiextensions.CustomResourceDefinitionSpec{ Spec: apiextensions.CustomResourceDefinitionSpec{
Group: test.group, Group: test.group,
Scope: apiextensions.NamespaceScoped,
Version: "v1",
Versions: []apiextensions.CustomResourceDefinitionVersion{{Name: "v1", Storage: true, Served: true}},
Names: apiextensions.CustomResourceDefinitionNames{Plural: "foos", Singular: "foo", Kind: "Foo", ListKind: "FooList"},
Validation: &apiextensions.CustomResourceValidation{
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{Type: "object", XPreserveUnknownFields: pointer.BoolPtr(true)},
},
},
Status: apiextensions.CustomResourceDefinitionStatus{
StoredVersions: []string{"v1"},
}, },
} }
} }
actual := validateAPIApproval(ctx, crd, oldCRD) var actual field.ErrorList
if oldCRD == nil {
actual = validation.ValidateCustomResourceDefinition(crd, schema.GroupVersion{Group: "apiextensions.k8s.io", Version: test.version})
} else {
actual = validation.ValidateCustomResourceDefinitionUpdate(crd, oldCRD, schema.GroupVersion{Group: "apiextensions.k8s.io", Version: test.version})
}
test.validateError(t, actual) test.validateError(t, actual)
}) })
} }