Skip to content

Commit 3a50c00

Browse files
authored
Merge pull request kubernetes#78788 from sttts/sttts-crd-embedded-resource
apiextensions: validate x-kubernetes-embedded-resource in CRs
2 parents 3d4124f + d86cc85 commit 3a50c00

File tree

30 files changed

+3288
-408
lines changed

30 files changed

+3288
-408
lines changed

staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ go_library(
1515
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions:go_default_library",
1616
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1:go_default_library",
1717
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema:go_default_library",
18+
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/objectmeta:go_default_library",
1819
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/pruning:go_default_library",
1920
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/validation:go_default_library",
2021
"//staging/src/k8s.io/apiextensions-apiserver/pkg/features:go_default_library",

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

Lines changed: 169 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323

2424
"github.com/go-openapi/strfmt"
2525
govalidate "github.com/go-openapi/validate"
26+
schemaobjectmeta "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/objectmeta"
2627

2728
apiequality "k8s.io/apimachinery/pkg/api/equality"
2829
genericvalidation "k8s.io/apimachinery/pkg/api/validation"
@@ -156,6 +157,9 @@ func validateCustomResourceDefinitionSpec(spec *apiextensions.CustomResourceDefi
156157
allErrs = append(allErrs, field.Invalid(fldPath.Child("preserveUnknownFields"), true, "must be false in order to use defaults in the schema"))
157158
}
158159
}
160+
if specHasKubernetesExtensions(spec) {
161+
mustBeStructural = true
162+
}
159163

160164
storageFlagCount := 0
161165
versionsMap := map[string]bool{}
@@ -562,6 +566,11 @@ func ValidateCustomResourceColumnDefinition(col *apiextensions.CustomResourceCol
562566
// specStandardValidator applies validations for different OpenAPI specification versions.
563567
type specStandardValidator interface {
564568
validate(spec *apiextensions.JSONSchemaProps, fldPath *field.Path) field.ErrorList
569+
withForbiddenDefaults(reason string) specStandardValidator
570+
571+
// insideResourceMeta returns true when validating either TypeMeta or ObjectMeta, from an embedded resource or on the top-level.
572+
insideResourceMeta() bool
573+
withInsideResourceMeta() specStandardValidator
565574
}
566575

567576
// ValidateCustomResourceDefinitionValidation statically validates
@@ -608,7 +617,7 @@ func ValidateCustomResourceDefinitionValidation(customResourceValidation *apiext
608617
openAPIV3Schema := &specStandardValidatorV3{
609618
allowDefaults: allowDefaults,
610619
}
611-
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(schema, fldPath.Child("openAPIV3Schema"), openAPIV3Schema)...)
620+
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(schema, fldPath.Child("openAPIV3Schema"), openAPIV3Schema, true)...)
612621

613622
if mustBeStructural {
614623
if ss, err := structuralschema.NewStructural(schema); err != nil {
@@ -631,8 +640,10 @@ func ValidateCustomResourceDefinitionValidation(customResourceValidation *apiext
631640
return allErrs
632641
}
633642

643+
var metaFields = sets.NewString("metadata", "apiVersion", "kind")
644+
634645
// ValidateCustomResourceDefinitionOpenAPISchema statically validates
635-
func ValidateCustomResourceDefinitionOpenAPISchema(schema *apiextensions.JSONSchemaProps, fldPath *field.Path, ssv specStandardValidator) field.ErrorList {
646+
func ValidateCustomResourceDefinitionOpenAPISchema(schema *apiextensions.JSONSchemaProps, fldPath *field.Path, ssv specStandardValidator, isRoot bool) field.ErrorList {
636647
allErrs := field.ErrorList{}
637648

638649
if schema == nil {
@@ -660,63 +671,68 @@ func ValidateCustomResourceDefinitionOpenAPISchema(schema *apiextensions.JSONSch
660671
allErrs = append(allErrs, field.Forbidden(fldPath.Child("additionalProperties"), "additionalProperties and properties are mutual exclusive"))
661672
}
662673
}
663-
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(schema.AdditionalProperties.Schema, fldPath.Child("additionalProperties"), ssv)...)
674+
// Note: we forbid additionalProperties at resource root, both embedded and top-level.
675+
// But further inside, additionalProperites is possible, e.g. for labels or annotations.
676+
subSsv := ssv
677+
if ssv.insideResourceMeta() {
678+
// we have to forbid defaults inside additionalProperties because pruning without actual value is ambiguous
679+
subSsv = ssv.withForbiddenDefaults("inside additionalProperties applying to object metadata")
680+
}
681+
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(schema.AdditionalProperties.Schema, fldPath.Child("additionalProperties"), subSsv, false)...)
664682
}
665683

666684
if len(schema.Properties) != 0 {
667685
for property, jsonSchema := range schema.Properties {
668-
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(&jsonSchema, fldPath.Child("properties").Key(property), ssv)...)
686+
subSsv := ssv
687+
if (isRoot || schema.XEmbeddedResource) && metaFields.Has(property) {
688+
// we recurse into the schema that applies to ObjectMeta.
689+
subSsv = ssv.withInsideResourceMeta()
690+
if isRoot {
691+
subSsv = subSsv.withForbiddenDefaults(fmt.Sprintf("in top-level %s", property))
692+
}
693+
}
694+
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(&jsonSchema, fldPath.Child("properties").Key(property), subSsv, false)...)
669695
}
670696
}
671697

672-
if len(schema.PatternProperties) != 0 {
673-
for property, jsonSchema := range schema.PatternProperties {
674-
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(&jsonSchema, fldPath.Child("patternProperties").Key(property), ssv)...)
675-
}
676-
}
677-
678-
if schema.AdditionalItems != nil {
679-
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(schema.AdditionalItems.Schema, fldPath.Child("additionalItems"), ssv)...)
680-
}
681-
682-
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(schema.Not, fldPath.Child("not"), ssv)...)
698+
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(schema.Not, fldPath.Child("not"), ssv, false)...)
683699

684700
if len(schema.AllOf) != 0 {
685701
for i, jsonSchema := range schema.AllOf {
686-
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(&jsonSchema, fldPath.Child("allOf").Index(i), ssv)...)
702+
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(&jsonSchema, fldPath.Child("allOf").Index(i), ssv, false)...)
687703
}
688704
}
689705

690706
if len(schema.OneOf) != 0 {
691707
for i, jsonSchema := range schema.OneOf {
692-
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(&jsonSchema, fldPath.Child("oneOf").Index(i), ssv)...)
708+
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(&jsonSchema, fldPath.Child("oneOf").Index(i), ssv, false)...)
693709
}
694710
}
695711

696712
if len(schema.AnyOf) != 0 {
697713
for i, jsonSchema := range schema.AnyOf {
698-
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(&jsonSchema, fldPath.Child("anyOf").Index(i), ssv)...)
714+
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(&jsonSchema, fldPath.Child("anyOf").Index(i), ssv, false)...)
699715
}
700716
}
701717

702718
if len(schema.Definitions) != 0 {
703719
for definition, jsonSchema := range schema.Definitions {
704-
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(&jsonSchema, fldPath.Child("definitions").Key(definition), ssv)...)
720+
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(&jsonSchema, fldPath.Child("definitions").Key(definition), ssv, false)...)
705721
}
706722
}
707723

708724
if schema.Items != nil {
709-
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(schema.Items.Schema, fldPath.Child("items"), ssv)...)
725+
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(schema.Items.Schema, fldPath.Child("items"), ssv, false)...)
710726
if len(schema.Items.JSONSchemas) != 0 {
711727
for i, jsonSchema := range schema.Items.JSONSchemas {
712-
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(&jsonSchema, fldPath.Child("items").Index(i), ssv)...)
728+
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(&jsonSchema, fldPath.Child("items").Index(i), ssv, false)...)
713729
}
714730
}
715731
}
716732

717733
if schema.Dependencies != nil {
718734
for dependency, jsonSchemaPropsOrStringArray := range schema.Dependencies {
719-
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(jsonSchemaPropsOrStringArray.Schema, fldPath.Child("dependencies").Key(dependency), ssv)...)
735+
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(jsonSchemaPropsOrStringArray.Schema, fldPath.Child("dependencies").Key(dependency), ssv, false)...)
720736
}
721737
}
722738

@@ -728,7 +744,26 @@ func ValidateCustomResourceDefinitionOpenAPISchema(schema *apiextensions.JSONSch
728744
}
729745

730746
type specStandardValidatorV3 struct {
731-
allowDefaults bool
747+
allowDefaults bool
748+
disallowDefaultsReason string
749+
isInsideResourceMeta bool
750+
}
751+
752+
func (v *specStandardValidatorV3) withForbiddenDefaults(reason string) specStandardValidator {
753+
clone := *v
754+
clone.disallowDefaultsReason = reason
755+
clone.allowDefaults = false
756+
return &clone
757+
}
758+
759+
func (v *specStandardValidatorV3) withInsideResourceMeta() specStandardValidator {
760+
clone := *v
761+
clone.isInsideResourceMeta = true
762+
return &clone
763+
}
764+
765+
func (v *specStandardValidatorV3) insideResourceMeta() bool {
766+
return v.isInsideResourceMeta
732767
}
733768

734769
// validate validates against OpenAPI Schema v3.
@@ -747,20 +782,37 @@ func (v *specStandardValidatorV3) validate(schema *apiextensions.JSONSchemaProps
747782
if v.allowDefaults {
748783
if s, err := structuralschema.NewStructural(schema); err == nil {
749784
// ignore errors here locally. They will show up for the root of the schema.
750-
pruned := runtime.DeepCopyJSONValue(*schema.Default)
751-
pruning.Prune(pruned, s)
752-
if !reflect.DeepEqual(pruned, *schema.Default) {
753-
allErrs = append(allErrs, field.Invalid(fldPath.Child("default"), schema.Default, "must not have unspecified fields"))
785+
786+
clone := runtime.DeepCopyJSONValue(interface{}(*schema.Default))
787+
if !v.isInsideResourceMeta || s.XEmbeddedResource {
788+
pruning.Prune(clone, s, s.XEmbeddedResource)
789+
// If we are under metadata, there are implicitly specified fields like kind, apiVersion, metadata, labels.
790+
// We cannot prune as they are pruned as well. This allows more defaults than we would like to.
791+
// TODO: be precise about pruning under metadata
792+
}
793+
// TODO: coerce correctly if we are not at the object root, but somewhere below.
794+
if err := schemaobjectmeta.Coerce(fldPath, clone, s, s.XEmbeddedResource, false); err != nil {
795+
allErrs = append(allErrs, err)
796+
}
797+
if !reflect.DeepEqual(clone, interface{}(*schema.Default)) {
798+
allErrs = append(allErrs, field.Invalid(fldPath.Child("default"), schema.Default, "must not have unknown fields"))
799+
} else if s.XEmbeddedResource {
800+
// validate an embedded resource
801+
schemaobjectmeta.Validate(fldPath, interface{}(*schema.Default), nil, true)
754802
}
755803

756-
// validate the default value. Only validating and pruned defaults are allowed.
804+
// validate the default value with user the provided schema.
757805
validator := govalidate.NewSchemaValidator(s.ToGoOpenAPI(), nil, "", strfmt.Default)
758-
if err := apiservervalidation.ValidateCustomResource(pruned, validator); err != nil {
806+
if err := apiservervalidation.ValidateCustomResource(interface{}(*schema.Default), validator); err != nil {
759807
allErrs = append(allErrs, field.Invalid(fldPath.Child("default"), schema.Default, fmt.Sprintf("must validate: %v", err)))
760808
}
761809
}
762810
} else {
763-
allErrs = append(allErrs, field.Forbidden(fldPath.Child("default"), "must not be set"))
811+
detail := "must not be set"
812+
if len(v.disallowDefaultsReason) > 0 {
813+
detail += " " + v.disallowDefaultsReason
814+
}
815+
allErrs = append(allErrs, field.Forbidden(fldPath.Child("default"), detail))
764816
}
765817
}
766818

@@ -796,6 +848,10 @@ func (v *specStandardValidatorV3) validate(schema *apiextensions.JSONSchemaProps
796848
allErrs = append(allErrs, field.Forbidden(fldPath.Child("items"), "items must be a schema object and not an array"))
797849
}
798850

851+
if v.isInsideResourceMeta && schema.XEmbeddedResource {
852+
allErrs = append(allErrs, field.Forbidden(fldPath.Child("x-kubernetes-embedded-resource"), "must not be used inside of resource meta"))
853+
}
854+
799855
return allErrs
800856
}
801857

@@ -953,3 +1009,86 @@ func schemaHasDefaults(s *apiextensions.JSONSchemaProps) bool {
9531009

9541010
return false
9551011
}
1012+
1013+
func specHasKubernetesExtensions(spec *apiextensions.CustomResourceDefinitionSpec) bool {
1014+
if spec.Validation != nil && schemaHasKubernetesExtensions(spec.Validation.OpenAPIV3Schema) {
1015+
return true
1016+
}
1017+
for _, v := range spec.Versions {
1018+
if v.Schema != nil && schemaHasKubernetesExtensions(v.Schema.OpenAPIV3Schema) {
1019+
return true
1020+
}
1021+
}
1022+
return false
1023+
}
1024+
1025+
func schemaHasKubernetesExtensions(s *apiextensions.JSONSchemaProps) bool {
1026+
if s == nil {
1027+
return false
1028+
}
1029+
1030+
if s.XEmbeddedResource || s.XPreserveUnknownFields != nil || s.XIntOrString {
1031+
return true
1032+
}
1033+
1034+
if s.Items != nil {
1035+
if s.Items != nil && schemaHasKubernetesExtensions(s.Items.Schema) {
1036+
return true
1037+
}
1038+
for _, s := range s.Items.JSONSchemas {
1039+
if schemaHasKubernetesExtensions(&s) {
1040+
return true
1041+
}
1042+
}
1043+
}
1044+
for _, s := range s.AllOf {
1045+
if schemaHasKubernetesExtensions(&s) {
1046+
return true
1047+
}
1048+
}
1049+
for _, s := range s.AnyOf {
1050+
if schemaHasKubernetesExtensions(&s) {
1051+
return true
1052+
}
1053+
}
1054+
for _, s := range s.OneOf {
1055+
if schemaHasKubernetesExtensions(&s) {
1056+
return true
1057+
}
1058+
}
1059+
if schemaHasKubernetesExtensions(s.Not) {
1060+
return true
1061+
}
1062+
for _, s := range s.Properties {
1063+
if schemaHasKubernetesExtensions(&s) {
1064+
return true
1065+
}
1066+
}
1067+
if s.AdditionalProperties != nil {
1068+
if schemaHasKubernetesExtensions(s.AdditionalProperties.Schema) {
1069+
return true
1070+
}
1071+
}
1072+
for _, s := range s.PatternProperties {
1073+
if schemaHasKubernetesExtensions(&s) {
1074+
return true
1075+
}
1076+
}
1077+
if s.AdditionalItems != nil {
1078+
if schemaHasKubernetesExtensions(s.AdditionalItems.Schema) {
1079+
return true
1080+
}
1081+
}
1082+
for _, s := range s.Definitions {
1083+
if schemaHasKubernetesExtensions(&s) {
1084+
return true
1085+
}
1086+
}
1087+
for _, d := range s.Dependencies {
1088+
if schemaHasKubernetesExtensions(d.Schema) {
1089+
return true
1090+
}
1091+
}
1092+
1093+
return false
1094+
}

0 commit comments

Comments
 (0)