1
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
59 StaticEstimatedCostLimit = 10000000
60
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
74
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
108 type validationOptions struct {
109
110 allowDefaults bool
111
112 disallowDefaultsReason string
113
114 requireRecognizedConversionReviewVersion bool
115
116 requireImmutableNames bool
117
118 requireOpenAPISchema bool
119
120 requireValidPropertyType bool
121
122 requireStructuralSchema bool
123
124 requirePrunedDefaults bool
125
126 requireAtomicSetType bool
127
128
129
130 requireMapListKeysMapSetValidation bool
131
132 preexistingExpressions preexistingExpressions
133
134
135
136 versionsWithUnchangedSchemas sets.Set[string]
137
138
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
189
190
191
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
211
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
220
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
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
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
284
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
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
356 if spec.PreserveUnknownFields == nil || *spec.PreserveUnknownFields == false {
357 opts.requireStructuralSchema = true
358 }
359
360 if opts.requireOpenAPISchema {
361
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
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
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
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
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
507
508
509
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
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
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
597
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
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
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
615
616
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
630
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
644 func hasStatusEnabled(subresources *apiextensions.CustomResourceSubresources) bool {
645 if subresources != nil && subresources.Status != nil {
646 return true
647 }
648 return false
649 }
650
651
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
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
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
682
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
697
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
712
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
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
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
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
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
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
842 type specStandardValidator interface {
843 validate(spec *apiextensions.JSONSchemaProps, fldPath *field.Path) field.ErrorList
844 withForbiddenDefaults(reason string) specStandardValidator
845
846
847 insideResourceMeta() bool
848 withInsideResourceMeta() specStandardValidator
849
850
851 forbidOldSelfValidations() *field.Path
852 withForbidOldSelfValidations(path *field.Path) specStandardValidator
853 }
854
855
856
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
866
867 if statusSubresourceEnabled {
868 v := reflect.ValueOf(schema).Elem()
869 for i := 0; i < v.NumField(); i++ {
870
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
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
908
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
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
919
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
927
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
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
956
957 type OpenAPISchemaErrorList struct {
958 SchemaErrors field.ErrorList
959 CELErrors field.ErrorList
960 }
961
962
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
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
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
993
994
995
996
997
998
999
1000
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
1008
1009 subSsv := ssv
1010 if ssv.insideResourceMeta() {
1011
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
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
1071
1072
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 {
1115 is := schema.Items.Schema
1116 switch is.Type {
1117 case "array":
1118 if is.XListType != nil && *is.XListType != "atomic" {
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" {
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
1214
1215
1216
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
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
1283
1284 func multiplyWithOverflowGuard(baseCost, cardinality uint64) uint64 {
1285 if baseCost == 0 {
1286
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
1306
1307
1308 factor = fmt.Sprintf("more than 100x")
1309 } else if exceedFactor < 1.5 {
1310 factor = fmt.Sprintf("%fx", exceedFactor)
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]+`)
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
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
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
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
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
1398
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
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
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
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
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
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
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
1546 func requireOpenAPISchema(oldCRDSpec *apiextensions.CustomResourceDefinitionSpec) bool {
1547 if oldCRDSpec != nil && !allVersionsSpecifyOpenAPISchema(oldCRDSpec) {
1548
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
1604
1605
1606
1607
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
1698 func requireStructuralSchema(oldCRDSpec *apiextensions.CustomResourceDefinitionSpec) bool {
1699 if oldCRDSpec != nil && specHasNonStructuralSchema(oldCRDSpec) {
1700
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
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
1764 func requireAtomicSetType(oldCRDSpec *apiextensions.CustomResourceDefinitionSpec) bool {
1765 return !HasSchemaWith(oldCRDSpec, hasNonAtomicSetType)
1766 }
1767
1768
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 {
1772 is := schema.Items.Schema
1773 switch is.Type {
1774 case "array":
1775 return is.XListType != nil && *is.XListType != "atomic"
1776 case "object":
1777 return is.XMapType == nil || *is.XMapType != "atomic"
1778 default:
1779 return false
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
1797 func requireValidPropertyType(oldCRDSpec *apiextensions.CustomResourceDefinitionSpec) bool {
1798 if oldCRDSpec != nil && specHasInvalidTypes(oldCRDSpec) {
1799
1800 return false
1801 }
1802 return true
1803 }
1804
1805
1806 func validateAPIApproval(newCRD, oldCRD *apiextensions.CustomResourceDefinition) field.ErrorList {
1807
1808 if !apihelpers.IsProtectedCommunityGroup(newCRD.Spec.Group) {
1809
1810 return nil
1811 }
1812
1813
1814 var oldApprovalState *apihelpers.APIApprovalState
1815 if oldCRD != nil {
1816 t, _ := apihelpers.GetAPIApprovalState(oldCRD.Annotations)
1817 oldApprovalState = &t
1818 }
1819 newApprovalState, reason := apihelpers.GetAPIApprovalState(newCRD.Annotations)
1820
1821
1822
1823 if oldApprovalState != nil && *oldApprovalState == newApprovalState {
1824 return nil
1825 }
1826
1827
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
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
1844 return nil
1845 }
1846
1847 var errs field.ErrorList
1848 if crd != nil && crd.Spec.PreserveUnknownFields != nil && *crd.Spec.PreserveUnknownFields {
1849
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
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