Merge pull request #78788 from sttts/sttts-crd-embedded-resource
apiextensions: validate x-kubernetes-embedded-resource in CRs
This commit is contained in:
@@ -15,6 +15,7 @@ go_library(
|
|||||||
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions:go_default_library",
|
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions:go_default_library",
|
||||||
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1:go_default_library",
|
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1:go_default_library",
|
||||||
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema:go_default_library",
|
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema:go_default_library",
|
||||||
|
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/objectmeta:go_default_library",
|
||||||
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/pruning:go_default_library",
|
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/pruning:go_default_library",
|
||||||
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/validation:go_default_library",
|
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/validation:go_default_library",
|
||||||
"//staging/src/k8s.io/apiextensions-apiserver/pkg/features:go_default_library",
|
"//staging/src/k8s.io/apiextensions-apiserver/pkg/features:go_default_library",
|
||||||
|
@@ -23,6 +23,7 @@ import (
|
|||||||
|
|
||||||
"github.com/go-openapi/strfmt"
|
"github.com/go-openapi/strfmt"
|
||||||
govalidate "github.com/go-openapi/validate"
|
govalidate "github.com/go-openapi/validate"
|
||||||
|
schemaobjectmeta "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/objectmeta"
|
||||||
|
|
||||||
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
||||||
genericvalidation "k8s.io/apimachinery/pkg/api/validation"
|
genericvalidation "k8s.io/apimachinery/pkg/api/validation"
|
||||||
@@ -156,6 +157,9 @@ func validateCustomResourceDefinitionSpec(spec *apiextensions.CustomResourceDefi
|
|||||||
allErrs = append(allErrs, field.Invalid(fldPath.Child("preserveUnknownFields"), true, "must be false in order to use defaults in the schema"))
|
allErrs = append(allErrs, field.Invalid(fldPath.Child("preserveUnknownFields"), true, "must be false in order to use defaults in the schema"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if specHasKubernetesExtensions(spec) {
|
||||||
|
mustBeStructural = true
|
||||||
|
}
|
||||||
|
|
||||||
storageFlagCount := 0
|
storageFlagCount := 0
|
||||||
versionsMap := map[string]bool{}
|
versionsMap := map[string]bool{}
|
||||||
@@ -562,6 +566,11 @@ func ValidateCustomResourceColumnDefinition(col *apiextensions.CustomResourceCol
|
|||||||
// specStandardValidator applies validations for different OpenAPI specification versions.
|
// specStandardValidator applies validations for different OpenAPI specification versions.
|
||||||
type specStandardValidator interface {
|
type specStandardValidator interface {
|
||||||
validate(spec *apiextensions.JSONSchemaProps, fldPath *field.Path) field.ErrorList
|
validate(spec *apiextensions.JSONSchemaProps, fldPath *field.Path) field.ErrorList
|
||||||
|
withForbiddenDefaults(reason string) specStandardValidator
|
||||||
|
|
||||||
|
// insideResourceMeta returns true when validating either TypeMeta or ObjectMeta, from an embedded resource or on the top-level.
|
||||||
|
insideResourceMeta() bool
|
||||||
|
withInsideResourceMeta() specStandardValidator
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateCustomResourceDefinitionValidation statically validates
|
// ValidateCustomResourceDefinitionValidation statically validates
|
||||||
@@ -608,7 +617,7 @@ func ValidateCustomResourceDefinitionValidation(customResourceValidation *apiext
|
|||||||
openAPIV3Schema := &specStandardValidatorV3{
|
openAPIV3Schema := &specStandardValidatorV3{
|
||||||
allowDefaults: allowDefaults,
|
allowDefaults: allowDefaults,
|
||||||
}
|
}
|
||||||
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(schema, fldPath.Child("openAPIV3Schema"), openAPIV3Schema)...)
|
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(schema, fldPath.Child("openAPIV3Schema"), openAPIV3Schema, true)...)
|
||||||
|
|
||||||
if mustBeStructural {
|
if mustBeStructural {
|
||||||
if ss, err := structuralschema.NewStructural(schema); err != nil {
|
if ss, err := structuralschema.NewStructural(schema); err != nil {
|
||||||
@@ -631,8 +640,10 @@ func ValidateCustomResourceDefinitionValidation(customResourceValidation *apiext
|
|||||||
return allErrs
|
return allErrs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var metaFields = sets.NewString("metadata", "apiVersion", "kind")
|
||||||
|
|
||||||
// ValidateCustomResourceDefinitionOpenAPISchema statically validates
|
// ValidateCustomResourceDefinitionOpenAPISchema statically validates
|
||||||
func ValidateCustomResourceDefinitionOpenAPISchema(schema *apiextensions.JSONSchemaProps, fldPath *field.Path, ssv specStandardValidator) field.ErrorList {
|
func ValidateCustomResourceDefinitionOpenAPISchema(schema *apiextensions.JSONSchemaProps, fldPath *field.Path, ssv specStandardValidator, isRoot bool) field.ErrorList {
|
||||||
allErrs := field.ErrorList{}
|
allErrs := field.ErrorList{}
|
||||||
|
|
||||||
if schema == nil {
|
if schema == nil {
|
||||||
@@ -660,63 +671,68 @@ func ValidateCustomResourceDefinitionOpenAPISchema(schema *apiextensions.JSONSch
|
|||||||
allErrs = append(allErrs, field.Forbidden(fldPath.Child("additionalProperties"), "additionalProperties and properties are mutual exclusive"))
|
allErrs = append(allErrs, field.Forbidden(fldPath.Child("additionalProperties"), "additionalProperties and properties are mutual exclusive"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(schema.AdditionalProperties.Schema, fldPath.Child("additionalProperties"), ssv)...)
|
// Note: we forbid additionalProperties at resource root, both embedded and top-level.
|
||||||
|
// But further inside, additionalProperites is possible, e.g. for labels or annotations.
|
||||||
|
subSsv := ssv
|
||||||
|
if ssv.insideResourceMeta() {
|
||||||
|
// we have to forbid defaults inside additionalProperties because pruning without actual value is ambiguous
|
||||||
|
subSsv = ssv.withForbiddenDefaults("inside additionalProperties applying to object metadata")
|
||||||
|
}
|
||||||
|
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(schema.AdditionalProperties.Schema, fldPath.Child("additionalProperties"), subSsv, false)...)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(schema.Properties) != 0 {
|
if len(schema.Properties) != 0 {
|
||||||
for property, jsonSchema := range schema.Properties {
|
for property, jsonSchema := range schema.Properties {
|
||||||
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(&jsonSchema, fldPath.Child("properties").Key(property), ssv)...)
|
subSsv := ssv
|
||||||
|
if (isRoot || schema.XEmbeddedResource) && metaFields.Has(property) {
|
||||||
|
// we recurse into the schema that applies to ObjectMeta.
|
||||||
|
subSsv = ssv.withInsideResourceMeta()
|
||||||
|
if isRoot {
|
||||||
|
subSsv = subSsv.withForbiddenDefaults(fmt.Sprintf("in top-level %s", property))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(&jsonSchema, fldPath.Child("properties").Key(property), subSsv, false)...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(schema.PatternProperties) != 0 {
|
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(schema.Not, fldPath.Child("not"), ssv, false)...)
|
||||||
for property, jsonSchema := range schema.PatternProperties {
|
|
||||||
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(&jsonSchema, fldPath.Child("patternProperties").Key(property), ssv)...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if schema.AdditionalItems != nil {
|
|
||||||
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(schema.AdditionalItems.Schema, fldPath.Child("additionalItems"), ssv)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(schema.Not, fldPath.Child("not"), ssv)...)
|
|
||||||
|
|
||||||
if len(schema.AllOf) != 0 {
|
if len(schema.AllOf) != 0 {
|
||||||
for i, jsonSchema := range schema.AllOf {
|
for i, jsonSchema := range schema.AllOf {
|
||||||
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(&jsonSchema, fldPath.Child("allOf").Index(i), ssv)...)
|
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(&jsonSchema, fldPath.Child("allOf").Index(i), ssv, false)...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(schema.OneOf) != 0 {
|
if len(schema.OneOf) != 0 {
|
||||||
for i, jsonSchema := range schema.OneOf {
|
for i, jsonSchema := range schema.OneOf {
|
||||||
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(&jsonSchema, fldPath.Child("oneOf").Index(i), ssv)...)
|
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(&jsonSchema, fldPath.Child("oneOf").Index(i), ssv, false)...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(schema.AnyOf) != 0 {
|
if len(schema.AnyOf) != 0 {
|
||||||
for i, jsonSchema := range schema.AnyOf {
|
for i, jsonSchema := range schema.AnyOf {
|
||||||
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(&jsonSchema, fldPath.Child("anyOf").Index(i), ssv)...)
|
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(&jsonSchema, fldPath.Child("anyOf").Index(i), ssv, false)...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(schema.Definitions) != 0 {
|
if len(schema.Definitions) != 0 {
|
||||||
for definition, jsonSchema := range schema.Definitions {
|
for definition, jsonSchema := range schema.Definitions {
|
||||||
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(&jsonSchema, fldPath.Child("definitions").Key(definition), ssv)...)
|
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(&jsonSchema, fldPath.Child("definitions").Key(definition), ssv, false)...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if schema.Items != nil {
|
if schema.Items != nil {
|
||||||
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(schema.Items.Schema, fldPath.Child("items"), ssv)...)
|
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(schema.Items.Schema, fldPath.Child("items"), ssv, false)...)
|
||||||
if len(schema.Items.JSONSchemas) != 0 {
|
if len(schema.Items.JSONSchemas) != 0 {
|
||||||
for i, jsonSchema := range schema.Items.JSONSchemas {
|
for i, jsonSchema := range schema.Items.JSONSchemas {
|
||||||
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(&jsonSchema, fldPath.Child("items").Index(i), ssv)...)
|
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(&jsonSchema, fldPath.Child("items").Index(i), ssv, false)...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if schema.Dependencies != nil {
|
if schema.Dependencies != nil {
|
||||||
for dependency, jsonSchemaPropsOrStringArray := range schema.Dependencies {
|
for dependency, jsonSchemaPropsOrStringArray := range schema.Dependencies {
|
||||||
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(jsonSchemaPropsOrStringArray.Schema, fldPath.Child("dependencies").Key(dependency), ssv)...)
|
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(jsonSchemaPropsOrStringArray.Schema, fldPath.Child("dependencies").Key(dependency), ssv, false)...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -728,7 +744,26 @@ func ValidateCustomResourceDefinitionOpenAPISchema(schema *apiextensions.JSONSch
|
|||||||
}
|
}
|
||||||
|
|
||||||
type specStandardValidatorV3 struct {
|
type specStandardValidatorV3 struct {
|
||||||
allowDefaults bool
|
allowDefaults bool
|
||||||
|
disallowDefaultsReason string
|
||||||
|
isInsideResourceMeta bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *specStandardValidatorV3) withForbiddenDefaults(reason string) specStandardValidator {
|
||||||
|
clone := *v
|
||||||
|
clone.disallowDefaultsReason = reason
|
||||||
|
clone.allowDefaults = false
|
||||||
|
return &clone
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *specStandardValidatorV3) withInsideResourceMeta() specStandardValidator {
|
||||||
|
clone := *v
|
||||||
|
clone.isInsideResourceMeta = true
|
||||||
|
return &clone
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *specStandardValidatorV3) insideResourceMeta() bool {
|
||||||
|
return v.isInsideResourceMeta
|
||||||
}
|
}
|
||||||
|
|
||||||
// validate validates against OpenAPI Schema v3.
|
// validate validates against OpenAPI Schema v3.
|
||||||
@@ -747,20 +782,37 @@ func (v *specStandardValidatorV3) validate(schema *apiextensions.JSONSchemaProps
|
|||||||
if v.allowDefaults {
|
if v.allowDefaults {
|
||||||
if s, err := structuralschema.NewStructural(schema); err == nil {
|
if s, err := structuralschema.NewStructural(schema); err == nil {
|
||||||
// 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.
|
||||||
pruned := runtime.DeepCopyJSONValue(*schema.Default)
|
|
||||||
pruning.Prune(pruned, s)
|
clone := runtime.DeepCopyJSONValue(interface{}(*schema.Default))
|
||||||
if !reflect.DeepEqual(pruned, *schema.Default) {
|
if !v.isInsideResourceMeta || s.XEmbeddedResource {
|
||||||
allErrs = append(allErrs, field.Invalid(fldPath.Child("default"), schema.Default, "must not have unspecified fields"))
|
pruning.Prune(clone, s, s.XEmbeddedResource)
|
||||||
|
// 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.
|
||||||
|
// TODO: be precise about pruning under metadata
|
||||||
|
}
|
||||||
|
// 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 {
|
||||||
|
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 {
|
||||||
|
// validate an embedded resource
|
||||||
|
schemaobjectmeta.Validate(fldPath, interface{}(*schema.Default), nil, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// validate the default value. Only validating and pruned defaults are allowed.
|
// validate the default value with user the provided schema.
|
||||||
validator := govalidate.NewSchemaValidator(s.ToGoOpenAPI(), nil, "", strfmt.Default)
|
validator := govalidate.NewSchemaValidator(s.ToGoOpenAPI(), nil, "", strfmt.Default)
|
||||||
if err := apiservervalidation.ValidateCustomResource(pruned, validator); err != nil {
|
if err := apiservervalidation.ValidateCustomResource(interface{}(*schema.Default), validator); err != nil {
|
||||||
allErrs = append(allErrs, field.Invalid(fldPath.Child("default"), schema.Default, fmt.Sprintf("must validate: %v", err)))
|
allErrs = append(allErrs, field.Invalid(fldPath.Child("default"), schema.Default, fmt.Sprintf("must validate: %v", err)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
allErrs = append(allErrs, field.Forbidden(fldPath.Child("default"), "must not be set"))
|
detail := "must not be set"
|
||||||
|
if len(v.disallowDefaultsReason) > 0 {
|
||||||
|
detail += " " + v.disallowDefaultsReason
|
||||||
|
}
|
||||||
|
allErrs = append(allErrs, field.Forbidden(fldPath.Child("default"), detail))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -796,6 +848,10 @@ func (v *specStandardValidatorV3) validate(schema *apiextensions.JSONSchemaProps
|
|||||||
allErrs = append(allErrs, field.Forbidden(fldPath.Child("items"), "items must be a schema object and not an array"))
|
allErrs = append(allErrs, field.Forbidden(fldPath.Child("items"), "items must be a schema object and not an array"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if v.isInsideResourceMeta && schema.XEmbeddedResource {
|
||||||
|
allErrs = append(allErrs, field.Forbidden(fldPath.Child("x-kubernetes-embedded-resource"), "must not be used inside of resource meta"))
|
||||||
|
}
|
||||||
|
|
||||||
return allErrs
|
return allErrs
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -953,3 +1009,86 @@ func schemaHasDefaults(s *apiextensions.JSONSchemaProps) bool {
|
|||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func specHasKubernetesExtensions(spec *apiextensions.CustomResourceDefinitionSpec) bool {
|
||||||
|
if spec.Validation != nil && schemaHasKubernetesExtensions(spec.Validation.OpenAPIV3Schema) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for _, v := range spec.Versions {
|
||||||
|
if v.Schema != nil && schemaHasKubernetesExtensions(v.Schema.OpenAPIV3Schema) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func schemaHasKubernetesExtensions(s *apiextensions.JSONSchemaProps) bool {
|
||||||
|
if s == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.XEmbeddedResource || s.XPreserveUnknownFields != nil || s.XIntOrString {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.Items != nil {
|
||||||
|
if s.Items != nil && schemaHasKubernetesExtensions(s.Items.Schema) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for _, s := range s.Items.JSONSchemas {
|
||||||
|
if schemaHasKubernetesExtensions(&s) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, s := range s.AllOf {
|
||||||
|
if schemaHasKubernetesExtensions(&s) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, s := range s.AnyOf {
|
||||||
|
if schemaHasKubernetesExtensions(&s) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, s := range s.OneOf {
|
||||||
|
if schemaHasKubernetesExtensions(&s) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if schemaHasKubernetesExtensions(s.Not) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for _, s := range s.Properties {
|
||||||
|
if schemaHasKubernetesExtensions(&s) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if s.AdditionalProperties != nil {
|
||||||
|
if schemaHasKubernetesExtensions(s.AdditionalProperties.Schema) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, s := range s.PatternProperties {
|
||||||
|
if schemaHasKubernetesExtensions(&s) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if s.AdditionalItems != nil {
|
||||||
|
if schemaHasKubernetesExtensions(s.AdditionalItems.Schema) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, s := range s.Definitions {
|
||||||
|
if schemaHasKubernetesExtensions(&s) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, d := range s.Dependencies {
|
||||||
|
if schemaHasKubernetesExtensions(d.Schema) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
@@ -1442,8 +1442,12 @@ func TestValidateCustomResourceDefinition(t *testing.T) {
|
|||||||
},
|
},
|
||||||
Validation: &apiextensions.CustomResourceValidation{
|
Validation: &apiextensions.CustomResourceValidation{
|
||||||
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
||||||
|
Type: "object",
|
||||||
Properties: map[string]apiextensions.JSONSchemaProps{
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
"a": {Default: jsonPtr(42.0)},
|
"a": {
|
||||||
|
Type: "number",
|
||||||
|
Default: jsonPtr(42.0),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -1457,6 +1461,177 @@ func TestValidateCustomResourceDefinition(t *testing.T) {
|
|||||||
forbidden("spec", "validation", "openAPIV3Schema", "properties[a]", "default"), // disabled feature-gate
|
forbidden("spec", "validation", "openAPIV3Schema", "properties[a]", "default"), // disabled feature-gate
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "x-kubernetes-int-or-string without structural",
|
||||||
|
resource: &apiextensions.CustomResourceDefinition{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "plural.group.com"},
|
||||||
|
Spec: apiextensions.CustomResourceDefinitionSpec{
|
||||||
|
Group: "group.com",
|
||||||
|
Version: "version",
|
||||||
|
Versions: singleVersionList,
|
||||||
|
Scope: apiextensions.NamespaceScoped,
|
||||||
|
Names: apiextensions.CustomResourceDefinitionNames{
|
||||||
|
Plural: "plural",
|
||||||
|
Singular: "singular",
|
||||||
|
Kind: "Plural",
|
||||||
|
ListKind: "PluralList",
|
||||||
|
},
|
||||||
|
Validation: &apiextensions.CustomResourceValidation{
|
||||||
|
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"intorstring": {
|
||||||
|
XIntOrString: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
PreserveUnknownFields: pointer.BoolPtr(true),
|
||||||
|
},
|
||||||
|
Status: apiextensions.CustomResourceDefinitionStatus{
|
||||||
|
StoredVersions: []string{"version"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errors: []validationMatch{
|
||||||
|
required("spec", "validation", "openAPIV3Schema", "type"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "x-kubernetes-preserve-unknown-fields without structural",
|
||||||
|
resource: &apiextensions.CustomResourceDefinition{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "plural.group.com"},
|
||||||
|
Spec: apiextensions.CustomResourceDefinitionSpec{
|
||||||
|
Group: "group.com",
|
||||||
|
Version: "version",
|
||||||
|
Versions: singleVersionList,
|
||||||
|
Scope: apiextensions.NamespaceScoped,
|
||||||
|
Names: apiextensions.CustomResourceDefinitionNames{
|
||||||
|
Plural: "plural",
|
||||||
|
Singular: "singular",
|
||||||
|
Kind: "Plural",
|
||||||
|
ListKind: "PluralList",
|
||||||
|
},
|
||||||
|
Validation: &apiextensions.CustomResourceValidation{
|
||||||
|
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"raw": {
|
||||||
|
XPreserveUnknownFields: pointer.BoolPtr(true),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
PreserveUnknownFields: pointer.BoolPtr(true),
|
||||||
|
},
|
||||||
|
Status: apiextensions.CustomResourceDefinitionStatus{
|
||||||
|
StoredVersions: []string{"version"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errors: []validationMatch{
|
||||||
|
required("spec", "validation", "openAPIV3Schema", "type"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "x-kubernetes-embedded-resource without structural",
|
||||||
|
resource: &apiextensions.CustomResourceDefinition{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "plural.group.com"},
|
||||||
|
Spec: apiextensions.CustomResourceDefinitionSpec{
|
||||||
|
Group: "group.com",
|
||||||
|
Version: "version",
|
||||||
|
Versions: singleVersionList,
|
||||||
|
Scope: apiextensions.NamespaceScoped,
|
||||||
|
Names: apiextensions.CustomResourceDefinitionNames{
|
||||||
|
Plural: "plural",
|
||||||
|
Singular: "singular",
|
||||||
|
Kind: "Plural",
|
||||||
|
ListKind: "PluralList",
|
||||||
|
},
|
||||||
|
Validation: &apiextensions.CustomResourceValidation{
|
||||||
|
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"embedded": {
|
||||||
|
Type: "object",
|
||||||
|
XEmbeddedResource: true,
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"foo": {Type: "string"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
PreserveUnknownFields: pointer.BoolPtr(true),
|
||||||
|
},
|
||||||
|
Status: apiextensions.CustomResourceDefinitionStatus{
|
||||||
|
StoredVersions: []string{"version"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errors: []validationMatch{
|
||||||
|
required("spec", "validation", "openAPIV3Schema", "type"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "x-kubernetes-embedded-resource inside resource meta",
|
||||||
|
resource: &apiextensions.CustomResourceDefinition{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "plural.group.com"},
|
||||||
|
Spec: apiextensions.CustomResourceDefinitionSpec{
|
||||||
|
Group: "group.com",
|
||||||
|
Version: "version",
|
||||||
|
Versions: singleVersionList,
|
||||||
|
Scope: apiextensions.NamespaceScoped,
|
||||||
|
Names: apiextensions.CustomResourceDefinitionNames{
|
||||||
|
Plural: "plural",
|
||||||
|
Singular: "singular",
|
||||||
|
Kind: "Plural",
|
||||||
|
ListKind: "PluralList",
|
||||||
|
},
|
||||||
|
Validation: &apiextensions.CustomResourceValidation{
|
||||||
|
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
||||||
|
Type: "object",
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"embedded": {
|
||||||
|
Type: "object",
|
||||||
|
XEmbeddedResource: true,
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"metadata": {
|
||||||
|
Type: "object",
|
||||||
|
XEmbeddedResource: true,
|
||||||
|
XPreserveUnknownFields: pointer.BoolPtr(true),
|
||||||
|
},
|
||||||
|
"apiVersion": {
|
||||||
|
Type: "string",
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"foo": {
|
||||||
|
Type: "object",
|
||||||
|
XEmbeddedResource: true,
|
||||||
|
XPreserveUnknownFields: pointer.BoolPtr(true),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"kind": {
|
||||||
|
Type: "string",
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"foo": {
|
||||||
|
Type: "object",
|
||||||
|
XEmbeddedResource: true,
|
||||||
|
XPreserveUnknownFields: pointer.BoolPtr(true),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
PreserveUnknownFields: pointer.BoolPtr(true),
|
||||||
|
},
|
||||||
|
Status: apiextensions.CustomResourceDefinitionStatus{
|
||||||
|
StoredVersions: []string{"version"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errors: []validationMatch{
|
||||||
|
forbidden("spec", "validation", "openAPIV3Schema", "properties[embedded]", "properties[metadata]", "x-kubernetes-embedded-resource"),
|
||||||
|
forbidden("spec", "validation", "openAPIV3Schema", "properties[embedded]", "properties[apiVersion]", "properties[foo]", "x-kubernetes-embedded-resource"),
|
||||||
|
forbidden("spec", "validation", "openAPIV3Schema", "properties[embedded]", "properties[kind]", "properties[foo]", "x-kubernetes-embedded-resource"),
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "defaults with enabled feature gate, unstructural schema",
|
name: "defaults with enabled feature gate, unstructural schema",
|
||||||
resource: &apiextensions.CustomResourceDefinition{
|
resource: &apiextensions.CustomResourceDefinition{
|
||||||
@@ -1679,6 +1854,63 @@ func TestValidateCustomResourceDefinition(t *testing.T) {
|
|||||||
},
|
},
|
||||||
XPreserveUnknownFields: pointer.BoolPtr(true),
|
XPreserveUnknownFields: pointer.BoolPtr(true),
|
||||||
},
|
},
|
||||||
|
// x-kubernetes-embedded-resource: true
|
||||||
|
"embedded-fine": {
|
||||||
|
Type: "object",
|
||||||
|
XEmbeddedResource: true,
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"foo": {
|
||||||
|
Type: "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Default: jsonPtr(map[string]interface{}{
|
||||||
|
"foo": "abc",
|
||||||
|
"apiVersion": "foo/v1",
|
||||||
|
"kind": "v1",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "foo",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
"embedded-preserve": {
|
||||||
|
Type: "object",
|
||||||
|
XEmbeddedResource: true,
|
||||||
|
XPreserveUnknownFields: pointer.BoolPtr(true),
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"foo": {
|
||||||
|
Type: "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Default: jsonPtr(map[string]interface{}{
|
||||||
|
"foo": "abc",
|
||||||
|
"apiVersion": "foo/v1",
|
||||||
|
"kind": "v1",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "foo",
|
||||||
|
},
|
||||||
|
"bar": int64(42),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
"embedded-preserve-unpruned-objectmeta": {
|
||||||
|
Type: "object",
|
||||||
|
XEmbeddedResource: true,
|
||||||
|
XPreserveUnknownFields: pointer.BoolPtr(true),
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"foo": {
|
||||||
|
Type: "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Default: jsonPtr(map[string]interface{}{
|
||||||
|
"foo": "abc",
|
||||||
|
"apiVersion": "foo/v1",
|
||||||
|
"kind": "v1",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "foo",
|
||||||
|
"unspecified": "bar",
|
||||||
|
},
|
||||||
|
"bar": int64(42),
|
||||||
|
}),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -1697,6 +1929,7 @@ func TestValidateCustomResourceDefinition(t *testing.T) {
|
|||||||
// strict here, but want to encourage proper specifications by forbidding other defaults.
|
// strict here, but want to encourage proper specifications by forbidding other defaults.
|
||||||
invalid("spec", "validation", "openAPIV3Schema", "properties[e]", "properties[preserveUnknownFields]", "default"),
|
invalid("spec", "validation", "openAPIV3Schema", "properties[e]", "properties[preserveUnknownFields]", "default"),
|
||||||
invalid("spec", "validation", "openAPIV3Schema", "properties[e]", "properties[nestedProperties]", "default"),
|
invalid("spec", "validation", "openAPIV3Schema", "properties[e]", "properties[nestedProperties]", "default"),
|
||||||
|
invalid("spec", "validation", "openAPIV3Schema", "properties[embedded-preserve-unpruned-objectmeta]", "default"),
|
||||||
},
|
},
|
||||||
|
|
||||||
enabledFeatures: []featuregate.Feature{features.CustomResourceDefaulting},
|
enabledFeatures: []featuregate.Feature{features.CustomResourceDefaulting},
|
||||||
@@ -1738,6 +1971,412 @@ func TestValidateCustomResourceDefinition(t *testing.T) {
|
|||||||
},
|
},
|
||||||
enabledFeatures: []featuregate.Feature{features.CustomResourceDefaulting},
|
enabledFeatures: []featuregate.Feature{features.CustomResourceDefaulting},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "additionalProperties at resource root",
|
||||||
|
resource: &apiextensions.CustomResourceDefinition{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "plural.group.com"},
|
||||||
|
Spec: apiextensions.CustomResourceDefinitionSpec{
|
||||||
|
Group: "group.com",
|
||||||
|
Version: "version",
|
||||||
|
Versions: singleVersionList,
|
||||||
|
Scope: apiextensions.NamespaceScoped,
|
||||||
|
Names: apiextensions.CustomResourceDefinitionNames{
|
||||||
|
Plural: "plural",
|
||||||
|
Singular: "singular",
|
||||||
|
Kind: "Plural",
|
||||||
|
ListKind: "PluralList",
|
||||||
|
},
|
||||||
|
Validation: &apiextensions.CustomResourceValidation{
|
||||||
|
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
||||||
|
Type: "object",
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"embedded1": {
|
||||||
|
Type: "object",
|
||||||
|
XEmbeddedResource: true,
|
||||||
|
AdditionalProperties: &apiextensions.JSONSchemaPropsOrBool{
|
||||||
|
Schema: &apiextensions.JSONSchemaProps{Type: "string"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"embedded2": {
|
||||||
|
Type: "object",
|
||||||
|
XEmbeddedResource: true,
|
||||||
|
XPreserveUnknownFields: pointer.BoolPtr(true),
|
||||||
|
AdditionalProperties: &apiextensions.JSONSchemaPropsOrBool{
|
||||||
|
Schema: &apiextensions.JSONSchemaProps{Type: "string"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
PreserveUnknownFields: pointer.BoolPtr(false),
|
||||||
|
},
|
||||||
|
Status: apiextensions.CustomResourceDefinitionStatus{
|
||||||
|
StoredVersions: []string{"version"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errors: []validationMatch{
|
||||||
|
forbidden("spec", "validation", "openAPIV3Schema", "properties[embedded1]", "additionalProperties"),
|
||||||
|
required("spec", "validation", "openAPIV3Schema", "properties[embedded1]", "properties"),
|
||||||
|
forbidden("spec", "validation", "openAPIV3Schema", "properties[embedded2]", "additionalProperties"),
|
||||||
|
},
|
||||||
|
enabledFeatures: []featuregate.Feature{features.CustomResourceDefaulting},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "metadata defaults",
|
||||||
|
resource: &apiextensions.CustomResourceDefinition{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "plural.group.com"},
|
||||||
|
Spec: apiextensions.CustomResourceDefinitionSpec{
|
||||||
|
Group: "group.com",
|
||||||
|
Version: "v1",
|
||||||
|
Versions: []apiextensions.CustomResourceDefinitionVersion{
|
||||||
|
{
|
||||||
|
Name: "v1",
|
||||||
|
Served: true,
|
||||||
|
Storage: true,
|
||||||
|
Schema: &apiextensions.CustomResourceValidation{
|
||||||
|
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
||||||
|
Type: "object",
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"metadata": {
|
||||||
|
Type: "object",
|
||||||
|
// forbidden: no default for top-level metadata
|
||||||
|
Default: jsonPtr(map[string]interface{}{
|
||||||
|
"name": "foo",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
"embedded": {
|
||||||
|
Type: "object",
|
||||||
|
XEmbeddedResource: true,
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"metadata": {
|
||||||
|
Type: "object",
|
||||||
|
Default: jsonPtr(map[string]interface{}{
|
||||||
|
"name": "foo",
|
||||||
|
// TODO: forbid unknown field under metadata
|
||||||
|
"unknown": int64(42),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "v2",
|
||||||
|
Served: true,
|
||||||
|
Storage: false,
|
||||||
|
Schema: &apiextensions.CustomResourceValidation{
|
||||||
|
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
||||||
|
Type: "object",
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"metadata": {
|
||||||
|
Type: "object",
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"name": {
|
||||||
|
Type: "string",
|
||||||
|
// forbidden: no default in top-level metadata
|
||||||
|
Default: jsonPtr("foo"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"embedded": {
|
||||||
|
Type: "object",
|
||||||
|
XEmbeddedResource: true,
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"apiVersion": {
|
||||||
|
Type: "string",
|
||||||
|
Default: jsonPtr("v1"),
|
||||||
|
},
|
||||||
|
"kind": {
|
||||||
|
Type: "string",
|
||||||
|
Default: jsonPtr("Pod"),
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
Type: "object",
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"name": {
|
||||||
|
Type: "string",
|
||||||
|
Default: jsonPtr("foo"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "v3",
|
||||||
|
Served: true,
|
||||||
|
Storage: false,
|
||||||
|
Schema: &apiextensions.CustomResourceValidation{
|
||||||
|
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
||||||
|
Type: "object",
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"embedded": {
|
||||||
|
Type: "object",
|
||||||
|
XEmbeddedResource: true,
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"apiVersion": {
|
||||||
|
Type: "string",
|
||||||
|
Default: jsonPtr("v1"),
|
||||||
|
},
|
||||||
|
"kind": {
|
||||||
|
Type: "string",
|
||||||
|
// TODO: forbid non-validating nested values in metadata
|
||||||
|
Default: jsonPtr("%"),
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
Type: "object",
|
||||||
|
Default: jsonPtr(map[string]interface{}{
|
||||||
|
"labels": map[string]interface{}{
|
||||||
|
// TODO: forbid non-validating nested field in meta
|
||||||
|
"bar": "x y",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "v4",
|
||||||
|
Served: true,
|
||||||
|
Storage: false,
|
||||||
|
Schema: &apiextensions.CustomResourceValidation{
|
||||||
|
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
||||||
|
Type: "object",
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"embedded": {
|
||||||
|
Type: "object",
|
||||||
|
XEmbeddedResource: true,
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"metadata": {
|
||||||
|
Type: "object",
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"name": {
|
||||||
|
Type: "string",
|
||||||
|
// TODO: forbid wrongly typed nested fields in metadata
|
||||||
|
Default: jsonPtr("%"),
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
Type: "object",
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"bar": {
|
||||||
|
Type: "string",
|
||||||
|
// TODO: forbid non-validating nested fields in metadata
|
||||||
|
Default: jsonPtr("x y"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"annotations": {
|
||||||
|
Type: "object",
|
||||||
|
AdditionalProperties: &apiextensions.JSONSchemaPropsOrBool{
|
||||||
|
Schema: &apiextensions.JSONSchemaProps{
|
||||||
|
Type: "string",
|
||||||
|
// forbidden: no default under additionalProperties inside of metadata
|
||||||
|
Default: jsonPtr("abc"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Scope: apiextensions.NamespaceScoped,
|
||||||
|
Names: apiextensions.CustomResourceDefinitionNames{
|
||||||
|
Plural: "plural",
|
||||||
|
Singular: "singular",
|
||||||
|
Kind: "Plural",
|
||||||
|
ListKind: "PluralList",
|
||||||
|
},
|
||||||
|
PreserveUnknownFields: pointer.BoolPtr(false),
|
||||||
|
},
|
||||||
|
Status: apiextensions.CustomResourceDefinitionStatus{
|
||||||
|
StoredVersions: []string{"v1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errors: []validationMatch{
|
||||||
|
// Forbidden: must not be set in top-level metadata
|
||||||
|
forbidden("spec", "versions[0]", "schema", "openAPIV3Schema", "properties[metadata]", "default"),
|
||||||
|
// Invalid value: map[string]interface {}{"name":"foo", "unknown":42}: must not have unknown fields
|
||||||
|
// TODO: invalid("spec", "versions[0]", "schema", "openAPIV3Schema", "properties[embedded]", "properties[metadata]", "default"),
|
||||||
|
|
||||||
|
// Forbidden: must not be set in top-level metadata
|
||||||
|
forbidden("spec", "versions[1]", "schema", "openAPIV3Schema", "properties[metadata]", "properties[name]", "default"),
|
||||||
|
|
||||||
|
// Invalid value: "x y"
|
||||||
|
// TODO: invalid("spec", "versions[2]", "schema", "openAPIV3Schema", "properties[embedded]", "properties[metadata]", "default"),
|
||||||
|
// Invalid value: "%": kind: Invalid value: "%"
|
||||||
|
// TODO: invalid("spec", "versions[2]", "schema", "openAPIV3Schema", "properties[embedded]", "properties[kind]", "default"),
|
||||||
|
|
||||||
|
// Invalid value: "%"
|
||||||
|
// TODO: invalid("spec", "versions[3]", "schema", "openAPIV3Schema", "properties[embedded]", "properties[metadata]", "properties[labels]", "properties[bar]", "default"),
|
||||||
|
// Invalid value: "x y"
|
||||||
|
// TODO: invalid("spec", "versions[3]", "schema", "openAPIV3Schema", "properties[embedded]", "properties[metadata]", "properties[name]", "default"),
|
||||||
|
// Forbidden: must not be set inside additionalProperties applying to object metadata
|
||||||
|
forbidden("spec", "versions[3]", "schema", "openAPIV3Schema", "properties[embedded]", "properties[metadata]", "properties[annotations]", "additionalProperties", "default"),
|
||||||
|
},
|
||||||
|
enabledFeatures: []featuregate.Feature{features.CustomResourceDefaulting},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "contradicting meta field types",
|
||||||
|
resource: &apiextensions.CustomResourceDefinition{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "plural.group.com"},
|
||||||
|
Spec: apiextensions.CustomResourceDefinitionSpec{
|
||||||
|
Group: "group.com",
|
||||||
|
Version: "version",
|
||||||
|
Versions: singleVersionList,
|
||||||
|
Scope: apiextensions.NamespaceScoped,
|
||||||
|
Names: apiextensions.CustomResourceDefinitionNames{
|
||||||
|
Plural: "plural",
|
||||||
|
Singular: "singular",
|
||||||
|
Kind: "Plural",
|
||||||
|
ListKind: "PluralList",
|
||||||
|
},
|
||||||
|
Validation: &apiextensions.CustomResourceValidation{
|
||||||
|
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
||||||
|
Type: "object",
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"apiVersion": {Type: "number"},
|
||||||
|
"kind": {Type: "number"},
|
||||||
|
"metadata": {
|
||||||
|
Type: "number",
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"name": {
|
||||||
|
Type: "string",
|
||||||
|
Pattern: "abc",
|
||||||
|
},
|
||||||
|
"generateName": {
|
||||||
|
Type: "string",
|
||||||
|
Pattern: "abc",
|
||||||
|
},
|
||||||
|
"generation": {
|
||||||
|
Type: "integer",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"valid": {
|
||||||
|
Type: "object",
|
||||||
|
XEmbeddedResource: true,
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"apiVersion": {Type: "string"},
|
||||||
|
"kind": {Type: "string"},
|
||||||
|
"metadata": {
|
||||||
|
Type: "object",
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"name": {
|
||||||
|
Type: "string",
|
||||||
|
Pattern: "abc",
|
||||||
|
},
|
||||||
|
"generateName": {
|
||||||
|
Type: "string",
|
||||||
|
Pattern: "abc",
|
||||||
|
},
|
||||||
|
"generation": {
|
||||||
|
Type: "integer",
|
||||||
|
Minimum: float64Ptr(42.0), // does not make sense, but is allowed for nested ObjectMeta
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"invalid": {
|
||||||
|
Type: "object",
|
||||||
|
XEmbeddedResource: true,
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"apiVersion": {Type: "number"},
|
||||||
|
"kind": {Type: "number"},
|
||||||
|
"metadata": {
|
||||||
|
Type: "number",
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"name": {
|
||||||
|
Type: "string",
|
||||||
|
Pattern: "abc",
|
||||||
|
},
|
||||||
|
"generateName": {
|
||||||
|
Type: "string",
|
||||||
|
Pattern: "abc",
|
||||||
|
},
|
||||||
|
"generation": {
|
||||||
|
Type: "integer",
|
||||||
|
Minimum: float64Ptr(42.0), // does not make sense, but is allowed for nested ObjectMeta
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"nested": {
|
||||||
|
Type: "object",
|
||||||
|
XEmbeddedResource: true,
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"invalid": {
|
||||||
|
Type: "object",
|
||||||
|
XEmbeddedResource: true,
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"apiVersion": {Type: "number"},
|
||||||
|
"kind": {Type: "number"},
|
||||||
|
"metadata": {
|
||||||
|
Type: "number",
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"name": {
|
||||||
|
Type: "string",
|
||||||
|
Pattern: "abc",
|
||||||
|
},
|
||||||
|
"generateName": {
|
||||||
|
Type: "string",
|
||||||
|
Pattern: "abc",
|
||||||
|
},
|
||||||
|
"generation": {
|
||||||
|
Type: "integer",
|
||||||
|
Minimum: float64Ptr(42.0), // does not make sense, but is allowed for nested ObjectMeta
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"noEmbeddedObject": {
|
||||||
|
Type: "object",
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"apiVersion": {Type: "number"},
|
||||||
|
"kind": {Type: "number"},
|
||||||
|
"metadata": {Type: "number"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
PreserveUnknownFields: pointer.BoolPtr(false),
|
||||||
|
},
|
||||||
|
Status: apiextensions.CustomResourceDefinitionStatus{
|
||||||
|
StoredVersions: []string{"version"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errors: []validationMatch{
|
||||||
|
forbidden("spec", "validation", "openAPIV3Schema", "properties[metadata]"),
|
||||||
|
invalid("spec", "validation", "openAPIV3Schema", "properties[apiVersion]", "type"),
|
||||||
|
invalid("spec", "validation", "openAPIV3Schema", "properties[kind]", "type"),
|
||||||
|
invalid("spec", "validation", "openAPIV3Schema", "properties[metadata]", "type"),
|
||||||
|
invalid("spec", "validation", "openAPIV3Schema", "properties[invalid]", "properties[apiVersion]", "type"),
|
||||||
|
invalid("spec", "validation", "openAPIV3Schema", "properties[invalid]", "properties[kind]", "type"),
|
||||||
|
invalid("spec", "validation", "openAPIV3Schema", "properties[invalid]", "properties[metadata]", "type"),
|
||||||
|
invalid("spec", "validation", "openAPIV3Schema", "properties[nested]", "properties[invalid]", "properties[apiVersion]", "type"),
|
||||||
|
invalid("spec", "validation", "openAPIV3Schema", "properties[nested]", "properties[invalid]", "properties[kind]", "type"),
|
||||||
|
invalid("spec", "validation", "openAPIV3Schema", "properties[nested]", "properties[invalid]", "properties[metadata]", "type"),
|
||||||
|
},
|
||||||
|
enabledFeatures: []featuregate.Feature{features.CustomResourceDefaulting},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
|
@@ -25,6 +25,7 @@ go_library(
|
|||||||
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/conversion:go_default_library",
|
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/conversion:go_default_library",
|
||||||
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema:go_default_library",
|
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema:go_default_library",
|
||||||
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/defaulting:go_default_library",
|
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/defaulting:go_default_library",
|
||||||
|
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/objectmeta:go_default_library",
|
||||||
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/pruning:go_default_library",
|
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/pruning:go_default_library",
|
||||||
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/validation:go_default_library",
|
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/validation:go_default_library",
|
||||||
"//staging/src/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset:go_default_library",
|
"//staging/src/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset:go_default_library",
|
||||||
@@ -92,26 +93,12 @@ go_library(
|
|||||||
|
|
||||||
go_test(
|
go_test(
|
||||||
name = "go_default_test",
|
name = "go_default_test",
|
||||||
srcs = [
|
srcs = ["customresource_handler_test.go"],
|
||||||
"customresource_handler_test.go",
|
|
||||||
"jsonpath_test.go",
|
|
||||||
],
|
|
||||||
embed = [":go_default_library"],
|
embed = [":go_default_library"],
|
||||||
deps = [
|
deps = [
|
||||||
"//staging/src/k8s.io/api/core/v1:go_default_library",
|
|
||||||
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions:go_default_library",
|
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions:go_default_library",
|
||||||
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/conversion:go_default_library",
|
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/conversion:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/api/apitesting/fuzzer:go_default_library",
|
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/api/equality:go_default_library",
|
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/fuzzer:go_default_library",
|
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library",
|
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/runtime/serializer:go_default_library",
|
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/runtime/serializer/json:go_default_library",
|
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/util/diff:go_default_library",
|
|
||||||
"//staging/src/k8s.io/client-go/kubernetes/scheme:go_default_library",
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@@ -28,10 +28,12 @@ import (
|
|||||||
"github.com/go-openapi/spec"
|
"github.com/go-openapi/spec"
|
||||||
"github.com/go-openapi/strfmt"
|
"github.com/go-openapi/strfmt"
|
||||||
"github.com/go-openapi/validate"
|
"github.com/go-openapi/validate"
|
||||||
|
|
||||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
||||||
"k8s.io/apiextensions-apiserver/pkg/apiserver/conversion"
|
"k8s.io/apiextensions-apiserver/pkg/apiserver/conversion"
|
||||||
structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
|
structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
|
||||||
structuraldefaulting "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/defaulting"
|
structuraldefaulting "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/defaulting"
|
||||||
|
schemaobjectmeta "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/objectmeta"
|
||||||
structuralpruning "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/pruning"
|
structuralpruning "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/pruning"
|
||||||
apiservervalidation "k8s.io/apiextensions-apiserver/pkg/apiserver/validation"
|
apiservervalidation "k8s.io/apiextensions-apiserver/pkg/apiserver/validation"
|
||||||
informers "k8s.io/apiextensions-apiserver/pkg/client/informers/internalversion/apiextensions/internalversion"
|
informers "k8s.io/apiextensions-apiserver/pkg/client/informers/internalversion/apiextensions/internalversion"
|
||||||
@@ -603,6 +605,7 @@ func (r *crdHandler) getOrCreateServingInfoFor(crd *apiextensions.CustomResource
|
|||||||
kind,
|
kind,
|
||||||
validator,
|
validator,
|
||||||
statusValidator,
|
statusValidator,
|
||||||
|
structuralSchemas,
|
||||||
statusSpec,
|
statusSpec,
|
||||||
scaleSpec,
|
scaleSpec,
|
||||||
),
|
),
|
||||||
@@ -1022,7 +1025,7 @@ func (v *unstructuredSchemaCoercer) apply(u *unstructured.Unstructured) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
objectMeta, foundObjectMeta, err := getObjectMeta(u, v.dropInvalidMetadata)
|
objectMeta, foundObjectMeta, err := schemaobjectmeta.GetObjectMeta(u.Object, v.dropInvalidMetadata)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -1032,8 +1035,14 @@ func (v *unstructuredSchemaCoercer) apply(u *unstructured.Unstructured) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if !v.preserveUnknownFields && gv.Group == v.structuralSchemaGK.Group && kind == v.structuralSchemaGK.Kind {
|
if gv.Group == v.structuralSchemaGK.Group && kind == v.structuralSchemaGK.Kind {
|
||||||
structuralpruning.Prune(u.Object, v.structuralSchemas[gv.Version])
|
if !v.preserveUnknownFields {
|
||||||
|
// TODO: switch over pruning and coercing at the root to schemaobjectmeta.Coerce too
|
||||||
|
structuralpruning.Prune(u.Object, v.structuralSchemas[gv.Version], false)
|
||||||
|
}
|
||||||
|
if err := schemaobjectmeta.Coerce(nil, u.Object, v.structuralSchemas[gv.Version], false, v.dropInvalidMetadata); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// restore meta fields, starting clean
|
// restore meta fields, starting clean
|
||||||
@@ -1044,72 +1053,10 @@ func (v *unstructuredSchemaCoercer) apply(u *unstructured.Unstructured) error {
|
|||||||
u.SetAPIVersion(apiVersion)
|
u.SetAPIVersion(apiVersion)
|
||||||
}
|
}
|
||||||
if foundObjectMeta {
|
if foundObjectMeta {
|
||||||
if err := setObjectMeta(u, objectMeta); err != nil {
|
if err := schemaobjectmeta.SetObjectMeta(u.Object, objectMeta); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var encodingjson = json.CaseSensitiveJsonIterator()
|
|
||||||
|
|
||||||
func getObjectMeta(u *unstructured.Unstructured, dropMalformedFields bool) (*metav1.ObjectMeta, bool, error) {
|
|
||||||
metadata, found := u.UnstructuredContent()["metadata"]
|
|
||||||
if !found {
|
|
||||||
return nil, false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// round-trip through JSON first, hoping that unmarshaling just works
|
|
||||||
objectMeta := &metav1.ObjectMeta{}
|
|
||||||
metadataBytes, err := encodingjson.Marshal(metadata)
|
|
||||||
if err != nil {
|
|
||||||
return nil, false, err
|
|
||||||
}
|
|
||||||
if err = encodingjson.Unmarshal(metadataBytes, objectMeta); err == nil {
|
|
||||||
// if successful, return
|
|
||||||
return objectMeta, true, nil
|
|
||||||
}
|
|
||||||
if !dropMalformedFields {
|
|
||||||
// if we're not trying to drop malformed fields, return the error
|
|
||||||
return nil, true, err
|
|
||||||
}
|
|
||||||
|
|
||||||
metadataMap, ok := metadata.(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
return nil, false, fmt.Errorf("invalid metadata: expected object, got %T", metadata)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Go field by field accumulating into the metadata object.
|
|
||||||
// This takes advantage of the fact that you can repeatedly unmarshal individual fields into a single struct,
|
|
||||||
// each iteration preserving the old key-values.
|
|
||||||
accumulatedObjectMeta := &metav1.ObjectMeta{}
|
|
||||||
testObjectMeta := &metav1.ObjectMeta{}
|
|
||||||
for k, v := range metadataMap {
|
|
||||||
// serialize a single field
|
|
||||||
if singleFieldBytes, err := encodingjson.Marshal(map[string]interface{}{k: v}); err == nil {
|
|
||||||
// do a test unmarshal
|
|
||||||
if encodingjson.Unmarshal(singleFieldBytes, testObjectMeta) == nil {
|
|
||||||
// if that succeeds, unmarshal for real
|
|
||||||
encodingjson.Unmarshal(singleFieldBytes, accumulatedObjectMeta)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return accumulatedObjectMeta, true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func setObjectMeta(u *unstructured.Unstructured, objectMeta *metav1.ObjectMeta) error {
|
|
||||||
if objectMeta == nil {
|
|
||||||
unstructured.RemoveNestedField(u.UnstructuredContent(), "metadata")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
metadata, err := runtime.DefaultUnstructuredConverter.ToUnstructured(objectMeta)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
u.UnstructuredContent()["metadata"] = metadata
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
@@ -17,24 +17,11 @@ limitations under the License.
|
|||||||
package apiserver
|
package apiserver
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"math/rand"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
corev1 "k8s.io/api/core/v1"
|
|
||||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
||||||
"k8s.io/apiextensions-apiserver/pkg/apiserver/conversion"
|
"k8s.io/apiextensions-apiserver/pkg/apiserver/conversion"
|
||||||
"k8s.io/apimachinery/pkg/api/apitesting/fuzzer"
|
|
||||||
"k8s.io/apimachinery/pkg/api/equality"
|
|
||||||
metafuzzer "k8s.io/apimachinery/pkg/apis/meta/fuzzer"
|
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
|
||||||
"k8s.io/apimachinery/pkg/runtime/serializer/json"
|
|
||||||
"k8s.io/apimachinery/pkg/util/diff"
|
|
||||||
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestConvertFieldLabel(t *testing.T) {
|
func TestConvertFieldLabel(t *testing.T) {
|
||||||
@@ -118,202 +105,3 @@ func TestConvertFieldLabel(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRoundtripObjectMeta(t *testing.T) {
|
|
||||||
scheme := runtime.NewScheme()
|
|
||||||
codecs := serializer.NewCodecFactory(scheme)
|
|
||||||
codec := json.NewSerializer(json.DefaultMetaFactory, scheme, scheme, false)
|
|
||||||
seed := rand.Int63()
|
|
||||||
fuzzer := fuzzer.FuzzerFor(metafuzzer.Funcs, rand.NewSource(seed), codecs)
|
|
||||||
|
|
||||||
N := 1000
|
|
||||||
for i := 0; i < N; i++ {
|
|
||||||
u := &unstructured.Unstructured{Object: map[string]interface{}{}}
|
|
||||||
original := &metav1.ObjectMeta{}
|
|
||||||
fuzzer.Fuzz(original)
|
|
||||||
if err := setObjectMeta(u, original); err != nil {
|
|
||||||
t.Fatalf("unexpected error setting ObjectMeta: %v", err)
|
|
||||||
}
|
|
||||||
o, _, err := getObjectMeta(u, false)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unexpected error getting the Objectmeta: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !equality.Semantic.DeepEqual(original, o) {
|
|
||||||
t.Errorf("diff: %v\nCodec: %#v", diff.ObjectReflectDiff(original, o), codec)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestMalformedObjectMetaFields sets a number of different random values and types for all
|
|
||||||
// metadata fields. If json.Unmarshal accepts them, compare that getObjectMeta
|
|
||||||
// gives the same result. Otherwise, drop malformed fields.
|
|
||||||
func TestMalformedObjectMetaFields(t *testing.T) {
|
|
||||||
fuzzer := fuzzer.FuzzerFor(metafuzzer.Funcs, rand.NewSource(rand.Int63()), serializer.NewCodecFactory(runtime.NewScheme()))
|
|
||||||
spuriousValues := func() []interface{} {
|
|
||||||
return []interface{}{
|
|
||||||
// primitives
|
|
||||||
nil,
|
|
||||||
int64(1),
|
|
||||||
float64(1.5),
|
|
||||||
true,
|
|
||||||
"a",
|
|
||||||
// well-formed complex values
|
|
||||||
[]interface{}{"a", "b"},
|
|
||||||
map[string]interface{}{"a": "1", "b": "2"},
|
|
||||||
[]interface{}{int64(1), int64(2)},
|
|
||||||
[]interface{}{float64(1.5), float64(2.5)},
|
|
||||||
// known things json decoding tolerates
|
|
||||||
map[string]interface{}{"a": "1", "b": nil},
|
|
||||||
// malformed things
|
|
||||||
map[string]interface{}{"a": "1", "b": []interface{}{"nested"}},
|
|
||||||
[]interface{}{"a", int64(1), float64(1.5), true, []interface{}{"nested"}},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
N := 100
|
|
||||||
for i := 0; i < N; i++ {
|
|
||||||
fuzzedObjectMeta := &metav1.ObjectMeta{}
|
|
||||||
fuzzer.Fuzz(fuzzedObjectMeta)
|
|
||||||
goodMetaMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(fuzzedObjectMeta.DeepCopy())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
for _, pth := range jsonPaths(nil, goodMetaMap) {
|
|
||||||
for _, v := range spuriousValues() {
|
|
||||||
// skip values of same type, because they can only cause decoding errors further insides
|
|
||||||
orig, err := JsonPathValue(goodMetaMap, pth, 0)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unexpected to not find something at %v: %v", pth, err)
|
|
||||||
}
|
|
||||||
if reflect.TypeOf(v) == reflect.TypeOf(orig) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// make a spurious map
|
|
||||||
spuriousMetaMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(fuzzedObjectMeta.DeepCopy())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if err := SetJsonPath(spuriousMetaMap, pth, 0, v); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// See if it can unmarshal to object meta
|
|
||||||
spuriousJSON, err := encodingjson.Marshal(spuriousMetaMap)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("error on %v=%#v: %v", pth, v, err)
|
|
||||||
}
|
|
||||||
expectedObjectMeta := &metav1.ObjectMeta{}
|
|
||||||
if err := encodingjson.Unmarshal(spuriousJSON, expectedObjectMeta); err != nil {
|
|
||||||
// if standard json unmarshal would fail decoding this field, drop the field entirely
|
|
||||||
truncatedMetaMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(fuzzedObjectMeta.DeepCopy())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// we expect this logic for the different fields:
|
|
||||||
switch {
|
|
||||||
default:
|
|
||||||
// delete complete top-level field by default
|
|
||||||
DeleteJsonPath(truncatedMetaMap, pth[:1], 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
truncatedJSON, err := encodingjson.Marshal(truncatedMetaMap)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("error on %v=%#v: %v", pth, v, err)
|
|
||||||
}
|
|
||||||
expectedObjectMeta = &metav1.ObjectMeta{}
|
|
||||||
if err := encodingjson.Unmarshal(truncatedJSON, expectedObjectMeta); err != nil {
|
|
||||||
t.Fatalf("error on %v=%#v: %v", pth, v, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// make sure dropInvalidTypedFields+getObjectMeta matches what we expect
|
|
||||||
u := &unstructured.Unstructured{Object: map[string]interface{}{"metadata": spuriousMetaMap}}
|
|
||||||
actualObjectMeta, _, err := getObjectMeta(u, true)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("got unexpected error after dropping invalid typed fields on %v=%#v: %v", pth, v, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if !equality.Semantic.DeepEqual(expectedObjectMeta, actualObjectMeta) {
|
|
||||||
t.Errorf("%v=%#v, diff: %v\n", pth, v, diff.ObjectReflectDiff(expectedObjectMeta, actualObjectMeta))
|
|
||||||
t.Errorf("expectedObjectMeta %#v", expectedObjectMeta)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetObjectMetaNils(t *testing.T) {
|
|
||||||
u := &unstructured.Unstructured{
|
|
||||||
Object: map[string]interface{}{
|
|
||||||
"kind": "Pod",
|
|
||||||
"apiVersion": "v1",
|
|
||||||
"metadata": map[string]interface{}{
|
|
||||||
"generateName": nil,
|
|
||||||
"labels": map[string]interface{}{
|
|
||||||
"foo": nil,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
o, _, err := getObjectMeta(u, true)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if o.GenerateName != "" {
|
|
||||||
t.Errorf("expected null json value to be read as \"\" string, but got: %q", o.GenerateName)
|
|
||||||
}
|
|
||||||
if got, expected := o.Labels, map[string]string{"foo": ""}; !reflect.DeepEqual(got, expected) {
|
|
||||||
t.Errorf("unexpected labels, expected=%#v, got=%#v", expected, got)
|
|
||||||
}
|
|
||||||
|
|
||||||
// double check this what the kube JSON decode is doing
|
|
||||||
bs, _ := encodingjson.Marshal(u.UnstructuredContent())
|
|
||||||
kubeObj, _, err := clientgoscheme.Codecs.UniversalDecoder(corev1.SchemeGroupVersion).Decode(bs, nil, nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
pod, ok := kubeObj.(*corev1.Pod)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("expected v1 Pod, got: %T", pod)
|
|
||||||
}
|
|
||||||
if got, expected := o.GenerateName, pod.ObjectMeta.GenerateName; got != expected {
|
|
||||||
t.Errorf("expected generatedName to be %q, got %q", expected, got)
|
|
||||||
}
|
|
||||||
if got, expected := o.Labels, pod.ObjectMeta.Labels; !reflect.DeepEqual(got, expected) {
|
|
||||||
t.Errorf("expected labels to be %v, got %v", expected, got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetObjectMeta(t *testing.T) {
|
|
||||||
for i := 0; i < 100; i++ {
|
|
||||||
u := &unstructured.Unstructured{Object: map[string]interface{}{
|
|
||||||
"metadata": map[string]interface{}{
|
|
||||||
"name": "good",
|
|
||||||
"Name": "bad1",
|
|
||||||
"nAme": "bad2",
|
|
||||||
"naMe": "bad3",
|
|
||||||
"namE": "bad4",
|
|
||||||
|
|
||||||
"namespace": "good",
|
|
||||||
"Namespace": "bad1",
|
|
||||||
"nAmespace": "bad2",
|
|
||||||
"naMespace": "bad3",
|
|
||||||
"namEspace": "bad4",
|
|
||||||
|
|
||||||
"creationTimestamp": "a",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
|
|
||||||
meta, _, err := getObjectMeta(u, true)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if meta.Name != "good" || meta.Namespace != "good" {
|
|
||||||
t.Fatalf("got %#v", meta)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@@ -35,6 +35,7 @@ filegroup(
|
|||||||
srcs = [
|
srcs = [
|
||||||
":package-srcs",
|
":package-srcs",
|
||||||
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/defaulting:all-srcs",
|
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/defaulting:all-srcs",
|
||||||
|
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/objectmeta:all-srcs",
|
||||||
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/pruning:all-srcs",
|
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/pruning:all-srcs",
|
||||||
],
|
],
|
||||||
tags = ["automanaged"],
|
tags = ["automanaged"],
|
||||||
|
@@ -0,0 +1,66 @@
|
|||||||
|
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
|
||||||
|
|
||||||
|
go_library(
|
||||||
|
name = "go_default_library",
|
||||||
|
srcs = [
|
||||||
|
"algorithm.go",
|
||||||
|
"coerce.go",
|
||||||
|
"validation.go",
|
||||||
|
],
|
||||||
|
importmap = "k8s.io/kubernetes/vendor/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/objectmeta",
|
||||||
|
importpath = "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/objectmeta",
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
deps = [
|
||||||
|
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema:go_default_library",
|
||||||
|
"//staging/src/k8s.io/apimachinery/pkg/api/validation:go_default_library",
|
||||||
|
"//staging/src/k8s.io/apimachinery/pkg/api/validation/path:go_default_library",
|
||||||
|
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||||
|
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library",
|
||||||
|
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||||
|
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
||||||
|
"//staging/src/k8s.io/apimachinery/pkg/runtime/serializer/json:go_default_library",
|
||||||
|
"//staging/src/k8s.io/apimachinery/pkg/util/validation:go_default_library",
|
||||||
|
"//staging/src/k8s.io/apimachinery/pkg/util/validation/field:go_default_library",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
go_test(
|
||||||
|
name = "go_default_test",
|
||||||
|
srcs = [
|
||||||
|
"algorithm_test.go",
|
||||||
|
"coerce_test.go",
|
||||||
|
"jsonpath_test.go",
|
||||||
|
"validation_test.go",
|
||||||
|
],
|
||||||
|
embed = [":go_default_library"],
|
||||||
|
deps = [
|
||||||
|
"//staging/src/k8s.io/api/core/v1:go_default_library",
|
||||||
|
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema:go_default_library",
|
||||||
|
"//staging/src/k8s.io/apimachinery/pkg/api/apitesting/fuzzer:go_default_library",
|
||||||
|
"//staging/src/k8s.io/apimachinery/pkg/api/equality:go_default_library",
|
||||||
|
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/fuzzer:go_default_library",
|
||||||
|
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||||
|
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library",
|
||||||
|
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||||
|
"//staging/src/k8s.io/apimachinery/pkg/runtime/serializer:go_default_library",
|
||||||
|
"//staging/src/k8s.io/apimachinery/pkg/runtime/serializer/json:go_default_library",
|
||||||
|
"//staging/src/k8s.io/apimachinery/pkg/util/diff:go_default_library",
|
||||||
|
"//staging/src/k8s.io/apimachinery/pkg/util/json:go_default_library",
|
||||||
|
"//staging/src/k8s.io/apimachinery/pkg/util/validation/field:go_default_library",
|
||||||
|
"//staging/src/k8s.io/client-go/kubernetes/scheme:go_default_library",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
filegroup(
|
||||||
|
name = "package-srcs",
|
||||||
|
srcs = glob(["**"]),
|
||||||
|
tags = ["automanaged"],
|
||||||
|
visibility = ["//visibility:private"],
|
||||||
|
)
|
||||||
|
|
||||||
|
filegroup(
|
||||||
|
name = "all-srcs",
|
||||||
|
srcs = [":package-srcs"],
|
||||||
|
tags = ["automanaged"],
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
)
|
@@ -0,0 +1,99 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2019 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package objectmeta
|
||||||
|
|
||||||
|
import (
|
||||||
|
structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
|
||||||
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
|
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
// 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 {
|
||||||
|
if includeRoot {
|
||||||
|
if s == nil {
|
||||||
|
s = &structuralschema.Structural{}
|
||||||
|
}
|
||||||
|
clone := *s
|
||||||
|
clone.XEmbeddedResource = true
|
||||||
|
s = &clone
|
||||||
|
}
|
||||||
|
c := coercer{dropInvalidFields: dropInvalidFields}
|
||||||
|
return c.coerce(pth, obj, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
type coercer struct {
|
||||||
|
dropInvalidFields bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *coercer) coerce(pth *field.Path, x interface{}, s *structuralschema.Structural) *field.Error {
|
||||||
|
if s == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
switch x := x.(type) {
|
||||||
|
case map[string]interface{}:
|
||||||
|
for k, v := range x {
|
||||||
|
if s.XEmbeddedResource {
|
||||||
|
switch k {
|
||||||
|
case "apiVersion", "kind":
|
||||||
|
if _, ok := v.(string); !ok && c.dropInvalidFields {
|
||||||
|
delete(x, k)
|
||||||
|
} else if !ok {
|
||||||
|
return field.Invalid(pth.Child(k), v, "must be a string")
|
||||||
|
}
|
||||||
|
case "metadata":
|
||||||
|
meta, found, err := GetObjectMeta(x, c.dropInvalidFields)
|
||||||
|
if err != nil {
|
||||||
|
if !c.dropInvalidFields {
|
||||||
|
return field.Invalid(pth.Child("metadata"), v, err.Error())
|
||||||
|
}
|
||||||
|
// pass through on error if dropInvalidFields is true
|
||||||
|
} else if found {
|
||||||
|
if err := SetObjectMeta(x, meta); err != nil {
|
||||||
|
return field.Invalid(pth.Child("metadata"), v, err.Error())
|
||||||
|
}
|
||||||
|
if meta.CreationTimestamp.IsZero() {
|
||||||
|
unstructured.RemoveNestedField(x, "metadata", "creationTimestamp")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
prop, ok := s.Properties[k]
|
||||||
|
if ok {
|
||||||
|
if err := c.coerce(pth.Child(k), v, &prop); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else if s.AdditionalProperties != nil {
|
||||||
|
if err := c.coerce(pth.Key(k), v, s.AdditionalProperties.Structural); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case []interface{}:
|
||||||
|
for i, v := range x {
|
||||||
|
if err := c.coerce(pth.Index(i), v, s.Items); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// scalars, do nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
@@ -0,0 +1,515 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2019 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package objectmeta
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
|
||||||
|
"k8s.io/apimachinery/pkg/util/diff"
|
||||||
|
"k8s.io/apimachinery/pkg/util/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCoerce(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
json string
|
||||||
|
includeRoot bool
|
||||||
|
dropInvalidFields bool
|
||||||
|
schema *structuralschema.Structural
|
||||||
|
expected string
|
||||||
|
expectedError bool
|
||||||
|
}{
|
||||||
|
{name: "empty", json: "null", schema: nil, expected: "null"},
|
||||||
|
{name: "scalar", json: "4", schema: &structuralschema.Structural{}, expected: "4"},
|
||||||
|
{name: "scalar array", json: "[1,2]", schema: &structuralschema.Structural{
|
||||||
|
Items: &structuralschema.Structural{},
|
||||||
|
}, expected: "[1,2]"},
|
||||||
|
{name: "x-kubernetes-embedded-resource", json: `
|
||||||
|
{
|
||||||
|
"apiVersion": "foo/v1",
|
||||||
|
"kind": "Foo",
|
||||||
|
"metadata": {
|
||||||
|
"name": "instance",
|
||||||
|
"unspecified": "bar"
|
||||||
|
},
|
||||||
|
"unspecified":"bar",
|
||||||
|
"pruned": {
|
||||||
|
"apiVersion": "foo/v1",
|
||||||
|
"kind": "Foo",
|
||||||
|
"unspecified": "bar",
|
||||||
|
"metadata": {
|
||||||
|
"name": "instance",
|
||||||
|
"unspecified": "bar"
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
"unspecified": "bar"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"preserving": {
|
||||||
|
"apiVersion": "foo/v1",
|
||||||
|
"kind": "Foo",
|
||||||
|
"unspecified": "bar",
|
||||||
|
"metadata": {
|
||||||
|
"name": "instance",
|
||||||
|
"unspecified": "bar"
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
"unspecified": "bar"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nested": {
|
||||||
|
"apiVersion": "foo/v1",
|
||||||
|
"kind": "Foo",
|
||||||
|
"unspecified": "bar",
|
||||||
|
"metadata": {
|
||||||
|
"name": "instance",
|
||||||
|
"unspecified": "bar"
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
"unspecified": "bar",
|
||||||
|
"embedded": {
|
||||||
|
"apiVersion": "foo/v1",
|
||||||
|
"kind": "Foo",
|
||||||
|
"unspecified": "bar",
|
||||||
|
"metadata": {
|
||||||
|
"name": "instance",
|
||||||
|
"unspecified": "bar"
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
"unspecified": "bar"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`, schema: &structuralschema.Structural{
|
||||||
|
Generic: structuralschema.Generic{Type: "object"},
|
||||||
|
Properties: map[string]structuralschema.Structural{
|
||||||
|
"pruned": {
|
||||||
|
Generic: structuralschema.Generic{Type: "object"},
|
||||||
|
Extensions: structuralschema.Extensions{
|
||||||
|
XEmbeddedResource: true,
|
||||||
|
},
|
||||||
|
Properties: map[string]structuralschema.Structural{
|
||||||
|
"spec": {
|
||||||
|
Generic: structuralschema.Generic{Type: "object"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"preserving": {
|
||||||
|
Generic: structuralschema.Generic{Type: "object"},
|
||||||
|
Extensions: structuralschema.Extensions{
|
||||||
|
XEmbeddedResource: true,
|
||||||
|
XPreserveUnknownFields: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"nested": {
|
||||||
|
Generic: structuralschema.Generic{Type: "object"},
|
||||||
|
Extensions: structuralschema.Extensions{
|
||||||
|
XEmbeddedResource: true,
|
||||||
|
},
|
||||||
|
Properties: map[string]structuralschema.Structural{
|
||||||
|
"spec": {
|
||||||
|
Generic: structuralschema.Generic{Type: "object"},
|
||||||
|
Properties: map[string]structuralschema.Structural{
|
||||||
|
"embedded": {
|
||||||
|
Generic: structuralschema.Generic{Type: "object"},
|
||||||
|
Extensions: structuralschema.Extensions{
|
||||||
|
XEmbeddedResource: true,
|
||||||
|
},
|
||||||
|
Properties: map[string]structuralschema.Structural{
|
||||||
|
"spec": {
|
||||||
|
Generic: structuralschema.Generic{Type: "object"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, expected: `
|
||||||
|
{
|
||||||
|
"apiVersion": "foo/v1",
|
||||||
|
"kind": "Foo",
|
||||||
|
"metadata": {
|
||||||
|
"name": "instance",
|
||||||
|
"unspecified": "bar"
|
||||||
|
},
|
||||||
|
"unspecified":"bar",
|
||||||
|
"pruned": {
|
||||||
|
"apiVersion": "foo/v1",
|
||||||
|
"kind": "Foo",
|
||||||
|
"unspecified": "bar",
|
||||||
|
"metadata": {
|
||||||
|
"name": "instance"
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
"unspecified": "bar"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"preserving": {
|
||||||
|
"apiVersion": "foo/v1",
|
||||||
|
"kind": "Foo",
|
||||||
|
"unspecified": "bar",
|
||||||
|
"metadata": {
|
||||||
|
"name": "instance"
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
"unspecified": "bar"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nested": {
|
||||||
|
"apiVersion": "foo/v1",
|
||||||
|
"kind": "Foo",
|
||||||
|
"unspecified": "bar",
|
||||||
|
"metadata": {
|
||||||
|
"name": "instance"
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
"unspecified": "bar",
|
||||||
|
"embedded": {
|
||||||
|
"apiVersion": "foo/v1",
|
||||||
|
"kind": "Foo",
|
||||||
|
"unspecified": "bar",
|
||||||
|
"metadata": {
|
||||||
|
"name": "instance"
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
"unspecified": "bar"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`},
|
||||||
|
{name: "x-kubernetes-embedded-resource, with includeRoot=true", json: `
|
||||||
|
{
|
||||||
|
"apiVersion": "foo/v1",
|
||||||
|
"kind": "Foo",
|
||||||
|
"metadata": {
|
||||||
|
"name": "instance",
|
||||||
|
"unspecified": "bar"
|
||||||
|
},
|
||||||
|
"unspecified":"bar",
|
||||||
|
"pruned": {
|
||||||
|
"apiVersion": "foo/v1",
|
||||||
|
"kind": "Foo",
|
||||||
|
"unspecified": "bar",
|
||||||
|
"metadata": {
|
||||||
|
"name": "instance",
|
||||||
|
"unspecified": "bar"
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
"unspecified": "bar"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"preserving": {
|
||||||
|
"apiVersion": "foo/v1",
|
||||||
|
"kind": "Foo",
|
||||||
|
"unspecified": "bar",
|
||||||
|
"metadata": {
|
||||||
|
"name": "instance",
|
||||||
|
"unspecified": "bar"
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
"unspecified": "bar"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nested": {
|
||||||
|
"apiVersion": "foo/v1",
|
||||||
|
"kind": "Foo",
|
||||||
|
"unspecified": "bar",
|
||||||
|
"metadata": {
|
||||||
|
"name": "instance",
|
||||||
|
"unspecified": "bar"
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
"unspecified": "bar",
|
||||||
|
"embedded": {
|
||||||
|
"apiVersion": "foo/v1",
|
||||||
|
"kind": "Foo",
|
||||||
|
"unspecified": "bar",
|
||||||
|
"metadata": {
|
||||||
|
"name": "instance",
|
||||||
|
"unspecified": "bar"
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
"unspecified": "bar"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`, includeRoot: true, schema: &structuralschema.Structural{
|
||||||
|
Generic: structuralschema.Generic{Type: "object"},
|
||||||
|
Properties: map[string]structuralschema.Structural{
|
||||||
|
"pruned": {
|
||||||
|
Generic: structuralschema.Generic{Type: "object"},
|
||||||
|
Extensions: structuralschema.Extensions{
|
||||||
|
XEmbeddedResource: true,
|
||||||
|
},
|
||||||
|
Properties: map[string]structuralschema.Structural{
|
||||||
|
"spec": {
|
||||||
|
Generic: structuralschema.Generic{Type: "object"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"preserving": {
|
||||||
|
Generic: structuralschema.Generic{Type: "object"},
|
||||||
|
Extensions: structuralschema.Extensions{
|
||||||
|
XEmbeddedResource: true,
|
||||||
|
XPreserveUnknownFields: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"nested": {
|
||||||
|
Generic: structuralschema.Generic{Type: "object"},
|
||||||
|
Extensions: structuralschema.Extensions{
|
||||||
|
XEmbeddedResource: true,
|
||||||
|
},
|
||||||
|
Properties: map[string]structuralschema.Structural{
|
||||||
|
"spec": {
|
||||||
|
Generic: structuralschema.Generic{Type: "object"},
|
||||||
|
Properties: map[string]structuralschema.Structural{
|
||||||
|
"embedded": {
|
||||||
|
Generic: structuralschema.Generic{Type: "object"},
|
||||||
|
Extensions: structuralschema.Extensions{
|
||||||
|
XEmbeddedResource: true,
|
||||||
|
},
|
||||||
|
Properties: map[string]structuralschema.Structural{
|
||||||
|
"spec": {
|
||||||
|
Generic: structuralschema.Generic{Type: "object"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, expected: `
|
||||||
|
{
|
||||||
|
"apiVersion": "foo/v1",
|
||||||
|
"kind": "Foo",
|
||||||
|
"metadata": {
|
||||||
|
"name": "instance"
|
||||||
|
},
|
||||||
|
"unspecified":"bar",
|
||||||
|
"pruned": {
|
||||||
|
"apiVersion": "foo/v1",
|
||||||
|
"kind": "Foo",
|
||||||
|
"unspecified": "bar",
|
||||||
|
"metadata": {
|
||||||
|
"name": "instance"
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
"unspecified": "bar"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"preserving": {
|
||||||
|
"apiVersion": "foo/v1",
|
||||||
|
"kind": "Foo",
|
||||||
|
"unspecified": "bar",
|
||||||
|
"metadata": {
|
||||||
|
"name": "instance"
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
"unspecified": "bar"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nested": {
|
||||||
|
"apiVersion": "foo/v1",
|
||||||
|
"kind": "Foo",
|
||||||
|
"unspecified": "bar",
|
||||||
|
"metadata": {
|
||||||
|
"name": "instance"
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
"unspecified": "bar",
|
||||||
|
"embedded": {
|
||||||
|
"apiVersion": "foo/v1",
|
||||||
|
"kind": "Foo",
|
||||||
|
"unspecified": "bar",
|
||||||
|
"metadata": {
|
||||||
|
"name": "instance"
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
"unspecified": "bar"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`},
|
||||||
|
{name: "without name", json: `
|
||||||
|
{
|
||||||
|
"apiVersion": "foo/v1",
|
||||||
|
"kind": "Foo",
|
||||||
|
"metadata": {
|
||||||
|
"name": "instance"
|
||||||
|
},
|
||||||
|
"pruned": {
|
||||||
|
"apiVersion": "foo/v1",
|
||||||
|
"kind": "Foo",
|
||||||
|
"metadata": {
|
||||||
|
"namespace": "kube-system"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`, schema: &structuralschema.Structural{
|
||||||
|
Generic: structuralschema.Generic{Type: "object"},
|
||||||
|
Properties: map[string]structuralschema.Structural{
|
||||||
|
"pruned": {
|
||||||
|
Generic: structuralschema.Generic{Type: "object"},
|
||||||
|
Extensions: structuralschema.Extensions{
|
||||||
|
XEmbeddedResource: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, expected: `
|
||||||
|
{
|
||||||
|
"apiVersion": "foo/v1",
|
||||||
|
"kind": "Foo",
|
||||||
|
"metadata": {
|
||||||
|
"name": "instance"
|
||||||
|
},
|
||||||
|
"pruned": {
|
||||||
|
"apiVersion": "foo/v1",
|
||||||
|
"kind": "Foo",
|
||||||
|
"metadata": {
|
||||||
|
"namespace": "kube-system"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`},
|
||||||
|
{name: "x-kubernetes-embedded-resource, with dropInvalidFields=true", json: `
|
||||||
|
{
|
||||||
|
"apiVersion": "foo/v1",
|
||||||
|
"kind": "Foo",
|
||||||
|
"metadata": {
|
||||||
|
"name": "instance"
|
||||||
|
},
|
||||||
|
"pruned": {
|
||||||
|
"apiVersion": 42,
|
||||||
|
"kind": 42,
|
||||||
|
"metadata": {
|
||||||
|
"name": "instance",
|
||||||
|
"namespace": ["abc"],
|
||||||
|
"labels": {
|
||||||
|
"foo": 42
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`, dropInvalidFields: true, schema: &structuralschema.Structural{
|
||||||
|
Generic: structuralschema.Generic{Type: "object"},
|
||||||
|
Properties: map[string]structuralschema.Structural{
|
||||||
|
"pruned": {
|
||||||
|
Generic: structuralschema.Generic{Type: "object"},
|
||||||
|
Extensions: structuralschema.Extensions{
|
||||||
|
XEmbeddedResource: true,
|
||||||
|
},
|
||||||
|
Properties: map[string]structuralschema.Structural{
|
||||||
|
"spec": {
|
||||||
|
Generic: structuralschema.Generic{Type: "object"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, expected: `
|
||||||
|
{
|
||||||
|
"apiVersion": "foo/v1",
|
||||||
|
"kind": "Foo",
|
||||||
|
"metadata": {
|
||||||
|
"name": "instance"
|
||||||
|
},
|
||||||
|
"pruned": {
|
||||||
|
"metadata": {
|
||||||
|
"name": "instance"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`},
|
||||||
|
{name: "invalid metadata type, with dropInvalidFields=true", json: `
|
||||||
|
{
|
||||||
|
"apiVersion": "foo/v1",
|
||||||
|
"kind": "Foo",
|
||||||
|
"metadata": {
|
||||||
|
"name": "instance"
|
||||||
|
},
|
||||||
|
"pruned": {
|
||||||
|
"apiVersion": 42,
|
||||||
|
"kind": 42,
|
||||||
|
"metadata": [42]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`, dropInvalidFields: true, schema: &structuralschema.Structural{
|
||||||
|
Generic: structuralschema.Generic{Type: "object"},
|
||||||
|
Properties: map[string]structuralschema.Structural{
|
||||||
|
"pruned": {
|
||||||
|
Generic: structuralschema.Generic{Type: "object"},
|
||||||
|
Extensions: structuralschema.Extensions{
|
||||||
|
XEmbeddedResource: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, expected: `
|
||||||
|
{
|
||||||
|
"apiVersion": "foo/v1",
|
||||||
|
"kind": "Foo",
|
||||||
|
"metadata": {
|
||||||
|
"name": "instance"
|
||||||
|
},
|
||||||
|
"pruned": {
|
||||||
|
"metadata": [42]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
var in interface{}
|
||||||
|
if err := json.Unmarshal([]byte(tt.json), &in); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var expected interface{}
|
||||||
|
if err := json.Unmarshal([]byte(tt.expected), &expected); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := Coerce(nil, in, tt.schema, tt.includeRoot, tt.dropInvalidFields)
|
||||||
|
if tt.expectedError && err == nil {
|
||||||
|
t.Error("expected error, but did not get any")
|
||||||
|
} else if !tt.expectedError && err != nil {
|
||||||
|
t.Errorf("expected no error, but got: %v", err)
|
||||||
|
} else if !reflect.DeepEqual(in, expected) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
enc := json.NewEncoder(&buf)
|
||||||
|
enc.SetIndent("", " ")
|
||||||
|
err := enc.Encode(in)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected result mashalling error: %v", err)
|
||||||
|
}
|
||||||
|
t.Errorf("expected: %s\ngot: %s\ndiff: %s", tt.expected, buf.String(), diff.ObjectDiff(expected, in))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,92 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2019 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package objectmeta
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/serializer/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
var encodingjson = json.CaseSensitiveJsonIterator()
|
||||||
|
|
||||||
|
// GetObjectMeta does conversion of JSON to ObjectMeta. It first tries json.Unmarshal into a metav1.ObjectMeta
|
||||||
|
// type. If that does not work and dropMalformedFields is true, it does field-by-field best-effort conversion
|
||||||
|
// throwing away fields which lead to errors.
|
||||||
|
func GetObjectMeta(obj map[string]interface{}, dropMalformedFields bool) (*metav1.ObjectMeta, bool, error) {
|
||||||
|
metadata, found := obj["metadata"]
|
||||||
|
if !found {
|
||||||
|
return nil, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// round-trip through JSON first, hoping that unmarshalling just works
|
||||||
|
objectMeta := &metav1.ObjectMeta{}
|
||||||
|
metadataBytes, err := encodingjson.Marshal(metadata)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
if err = encodingjson.Unmarshal(metadataBytes, objectMeta); err == nil {
|
||||||
|
// if successful, return
|
||||||
|
return objectMeta, true, nil
|
||||||
|
}
|
||||||
|
if !dropMalformedFields {
|
||||||
|
// if we're not trying to drop malformed fields, return the error
|
||||||
|
return nil, true, err
|
||||||
|
}
|
||||||
|
|
||||||
|
metadataMap, ok := metadata.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
return nil, false, fmt.Errorf("invalid metadata: expected object, got %T", metadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Go field by field accumulating into the metadata object.
|
||||||
|
// This takes advantage of the fact that you can repeatedly unmarshal individual fields into a single struct,
|
||||||
|
// each iteration preserving the old key-values.
|
||||||
|
accumulatedObjectMeta := &metav1.ObjectMeta{}
|
||||||
|
testObjectMeta := &metav1.ObjectMeta{}
|
||||||
|
for k, v := range metadataMap {
|
||||||
|
// serialize a single field
|
||||||
|
if singleFieldBytes, err := encodingjson.Marshal(map[string]interface{}{k: v}); err == nil {
|
||||||
|
// do a test unmarshal
|
||||||
|
if encodingjson.Unmarshal(singleFieldBytes, testObjectMeta) == nil {
|
||||||
|
// if that succeeds, unmarshal for real
|
||||||
|
encodingjson.Unmarshal(singleFieldBytes, accumulatedObjectMeta)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return accumulatedObjectMeta, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetObjectMeta writes back ObjectMeta into a JSON data structure.
|
||||||
|
func SetObjectMeta(obj map[string]interface{}, objectMeta *metav1.ObjectMeta) error {
|
||||||
|
if objectMeta == nil {
|
||||||
|
unstructured.RemoveNestedField(obj, "metadata")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata, err := runtime.DefaultUnstructuredConverter.ToUnstructured(objectMeta)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
obj["metadata"] = metadata
|
||||||
|
return nil
|
||||||
|
}
|
@@ -0,0 +1,234 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2019 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package objectmeta
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/api/apitesting/fuzzer"
|
||||||
|
"k8s.io/apimachinery/pkg/api/equality"
|
||||||
|
metafuzzer "k8s.io/apimachinery/pkg/apis/meta/fuzzer"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/serializer/json"
|
||||||
|
"k8s.io/apimachinery/pkg/util/diff"
|
||||||
|
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRoundtripObjectMeta(t *testing.T) {
|
||||||
|
scheme := runtime.NewScheme()
|
||||||
|
codecs := serializer.NewCodecFactory(scheme)
|
||||||
|
codec := json.NewSerializer(json.DefaultMetaFactory, scheme, scheme, false)
|
||||||
|
seed := rand.Int63()
|
||||||
|
fuzzer := fuzzer.FuzzerFor(metafuzzer.Funcs, rand.NewSource(seed), codecs)
|
||||||
|
|
||||||
|
N := 1000
|
||||||
|
for i := 0; i < N; i++ {
|
||||||
|
u := &unstructured.Unstructured{Object: map[string]interface{}{}}
|
||||||
|
original := &metav1.ObjectMeta{}
|
||||||
|
fuzzer.Fuzz(original)
|
||||||
|
if err := SetObjectMeta(u.Object, original); err != nil {
|
||||||
|
t.Fatalf("unexpected error setting ObjectMeta: %v", err)
|
||||||
|
}
|
||||||
|
o, _, err := GetObjectMeta(u.Object, false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error getting the Objectmeta: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !equality.Semantic.DeepEqual(original, o) {
|
||||||
|
t.Errorf("diff: %v\nCodec: %#v", diff.ObjectReflectDiff(original, o), codec)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestMalformedObjectMetaFields sets a number of different random values and types for all
|
||||||
|
// metadata fields. If json.Unmarshal accepts them, compare that getObjectMeta
|
||||||
|
// gives the same result. Otherwise, drop malformed fields.
|
||||||
|
func TestMalformedObjectMetaFields(t *testing.T) {
|
||||||
|
fuzzer := fuzzer.FuzzerFor(metafuzzer.Funcs, rand.NewSource(rand.Int63()), serializer.NewCodecFactory(runtime.NewScheme()))
|
||||||
|
spuriousValues := func() []interface{} {
|
||||||
|
return []interface{}{
|
||||||
|
// primitives
|
||||||
|
nil,
|
||||||
|
int64(1),
|
||||||
|
float64(1.5),
|
||||||
|
true,
|
||||||
|
"a",
|
||||||
|
// well-formed complex values
|
||||||
|
[]interface{}{"a", "b"},
|
||||||
|
map[string]interface{}{"a": "1", "b": "2"},
|
||||||
|
[]interface{}{int64(1), int64(2)},
|
||||||
|
[]interface{}{float64(1.5), float64(2.5)},
|
||||||
|
// known things json decoding tolerates
|
||||||
|
map[string]interface{}{"a": "1", "b": nil},
|
||||||
|
// malformed things
|
||||||
|
map[string]interface{}{"a": "1", "b": []interface{}{"nested"}},
|
||||||
|
[]interface{}{"a", int64(1), float64(1.5), true, []interface{}{"nested"}},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
N := 100
|
||||||
|
for i := 0; i < N; i++ {
|
||||||
|
fuzzedObjectMeta := &metav1.ObjectMeta{}
|
||||||
|
fuzzer.Fuzz(fuzzedObjectMeta)
|
||||||
|
goodMetaMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(fuzzedObjectMeta.DeepCopy())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, pth := range jsonPaths(nil, goodMetaMap) {
|
||||||
|
for _, v := range spuriousValues() {
|
||||||
|
// skip values of same type, because they can only cause decoding errors further insides
|
||||||
|
orig, err := JSONPathValue(goodMetaMap, pth, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected to not find something at %v: %v", pth, err)
|
||||||
|
}
|
||||||
|
if reflect.TypeOf(v) == reflect.TypeOf(orig) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// make a spurious map
|
||||||
|
spuriousMetaMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(fuzzedObjectMeta.DeepCopy())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := SetJSONPath(spuriousMetaMap, pth, 0, v); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// See if it can unmarshal to object meta
|
||||||
|
spuriousJSON, err := encodingjson.Marshal(spuriousMetaMap)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error on %v=%#v: %v", pth, v, err)
|
||||||
|
}
|
||||||
|
expectedObjectMeta := &metav1.ObjectMeta{}
|
||||||
|
if err := encodingjson.Unmarshal(spuriousJSON, expectedObjectMeta); err != nil {
|
||||||
|
// if standard json unmarshal would fail decoding this field, drop the field entirely
|
||||||
|
truncatedMetaMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(fuzzedObjectMeta.DeepCopy())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// we expect this logic for the different fields:
|
||||||
|
switch {
|
||||||
|
default:
|
||||||
|
// delete complete top-level field by default
|
||||||
|
DeleteJSONPath(truncatedMetaMap, pth[:1], 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
truncatedJSON, err := encodingjson.Marshal(truncatedMetaMap)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error on %v=%#v: %v", pth, v, err)
|
||||||
|
}
|
||||||
|
expectedObjectMeta = &metav1.ObjectMeta{}
|
||||||
|
if err := encodingjson.Unmarshal(truncatedJSON, expectedObjectMeta); err != nil {
|
||||||
|
t.Fatalf("error on %v=%#v: %v", pth, v, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure dropInvalidTypedFields+getObjectMeta matches what we expect
|
||||||
|
u := &unstructured.Unstructured{Object: map[string]interface{}{"metadata": spuriousMetaMap}}
|
||||||
|
actualObjectMeta, _, err := GetObjectMeta(u.Object, true)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("got unexpected error after dropping invalid typed fields on %v=%#v: %v", pth, v, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !equality.Semantic.DeepEqual(expectedObjectMeta, actualObjectMeta) {
|
||||||
|
t.Errorf("%v=%#v, diff: %v\n", pth, v, diff.ObjectReflectDiff(expectedObjectMeta, actualObjectMeta))
|
||||||
|
t.Errorf("expectedObjectMeta %#v", expectedObjectMeta)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetObjectMetaNils(t *testing.T) {
|
||||||
|
u := &unstructured.Unstructured{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"kind": "Pod",
|
||||||
|
"apiVersion": "v1",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"generateName": nil,
|
||||||
|
"labels": map[string]interface{}{
|
||||||
|
"foo": nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
o, _, err := GetObjectMeta(u.Object, true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if o.GenerateName != "" {
|
||||||
|
t.Errorf("expected null json value to be read as \"\" string, but got: %q", o.GenerateName)
|
||||||
|
}
|
||||||
|
if got, expected := o.Labels, map[string]string{"foo": ""}; !reflect.DeepEqual(got, expected) {
|
||||||
|
t.Errorf("unexpected labels, expected=%#v, got=%#v", expected, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
// double check this what the kube JSON decode is doing
|
||||||
|
bs, _ := encodingjson.Marshal(u.UnstructuredContent())
|
||||||
|
kubeObj, _, err := clientgoscheme.Codecs.UniversalDecoder(corev1.SchemeGroupVersion).Decode(bs, nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
pod, ok := kubeObj.(*corev1.Pod)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected v1 Pod, got: %T", pod)
|
||||||
|
}
|
||||||
|
if got, expected := o.GenerateName, pod.ObjectMeta.GenerateName; got != expected {
|
||||||
|
t.Errorf("expected generatedName to be %q, got %q", expected, got)
|
||||||
|
}
|
||||||
|
if got, expected := o.Labels, pod.ObjectMeta.Labels; !reflect.DeepEqual(got, expected) {
|
||||||
|
t.Errorf("expected labels to be %v, got %v", expected, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetObjectMeta(t *testing.T) {
|
||||||
|
for i := 0; i < 100; i++ {
|
||||||
|
u := &unstructured.Unstructured{Object: map[string]interface{}{
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "good",
|
||||||
|
"Name": "bad1",
|
||||||
|
"nAme": "bad2",
|
||||||
|
"naMe": "bad3",
|
||||||
|
"namE": "bad4",
|
||||||
|
|
||||||
|
"namespace": "good",
|
||||||
|
"Namespace": "bad1",
|
||||||
|
"nAmespace": "bad2",
|
||||||
|
"naMespace": "bad3",
|
||||||
|
"namEspace": "bad4",
|
||||||
|
|
||||||
|
"creationTimestamp": "a",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
|
||||||
|
meta, _, err := GetObjectMeta(u.Object, true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if meta.Name != "good" || meta.Namespace != "good" {
|
||||||
|
t.Fatalf("got %#v", meta)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package apiserver
|
package objectmeta
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@@ -28,10 +28,10 @@ type (
|
|||||||
index *int
|
index *int
|
||||||
field string
|
field string
|
||||||
}
|
}
|
||||||
JsonPath []jsonPathNode
|
JSONPath []jsonPathNode
|
||||||
)
|
)
|
||||||
|
|
||||||
func (p JsonPath) String() string {
|
func (p JSONPath) String() string {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
for _, n := range p {
|
for _, n := range p {
|
||||||
if n.index == nil {
|
if n.index == nil {
|
||||||
@@ -43,8 +43,8 @@ func (p JsonPath) String() string {
|
|||||||
return buf.String()
|
return buf.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func jsonPaths(base JsonPath, j map[string]interface{}) []JsonPath {
|
func jsonPaths(base JSONPath, j map[string]interface{}) []JSONPath {
|
||||||
res := make([]JsonPath, 0, len(j))
|
res := make([]JSONPath, 0, len(j))
|
||||||
for k, old := range j {
|
for k, old := range j {
|
||||||
kPth := append(append([]jsonPathNode(nil), base...), jsonPathNode{field: k})
|
kPth := append(append([]jsonPathNode(nil), base...), jsonPathNode{field: k})
|
||||||
res = append(res, kPth)
|
res = append(res, kPth)
|
||||||
@@ -59,8 +59,8 @@ func jsonPaths(base JsonPath, j map[string]interface{}) []JsonPath {
|
|||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
func jsonIterSlice(base JsonPath, j []interface{}) []JsonPath {
|
func jsonIterSlice(base JSONPath, j []interface{}) []JSONPath {
|
||||||
res := make([]JsonPath, 0, len(j))
|
res := make([]JSONPath, 0, len(j))
|
||||||
for i, old := range j {
|
for i, old := range j {
|
||||||
index := i
|
index := i
|
||||||
iPth := append(append([]jsonPathNode(nil), base...), jsonPathNode{index: &index})
|
iPth := append(append([]jsonPathNode(nil), base...), jsonPathNode{index: &index})
|
||||||
@@ -76,7 +76,7 @@ func jsonIterSlice(base JsonPath, j []interface{}) []JsonPath {
|
|||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
func JsonPathValue(j map[string]interface{}, pth JsonPath, base int) (interface{}, error) {
|
func JSONPathValue(j map[string]interface{}, pth JSONPath, base int) (interface{}, error) {
|
||||||
if len(pth) == base {
|
if len(pth) == base {
|
||||||
return nil, fmt.Errorf("empty json path is invalid for object")
|
return nil, fmt.Errorf("empty json path is invalid for object")
|
||||||
}
|
}
|
||||||
@@ -92,7 +92,7 @@ func JsonPathValue(j map[string]interface{}, pth JsonPath, base int) (interface{
|
|||||||
}
|
}
|
||||||
switch field := field.(type) {
|
switch field := field.(type) {
|
||||||
case map[string]interface{}:
|
case map[string]interface{}:
|
||||||
return JsonPathValue(field, pth, base+1)
|
return JSONPathValue(field, pth, base+1)
|
||||||
case []interface{}:
|
case []interface{}:
|
||||||
return jsonPathValueSlice(field, pth, base+1)
|
return jsonPathValueSlice(field, pth, base+1)
|
||||||
default:
|
default:
|
||||||
@@ -100,7 +100,7 @@ func JsonPathValue(j map[string]interface{}, pth JsonPath, base int) (interface{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func jsonPathValueSlice(j []interface{}, pth JsonPath, base int) (interface{}, error) {
|
func jsonPathValueSlice(j []interface{}, pth JSONPath, base int) (interface{}, error) {
|
||||||
if len(pth) == base {
|
if len(pth) == base {
|
||||||
return nil, fmt.Errorf("empty json path %q is invalid for object", pth)
|
return nil, fmt.Errorf("empty json path %q is invalid for object", pth)
|
||||||
}
|
}
|
||||||
@@ -115,7 +115,7 @@ func jsonPathValueSlice(j []interface{}, pth JsonPath, base int) (interface{}, e
|
|||||||
}
|
}
|
||||||
switch item := j[*pth[base].index].(type) {
|
switch item := j[*pth[base].index].(type) {
|
||||||
case map[string]interface{}:
|
case map[string]interface{}:
|
||||||
return JsonPathValue(item, pth, base+1)
|
return JSONPathValue(item, pth, base+1)
|
||||||
case []interface{}:
|
case []interface{}:
|
||||||
return jsonPathValueSlice(item, pth, base+1)
|
return jsonPathValueSlice(item, pth, base+1)
|
||||||
default:
|
default:
|
||||||
@@ -123,7 +123,7 @@ func jsonPathValueSlice(j []interface{}, pth JsonPath, base int) (interface{}, e
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetJsonPath(j map[string]interface{}, pth JsonPath, base int, value interface{}) error {
|
func SetJSONPath(j map[string]interface{}, pth JSONPath, base int, value interface{}) error {
|
||||||
if len(pth) == base {
|
if len(pth) == base {
|
||||||
return fmt.Errorf("empty json path is invalid for object")
|
return fmt.Errorf("empty json path is invalid for object")
|
||||||
}
|
}
|
||||||
@@ -140,15 +140,15 @@ func SetJsonPath(j map[string]interface{}, pth JsonPath, base int, value interfa
|
|||||||
}
|
}
|
||||||
switch field := field.(type) {
|
switch field := field.(type) {
|
||||||
case map[string]interface{}:
|
case map[string]interface{}:
|
||||||
return SetJsonPath(field, pth, base+1, value)
|
return SetJSONPath(field, pth, base+1, value)
|
||||||
case []interface{}:
|
case []interface{}:
|
||||||
return setJsonPathSlice(field, pth, base+1, value)
|
return setJSONPathSlice(field, pth, base+1, value)
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("invalid non-terminal json path %q for field", pth[:base+1])
|
return fmt.Errorf("invalid non-terminal json path %q for field", pth[:base+1])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func setJsonPathSlice(j []interface{}, pth JsonPath, base int, value interface{}) error {
|
func setJSONPathSlice(j []interface{}, pth JSONPath, base int, value interface{}) error {
|
||||||
if len(pth) == base {
|
if len(pth) == base {
|
||||||
return fmt.Errorf("empty json path %q is invalid for object", pth)
|
return fmt.Errorf("empty json path %q is invalid for object", pth)
|
||||||
}
|
}
|
||||||
@@ -164,15 +164,15 @@ func setJsonPathSlice(j []interface{}, pth JsonPath, base int, value interface{}
|
|||||||
}
|
}
|
||||||
switch item := j[*pth[base].index].(type) {
|
switch item := j[*pth[base].index].(type) {
|
||||||
case map[string]interface{}:
|
case map[string]interface{}:
|
||||||
return SetJsonPath(item, pth, base+1, value)
|
return SetJSONPath(item, pth, base+1, value)
|
||||||
case []interface{}:
|
case []interface{}:
|
||||||
return setJsonPathSlice(item, pth, base+1, value)
|
return setJSONPathSlice(item, pth, base+1, value)
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("invalid non-terminal json path %q for index", pth[:base+1])
|
return fmt.Errorf("invalid non-terminal json path %q for index", pth[:base+1])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func DeleteJsonPath(j map[string]interface{}, pth JsonPath, base int) error {
|
func DeleteJSONPath(j map[string]interface{}, pth JSONPath, base int) error {
|
||||||
if len(pth) == base {
|
if len(pth) == base {
|
||||||
return fmt.Errorf("empty json path is invalid for object")
|
return fmt.Errorf("empty json path is invalid for object")
|
||||||
}
|
}
|
||||||
@@ -189,7 +189,7 @@ func DeleteJsonPath(j map[string]interface{}, pth JsonPath, base int) error {
|
|||||||
}
|
}
|
||||||
switch field := field.(type) {
|
switch field := field.(type) {
|
||||||
case map[string]interface{}:
|
case map[string]interface{}:
|
||||||
return DeleteJsonPath(field, pth, base+1)
|
return DeleteJSONPath(field, pth, base+1)
|
||||||
case []interface{}:
|
case []interface{}:
|
||||||
if len(pth) == base+2 {
|
if len(pth) == base+2 {
|
||||||
if pth[base+1].index == nil {
|
if pth[base+1].index == nil {
|
||||||
@@ -198,13 +198,13 @@ func DeleteJsonPath(j map[string]interface{}, pth JsonPath, base int) error {
|
|||||||
j[pth[base].field] = append(field[:*pth[base+1].index], field[*pth[base+1].index+1:]...)
|
j[pth[base].field] = append(field[:*pth[base+1].index], field[*pth[base+1].index+1:]...)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return deleteJsonPathSlice(field, pth, base+1)
|
return deleteJSONPathSlice(field, pth, base+1)
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("invalid non-terminal json path %q for field", pth[:base+1])
|
return fmt.Errorf("invalid non-terminal json path %q for field", pth[:base+1])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteJsonPathSlice(j []interface{}, pth JsonPath, base int) error {
|
func deleteJSONPathSlice(j []interface{}, pth JSONPath, base int) error {
|
||||||
if len(pth) == base {
|
if len(pth) == base {
|
||||||
return fmt.Errorf("empty json path %q is invalid for object", pth)
|
return fmt.Errorf("empty json path %q is invalid for object", pth)
|
||||||
}
|
}
|
||||||
@@ -219,7 +219,7 @@ func deleteJsonPathSlice(j []interface{}, pth JsonPath, base int) error {
|
|||||||
}
|
}
|
||||||
switch item := j[*pth[base].index].(type) {
|
switch item := j[*pth[base].index].(type) {
|
||||||
case map[string]interface{}:
|
case map[string]interface{}:
|
||||||
return DeleteJsonPath(item, pth, base+1)
|
return DeleteJSONPath(item, pth, base+1)
|
||||||
case []interface{}:
|
case []interface{}:
|
||||||
if len(pth) == base+2 {
|
if len(pth) == base+2 {
|
||||||
if pth[base+1].index == nil {
|
if pth[base+1].index == nil {
|
||||||
@@ -228,7 +228,7 @@ func deleteJsonPathSlice(j []interface{}, pth JsonPath, base int) error {
|
|||||||
j[*pth[base].index] = append(item[:*pth[base+1].index], item[*pth[base+1].index+1:])
|
j[*pth[base].index] = append(item[:*pth[base+1].index], item[*pth[base+1].index+1:])
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return deleteJsonPathSlice(item, pth, base+1)
|
return deleteJSONPathSlice(item, pth, base+1)
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("invalid non-terminal json path %q for index", pth[:base+1])
|
return fmt.Errorf("invalid non-terminal json path %q for index", pth[:base+1])
|
||||||
}
|
}
|
@@ -0,0 +1,119 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2019 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package objectmeta
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
|
||||||
|
metavalidation "k8s.io/apimachinery/pkg/api/validation"
|
||||||
|
"k8s.io/apimachinery/pkg/api/validation/path"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
utilvalidation "k8s.io/apimachinery/pkg/util/validation"
|
||||||
|
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Validate validates embedded ObjectMeta and TypeMeta.
|
||||||
|
// It also validate those at the root if includeRoot is true.
|
||||||
|
func Validate(pth *field.Path, obj interface{}, s *structuralschema.Structural, includeRoot bool) field.ErrorList {
|
||||||
|
if includeRoot {
|
||||||
|
if s == nil {
|
||||||
|
s = &structuralschema.Structural{}
|
||||||
|
}
|
||||||
|
clone := *s
|
||||||
|
clone.XEmbeddedResource = true
|
||||||
|
s = &clone
|
||||||
|
}
|
||||||
|
return validate(pth, obj, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func validate(pth *field.Path, x interface{}, s *structuralschema.Structural) field.ErrorList {
|
||||||
|
if s == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var allErrs field.ErrorList
|
||||||
|
|
||||||
|
switch x := x.(type) {
|
||||||
|
case map[string]interface{}:
|
||||||
|
if s.XEmbeddedResource {
|
||||||
|
allErrs = append(allErrs, validateEmbeddedResource(pth, x, s)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range x {
|
||||||
|
prop, ok := s.Properties[k]
|
||||||
|
if ok {
|
||||||
|
allErrs = append(allErrs, validate(pth.Child(k), v, &prop)...)
|
||||||
|
} else if s.AdditionalProperties != nil {
|
||||||
|
allErrs = append(allErrs, validate(pth.Key(k), v, s.AdditionalProperties.Structural)...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case []interface{}:
|
||||||
|
for i, v := range x {
|
||||||
|
allErrs = append(allErrs, validate(pth.Index(i), v, s.Items)...)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// scalars, do nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
return allErrs
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateEmbeddedResource(pth *field.Path, x map[string]interface{}, s *structuralschema.Structural) field.ErrorList {
|
||||||
|
var allErrs field.ErrorList
|
||||||
|
|
||||||
|
// require apiVersion and kind, but not metadata
|
||||||
|
if _, found := x["apiVersion"]; !found {
|
||||||
|
allErrs = append(allErrs, field.Required(pth.Child("apiVersion"), "must not be empty"))
|
||||||
|
}
|
||||||
|
if _, found := x["kind"]; !found {
|
||||||
|
allErrs = append(allErrs, field.Required(pth.Child("kind"), "must not be empty"))
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range x {
|
||||||
|
switch k {
|
||||||
|
case "apiVersion":
|
||||||
|
if apiVersion, ok := v.(string); !ok {
|
||||||
|
allErrs = append(allErrs, field.Invalid(pth.Child("apiVersion"), v, "must be a string"))
|
||||||
|
} else if len(apiVersion) == 0 {
|
||||||
|
allErrs = append(allErrs, field.Invalid(pth.Child("apiVersion"), apiVersion, "must not be empty"))
|
||||||
|
} else if _, err := schema.ParseGroupVersion(apiVersion); err != nil {
|
||||||
|
allErrs = append(allErrs, field.Invalid(pth.Child("apiVersion"), apiVersion, err.Error()))
|
||||||
|
}
|
||||||
|
case "kind":
|
||||||
|
if kind, ok := v.(string); !ok {
|
||||||
|
allErrs = append(allErrs, field.Invalid(pth.Child("kind"), v, "must be a string"))
|
||||||
|
} else if len(kind) == 0 {
|
||||||
|
allErrs = append(allErrs, field.Invalid(pth.Child("kind"), kind, "must not be empty"))
|
||||||
|
} else if errs := utilvalidation.IsDNS1035Label(strings.ToLower(kind)); len(errs) > 0 {
|
||||||
|
allErrs = append(allErrs, field.Invalid(pth.Child("kind"), kind, "may have mixed case, but should otherwise match: "+strings.Join(errs, ",")))
|
||||||
|
}
|
||||||
|
case "metadata":
|
||||||
|
meta, _, err := GetObjectMeta(x, false)
|
||||||
|
if err != nil {
|
||||||
|
allErrs = append(allErrs, field.Invalid(pth.Child("metadata"), v, err.Error()))
|
||||||
|
} else {
|
||||||
|
if len(meta.Name) == 0 {
|
||||||
|
meta.Name = "fakename" // we have to set something to avoid an error
|
||||||
|
}
|
||||||
|
allErrs = append(allErrs, metavalidation.ValidateObjectMeta(meta, len(meta.Namespace) > 0, path.ValidatePathSegmentName, pth.Child("metadata"))...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return allErrs
|
||||||
|
}
|
@@ -0,0 +1,215 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2019 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package objectmeta
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
|
||||||
|
"k8s.io/apimachinery/pkg/util/json"
|
||||||
|
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestValidateEmbeddedResource(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
object map[string]interface{}
|
||||||
|
errors []validationMatch
|
||||||
|
}{
|
||||||
|
{name: "empty", object: map[string]interface{}{}, errors: []validationMatch{
|
||||||
|
required("apiVersion"),
|
||||||
|
required("kind"),
|
||||||
|
}},
|
||||||
|
{name: "version and kind", object: map[string]interface{}{
|
||||||
|
"apiVersion": "foo/v1",
|
||||||
|
"kind": "Foo",
|
||||||
|
}},
|
||||||
|
{name: "invalid kind", object: map[string]interface{}{
|
||||||
|
"apiVersion": "foo/v1",
|
||||||
|
"kind": "foo.bar-com",
|
||||||
|
}, errors: []validationMatch{
|
||||||
|
invalid("kind"),
|
||||||
|
}},
|
||||||
|
{name: "no name", object: map[string]interface{}{
|
||||||
|
"apiVersion": "foo/v1",
|
||||||
|
"kind": "Foo",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"namespace": "kube-system",
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
{name: "no namespace", object: map[string]interface{}{
|
||||||
|
"apiVersion": "foo/v1",
|
||||||
|
"kind": "Foo",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "foo",
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
{name: "invalid", object: map[string]interface{}{
|
||||||
|
"apiVersion": "foo/v1",
|
||||||
|
"kind": "Foo",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "..",
|
||||||
|
"namespace": "$$$",
|
||||||
|
"labels": map[string]interface{}{
|
||||||
|
"#": "#",
|
||||||
|
},
|
||||||
|
"annotations": map[string]interface{}{
|
||||||
|
"#": "#",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, errors: []validationMatch{
|
||||||
|
invalid("metadata", "name"),
|
||||||
|
invalid("metadata", "namespace"),
|
||||||
|
invalid("metadata", "labels"), // key
|
||||||
|
invalid("metadata", "labels"), // value
|
||||||
|
invalid("metadata", "annotations"), // key
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
schema := &structuralschema.Structural{Extensions: structuralschema.Extensions{XEmbeddedResource: true}}
|
||||||
|
errs := validateEmbeddedResource(nil, tt.object, schema)
|
||||||
|
seenErrs := make([]bool, len(errs))
|
||||||
|
|
||||||
|
for _, expectedError := range tt.errors {
|
||||||
|
found := false
|
||||||
|
for i, err := range errs {
|
||||||
|
if expectedError.matches(err) && !seenErrs[i] {
|
||||||
|
found = true
|
||||||
|
seenErrs[i] = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
t.Errorf("expected %v at %v, got %v", expectedError.errorType, expectedError.path.String(), errs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, seen := range seenErrs {
|
||||||
|
if !seen {
|
||||||
|
t.Errorf("unexpected error: %v", errs[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidate(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
object string
|
||||||
|
includeRoot bool
|
||||||
|
errors []validationMatch
|
||||||
|
}{
|
||||||
|
{name: "empty", object: `{}`, errors: []validationMatch{}},
|
||||||
|
{name: "include root", object: `{}`, includeRoot: true, errors: []validationMatch{
|
||||||
|
required("apiVersion"),
|
||||||
|
required("kind"),
|
||||||
|
}},
|
||||||
|
{name: "embedded", object: `
|
||||||
|
{
|
||||||
|
"embedded": {}
|
||||||
|
}`, errors: []validationMatch{
|
||||||
|
required("embedded", "apiVersion"),
|
||||||
|
required("embedded", "kind"),
|
||||||
|
}},
|
||||||
|
{name: "nested", object: `
|
||||||
|
{
|
||||||
|
"nested": {
|
||||||
|
"embedded": {}
|
||||||
|
}
|
||||||
|
}`, errors: []validationMatch{
|
||||||
|
required("nested", "apiVersion"),
|
||||||
|
required("nested", "kind"),
|
||||||
|
required("nested", "embedded", "apiVersion"),
|
||||||
|
required("nested", "embedded", "kind"),
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
schema := &structuralschema.Structural{
|
||||||
|
Properties: map[string]structuralschema.Structural{
|
||||||
|
"embedded": {Extensions: structuralschema.Extensions{XEmbeddedResource: true}},
|
||||||
|
"nested": {
|
||||||
|
Extensions: structuralschema.Extensions{XEmbeddedResource: true},
|
||||||
|
Properties: map[string]structuralschema.Structural{
|
||||||
|
"embedded": {Extensions: structuralschema.Extensions{XEmbeddedResource: true}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var obj map[string]interface{}
|
||||||
|
if err := json.Unmarshal([]byte(tt.object), &obj); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
errs := Validate(nil, obj, schema, tt.includeRoot)
|
||||||
|
seenErrs := make([]bool, len(errs))
|
||||||
|
|
||||||
|
for _, expectedError := range tt.errors {
|
||||||
|
found := false
|
||||||
|
for i, err := range errs {
|
||||||
|
if expectedError.matches(err) && !seenErrs[i] {
|
||||||
|
found = true
|
||||||
|
seenErrs[i] = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
t.Errorf("expected %v at %v, got %v", expectedError.errorType, expectedError.path.String(), errs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, seen := range seenErrs {
|
||||||
|
if !seen {
|
||||||
|
t.Errorf("unexpected error: %v", errs[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type validationMatch struct {
|
||||||
|
path *field.Path
|
||||||
|
errorType field.ErrorType
|
||||||
|
}
|
||||||
|
|
||||||
|
func required(path ...string) validationMatch {
|
||||||
|
return validationMatch{path: field.NewPath(path[0], path[1:]...), errorType: field.ErrorTypeRequired}
|
||||||
|
}
|
||||||
|
func invalid(path ...string) validationMatch {
|
||||||
|
return validationMatch{path: field.NewPath(path[0], path[1:]...), errorType: field.ErrorTypeInvalid}
|
||||||
|
}
|
||||||
|
func invalidIndex(index int, path ...string) validationMatch {
|
||||||
|
return validationMatch{path: field.NewPath(path[0], path[1:]...).Index(index), errorType: field.ErrorTypeInvalid}
|
||||||
|
}
|
||||||
|
func unsupported(path ...string) validationMatch {
|
||||||
|
return validationMatch{path: field.NewPath(path[0], path[1:]...), errorType: field.ErrorTypeNotSupported}
|
||||||
|
}
|
||||||
|
func immutable(path ...string) validationMatch {
|
||||||
|
return validationMatch{path: field.NewPath(path[0], path[1:]...), errorType: field.ErrorTypeInvalid}
|
||||||
|
}
|
||||||
|
func forbidden(path ...string) validationMatch {
|
||||||
|
return validationMatch{path: field.NewPath(path[0], path[1:]...), errorType: field.ErrorTypeForbidden}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v validationMatch) matches(err *field.Error) bool {
|
||||||
|
return err.Type == v.errorType && err.Field == v.path.String()
|
||||||
|
}
|
@@ -16,6 +16,7 @@ go_test(
|
|||||||
deps = [
|
deps = [
|
||||||
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema:go_default_library",
|
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||||
|
"//staging/src/k8s.io/apimachinery/pkg/util/diff:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/util/json:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/util/json:go_default_library",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@@ -20,11 +20,26 @@ import (
|
|||||||
structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
|
structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Prune removes object fields in obj which are not specified in s.
|
// Prune removes object fields in obj which are not specified in s. It skips TypeMeta and ObjectMeta fields
|
||||||
func Prune(obj interface{}, s *structuralschema.Structural) {
|
// if XEmbeddedResource is set to true, or for the root if root=true.
|
||||||
|
func Prune(obj interface{}, s *structuralschema.Structural, root bool) {
|
||||||
|
if root {
|
||||||
|
if s == nil {
|
||||||
|
s = &structuralschema.Structural{}
|
||||||
|
}
|
||||||
|
clone := *s
|
||||||
|
clone.XEmbeddedResource = true
|
||||||
|
s = &clone
|
||||||
|
}
|
||||||
prune(obj, s)
|
prune(obj, s)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var metaFields = map[string]bool{
|
||||||
|
"apiVersion": true,
|
||||||
|
"kind": true,
|
||||||
|
"metadata": true,
|
||||||
|
}
|
||||||
|
|
||||||
func prune(x interface{}, s *structuralschema.Structural) {
|
func prune(x interface{}, s *structuralschema.Structural) {
|
||||||
if s != nil && s.XPreserveUnknownFields {
|
if s != nil && s.XPreserveUnknownFields {
|
||||||
skipPrune(x, s)
|
skipPrune(x, s)
|
||||||
@@ -40,6 +55,9 @@ func prune(x interface{}, s *structuralschema.Structural) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
for k, v := range x {
|
for k, v := range x {
|
||||||
|
if s.XEmbeddedResource && metaFields[k] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
prop, ok := s.Properties[k]
|
prop, ok := s.Properties[k]
|
||||||
if ok {
|
if ok {
|
||||||
prune(v, &prop)
|
prune(v, &prop)
|
||||||
@@ -72,10 +90,13 @@ func skipPrune(x interface{}, s *structuralschema.Structural) {
|
|||||||
switch x := x.(type) {
|
switch x := x.(type) {
|
||||||
case map[string]interface{}:
|
case map[string]interface{}:
|
||||||
for k, v := range x {
|
for k, v := range x {
|
||||||
|
if s.XEmbeddedResource && metaFields[k] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if prop, ok := s.Properties[k]; ok {
|
if prop, ok := s.Properties[k]; ok {
|
||||||
prune(v, &prop)
|
prune(v, &prop)
|
||||||
} else {
|
} else if s.AdditionalProperties != nil {
|
||||||
skipPrune(v, nil)
|
prune(v, s.AdditionalProperties.Structural)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case []interface{}:
|
case []interface{}:
|
||||||
|
@@ -23,31 +23,33 @@ import (
|
|||||||
|
|
||||||
structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
|
structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/util/diff"
|
||||||
"k8s.io/apimachinery/pkg/util/json"
|
"k8s.io/apimachinery/pkg/util/json"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPrune(t *testing.T) {
|
func TestPrune(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
json string
|
json string
|
||||||
schema *structuralschema.Structural
|
dontPruneMetaAtRoot bool
|
||||||
expected string
|
schema *structuralschema.Structural
|
||||||
|
expected string
|
||||||
}{
|
}{
|
||||||
{"empty", "null", nil, "null"},
|
{name: "empty", json: "null", expected: "null"},
|
||||||
{"scalar", "4", &structuralschema.Structural{}, "4"},
|
{name: "scalar", json: "4", schema: &structuralschema.Structural{}, expected: "4"},
|
||||||
{"scalar array", "[1,2]", &structuralschema.Structural{
|
{name: "scalar array", json: "[1,2]", schema: &structuralschema.Structural{
|
||||||
Items: &structuralschema.Structural{},
|
Items: &structuralschema.Structural{},
|
||||||
}, "[1,2]"},
|
}, expected: "[1,2]"},
|
||||||
{"object array", `[{"a":1},{"b":1},{"a":1,"b":2,"c":3}]`, &structuralschema.Structural{
|
{name: "object array", json: `[{"a":1},{"b":1},{"a":1,"b":2,"c":3}]`, schema: &structuralschema.Structural{
|
||||||
Items: &structuralschema.Structural{
|
Items: &structuralschema.Structural{
|
||||||
Properties: map[string]structuralschema.Structural{
|
Properties: map[string]structuralschema.Structural{
|
||||||
"a": {},
|
"a": {},
|
||||||
"c": {},
|
"c": {},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}, `[{"a":1},{},{"a":1,"c":3}]`},
|
}, expected: `[{"a":1},{},{"a":1,"c":3}]`},
|
||||||
{"object array with nil schema", `[{"a":1},{"b":1},{"a":1,"b":2,"c":3}]`, nil, `[{},{},{}]`},
|
{name: "object array with nil schema", json: `[{"a":1},{"b":1},{"a":1,"b":2,"c":3}]`, expected: `[{},{},{}]`},
|
||||||
{"object array object", `{"array":[{"a":1},{"b":1},{"a":1,"b":2,"c":3}],"unspecified":{"a":1},"specified":{"a":1,"b":2,"c":3}}`, &structuralschema.Structural{
|
{name: "object array object", json: `{"array":[{"a":1},{"b":1},{"a":1,"b":2,"c":3}],"unspecified":{"a":1},"specified":{"a":1,"b":2,"c":3}}`, schema: &structuralschema.Structural{
|
||||||
Properties: map[string]structuralschema.Structural{
|
Properties: map[string]structuralschema.Structural{
|
||||||
"array": {
|
"array": {
|
||||||
Items: &structuralschema.Structural{
|
Items: &structuralschema.Structural{
|
||||||
@@ -64,8 +66,8 @@ func TestPrune(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}, `{"array":[{"a":1},{},{"a":1,"c":3}],"specified":{"a":1,"c":3}}`},
|
}, expected: `{"array":[{"a":1},{},{"a":1,"c":3}],"specified":{"a":1,"c":3}}`},
|
||||||
{"nested x-kubernetes-preserve-unknown-fields", `
|
{name: "nested x-kubernetes-preserve-unknown-fields", json: `
|
||||||
{
|
{
|
||||||
"unspecified":"bar",
|
"unspecified":"bar",
|
||||||
"alpha": "abc",
|
"alpha": "abc",
|
||||||
@@ -82,9 +84,15 @@ func TestPrune(t *testing.T) {
|
|||||||
"unspecifiedObject": {"unspecified": "bar"},
|
"unspecifiedObject": {"unspecified": "bar"},
|
||||||
"pruning": {"unspecified": "bar"},
|
"pruning": {"unspecified": "bar"},
|
||||||
"preserving": {"unspecified": "bar"}
|
"preserving": {"unspecified": "bar"}
|
||||||
|
},
|
||||||
|
"preservingAdditionalProperties": {
|
||||||
|
"foo": {
|
||||||
|
"specified": {"unspecified":"bar"},
|
||||||
|
"unspecified": "bar"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`, &structuralschema.Structural{
|
`, schema: &structuralschema.Structural{
|
||||||
Generic: structuralschema.Generic{Type: "object"},
|
Generic: structuralschema.Generic{Type: "object"},
|
||||||
Extensions: structuralschema.Extensions{XPreserveUnknownFields: true},
|
Extensions: structuralschema.Extensions{XPreserveUnknownFields: true},
|
||||||
Properties: map[string]structuralschema.Structural{
|
Properties: map[string]structuralschema.Structural{
|
||||||
@@ -115,8 +123,22 @@ func TestPrune(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"preservingAdditionalProperties": {
|
||||||
|
Extensions: structuralschema.Extensions{XPreserveUnknownFields: true},
|
||||||
|
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: `
|
||||||
{
|
{
|
||||||
"unspecified":"bar",
|
"unspecified":"bar",
|
||||||
"alpha": "abc",
|
"alpha": "abc",
|
||||||
@@ -131,10 +153,15 @@ func TestPrune(t *testing.T) {
|
|||||||
"unspecifiedObject": {"unspecified": "bar"},
|
"unspecifiedObject": {"unspecified": "bar"},
|
||||||
"pruning": {},
|
"pruning": {},
|
||||||
"preserving": {"unspecified": "bar"}
|
"preserving": {"unspecified": "bar"}
|
||||||
|
},
|
||||||
|
"preservingAdditionalProperties": {
|
||||||
|
"foo": {
|
||||||
|
"specified": {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`},
|
`},
|
||||||
{"additionalProperties with schema", `{"a":1,"b":1,"c":{"a":1,"b":2,"c":{"a":1}}}`, &structuralschema.Structural{
|
{name: "additionalProperties with schema", json: `{"a":1,"b":1,"c":{"a":1,"b":2,"c":{"a":1}}}`, schema: &structuralschema.Structural{
|
||||||
Properties: map[string]structuralschema.Structural{
|
Properties: map[string]structuralschema.Structural{
|
||||||
"a": {},
|
"a": {},
|
||||||
"c": {
|
"c": {
|
||||||
@@ -149,8 +176,8 @@ func TestPrune(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}, `{"a":1,"c":{"a":1,"b":2,"c":{}}}`},
|
}, expected: `{"a":1,"c":{"a":1,"b":2,"c":{}}}`},
|
||||||
{"additionalProperties with bool", `{"a":1,"b":1,"c":{"a":1,"b":2,"c":{"a":1}}}`, &structuralschema.Structural{
|
{name: "additionalProperties with bool", json: `{"a":1,"b":1,"c":{"a":1,"b":2,"c":{"a":1}}}`, schema: &structuralschema.Structural{
|
||||||
Properties: map[string]structuralschema.Structural{
|
Properties: map[string]structuralschema.Structural{
|
||||||
"a": {},
|
"a": {},
|
||||||
"c": {
|
"c": {
|
||||||
@@ -161,7 +188,313 @@ func TestPrune(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}, `{"a":1,"c":{"a":1,"b":2,"c":{}}}`},
|
}, expected: `{"a":1,"c":{"a":1,"b":2,"c":{}}}`},
|
||||||
|
{name: "x-kubernetes-embedded-resource", json: `
|
||||||
|
{
|
||||||
|
"apiVersion": "foo/v1",
|
||||||
|
"kind": "Foo",
|
||||||
|
"metadata": {
|
||||||
|
"name": "instance",
|
||||||
|
"unspecified": "bar"
|
||||||
|
},
|
||||||
|
"unspecified":"bar",
|
||||||
|
"pruned": {
|
||||||
|
"apiVersion": "foo/v1",
|
||||||
|
"kind": "Foo",
|
||||||
|
"unspecified": "bar",
|
||||||
|
"metadata": {
|
||||||
|
"name": "instance",
|
||||||
|
"unspecified": "bar"
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
"unspecified": "bar"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"preserving": {
|
||||||
|
"apiVersion": "foo/v1",
|
||||||
|
"kind": "Foo",
|
||||||
|
"unspecified": "bar",
|
||||||
|
"metadata": {
|
||||||
|
"name": "instance",
|
||||||
|
"unspecified": "bar"
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
"unspecified": "bar"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nested": {
|
||||||
|
"apiVersion": "foo/v1",
|
||||||
|
"kind": "Foo",
|
||||||
|
"unspecified": "bar",
|
||||||
|
"metadata": {
|
||||||
|
"name": "instance",
|
||||||
|
"unspecified": "bar"
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
"unspecified": "bar",
|
||||||
|
"embedded": {
|
||||||
|
"apiVersion": "foo/v1",
|
||||||
|
"kind": "Foo",
|
||||||
|
"unspecified": "bar",
|
||||||
|
"metadata": {
|
||||||
|
"name": "instance",
|
||||||
|
"unspecified": "bar"
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
"unspecified": "bar"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`, schema: &structuralschema.Structural{
|
||||||
|
Generic: structuralschema.Generic{Type: "object"},
|
||||||
|
Properties: map[string]structuralschema.Structural{
|
||||||
|
"pruned": {
|
||||||
|
Generic: structuralschema.Generic{Type: "object"},
|
||||||
|
Extensions: structuralschema.Extensions{
|
||||||
|
XEmbeddedResource: true,
|
||||||
|
},
|
||||||
|
Properties: map[string]structuralschema.Structural{
|
||||||
|
"spec": {
|
||||||
|
Generic: structuralschema.Generic{Type: "object"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"preserving": {
|
||||||
|
Generic: structuralschema.Generic{Type: "object"},
|
||||||
|
Extensions: structuralschema.Extensions{
|
||||||
|
XEmbeddedResource: true,
|
||||||
|
XPreserveUnknownFields: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"nested": {
|
||||||
|
Generic: structuralschema.Generic{Type: "object"},
|
||||||
|
Extensions: structuralschema.Extensions{
|
||||||
|
XEmbeddedResource: true,
|
||||||
|
},
|
||||||
|
Properties: map[string]structuralschema.Structural{
|
||||||
|
"spec": {
|
||||||
|
Generic: structuralschema.Generic{Type: "object"},
|
||||||
|
Properties: map[string]structuralschema.Structural{
|
||||||
|
"embedded": {
|
||||||
|
Generic: structuralschema.Generic{Type: "object"},
|
||||||
|
Extensions: structuralschema.Extensions{
|
||||||
|
XEmbeddedResource: true,
|
||||||
|
},
|
||||||
|
Properties: map[string]structuralschema.Structural{
|
||||||
|
"spec": {
|
||||||
|
Generic: structuralschema.Generic{Type: "object"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, expected: `
|
||||||
|
{
|
||||||
|
"pruned": {
|
||||||
|
"apiVersion": "foo/v1",
|
||||||
|
"kind": "Foo",
|
||||||
|
"metadata": {
|
||||||
|
"name": "instance",
|
||||||
|
"unspecified": "bar"
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"preserving": {
|
||||||
|
"apiVersion": "foo/v1",
|
||||||
|
"kind": "Foo",
|
||||||
|
"unspecified": "bar",
|
||||||
|
"metadata": {
|
||||||
|
"name": "instance",
|
||||||
|
"unspecified": "bar"
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
"unspecified": "bar"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nested": {
|
||||||
|
"apiVersion": "foo/v1",
|
||||||
|
"kind": "Foo",
|
||||||
|
"metadata": {
|
||||||
|
"name": "instance",
|
||||||
|
"unspecified": "bar"
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
"embedded": {
|
||||||
|
"apiVersion": "foo/v1",
|
||||||
|
"kind": "Foo",
|
||||||
|
"metadata": {
|
||||||
|
"name": "instance",
|
||||||
|
"unspecified": "bar"
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`},
|
||||||
|
{name: "x-kubernetes-embedded-resource, with root=true", json: `
|
||||||
|
{
|
||||||
|
"apiVersion": "foo/v1",
|
||||||
|
"kind": "Foo",
|
||||||
|
"metadata": {
|
||||||
|
"name": "instance",
|
||||||
|
"unspecified": "bar"
|
||||||
|
},
|
||||||
|
"unspecified":"bar",
|
||||||
|
"pruned": {
|
||||||
|
"apiVersion": "foo/v1",
|
||||||
|
"kind": "Foo",
|
||||||
|
"unspecified": "bar",
|
||||||
|
"metadata": {
|
||||||
|
"name": "instance",
|
||||||
|
"unspecified": "bar"
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
"unspecified": "bar"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"preserving": {
|
||||||
|
"apiVersion": "foo/v1",
|
||||||
|
"kind": "Foo",
|
||||||
|
"unspecified": "bar",
|
||||||
|
"metadata": {
|
||||||
|
"name": "instance",
|
||||||
|
"unspecified": "bar"
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
"unspecified": "bar"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nested": {
|
||||||
|
"apiVersion": "foo/v1",
|
||||||
|
"kind": "Foo",
|
||||||
|
"unspecified": "bar",
|
||||||
|
"metadata": {
|
||||||
|
"name": "instance",
|
||||||
|
"unspecified": "bar"
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
"unspecified": "bar",
|
||||||
|
"embedded": {
|
||||||
|
"apiVersion": "foo/v1",
|
||||||
|
"kind": "Foo",
|
||||||
|
"unspecified": "bar",
|
||||||
|
"metadata": {
|
||||||
|
"name": "instance",
|
||||||
|
"unspecified": "bar"
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
"unspecified": "bar"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`, dontPruneMetaAtRoot: true, schema: &structuralschema.Structural{
|
||||||
|
Generic: structuralschema.Generic{Type: "object"},
|
||||||
|
Properties: map[string]structuralschema.Structural{
|
||||||
|
"pruned": {
|
||||||
|
Generic: structuralschema.Generic{Type: "object"},
|
||||||
|
Extensions: structuralschema.Extensions{
|
||||||
|
XEmbeddedResource: true,
|
||||||
|
},
|
||||||
|
Properties: map[string]structuralschema.Structural{
|
||||||
|
"spec": {
|
||||||
|
Generic: structuralschema.Generic{Type: "object"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"preserving": {
|
||||||
|
Generic: structuralschema.Generic{Type: "object"},
|
||||||
|
Extensions: structuralschema.Extensions{
|
||||||
|
XEmbeddedResource: true,
|
||||||
|
XPreserveUnknownFields: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"nested": {
|
||||||
|
Generic: structuralschema.Generic{Type: "object"},
|
||||||
|
Extensions: structuralschema.Extensions{
|
||||||
|
XEmbeddedResource: true,
|
||||||
|
},
|
||||||
|
Properties: map[string]structuralschema.Structural{
|
||||||
|
"spec": {
|
||||||
|
Generic: structuralschema.Generic{Type: "object"},
|
||||||
|
Properties: map[string]structuralschema.Structural{
|
||||||
|
"embedded": {
|
||||||
|
Generic: structuralschema.Generic{Type: "object"},
|
||||||
|
Extensions: structuralschema.Extensions{
|
||||||
|
XEmbeddedResource: true,
|
||||||
|
},
|
||||||
|
Properties: map[string]structuralschema.Structural{
|
||||||
|
"spec": {
|
||||||
|
Generic: structuralschema.Generic{Type: "object"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, expected: `
|
||||||
|
{
|
||||||
|
"apiVersion": "foo/v1",
|
||||||
|
"kind": "Foo",
|
||||||
|
"metadata": {
|
||||||
|
"name": "instance",
|
||||||
|
"unspecified": "bar"
|
||||||
|
},
|
||||||
|
"pruned": {
|
||||||
|
"apiVersion": "foo/v1",
|
||||||
|
"kind": "Foo",
|
||||||
|
"metadata": {
|
||||||
|
"name": "instance",
|
||||||
|
"unspecified": "bar"
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"preserving": {
|
||||||
|
"apiVersion": "foo/v1",
|
||||||
|
"kind": "Foo",
|
||||||
|
"unspecified": "bar",
|
||||||
|
"metadata": {
|
||||||
|
"name": "instance",
|
||||||
|
"unspecified": "bar"
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
"unspecified": "bar"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nested": {
|
||||||
|
"apiVersion": "foo/v1",
|
||||||
|
"kind": "Foo",
|
||||||
|
"metadata": {
|
||||||
|
"name": "instance",
|
||||||
|
"unspecified": "bar"
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
"embedded": {
|
||||||
|
"apiVersion": "foo/v1",
|
||||||
|
"kind": "Foo",
|
||||||
|
"metadata": {
|
||||||
|
"name": "instance",
|
||||||
|
"unspecified": "bar"
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
@@ -175,7 +508,7 @@ func TestPrune(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
prune(in, tt.schema)
|
Prune(in, tt.schema, tt.dontPruneMetaAtRoot)
|
||||||
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)
|
||||||
@@ -184,7 +517,7 @@ func TestPrune(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected result mashalling error: %v", err)
|
t.Fatalf("unexpected result mashalling error: %v", err)
|
||||||
}
|
}
|
||||||
t.Errorf("expected: %s\ngot: %s", tt.expected, buf.String())
|
t.Errorf("expected: %s\ngot: %s\ndiff: %s", tt.expected, buf.String(), diff.ObjectDiff(expected, in))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -260,7 +593,7 @@ func BenchmarkPrune(b *testing.B) {
|
|||||||
|
|
||||||
b.StartTimer()
|
b.StartTimer()
|
||||||
for i := 0; i < b.N; i++ {
|
for i := 0; i < b.N; i++ {
|
||||||
Prune(instances[i], schema)
|
Prune(instances[i], schema, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -108,6 +108,8 @@ func validateStructuralInvariants(s *Structural, lvl level, fldPath *field.Path)
|
|||||||
|
|
||||||
allErrs = append(allErrs, validateValueValidation(s.ValueValidation, skipAnyOf, skipFirstAllOfAnyOf, lvl, fldPath)...)
|
allErrs = append(allErrs, validateValueValidation(s.ValueValidation, skipAnyOf, skipFirstAllOfAnyOf, lvl, fldPath)...)
|
||||||
|
|
||||||
|
checkMetadata := (lvl == rootLevel) || s.XEmbeddedResource
|
||||||
|
|
||||||
if s.XEmbeddedResource && s.Type != "object" {
|
if s.XEmbeddedResource && s.Type != "object" {
|
||||||
if len(s.Type) == 0 {
|
if len(s.Type) == 0 {
|
||||||
allErrs = append(allErrs, field.Required(fldPath.Child("type"), "must be object if x-kubernetes-embedded-resource is true"))
|
allErrs = append(allErrs, field.Required(fldPath.Child("type"), "must be object if x-kubernetes-embedded-resource is true"))
|
||||||
@@ -124,12 +126,30 @@ func validateStructuralInvariants(s *Structural, lvl level, fldPath *field.Path)
|
|||||||
allErrs = append(allErrs, field.Required(fldPath.Child("type"), "must not be empty for specified object fields"))
|
allErrs = append(allErrs, field.Required(fldPath.Child("type"), "must not be empty for specified object fields"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if s.XEmbeddedResource && s.AdditionalProperties != nil {
|
||||||
|
allErrs = append(allErrs, field.Forbidden(fldPath.Child("additionalProperties"), "must not be used if x-kubernetes-embedded-resource is set"))
|
||||||
|
}
|
||||||
|
|
||||||
if lvl == rootLevel && len(s.Type) > 0 && s.Type != "object" {
|
if lvl == rootLevel && len(s.Type) > 0 && s.Type != "object" {
|
||||||
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
|
// restrict metadata schemas to name and generateName only
|
||||||
|
if kind, found := s.Properties["kind"]; found && checkMetadata {
|
||||||
|
if kind.Type != "string" {
|
||||||
|
allErrs = append(allErrs, field.Invalid(fldPath.Child("properties").Key("kind").Child("type"), kind.Type, "must be string"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if apiVersion, found := s.Properties["apiVersion"]; found && checkMetadata {
|
||||||
|
if apiVersion.Type != "string" {
|
||||||
|
allErrs = append(allErrs, field.Invalid(fldPath.Child("properties").Key("apiVersion").Child("type"), apiVersion.Type, "must be string"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if metadata, found := s.Properties["metadata"]; found && checkMetadata {
|
||||||
|
if metadata.Type != "object" {
|
||||||
|
allErrs = append(allErrs, field.Invalid(fldPath.Child("properties").Key("metadata").Child("type"), metadata.Type, "must be object"))
|
||||||
|
}
|
||||||
|
}
|
||||||
if metadata, found := s.Properties["metadata"]; found && lvl == rootLevel {
|
if metadata, found := s.Properties["metadata"]; found && lvl == rootLevel {
|
||||||
// metadata is a shallow copy. We can mutate it.
|
// metadata is a shallow copy. We can mutate it.
|
||||||
_, foundName := metadata.Properties["name"]
|
_, foundName := metadata.Properties["name"]
|
||||||
@@ -140,6 +160,7 @@ func validateStructuralInvariants(s *Structural, lvl level, fldPath *field.Path)
|
|||||||
metadata.Properties = nil
|
metadata.Properties = nil
|
||||||
}
|
}
|
||||||
metadata.Type = ""
|
metadata.Type = ""
|
||||||
|
metadata.Default.Object = nil // this is checked in API validation (and also tested)
|
||||||
if metadata.ValueValidation == nil {
|
if metadata.ValueValidation == nil {
|
||||||
metadata.ValueValidation = &ValueValidation{}
|
metadata.ValueValidation = &ValueValidation{}
|
||||||
}
|
}
|
||||||
|
@@ -24,6 +24,7 @@ go_library(
|
|||||||
"//staging/src/k8s.io/apimachinery/pkg/labels:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/labels:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/util/runtime:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/util/runtime:go_default_library",
|
||||||
|
"//staging/src/k8s.io/apimachinery/pkg/util/sets:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/util/wait:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/util/wait:go_default_library",
|
||||||
"//staging/src/k8s.io/apiserver/pkg/endpoints:go_default_library",
|
"//staging/src/k8s.io/apiserver/pkg/endpoints:go_default_library",
|
||||||
"//staging/src/k8s.io/apiserver/pkg/endpoints/openapi:go_default_library",
|
"//staging/src/k8s.io/apiserver/pkg/endpoints/openapi:go_default_library",
|
||||||
|
@@ -30,6 +30,7 @@ import (
|
|||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1"
|
metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/util/sets"
|
||||||
"k8s.io/apiserver/pkg/endpoints"
|
"k8s.io/apiserver/pkg/endpoints"
|
||||||
"k8s.io/apiserver/pkg/endpoints/openapi"
|
"k8s.io/apiserver/pkg/endpoints/openapi"
|
||||||
openapibuilder "k8s.io/kube-openapi/pkg/builder"
|
openapibuilder "k8s.io/kube-openapi/pkg/builder"
|
||||||
@@ -310,6 +311,7 @@ func (b *builder) buildKubeNative(schema *structuralschema.Structural, v2 bool)
|
|||||||
ret.SetProperty("metadata", *spec.RefSchema(objectMetaSchemaRef).
|
ret.SetProperty("metadata", *spec.RefSchema(objectMetaSchemaRef).
|
||||||
WithDescription(swaggerPartialObjectMetadataDescriptions["metadata"]))
|
WithDescription(swaggerPartialObjectMetadataDescriptions["metadata"]))
|
||||||
addTypeMetaProperties(ret)
|
addTypeMetaProperties(ret)
|
||||||
|
addEmbeddedProperties(ret)
|
||||||
}
|
}
|
||||||
ret.AddExtension(endpoints.ROUTE_META_GVK, []interface{}{
|
ret.AddExtension(endpoints.ROUTE_META_GVK, []interface{}{
|
||||||
map[string]interface{}{
|
map[string]interface{}{
|
||||||
@@ -322,6 +324,42 @@ func (b *builder) buildKubeNative(schema *structuralschema.Structural, v2 bool)
|
|||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func addEmbeddedProperties(s *spec.Schema) {
|
||||||
|
if s == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for k := range s.Properties {
|
||||||
|
v := s.Properties[k]
|
||||||
|
addEmbeddedProperties(&v)
|
||||||
|
s.Properties[k] = v
|
||||||
|
}
|
||||||
|
if s.Items != nil {
|
||||||
|
addEmbeddedProperties(s.Items.Schema)
|
||||||
|
}
|
||||||
|
if s.AdditionalProperties != nil {
|
||||||
|
addEmbeddedProperties(s.AdditionalProperties.Schema)
|
||||||
|
}
|
||||||
|
|
||||||
|
if isTrue, ok := s.VendorExtensible.Extensions.GetBool("x-kubernetes-embedded-resource"); ok && isTrue {
|
||||||
|
s.SetProperty("apiVersion", withDescription(getDefinition(typeMetaType).SchemaProps.Properties["apiVersion"],
|
||||||
|
"apiVersion defines the versioned schema of this representation of an object. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#resources",
|
||||||
|
))
|
||||||
|
s.SetProperty("kind", withDescription(getDefinition(typeMetaType).SchemaProps.Properties["kind"],
|
||||||
|
"kind is a string value representing the type of this object. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#types-kinds",
|
||||||
|
))
|
||||||
|
s.SetProperty("metadata", *spec.RefSchema(objectMetaSchemaRef).WithDescription(swaggerPartialObjectMetadataDescriptions["metadata"]))
|
||||||
|
|
||||||
|
req := sets.NewString(s.Required...)
|
||||||
|
if !req.Has("kind") {
|
||||||
|
s.Required = append(s.Required, "kind")
|
||||||
|
}
|
||||||
|
if !req.Has("apiVersion") {
|
||||||
|
s.Required = append(s.Required, "apiVersion")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// getDefinition gets definition for given Kubernetes type. This function is extracted from
|
// getDefinition gets definition for given Kubernetes type. This function is extracted from
|
||||||
// kube-openapi builder logic
|
// kube-openapi builder logic
|
||||||
func getDefinition(name string) spec.Schema {
|
func getDefinition(name string) spec.Schema {
|
||||||
@@ -329,6 +367,10 @@ func getDefinition(name string) spec.Schema {
|
|||||||
return definitions[name].Schema
|
return definitions[name].Schema
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func withDescription(s spec.Schema, desc string) spec.Schema {
|
||||||
|
return *s.WithDescription(desc)
|
||||||
|
}
|
||||||
|
|
||||||
func buildDefinitionsFunc() {
|
func buildDefinitionsFunc() {
|
||||||
namer = openapi.NewDefinitionNamer(runtime.NewScheme())
|
namer = openapi.NewDefinitionNamer(runtime.NewScheme())
|
||||||
definitions = generatedopenapi.GetOpenAPIDefinitions(func(name string) spec.Ref {
|
definitions = generatedopenapi.GetOpenAPIDefinitions(func(name string) spec.Ref {
|
||||||
|
@@ -21,6 +21,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/go-openapi/spec"
|
"github.com/go-openapi/spec"
|
||||||
|
|
||||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
||||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
|
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
|
||||||
structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
|
structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
|
||||||
@@ -154,7 +155,22 @@ func TestNewBuilder(t *testing.T) {
|
|||||||
"embedded-object": {
|
"embedded-object": {
|
||||||
"x-kubernetes-embedded-resource": true,
|
"x-kubernetes-embedded-resource": true,
|
||||||
"x-kubernetes-preserve-unknown-fields": true,
|
"x-kubernetes-preserve-unknown-fields": true,
|
||||||
"type": "object"
|
"type": "object",
|
||||||
|
"required":["kind","apiVersion"],
|
||||||
|
"properties":{
|
||||||
|
"apiVersion":{
|
||||||
|
"description":"apiVersion defines the versioned schema of this representation of an object. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#resources",
|
||||||
|
"type":"string"
|
||||||
|
},
|
||||||
|
"kind":{
|
||||||
|
"description":"kind is a string value representing the type of this object. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#types-kinds",
|
||||||
|
"type":"string"
|
||||||
|
},
|
||||||
|
"metadata":{
|
||||||
|
"description":"Standard object's metadata. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#metadata",
|
||||||
|
"$ref":"#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"x-kubernetes-group-version-kind":[{"group":"bar.k8s.io","kind":"Foo","version":"v1"}]
|
"x-kubernetes-group-version-kind":[{"group":"bar.k8s.io","kind":"Foo","version":"v1"}]
|
||||||
@@ -305,7 +321,22 @@ func TestNewBuilder(t *testing.T) {
|
|||||||
"embedded-object": {
|
"embedded-object": {
|
||||||
"x-kubernetes-embedded-resource": true,
|
"x-kubernetes-embedded-resource": true,
|
||||||
"x-kubernetes-preserve-unknown-fields": true,
|
"x-kubernetes-preserve-unknown-fields": true,
|
||||||
"type": "object"
|
"type": "object",
|
||||||
|
"required":["kind","apiVersion"],
|
||||||
|
"properties":{
|
||||||
|
"apiVersion":{
|
||||||
|
"description":"apiVersion defines the versioned schema of this representation of an object. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#resources",
|
||||||
|
"type":"string"
|
||||||
|
},
|
||||||
|
"kind":{
|
||||||
|
"description":"kind is a string value representing the type of this object. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#types-kinds",
|
||||||
|
"type":"string"
|
||||||
|
},
|
||||||
|
"metadata":{
|
||||||
|
"description":"Standard object's metadata. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#metadata",
|
||||||
|
"$ref":"#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"x-kubernetes-group-version-kind":[{"group":"bar.k8s.io","kind":"Foo","version":"v1"}]
|
"x-kubernetes-group-version-kind":[{"group":"bar.k8s.io","kind":"Foo","version":"v1"}]
|
||||||
@@ -373,7 +404,7 @@ func TestNewBuilder(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !reflect.DeepEqual(&wantedSchema, got.schema) {
|
if !reflect.DeepEqual(&wantedSchema, got.schema) {
|
||||||
t.Errorf("unexpected schema: %s\nwant = %#v\ngot = %#v", diff.ObjectDiff(&wantedSchema, got.schema), &wantedSchema, got.schema)
|
t.Errorf("unexpected schema: %s\nwant = %#v\ngot = %#v", schemaDiff(&wantedSchema, got.schema), &wantedSchema, got.schema)
|
||||||
}
|
}
|
||||||
|
|
||||||
gotListProperties := properties(got.listSchema.Properties)
|
gotListProperties := properties(got.listSchema.Properties)
|
||||||
@@ -383,7 +414,7 @@ func TestNewBuilder(t *testing.T) {
|
|||||||
|
|
||||||
gotListSchema := got.listSchema.Properties["items"].Items.Schema
|
gotListSchema := got.listSchema.Properties["items"].Items.Schema
|
||||||
if !reflect.DeepEqual(&wantedItemsSchema, gotListSchema) {
|
if !reflect.DeepEqual(&wantedItemsSchema, gotListSchema) {
|
||||||
t.Errorf("unexpected list schema: %s (want/got)", diff.ObjectDiff(&wantedItemsSchema, &gotListSchema))
|
t.Errorf("unexpected list schema: %s (want/got)", schemaDiff(&wantedItemsSchema, gotListSchema))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -396,3 +427,15 @@ func properties(p map[string]spec.Schema) sets.String {
|
|||||||
}
|
}
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func schemaDiff(a, b *spec.Schema) string {
|
||||||
|
as, err := json.Marshal(a)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
bs, err := json.Marshal(b)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return diff.StringDiff(string(as), string(bs))
|
||||||
|
}
|
||||||
|
@@ -19,6 +19,8 @@ go_library(
|
|||||||
deps = [
|
deps = [
|
||||||
"//staging/src/k8s.io/api/autoscaling/v1:go_default_library",
|
"//staging/src/k8s.io/api/autoscaling/v1:go_default_library",
|
||||||
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions:go_default_library",
|
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions:go_default_library",
|
||||||
|
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema:go_default_library",
|
||||||
|
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/objectmeta:go_default_library",
|
||||||
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/validation:go_default_library",
|
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/validation:go_default_library",
|
||||||
"//staging/src/k8s.io/apiextensions-apiserver/pkg/features:go_default_library",
|
"//staging/src/k8s.io/apiextensions-apiserver/pkg/features:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/api/equality:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/api/equality:go_default_library",
|
||||||
|
@@ -99,6 +99,7 @@ func newStorage(t *testing.T) (customresource.CustomResourceStorage, *etcdtestin
|
|||||||
kind,
|
kind,
|
||||||
nil,
|
nil,
|
||||||
nil,
|
nil,
|
||||||
|
nil,
|
||||||
status,
|
status,
|
||||||
scale,
|
scale,
|
||||||
),
|
),
|
||||||
|
@@ -35,6 +35,8 @@ import (
|
|||||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||||
|
|
||||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
||||||
|
structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
|
||||||
|
schemaobjectmeta "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/objectmeta"
|
||||||
apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features"
|
apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -45,11 +47,12 @@ type customResourceStrategy struct {
|
|||||||
|
|
||||||
namespaceScoped bool
|
namespaceScoped bool
|
||||||
validator customResourceValidator
|
validator customResourceValidator
|
||||||
|
schemas map[string]*structuralschema.Structural
|
||||||
status *apiextensions.CustomResourceSubresourceStatus
|
status *apiextensions.CustomResourceSubresourceStatus
|
||||||
scale *apiextensions.CustomResourceSubresourceScale
|
scale *apiextensions.CustomResourceSubresourceScale
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewStrategy(typer runtime.ObjectTyper, namespaceScoped bool, kind schema.GroupVersionKind, schemaValidator, statusSchemaValidator *validate.SchemaValidator, status *apiextensions.CustomResourceSubresourceStatus, scale *apiextensions.CustomResourceSubresourceScale) customResourceStrategy {
|
func NewStrategy(typer runtime.ObjectTyper, namespaceScoped bool, kind schema.GroupVersionKind, schemaValidator, statusSchemaValidator *validate.SchemaValidator, schemas map[string]*structuralschema.Structural, status *apiextensions.CustomResourceSubresourceStatus, scale *apiextensions.CustomResourceSubresourceScale) customResourceStrategy {
|
||||||
return customResourceStrategy{
|
return customResourceStrategy{
|
||||||
ObjectTyper: typer,
|
ObjectTyper: typer,
|
||||||
NameGenerator: names.SimpleNameGenerator,
|
NameGenerator: names.SimpleNameGenerator,
|
||||||
@@ -62,6 +65,7 @@ func NewStrategy(typer runtime.ObjectTyper, namespaceScoped bool, kind schema.Gr
|
|||||||
schemaValidator: schemaValidator,
|
schemaValidator: schemaValidator,
|
||||||
statusSchemaValidator: statusSchemaValidator,
|
statusSchemaValidator: statusSchemaValidator,
|
||||||
},
|
},
|
||||||
|
schemas: schemas,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,7 +133,16 @@ func copyNonMetadata(original map[string]interface{}) map[string]interface{} {
|
|||||||
|
|
||||||
// Validate validates a new CustomResource.
|
// Validate validates a new CustomResource.
|
||||||
func (a customResourceStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList {
|
func (a customResourceStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList {
|
||||||
return a.validator.Validate(ctx, obj, a.scale)
|
var errs field.ErrorList
|
||||||
|
errs = append(errs, a.validator.Validate(ctx, obj, a.scale)...)
|
||||||
|
|
||||||
|
// validate embedded resources
|
||||||
|
if u, ok := obj.(*unstructured.Unstructured); ok {
|
||||||
|
v := obj.GetObjectKind().GroupVersionKind().Version
|
||||||
|
errs = append(errs, schemaobjectmeta.Validate(nil, u.Object, a.schemas[v], false)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return errs
|
||||||
}
|
}
|
||||||
|
|
||||||
// Canonicalize normalizes the object after validation.
|
// Canonicalize normalizes the object after validation.
|
||||||
@@ -149,7 +162,16 @@ func (customResourceStrategy) AllowUnconditionalUpdate() bool {
|
|||||||
|
|
||||||
// ValidateUpdate is the default update validation for an end user updating status.
|
// ValidateUpdate is the default update validation for an end user updating status.
|
||||||
func (a customResourceStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList {
|
func (a customResourceStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList {
|
||||||
return a.validator.ValidateUpdate(ctx, obj, old, a.scale)
|
var errs field.ErrorList
|
||||||
|
errs = append(errs, a.validator.ValidateUpdate(ctx, obj, old, a.scale)...)
|
||||||
|
|
||||||
|
// Checks the embedded objects. We don't make a difference between update and create for those.
|
||||||
|
if u, ok := obj.(*unstructured.Unstructured); ok {
|
||||||
|
v := obj.GetObjectKind().GroupVersionKind().Version
|
||||||
|
errs = append(errs, schemaobjectmeta.Validate(nil, u.Object, a.schemas[v], false)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return errs
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAttrs returns labels and fields of a given object for filtering purposes.
|
// GetAttrs returns labels and fields of a given object for filtering purposes.
|
||||||
|
@@ -42,6 +42,7 @@ go_test(
|
|||||||
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/runtime/serializer:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/runtime/serializer:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/types:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/types:go_default_library",
|
||||||
|
"//staging/src/k8s.io/apimachinery/pkg/util/diff:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/util/json:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/util/json:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/util/wait:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/util/wait:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/util/yaml:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/util/yaml:go_default_library",
|
||||||
|
@@ -24,18 +24,24 @@ import (
|
|||||||
|
|
||||||
"github.com/coreos/etcd/clientv3"
|
"github.com/coreos/etcd/clientv3"
|
||||||
"github.com/coreos/etcd/pkg/transport"
|
"github.com/coreos/etcd/pkg/transport"
|
||||||
|
"sigs.k8s.io/yaml"
|
||||||
|
|
||||||
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
|
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
|
||||||
"k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
|
"k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
|
||||||
serveroptions "k8s.io/apiextensions-apiserver/pkg/cmd/server/options"
|
serveroptions "k8s.io/apiextensions-apiserver/pkg/cmd/server/options"
|
||||||
|
"k8s.io/apiextensions-apiserver/pkg/features"
|
||||||
"k8s.io/apiextensions-apiserver/test/integration/fixtures"
|
"k8s.io/apiextensions-apiserver/test/integration/fixtures"
|
||||||
"k8s.io/apimachinery/pkg/api/errors"
|
"k8s.io/apimachinery/pkg/api/errors"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
"k8s.io/apimachinery/pkg/util/diff"
|
||||||
"k8s.io/apimachinery/pkg/util/json"
|
"k8s.io/apimachinery/pkg/util/json"
|
||||||
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
|
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
|
||||||
|
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||||
"k8s.io/client-go/dynamic"
|
"k8s.io/client-go/dynamic"
|
||||||
|
featuregatetesting "k8s.io/component-base/featuregate/testing"
|
||||||
|
"k8s.io/utils/pointer"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPostInvalidObjectMeta(t *testing.T) {
|
func TestPostInvalidObjectMeta(t *testing.T) {
|
||||||
@@ -99,6 +105,18 @@ func TestInvalidObjectMetaInStorage(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
noxuDefinition := fixtures.NewNoxuCustomResourceDefinition(apiextensionsv1beta1.NamespaceScoped)
|
noxuDefinition := fixtures.NewNoxuCustomResourceDefinition(apiextensionsv1beta1.NamespaceScoped)
|
||||||
|
noxuDefinition.Spec.Validation = &apiextensionsv1beta1.CustomResourceValidation{
|
||||||
|
OpenAPIV3Schema: &apiextensionsv1beta1.JSONSchemaProps{
|
||||||
|
Type: "object",
|
||||||
|
Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
|
||||||
|
"embedded": {
|
||||||
|
Type: "object",
|
||||||
|
XEmbeddedResource: true,
|
||||||
|
XPreserveUnknownFields: pointer.BoolPtr(true),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
|
noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -127,11 +145,17 @@ func TestInvalidObjectMetaInStorage(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Logf("Creating object with invalid labels manually in etcd")
|
t.Logf("Creating object with wrongly typed annotations and non-validating labels manually in etcd")
|
||||||
|
|
||||||
original := fixtures.NewNoxuInstance("default", "foo")
|
original := fixtures.NewNoxuInstance("default", "foo")
|
||||||
unstructured.SetNestedField(original.UnstructuredContent(), int64(42), "metadata", "unknown")
|
unstructured.SetNestedField(original.UnstructuredContent(), int64(42), "metadata", "unknown")
|
||||||
unstructured.SetNestedField(original.UnstructuredContent(), map[string]interface{}{"foo": int64(42), "bar": "abc"}, "metadata", "labels")
|
unstructured.SetNestedField(original.UnstructuredContent(), map[string]interface{}{"foo": int64(42), "bar": "abc"}, "metadata", "annotations")
|
||||||
|
unstructured.SetNestedField(original.UnstructuredContent(), map[string]interface{}{"invalid": "x y"}, "metadata", "labels")
|
||||||
|
unstructured.SetNestedField(original.UnstructuredContent(), int64(42), "embedded", "metadata", "unknown")
|
||||||
|
unstructured.SetNestedField(original.UnstructuredContent(), map[string]interface{}{"foo": int64(42), "bar": "abc"}, "embedded", "metadata", "annotations")
|
||||||
|
unstructured.SetNestedField(original.UnstructuredContent(), map[string]interface{}{"invalid": "x y"}, "embedded", "metadata", "labels")
|
||||||
|
unstructured.SetNestedField(original.UnstructuredContent(), "Foo", "embedded", "kind")
|
||||||
|
unstructured.SetNestedField(original.UnstructuredContent(), "foo/v1", "embedded", "apiVersion")
|
||||||
|
|
||||||
ctx := genericapirequest.WithNamespace(genericapirequest.NewContext(), metav1.NamespaceDefault)
|
ctx := genericapirequest.WithNamespace(genericapirequest.NewContext(), metav1.NamespaceDefault)
|
||||||
key := path.Join("/", restOptions.StorageConfig.Prefix, noxuDefinition.Spec.Group, "noxus/default/foo")
|
key := path.Join("/", restOptions.StorageConfig.Prefix, noxuDefinition.Spec.Group, "noxus/default/foo")
|
||||||
@@ -140,25 +164,369 @@ func TestInvalidObjectMetaInStorage(t *testing.T) {
|
|||||||
t.Fatalf("unexpected error: %v", err)
|
t.Fatalf("unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Logf("Checking that ObjectMeta is pruned from unknown fields")
|
t.Logf("Checking that invalid objects can be deleted")
|
||||||
|
|
||||||
noxuResourceClient := newNamespacedCustomResourceClient("default", dynamicClient, noxuDefinition)
|
noxuResourceClient := newNamespacedCustomResourceClient("default", dynamicClient, noxuDefinition)
|
||||||
obj, err := noxuResourceClient.Get("foo", metav1.GetOptions{})
|
if err := noxuResourceClient.Delete("foo", &metav1.DeleteOptions{}); err != nil {
|
||||||
if err != nil {
|
t.Fatalf("Unexpected delete error %v", err)
|
||||||
|
}
|
||||||
|
if _, err := etcdclient.Put(ctx, key, string(val)); err != nil {
|
||||||
t.Fatalf("unexpected error: %v", err)
|
t.Fatalf("unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
t.Logf("Checking that ObjectMeta is pruned from unknown fields")
|
||||||
|
obj, err := noxuResourceClient.Get("foo", metav1.GetOptions{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
objJSON, _ := json.Marshal(obj.Object)
|
||||||
|
t.Logf("Got object: %v", string(objJSON))
|
||||||
|
|
||||||
if unknown, found, err := unstructured.NestedFieldNoCopy(obj.UnstructuredContent(), "metadata", "unknown"); err != nil {
|
if unknown, found, err := unstructured.NestedFieldNoCopy(obj.UnstructuredContent(), "metadata", "unknown"); err != nil {
|
||||||
t.Errorf("unexpected error: %v", err)
|
t.Errorf("Unexpected error: %v", err)
|
||||||
} else if found {
|
} else if found {
|
||||||
t.Errorf("unexpected to find metadata.unknown=%#v", unknown)
|
t.Errorf("Unexpected to find metadata.unknown=%#v", unknown)
|
||||||
|
}
|
||||||
|
if unknown, found, err := unstructured.NestedFieldNoCopy(obj.UnstructuredContent(), "embedded", "metadata", "unknown"); err != nil {
|
||||||
|
t.Errorf("Unexpected error: %v", err)
|
||||||
|
} else if found {
|
||||||
|
t.Errorf("Unexpected to find embedded.metadata.unknown=%#v", unknown)
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Logf("Checking that ObjectMeta is pruned from invalid typed fields")
|
t.Logf("Checking that ObjectMeta is pruned from wrongly-typed annotations")
|
||||||
|
|
||||||
|
if annotations, found, err := unstructured.NestedStringMap(obj.UnstructuredContent(), "metadata", "annotations"); err != nil {
|
||||||
|
t.Errorf("Unexpected error: %v", err)
|
||||||
|
} else if found {
|
||||||
|
t.Errorf("Unexpected to find metadata.annotations: %#v", annotations)
|
||||||
|
}
|
||||||
|
if annotations, found, err := unstructured.NestedStringMap(obj.UnstructuredContent(), "embedded", "metadata", "annotations"); err != nil {
|
||||||
|
t.Errorf("Unexpected error: %v", err)
|
||||||
|
} else if found {
|
||||||
|
t.Errorf("Unexpected to find embedded.metadata.annotations: %#v", annotations)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Checking that ObjectMeta still has the non-validating labels")
|
||||||
|
|
||||||
if labels, found, err := unstructured.NestedStringMap(obj.UnstructuredContent(), "metadata", "labels"); err != nil {
|
if labels, found, err := unstructured.NestedStringMap(obj.UnstructuredContent(), "metadata", "labels"); err != nil {
|
||||||
t.Errorf("unexpected error: %v", err)
|
t.Errorf("unexpected error: %v", err)
|
||||||
} else if found && !reflect.DeepEqual(labels, map[string]string{"bar": "abc"}) {
|
} else if !found {
|
||||||
t.Errorf("unexpected to find metadata.lables=%#v", labels)
|
t.Errorf("Expected to find metadata.labels, but didn't")
|
||||||
|
} else if expected := map[string]string{"invalid": "x y"}; !reflect.DeepEqual(labels, expected) {
|
||||||
|
t.Errorf("Expected metadata.labels to be %#v, got: %#v", expected, labels)
|
||||||
|
}
|
||||||
|
if labels, found, err := unstructured.NestedStringMap(obj.UnstructuredContent(), "embedded", "metadata", "labels"); err != nil {
|
||||||
|
t.Errorf("Unexpected error: %v", err)
|
||||||
|
} else if !found {
|
||||||
|
t.Errorf("Expected to find embedded.metadata.labels, but didn't")
|
||||||
|
} else if expected := map[string]string{"invalid": "x y"}; !reflect.DeepEqual(labels, expected) {
|
||||||
|
t.Errorf("Expected embedded.metadata.labels to be %#v, got: %#v", expected, labels)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Trying to fail on updating with invalid labels")
|
||||||
|
unstructured.SetNestedField(obj.Object, "changed", "metadata", "labels", "something")
|
||||||
|
if got, err := noxuResourceClient.Update(obj, metav1.UpdateOptions{}); err == nil {
|
||||||
|
objJSON, _ := json.Marshal(obj.Object)
|
||||||
|
gotJSON, _ := json.Marshal(got.Object)
|
||||||
|
t.Fatalf("Expected update error, but didn't get one\nin: %s\nresponse: %v", string(objJSON), string(gotJSON))
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Trying to fail on updating with invalid embedded label")
|
||||||
|
unstructured.SetNestedField(obj.Object, "fixed", "metadata", "labels", "invalid")
|
||||||
|
if got, err := noxuResourceClient.Update(obj, metav1.UpdateOptions{}); err == nil {
|
||||||
|
objJSON, _ := json.Marshal(obj.Object)
|
||||||
|
gotJSON, _ := json.Marshal(got.Object)
|
||||||
|
t.Fatalf("Expected update error, but didn't get one\nin: %s\nresponse: %v", string(objJSON), string(gotJSON))
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Fixed all labels and update should work")
|
||||||
|
unstructured.SetNestedField(obj.Object, "fixed", "embedded", "metadata", "labels", "invalid")
|
||||||
|
if _, err := noxuResourceClient.Update(obj, metav1.UpdateOptions{}); err != nil {
|
||||||
|
t.Errorf("Unexpected update error with fixed labels: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Trying to fail on updating with wrongly-typed embedded label")
|
||||||
|
unstructured.SetNestedField(obj.Object, int64(42), "embedded", "metadata", "labels", "invalid")
|
||||||
|
if got, err := noxuResourceClient.Update(obj, metav1.UpdateOptions{}); err == nil {
|
||||||
|
objJSON, _ := json.Marshal(obj.Object)
|
||||||
|
gotJSON, _ := json.Marshal(got.Object)
|
||||||
|
t.Fatalf("Expected update error, but didn't get one\nin: %s\nresponse: %v", string(objJSON), string(gotJSON))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var embeddedResourceFixture = &apiextensionsv1beta1.CustomResourceDefinition{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "foos.tests.apiextensions.k8s.io"},
|
||||||
|
Spec: apiextensionsv1beta1.CustomResourceDefinitionSpec{
|
||||||
|
Group: "tests.apiextensions.k8s.io",
|
||||||
|
Version: "v1beta1",
|
||||||
|
Names: apiextensionsv1beta1.CustomResourceDefinitionNames{
|
||||||
|
Plural: "foos",
|
||||||
|
Singular: "foo",
|
||||||
|
Kind: "Foo",
|
||||||
|
ListKind: "FooList",
|
||||||
|
},
|
||||||
|
Scope: apiextensionsv1beta1.ClusterScoped,
|
||||||
|
PreserveUnknownFields: pointer.BoolPtr(false),
|
||||||
|
Subresources: &apiextensionsv1beta1.CustomResourceSubresources{
|
||||||
|
Status: &apiextensionsv1beta1.CustomResourceSubresourceStatus{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
embeddedResourceSchema = `
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
embedded:
|
||||||
|
type: object
|
||||||
|
x-kubernetes-embedded-resource: true
|
||||||
|
x-kubernetes-preserve-unknown-fields: true
|
||||||
|
noEmbeddedObject:
|
||||||
|
type: object
|
||||||
|
x-kubernetes-preserve-unknown-fields: true
|
||||||
|
embeddedNested:
|
||||||
|
type: object
|
||||||
|
x-kubernetes-embedded-resource: true
|
||||||
|
x-kubernetes-preserve-unknown-fields: true
|
||||||
|
properties:
|
||||||
|
embedded:
|
||||||
|
type: object
|
||||||
|
x-kubernetes-embedded-resource: true
|
||||||
|
x-kubernetes-preserve-unknown-fields: true
|
||||||
|
defaults:
|
||||||
|
type: object
|
||||||
|
x-kubernetes-embedded-resource: true
|
||||||
|
x-kubernetes-preserve-unknown-fields: true
|
||||||
|
default:
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Pod
|
||||||
|
labels:
|
||||||
|
foo: bar
|
||||||
|
invalidDefaults:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
embedded:
|
||||||
|
type: object
|
||||||
|
x-kubernetes-embedded-resource: true
|
||||||
|
x-kubernetes-preserve-unknown-fields: true
|
||||||
|
default:
|
||||||
|
apiVersion: "foo/v1"
|
||||||
|
kind: "%"
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
foo: bar
|
||||||
|
abc: "x y"
|
||||||
|
`
|
||||||
|
|
||||||
|
embeddedResourceInstance = `
|
||||||
|
kind: Foo
|
||||||
|
apiVersion: tests.apiextensions.k8s.io/v1beta1
|
||||||
|
embedded:
|
||||||
|
apiVersion: foo/v1
|
||||||
|
kind: Foo
|
||||||
|
metadata:
|
||||||
|
name: foo
|
||||||
|
unspecified: bar
|
||||||
|
noEmbeddedObject:
|
||||||
|
apiVersion: foo/v1
|
||||||
|
kind: Foo
|
||||||
|
metadata:
|
||||||
|
name: foo
|
||||||
|
unspecified: bar
|
||||||
|
embeddedNested:
|
||||||
|
apiVersion: foo/v1
|
||||||
|
kind: Foo
|
||||||
|
metadata:
|
||||||
|
name: foo
|
||||||
|
unspecified: bar
|
||||||
|
embedded:
|
||||||
|
apiVersion: foo/v1
|
||||||
|
kind: Foo
|
||||||
|
metadata:
|
||||||
|
name: foo
|
||||||
|
unspecified: bar
|
||||||
|
`
|
||||||
|
|
||||||
|
expectedEmbeddedResourceInstance = `
|
||||||
|
kind: Foo
|
||||||
|
apiVersion: tests.apiextensions.k8s.io/v1beta1
|
||||||
|
embedded:
|
||||||
|
apiVersion: foo/v1
|
||||||
|
kind: Foo
|
||||||
|
metadata:
|
||||||
|
name: foo
|
||||||
|
noEmbeddedObject:
|
||||||
|
apiVersion: foo/v1
|
||||||
|
kind: Foo
|
||||||
|
metadata:
|
||||||
|
name: foo
|
||||||
|
unspecified: bar
|
||||||
|
embeddedNested:
|
||||||
|
apiVersion: foo/v1
|
||||||
|
kind: Foo
|
||||||
|
metadata:
|
||||||
|
name: foo
|
||||||
|
embedded:
|
||||||
|
apiVersion: foo/v1
|
||||||
|
kind: Foo
|
||||||
|
metadata:
|
||||||
|
name: foo
|
||||||
|
defaults:
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Pod
|
||||||
|
labels:
|
||||||
|
foo: bar
|
||||||
|
`
|
||||||
|
|
||||||
|
wronglyTypedEmbeddedResourceInstance = `
|
||||||
|
kind: Foo
|
||||||
|
apiVersion: tests.apiextensions.k8s.io/v1beta1
|
||||||
|
embedded:
|
||||||
|
apiVersion: foo/v1
|
||||||
|
kind: Foo
|
||||||
|
metadata:
|
||||||
|
name: instance
|
||||||
|
namespace: 42
|
||||||
|
`
|
||||||
|
|
||||||
|
invalidEmbeddedResourceInstance = `
|
||||||
|
kind: Foo
|
||||||
|
apiVersion: tests.apiextensions.k8s.io/v1beta1
|
||||||
|
embedded:
|
||||||
|
apiVersion: foo/v1
|
||||||
|
kind: "%"
|
||||||
|
metadata:
|
||||||
|
name: ..
|
||||||
|
embeddedNested:
|
||||||
|
apiVersion: foo/v1
|
||||||
|
kind: "%"
|
||||||
|
metadata:
|
||||||
|
name: ..
|
||||||
|
embedded:
|
||||||
|
apiVersion: foo/v1
|
||||||
|
kind: "%"
|
||||||
|
metadata:
|
||||||
|
name: ..
|
||||||
|
invalidDefaults: {}
|
||||||
|
`
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEmbeddedResources(t *testing.T) {
|
||||||
|
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.CustomResourceDefaulting, true)()
|
||||||
|
|
||||||
|
tearDownFn, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer tearDownFn()
|
||||||
|
|
||||||
|
crd := embeddedResourceFixture.DeepCopy()
|
||||||
|
crd.Spec.Validation = &apiextensionsv1beta1.CustomResourceValidation{}
|
||||||
|
if err := yaml.Unmarshal([]byte(embeddedResourceSchema), &crd.Spec.Validation.OpenAPIV3Schema); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
crd, err = fixtures.CreateNewCustomResourceDefinition(crd, apiExtensionClient, dynamicClient)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Creating CR and expect 'unspecified' fields to be pruned inside ObjectMetas")
|
||||||
|
fooClient := dynamicClient.Resource(schema.GroupVersionResource{crd.Spec.Group, crd.Spec.Version, crd.Spec.Names.Plural})
|
||||||
|
foo := &unstructured.Unstructured{}
|
||||||
|
if err := yaml.Unmarshal([]byte(embeddedResourceInstance), &foo.Object); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
unstructured.SetNestedField(foo.Object, "foo", "metadata", "name")
|
||||||
|
foo, err = fooClient.Create(foo, metav1.CreateOptions{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unable to create CR: %v", err)
|
||||||
|
}
|
||||||
|
t.Logf("CR created: %#v", foo.UnstructuredContent())
|
||||||
|
|
||||||
|
t.Logf("Checking that everything unknown inside ObjectMeta is gone")
|
||||||
|
delete(foo.Object, "metadata")
|
||||||
|
var expected map[string]interface{}
|
||||||
|
if err := yaml.Unmarshal([]byte(expectedEmbeddedResourceInstance), &expected); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(expected, foo.Object) {
|
||||||
|
t.Errorf("unexpected diff: %s", diff.ObjectDiff(expected, foo.Object))
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Trying to create wrongly typed CR")
|
||||||
|
wronglyTyped := &unstructured.Unstructured{}
|
||||||
|
if err := yaml.Unmarshal([]byte(wronglyTypedEmbeddedResourceInstance), &wronglyTyped.Object); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
unstructured.SetNestedField(wronglyTyped.Object, "invalid", "metadata", "name")
|
||||||
|
_, err = fooClient.Create(wronglyTyped, metav1.CreateOptions{})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("Expected creation to fail, but didn't")
|
||||||
|
}
|
||||||
|
t.Logf("Creation of wrongly typed object failed with: %v", err)
|
||||||
|
|
||||||
|
for _, s := range []string{
|
||||||
|
`embedded.metadata: Invalid value`,
|
||||||
|
} {
|
||||||
|
if !strings.Contains(err.Error(), s) {
|
||||||
|
t.Errorf("missing error: %s", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Trying to create invalid CR")
|
||||||
|
invalid := &unstructured.Unstructured{}
|
||||||
|
if err := yaml.Unmarshal([]byte(invalidEmbeddedResourceInstance), &invalid.Object); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
unstructured.SetNestedField(invalid.Object, "invalid", "metadata", "name")
|
||||||
|
unstructured.SetNestedField(invalid.Object, "x y", "metadata", "labels", "foo")
|
||||||
|
_, err = fooClient.Create(invalid, metav1.CreateOptions{})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("Expected creation to fail, but didn't")
|
||||||
|
}
|
||||||
|
t.Logf("Creation of invalid object failed with: %v", err)
|
||||||
|
|
||||||
|
invalidErrors := []string{
|
||||||
|
`[metadata.labels: Invalid value: "x y"`,
|
||||||
|
` embedded.kind: Invalid value: "%"`,
|
||||||
|
` embedded.metadata.name: Invalid value: ".."`,
|
||||||
|
` embeddedNested.kind: Invalid value: "%"`,
|
||||||
|
` embeddedNested.metadata.name: Invalid value: ".."`,
|
||||||
|
` embeddedNested.embedded.kind: Invalid value: "%"`,
|
||||||
|
` embeddedNested.embedded.metadata.name: Invalid value: ".."`,
|
||||||
|
` invalidDefaults.embedded.kind: Invalid value: "%"`,
|
||||||
|
` invalidDefaults.embedded.metadata.labels: Invalid value: "x y"`,
|
||||||
|
}
|
||||||
|
for _, s := range invalidErrors {
|
||||||
|
if !strings.Contains(err.Error(), s) {
|
||||||
|
t.Errorf("missing error: %s", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Creating a valid CR and then updating it with invalid values, expecting the same errors")
|
||||||
|
valid := &unstructured.Unstructured{}
|
||||||
|
if err := yaml.Unmarshal([]byte(embeddedResourceInstance), &valid.Object); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
unstructured.SetNestedField(valid.Object, "valid", "metadata", "name")
|
||||||
|
valid, err = fooClient.Create(valid, metav1.CreateOptions{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unable to create CR: %v", err)
|
||||||
|
}
|
||||||
|
for k, v := range invalid.Object {
|
||||||
|
if k == "metadata" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
valid.Object[k] = v
|
||||||
|
}
|
||||||
|
unstructured.SetNestedField(valid.Object, "x y", "metadata", "labels", "foo")
|
||||||
|
if _, err = fooClient.Update(valid, metav1.UpdateOptions{}); err == nil {
|
||||||
|
t.Fatal("Expected update error, but got none")
|
||||||
|
}
|
||||||
|
t.Logf("Update failed with: %v", err)
|
||||||
|
for _, s := range invalidErrors {
|
||||||
|
if !strings.Contains(err.Error(), s) {
|
||||||
|
t.Errorf("missing error: %s", s)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -18,17 +18,21 @@ package integration
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"path"
|
"path"
|
||||||
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/coreos/etcd/clientv3"
|
"github.com/coreos/etcd/clientv3"
|
||||||
"github.com/coreos/etcd/pkg/transport"
|
"github.com/coreos/etcd/pkg/transport"
|
||||||
|
|
||||||
"sigs.k8s.io/yaml"
|
"sigs.k8s.io/yaml"
|
||||||
|
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
types "k8s.io/apimachinery/pkg/types"
|
types "k8s.io/apimachinery/pkg/types"
|
||||||
|
"k8s.io/apimachinery/pkg/util/diff"
|
||||||
"k8s.io/apimachinery/pkg/util/json"
|
"k8s.io/apimachinery/pkg/util/json"
|
||||||
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
|
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
|
||||||
"k8s.io/client-go/dynamic"
|
"k8s.io/client-go/dynamic"
|
||||||
@@ -107,6 +111,65 @@ properties:
|
|||||||
x-kubernetes-preserve-unknown-fields: true
|
x-kubernetes-preserve-unknown-fields: true
|
||||||
`
|
`
|
||||||
|
|
||||||
|
fooSchemaEmbeddedResource = `
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
embeddedPruning:
|
||||||
|
type: object
|
||||||
|
x-kubernetes-embedded-resource: true
|
||||||
|
properties:
|
||||||
|
specified:
|
||||||
|
type: string
|
||||||
|
embeddedPreserving:
|
||||||
|
type: object
|
||||||
|
x-kubernetes-embedded-resource: true
|
||||||
|
x-kubernetes-preserve-unknown-fields: true
|
||||||
|
embeddedNested:
|
||||||
|
type: object
|
||||||
|
x-kubernetes-embedded-resource: true
|
||||||
|
x-kubernetes-preserve-unknown-fields: true
|
||||||
|
properties:
|
||||||
|
embeddedPruning:
|
||||||
|
type: object
|
||||||
|
x-kubernetes-embedded-resource: true
|
||||||
|
properties:
|
||||||
|
specified:
|
||||||
|
type: string
|
||||||
|
`
|
||||||
|
|
||||||
|
fooSchemaEmbeddedResourceInstance = fooInstance + `
|
||||||
|
embeddedPruning:
|
||||||
|
apiVersion: foo/v1
|
||||||
|
kind: Foo
|
||||||
|
metadata:
|
||||||
|
name: foo
|
||||||
|
unspecified: bar
|
||||||
|
unspecified: bar
|
||||||
|
specified: bar
|
||||||
|
embeddedPreserving:
|
||||||
|
apiVersion: foo/v1
|
||||||
|
kind: Foo
|
||||||
|
metadata:
|
||||||
|
name: foo
|
||||||
|
unspecified: bar
|
||||||
|
unspecified: bar
|
||||||
|
embeddedNested:
|
||||||
|
apiVersion: foo/v1
|
||||||
|
kind: Foo
|
||||||
|
metadata:
|
||||||
|
name: foo
|
||||||
|
unspecified: bar
|
||||||
|
unspecified: bar
|
||||||
|
embeddedPruning:
|
||||||
|
apiVersion: foo/v1
|
||||||
|
kind: Foo
|
||||||
|
metadata:
|
||||||
|
name: foo
|
||||||
|
unspecified: bar
|
||||||
|
unspecified: bar
|
||||||
|
specified: bar
|
||||||
|
`
|
||||||
|
|
||||||
fooInstance = `
|
fooInstance = `
|
||||||
kind: Foo
|
kind: Foo
|
||||||
apiVersion: tests.apiextensions.k8s.io/v1beta1
|
apiVersion: tests.apiextensions.k8s.io/v1beta1
|
||||||
@@ -457,3 +520,72 @@ func TestPruningCreatePreservingUnknownFields(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPruningEmbeddedResources(t *testing.T) {
|
||||||
|
tearDownFn, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer tearDownFn()
|
||||||
|
|
||||||
|
crd := pruningFixture.DeepCopy()
|
||||||
|
crd.Spec.Validation = &apiextensionsv1beta1.CustomResourceValidation{}
|
||||||
|
if err := yaml.Unmarshal([]byte(fooSchemaEmbeddedResource), &crd.Spec.Validation.OpenAPIV3Schema); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
crd, err = fixtures.CreateNewCustomResourceDefinition(crd, apiExtensionClient, dynamicClient)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Creating CR and expect 'unspecified' field to be pruned")
|
||||||
|
fooClient := dynamicClient.Resource(schema.GroupVersionResource{crd.Spec.Group, crd.Spec.Version, crd.Spec.Names.Plural})
|
||||||
|
foo := &unstructured.Unstructured{}
|
||||||
|
if err := yaml.Unmarshal([]byte(fooSchemaEmbeddedResourceInstance), &foo.Object); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
foo, err = fooClient.Create(foo, metav1.CreateOptions{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unable to create CR: %v", err)
|
||||||
|
}
|
||||||
|
t.Logf("CR created: %#v", foo.UnstructuredContent())
|
||||||
|
|
||||||
|
t.Logf("Comparing with expected, pruned value")
|
||||||
|
x := runtime.DeepCopyJSON(foo.Object)
|
||||||
|
delete(x, "apiVersion")
|
||||||
|
delete(x, "kind")
|
||||||
|
delete(x, "metadata")
|
||||||
|
var expected map[string]interface{}
|
||||||
|
if err := yaml.Unmarshal([]byte(`
|
||||||
|
embeddedPruning:
|
||||||
|
apiVersion: foo/v1
|
||||||
|
kind: Foo
|
||||||
|
metadata:
|
||||||
|
name: foo
|
||||||
|
specified: bar
|
||||||
|
embeddedPreserving:
|
||||||
|
apiVersion: foo/v1
|
||||||
|
kind: Foo
|
||||||
|
metadata:
|
||||||
|
name: foo
|
||||||
|
unspecified: bar
|
||||||
|
embeddedNested:
|
||||||
|
apiVersion: foo/v1
|
||||||
|
kind: Foo
|
||||||
|
metadata:
|
||||||
|
name: foo
|
||||||
|
embeddedPruning:
|
||||||
|
apiVersion: foo/v1
|
||||||
|
kind: Foo
|
||||||
|
metadata:
|
||||||
|
name: foo
|
||||||
|
specified: bar
|
||||||
|
unspecified: bar
|
||||||
|
`), &expected); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(expected, x) {
|
||||||
|
t.Errorf("unexpected diff: %s", diff.ObjectDiff(expected, x))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -705,19 +705,20 @@ spec:
|
|||||||
type Test struct {
|
type Test struct {
|
||||||
desc string
|
desc string
|
||||||
globalSchema, v1Schema, v1beta1Schema string
|
globalSchema, v1Schema, v1beta1Schema string
|
||||||
expectedCreateError bool
|
expectedCreateErrors []string
|
||||||
|
unexpectedCreateErrors []string
|
||||||
expectedViolations []string
|
expectedViolations []string
|
||||||
unexpectedViolations []string
|
unexpectedViolations []string
|
||||||
}
|
}
|
||||||
tests := []Test{
|
tests := []Test{
|
||||||
{"empty", "", "", "", false, nil, nil},
|
{"empty", "", "", "", nil, nil, nil, nil},
|
||||||
{
|
{
|
||||||
desc: "int-or-string and preserve-unknown-fields true",
|
desc: "int-or-string and preserve-unknown-fields true",
|
||||||
globalSchema: `
|
globalSchema: `
|
||||||
x-kubernetes-preserve-unknown-fields: true
|
x-kubernetes-preserve-unknown-fields: true
|
||||||
x-kubernetes-int-or-string: true
|
x-kubernetes-int-or-string: true
|
||||||
`,
|
`,
|
||||||
expectedViolations: []string{
|
expectedCreateErrors: []string{
|
||||||
"spec.validation.openAPIV3Schema.x-kubernetes-preserve-unknown-fields: Invalid value: true: must be false if x-kubernetes-int-or-string is true",
|
"spec.validation.openAPIV3Schema.x-kubernetes-preserve-unknown-fields: Invalid value: true: must be false if x-kubernetes-int-or-string is true",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -728,7 +729,7 @@ type: object
|
|||||||
x-kubernetes-embedded-resource: true
|
x-kubernetes-embedded-resource: true
|
||||||
x-kubernetes-int-or-string: true
|
x-kubernetes-int-or-string: true
|
||||||
`,
|
`,
|
||||||
expectedViolations: []string{
|
expectedCreateErrors: []string{
|
||||||
"spec.validation.openAPIV3Schema.x-kubernetes-embedded-resource: Invalid value: true: must be false if x-kubernetes-int-or-string is true",
|
"spec.validation.openAPIV3Schema.x-kubernetes-embedded-resource: Invalid value: true: must be false if x-kubernetes-int-or-string is true",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -738,7 +739,7 @@ x-kubernetes-int-or-string: true
|
|||||||
type: object
|
type: object
|
||||||
x-kubernetes-embedded-resource: true
|
x-kubernetes-embedded-resource: true
|
||||||
`,
|
`,
|
||||||
expectedViolations: []string{
|
expectedCreateErrors: []string{
|
||||||
"spec.validation.openAPIV3Schema.properties: Required value: must not be empty if x-kubernetes-embedded-resource is true without x-kubernetes-preserve-unknown-fields",
|
"spec.validation.openAPIV3Schema.properties: Required value: must not be empty if x-kubernetes-embedded-resource is true without x-kubernetes-preserve-unknown-fields",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -773,7 +774,7 @@ type: array
|
|||||||
x-kubernetes-embedded-resource: true
|
x-kubernetes-embedded-resource: true
|
||||||
x-kubernetes-preserve-unknown-fields: true
|
x-kubernetes-preserve-unknown-fields: true
|
||||||
`,
|
`,
|
||||||
expectedViolations: []string{
|
expectedCreateErrors: []string{
|
||||||
"spec.validation.openAPIV3Schema.type: Invalid value: \"array\": must be object if x-kubernetes-embedded-resource is true",
|
"spec.validation.openAPIV3Schema.type: Invalid value: \"array\": must be object if x-kubernetes-embedded-resource is true",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -784,7 +785,7 @@ type: ""
|
|||||||
x-kubernetes-embedded-resource: true
|
x-kubernetes-embedded-resource: true
|
||||||
x-kubernetes-preserve-unknown-fields: true
|
x-kubernetes-preserve-unknown-fields: true
|
||||||
`,
|
`,
|
||||||
expectedViolations: []string{
|
expectedCreateErrors: []string{
|
||||||
"spec.validation.openAPIV3Schema.type: Required value: must be object if x-kubernetes-embedded-resource is true",
|
"spec.validation.openAPIV3Schema.type: Required value: must be object if x-kubernetes-embedded-resource is true",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -923,7 +924,7 @@ oneOf:
|
|||||||
x-kubernetes-embedded-resource: true
|
x-kubernetes-embedded-resource: true
|
||||||
x-kubernetes-preserve-unknown-fields: true
|
x-kubernetes-preserve-unknown-fields: true
|
||||||
`,
|
`,
|
||||||
expectedViolations: []string{
|
expectedCreateErrors: []string{
|
||||||
"spec.validation.openAPIV3Schema.allOf[0].properties[embedded-resource].x-kubernetes-preserve-unknown-fields: Forbidden: must be false to be structural",
|
"spec.validation.openAPIV3Schema.allOf[0].properties[embedded-resource].x-kubernetes-preserve-unknown-fields: Forbidden: must be false to be structural",
|
||||||
"spec.validation.openAPIV3Schema.allOf[0].properties[embedded-resource].x-kubernetes-embedded-resource: Forbidden: must be false to be structural",
|
"spec.validation.openAPIV3Schema.allOf[0].properties[embedded-resource].x-kubernetes-embedded-resource: Forbidden: must be false to be structural",
|
||||||
"spec.validation.openAPIV3Schema.allOf[0].properties[int-or-string].x-kubernetes-int-or-string: Forbidden: must be false to be structural",
|
"spec.validation.openAPIV3Schema.allOf[0].properties[int-or-string].x-kubernetes-int-or-string: Forbidden: must be false to be structural",
|
||||||
@@ -939,7 +940,7 @@ oneOf:
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "missing types",
|
desc: "missing types with extensions",
|
||||||
globalSchema: `
|
globalSchema: `
|
||||||
properties:
|
properties:
|
||||||
foo:
|
foo:
|
||||||
@@ -967,7 +968,7 @@ properties:
|
|||||||
properties:
|
properties:
|
||||||
a: {}
|
a: {}
|
||||||
`,
|
`,
|
||||||
expectedViolations: []string{
|
expectedCreateErrors: []string{
|
||||||
"spec.validation.openAPIV3Schema.properties[foo].properties[a].type: Required value: must not be empty for specified object fields",
|
"spec.validation.openAPIV3Schema.properties[foo].properties[a].type: Required value: must not be empty for specified object fields",
|
||||||
"spec.validation.openAPIV3Schema.properties[foo].type: Required value: must not be empty for specified object fields",
|
"spec.validation.openAPIV3Schema.properties[foo].type: Required value: must not be empty for specified object fields",
|
||||||
"spec.validation.openAPIV3Schema.properties[int-or-string].properties[a].type: Required value: must not be empty for specified object fields",
|
"spec.validation.openAPIV3Schema.properties[int-or-string].properties[a].type: Required value: must not be empty for specified object fields",
|
||||||
@@ -985,6 +986,43 @@ properties:
|
|||||||
"spec.validation.openAPIV3Schema.type: Required value: must not be empty at the root",
|
"spec.validation.openAPIV3Schema.type: Required value: must not be empty at the root",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
desc: "missing types without extensions",
|
||||||
|
globalSchema: `
|
||||||
|
properties:
|
||||||
|
foo:
|
||||||
|
properties:
|
||||||
|
a: {}
|
||||||
|
bar:
|
||||||
|
items:
|
||||||
|
additionalProperties:
|
||||||
|
properties:
|
||||||
|
a: {}
|
||||||
|
items: {}
|
||||||
|
abc:
|
||||||
|
additionalProperties:
|
||||||
|
properties:
|
||||||
|
a:
|
||||||
|
items:
|
||||||
|
additionalProperties:
|
||||||
|
items:
|
||||||
|
`,
|
||||||
|
expectedViolations: []string{
|
||||||
|
"spec.validation.openAPIV3Schema.properties[foo].properties[a].type: Required value: must not be empty for specified object fields",
|
||||||
|
"spec.validation.openAPIV3Schema.properties[foo].type: Required value: must not be empty for specified object fields",
|
||||||
|
"spec.validation.openAPIV3Schema.properties[abc].additionalProperties.properties[a].items.additionalProperties.type: Required value: must not be empty for specified object fields",
|
||||||
|
"spec.validation.openAPIV3Schema.properties[abc].additionalProperties.properties[a].items.type: Required value: must not be empty for specified array items",
|
||||||
|
"spec.validation.openAPIV3Schema.properties[abc].additionalProperties.properties[a].type: Required value: must not be empty for specified object fields",
|
||||||
|
"spec.validation.openAPIV3Schema.properties[abc].additionalProperties.type: Required value: must not be empty for specified object fields",
|
||||||
|
"spec.validation.openAPIV3Schema.properties[abc].type: Required value: must not be empty for specified object fields",
|
||||||
|
"spec.validation.openAPIV3Schema.properties[bar].items.additionalProperties.items.type: Required value: must not be empty for specified array items",
|
||||||
|
"spec.validation.openAPIV3Schema.properties[bar].items.additionalProperties.properties[a].type: Required value: must not be empty for specified object fields",
|
||||||
|
"spec.validation.openAPIV3Schema.properties[bar].items.additionalProperties.type: Required value: must not be empty for specified object fields",
|
||||||
|
"spec.validation.openAPIV3Schema.properties[bar].items.type: Required value: must not be empty for specified array items",
|
||||||
|
"spec.validation.openAPIV3Schema.properties[bar].type: Required value: must not be empty for specified object fields",
|
||||||
|
"spec.validation.openAPIV3Schema.type: Required value: must not be empty at the root",
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
desc: "int-or-string variants",
|
desc: "int-or-string variants",
|
||||||
globalSchema: `
|
globalSchema: `
|
||||||
@@ -1033,7 +1071,7 @@ properties:
|
|||||||
- type: string
|
- type: string
|
||||||
- type: integer
|
- type: integer
|
||||||
`,
|
`,
|
||||||
expectedViolations: []string{
|
expectedCreateErrors: []string{
|
||||||
"spec.validation.openAPIV3Schema.properties[d].anyOf[0].type: Forbidden: must be empty to be structural",
|
"spec.validation.openAPIV3Schema.properties[d].anyOf[0].type: Forbidden: must be empty to be structural",
|
||||||
"spec.validation.openAPIV3Schema.properties[d].anyOf[1].type: Forbidden: must be empty to be structural",
|
"spec.validation.openAPIV3Schema.properties[d].anyOf[1].type: Forbidden: must be empty to be structural",
|
||||||
"spec.validation.openAPIV3Schema.properties[e].allOf[0].anyOf[0].type: Forbidden: must be empty to be structural",
|
"spec.validation.openAPIV3Schema.properties[e].allOf[0].anyOf[0].type: Forbidden: must be empty to be structural",
|
||||||
@@ -1043,7 +1081,7 @@ properties:
|
|||||||
"spec.validation.openAPIV3Schema.properties[g].anyOf[0].type: Forbidden: must be empty to be structural",
|
"spec.validation.openAPIV3Schema.properties[g].anyOf[0].type: Forbidden: must be empty to be structural",
|
||||||
"spec.validation.openAPIV3Schema.properties[g].anyOf[1].type: Forbidden: must be empty to be structural",
|
"spec.validation.openAPIV3Schema.properties[g].anyOf[1].type: Forbidden: must be empty to be structural",
|
||||||
},
|
},
|
||||||
unexpectedViolations: []string{
|
unexpectedCreateErrors: []string{
|
||||||
"spec.validation.openAPIV3Schema.properties[a]",
|
"spec.validation.openAPIV3Schema.properties[a]",
|
||||||
"spec.validation.openAPIV3Schema.properties[b]",
|
"spec.validation.openAPIV3Schema.properties[b]",
|
||||||
"spec.validation.openAPIV3Schema.properties[c]",
|
"spec.validation.openAPIV3Schema.properties[c]",
|
||||||
@@ -1354,7 +1392,7 @@ properties:
|
|||||||
- type: string
|
- type: string
|
||||||
- type: integer
|
- type: integer
|
||||||
`,
|
`,
|
||||||
expectedCreateError: true,
|
expectedCreateErrors: []string{"spec.validation.openAPIV3Schema.properties[slice].items: Forbidden: items must be a schema object and not an array"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "items slice in value validation",
|
desc: "items slice in value validation",
|
||||||
@@ -1369,7 +1407,7 @@ properties:
|
|||||||
items:
|
items:
|
||||||
- type: string
|
- type: string
|
||||||
`,
|
`,
|
||||||
expectedCreateError: true,
|
expectedCreateErrors: []string{"spec.validation.openAPIV3Schema.properties[slice].not.items: Forbidden: items must be a schema object and not an array"},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1394,10 +1432,21 @@ properties:
|
|||||||
|
|
||||||
// create CRDs
|
// create CRDs
|
||||||
crd, err = apiExtensionClient.ApiextensionsV1beta1().CustomResourceDefinitions().Create(crd)
|
crd, err = apiExtensionClient.ApiextensionsV1beta1().CustomResourceDefinitions().Create(crd)
|
||||||
if tst.expectedCreateError && err == nil {
|
if len(tst.expectedCreateErrors) > 0 && err == nil {
|
||||||
t.Fatalf("expected error, got none")
|
t.Fatalf("expected create errors, got none")
|
||||||
} else if !tst.expectedCreateError && err != nil {
|
} else if len(tst.expectedCreateErrors) == 0 && err != nil {
|
||||||
t.Fatalf("unexpected create error: %v", err)
|
t.Fatalf("unexpected create error: %v", err)
|
||||||
|
} else if err != nil {
|
||||||
|
for _, expectedErr := range tst.expectedCreateErrors {
|
||||||
|
if !strings.Contains(err.Error(), expectedErr) {
|
||||||
|
t.Errorf("expected error containing '%s', got '%s'", expectedErr, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, unexpectedErr := range tst.unexpectedCreateErrors {
|
||||||
|
if strings.Contains(err.Error(), unexpectedErr) {
|
||||||
|
t.Errorf("unexpected error containing '%s': '%s'", unexpectedErr, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
|
1
vendor/modules.txt
vendored
1
vendor/modules.txt
vendored
@@ -1075,6 +1075,7 @@ k8s.io/apiextensions-apiserver/pkg/apiserver
|
|||||||
k8s.io/apiextensions-apiserver/pkg/apiserver/conversion
|
k8s.io/apiextensions-apiserver/pkg/apiserver/conversion
|
||||||
k8s.io/apiextensions-apiserver/pkg/apiserver/schema
|
k8s.io/apiextensions-apiserver/pkg/apiserver/schema
|
||||||
k8s.io/apiextensions-apiserver/pkg/apiserver/schema/defaulting
|
k8s.io/apiextensions-apiserver/pkg/apiserver/schema/defaulting
|
||||||
|
k8s.io/apiextensions-apiserver/pkg/apiserver/schema/objectmeta
|
||||||
k8s.io/apiextensions-apiserver/pkg/apiserver/schema/pruning
|
k8s.io/apiextensions-apiserver/pkg/apiserver/schema/pruning
|
||||||
k8s.io/apiextensions-apiserver/pkg/apiserver/validation
|
k8s.io/apiextensions-apiserver/pkg/apiserver/validation
|
||||||
k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset
|
k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset
|
||||||
|
Reference in New Issue
Block a user