Change:
We allow CRD authors to write . Currently we don't restrict writing validation for , which means people can have . For example, people can have additive name constraints (require CR to be ) with the following schema: This brings us two questions: do we if "metadata" is included in the schema: simplest option is to publish standard metav1.ObjectMeta schema info, and use any CRD-specified "metadata" properties as additive validation (just like in-tree types do, publishing standard ObjectMeta schema info, and implementing type-specific name validation internally) alternatively, we can try merging user-specified metadata schema with the standard one. Technically people should only add extra validation to existing metadata properties (adding new fields to metadata won't work since they will be pruned by built-in validation first), but it's unclear to me how arbitrary people may want the metadata validation to be term question is whether to allow people specifying metadata validation at all, or just allow a subset (like name validation) in CRD v1 /sig api-machinery /area custom-resources cc
What is the difference between these two? I mean the end result is the same right? The only thing that can be is validation. Nothing else works unless you are considering extensions?
The former means we drop/hide user-specified metadata schema when we publish CRD OpenAPI. We still validate user-specified constraints on server side, but client-side validation won't see them. The latter enables client-side user-specified metadata validation.
If it's possible to merge with standard metadata schema info, or at the cost of not publishing the standard schema. My inclination would be to publish the complete standard schema for metadata/apiVersion/kind, which we know is complete and accurate, and use and user-specified schema for those fields as addition validations server side

Discussion:
//	    - ... zero or more // // * every specified field or array in s is also specified outside of value validation. <ins> // * metadata at the root can only restrict the name and generateName, and not be specified at all in nested contexts. </ins> // * additionalProperties at the root is not allowed. func ValidateStructural(s *Structural, fldPath *field.Path) field.ErrorList { allErrs := field.ErrorList{}

} } <del> allErrs = append(allErrs, validateValueValidation(s.ValueValidation, skipAnyOf, skipFirstAllOfAnyOf, fldPath)...) </del> <ins> allErrs = append(allErrs, validateValueValidation(s.ValueValidation, skipAnyOf, skipFirstAllOfAnyOf, lvl, fldPath)...) </ins> if s.XEmbeddedResource && s.Type != "object" { if len(s.Type) == 0 {

allErrs = append(allErrs, field.Invalid(fldPath.Child("type"), s.Type, "must be object at the root")) } <ins> // 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")) } } </ins> 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")) }

} // validateValueValidation checks the value validation in a structural schema. <del> func validateValueValidation(v *ValueValidation, skipAnyOf, skipFirstAllOfAnyOf bool, fldPath *field.Path) field.ErrorList { </del> <ins> func validateValueValidation(v *ValueValidation, skipAnyOf, skipFirstAllOfAnyOf bool, lvl level, fldPath *field.Path) field.ErrorList { </ins> if v == nil { return nil }

if !skipAnyOf { for i := range v.AnyOf { <del> allErrs = append(allErrs, validateNestedValueValidation(&v.AnyOf[i], false, false, fldPath.Child("anyOf").Index(i))...) </del> <ins> allErrs = append(allErrs, validateNestedValueValidation(&v.AnyOf[i], false, false, lvl, fldPath.Child("anyOf").Index(i))...) </ins> } }

if skipFirstAllOfAnyOf && i == 0 { skipAnyOf = true } <del> allErrs = append(allErrs, validateNestedValueValidation(&v.AllOf[i], skipAnyOf, false, fldPath.Child("allOf").Index(i))...) </del> <ins> allErrs = append(allErrs, validateNestedValueValidation(&v.AllOf[i], skipAnyOf, false, lvl, fldPath.Child("allOf").Index(i))...) </ins> } for i := range v.OneOf { <del> allErrs = append(allErrs, validateNestedValueValidation(&v.OneOf[i], false, false, fldPath.Child("oneOf").Index(i))...) </del> <ins> allErrs = append(allErrs, validateNestedValueValidation(&v.OneOf[i], false, false, lvl, fldPath.Child("oneOf").Index(i))...) </ins> } <del> allErrs = append(allErrs, validateNestedValueValidation(v.Not, false, false, fldPath.Child("not"))...) </del> <ins> allErrs = append(allErrs, validateNestedValueValidation(v.Not, false, false, lvl, fldPath.Child("not"))...) </ins> return allErrs } // validateNestedValueValidation checks the nested value validation under a logic junctor in a structural schema. <del> func validateNestedValueValidation(v *NestedValueValidation, skipAnyOf, skipAllOfAnyOf bool, fldPath *field.Path) field.ErrorList { </del> <ins> func validateNestedValueValidation(v *NestedValueValidation, skipAnyOf, skipAllOfAnyOf bool, lvl level, fldPath *field.Path) field.ErrorList { </ins> if v == nil { return nil } allErrs := field.ErrorList{} <del> allErrs = append(allErrs, validateValueValidation(&v.ValueValidation, skipAnyOf, skipAllOfAnyOf, fldPath)...) allErrs = append(allErrs, validateNestedValueValidation(v.Items, false, false, fldPath.Child("items"))...) </del> <ins> allErrs = append(allErrs, validateValueValidation(&v.ValueValidation, skipAnyOf, skipAllOfAnyOf, lvl, fldPath)...) allErrs = append(allErrs, validateNestedValueValidation(v.Items, false, false, lvl, fldPath.Child("items"))...) </ins> for k, fld := range v.Properties { <del> allErrs = append(allErrs, validateNestedValueValidation(&fld, false, false, fldPath.Child("properties").Key(k))...) </del> <ins> allErrs = append(allErrs, validateNestedValueValidation(&fld, false, false, fieldLevel, fldPath.Child("properties").Key(k))...) </ins> } if len(v.ForbiddenGenerics.Type) > 0 {

allErrs = append(allErrs, field.Forbidden(fldPath.Child("x-kubernetes-int-or-string"), "must be false to be structural")) } <ins> // 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")) } </ins> return allErrs }

i := rand.Intn(x.NumField()) fuzzer.Fuzz(x.Field(i).Addr().Interface()) <del> errs := validateNestedValueValidation(vv, false, false, nil) </del> <ins> errs := validateNestedValueValidation(vv, false, false, fieldLevel, nil) </ins> if len(errs) == 0 && !reflect.DeepEqual(vv.ForbiddenGenerics, Generic{}) { t.Errorf("expected ForbiddenGenerics validation errors for: %#v", vv) }

i := rand.Intn(x.NumField()) fuzzer.Fuzz(x.Field(i).Addr().Interface()) <del> errs := validateNestedValueValidation(vv, false, false, nil) </del> <ins> errs := validateNestedValueValidation(vv, false, false, fieldLevel, nil) </ins> if len(errs) == 0 && !reflect.DeepEqual(vv.ForbiddenExtensions, Extensions{}) { t.Errorf("expected ForbiddenExtensions validation errors for: %#v", vv) }

"spec.version[v1].schema.openAPIV3Schema.properties[d]: Required value: because it is defined in spec.version[v1].schema.openAPIV3Schema.not.properties[d]", }, }, <ins> { 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", }, }, </ins> } for i := range tests {