mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-23 11:50:44 +00:00
Merge pull request #81105 from liggitt/crd-validation-refactor
CRD validation refactor
This commit is contained in:
commit
35cacd44e6
@ -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",
|
||||||
|
@ -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)}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@ -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 {
|
||||||
|
@ -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)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user