@@ -23,6 +23,7 @@ import (
23
23
24
24
"github.com/go-openapi/strfmt"
25
25
govalidate "github.com/go-openapi/validate"
26
+ schemaobjectmeta "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/objectmeta"
26
27
27
28
apiequality "k8s.io/apimachinery/pkg/api/equality"
28
29
genericvalidation "k8s.io/apimachinery/pkg/api/validation"
@@ -156,6 +157,9 @@ func validateCustomResourceDefinitionSpec(spec *apiextensions.CustomResourceDefi
156
157
allErrs = append (allErrs , field .Invalid (fldPath .Child ("preserveUnknownFields" ), true , "must be false in order to use defaults in the schema" ))
157
158
}
158
159
}
160
+ if specHasKubernetesExtensions (spec ) {
161
+ mustBeStructural = true
162
+ }
159
163
160
164
storageFlagCount := 0
161
165
versionsMap := map [string ]bool {}
@@ -562,6 +566,11 @@ func ValidateCustomResourceColumnDefinition(col *apiextensions.CustomResourceCol
562
566
// specStandardValidator applies validations for different OpenAPI specification versions.
563
567
type specStandardValidator interface {
564
568
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
565
574
}
566
575
567
576
// ValidateCustomResourceDefinitionValidation statically validates
@@ -608,7 +617,7 @@ func ValidateCustomResourceDefinitionValidation(customResourceValidation *apiext
608
617
openAPIV3Schema := & specStandardValidatorV3 {
609
618
allowDefaults : allowDefaults ,
610
619
}
611
- allErrs = append (allErrs , ValidateCustomResourceDefinitionOpenAPISchema (schema , fldPath .Child ("openAPIV3Schema" ), openAPIV3Schema )... )
620
+ allErrs = append (allErrs , ValidateCustomResourceDefinitionOpenAPISchema (schema , fldPath .Child ("openAPIV3Schema" ), openAPIV3Schema , true )... )
612
621
613
622
if mustBeStructural {
614
623
if ss , err := structuralschema .NewStructural (schema ); err != nil {
@@ -631,8 +640,10 @@ func ValidateCustomResourceDefinitionValidation(customResourceValidation *apiext
631
640
return allErrs
632
641
}
633
642
643
+ var metaFields = sets .NewString ("metadata" , "apiVersion" , "kind" )
644
+
634
645
// 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 {
636
647
allErrs := field.ErrorList {}
637
648
638
649
if schema == nil {
@@ -660,63 +671,68 @@ func ValidateCustomResourceDefinitionOpenAPISchema(schema *apiextensions.JSONSch
660
671
allErrs = append (allErrs , field .Forbidden (fldPath .Child ("additionalProperties" ), "additionalProperties and properties are mutual exclusive" ))
661
672
}
662
673
}
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 )... )
664
682
}
665
683
666
684
if len (schema .Properties ) != 0 {
667
685
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 )... )
669
695
}
670
696
}
671
697
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 )... )
683
699
684
700
if len (schema .AllOf ) != 0 {
685
701
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 )... )
687
703
}
688
704
}
689
705
690
706
if len (schema .OneOf ) != 0 {
691
707
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 )... )
693
709
}
694
710
}
695
711
696
712
if len (schema .AnyOf ) != 0 {
697
713
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 )... )
699
715
}
700
716
}
701
717
702
718
if len (schema .Definitions ) != 0 {
703
719
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 )... )
705
721
}
706
722
}
707
723
708
724
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 )... )
710
726
if len (schema .Items .JSONSchemas ) != 0 {
711
727
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 )... )
713
729
}
714
730
}
715
731
}
716
732
717
733
if schema .Dependencies != nil {
718
734
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 )... )
720
736
}
721
737
}
722
738
@@ -728,7 +744,26 @@ func ValidateCustomResourceDefinitionOpenAPISchema(schema *apiextensions.JSONSch
728
744
}
729
745
730
746
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
732
767
}
733
768
734
769
// validate validates against OpenAPI Schema v3.
@@ -747,20 +782,37 @@ func (v *specStandardValidatorV3) validate(schema *apiextensions.JSONSchemaProps
747
782
if v .allowDefaults {
748
783
if s , err := structuralschema .NewStructural (schema ); err == nil {
749
784
// 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 )
754
802
}
755
803
756
- // validate the default value. Only validating and pruned defaults are allowed .
804
+ // validate the default value with user the provided schema .
757
805
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 {
759
807
allErrs = append (allErrs , field .Invalid (fldPath .Child ("default" ), schema .Default , fmt .Sprintf ("must validate: %v" , err )))
760
808
}
761
809
}
762
810
} 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 ))
764
816
}
765
817
}
766
818
@@ -796,6 +848,10 @@ func (v *specStandardValidatorV3) validate(schema *apiextensions.JSONSchemaProps
796
848
allErrs = append (allErrs , field .Forbidden (fldPath .Child ("items" ), "items must be a schema object and not an array" ))
797
849
}
798
850
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
+
799
855
return allErrs
800
856
}
801
857
@@ -953,3 +1009,86 @@ func schemaHasDefaults(s *apiextensions.JSONSchemaProps) bool {
953
1009
954
1010
return false
955
1011
}
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