mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-28 14:07:14 +00:00
Merge pull request #77653 from sttts/sttts-structural-schema-metadata
apiextensions: disallow metadata specs other than name and generateName
This commit is contained in:
commit
fb41b7a801
@ -56,6 +56,7 @@ const (
|
|||||||
// - ... zero or more
|
// - ... zero or more
|
||||||
//
|
//
|
||||||
// * every specified field or array in s is also specified outside of value validation.
|
// * every specified field or array in s is also specified outside of value validation.
|
||||||
|
// * metadata at the root can only restrict the name and generateName, and not be specified at all in nested contexts.
|
||||||
// * additionalProperties at the root is not allowed.
|
// * additionalProperties at the root is not allowed.
|
||||||
func ValidateStructural(s *Structural, fldPath *field.Path) field.ErrorList {
|
func ValidateStructural(s *Structural, fldPath *field.Path) field.ErrorList {
|
||||||
allErrs := field.ErrorList{}
|
allErrs := field.ErrorList{}
|
||||||
@ -106,7 +107,7 @@ func validateStructuralInvariants(s *Structural, lvl level, fldPath *field.Path)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
allErrs = append(allErrs, validateValueValidation(s.ValueValidation, skipAnyOf, skipFirstAllOfAnyOf, fldPath)...)
|
allErrs = append(allErrs, validateValueValidation(s.ValueValidation, skipAnyOf, skipFirstAllOfAnyOf, lvl, fldPath)...)
|
||||||
|
|
||||||
if s.XEmbeddedResource && s.Type != "object" {
|
if s.XEmbeddedResource && s.Type != "object" {
|
||||||
if len(s.Type) == 0 {
|
if len(s.Type) == 0 {
|
||||||
@ -129,6 +130,26 @@ func validateStructuralInvariants(s *Structural, lvl level, fldPath *field.Path)
|
|||||||
allErrs = append(allErrs, field.Invalid(fldPath.Child("type"), s.Type, "must be object at the root"))
|
allErrs = append(allErrs, field.Invalid(fldPath.Child("type"), s.Type, "must be object at the root"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// restrict metadata schemas to name and generateName only
|
||||||
|
if metadata, found := s.Properties["metadata"]; found && lvl == rootLevel {
|
||||||
|
// metadata is a shallow copy. We can mutate it.
|
||||||
|
_, foundName := metadata.Properties["name"]
|
||||||
|
_, foundGenerateName := metadata.Properties["generateName"]
|
||||||
|
if foundName && foundGenerateName && len(metadata.Properties) == 2 {
|
||||||
|
metadata.Properties = nil
|
||||||
|
} else if (foundName || foundGenerateName) && len(metadata.Properties) == 1 {
|
||||||
|
metadata.Properties = nil
|
||||||
|
}
|
||||||
|
metadata.Type = ""
|
||||||
|
if metadata.ValueValidation == nil {
|
||||||
|
metadata.ValueValidation = &ValueValidation{}
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(metadata, Structural{ValueValidation: &ValueValidation{}}) {
|
||||||
|
// TODO: this is actually a field.Invalid error, but we cannot do JSON serialization of metadata here to get a proper message
|
||||||
|
allErrs = append(allErrs, field.Forbidden(fldPath.Child("properties").Key("metadata"), "must not specify anything other than name and generateName, but metadata is implicitly specified"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if s.XEmbeddedResource && !s.XPreserveUnknownFields && s.Properties == nil {
|
if s.XEmbeddedResource && !s.XPreserveUnknownFields && s.Properties == nil {
|
||||||
allErrs = append(allErrs, field.Required(fldPath.Child("properties"), "must not be empty if x-kubernetes-embedded-resource is true without x-kubernetes-preserve-unknown-fields"))
|
allErrs = append(allErrs, field.Required(fldPath.Child("properties"), "must not be empty if x-kubernetes-embedded-resource is true without x-kubernetes-preserve-unknown-fields"))
|
||||||
}
|
}
|
||||||
@ -171,7 +192,7 @@ func validateExtensions(x *Extensions, fldPath *field.Path) field.ErrorList {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// validateValueValidation checks the value validation in a structural schema.
|
// validateValueValidation checks the value validation in a structural schema.
|
||||||
func validateValueValidation(v *ValueValidation, skipAnyOf, skipFirstAllOfAnyOf bool, fldPath *field.Path) field.ErrorList {
|
func validateValueValidation(v *ValueValidation, skipAnyOf, skipFirstAllOfAnyOf bool, lvl level, fldPath *field.Path) field.ErrorList {
|
||||||
if v == nil {
|
if v == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -180,7 +201,7 @@ func validateValueValidation(v *ValueValidation, skipAnyOf, skipFirstAllOfAnyOf
|
|||||||
|
|
||||||
if !skipAnyOf {
|
if !skipAnyOf {
|
||||||
for i := range v.AnyOf {
|
for i := range v.AnyOf {
|
||||||
allErrs = append(allErrs, validateNestedValueValidation(&v.AnyOf[i], false, false, fldPath.Child("anyOf").Index(i))...)
|
allErrs = append(allErrs, validateNestedValueValidation(&v.AnyOf[i], false, false, lvl, fldPath.Child("anyOf").Index(i))...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -189,31 +210,31 @@ func validateValueValidation(v *ValueValidation, skipAnyOf, skipFirstAllOfAnyOf
|
|||||||
if skipFirstAllOfAnyOf && i == 0 {
|
if skipFirstAllOfAnyOf && i == 0 {
|
||||||
skipAnyOf = true
|
skipAnyOf = true
|
||||||
}
|
}
|
||||||
allErrs = append(allErrs, validateNestedValueValidation(&v.AllOf[i], skipAnyOf, false, fldPath.Child("allOf").Index(i))...)
|
allErrs = append(allErrs, validateNestedValueValidation(&v.AllOf[i], skipAnyOf, false, lvl, fldPath.Child("allOf").Index(i))...)
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := range v.OneOf {
|
for i := range v.OneOf {
|
||||||
allErrs = append(allErrs, validateNestedValueValidation(&v.OneOf[i], false, false, fldPath.Child("oneOf").Index(i))...)
|
allErrs = append(allErrs, validateNestedValueValidation(&v.OneOf[i], false, false, lvl, fldPath.Child("oneOf").Index(i))...)
|
||||||
}
|
}
|
||||||
|
|
||||||
allErrs = append(allErrs, validateNestedValueValidation(v.Not, false, false, fldPath.Child("not"))...)
|
allErrs = append(allErrs, validateNestedValueValidation(v.Not, false, false, lvl, fldPath.Child("not"))...)
|
||||||
|
|
||||||
return allErrs
|
return allErrs
|
||||||
}
|
}
|
||||||
|
|
||||||
// validateNestedValueValidation checks the nested value validation under a logic junctor in a structural schema.
|
// validateNestedValueValidation checks the nested value validation under a logic junctor in a structural schema.
|
||||||
func validateNestedValueValidation(v *NestedValueValidation, skipAnyOf, skipAllOfAnyOf bool, fldPath *field.Path) field.ErrorList {
|
func validateNestedValueValidation(v *NestedValueValidation, skipAnyOf, skipAllOfAnyOf bool, lvl level, fldPath *field.Path) field.ErrorList {
|
||||||
if v == nil {
|
if v == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
allErrs := field.ErrorList{}
|
allErrs := field.ErrorList{}
|
||||||
|
|
||||||
allErrs = append(allErrs, validateValueValidation(&v.ValueValidation, skipAnyOf, skipAllOfAnyOf, fldPath)...)
|
allErrs = append(allErrs, validateValueValidation(&v.ValueValidation, skipAnyOf, skipAllOfAnyOf, lvl, fldPath)...)
|
||||||
allErrs = append(allErrs, validateNestedValueValidation(v.Items, false, false, fldPath.Child("items"))...)
|
allErrs = append(allErrs, validateNestedValueValidation(v.Items, false, false, lvl, fldPath.Child("items"))...)
|
||||||
|
|
||||||
for k, fld := range v.Properties {
|
for k, fld := range v.Properties {
|
||||||
allErrs = append(allErrs, validateNestedValueValidation(&fld, false, false, fldPath.Child("properties").Key(k))...)
|
allErrs = append(allErrs, validateNestedValueValidation(&fld, false, false, fieldLevel, fldPath.Child("properties").Key(k))...)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(v.ForbiddenGenerics.Type) > 0 {
|
if len(v.ForbiddenGenerics.Type) > 0 {
|
||||||
@ -245,5 +266,10 @@ func validateNestedValueValidation(v *NestedValueValidation, skipAnyOf, skipAllO
|
|||||||
allErrs = append(allErrs, field.Forbidden(fldPath.Child("x-kubernetes-int-or-string"), "must be false to be structural"))
|
allErrs = append(allErrs, field.Forbidden(fldPath.Child("x-kubernetes-int-or-string"), "must be false to be structural"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// forbid reasoning about metadata because it can lead to metadata restriction we don't want
|
||||||
|
if _, found := v.Properties["metadata"]; found {
|
||||||
|
allErrs = append(allErrs, field.Forbidden(fldPath.Child("properties").Key("metadata"), "must not be specified in a nested context"))
|
||||||
|
}
|
||||||
|
|
||||||
return allErrs
|
return allErrs
|
||||||
}
|
}
|
||||||
|
@ -49,7 +49,7 @@ func TestValidateNestedValueValidationComplete(t *testing.T) {
|
|||||||
i := rand.Intn(x.NumField())
|
i := rand.Intn(x.NumField())
|
||||||
fuzzer.Fuzz(x.Field(i).Addr().Interface())
|
fuzzer.Fuzz(x.Field(i).Addr().Interface())
|
||||||
|
|
||||||
errs := validateNestedValueValidation(vv, false, false, nil)
|
errs := validateNestedValueValidation(vv, false, false, fieldLevel, nil)
|
||||||
if len(errs) == 0 && !reflect.DeepEqual(vv.ForbiddenGenerics, Generic{}) {
|
if len(errs) == 0 && !reflect.DeepEqual(vv.ForbiddenGenerics, Generic{}) {
|
||||||
t.Errorf("expected ForbiddenGenerics validation errors for: %#v", vv)
|
t.Errorf("expected ForbiddenGenerics validation errors for: %#v", vv)
|
||||||
}
|
}
|
||||||
@ -63,7 +63,7 @@ func TestValidateNestedValueValidationComplete(t *testing.T) {
|
|||||||
i := rand.Intn(x.NumField())
|
i := rand.Intn(x.NumField())
|
||||||
fuzzer.Fuzz(x.Field(i).Addr().Interface())
|
fuzzer.Fuzz(x.Field(i).Addr().Interface())
|
||||||
|
|
||||||
errs := validateNestedValueValidation(vv, false, false, nil)
|
errs := validateNestedValueValidation(vv, false, false, fieldLevel, nil)
|
||||||
if len(errs) == 0 && !reflect.DeepEqual(vv.ForbiddenExtensions, Extensions{}) {
|
if len(errs) == 0 && !reflect.DeepEqual(vv.ForbiddenExtensions, Extensions{}) {
|
||||||
t.Errorf("expected ForbiddenExtensions validation errors for: %#v", vv)
|
t.Errorf("expected ForbiddenExtensions validation errors for: %#v", vv)
|
||||||
}
|
}
|
||||||
|
@ -1222,6 +1222,114 @@ not:
|
|||||||
"spec.version[v1].schema.openAPIV3Schema.properties[d]: Required value: because it is defined in spec.version[v1].schema.openAPIV3Schema.not.properties[d]",
|
"spec.version[v1].schema.openAPIV3Schema.properties[d]: Required value: because it is defined in spec.version[v1].schema.openAPIV3Schema.not.properties[d]",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
desc: "metadata with non-properties",
|
||||||
|
globalSchema: `
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
metadata:
|
||||||
|
minimum: 42.0
|
||||||
|
`,
|
||||||
|
expectedViolations: []string{
|
||||||
|
"spec.validation.openAPIV3Schema.properties[metadata]: Forbidden: must not specify anything other than name and generateName, but metadata is implicitly specified",
|
||||||
|
"spec.validation.openAPIV3Schema.properties[metadata].type: Required value: must not be empty for specified object fields",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "metadata with other properties",
|
||||||
|
globalSchema: `
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
metadata:
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
pattern: "^[a-z]+$"
|
||||||
|
labels:
|
||||||
|
type: object
|
||||||
|
maxLength: 4
|
||||||
|
`,
|
||||||
|
expectedViolations: []string{
|
||||||
|
"spec.validation.openAPIV3Schema.properties[metadata]: Forbidden: must not specify anything other than name and generateName, but metadata is implicitly specified",
|
||||||
|
"spec.validation.openAPIV3Schema.properties[metadata].type: Required value: must not be empty for specified object fields",
|
||||||
|
"spec.validation.openAPIV3Schema.properties[metadata].properties[name].type: Required value: must not be empty for specified object fields",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "metadata with name property",
|
||||||
|
globalSchema: `
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
metadata:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
pattern: "^[a-z]+$"
|
||||||
|
`,
|
||||||
|
expectedViolations: []string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "metadata with generateName property",
|
||||||
|
globalSchema: `
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
metadata:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
generateName:
|
||||||
|
type: string
|
||||||
|
pattern: "^[a-z]+$"
|
||||||
|
`,
|
||||||
|
expectedViolations: []string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "metadata with name and generateName property",
|
||||||
|
globalSchema: `
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
metadata:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
pattern: "^[a-z]+$"
|
||||||
|
generateName:
|
||||||
|
type: string
|
||||||
|
pattern: "^[a-z]+$"
|
||||||
|
`,
|
||||||
|
expectedViolations: []string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "metadata under junctors",
|
||||||
|
globalSchema: `
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
metadata:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
pattern: "^[a-z]+$"
|
||||||
|
allOf:
|
||||||
|
- properties:
|
||||||
|
metadata: {}
|
||||||
|
anyOf:
|
||||||
|
- properties:
|
||||||
|
metadata: {}
|
||||||
|
oneOf:
|
||||||
|
- properties:
|
||||||
|
metadata: {}
|
||||||
|
not:
|
||||||
|
properties:
|
||||||
|
metadata: {}
|
||||||
|
`,
|
||||||
|
expectedViolations: []string{
|
||||||
|
"spec.validation.openAPIV3Schema.anyOf[0].properties[metadata]: Forbidden: must not be specified in a nested context",
|
||||||
|
"spec.validation.openAPIV3Schema.allOf[0].properties[metadata]: Forbidden: must not be specified in a nested context",
|
||||||
|
"spec.validation.openAPIV3Schema.oneOf[0].properties[metadata]: Forbidden: must not be specified in a nested context",
|
||||||
|
"spec.validation.openAPIV3Schema.not.properties[metadata]: Forbidden: must not be specified in a nested context",
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := range tests {
|
for i := range tests {
|
||||||
|
Loading…
Reference in New Issue
Block a user