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/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/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/validation:go_default_library",
|
||||
"//staging/src/k8s.io/apiextensions-apiserver/pkg/features:go_default_library",
|
||||
|
@@ -23,6 +23,7 @@ import (
|
||||
|
||||
"github.com/go-openapi/strfmt"
|
||||
govalidate "github.com/go-openapi/validate"
|
||||
schemaobjectmeta "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/objectmeta"
|
||||
|
||||
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
||||
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"))
|
||||
}
|
||||
}
|
||||
if specHasKubernetesExtensions(spec) {
|
||||
mustBeStructural = true
|
||||
}
|
||||
|
||||
storageFlagCount := 0
|
||||
versionsMap := map[string]bool{}
|
||||
@@ -562,6 +566,11 @@ func ValidateCustomResourceColumnDefinition(col *apiextensions.CustomResourceCol
|
||||
// specStandardValidator applies validations for different OpenAPI specification versions.
|
||||
type specStandardValidator interface {
|
||||
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
|
||||
@@ -608,7 +617,7 @@ func ValidateCustomResourceDefinitionValidation(customResourceValidation *apiext
|
||||
openAPIV3Schema := &specStandardValidatorV3{
|
||||
allowDefaults: allowDefaults,
|
||||
}
|
||||
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(schema, fldPath.Child("openAPIV3Schema"), openAPIV3Schema)...)
|
||||
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(schema, fldPath.Child("openAPIV3Schema"), openAPIV3Schema, true)...)
|
||||
|
||||
if mustBeStructural {
|
||||
if ss, err := structuralschema.NewStructural(schema); err != nil {
|
||||
@@ -631,8 +640,10 @@ func ValidateCustomResourceDefinitionValidation(customResourceValidation *apiext
|
||||
return allErrs
|
||||
}
|
||||
|
||||
var metaFields = sets.NewString("metadata", "apiVersion", "kind")
|
||||
|
||||
// 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{}
|
||||
|
||||
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, 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 {
|
||||
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 {
|
||||
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)...)
|
||||
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(schema.Not, fldPath.Child("not"), ssv, false)...)
|
||||
|
||||
if len(schema.AllOf) != 0 {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
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.
|
||||
@@ -747,20 +782,37 @@ func (v *specStandardValidatorV3) validate(schema *apiextensions.JSONSchemaProps
|
||||
if v.allowDefaults {
|
||||
if s, err := structuralschema.NewStructural(schema); err == nil {
|
||||
// ignore errors here locally. They will show up for the root of the schema.
|
||||
pruned := runtime.DeepCopyJSONValue(*schema.Default)
|
||||
pruning.Prune(pruned, s)
|
||||
if !reflect.DeepEqual(pruned, *schema.Default) {
|
||||
allErrs = append(allErrs, field.Invalid(fldPath.Child("default"), schema.Default, "must not have unspecified fields"))
|
||||
|
||||
clone := runtime.DeepCopyJSONValue(interface{}(*schema.Default))
|
||||
if !v.isInsideResourceMeta || s.XEmbeddedResource {
|
||||
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)
|
||||
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)))
|
||||
}
|
||||
}
|
||||
} 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"))
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -953,3 +1009,86 @@ func schemaHasDefaults(s *apiextensions.JSONSchemaProps) bool {
|
||||
|
||||
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{
|
||||
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
||||
Type: "object",
|
||||
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
|
||||
},
|
||||
},
|
||||
{
|
||||
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",
|
||||
resource: &apiextensions.CustomResourceDefinition{
|
||||
@@ -1679,6 +1854,63 @@ func TestValidateCustomResourceDefinition(t *testing.T) {
|
||||
},
|
||||
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.
|
||||
invalid("spec", "validation", "openAPIV3Schema", "properties[e]", "properties[preserveUnknownFields]", "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},
|
||||
@@ -1738,6 +1971,412 @@ func TestValidateCustomResourceDefinition(t *testing.T) {
|
||||
},
|
||||
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 {
|
||||
|
@@ -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/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/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/validation:go_default_library",
|
||||
"//staging/src/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset:go_default_library",
|
||||
@@ -92,26 +93,12 @@ go_library(
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = [
|
||||
"customresource_handler_test.go",
|
||||
"jsonpath_test.go",
|
||||
],
|
||||
srcs = ["customresource_handler_test.go"],
|
||||
embed = [":go_default_library"],
|
||||
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/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/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/strfmt"
|
||||
"github.com/go-openapi/validate"
|
||||
|
||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
||||
"k8s.io/apiextensions-apiserver/pkg/apiserver/conversion"
|
||||
structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
|
||||
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"
|
||||
apiservervalidation "k8s.io/apiextensions-apiserver/pkg/apiserver/validation"
|
||||
informers "k8s.io/apiextensions-apiserver/pkg/client/informers/internalversion/apiextensions/internalversion"
|
||||
@@ -603,6 +605,7 @@ func (r *crdHandler) getOrCreateServingInfoFor(crd *apiextensions.CustomResource
|
||||
kind,
|
||||
validator,
|
||||
statusValidator,
|
||||
structuralSchemas,
|
||||
statusSpec,
|
||||
scaleSpec,
|
||||
),
|
||||
@@ -1022,7 +1025,7 @@ func (v *unstructuredSchemaCoercer) apply(u *unstructured.Unstructured) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
objectMeta, foundObjectMeta, err := getObjectMeta(u, v.dropInvalidMetadata)
|
||||
objectMeta, foundObjectMeta, err := schemaobjectmeta.GetObjectMeta(u.Object, v.dropInvalidMetadata)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1032,8 +1035,14 @@ func (v *unstructuredSchemaCoercer) apply(u *unstructured.Unstructured) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !v.preserveUnknownFields && gv.Group == v.structuralSchemaGK.Group && kind == v.structuralSchemaGK.Kind {
|
||||
structuralpruning.Prune(u.Object, v.structuralSchemas[gv.Version])
|
||||
if gv.Group == v.structuralSchemaGK.Group && kind == v.structuralSchemaGK.Kind {
|
||||
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
|
||||
@@ -1044,72 +1053,10 @@ func (v *unstructuredSchemaCoercer) apply(u *unstructured.Unstructured) error {
|
||||
u.SetAPIVersion(apiVersion)
|
||||
}
|
||||
if foundObjectMeta {
|
||||
if err := setObjectMeta(u, objectMeta); err != nil {
|
||||
if err := schemaobjectmeta.SetObjectMeta(u.Object, objectMeta); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
||||
"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/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) {
|
||||
@@ -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 = [
|
||||
":package-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",
|
||||
],
|
||||
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.
|
||||
*/
|
||||
|
||||
package apiserver
|
||||
package objectmeta
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -28,10 +28,10 @@ type (
|
||||
index *int
|
||||
field string
|
||||
}
|
||||
JsonPath []jsonPathNode
|
||||
JSONPath []jsonPathNode
|
||||
)
|
||||
|
||||
func (p JsonPath) String() string {
|
||||
func (p JSONPath) String() string {
|
||||
var buf bytes.Buffer
|
||||
for _, n := range p {
|
||||
if n.index == nil {
|
||||
@@ -43,8 +43,8 @@ func (p JsonPath) String() string {
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func jsonPaths(base JsonPath, j map[string]interface{}) []JsonPath {
|
||||
res := make([]JsonPath, 0, len(j))
|
||||
func jsonPaths(base JSONPath, j map[string]interface{}) []JSONPath {
|
||||
res := make([]JSONPath, 0, len(j))
|
||||
for k, old := range j {
|
||||
kPth := append(append([]jsonPathNode(nil), base...), jsonPathNode{field: k})
|
||||
res = append(res, kPth)
|
||||
@@ -59,8 +59,8 @@ func jsonPaths(base JsonPath, j map[string]interface{}) []JsonPath {
|
||||
return res
|
||||
}
|
||||
|
||||
func jsonIterSlice(base JsonPath, j []interface{}) []JsonPath {
|
||||
res := make([]JsonPath, 0, len(j))
|
||||
func jsonIterSlice(base JSONPath, j []interface{}) []JSONPath {
|
||||
res := make([]JSONPath, 0, len(j))
|
||||
for i, old := range j {
|
||||
index := i
|
||||
iPth := append(append([]jsonPathNode(nil), base...), jsonPathNode{index: &index})
|
||||
@@ -76,7 +76,7 @@ func jsonIterSlice(base JsonPath, j []interface{}) []JsonPath {
|
||||
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 {
|
||||
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) {
|
||||
case map[string]interface{}:
|
||||
return JsonPathValue(field, pth, base+1)
|
||||
return JSONPathValue(field, pth, base+1)
|
||||
case []interface{}:
|
||||
return jsonPathValueSlice(field, pth, base+1)
|
||||
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 {
|
||||
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) {
|
||||
case map[string]interface{}:
|
||||
return JsonPathValue(item, pth, base+1)
|
||||
return JSONPathValue(item, pth, base+1)
|
||||
case []interface{}:
|
||||
return jsonPathValueSlice(item, pth, base+1)
|
||||
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 {
|
||||
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) {
|
||||
case map[string]interface{}:
|
||||
return SetJsonPath(field, pth, base+1, value)
|
||||
return SetJSONPath(field, pth, base+1, value)
|
||||
case []interface{}:
|
||||
return setJsonPathSlice(field, pth, base+1, value)
|
||||
return setJSONPathSlice(field, pth, base+1, value)
|
||||
default:
|
||||
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 {
|
||||
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) {
|
||||
case map[string]interface{}:
|
||||
return SetJsonPath(item, pth, base+1, value)
|
||||
return SetJSONPath(item, pth, base+1, value)
|
||||
case []interface{}:
|
||||
return setJsonPathSlice(item, pth, base+1, value)
|
||||
return setJSONPathSlice(item, pth, base+1, value)
|
||||
default:
|
||||
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 {
|
||||
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) {
|
||||
case map[string]interface{}:
|
||||
return DeleteJsonPath(field, pth, base+1)
|
||||
return DeleteJSONPath(field, pth, base+1)
|
||||
case []interface{}:
|
||||
if len(pth) == base+2 {
|
||||
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:]...)
|
||||
return nil
|
||||
}
|
||||
return deleteJsonPathSlice(field, pth, base+1)
|
||||
return deleteJSONPathSlice(field, pth, base+1)
|
||||
default:
|
||||
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 {
|
||||
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) {
|
||||
case map[string]interface{}:
|
||||
return DeleteJsonPath(item, pth, base+1)
|
||||
return DeleteJSONPath(item, pth, base+1)
|
||||
case []interface{}:
|
||||
if len(pth) == base+2 {
|
||||
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:])
|
||||
return nil
|
||||
}
|
||||
return deleteJsonPathSlice(item, pth, base+1)
|
||||
return deleteJSONPathSlice(item, pth, base+1)
|
||||
default:
|
||||
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 = [
|
||||
"//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/util/diff: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"
|
||||
)
|
||||
|
||||
// Prune removes object fields in obj which are not specified in s.
|
||||
func Prune(obj interface{}, s *structuralschema.Structural) {
|
||||
// Prune removes object fields in obj which are not specified in s. It skips TypeMeta and ObjectMeta fields
|
||||
// if XEmbeddedResource is set to true, or for the root if root=true.
|
||||
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)
|
||||
}
|
||||
|
||||
var metaFields = map[string]bool{
|
||||
"apiVersion": true,
|
||||
"kind": true,
|
||||
"metadata": true,
|
||||
}
|
||||
|
||||
func prune(x interface{}, s *structuralschema.Structural) {
|
||||
if s != nil && s.XPreserveUnknownFields {
|
||||
skipPrune(x, s)
|
||||
@@ -40,6 +55,9 @@ func prune(x interface{}, s *structuralschema.Structural) {
|
||||
return
|
||||
}
|
||||
for k, v := range x {
|
||||
if s.XEmbeddedResource && metaFields[k] {
|
||||
continue
|
||||
}
|
||||
prop, ok := s.Properties[k]
|
||||
if ok {
|
||||
prune(v, &prop)
|
||||
@@ -72,10 +90,13 @@ func skipPrune(x interface{}, s *structuralschema.Structural) {
|
||||
switch x := x.(type) {
|
||||
case map[string]interface{}:
|
||||
for k, v := range x {
|
||||
if s.XEmbeddedResource && metaFields[k] {
|
||||
continue
|
||||
}
|
||||
if prop, ok := s.Properties[k]; ok {
|
||||
prune(v, &prop)
|
||||
} else {
|
||||
skipPrune(v, nil)
|
||||
} else if s.AdditionalProperties != nil {
|
||||
prune(v, s.AdditionalProperties.Structural)
|
||||
}
|
||||
}
|
||||
case []interface{}:
|
||||
|
@@ -23,31 +23,33 @@ import (
|
||||
|
||||
structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/diff"
|
||||
"k8s.io/apimachinery/pkg/util/json"
|
||||
)
|
||||
|
||||
func TestPrune(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
json string
|
||||
schema *structuralschema.Structural
|
||||
expected string
|
||||
name string
|
||||
json string
|
||||
dontPruneMetaAtRoot bool
|
||||
schema *structuralschema.Structural
|
||||
expected string
|
||||
}{
|
||||
{"empty", "null", nil, "null"},
|
||||
{"scalar", "4", &structuralschema.Structural{}, "4"},
|
||||
{"scalar array", "[1,2]", &structuralschema.Structural{
|
||||
{name: "empty", json: "null", expected: "null"},
|
||||
{name: "scalar", json: "4", schema: &structuralschema.Structural{}, expected: "4"},
|
||||
{name: "scalar array", json: "[1,2]", schema: &structuralschema.Structural{
|
||||
Items: &structuralschema.Structural{},
|
||||
}, "[1,2]"},
|
||||
{"object array", `[{"a":1},{"b":1},{"a":1,"b":2,"c":3}]`, &structuralschema.Structural{
|
||||
}, expected: "[1,2]"},
|
||||
{name: "object array", json: `[{"a":1},{"b":1},{"a":1,"b":2,"c":3}]`, schema: &structuralschema.Structural{
|
||||
Items: &structuralschema.Structural{
|
||||
Properties: map[string]structuralschema.Structural{
|
||||
"a": {},
|
||||
"c": {},
|
||||
},
|
||||
},
|
||||
}, `[{"a":1},{},{"a":1,"c":3}]`},
|
||||
{"object array with nil schema", `[{"a":1},{"b":1},{"a":1,"b":2,"c":3}]`, nil, `[{},{},{}]`},
|
||||
{"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{
|
||||
}, expected: `[{"a":1},{},{"a":1,"c":3}]`},
|
||||
{name: "object array with nil schema", json: `[{"a":1},{"b":1},{"a":1,"b":2,"c":3}]`, expected: `[{},{},{}]`},
|
||||
{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{
|
||||
"array": {
|
||||
Items: &structuralschema.Structural{
|
||||
@@ -64,8 +66,8 @@ func TestPrune(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
}, `{"array":[{"a":1},{},{"a":1,"c":3}],"specified":{"a":1,"c":3}}`},
|
||||
{"nested x-kubernetes-preserve-unknown-fields", `
|
||||
}, expected: `{"array":[{"a":1},{},{"a":1,"c":3}],"specified":{"a":1,"c":3}}`},
|
||||
{name: "nested x-kubernetes-preserve-unknown-fields", json: `
|
||||
{
|
||||
"unspecified":"bar",
|
||||
"alpha": "abc",
|
||||
@@ -82,9 +84,15 @@ func TestPrune(t *testing.T) {
|
||||
"unspecifiedObject": {"unspecified": "bar"},
|
||||
"pruning": {"unspecified": "bar"},
|
||||
"preserving": {"unspecified": "bar"}
|
||||
},
|
||||
"preservingAdditionalProperties": {
|
||||
"foo": {
|
||||
"specified": {"unspecified":"bar"},
|
||||
"unspecified": "bar"
|
||||
}
|
||||
}
|
||||
}
|
||||
`, &structuralschema.Structural{
|
||||
`, schema: &structuralschema.Structural{
|
||||
Generic: structuralschema.Generic{Type: "object"},
|
||||
Extensions: structuralschema.Extensions{XPreserveUnknownFields: true},
|
||||
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",
|
||||
"alpha": "abc",
|
||||
@@ -131,10 +153,15 @@ func TestPrune(t *testing.T) {
|
||||
"unspecifiedObject": {"unspecified": "bar"},
|
||||
"pruning": {},
|
||||
"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{
|
||||
"a": {},
|
||||
"c": {
|
||||
@@ -149,8 +176,8 @@ func TestPrune(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
}, `{"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{
|
||||
}, expected: `{"a":1,"c":{"a":1,"b":2,"c":{}}}`},
|
||||
{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{
|
||||
"a": {},
|
||||
"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 {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
@@ -175,7 +508,7 @@ func TestPrune(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
prune(in, tt.schema)
|
||||
Prune(in, tt.schema, tt.dontPruneMetaAtRoot)
|
||||
if !reflect.DeepEqual(in, expected) {
|
||||
var buf bytes.Buffer
|
||||
enc := json.NewEncoder(&buf)
|
||||
@@ -184,7 +517,7 @@ func TestPrune(t *testing.T) {
|
||||
if err != nil {
|
||||
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()
|
||||
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)...)
|
||||
|
||||
checkMetadata := (lvl == rootLevel) || s.XEmbeddedResource
|
||||
|
||||
if s.XEmbeddedResource && s.Type != "object" {
|
||||
if len(s.Type) == 0 {
|
||||
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"))
|
||||
}
|
||||
}
|
||||
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" {
|
||||
allErrs = append(allErrs, field.Invalid(fldPath.Child("type"), s.Type, "must be object at the root"))
|
||||
}
|
||||
|
||||
// restrict metadata schemas to name and generateName only
|
||||
if 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 {
|
||||
// metadata is a shallow copy. We can mutate it.
|
||||
_, foundName := metadata.Properties["name"]
|
||||
@@ -140,6 +160,7 @@ func validateStructuralInvariants(s *Structural, lvl level, fldPath *field.Path)
|
||||
metadata.Properties = nil
|
||||
}
|
||||
metadata.Type = ""
|
||||
metadata.Default.Object = nil // this is checked in API validation (and also tested)
|
||||
if metadata.ValueValidation == nil {
|
||||
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/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/apiserver/pkg/endpoints: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"
|
||||
metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apiserver/pkg/endpoints"
|
||||
"k8s.io/apiserver/pkg/endpoints/openapi"
|
||||
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).
|
||||
WithDescription(swaggerPartialObjectMetadataDescriptions["metadata"]))
|
||||
addTypeMetaProperties(ret)
|
||||
addEmbeddedProperties(ret)
|
||||
}
|
||||
ret.AddExtension(endpoints.ROUTE_META_GVK, []interface{}{
|
||||
map[string]interface{}{
|
||||
@@ -322,6 +324,42 @@ func (b *builder) buildKubeNative(schema *structuralschema.Structural, v2 bool)
|
||||
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
|
||||
// kube-openapi builder logic
|
||||
func getDefinition(name string) spec.Schema {
|
||||
@@ -329,6 +367,10 @@ func getDefinition(name string) spec.Schema {
|
||||
return definitions[name].Schema
|
||||
}
|
||||
|
||||
func withDescription(s spec.Schema, desc string) spec.Schema {
|
||||
return *s.WithDescription(desc)
|
||||
}
|
||||
|
||||
func buildDefinitionsFunc() {
|
||||
namer = openapi.NewDefinitionNamer(runtime.NewScheme())
|
||||
definitions = generatedopenapi.GetOpenAPIDefinitions(func(name string) spec.Ref {
|
||||
|
@@ -21,6 +21,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/go-openapi/spec"
|
||||
|
||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
|
||||
structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
|
||||
@@ -154,7 +155,22 @@ func TestNewBuilder(t *testing.T) {
|
||||
"embedded-object": {
|
||||
"x-kubernetes-embedded-resource": 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"}]
|
||||
@@ -305,7 +321,22 @@ func TestNewBuilder(t *testing.T) {
|
||||
"embedded-object": {
|
||||
"x-kubernetes-embedded-resource": 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"}]
|
||||
@@ -373,7 +404,7 @@ func TestNewBuilder(t *testing.T) {
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -383,7 +414,7 @@ func TestNewBuilder(t *testing.T) {
|
||||
|
||||
gotListSchema := got.listSchema.Properties["items"].Items.Schema
|
||||
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
|
||||
}
|
||||
|
||||
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 = [
|
||||
"//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/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/features: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,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
status,
|
||||
scale,
|
||||
),
|
||||
|
@@ -35,6 +35,8 @@ import (
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
@@ -45,11 +47,12 @@ type customResourceStrategy struct {
|
||||
|
||||
namespaceScoped bool
|
||||
validator customResourceValidator
|
||||
schemas map[string]*structuralschema.Structural
|
||||
status *apiextensions.CustomResourceSubresourceStatus
|
||||
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{
|
||||
ObjectTyper: typer,
|
||||
NameGenerator: names.SimpleNameGenerator,
|
||||
@@ -62,6 +65,7 @@ func NewStrategy(typer runtime.ObjectTyper, namespaceScoped bool, kind schema.Gr
|
||||
schemaValidator: schemaValidator,
|
||||
statusSchemaValidator: statusSchemaValidator,
|
||||
},
|
||||
schemas: schemas,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,7 +133,16 @@ func copyNonMetadata(original map[string]interface{}) map[string]interface{} {
|
||||
|
||||
// Validate validates a new CustomResource.
|
||||
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.
|
||||
@@ -149,7 +162,16 @@ func (customResourceStrategy) AllowUnconditionalUpdate() bool {
|
||||
|
||||
// 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 {
|
||||
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.
|
||||
|
@@ -42,6 +42,7 @@ go_test(
|
||||
"//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/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/wait: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/pkg/transport"
|
||||
"sigs.k8s.io/yaml"
|
||||
|
||||
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
|
||||
"k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
|
||||
serveroptions "k8s.io/apiextensions-apiserver/pkg/cmd/server/options"
|
||||
"k8s.io/apiextensions-apiserver/pkg/features"
|
||||
"k8s.io/apiextensions-apiserver/test/integration/fixtures"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/diff"
|
||||
"k8s.io/apimachinery/pkg/util/json"
|
||||
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
"k8s.io/client-go/dynamic"
|
||||
featuregatetesting "k8s.io/component-base/featuregate/testing"
|
||||
"k8s.io/utils/pointer"
|
||||
)
|
||||
|
||||
func TestPostInvalidObjectMeta(t *testing.T) {
|
||||
@@ -99,6 +105,18 @@ func TestInvalidObjectMetaInStorage(t *testing.T) {
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -127,11 +145,17 @@ func TestInvalidObjectMetaInStorage(t *testing.T) {
|
||||
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")
|
||||
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)
|
||||
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.Logf("Checking that ObjectMeta is pruned from unknown fields")
|
||||
|
||||
t.Logf("Checking that invalid objects can be deleted")
|
||||
noxuResourceClient := newNamespacedCustomResourceClient("default", dynamicClient, noxuDefinition)
|
||||
obj, err := noxuResourceClient.Get("foo", metav1.GetOptions{})
|
||||
if err != nil {
|
||||
if err := noxuResourceClient.Delete("foo", &metav1.DeleteOptions{}); 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.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 {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
} 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 {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
} else if found && !reflect.DeepEqual(labels, map[string]string{"bar": "abc"}) {
|
||||
t.Errorf("unexpected to find metadata.lables=%#v", labels)
|
||||
} else if !found {
|
||||
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 (
|
||||
"path"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/coreos/etcd/clientv3"
|
||||
"github.com/coreos/etcd/pkg/transport"
|
||||
|
||||
"sigs.k8s.io/yaml"
|
||||
|
||||
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"
|
||||
types "k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/diff"
|
||||
"k8s.io/apimachinery/pkg/util/json"
|
||||
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
|
||||
"k8s.io/client-go/dynamic"
|
||||
@@ -107,6 +111,65 @@ properties:
|
||||
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 = `
|
||||
kind: Foo
|
||||
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 {
|
||||
desc string
|
||||
globalSchema, v1Schema, v1beta1Schema string
|
||||
expectedCreateError bool
|
||||
expectedCreateErrors []string
|
||||
unexpectedCreateErrors []string
|
||||
expectedViolations []string
|
||||
unexpectedViolations []string
|
||||
}
|
||||
tests := []Test{
|
||||
{"empty", "", "", "", false, nil, nil},
|
||||
{"empty", "", "", "", nil, nil, nil, nil},
|
||||
{
|
||||
desc: "int-or-string and preserve-unknown-fields true",
|
||||
globalSchema: `
|
||||
x-kubernetes-preserve-unknown-fields: 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",
|
||||
},
|
||||
},
|
||||
@@ -728,7 +729,7 @@ type: object
|
||||
x-kubernetes-embedded-resource: 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",
|
||||
},
|
||||
},
|
||||
@@ -738,7 +739,7 @@ x-kubernetes-int-or-string: true
|
||||
type: object
|
||||
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",
|
||||
},
|
||||
},
|
||||
@@ -773,7 +774,7 @@ type: array
|
||||
x-kubernetes-embedded-resource: 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",
|
||||
},
|
||||
},
|
||||
@@ -784,7 +785,7 @@ type: ""
|
||||
x-kubernetes-embedded-resource: 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",
|
||||
},
|
||||
},
|
||||
@@ -923,7 +924,7 @@ oneOf:
|
||||
x-kubernetes-embedded-resource: 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-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",
|
||||
@@ -939,7 +940,7 @@ oneOf:
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "missing types",
|
||||
desc: "missing types with extensions",
|
||||
globalSchema: `
|
||||
properties:
|
||||
foo:
|
||||
@@ -967,7 +968,7 @@ properties:
|
||||
properties:
|
||||
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].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",
|
||||
},
|
||||
},
|
||||
{
|
||||
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",
|
||||
globalSchema: `
|
||||
@@ -1033,7 +1071,7 @@ properties:
|
||||
- type: string
|
||||
- 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[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",
|
||||
@@ -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[1].type: Forbidden: must be empty to be structural",
|
||||
},
|
||||
unexpectedViolations: []string{
|
||||
unexpectedCreateErrors: []string{
|
||||
"spec.validation.openAPIV3Schema.properties[a]",
|
||||
"spec.validation.openAPIV3Schema.properties[b]",
|
||||
"spec.validation.openAPIV3Schema.properties[c]",
|
||||
@@ -1354,7 +1392,7 @@ properties:
|
||||
- type: string
|
||||
- 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",
|
||||
@@ -1369,7 +1407,7 @@ properties:
|
||||
items:
|
||||
- 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
|
||||
crd, err = apiExtensionClient.ApiextensionsV1beta1().CustomResourceDefinitions().Create(crd)
|
||||
if tst.expectedCreateError && err == nil {
|
||||
t.Fatalf("expected error, got none")
|
||||
} else if !tst.expectedCreateError && err != nil {
|
||||
if len(tst.expectedCreateErrors) > 0 && err == nil {
|
||||
t.Fatalf("expected create errors, got none")
|
||||
} else if len(tst.expectedCreateErrors) == 0 && err != nil {
|
||||
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 {
|
||||
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/schema
|
||||
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/validation
|
||||
k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset
|
||||
|
Reference in New Issue
Block a user