...

Source file src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation/validation.go

Documentation: k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation

     1  /*
     2  Copyright 2017 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package validation
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"math"
    23  	"reflect"
    24  	"regexp"
    25  	"strings"
    26  	"sync"
    27  	"unicode"
    28  	"unicode/utf8"
    29  
    30  	celgo "github.com/google/cel-go/cel"
    31  
    32  	"k8s.io/apiextensions-apiserver/pkg/apihelpers"
    33  	"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
    34  	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    35  	apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
    36  	structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
    37  	"k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel"
    38  	structuraldefaulting "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/defaulting"
    39  	apiservervalidation "k8s.io/apiextensions-apiserver/pkg/apiserver/validation"
    40  	apiequality "k8s.io/apimachinery/pkg/api/equality"
    41  	genericvalidation "k8s.io/apimachinery/pkg/api/validation"
    42  	"k8s.io/apimachinery/pkg/util/sets"
    43  	utilvalidation "k8s.io/apimachinery/pkg/util/validation"
    44  	"k8s.io/apimachinery/pkg/util/validation/field"
    45  	celconfig "k8s.io/apiserver/pkg/apis/cel"
    46  	apiservercel "k8s.io/apiserver/pkg/cel"
    47  	"k8s.io/apiserver/pkg/cel/environment"
    48  	"k8s.io/apiserver/pkg/util/webhook"
    49  )
    50  
    51  var (
    52  	printerColumnDatatypes                = sets.NewString("integer", "number", "string", "boolean", "date")
    53  	customResourceColumnDefinitionFormats = sets.NewString("int32", "int64", "float", "double", "byte", "date", "date-time", "password")
    54  	openapiV3Types                        = sets.NewString("string", "number", "integer", "boolean", "array", "object")
    55  )
    56  
    57  const (
    58  	// StaticEstimatedCostLimit represents the largest-allowed static CEL cost on a per-expression basis.
    59  	StaticEstimatedCostLimit = 10000000
    60  	// StaticEstimatedCRDCostLimit represents the largest-allowed total cost for the x-kubernetes-validations rules of a CRD.
    61  	StaticEstimatedCRDCostLimit = 100000000
    62  
    63  	MaxSelectableFields = 8
    64  )
    65  
    66  var supportedValidationReason = sets.NewString(
    67  	string(apiextensions.FieldValueRequired),
    68  	string(apiextensions.FieldValueForbidden),
    69  	string(apiextensions.FieldValueInvalid),
    70  	string(apiextensions.FieldValueDuplicate),
    71  )
    72  
    73  // ValidateCustomResourceDefinition statically validates
    74  // context is passed for supporting context cancellation during cel validation when validating defaults
    75  func ValidateCustomResourceDefinition(ctx context.Context, obj *apiextensions.CustomResourceDefinition) field.ErrorList {
    76  	nameValidationFn := func(name string, prefix bool) []string {
    77  		ret := genericvalidation.NameIsDNSSubdomain(name, prefix)
    78  		requiredName := obj.Spec.Names.Plural + "." + obj.Spec.Group
    79  		if name != requiredName {
    80  			ret = append(ret, fmt.Sprintf(`must be spec.names.plural+"."+spec.group`))
    81  		}
    82  		return ret
    83  	}
    84  
    85  	opts := validationOptions{
    86  		allowDefaults:                            true,
    87  		requireRecognizedConversionReviewVersion: true,
    88  		requireImmutableNames:                    false,
    89  		requireOpenAPISchema:                     true,
    90  		requireValidPropertyType:                 true,
    91  		requireStructuralSchema:                  true,
    92  		requirePrunedDefaults:                    true,
    93  		requireAtomicSetType:                     true,
    94  		requireMapListKeysMapSetValidation:       true,
    95  		celEnvironmentSet:                        environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()),
    96  	}
    97  
    98  	allErrs := genericvalidation.ValidateObjectMeta(&obj.ObjectMeta, false, nameValidationFn, field.NewPath("metadata"))
    99  	allErrs = append(allErrs, validateCustomResourceDefinitionSpec(ctx, &obj.Spec, opts, field.NewPath("spec"))...)
   100  	allErrs = append(allErrs, ValidateCustomResourceDefinitionStatus(&obj.Status, field.NewPath("status"))...)
   101  	allErrs = append(allErrs, ValidateCustomResourceDefinitionStoredVersions(obj.Status.StoredVersions, obj.Spec.Versions, field.NewPath("status").Child("storedVersions"))...)
   102  	allErrs = append(allErrs, validateAPIApproval(obj, nil)...)
   103  	allErrs = append(allErrs, validatePreserveUnknownFields(obj, nil)...)
   104  	return allErrs
   105  }
   106  
   107  // validationOptions groups several validation options, to avoid passing multiple bool parameters to methods
   108  type validationOptions struct {
   109  	// allowDefaults permits the validation schema to contain default attributes
   110  	allowDefaults bool
   111  	// disallowDefaultsReason gives a reason as to why allowDefaults is false (for better user feedback)
   112  	disallowDefaultsReason string
   113  	// requireRecognizedConversionReviewVersion requires accepted webhook conversion versions to contain a recognized version
   114  	requireRecognizedConversionReviewVersion bool
   115  	// requireImmutableNames disables changing spec.names
   116  	requireImmutableNames bool
   117  	// requireOpenAPISchema requires an openapi V3 schema be specified
   118  	requireOpenAPISchema bool
   119  	// requireValidPropertyType requires property types specified in the validation schema to be valid openapi v3 types
   120  	requireValidPropertyType bool
   121  	// requireStructuralSchema indicates that any schemas present must be structural
   122  	requireStructuralSchema bool
   123  	// requirePrunedDefaults indicates that defaults must be pruned
   124  	requirePrunedDefaults bool
   125  	// requireAtomicSetType indicates that the items type for a x-kubernetes-list-type=set list must be atomic.
   126  	requireAtomicSetType bool
   127  	// requireMapListKeysMapSetValidation indicates that:
   128  	// 1. For x-kubernetes-list-type=map list, key fields are not nullable, and are required or have a default
   129  	// 2. For x-kubernetes-list-type=map or x-kubernetes-list-type=set list, the whole item must not be nullable.
   130  	requireMapListKeysMapSetValidation bool
   131  	// preexistingExpressions tracks which CEL expressions existed in an object before an update. May be nil for create.
   132  	preexistingExpressions preexistingExpressions
   133  	// versionsWithUnchangedSchemas tracks schemas of which versions are unchanged when updating a CRD.
   134  	// Does not apply to creation or deletion.
   135  	// Some checks use this to avoid rejecting previously accepted versions due to a control plane upgrade/downgrade.
   136  	versionsWithUnchangedSchemas sets.Set[string]
   137  	// suppressPerExpressionCost indicates whether CEL per-expression cost limit should be suppressed.
   138  	// It will be automatically set during Versions validation if the version is in versionsWithUnchangedSchemas.
   139  	suppressPerExpressionCost bool
   140  
   141  	celEnvironmentSet *environment.EnvSet
   142  }
   143  
   144  type preexistingExpressions struct {
   145  	rules              sets.Set[string]
   146  	messageExpressions sets.Set[string]
   147  }
   148  
   149  func (pe preexistingExpressions) RuleEnv(envSet *environment.EnvSet, expression string) *celgo.Env {
   150  	if pe.rules.Has(expression) {
   151  		return envSet.StoredExpressionsEnv()
   152  	}
   153  	return envSet.NewExpressionsEnv()
   154  }
   155  
   156  func (pe preexistingExpressions) MessageExpressionEnv(envSet *environment.EnvSet, expression string) *celgo.Env {
   157  	if pe.messageExpressions.Has(expression) {
   158  		return envSet.StoredExpressionsEnv()
   159  	}
   160  	return envSet.NewExpressionsEnv()
   161  }
   162  
   163  func findPreexistingExpressions(spec *apiextensions.CustomResourceDefinitionSpec) preexistingExpressions {
   164  	expressions := preexistingExpressions{rules: sets.New[string](), messageExpressions: sets.New[string]()}
   165  	if spec.Validation != nil && spec.Validation.OpenAPIV3Schema != nil {
   166  		findPreexistingExpressionsInSchema(spec.Validation.OpenAPIV3Schema, expressions)
   167  	}
   168  	for _, v := range spec.Versions {
   169  		if v.Schema != nil && v.Schema.OpenAPIV3Schema != nil {
   170  			findPreexistingExpressionsInSchema(v.Schema.OpenAPIV3Schema, expressions)
   171  		}
   172  	}
   173  	return expressions
   174  }
   175  
   176  func findPreexistingExpressionsInSchema(schema *apiextensions.JSONSchemaProps, expressions preexistingExpressions) {
   177  	SchemaHas(schema, func(s *apiextensions.JSONSchemaProps) bool {
   178  		for _, v := range s.XValidations {
   179  			expressions.rules.Insert(v.Rule)
   180  			if len(v.MessageExpression) > 0 {
   181  				expressions.messageExpressions.Insert(v.Rule)
   182  			}
   183  		}
   184  		return false
   185  	})
   186  }
   187  
   188  // findVersionsWithUnchangedSchemas finds each version that is in the new CRD object and differs from that of the old CRD object.
   189  // It returns a set of the names of mutated versions.
   190  // This function does not check for duplicated versions, top-level version not in versions, or coexistence of
   191  // top-level and per-version schemas, as further validations will check for these problems.
   192  func findVersionsWithUnchangedSchemas(obj, oldObject *apiextensions.CustomResourceDefinition) sets.Set[string] {
   193  	versionsWithUnchangedSchemas := sets.New[string]()
   194  	for _, version := range obj.Spec.Versions {
   195  		newSchema, err := apiextensions.GetSchemaForVersion(obj, version.Name)
   196  		if err != nil {
   197  			continue
   198  		}
   199  		oldSchema, err := apiextensions.GetSchemaForVersion(oldObject, version.Name)
   200  		if err != nil {
   201  			continue
   202  		}
   203  		if apiequality.Semantic.DeepEqual(newSchema, oldSchema) {
   204  			versionsWithUnchangedSchemas.Insert(version.Name)
   205  		}
   206  	}
   207  	return versionsWithUnchangedSchemas
   208  }
   209  
   210  // suppressExpressionCostForUnchangedSchema returns a copy of opts with suppressPerExpressionCost set to true if
   211  // the specified version's schema is unchanged.
   212  func suppressExpressionCostForUnchangedSchema(opts validationOptions, version string) validationOptions {
   213  	if opts.versionsWithUnchangedSchemas.Has(version) {
   214  		opts.suppressPerExpressionCost = true
   215  	}
   216  	return opts
   217  }
   218  
   219  // ValidateCustomResourceDefinitionUpdate statically validates
   220  // context is passed for supporting context cancellation during cel validation when validating defaults
   221  func ValidateCustomResourceDefinitionUpdate(ctx context.Context, obj, oldObj *apiextensions.CustomResourceDefinition) field.ErrorList {
   222  	opts := validationOptions{
   223  		allowDefaults:                            true,
   224  		requireRecognizedConversionReviewVersion: oldObj.Spec.Conversion == nil || hasValidConversionReviewVersionOrEmpty(oldObj.Spec.Conversion.ConversionReviewVersions),
   225  		requireImmutableNames:                    apiextensions.IsCRDConditionTrue(oldObj, apiextensions.Established),
   226  		requireOpenAPISchema:                     requireOpenAPISchema(&oldObj.Spec),
   227  		requireValidPropertyType:                 requireValidPropertyType(&oldObj.Spec),
   228  		requireStructuralSchema:                  requireStructuralSchema(&oldObj.Spec),
   229  		requirePrunedDefaults:                    requirePrunedDefaults(&oldObj.Spec),
   230  		requireAtomicSetType:                     requireAtomicSetType(&oldObj.Spec),
   231  		requireMapListKeysMapSetValidation:       requireMapListKeysMapSetValidation(&oldObj.Spec),
   232  		preexistingExpressions:                   findPreexistingExpressions(&oldObj.Spec),
   233  		versionsWithUnchangedSchemas:             findVersionsWithUnchangedSchemas(obj, oldObj),
   234  		celEnvironmentSet:                        environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()),
   235  	}
   236  	return validateCustomResourceDefinitionUpdate(ctx, obj, oldObj, opts)
   237  }
   238  
   239  func validateCustomResourceDefinitionUpdate(ctx context.Context, obj, oldObj *apiextensions.CustomResourceDefinition, opts validationOptions) field.ErrorList {
   240  	allErrs := genericvalidation.ValidateObjectMetaUpdate(&obj.ObjectMeta, &oldObj.ObjectMeta, field.NewPath("metadata"))
   241  	allErrs = append(allErrs, validateCustomResourceDefinitionSpecUpdate(ctx, &obj.Spec, &oldObj.Spec, opts, field.NewPath("spec"))...)
   242  	allErrs = append(allErrs, ValidateCustomResourceDefinitionStatus(&obj.Status, field.NewPath("status"))...)
   243  	allErrs = append(allErrs, ValidateCustomResourceDefinitionStoredVersions(obj.Status.StoredVersions, obj.Spec.Versions, field.NewPath("status").Child("storedVersions"))...)
   244  	allErrs = append(allErrs, validateAPIApproval(obj, oldObj)...)
   245  	allErrs = append(allErrs, validatePreserveUnknownFields(obj, oldObj)...)
   246  	return allErrs
   247  }
   248  
   249  // ValidateCustomResourceDefinitionStoredVersions statically validates
   250  func ValidateCustomResourceDefinitionStoredVersions(storedVersions []string, versions []apiextensions.CustomResourceDefinitionVersion, fldPath *field.Path) field.ErrorList {
   251  	if len(storedVersions) == 0 {
   252  		return field.ErrorList{field.Invalid(fldPath, storedVersions, "must have at least one stored version")}
   253  	}
   254  	allErrs := field.ErrorList{}
   255  	storedVersionsMap := map[string]int{}
   256  	for i, v := range storedVersions {
   257  		storedVersionsMap[v] = i
   258  	}
   259  	for _, v := range versions {
   260  		_, ok := storedVersionsMap[v.Name]
   261  		if v.Storage && !ok {
   262  			allErrs = append(allErrs, field.Invalid(fldPath, storedVersions, "must have the storage version "+v.Name))
   263  		}
   264  		if ok {
   265  			delete(storedVersionsMap, v.Name)
   266  		}
   267  	}
   268  
   269  	for v, i := range storedVersionsMap {
   270  		allErrs = append(allErrs, field.Invalid(fldPath.Index(i), v, "must appear in spec.versions"))
   271  	}
   272  
   273  	return allErrs
   274  }
   275  
   276  // ValidateUpdateCustomResourceDefinitionStatus statically validates
   277  func ValidateUpdateCustomResourceDefinitionStatus(obj, oldObj *apiextensions.CustomResourceDefinition) field.ErrorList {
   278  	allErrs := genericvalidation.ValidateObjectMetaUpdate(&obj.ObjectMeta, &oldObj.ObjectMeta, field.NewPath("metadata"))
   279  	allErrs = append(allErrs, ValidateCustomResourceDefinitionStatus(&obj.Status, field.NewPath("status"))...)
   280  	return allErrs
   281  }
   282  
   283  // validateCustomResourceDefinitionVersion statically validates.
   284  // context is passed for supporting context cancellation during cel validation when validating defaults
   285  func validateCustomResourceDefinitionVersion(ctx context.Context, version *apiextensions.CustomResourceDefinitionVersion, fldPath *field.Path, statusEnabled bool, opts validationOptions) field.ErrorList {
   286  	allErrs := field.ErrorList{}
   287  	for _, err := range validateDeprecationWarning(version.Deprecated, version.DeprecationWarning) {
   288  		allErrs = append(allErrs, field.Invalid(fldPath.Child("deprecationWarning"), version.DeprecationWarning, err))
   289  	}
   290  	opts = suppressExpressionCostForUnchangedSchema(opts, version.Name)
   291  	allErrs = append(allErrs, validateCustomResourceDefinitionValidation(ctx, version.Schema, statusEnabled, opts, fldPath.Child("schema"))...)
   292  	allErrs = append(allErrs, ValidateCustomResourceDefinitionSubresources(version.Subresources, fldPath.Child("subresources"))...)
   293  	for i := range version.AdditionalPrinterColumns {
   294  		allErrs = append(allErrs, ValidateCustomResourceColumnDefinition(&version.AdditionalPrinterColumns[i], fldPath.Child("additionalPrinterColumns").Index(i))...)
   295  	}
   296  
   297  	if len(version.SelectableFields) > 0 {
   298  		if version.Schema == nil || version.Schema.OpenAPIV3Schema == nil {
   299  			allErrs = append(allErrs, field.Invalid(fldPath.Child("selectableFields"), "", "selectableFields may only be set when version.schema.openAPIV3Schema is not included"))
   300  		} else {
   301  			schema, err := structuralschema.NewStructural(version.Schema.OpenAPIV3Schema)
   302  			if err != nil {
   303  				allErrs = append(allErrs, field.Invalid(fldPath.Child("schema.openAPIV3Schema"), "", err.Error()))
   304  			}
   305  			allErrs = append(allErrs, ValidateCustomResourceSelectableFields(version.SelectableFields, schema, fldPath.Child("selectableFields"))...)
   306  		}
   307  	}
   308  	return allErrs
   309  }
   310  
   311  func validateDeprecationWarning(deprecated bool, deprecationWarning *string) []string {
   312  	if !deprecated && deprecationWarning != nil {
   313  		return []string{"can only be set for deprecated versions"}
   314  	}
   315  	if deprecationWarning == nil {
   316  		return nil
   317  	}
   318  	var errors []string
   319  	if len(*deprecationWarning) > 256 {
   320  		errors = append(errors, "must be <= 256 characters long")
   321  	}
   322  	if len(*deprecationWarning) == 0 {
   323  		errors = append(errors, "must not be an empty string")
   324  	}
   325  	for i, r := range *deprecationWarning {
   326  		if !unicode.IsPrint(r) {
   327  			errors = append(errors, fmt.Sprintf("must only contain printable UTF-8 characters; non-printable character found at index %d", i))
   328  			break
   329  		}
   330  		if unicode.IsControl(r) {
   331  			errors = append(errors, fmt.Sprintf("must only contain printable UTF-8 characters; control character found at index %d", i))
   332  			break
   333  		}
   334  	}
   335  	if !utf8.ValidString(*deprecationWarning) {
   336  		errors = append(errors, "must only contain printable UTF-8 characters")
   337  	}
   338  	return errors
   339  }
   340  
   341  // context is passed for supporting context cancellation during cel validation when validating defaults
   342  func validateCustomResourceDefinitionSpec(ctx context.Context, spec *apiextensions.CustomResourceDefinitionSpec, opts validationOptions, fldPath *field.Path) field.ErrorList {
   343  	allErrs := field.ErrorList{}
   344  
   345  	if len(spec.Group) == 0 {
   346  		allErrs = append(allErrs, field.Required(fldPath.Child("group"), ""))
   347  	} else if errs := utilvalidation.IsDNS1123Subdomain(spec.Group); len(errs) > 0 {
   348  		allErrs = append(allErrs, field.Invalid(fldPath.Child("group"), spec.Group, strings.Join(errs, ",")))
   349  	} else if len(strings.Split(spec.Group, ".")) < 2 {
   350  		allErrs = append(allErrs, field.Invalid(fldPath.Child("group"), spec.Group, "should be a domain with at least one dot"))
   351  	}
   352  
   353  	allErrs = append(allErrs, validateEnumStrings(fldPath.Child("scope"), string(spec.Scope), []string{string(apiextensions.ClusterScoped), string(apiextensions.NamespaceScoped)}, true)...)
   354  
   355  	// enabling pruning requires structural schemas
   356  	if spec.PreserveUnknownFields == nil || *spec.PreserveUnknownFields == false {
   357  		opts.requireStructuralSchema = true
   358  	}
   359  
   360  	if opts.requireOpenAPISchema {
   361  		// check that either a global schema or versioned schemas are set in all versions
   362  		if spec.Validation == nil || spec.Validation.OpenAPIV3Schema == nil {
   363  			for i, v := range spec.Versions {
   364  				if v.Schema == nil || v.Schema.OpenAPIV3Schema == nil {
   365  					allErrs = append(allErrs, field.Required(fldPath.Child("versions").Index(i).Child("schema").Child("openAPIV3Schema"), "schemas are required"))
   366  				}
   367  			}
   368  		}
   369  	} else if spec.PreserveUnknownFields == nil || *spec.PreserveUnknownFields == false {
   370  		// check that either a global schema or versioned schemas are set in served versions
   371  		if spec.Validation == nil || spec.Validation.OpenAPIV3Schema == nil {
   372  			for i, v := range spec.Versions {
   373  				schemaPath := fldPath.Child("versions").Index(i).Child("schema", "openAPIV3Schema")
   374  				if v.Served && (v.Schema == nil || v.Schema.OpenAPIV3Schema == nil) {
   375  					allErrs = append(allErrs, field.Required(schemaPath, "because otherwise all fields are pruned"))
   376  				}
   377  			}
   378  		}
   379  	}
   380  	if opts.allowDefaults && specHasDefaults(spec) {
   381  		opts.requireStructuralSchema = true
   382  		if spec.PreserveUnknownFields == nil || *spec.PreserveUnknownFields {
   383  			allErrs = append(allErrs, field.Invalid(fldPath.Child("preserveUnknownFields"), true, "must be false in order to use defaults in the schema"))
   384  		}
   385  	}
   386  	if specHasKubernetesExtensions(spec) {
   387  		opts.requireStructuralSchema = true
   388  	}
   389  
   390  	storageFlagCount := 0
   391  	versionsMap := map[string]bool{}
   392  	uniqueNames := true
   393  	for i, version := range spec.Versions {
   394  		if version.Storage {
   395  			storageFlagCount++
   396  		}
   397  		if versionsMap[version.Name] {
   398  			uniqueNames = false
   399  		} else {
   400  			versionsMap[version.Name] = true
   401  		}
   402  		if errs := utilvalidation.IsDNS1035Label(version.Name); len(errs) > 0 {
   403  			allErrs = append(allErrs, field.Invalid(fldPath.Child("versions").Index(i).Child("name"), spec.Versions[i].Name, strings.Join(errs, ",")))
   404  		}
   405  		subresources := getSubresourcesForVersion(spec, version.Name)
   406  		allErrs = append(allErrs, validateCustomResourceDefinitionVersion(ctx, &version, fldPath.Child("versions").Index(i), hasStatusEnabled(subresources), opts)...)
   407  	}
   408  
   409  	// The top-level and per-version fields are mutual exclusive
   410  	if spec.Validation != nil && hasPerVersionSchema(spec.Versions) {
   411  		allErrs = append(allErrs, field.Forbidden(fldPath.Child("validation"), "top-level and per-version schemas are mutually exclusive"))
   412  	}
   413  	if spec.Subresources != nil && hasPerVersionSubresources(spec.Versions) {
   414  		allErrs = append(allErrs, field.Forbidden(fldPath.Child("subresources"), "top-level and per-version subresources are mutually exclusive"))
   415  	}
   416  	if len(spec.AdditionalPrinterColumns) > 0 && hasPerVersionColumns(spec.Versions) {
   417  		allErrs = append(allErrs, field.Forbidden(fldPath.Child("additionalPrinterColumns"), "top-level and per-version additionalPrinterColumns are mutually exclusive"))
   418  	}
   419  
   420  	// Per-version fields may not all be set to identical values (top-level field should be used instead)
   421  	if hasIdenticalPerVersionSchema(spec.Versions) {
   422  		allErrs = append(allErrs, field.Invalid(fldPath.Child("versions"), spec.Versions, "per-version schemas may not all be set to identical values (top-level validation should be used instead)"))
   423  	}
   424  	if hasIdenticalPerVersionSubresources(spec.Versions) {
   425  		allErrs = append(allErrs, field.Invalid(fldPath.Child("versions"), spec.Versions, "per-version subresources may not all be set to identical values (top-level subresources should be used instead)"))
   426  	}
   427  	if hasIdenticalPerVersionColumns(spec.Versions) {
   428  		allErrs = append(allErrs, field.Invalid(fldPath.Child("versions"), spec.Versions, "per-version additionalPrinterColumns may not all be set to identical values (top-level additionalPrinterColumns should be used instead)"))
   429  	}
   430  
   431  	if !uniqueNames {
   432  		allErrs = append(allErrs, field.Invalid(fldPath.Child("versions"), spec.Versions, "must contain unique version names"))
   433  	}
   434  	if storageFlagCount != 1 {
   435  		allErrs = append(allErrs, field.Invalid(fldPath.Child("versions"), spec.Versions, "must have exactly one version marked as storage version"))
   436  	}
   437  	if len(spec.Version) != 0 {
   438  		if errs := utilvalidation.IsDNS1035Label(spec.Version); len(errs) > 0 {
   439  			allErrs = append(allErrs, field.Invalid(fldPath.Child("version"), spec.Version, strings.Join(errs, ",")))
   440  		}
   441  		if len(spec.Versions) >= 1 && spec.Versions[0].Name != spec.Version {
   442  			allErrs = append(allErrs, field.Invalid(fldPath.Child("version"), spec.Version, "must match the first version in spec.versions"))
   443  		}
   444  	}
   445  
   446  	// in addition to the basic name restrictions, some names are required for spec, but not for status
   447  	if len(spec.Names.Plural) == 0 {
   448  		allErrs = append(allErrs, field.Required(fldPath.Child("names", "plural"), ""))
   449  	}
   450  	if len(spec.Names.Singular) == 0 {
   451  		allErrs = append(allErrs, field.Required(fldPath.Child("names", "singular"), ""))
   452  	}
   453  	if len(spec.Names.Kind) == 0 {
   454  		allErrs = append(allErrs, field.Required(fldPath.Child("names", "kind"), ""))
   455  	}
   456  	if len(spec.Names.ListKind) == 0 {
   457  		allErrs = append(allErrs, field.Required(fldPath.Child("names", "listKind"), ""))
   458  	}
   459  
   460  	allErrs = append(allErrs, ValidateCustomResourceDefinitionNames(&spec.Names, fldPath.Child("names"))...)
   461  	allErrs = append(allErrs, validateCustomResourceDefinitionValidation(ctx, spec.Validation, hasAnyStatusEnabled(spec), suppressExpressionCostForUnchangedSchema(opts, spec.Version), fldPath.Child("validation"))...)
   462  	allErrs = append(allErrs, ValidateCustomResourceDefinitionSubresources(spec.Subresources, fldPath.Child("subresources"))...)
   463  
   464  	for i := range spec.AdditionalPrinterColumns {
   465  		if errs := ValidateCustomResourceColumnDefinition(&spec.AdditionalPrinterColumns[i], fldPath.Child("additionalPrinterColumns").Index(i)); len(errs) > 0 {
   466  			allErrs = append(allErrs, errs...)
   467  		}
   468  	}
   469  
   470  	if len(spec.SelectableFields) > 0 {
   471  		if spec.Validation == nil {
   472  			allErrs = append(allErrs, field.Invalid(fldPath.Child("selectableFields"), "", "selectableFields may only be set when validations.schema is included"))
   473  		} else {
   474  			schema, err := structuralschema.NewStructural(spec.Validation.OpenAPIV3Schema)
   475  			if err != nil {
   476  				allErrs = append(allErrs, field.Invalid(fldPath.Child("schema.openAPIV3Schema"), "", err.Error()))
   477  			}
   478  
   479  			allErrs = append(allErrs, ValidateCustomResourceSelectableFields(spec.SelectableFields, schema, fldPath.Child("selectableFields"))...)
   480  		}
   481  	}
   482  
   483  	if (spec.Conversion != nil && spec.Conversion.Strategy != apiextensions.NoneConverter) && (spec.PreserveUnknownFields == nil || *spec.PreserveUnknownFields) {
   484  		allErrs = append(allErrs, field.Invalid(fldPath.Child("conversion").Child("strategy"), spec.Conversion.Strategy, "must be None if spec.preserveUnknownFields is true"))
   485  	}
   486  	allErrs = append(allErrs, validateCustomResourceConversion(spec.Conversion, opts.requireRecognizedConversionReviewVersion, fldPath.Child("conversion"))...)
   487  
   488  	return allErrs
   489  }
   490  
   491  func validateEnumStrings(fldPath *field.Path, value string, accepted []string, required bool) field.ErrorList {
   492  	if value == "" {
   493  		if required {
   494  			return field.ErrorList{field.Required(fldPath, "")}
   495  		}
   496  		return field.ErrorList{}
   497  	}
   498  	for _, a := range accepted {
   499  		if a == value {
   500  			return field.ErrorList{}
   501  		}
   502  	}
   503  	return field.ErrorList{field.NotSupported(fldPath, value, accepted)}
   504  }
   505  
   506  // AcceptedConversionReviewVersions contains the list of ConversionReview versions the *prior* version of the API server understands.
   507  // 1.15: server understands v1beta1; accepted versions are ["v1beta1"]
   508  // 1.16: server understands v1, v1beta1; accepted versions are ["v1beta1"]
   509  // 1.17+: server understands v1, v1beta1; accepted versions are ["v1","v1beta1"]
   510  var acceptedConversionReviewVersions = sets.NewString(apiextensionsv1.SchemeGroupVersion.Version, apiextensionsv1beta1.SchemeGroupVersion.Version)
   511  
   512  func isAcceptedConversionReviewVersion(v string) bool {
   513  	return acceptedConversionReviewVersions.Has(v)
   514  }
   515  
   516  func validateConversionReviewVersions(versions []string, requireRecognizedVersion bool, fldPath *field.Path) field.ErrorList {
   517  	allErrs := field.ErrorList{}
   518  	if len(versions) < 1 {
   519  		allErrs = append(allErrs, field.Required(fldPath, ""))
   520  	} else {
   521  		seen := map[string]bool{}
   522  		hasAcceptedVersion := false
   523  		for i, v := range versions {
   524  			if seen[v] {
   525  				allErrs = append(allErrs, field.Invalid(fldPath.Index(i), v, "duplicate version"))
   526  				continue
   527  			}
   528  			seen[v] = true
   529  			for _, errString := range utilvalidation.IsDNS1035Label(v) {
   530  				allErrs = append(allErrs, field.Invalid(fldPath.Index(i), v, errString))
   531  			}
   532  			if isAcceptedConversionReviewVersion(v) {
   533  				hasAcceptedVersion = true
   534  			}
   535  		}
   536  		if requireRecognizedVersion && !hasAcceptedVersion {
   537  			allErrs = append(allErrs, field.Invalid(
   538  				fldPath, versions,
   539  				fmt.Sprintf("must include at least one of %v",
   540  					strings.Join(acceptedConversionReviewVersions.List(), ", "))))
   541  		}
   542  	}
   543  	return allErrs
   544  }
   545  
   546  // hasValidConversionReviewVersion return true if there is a valid version or if the list is empty.
   547  func hasValidConversionReviewVersionOrEmpty(versions []string) bool {
   548  	if len(versions) < 1 {
   549  		return true
   550  	}
   551  	for _, v := range versions {
   552  		if isAcceptedConversionReviewVersion(v) {
   553  			return true
   554  		}
   555  	}
   556  	return false
   557  }
   558  
   559  // ValidateCustomResourceConversion statically validates
   560  func ValidateCustomResourceConversion(conversion *apiextensions.CustomResourceConversion, fldPath *field.Path) field.ErrorList {
   561  	return validateCustomResourceConversion(conversion, true, fldPath)
   562  }
   563  
   564  func validateCustomResourceConversion(conversion *apiextensions.CustomResourceConversion, requireRecognizedVersion bool, fldPath *field.Path) field.ErrorList {
   565  	allErrs := field.ErrorList{}
   566  	if conversion == nil {
   567  		return allErrs
   568  	}
   569  	allErrs = append(allErrs, validateEnumStrings(fldPath.Child("strategy"), string(conversion.Strategy), []string{string(apiextensions.NoneConverter), string(apiextensions.WebhookConverter)}, true)...)
   570  	if conversion.Strategy == apiextensions.WebhookConverter {
   571  		if conversion.WebhookClientConfig == nil {
   572  			allErrs = append(allErrs, field.Required(fldPath.Child("webhookClientConfig"), "required when strategy is set to Webhook"))
   573  		} else {
   574  			cc := conversion.WebhookClientConfig
   575  			switch {
   576  			case (cc.URL == nil) == (cc.Service == nil):
   577  				allErrs = append(allErrs, field.Required(fldPath.Child("webhookClientConfig"), "exactly one of url or service is required"))
   578  			case cc.URL != nil:
   579  				allErrs = append(allErrs, webhook.ValidateWebhookURL(fldPath.Child("webhookClientConfig").Child("url"), *cc.URL, true)...)
   580  			case cc.Service != nil:
   581  				allErrs = append(allErrs, webhook.ValidateWebhookService(fldPath.Child("webhookClientConfig").Child("service"), cc.Service.Name, cc.Service.Namespace, cc.Service.Path, cc.Service.Port)...)
   582  			}
   583  		}
   584  		allErrs = append(allErrs, validateConversionReviewVersions(conversion.ConversionReviewVersions, requireRecognizedVersion, fldPath.Child("conversionReviewVersions"))...)
   585  	} else {
   586  		if conversion.WebhookClientConfig != nil {
   587  			allErrs = append(allErrs, field.Forbidden(fldPath.Child("webhookClientConfig"), "should not be set when strategy is not set to Webhook"))
   588  		}
   589  		if len(conversion.ConversionReviewVersions) > 0 {
   590  			allErrs = append(allErrs, field.Forbidden(fldPath.Child("conversionReviewVersions"), "should not be set when strategy is not set to Webhook"))
   591  		}
   592  	}
   593  	return allErrs
   594  }
   595  
   596  // validateCustomResourceDefinitionSpecUpdate statically validates
   597  // context is passed for supporting context cancellation during cel validation when validating defaults
   598  func validateCustomResourceDefinitionSpecUpdate(ctx context.Context, spec, oldSpec *apiextensions.CustomResourceDefinitionSpec, opts validationOptions, fldPath *field.Path) field.ErrorList {
   599  	allErrs := validateCustomResourceDefinitionSpec(ctx, spec, opts, fldPath)
   600  
   601  	if opts.requireImmutableNames {
   602  		// these effect the storage and cannot be changed therefore
   603  		allErrs = append(allErrs, genericvalidation.ValidateImmutableField(spec.Scope, oldSpec.Scope, fldPath.Child("scope"))...)
   604  		allErrs = append(allErrs, genericvalidation.ValidateImmutableField(spec.Names.Kind, oldSpec.Names.Kind, fldPath.Child("names", "kind"))...)
   605  	}
   606  
   607  	// these affects the resource name, which is always immutable, so this can't be updated.
   608  	allErrs = append(allErrs, genericvalidation.ValidateImmutableField(spec.Group, oldSpec.Group, fldPath.Child("group"))...)
   609  	allErrs = append(allErrs, genericvalidation.ValidateImmutableField(spec.Names.Plural, oldSpec.Names.Plural, fldPath.Child("names", "plural"))...)
   610  
   611  	return allErrs
   612  }
   613  
   614  // getSubresourcesForVersion returns the subresources for given version in given CRD spec.
   615  // NOTE That this function assumes version always exist since it's used by the validation process
   616  // that iterates through the existing versions.
   617  func getSubresourcesForVersion(crd *apiextensions.CustomResourceDefinitionSpec, version string) *apiextensions.CustomResourceSubresources {
   618  	if !hasPerVersionSubresources(crd.Versions) {
   619  		return crd.Subresources
   620  	}
   621  	for _, v := range crd.Versions {
   622  		if version == v.Name {
   623  			return v.Subresources
   624  		}
   625  	}
   626  	return nil
   627  }
   628  
   629  // hasAnyStatusEnabled returns true if given CRD spec has at least one Status Subresource set
   630  // among the top-level and per-version Subresources.
   631  func hasAnyStatusEnabled(crd *apiextensions.CustomResourceDefinitionSpec) bool {
   632  	if hasStatusEnabled(crd.Subresources) {
   633  		return true
   634  	}
   635  	for _, v := range crd.Versions {
   636  		if hasStatusEnabled(v.Subresources) {
   637  			return true
   638  		}
   639  	}
   640  	return false
   641  }
   642  
   643  // hasStatusEnabled returns true if given CRD Subresources has non-nil Status set.
   644  func hasStatusEnabled(subresources *apiextensions.CustomResourceSubresources) bool {
   645  	if subresources != nil && subresources.Status != nil {
   646  		return true
   647  	}
   648  	return false
   649  }
   650  
   651  // hasPerVersionSchema returns true if a CRD uses per-version schema.
   652  func hasPerVersionSchema(versions []apiextensions.CustomResourceDefinitionVersion) bool {
   653  	for _, v := range versions {
   654  		if v.Schema != nil {
   655  			return true
   656  		}
   657  	}
   658  	return false
   659  }
   660  
   661  // hasPerVersionSubresources returns true if a CRD uses per-version subresources.
   662  func hasPerVersionSubresources(versions []apiextensions.CustomResourceDefinitionVersion) bool {
   663  	for _, v := range versions {
   664  		if v.Subresources != nil {
   665  			return true
   666  		}
   667  	}
   668  	return false
   669  }
   670  
   671  // hasPerVersionColumns returns true if a CRD uses per-version columns.
   672  func hasPerVersionColumns(versions []apiextensions.CustomResourceDefinitionVersion) bool {
   673  	for _, v := range versions {
   674  		if len(v.AdditionalPrinterColumns) > 0 {
   675  			return true
   676  		}
   677  	}
   678  	return false
   679  }
   680  
   681  // hasIdenticalPerVersionSchema returns true if a CRD sets identical non-nil values
   682  // to all per-version schemas
   683  func hasIdenticalPerVersionSchema(versions []apiextensions.CustomResourceDefinitionVersion) bool {
   684  	if len(versions) == 0 {
   685  		return false
   686  	}
   687  	value := versions[0].Schema
   688  	for _, v := range versions {
   689  		if v.Schema == nil || !apiequality.Semantic.DeepEqual(v.Schema, value) {
   690  			return false
   691  		}
   692  	}
   693  	return true
   694  }
   695  
   696  // hasIdenticalPerVersionSubresources returns true if a CRD sets identical non-nil values
   697  // to all per-version subresources
   698  func hasIdenticalPerVersionSubresources(versions []apiextensions.CustomResourceDefinitionVersion) bool {
   699  	if len(versions) == 0 {
   700  		return false
   701  	}
   702  	value := versions[0].Subresources
   703  	for _, v := range versions {
   704  		if v.Subresources == nil || !apiequality.Semantic.DeepEqual(v.Subresources, value) {
   705  			return false
   706  		}
   707  	}
   708  	return true
   709  }
   710  
   711  // hasIdenticalPerVersionColumns returns true if a CRD sets identical non-nil values
   712  // to all per-version columns
   713  func hasIdenticalPerVersionColumns(versions []apiextensions.CustomResourceDefinitionVersion) bool {
   714  	if len(versions) == 0 {
   715  		return false
   716  	}
   717  	value := versions[0].AdditionalPrinterColumns
   718  	for _, v := range versions {
   719  		if len(v.AdditionalPrinterColumns) == 0 || !apiequality.Semantic.DeepEqual(v.AdditionalPrinterColumns, value) {
   720  			return false
   721  		}
   722  	}
   723  	return true
   724  }
   725  
   726  // ValidateCustomResourceDefinitionStatus statically validates
   727  func ValidateCustomResourceDefinitionStatus(status *apiextensions.CustomResourceDefinitionStatus, fldPath *field.Path) field.ErrorList {
   728  	allErrs := field.ErrorList{}
   729  	allErrs = append(allErrs, ValidateCustomResourceDefinitionNames(&status.AcceptedNames, fldPath.Child("acceptedNames"))...)
   730  	return allErrs
   731  }
   732  
   733  // ValidateCustomResourceDefinitionNames statically validates
   734  func ValidateCustomResourceDefinitionNames(names *apiextensions.CustomResourceDefinitionNames, fldPath *field.Path) field.ErrorList {
   735  	allErrs := field.ErrorList{}
   736  	if errs := utilvalidation.IsDNS1035Label(names.Plural); len(names.Plural) > 0 && len(errs) > 0 {
   737  		allErrs = append(allErrs, field.Invalid(fldPath.Child("plural"), names.Plural, strings.Join(errs, ",")))
   738  	}
   739  	if errs := utilvalidation.IsDNS1035Label(names.Singular); len(names.Singular) > 0 && len(errs) > 0 {
   740  		allErrs = append(allErrs, field.Invalid(fldPath.Child("singular"), names.Singular, strings.Join(errs, ",")))
   741  	}
   742  	if errs := utilvalidation.IsDNS1035Label(strings.ToLower(names.Kind)); len(names.Kind) > 0 && len(errs) > 0 {
   743  		allErrs = append(allErrs, field.Invalid(fldPath.Child("kind"), names.Kind, "may have mixed case, but should otherwise match: "+strings.Join(errs, ",")))
   744  	}
   745  	if errs := utilvalidation.IsDNS1035Label(strings.ToLower(names.ListKind)); len(names.ListKind) > 0 && len(errs) > 0 {
   746  		allErrs = append(allErrs, field.Invalid(fldPath.Child("listKind"), names.ListKind, "may have mixed case, but should otherwise match: "+strings.Join(errs, ",")))
   747  	}
   748  
   749  	for i, shortName := range names.ShortNames {
   750  		if errs := utilvalidation.IsDNS1035Label(shortName); len(errs) > 0 {
   751  			allErrs = append(allErrs, field.Invalid(fldPath.Child("shortNames").Index(i), shortName, strings.Join(errs, ",")))
   752  		}
   753  	}
   754  
   755  	// kind and listKind may not be the same or parsing become ambiguous
   756  	if len(names.Kind) > 0 && names.Kind == names.ListKind {
   757  		allErrs = append(allErrs, field.Invalid(fldPath.Child("listKind"), names.ListKind, "kind and listKind may not be the same"))
   758  	}
   759  
   760  	for i, category := range names.Categories {
   761  		if errs := utilvalidation.IsDNS1035Label(category); len(errs) > 0 {
   762  			allErrs = append(allErrs, field.Invalid(fldPath.Child("categories").Index(i), category, strings.Join(errs, ",")))
   763  		}
   764  	}
   765  
   766  	return allErrs
   767  }
   768  
   769  // ValidateCustomResourceColumnDefinition statically validates a printer column.
   770  func ValidateCustomResourceColumnDefinition(col *apiextensions.CustomResourceColumnDefinition, fldPath *field.Path) field.ErrorList {
   771  	allErrs := field.ErrorList{}
   772  
   773  	if len(col.Name) == 0 {
   774  		allErrs = append(allErrs, field.Required(fldPath.Child("name"), ""))
   775  	}
   776  
   777  	if len(col.Type) == 0 {
   778  		allErrs = append(allErrs, field.Required(fldPath.Child("type"), fmt.Sprintf("must be one of %s", strings.Join(printerColumnDatatypes.List(), ","))))
   779  	} else if !printerColumnDatatypes.Has(col.Type) {
   780  		allErrs = append(allErrs, field.Invalid(fldPath.Child("type"), col.Type, fmt.Sprintf("must be one of %s", strings.Join(printerColumnDatatypes.List(), ","))))
   781  	}
   782  
   783  	if len(col.Format) > 0 && !customResourceColumnDefinitionFormats.Has(col.Format) {
   784  		allErrs = append(allErrs, field.Invalid(fldPath.Child("format"), col.Format, fmt.Sprintf("must be one of %s", strings.Join(customResourceColumnDefinitionFormats.List(), ","))))
   785  	}
   786  
   787  	if len(col.JSONPath) == 0 {
   788  		allErrs = append(allErrs, field.Required(fldPath.Child("JSONPath"), ""))
   789  	} else if errs := validateSimpleJSONPath(col.JSONPath, fldPath.Child("JSONPath")); len(errs) > 0 {
   790  		allErrs = append(allErrs, errs...)
   791  	}
   792  
   793  	return allErrs
   794  }
   795  
   796  func ValidateCustomResourceSelectableFields(selectableFields []apiextensions.SelectableField, schema *structuralschema.Structural, fldPath *field.Path) (allErrs field.ErrorList) {
   797  	uniqueSelectableFields := sets.New[string]()
   798  	for i, selectableField := range selectableFields {
   799  		indexFldPath := fldPath.Index(i)
   800  		if len(selectableField.JSONPath) == 0 {
   801  			allErrs = append(allErrs, field.Required(indexFldPath.Child("jsonPath"), ""))
   802  			continue
   803  		}
   804  		// Leverage the field path validation originally built for use with CEL features
   805  		path, foundSchema, err := cel.ValidFieldPath(selectableField.JSONPath, schema, cel.WithFieldPathAllowArrayNotation(false))
   806  		if err != nil {
   807  			allErrs = append(allErrs, field.Invalid(indexFldPath.Child("jsonPath"), selectableField.JSONPath, fmt.Sprintf("is an invalid path: %v", err)))
   808  			continue
   809  		}
   810  		if path.Root().String() == "metadata" {
   811  			allErrs = append(allErrs, field.Invalid(indexFldPath.Child("jsonPath"), selectableField.JSONPath, "must not point to fields in metadata"))
   812  		}
   813  		if !allowedSelectableFieldSchema(foundSchema) {
   814  			allErrs = append(allErrs, field.Invalid(indexFldPath.Child("jsonPath"), selectableField.JSONPath, "must point to a field of type string, boolean or integer. Enum string fields and strings with formats are allowed."))
   815  		}
   816  		if uniqueSelectableFields.Has(path.String()) {
   817  			allErrs = append(allErrs, field.Duplicate(indexFldPath.Child("jsonPath"), selectableField.JSONPath))
   818  		} else {
   819  			uniqueSelectableFields.Insert(path.String())
   820  		}
   821  	}
   822  	uniqueSelectableFieldCount := uniqueSelectableFields.Len()
   823  	if uniqueSelectableFieldCount > MaxSelectableFields {
   824  		allErrs = append(allErrs, field.TooMany(fldPath, uniqueSelectableFieldCount, MaxSelectableFields))
   825  	}
   826  	return allErrs
   827  }
   828  
   829  func allowedSelectableFieldSchema(schema *structuralschema.Structural) bool {
   830  	if schema == nil {
   831  		return false
   832  	}
   833  	switch schema.Type {
   834  	case "string", "boolean", "integer":
   835  		return true
   836  	default:
   837  		return false
   838  	}
   839  }
   840  
   841  // specStandardValidator applies validations for different OpenAPI specification versions.
   842  type specStandardValidator interface {
   843  	validate(spec *apiextensions.JSONSchemaProps, fldPath *field.Path) field.ErrorList
   844  	withForbiddenDefaults(reason string) specStandardValidator
   845  
   846  	// insideResourceMeta returns true when validating either TypeMeta or ObjectMeta, from an embedded resource or on the top-level.
   847  	insideResourceMeta() bool
   848  	withInsideResourceMeta() specStandardValidator
   849  
   850  	// forbidOldSelfValidations returns the path to the first ancestor of the visited path that can't be safely correlated between two revisions of an object, or nil if there is no such path
   851  	forbidOldSelfValidations() *field.Path
   852  	withForbidOldSelfValidations(path *field.Path) specStandardValidator
   853  }
   854  
   855  // validateCustomResourceDefinitionValidation statically validates
   856  // context is passed for supporting context cancellation during cel validation when validating defaults
   857  func validateCustomResourceDefinitionValidation(ctx context.Context, customResourceValidation *apiextensions.CustomResourceValidation, statusSubresourceEnabled bool, opts validationOptions, fldPath *field.Path) field.ErrorList {
   858  	allErrs := field.ErrorList{}
   859  
   860  	if customResourceValidation == nil {
   861  		return allErrs
   862  	}
   863  
   864  	if schema := customResourceValidation.OpenAPIV3Schema; schema != nil {
   865  		// if the status subresource is enabled, only certain fields are allowed inside the root schema.
   866  		// these fields are chosen such that, if status is extracted as properties["status"], it's validation is not lost.
   867  		if statusSubresourceEnabled {
   868  			v := reflect.ValueOf(schema).Elem()
   869  			for i := 0; i < v.NumField(); i++ {
   870  				// skip zero values
   871  				if value := v.Field(i).Interface(); reflect.DeepEqual(value, reflect.Zero(reflect.TypeOf(value)).Interface()) {
   872  					continue
   873  				}
   874  
   875  				fieldName := v.Type().Field(i).Name
   876  
   877  				// only "object" type is valid at root of the schema since validation schema for status is extracted as properties["status"]
   878  				if fieldName == "Type" {
   879  					if schema.Type != "object" {
   880  						allErrs = append(allErrs, field.Invalid(fldPath.Child("openAPIV3Schema.type"), schema.Type, fmt.Sprintf(`only "object" is allowed as the type at the root of the schema if the status subresource is enabled`)))
   881  						break
   882  					}
   883  					continue
   884  				}
   885  
   886  				if !allowedAtRootSchema(fieldName) {
   887  					allErrs = append(allErrs, field.Invalid(fldPath.Child("openAPIV3Schema"), *schema, fmt.Sprintf(`only %v fields are allowed at the root of the schema if the status subresource is enabled`, allowedFieldsAtRootSchema)))
   888  					break
   889  				}
   890  			}
   891  		}
   892  
   893  		if schema.Nullable {
   894  			allErrs = append(allErrs, field.Forbidden(fldPath.Child("openAPIV3Schema.nullable"), fmt.Sprintf(`nullable cannot be true at the root`)))
   895  		}
   896  
   897  		openAPIV3Schema := &specStandardValidatorV3{
   898  			allowDefaults:            opts.allowDefaults,
   899  			disallowDefaultsReason:   opts.disallowDefaultsReason,
   900  			requireValidPropertyType: opts.requireValidPropertyType,
   901  		}
   902  
   903  		var celContext *CELSchemaContext
   904  		var structuralSchemaInitErrs field.ErrorList
   905  		if opts.requireStructuralSchema {
   906  			if ss, err := structuralschema.NewStructural(schema); err != nil {
   907  				// These validation errors overlap with  OpenAPISchema validation errors so we keep track of them
   908  				// separately and only show them if OpenAPISchema validation does not report any errors.
   909  				structuralSchemaInitErrs = append(structuralSchemaInitErrs, field.Invalid(fldPath.Child("openAPIV3Schema"), "", err.Error()))
   910  			} else if validationErrors := structuralschema.ValidateStructural(fldPath.Child("openAPIV3Schema"), ss); len(validationErrors) > 0 {
   911  				allErrs = append(allErrs, validationErrors...)
   912  			} else if validationErrors, err := structuraldefaulting.ValidateDefaults(ctx, fldPath.Child("openAPIV3Schema"), ss, true, opts.requirePrunedDefaults); err != nil {
   913  				// this should never happen
   914  				allErrs = append(allErrs, field.Invalid(fldPath.Child("openAPIV3Schema"), "", err.Error()))
   915  			} else if len(validationErrors) > 0 {
   916  				allErrs = append(allErrs, validationErrors...)
   917  			} else {
   918  				// Only initialize CEL rule validation context if the structural schemas are valid.
   919  				// A nil CELSchemaContext indicates that no CEL validation should be attempted.
   920  				celContext = RootCELContext(schema)
   921  			}
   922  		}
   923  		allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(schema, fldPath.Child("openAPIV3Schema"), openAPIV3Schema, true, &opts, celContext).AllErrors()...)
   924  
   925  		if len(allErrs) == 0 && len(structuralSchemaInitErrs) > 0 {
   926  			// Structural schema initialization errors overlap with OpenAPISchema validation errors so we only show them
   927  			// if there are no OpenAPISchema validation errors.
   928  			allErrs = append(allErrs, structuralSchemaInitErrs...)
   929  		}
   930  
   931  		if celContext != nil && celContext.TotalCost != nil {
   932  			if celContext.TotalCost.Total > StaticEstimatedCRDCostLimit {
   933  				for _, expensive := range celContext.TotalCost.MostExpensive {
   934  					costErrorMsg := fmt.Sprintf("contributed to estimated rule cost total exceeding cost limit for entire OpenAPIv3 schema")
   935  					allErrs = append(allErrs, field.Forbidden(expensive.Path, costErrorMsg))
   936  				}
   937  
   938  				costErrorMsg := getCostErrorMessage("x-kubernetes-validations estimated rule cost total for entire OpenAPIv3 schema", celContext.TotalCost.Total, StaticEstimatedCRDCostLimit)
   939  				allErrs = append(allErrs, field.Forbidden(fldPath.Child("openAPIV3Schema"), costErrorMsg))
   940  			}
   941  		}
   942  	}
   943  
   944  	// if validation passed otherwise, make sure we can actually construct a schema validator from this custom resource validation.
   945  	if len(allErrs) == 0 {
   946  		if _, _, err := apiservervalidation.NewSchemaValidator(customResourceValidation.OpenAPIV3Schema); err != nil {
   947  			allErrs = append(allErrs, field.Invalid(fldPath, "", fmt.Sprintf("error building validator: %v", err)))
   948  		}
   949  	}
   950  	return allErrs
   951  }
   952  
   953  var metaFields = sets.NewString("metadata", "kind", "apiVersion")
   954  
   955  // OpenAPISchemaErrorList tracks all validation errors reported ValidateCustomResourceDefinitionOpenAPISchema
   956  // with CEL related errors kept separate from schema related errors.
   957  type OpenAPISchemaErrorList struct {
   958  	SchemaErrors field.ErrorList
   959  	CELErrors    field.ErrorList
   960  }
   961  
   962  // AppendErrors appends all errors in the provided list with the errors of this list.
   963  func (o *OpenAPISchemaErrorList) AppendErrors(list *OpenAPISchemaErrorList) {
   964  	if o == nil || list == nil {
   965  		return
   966  	}
   967  	o.SchemaErrors = append(o.SchemaErrors, list.SchemaErrors...)
   968  	o.CELErrors = append(o.CELErrors, list.CELErrors...)
   969  }
   970  
   971  // AllErrors returns a list containing both schema and CEL errors.
   972  func (o *OpenAPISchemaErrorList) AllErrors() field.ErrorList {
   973  	if o == nil {
   974  		return field.ErrorList{}
   975  	}
   976  	return append(o.SchemaErrors, o.CELErrors...)
   977  }
   978  
   979  // ValidateCustomResourceDefinitionOpenAPISchema statically validates
   980  func ValidateCustomResourceDefinitionOpenAPISchema(schema *apiextensions.JSONSchemaProps, fldPath *field.Path, ssv specStandardValidator, isRoot bool, opts *validationOptions, celContext *CELSchemaContext) *OpenAPISchemaErrorList {
   981  	allErrs := &OpenAPISchemaErrorList{SchemaErrors: field.ErrorList{}, CELErrors: field.ErrorList{}}
   982  
   983  	if schema == nil {
   984  		return allErrs
   985  	}
   986  	allErrs.SchemaErrors = append(allErrs.SchemaErrors, ssv.validate(schema, fldPath)...)
   987  
   988  	if schema.UniqueItems {
   989  		allErrs.SchemaErrors = append(allErrs.SchemaErrors, field.Forbidden(fldPath.Child("uniqueItems"), "uniqueItems cannot be set to true since the runtime complexity becomes quadratic"))
   990  	}
   991  
   992  	// additionalProperties and properties are mutual exclusive because otherwise they
   993  	// contradict Kubernetes' API convention to ignore unknown fields.
   994  	//
   995  	// In other words:
   996  	// - properties are for structs,
   997  	// - additionalProperties are for map[string]interface{}
   998  	//
   999  	// Note: when patternProperties is added to OpenAPI some day, this will have to be
  1000  	//       restricted like additionalProperties.
  1001  	if schema.AdditionalProperties != nil {
  1002  		if len(schema.Properties) != 0 {
  1003  			if !schema.AdditionalProperties.Allows || schema.AdditionalProperties.Schema != nil {
  1004  				allErrs.SchemaErrors = append(allErrs.SchemaErrors, field.Forbidden(fldPath.Child("additionalProperties"), "additionalProperties and properties are mutual exclusive"))
  1005  			}
  1006  		}
  1007  		// Note: we forbid additionalProperties at resource root, both embedded and top-level.
  1008  		//       But further inside, additionalProperites is possible, e.g. for labels or annotations.
  1009  		subSsv := ssv
  1010  		if ssv.insideResourceMeta() {
  1011  			// we have to forbid defaults inside additionalProperties because pruning without actual value is ambiguous
  1012  			subSsv = ssv.withForbiddenDefaults("inside additionalProperties applying to object metadata")
  1013  		}
  1014  		allErrs.AppendErrors(ValidateCustomResourceDefinitionOpenAPISchema(schema.AdditionalProperties.Schema, fldPath.Child("additionalProperties"), subSsv, false, opts, celContext.ChildAdditionalPropertiesContext(schema.AdditionalProperties.Schema)))
  1015  	}
  1016  
  1017  	if len(schema.Properties) != 0 {
  1018  		for property, jsonSchema := range schema.Properties {
  1019  			subSsv := ssv
  1020  
  1021  			if !cel.MapIsCorrelatable(schema.XMapType) {
  1022  				subSsv = subSsv.withForbidOldSelfValidations(fldPath)
  1023  			}
  1024  
  1025  			if (isRoot || schema.XEmbeddedResource) && metaFields.Has(property) {
  1026  				// we recurse into the schema that applies to ObjectMeta.
  1027  				subSsv = subSsv.withInsideResourceMeta()
  1028  				if isRoot {
  1029  					subSsv = subSsv.withForbiddenDefaults(fmt.Sprintf("in top-level %s", property))
  1030  				}
  1031  			}
  1032  			propertySchema := jsonSchema
  1033  			allErrs.AppendErrors(ValidateCustomResourceDefinitionOpenAPISchema(&propertySchema, fldPath.Child("properties").Key(property), subSsv, false, opts, celContext.ChildPropertyContext(&propertySchema, property)))
  1034  		}
  1035  	}
  1036  
  1037  	allErrs.AppendErrors(ValidateCustomResourceDefinitionOpenAPISchema(schema.Not, fldPath.Child("not"), ssv, false, opts, nil))
  1038  
  1039  	if len(schema.AllOf) != 0 {
  1040  		for i, jsonSchema := range schema.AllOf {
  1041  			allOfSchema := jsonSchema
  1042  			allErrs.AppendErrors(ValidateCustomResourceDefinitionOpenAPISchema(&allOfSchema, fldPath.Child("allOf").Index(i), ssv, false, opts, nil))
  1043  		}
  1044  	}
  1045  
  1046  	if len(schema.OneOf) != 0 {
  1047  		for i, jsonSchema := range schema.OneOf {
  1048  			oneOfSchema := jsonSchema
  1049  			allErrs.AppendErrors(ValidateCustomResourceDefinitionOpenAPISchema(&oneOfSchema, fldPath.Child("oneOf").Index(i), ssv, false, opts, nil))
  1050  		}
  1051  	}
  1052  
  1053  	if len(schema.AnyOf) != 0 {
  1054  		for i, jsonSchema := range schema.AnyOf {
  1055  			anyOfSchema := jsonSchema
  1056  			allErrs.AppendErrors(ValidateCustomResourceDefinitionOpenAPISchema(&anyOfSchema, fldPath.Child("anyOf").Index(i), ssv, false, opts, nil))
  1057  		}
  1058  	}
  1059  
  1060  	if len(schema.Definitions) != 0 {
  1061  		for definition, jsonSchema := range schema.Definitions {
  1062  			definitionSchema := jsonSchema
  1063  			allErrs.AppendErrors(ValidateCustomResourceDefinitionOpenAPISchema(&definitionSchema, fldPath.Child("definitions").Key(definition), ssv, false, opts, nil))
  1064  		}
  1065  	}
  1066  
  1067  	if schema.Items != nil {
  1068  		subSsv := ssv
  1069  
  1070  		// we can only correlate old/new items for "map" and "set" lists, and correlation of
  1071  		// "set" elements by identity is not supported for cel (x-kubernetes-validations)
  1072  		// rules. an unset list type defaults to "atomic".
  1073  		if schema.XListType == nil || *schema.XListType != "map" {
  1074  			subSsv = subSsv.withForbidOldSelfValidations(fldPath)
  1075  		}
  1076  
  1077  		allErrs.AppendErrors(ValidateCustomResourceDefinitionOpenAPISchema(schema.Items.Schema, fldPath.Child("items"), subSsv, false, opts, celContext.ChildItemsContext(schema.Items.Schema)))
  1078  		if len(schema.Items.JSONSchemas) != 0 {
  1079  			for i, jsonSchema := range schema.Items.JSONSchemas {
  1080  				itemsSchema := jsonSchema
  1081  				allErrs.AppendErrors(ValidateCustomResourceDefinitionOpenAPISchema(&itemsSchema, fldPath.Child("items").Index(i), subSsv, false, opts, celContext.ChildItemsContext(&itemsSchema)))
  1082  			}
  1083  		}
  1084  	}
  1085  
  1086  	if schema.Dependencies != nil {
  1087  		for dependency, jsonSchemaPropsOrStringArray := range schema.Dependencies {
  1088  			allErrs.AppendErrors(ValidateCustomResourceDefinitionOpenAPISchema(jsonSchemaPropsOrStringArray.Schema, fldPath.Child("dependencies").Key(dependency), ssv, false, opts, nil))
  1089  		}
  1090  	}
  1091  
  1092  	if schema.XPreserveUnknownFields != nil && !*schema.XPreserveUnknownFields {
  1093  		allErrs.SchemaErrors = append(allErrs.SchemaErrors, field.Invalid(fldPath.Child("x-kubernetes-preserve-unknown-fields"), *schema.XPreserveUnknownFields, "must be true or undefined"))
  1094  	}
  1095  
  1096  	if schema.XMapType != nil && schema.Type != "object" {
  1097  		if len(schema.Type) == 0 {
  1098  			allErrs.SchemaErrors = append(allErrs.SchemaErrors, field.Required(fldPath.Child("type"), "must be object if x-kubernetes-map-type is specified"))
  1099  		} else {
  1100  			allErrs.SchemaErrors = append(allErrs.SchemaErrors, field.Invalid(fldPath.Child("type"), schema.Type, "must be object if x-kubernetes-map-type is specified"))
  1101  		}
  1102  	}
  1103  
  1104  	if schema.XMapType != nil && *schema.XMapType != "atomic" && *schema.XMapType != "granular" {
  1105  		allErrs.SchemaErrors = append(allErrs.SchemaErrors, field.NotSupported(fldPath.Child("x-kubernetes-map-type"), *schema.XMapType, []string{"atomic", "granular"}))
  1106  	}
  1107  
  1108  	if schema.XListType != nil && schema.Type != "array" {
  1109  		if len(schema.Type) == 0 {
  1110  			allErrs.SchemaErrors = append(allErrs.SchemaErrors, field.Required(fldPath.Child("type"), "must be array if x-kubernetes-list-type is specified"))
  1111  		} else {
  1112  			allErrs.SchemaErrors = append(allErrs.SchemaErrors, field.Invalid(fldPath.Child("type"), schema.Type, "must be array if x-kubernetes-list-type is specified"))
  1113  		}
  1114  	} else if opts.requireAtomicSetType && schema.XListType != nil && *schema.XListType == "set" && schema.Items != nil && schema.Items.Schema != nil { // by structural schema items are present
  1115  		is := schema.Items.Schema
  1116  		switch is.Type {
  1117  		case "array":
  1118  			if is.XListType != nil && *is.XListType != "atomic" { // atomic is the implicit default behaviour if unset, hence != atomic is wrong
  1119  				allErrs.SchemaErrors = append(allErrs.SchemaErrors, field.Invalid(fldPath.Child("items").Child("x-kubernetes-list-type"), is.XListType, "must be atomic as item of a list with x-kubernetes-list-type=set"))
  1120  			}
  1121  		case "object":
  1122  			if is.XMapType == nil || *is.XMapType != "atomic" { // granular is the implicit default behaviour if unset, hence nil and != atomic are wrong
  1123  				allErrs.SchemaErrors = append(allErrs.SchemaErrors, field.Invalid(fldPath.Child("items").Child("x-kubernetes-map-type"), is.XListType, "must be atomic as item of a list with x-kubernetes-list-type=set"))
  1124  			}
  1125  		}
  1126  	}
  1127  
  1128  	if schema.XListType != nil && *schema.XListType != "atomic" && *schema.XListType != "set" && *schema.XListType != "map" {
  1129  		allErrs.SchemaErrors = append(allErrs.SchemaErrors, field.NotSupported(fldPath.Child("x-kubernetes-list-type"), *schema.XListType, []string{"atomic", "set", "map"}))
  1130  	}
  1131  
  1132  	if len(schema.XListMapKeys) > 0 {
  1133  		if schema.XListType == nil {
  1134  			allErrs.SchemaErrors = append(allErrs.SchemaErrors, field.Required(fldPath.Child("x-kubernetes-list-type"), "must be map if x-kubernetes-list-map-keys is non-empty"))
  1135  		} else if *schema.XListType != "map" {
  1136  			allErrs.SchemaErrors = append(allErrs.SchemaErrors, field.Invalid(fldPath.Child("x-kubernetes-list-type"), *schema.XListType, "must be map if x-kubernetes-list-map-keys is non-empty"))
  1137  		}
  1138  	}
  1139  
  1140  	if schema.XListType != nil && *schema.XListType == "map" {
  1141  		if len(schema.XListMapKeys) == 0 {
  1142  			allErrs.SchemaErrors = append(allErrs.SchemaErrors, field.Required(fldPath.Child("x-kubernetes-list-map-keys"), "must not be empty if x-kubernetes-list-type is map"))
  1143  		}
  1144  
  1145  		if schema.Items == nil {
  1146  			allErrs.SchemaErrors = append(allErrs.SchemaErrors, field.Required(fldPath.Child("items"), "must have a schema if x-kubernetes-list-type is map"))
  1147  		}
  1148  
  1149  		if schema.Items != nil && schema.Items.Schema == nil {
  1150  			allErrs.SchemaErrors = append(allErrs.SchemaErrors, field.Invalid(fldPath.Child("items"), schema.Items, "must only have a single schema if x-kubernetes-list-type is map"))
  1151  		}
  1152  
  1153  		if schema.Items != nil && schema.Items.Schema != nil && schema.Items.Schema.Type != "object" {
  1154  			allErrs.SchemaErrors = append(allErrs.SchemaErrors, field.Invalid(fldPath.Child("items").Child("type"), schema.Items.Schema.Type, "must be object if parent array's x-kubernetes-list-type is map"))
  1155  		}
  1156  
  1157  		if schema.Items != nil && schema.Items.Schema != nil && schema.Items.Schema.Type == "object" {
  1158  			keys := map[string]struct{}{}
  1159  			for _, k := range schema.XListMapKeys {
  1160  				if s, ok := schema.Items.Schema.Properties[k]; ok {
  1161  					if s.Type == "array" || s.Type == "object" {
  1162  						allErrs.SchemaErrors = append(allErrs.SchemaErrors, field.Invalid(fldPath.Child("items").Child("properties").Key(k).Child("type"), schema.Items.Schema.Type, "must be a scalar type if parent array's x-kubernetes-list-type is map"))
  1163  					}
  1164  				} else {
  1165  					allErrs.SchemaErrors = append(allErrs.SchemaErrors, field.Invalid(fldPath.Child("x-kubernetes-list-map-keys"), schema.XListMapKeys, "entries must all be names of item properties"))
  1166  				}
  1167  				if _, ok := keys[k]; ok {
  1168  					allErrs.SchemaErrors = append(allErrs.SchemaErrors, field.Invalid(fldPath.Child("x-kubernetes-list-map-keys"), schema.XListMapKeys, "must not contain duplicate entries"))
  1169  				}
  1170  				keys[k] = struct{}{}
  1171  			}
  1172  		}
  1173  	}
  1174  
  1175  	if opts.requireMapListKeysMapSetValidation {
  1176  		allErrs.SchemaErrors = append(allErrs.SchemaErrors, validateMapListKeysMapSet(schema, fldPath)...)
  1177  	}
  1178  	if len(schema.XValidations) > 0 {
  1179  		for i, rule := range schema.XValidations {
  1180  			trimmedRule := strings.TrimSpace(rule.Rule)
  1181  			trimmedMsg := strings.TrimSpace(rule.Message)
  1182  			trimmedMsgExpr := strings.TrimSpace(rule.MessageExpression)
  1183  			if len(trimmedRule) == 0 {
  1184  				allErrs.SchemaErrors = append(allErrs.SchemaErrors, field.Required(fldPath.Child("x-kubernetes-validations").Index(i).Child("rule"), "rule is not specified"))
  1185  			} else if len(rule.Message) > 0 && len(trimmedMsg) == 0 {
  1186  				allErrs.SchemaErrors = append(allErrs.SchemaErrors, field.Invalid(fldPath.Child("x-kubernetes-validations").Index(i).Child("message"), rule.Message, "message must be non-empty if specified"))
  1187  			} else if hasNewlines(trimmedMsg) {
  1188  				allErrs.SchemaErrors = append(allErrs.SchemaErrors, field.Invalid(fldPath.Child("x-kubernetes-validations").Index(i).Child("message"), rule.Message, "message must not contain line breaks"))
  1189  			} else if hasNewlines(trimmedRule) && len(trimmedMsg) == 0 {
  1190  				allErrs.SchemaErrors = append(allErrs.SchemaErrors, field.Required(fldPath.Child("x-kubernetes-validations").Index(i).Child("message"), "message must be specified if rule contains line breaks"))
  1191  			}
  1192  			if len(rule.MessageExpression) > 0 && len(trimmedMsgExpr) == 0 {
  1193  				allErrs.SchemaErrors = append(allErrs.SchemaErrors, field.Required(fldPath.Child("x-kubernetes-validations").Index(i).Child("messageExpression"), "messageExpression must be non-empty if specified"))
  1194  			}
  1195  			if rule.Reason != nil && !supportedValidationReason.Has(string(*rule.Reason)) {
  1196  				allErrs.SchemaErrors = append(allErrs.SchemaErrors, field.NotSupported(fldPath.Child("x-kubernetes-validations").Index(i).Child("reason"), *rule.Reason, supportedValidationReason.List()))
  1197  			}
  1198  			trimmedFieldPath := strings.TrimSpace(rule.FieldPath)
  1199  			if len(rule.FieldPath) > 0 && len(trimmedFieldPath) == 0 {
  1200  				allErrs.SchemaErrors = append(allErrs.SchemaErrors, field.Invalid(fldPath.Child("x-kubernetes-validations").Index(i).Child("fieldPath"), rule.FieldPath, "fieldPath must be non-empty if specified"))
  1201  			}
  1202  			if hasNewlines(rule.FieldPath) {
  1203  				allErrs.SchemaErrors = append(allErrs.SchemaErrors, field.Invalid(fldPath.Child("x-kubernetes-validations").Index(i).Child("fieldPath"), rule.FieldPath, "fieldPath must not contain line breaks"))
  1204  			}
  1205  			if len(rule.FieldPath) > 0 {
  1206  				if !pathValid(schema, rule.FieldPath) {
  1207  					allErrs.SchemaErrors = append(allErrs.SchemaErrors, field.Invalid(fldPath.Child("x-kubernetes-validations").Index(i).Child("fieldPath"), rule.FieldPath, "fieldPath must be a valid path"))
  1208  				}
  1209  
  1210  			}
  1211  		}
  1212  
  1213  		// If any schema related validation errors have been found at this level or deeper, skip CEL expression validation.
  1214  		// Invalid OpenAPISchemas are not always possible to convert into valid CEL DeclTypes, and can lead to CEL
  1215  		// validation error messages that are not actionable (will go away once the schema errors are resolved) and that
  1216  		// are difficult for CEL expression authors to understand.
  1217  		if len(allErrs.SchemaErrors) == 0 && celContext != nil {
  1218  			typeInfo, err := celContext.TypeInfo()
  1219  			if err != nil {
  1220  				allErrs.CELErrors = append(allErrs.CELErrors, field.InternalError(fldPath.Child("x-kubernetes-validations"), fmt.Errorf("internal error: failed to construct type information for x-kubernetes-validations rules: %s", err)))
  1221  			} else if typeInfo == nil {
  1222  				allErrs.CELErrors = append(allErrs.CELErrors, field.InternalError(fldPath.Child("x-kubernetes-validations"), fmt.Errorf("internal error: failed to retrieve type information for x-kubernetes-validations")))
  1223  			} else {
  1224  				compResults, err := cel.Compile(typeInfo.Schema, typeInfo.DeclType, celconfig.PerCallLimit, opts.celEnvironmentSet, opts.preexistingExpressions)
  1225  				if err != nil {
  1226  					allErrs.CELErrors = append(allErrs.CELErrors, field.InternalError(fldPath.Child("x-kubernetes-validations"), err))
  1227  				} else {
  1228  					for i, cr := range compResults {
  1229  						expressionCost := getExpressionCost(cr, celContext)
  1230  						if !opts.suppressPerExpressionCost && expressionCost > StaticEstimatedCostLimit {
  1231  							costErrorMsg := getCostErrorMessage("estimated rule cost", expressionCost, StaticEstimatedCostLimit)
  1232  							allErrs.CELErrors = append(allErrs.CELErrors, field.Forbidden(fldPath.Child("x-kubernetes-validations").Index(i).Child("rule"), costErrorMsg))
  1233  						}
  1234  						if celContext.TotalCost != nil {
  1235  							celContext.TotalCost.ObserveExpressionCost(fldPath.Child("x-kubernetes-validations").Index(i).Child("rule"), expressionCost)
  1236  						}
  1237  						if cr.Error != nil {
  1238  							if cr.Error.Type == apiservercel.ErrorTypeRequired {
  1239  								allErrs.CELErrors = append(allErrs.CELErrors, field.Required(fldPath.Child("x-kubernetes-validations").Index(i).Child("rule"), cr.Error.Detail))
  1240  							} else {
  1241  								allErrs.CELErrors = append(allErrs.CELErrors, field.Invalid(fldPath.Child("x-kubernetes-validations").Index(i).Child("rule"), schema.XValidations[i], cr.Error.Detail))
  1242  							}
  1243  						}
  1244  						if cr.MessageExpressionError != nil {
  1245  							allErrs.CELErrors = append(allErrs.CELErrors, field.Invalid(fldPath.Child("x-kubernetes-validations").Index(i).Child("messageExpression"), schema.XValidations[i], cr.MessageExpressionError.Detail))
  1246  						} else {
  1247  							if cr.MessageExpression != nil {
  1248  								if !opts.suppressPerExpressionCost && cr.MessageExpressionMaxCost > StaticEstimatedCostLimit {
  1249  									costErrorMsg := getCostErrorMessage("estimated messageExpression cost", cr.MessageExpressionMaxCost, StaticEstimatedCostLimit)
  1250  									allErrs.CELErrors = append(allErrs.CELErrors, field.Forbidden(fldPath.Child("x-kubernetes-validations").Index(i).Child("messageExpression"), costErrorMsg))
  1251  								}
  1252  								if celContext.TotalCost != nil {
  1253  									celContext.TotalCost.ObserveExpressionCost(fldPath.Child("x-kubernetes-validations").Index(i).Child("messageExpression"), cr.MessageExpressionMaxCost)
  1254  								}
  1255  							}
  1256  						}
  1257  						if cr.UsesOldSelf {
  1258  							if uncorrelatablePath := ssv.forbidOldSelfValidations(); uncorrelatablePath != nil {
  1259  								allErrs.CELErrors = append(allErrs.CELErrors, field.Invalid(fldPath.Child("x-kubernetes-validations").Index(i).Child("rule"), schema.XValidations[i].Rule, fmt.Sprintf("oldSelf cannot be used on the uncorrelatable portion of the schema within %v", uncorrelatablePath)))
  1260  							}
  1261  						} else if schema.XValidations[i].OptionalOldSelf != nil {
  1262  							allErrs.CELErrors = append(allErrs.CELErrors, field.Invalid(fldPath.Child("x-kubernetes-validations").Index(i).Child("optionalOldSelf"), *schema.XValidations[i].OptionalOldSelf, "may not be set if oldSelf is not used in rule"))
  1263  						}
  1264  					}
  1265  				}
  1266  			}
  1267  		}
  1268  	}
  1269  
  1270  	return allErrs
  1271  }
  1272  
  1273  func pathValid(schema *apiextensions.JSONSchemaProps, path string) bool {
  1274  	// To avoid duplicated code and better maintain, using ValidaFieldPath func to check if the path is valid
  1275  	if ss, err := structuralschema.NewStructural(schema); err == nil {
  1276  		_, _, err := cel.ValidFieldPath(path, ss)
  1277  		return err == nil
  1278  	}
  1279  	return true
  1280  }
  1281  
  1282  // multiplyWithOverflowGuard returns the product of baseCost and cardinality unless that product
  1283  // would exceed math.MaxUint, in which case math.MaxUint is returned.
  1284  func multiplyWithOverflowGuard(baseCost, cardinality uint64) uint64 {
  1285  	if baseCost == 0 {
  1286  		// an empty rule can return 0, so guard for that here
  1287  		return 0
  1288  	} else if math.MaxUint/baseCost < cardinality {
  1289  		return math.MaxUint
  1290  	}
  1291  	return baseCost * cardinality
  1292  }
  1293  
  1294  func getExpressionCost(cr cel.CompilationResult, cardinalityCost *CELSchemaContext) uint64 {
  1295  	if cardinalityCost.MaxCardinality != unbounded {
  1296  		return multiplyWithOverflowGuard(cr.MaxCost, *cardinalityCost.MaxCardinality)
  1297  	}
  1298  	return multiplyWithOverflowGuard(cr.MaxCost, cr.MaxCardinality)
  1299  }
  1300  
  1301  func getCostErrorMessage(costName string, expressionCost, costLimit uint64) string {
  1302  	exceedFactor := float64(expressionCost) / float64(costLimit)
  1303  	var factor string
  1304  	if exceedFactor > 100.0 {
  1305  		// if exceedFactor is greater than 2 orders of magnitude, the rule is likely O(n^2) or worse
  1306  		// and will probably never validate without some set limits
  1307  		// also in such cases the cost estimation is generally large enough to not add any value
  1308  		factor = fmt.Sprintf("more than 100x")
  1309  	} else if exceedFactor < 1.5 {
  1310  		factor = fmt.Sprintf("%fx", exceedFactor) // avoid reporting "exceeds budge by a factor of 1.0x"
  1311  	} else {
  1312  		factor = fmt.Sprintf("%.1fx", exceedFactor)
  1313  	}
  1314  	return fmt.Sprintf("%s exceeds budget by factor of %s (try simplifying the rule, or adding maxItems, maxProperties, and maxLength where arrays, maps, and strings are declared)", costName, factor)
  1315  }
  1316  
  1317  var newlineMatcher = regexp.MustCompile(`[\n\r]+`) // valid newline chars in CEL grammar
  1318  func hasNewlines(s string) bool {
  1319  	return newlineMatcher.MatchString(s)
  1320  }
  1321  
  1322  func validateMapListKeysMapSet(schema *apiextensions.JSONSchemaProps, fldPath *field.Path) field.ErrorList {
  1323  	allErrs := field.ErrorList{}
  1324  
  1325  	if schema.Items == nil || schema.Items.Schema == nil {
  1326  		return nil
  1327  	}
  1328  	if schema.XListType == nil {
  1329  		return nil
  1330  	}
  1331  	if *schema.XListType != "set" && *schema.XListType != "map" {
  1332  		return nil
  1333  	}
  1334  
  1335  	// set and map list items cannot be nullable
  1336  	if schema.Items.Schema.Nullable {
  1337  		allErrs = append(allErrs, field.Forbidden(fldPath.Child("items").Child("nullable"), "cannot be nullable when x-kubernetes-list-type is "+*schema.XListType))
  1338  	}
  1339  
  1340  	switch *schema.XListType {
  1341  	case "map":
  1342  		// ensure all map keys are required or have a default
  1343  		isRequired := make(map[string]bool, len(schema.Items.Schema.Required))
  1344  		for _, required := range schema.Items.Schema.Required {
  1345  			isRequired[required] = true
  1346  		}
  1347  
  1348  		for _, k := range schema.XListMapKeys {
  1349  			obj, ok := schema.Items.Schema.Properties[k]
  1350  			if !ok {
  1351  				// we validate that all XListMapKeys are existing properties in ValidateCustomResourceDefinitionOpenAPISchema, so skipping here is ok
  1352  				continue
  1353  			}
  1354  
  1355  			if isRequired[k] == false && obj.Default == nil {
  1356  				allErrs = append(allErrs, field.Required(fldPath.Child("items").Child("properties").Key(k).Child("default"), "this property is in x-kubernetes-list-map-keys, so it must have a default or be a required property"))
  1357  			}
  1358  
  1359  			if obj.Nullable {
  1360  				allErrs = append(allErrs, field.Forbidden(fldPath.Child("items").Child("properties").Key(k).Child("nullable"), "this property is in x-kubernetes-list-map-keys, so it cannot be nullable"))
  1361  			}
  1362  		}
  1363  	case "set":
  1364  		// no other set-specific validation
  1365  	}
  1366  
  1367  	return allErrs
  1368  }
  1369  
  1370  type specStandardValidatorV3 struct {
  1371  	allowDefaults                       bool
  1372  	disallowDefaultsReason              string
  1373  	isInsideResourceMeta                bool
  1374  	requireValidPropertyType            bool
  1375  	uncorrelatableOldSelfValidationPath *field.Path
  1376  }
  1377  
  1378  func (v *specStandardValidatorV3) withForbiddenDefaults(reason string) specStandardValidator {
  1379  	clone := *v
  1380  	clone.disallowDefaultsReason = reason
  1381  	clone.allowDefaults = false
  1382  	return &clone
  1383  }
  1384  
  1385  func (v *specStandardValidatorV3) withInsideResourceMeta() specStandardValidator {
  1386  	clone := *v
  1387  	clone.isInsideResourceMeta = true
  1388  	return &clone
  1389  }
  1390  
  1391  func (v *specStandardValidatorV3) insideResourceMeta() bool {
  1392  	return v.isInsideResourceMeta
  1393  }
  1394  
  1395  func (v *specStandardValidatorV3) withForbidOldSelfValidations(path *field.Path) specStandardValidator {
  1396  	if v.uncorrelatableOldSelfValidationPath != nil {
  1397  		// oldSelf validations are already forbidden. preserve the highest-level path
  1398  		// causing oldSelf validations to be forbidden
  1399  		return v
  1400  	}
  1401  	clone := *v
  1402  	clone.uncorrelatableOldSelfValidationPath = path
  1403  	return &clone
  1404  }
  1405  
  1406  func (v *specStandardValidatorV3) forbidOldSelfValidations() *field.Path {
  1407  	return v.uncorrelatableOldSelfValidationPath
  1408  }
  1409  
  1410  // validate validates against OpenAPI Schema v3.
  1411  func (v *specStandardValidatorV3) validate(schema *apiextensions.JSONSchemaProps, fldPath *field.Path) field.ErrorList {
  1412  	allErrs := field.ErrorList{}
  1413  
  1414  	if schema == nil {
  1415  		return allErrs
  1416  	}
  1417  
  1418  	//
  1419  	// WARNING: if anything new is allowed below, NewStructural must be adapted to support it.
  1420  	//
  1421  
  1422  	if v.requireValidPropertyType && len(schema.Type) > 0 && !openapiV3Types.Has(schema.Type) {
  1423  		allErrs = append(allErrs, field.NotSupported(fldPath.Child("type"), schema.Type, openapiV3Types.List()))
  1424  	}
  1425  
  1426  	if schema.Default != nil && !v.allowDefaults {
  1427  		detail := "must not be set"
  1428  		if len(v.disallowDefaultsReason) > 0 {
  1429  			detail += " " + v.disallowDefaultsReason
  1430  		}
  1431  		allErrs = append(allErrs, field.Forbidden(fldPath.Child("default"), detail))
  1432  	}
  1433  
  1434  	if schema.ID != "" {
  1435  		allErrs = append(allErrs, field.Forbidden(fldPath.Child("id"), "id is not supported"))
  1436  	}
  1437  
  1438  	if schema.AdditionalItems != nil {
  1439  		allErrs = append(allErrs, field.Forbidden(fldPath.Child("additionalItems"), "additionalItems is not supported"))
  1440  	}
  1441  
  1442  	if len(schema.PatternProperties) != 0 {
  1443  		allErrs = append(allErrs, field.Forbidden(fldPath.Child("patternProperties"), "patternProperties is not supported"))
  1444  	}
  1445  
  1446  	if len(schema.Definitions) != 0 {
  1447  		allErrs = append(allErrs, field.Forbidden(fldPath.Child("definitions"), "definitions is not supported"))
  1448  	}
  1449  
  1450  	if schema.Dependencies != nil {
  1451  		allErrs = append(allErrs, field.Forbidden(fldPath.Child("dependencies"), "dependencies is not supported"))
  1452  	}
  1453  
  1454  	if schema.Ref != nil {
  1455  		allErrs = append(allErrs, field.Forbidden(fldPath.Child("$ref"), "$ref is not supported"))
  1456  	}
  1457  
  1458  	if schema.Type == "null" {
  1459  		allErrs = append(allErrs, field.Forbidden(fldPath.Child("type"), "type cannot be set to null, use nullable as an alternative"))
  1460  	}
  1461  
  1462  	if schema.Items != nil && len(schema.Items.JSONSchemas) != 0 {
  1463  		allErrs = append(allErrs, field.Forbidden(fldPath.Child("items"), "items must be a schema object and not an array"))
  1464  	}
  1465  
  1466  	if v.isInsideResourceMeta && schema.XEmbeddedResource {
  1467  		allErrs = append(allErrs, field.Forbidden(fldPath.Child("x-kubernetes-embedded-resource"), "must not be used inside of resource meta"))
  1468  	}
  1469  
  1470  	return allErrs
  1471  }
  1472  
  1473  // ValidateCustomResourceDefinitionSubresources statically validates
  1474  func ValidateCustomResourceDefinitionSubresources(subresources *apiextensions.CustomResourceSubresources, fldPath *field.Path) field.ErrorList {
  1475  	allErrs := field.ErrorList{}
  1476  
  1477  	if subresources == nil {
  1478  		return allErrs
  1479  	}
  1480  
  1481  	if subresources.Scale != nil {
  1482  		if len(subresources.Scale.SpecReplicasPath) == 0 {
  1483  			allErrs = append(allErrs, field.Required(fldPath.Child("scale.specReplicasPath"), ""))
  1484  		} else {
  1485  			// should be constrained json path under .spec
  1486  			if errs := validateSimpleJSONPath(subresources.Scale.SpecReplicasPath, fldPath.Child("scale.specReplicasPath")); len(errs) > 0 {
  1487  				allErrs = append(allErrs, errs...)
  1488  			} else if !strings.HasPrefix(subresources.Scale.SpecReplicasPath, ".spec.") {
  1489  				allErrs = append(allErrs, field.Invalid(fldPath.Child("scale.specReplicasPath"), subresources.Scale.SpecReplicasPath, "should be a json path under .spec"))
  1490  			}
  1491  		}
  1492  
  1493  		if len(subresources.Scale.StatusReplicasPath) == 0 {
  1494  			allErrs = append(allErrs, field.Required(fldPath.Child("scale.statusReplicasPath"), ""))
  1495  		} else {
  1496  			// should be constrained json path under .status
  1497  			if errs := validateSimpleJSONPath(subresources.Scale.StatusReplicasPath, fldPath.Child("scale.statusReplicasPath")); len(errs) > 0 {
  1498  				allErrs = append(allErrs, errs...)
  1499  			} else if !strings.HasPrefix(subresources.Scale.StatusReplicasPath, ".status.") {
  1500  				allErrs = append(allErrs, field.Invalid(fldPath.Child("scale.statusReplicasPath"), subresources.Scale.StatusReplicasPath, "should be a json path under .status"))
  1501  			}
  1502  		}
  1503  
  1504  		// if labelSelectorPath is present, it should be a constrained json path under .status
  1505  		if subresources.Scale.LabelSelectorPath != nil && len(*subresources.Scale.LabelSelectorPath) > 0 {
  1506  			if errs := validateSimpleJSONPath(*subresources.Scale.LabelSelectorPath, fldPath.Child("scale.labelSelectorPath")); len(errs) > 0 {
  1507  				allErrs = append(allErrs, errs...)
  1508  			} else if !strings.HasPrefix(*subresources.Scale.LabelSelectorPath, ".spec.") && !strings.HasPrefix(*subresources.Scale.LabelSelectorPath, ".status.") {
  1509  				allErrs = append(allErrs, field.Invalid(fldPath.Child("scale.labelSelectorPath"), subresources.Scale.LabelSelectorPath, "should be a json path under either .spec or .status"))
  1510  			}
  1511  		}
  1512  	}
  1513  
  1514  	return allErrs
  1515  }
  1516  
  1517  func validateSimpleJSONPath(s string, fldPath *field.Path) field.ErrorList {
  1518  	allErrs := field.ErrorList{}
  1519  
  1520  	switch {
  1521  	case len(s) == 0:
  1522  		allErrs = append(allErrs, field.Invalid(fldPath, s, "must not be empty"))
  1523  	case s[0] != '.':
  1524  		allErrs = append(allErrs, field.Invalid(fldPath, s, "must be a simple json path starting with ."))
  1525  	case s != ".":
  1526  		if cs := strings.Split(s[1:], "."); len(cs) < 1 {
  1527  			allErrs = append(allErrs, field.Invalid(fldPath, s, "must be a json path in the dot notation"))
  1528  		}
  1529  	}
  1530  
  1531  	return allErrs
  1532  }
  1533  
  1534  var allowedFieldsAtRootSchema = []string{"Description", "Type", "Format", "Title", "Maximum", "ExclusiveMaximum", "Minimum", "ExclusiveMinimum", "MaxLength", "MinLength", "Pattern", "MaxItems", "MinItems", "UniqueItems", "MultipleOf", "Required", "Items", "Properties", "ExternalDocs", "Example", "XPreserveUnknownFields", "XValidations"}
  1535  
  1536  func allowedAtRootSchema(field string) bool {
  1537  	for _, v := range allowedFieldsAtRootSchema {
  1538  		if field == v {
  1539  			return true
  1540  		}
  1541  	}
  1542  	return false
  1543  }
  1544  
  1545  // requireOpenAPISchema returns true if the request group version requires a schema
  1546  func requireOpenAPISchema(oldCRDSpec *apiextensions.CustomResourceDefinitionSpec) bool {
  1547  	if oldCRDSpec != nil && !allVersionsSpecifyOpenAPISchema(oldCRDSpec) {
  1548  		// don't tighten validation on existing persisted data
  1549  		return false
  1550  	}
  1551  	return true
  1552  }
  1553  func allVersionsSpecifyOpenAPISchema(spec *apiextensions.CustomResourceDefinitionSpec) bool {
  1554  	if spec.Validation != nil && spec.Validation.OpenAPIV3Schema != nil {
  1555  		return true
  1556  	}
  1557  	for _, v := range spec.Versions {
  1558  		if v.Schema == nil || v.Schema.OpenAPIV3Schema == nil {
  1559  			return false
  1560  		}
  1561  	}
  1562  	return true
  1563  }
  1564  
  1565  func specHasDefaults(spec *apiextensions.CustomResourceDefinitionSpec) bool {
  1566  	return HasSchemaWith(spec, schemaHasDefaults)
  1567  }
  1568  
  1569  func schemaHasDefaults(s *apiextensions.JSONSchemaProps) bool {
  1570  	return SchemaHas(s, func(s *apiextensions.JSONSchemaProps) bool {
  1571  		return s.Default != nil
  1572  	})
  1573  }
  1574  
  1575  func HasSchemaWith(spec *apiextensions.CustomResourceDefinitionSpec, pred func(s *apiextensions.JSONSchemaProps) bool) bool {
  1576  	if spec.Validation != nil && spec.Validation.OpenAPIV3Schema != nil && pred(spec.Validation.OpenAPIV3Schema) {
  1577  		return true
  1578  	}
  1579  	for _, v := range spec.Versions {
  1580  		if v.Schema != nil && v.Schema.OpenAPIV3Schema != nil && pred(v.Schema.OpenAPIV3Schema) {
  1581  			return true
  1582  		}
  1583  	}
  1584  	return false
  1585  }
  1586  
  1587  var schemaPool = sync.Pool{
  1588  	New: func() any {
  1589  		return new(apiextensions.JSONSchemaProps)
  1590  	},
  1591  }
  1592  
  1593  func schemaHasRecurse(s *apiextensions.JSONSchemaProps, pred func(s *apiextensions.JSONSchemaProps) bool) bool {
  1594  	if s == nil {
  1595  		return false
  1596  	}
  1597  	schema := schemaPool.Get().(*apiextensions.JSONSchemaProps)
  1598  	defer schemaPool.Put(schema)
  1599  	*schema = *s
  1600  	return SchemaHas(schema, pred)
  1601  }
  1602  
  1603  // SchemaHas recursively traverses the Schema and calls the `pred`
  1604  // predicate to see if the schema contains specific values.
  1605  //
  1606  // The predicate MUST NOT keep a copy of the json schema NOR modify the
  1607  // schema.
  1608  func SchemaHas(s *apiextensions.JSONSchemaProps, pred func(s *apiextensions.JSONSchemaProps) bool) bool {
  1609  	if s == nil {
  1610  		return false
  1611  	}
  1612  
  1613  	if pred(s) {
  1614  		return true
  1615  	}
  1616  
  1617  	if s.Items != nil {
  1618  		if s.Items != nil && schemaHasRecurse(s.Items.Schema, pred) {
  1619  			return true
  1620  		}
  1621  		for i := range s.Items.JSONSchemas {
  1622  			if schemaHasRecurse(&s.Items.JSONSchemas[i], pred) {
  1623  				return true
  1624  			}
  1625  		}
  1626  	}
  1627  	for i := range s.AllOf {
  1628  		if schemaHasRecurse(&s.AllOf[i], pred) {
  1629  			return true
  1630  		}
  1631  	}
  1632  	for i := range s.AnyOf {
  1633  		if schemaHasRecurse(&s.AnyOf[i], pred) {
  1634  			return true
  1635  		}
  1636  	}
  1637  	for i := range s.OneOf {
  1638  		if schemaHasRecurse(&s.OneOf[i], pred) {
  1639  			return true
  1640  		}
  1641  	}
  1642  	if schemaHasRecurse(s.Not, pred) {
  1643  		return true
  1644  	}
  1645  	for _, s := range s.Properties {
  1646  		if schemaHasRecurse(&s, pred) {
  1647  			return true
  1648  		}
  1649  	}
  1650  	if s.AdditionalProperties != nil {
  1651  		if schemaHasRecurse(s.AdditionalProperties.Schema, pred) {
  1652  			return true
  1653  		}
  1654  	}
  1655  	for _, s := range s.PatternProperties {
  1656  		if schemaHasRecurse(&s, pred) {
  1657  			return true
  1658  		}
  1659  	}
  1660  	if s.AdditionalItems != nil {
  1661  		if schemaHasRecurse(s.AdditionalItems.Schema, pred) {
  1662  			return true
  1663  		}
  1664  	}
  1665  	for _, s := range s.Definitions {
  1666  		if schemaHasRecurse(&s, pred) {
  1667  			return true
  1668  		}
  1669  	}
  1670  	for _, d := range s.Dependencies {
  1671  		if schemaHasRecurse(d.Schema, pred) {
  1672  			return true
  1673  		}
  1674  	}
  1675  
  1676  	return false
  1677  }
  1678  
  1679  func specHasKubernetesExtensions(spec *apiextensions.CustomResourceDefinitionSpec) bool {
  1680  	if spec.Validation != nil && schemaHasKubernetesExtensions(spec.Validation.OpenAPIV3Schema) {
  1681  		return true
  1682  	}
  1683  	for _, v := range spec.Versions {
  1684  		if v.Schema != nil && schemaHasKubernetesExtensions(v.Schema.OpenAPIV3Schema) {
  1685  			return true
  1686  		}
  1687  	}
  1688  	return false
  1689  }
  1690  
  1691  func schemaHasKubernetesExtensions(s *apiextensions.JSONSchemaProps) bool {
  1692  	return SchemaHas(s, func(s *apiextensions.JSONSchemaProps) bool {
  1693  		return s.XEmbeddedResource || s.XPreserveUnknownFields != nil || s.XIntOrString || len(s.XListMapKeys) > 0 || s.XListType != nil || len(s.XValidations) > 0
  1694  	})
  1695  }
  1696  
  1697  // requireStructuralSchema returns true if schemas specified must be structural
  1698  func requireStructuralSchema(oldCRDSpec *apiextensions.CustomResourceDefinitionSpec) bool {
  1699  	if oldCRDSpec != nil && specHasNonStructuralSchema(oldCRDSpec) {
  1700  		// don't tighten validation on existing persisted data
  1701  		return false
  1702  	}
  1703  	return true
  1704  }
  1705  
  1706  func specHasNonStructuralSchema(spec *apiextensions.CustomResourceDefinitionSpec) bool {
  1707  	if spec.Validation != nil && schemaIsNonStructural(spec.Validation.OpenAPIV3Schema) {
  1708  		return true
  1709  	}
  1710  	for _, v := range spec.Versions {
  1711  		if v.Schema != nil && schemaIsNonStructural(v.Schema.OpenAPIV3Schema) {
  1712  			return true
  1713  		}
  1714  	}
  1715  	return false
  1716  }
  1717  func schemaIsNonStructural(schema *apiextensions.JSONSchemaProps) bool {
  1718  	if schema == nil {
  1719  		return false
  1720  	}
  1721  	ss, err := structuralschema.NewStructural(schema)
  1722  	if err != nil {
  1723  		return true
  1724  	}
  1725  	return len(structuralschema.ValidateStructural(nil, ss)) > 0
  1726  }
  1727  
  1728  // requirePrunedDefaults returns false if there are any unpruned default in oldCRDSpec, and true otherwise.
  1729  func requirePrunedDefaults(oldCRDSpec *apiextensions.CustomResourceDefinitionSpec) bool {
  1730  	if oldCRDSpec.Validation != nil {
  1731  		if has, err := schemaHasUnprunedDefaults(oldCRDSpec.Validation.OpenAPIV3Schema); err == nil && has {
  1732  			return false
  1733  		}
  1734  	}
  1735  	for _, v := range oldCRDSpec.Versions {
  1736  		if v.Schema == nil {
  1737  			continue
  1738  		}
  1739  		if has, err := schemaHasUnprunedDefaults(v.Schema.OpenAPIV3Schema); err == nil && has {
  1740  			return false
  1741  		}
  1742  	}
  1743  	return true
  1744  }
  1745  func schemaHasUnprunedDefaults(schema *apiextensions.JSONSchemaProps) (bool, error) {
  1746  	if schema == nil || !schemaHasDefaults(schema) {
  1747  		return false, nil
  1748  	}
  1749  	ss, err := structuralschema.NewStructural(schema)
  1750  	if err != nil {
  1751  		return false, err
  1752  	}
  1753  	if errs := structuralschema.ValidateStructural(nil, ss); len(errs) > 0 {
  1754  		return false, errs.ToAggregate()
  1755  	}
  1756  	pruned := ss.DeepCopy()
  1757  	if err := structuraldefaulting.PruneDefaults(pruned); err != nil {
  1758  		return false, err
  1759  	}
  1760  	return !reflect.DeepEqual(ss, pruned), nil
  1761  }
  1762  
  1763  // requireAtomicSetType returns true if the old CRD spec as at least one x-kubernetes-list-type=set with non-atomic items type.
  1764  func requireAtomicSetType(oldCRDSpec *apiextensions.CustomResourceDefinitionSpec) bool {
  1765  	return !HasSchemaWith(oldCRDSpec, hasNonAtomicSetType)
  1766  }
  1767  
  1768  // hasNonAtomicSetType recurses over the schema and returns whether any list of type "set" as non-atomic item types.
  1769  func hasNonAtomicSetType(schema *apiextensions.JSONSchemaProps) bool {
  1770  	return SchemaHas(schema, func(schema *apiextensions.JSONSchemaProps) bool {
  1771  		if schema.XListType != nil && *schema.XListType == "set" && schema.Items != nil && schema.Items.Schema != nil { // we don't support schema.Items.JSONSchemas
  1772  			is := schema.Items.Schema
  1773  			switch is.Type {
  1774  			case "array":
  1775  				return is.XListType != nil && *is.XListType != "atomic" // atomic is the implicit default behaviour if unset, hence != atomic is wrong
  1776  			case "object":
  1777  				return is.XMapType == nil || *is.XMapType != "atomic" // granular is the implicit default behaviour if unset, hence nil and != atomic are wrong
  1778  			default:
  1779  				return false // scalar types are always atomic
  1780  			}
  1781  		}
  1782  		return false
  1783  	})
  1784  }
  1785  
  1786  func requireMapListKeysMapSetValidation(oldCRDSpec *apiextensions.CustomResourceDefinitionSpec) bool {
  1787  	return !HasSchemaWith(oldCRDSpec, hasInvalidMapListKeysMapSet)
  1788  }
  1789  
  1790  func hasInvalidMapListKeysMapSet(schema *apiextensions.JSONSchemaProps) bool {
  1791  	return SchemaHas(schema, func(schema *apiextensions.JSONSchemaProps) bool {
  1792  		return len(validateMapListKeysMapSet(schema, field.NewPath(""))) > 0
  1793  	})
  1794  }
  1795  
  1796  // requireValidPropertyType returns true if valid openapi v3 types should be required for the given API version
  1797  func requireValidPropertyType(oldCRDSpec *apiextensions.CustomResourceDefinitionSpec) bool {
  1798  	if oldCRDSpec != nil && specHasInvalidTypes(oldCRDSpec) {
  1799  		// don't tighten validation on existing persisted data
  1800  		return false
  1801  	}
  1802  	return true
  1803  }
  1804  
  1805  // validateAPIApproval returns a list of errors if the API approval annotation isn't valid
  1806  func validateAPIApproval(newCRD, oldCRD *apiextensions.CustomResourceDefinition) field.ErrorList {
  1807  	// check to see if we need confirm API approval for kube group.
  1808  	if !apihelpers.IsProtectedCommunityGroup(newCRD.Spec.Group) {
  1809  		// no-op for non-protected groups
  1810  		return nil
  1811  	}
  1812  
  1813  	// default to a state that allows missing values to continue to be missing
  1814  	var oldApprovalState *apihelpers.APIApprovalState
  1815  	if oldCRD != nil {
  1816  		t, _ := apihelpers.GetAPIApprovalState(oldCRD.Annotations)
  1817  		oldApprovalState = &t // +k8s:verify-mutation:reason=clone
  1818  	}
  1819  	newApprovalState, reason := apihelpers.GetAPIApprovalState(newCRD.Annotations)
  1820  
  1821  	// if the approval state hasn't changed, never fail on approval validation
  1822  	// 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.
  1823  	if oldApprovalState != nil && *oldApprovalState == newApprovalState {
  1824  		return nil
  1825  	}
  1826  
  1827  	// in v1, we require valid approval strings
  1828  	switch newApprovalState {
  1829  	case apihelpers.APIApprovalInvalid:
  1830  		return field.ErrorList{field.Invalid(field.NewPath("metadata", "annotations").Key(apiextensionsv1beta1.KubeAPIApprovedAnnotation), newCRD.Annotations[apiextensionsv1beta1.KubeAPIApprovedAnnotation], reason)}
  1831  	case apihelpers.APIApprovalMissing:
  1832  		return field.ErrorList{field.Required(field.NewPath("metadata", "annotations").Key(apiextensionsv1beta1.KubeAPIApprovedAnnotation), reason)}
  1833  	case apihelpers.APIApproved, apihelpers.APIApprovalBypassed:
  1834  		// success
  1835  		return nil
  1836  	default:
  1837  		return field.ErrorList{field.Invalid(field.NewPath("metadata", "annotations").Key(apiextensionsv1beta1.KubeAPIApprovedAnnotation), newCRD.Annotations[apiextensionsv1beta1.KubeAPIApprovedAnnotation], reason)}
  1838  	}
  1839  }
  1840  
  1841  func validatePreserveUnknownFields(crd, oldCRD *apiextensions.CustomResourceDefinition) field.ErrorList {
  1842  	if oldCRD != nil && oldCRD.Spec.PreserveUnknownFields != nil && *oldCRD.Spec.PreserveUnknownFields {
  1843  		// no-op for compatibility with existing data
  1844  		return nil
  1845  	}
  1846  
  1847  	var errs field.ErrorList
  1848  	if crd != nil && crd.Spec.PreserveUnknownFields != nil && *crd.Spec.PreserveUnknownFields {
  1849  		// disallow changing spec.preserveUnknownFields=false to spec.preserveUnknownFields=true
  1850  		errs = append(errs, field.Invalid(field.NewPath("spec").Child("preserveUnknownFields"), crd.Spec.PreserveUnknownFields, "cannot set to true, set x-kubernetes-preserve-unknown-fields to true in spec.versions[*].schema instead"))
  1851  	}
  1852  	return errs
  1853  }
  1854  
  1855  func specHasInvalidTypes(spec *apiextensions.CustomResourceDefinitionSpec) bool {
  1856  	if spec.Validation != nil && SchemaHasInvalidTypes(spec.Validation.OpenAPIV3Schema) {
  1857  		return true
  1858  	}
  1859  	for _, v := range spec.Versions {
  1860  		if v.Schema != nil && SchemaHasInvalidTypes(v.Schema.OpenAPIV3Schema) {
  1861  			return true
  1862  		}
  1863  	}
  1864  	return false
  1865  }
  1866  
  1867  // SchemaHasInvalidTypes returns true if it contains invalid offending openapi-v3 specification.
  1868  func SchemaHasInvalidTypes(s *apiextensions.JSONSchemaProps) bool {
  1869  	return SchemaHas(s, func(s *apiextensions.JSONSchemaProps) bool {
  1870  		return len(s.Type) > 0 && !openapiV3Types.Has(s.Type)
  1871  	})
  1872  }
  1873  

View as plain text