mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-29 14:37:00 +00:00
Merge pull request #78863 from sttts/sttts-crd-embedded-resource-items-additionalProperties
apiextensions: structural schema unit test coverage
This commit is contained in:
commit
e760b033ec
@ -784,21 +784,23 @@ func (v *specStandardValidatorV3) validate(schema *apiextensions.JSONSchemaProps
|
|||||||
// ignore errors here locally. They will show up for the root of the schema.
|
// ignore errors here locally. They will show up for the root of the schema.
|
||||||
|
|
||||||
clone := runtime.DeepCopyJSONValue(interface{}(*schema.Default))
|
clone := runtime.DeepCopyJSONValue(interface{}(*schema.Default))
|
||||||
if !v.isInsideResourceMeta || s.XEmbeddedResource {
|
if !v.isInsideResourceMeta {
|
||||||
pruning.Prune(clone, s, s.XEmbeddedResource)
|
|
||||||
// If we are under metadata, there are implicitly specified fields like kind, apiVersion, metadata, labels.
|
// If we are under metadata, there are implicitly specified fields like kind, apiVersion, metadata, labels.
|
||||||
// We cannot prune as they are pruned as well. This allows more defaults than we would like to.
|
// We cannot prune as they are pruned as well. This allows more defaults than we would like to.
|
||||||
// TODO: be precise about pruning under metadata
|
// TODO: be precise about pruning under metadata
|
||||||
}
|
pruning.Prune(clone, s, s.XEmbeddedResource)
|
||||||
// TODO: coerce correctly if we are not at the object root, but somewhere below.
|
|
||||||
if err := schemaobjectmeta.Coerce(fldPath, clone, s, s.XEmbeddedResource, false); err != nil {
|
// TODO: coerce correctly if we are not at the object root, but somewhere below.
|
||||||
allErrs = append(allErrs, err)
|
if err := schemaobjectmeta.Coerce(fldPath, clone, s, s.XEmbeddedResource, false); err != nil {
|
||||||
}
|
allErrs = append(allErrs, err)
|
||||||
if !reflect.DeepEqual(clone, interface{}(*schema.Default)) {
|
}
|
||||||
allErrs = append(allErrs, field.Invalid(fldPath.Child("default"), schema.Default, "must not have unknown fields"))
|
|
||||||
} else if s.XEmbeddedResource {
|
if !reflect.DeepEqual(clone, interface{}(*schema.Default)) {
|
||||||
// validate an embedded resource
|
allErrs = append(allErrs, field.Invalid(fldPath.Child("default"), schema.Default, "must not have unknown fields"))
|
||||||
schemaobjectmeta.Validate(fldPath, interface{}(*schema.Default), nil, true)
|
} else if s.XEmbeddedResource {
|
||||||
|
// validate an embedded resource
|
||||||
|
schemaobjectmeta.Validate(fldPath, interface{}(*schema.Default), nil, true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// validate the default value with user the provided schema.
|
// validate the default value with user the provided schema.
|
||||||
|
@ -23,16 +23,18 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Coerce checks types of embedded ObjectMeta and TypeMeta and prunes unknown fields inside the former.
|
// Coerce checks types of embedded ObjectMeta and TypeMeta and prunes unknown fields inside the former.
|
||||||
// It does coerce ObjectMeta and TypeMeta at the root if includeRoot is true.
|
// It does coerce ObjectMeta and TypeMeta at the root if isResourceRoot is true.
|
||||||
// If dropInvalidFields is true, fields of wrong type will be dropped.
|
// If dropInvalidFields is true, fields of wrong type will be dropped.
|
||||||
func Coerce(pth *field.Path, obj interface{}, s *structuralschema.Structural, includeRoot, dropInvalidFields bool) *field.Error {
|
func Coerce(pth *field.Path, obj interface{}, s *structuralschema.Structural, isResourceRoot, dropInvalidFields bool) *field.Error {
|
||||||
if includeRoot {
|
if isResourceRoot {
|
||||||
if s == nil {
|
if s == nil {
|
||||||
s = &structuralschema.Structural{}
|
s = &structuralschema.Structural{}
|
||||||
}
|
}
|
||||||
clone := *s
|
if !s.XEmbeddedResource {
|
||||||
clone.XEmbeddedResource = true
|
clone := *s
|
||||||
s = &clone
|
clone.XEmbeddedResource = true
|
||||||
|
s = &clone
|
||||||
|
}
|
||||||
}
|
}
|
||||||
c := coercer{dropInvalidFields: dropInvalidFields}
|
c := coercer{dropInvalidFields: dropInvalidFields}
|
||||||
return c.coerce(pth, obj, s)
|
return c.coerce(pth, obj, s)
|
||||||
|
@ -28,15 +28,17 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Validate validates embedded ObjectMeta and TypeMeta.
|
// Validate validates embedded ObjectMeta and TypeMeta.
|
||||||
// It also validate those at the root if includeRoot is true.
|
// It also validate those at the root if isResourceRoot is true.
|
||||||
func Validate(pth *field.Path, obj interface{}, s *structuralschema.Structural, includeRoot bool) field.ErrorList {
|
func Validate(pth *field.Path, obj interface{}, s *structuralschema.Structural, isResourceRoot bool) field.ErrorList {
|
||||||
if includeRoot {
|
if isResourceRoot {
|
||||||
if s == nil {
|
if s == nil {
|
||||||
s = &structuralschema.Structural{}
|
s = &structuralschema.Structural{}
|
||||||
}
|
}
|
||||||
clone := *s
|
if !s.XEmbeddedResource {
|
||||||
clone.XEmbeddedResource = true
|
clone := *s
|
||||||
s = &clone
|
clone.XEmbeddedResource = true
|
||||||
|
s = &clone
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return validate(pth, obj, s)
|
return validate(pth, obj, s)
|
||||||
}
|
}
|
||||||
|
@ -139,6 +139,20 @@ func TestValidate(t *testing.T) {
|
|||||||
required("nested", "embedded", "apiVersion"),
|
required("nested", "embedded", "apiVersion"),
|
||||||
required("nested", "embedded", "kind"),
|
required("nested", "embedded", "kind"),
|
||||||
}},
|
}},
|
||||||
|
{name: "items", object: `
|
||||||
|
{
|
||||||
|
"items": [{}]
|
||||||
|
}`, errors: []validationMatch{
|
||||||
|
required("items[0]", "apiVersion"),
|
||||||
|
required("items[0]", "kind"),
|
||||||
|
}},
|
||||||
|
{name: "additionalProperties", object: `
|
||||||
|
{
|
||||||
|
"additionalProperties": {"foo":{}}
|
||||||
|
}`, errors: []validationMatch{
|
||||||
|
required("additionalProperties[foo]", "apiVersion"),
|
||||||
|
required("additionalProperties[foo]", "kind"),
|
||||||
|
}},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
@ -151,6 +165,20 @@ func TestValidate(t *testing.T) {
|
|||||||
"embedded": {Extensions: structuralschema.Extensions{XEmbeddedResource: true}},
|
"embedded": {Extensions: structuralschema.Extensions{XEmbeddedResource: true}},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"items": {
|
||||||
|
Items: &structuralschema.Structural{
|
||||||
|
Extensions: structuralschema.Extensions{XEmbeddedResource: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"additionalProperties": {
|
||||||
|
Generic: structuralschema.Generic{
|
||||||
|
AdditionalProperties: &structuralschema.StructuralOrBool{
|
||||||
|
Structural: &structuralschema.Structural{
|
||||||
|
Extensions: structuralschema.Extensions{XEmbeddedResource: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,15 +21,17 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Prune removes object fields in obj which are not specified in s. It skips TypeMeta and ObjectMeta fields
|
// Prune removes object fields in obj which are not specified in s. It skips TypeMeta and ObjectMeta fields
|
||||||
// if XEmbeddedResource is set to true, or for the root if root=true.
|
// if XEmbeddedResource is set to true, or for the root if isResourceRoot=true.
|
||||||
func Prune(obj interface{}, s *structuralschema.Structural, root bool) {
|
func Prune(obj interface{}, s *structuralschema.Structural, isResourceRoot bool) {
|
||||||
if root {
|
if isResourceRoot {
|
||||||
if s == nil {
|
if s == nil {
|
||||||
s = &structuralschema.Structural{}
|
s = &structuralschema.Structural{}
|
||||||
}
|
}
|
||||||
clone := *s
|
if !s.XEmbeddedResource {
|
||||||
clone.XEmbeddedResource = true
|
clone := *s
|
||||||
s = &clone
|
clone.XEmbeddedResource = true
|
||||||
|
s = &clone
|
||||||
|
}
|
||||||
}
|
}
|
||||||
prune(obj, s)
|
prune(obj, s)
|
||||||
}
|
}
|
||||||
|
@ -29,11 +29,11 @@ import (
|
|||||||
|
|
||||||
func TestPrune(t *testing.T) {
|
func TestPrune(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
json string
|
json string
|
||||||
dontPruneMetaAtRoot bool
|
isResourceRoot bool
|
||||||
schema *structuralschema.Structural
|
schema *structuralschema.Structural
|
||||||
expected string
|
expected string
|
||||||
}{
|
}{
|
||||||
{name: "empty", json: "null", expected: "null"},
|
{name: "empty", json: "null", expected: "null"},
|
||||||
{name: "scalar", json: "4", schema: &structuralschema.Structural{}, expected: "4"},
|
{name: "scalar", json: "4", schema: &structuralschema.Structural{}, expected: "4"},
|
||||||
@ -85,7 +85,13 @@ func TestPrune(t *testing.T) {
|
|||||||
"pruning": {"unspecified": "bar"},
|
"pruning": {"unspecified": "bar"},
|
||||||
"preserving": {"unspecified": "bar"}
|
"preserving": {"unspecified": "bar"}
|
||||||
},
|
},
|
||||||
"preservingAdditionalProperties": {
|
"preservingAdditionalPropertiesNotInheritingXPreserveUnknownFields": {
|
||||||
|
"foo": {
|
||||||
|
"specified": {"unspecified":"bar"},
|
||||||
|
"unspecified": "bar"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"preservingAdditionalPropertiesKeyPruneValues": {
|
||||||
"foo": {
|
"foo": {
|
||||||
"specified": {"unspecified":"bar"},
|
"specified": {"unspecified":"bar"},
|
||||||
"unspecified": "bar"
|
"unspecified": "bar"
|
||||||
@ -123,7 +129,8 @@ func TestPrune(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"preservingAdditionalProperties": {
|
"preservingAdditionalPropertiesNotInheritingXPreserveUnknownFields": {
|
||||||
|
// this x-kubernetes-preserve-unknown-fields is not inherited by the schema inside of additionalProperties
|
||||||
Extensions: structuralschema.Extensions{XPreserveUnknownFields: true},
|
Extensions: structuralschema.Extensions{XPreserveUnknownFields: true},
|
||||||
Generic: structuralschema.Generic{
|
Generic: structuralschema.Generic{
|
||||||
Type: "object",
|
Type: "object",
|
||||||
@ -137,6 +144,19 @@ func TestPrune(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"preservingAdditionalPropertiesKeyPruneValues": {
|
||||||
|
Generic: structuralschema.Generic{
|
||||||
|
Type: "object",
|
||||||
|
AdditionalProperties: &structuralschema.StructuralOrBool{
|
||||||
|
Structural: &structuralschema.Structural{
|
||||||
|
Generic: structuralschema.Generic{Type: "object"},
|
||||||
|
Properties: map[string]structuralschema.Structural{
|
||||||
|
"specified": {Generic: structuralschema.Generic{Type: "object"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}, expected: `
|
}, expected: `
|
||||||
{
|
{
|
||||||
@ -154,7 +174,12 @@ func TestPrune(t *testing.T) {
|
|||||||
"pruning": {},
|
"pruning": {},
|
||||||
"preserving": {"unspecified": "bar"}
|
"preserving": {"unspecified": "bar"}
|
||||||
},
|
},
|
||||||
"preservingAdditionalProperties": {
|
"preservingAdditionalPropertiesNotInheritingXPreserveUnknownFields": {
|
||||||
|
"foo": {
|
||||||
|
"specified": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"preservingAdditionalPropertiesKeyPruneValues": {
|
||||||
"foo": {
|
"foo": {
|
||||||
"specified": {}
|
"specified": {}
|
||||||
}
|
}
|
||||||
@ -397,7 +422,7 @@ func TestPrune(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`, dontPruneMetaAtRoot: true, schema: &structuralschema.Structural{
|
`, isResourceRoot: true, schema: &structuralschema.Structural{
|
||||||
Generic: structuralschema.Generic{Type: "object"},
|
Generic: structuralschema.Generic{Type: "object"},
|
||||||
Properties: map[string]structuralschema.Structural{
|
Properties: map[string]structuralschema.Structural{
|
||||||
"pruned": {
|
"pruned": {
|
||||||
@ -508,7 +533,7 @@ func TestPrune(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
Prune(in, tt.schema, tt.dontPruneMetaAtRoot)
|
Prune(in, tt.schema, tt.isResourceRoot)
|
||||||
if !reflect.DeepEqual(in, expected) {
|
if !reflect.DeepEqual(in, expected) {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
enc := json.NewEncoder(&buf)
|
enc := json.NewEncoder(&buf)
|
||||||
|
Loading…
Reference in New Issue
Block a user