Merge pull request #78788 from sttts/sttts-crd-embedded-resource

apiextensions: validate x-kubernetes-embedded-resource in CRs
This commit is contained in:
Kubernetes Prow Robot
2019-06-10 09:01:11 -07:00
committed by GitHub
30 changed files with 3289 additions and 409 deletions

View File

@@ -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",

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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",
],
)

View File

@@ -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
}

View File

@@ -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)
}
}
}

View File

@@ -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"],

View File

@@ -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"],
)

View File

@@ -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
}

View File

@@ -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))
}
})
}
}

View File

@@ -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
}

View File

@@ -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)
}
}
}

View File

@@ -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])
}

View File

@@ -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
}

View File

@@ -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()
}

View File

@@ -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",
],
)

View File

@@ -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{}:

View File

@@ -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)
}
}

View File

@@ -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{}
}

View File

@@ -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",

View File

@@ -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 {

View File

@@ -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))
}

View File

@@ -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",

View File

@@ -99,6 +99,7 @@ func newStorage(t *testing.T) (customresource.CustomResourceStorage, *etcdtestin
kind,
nil,
nil,
nil,
status,
scale,
),

View File

@@ -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.

View File

@@ -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",

View File

@@ -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)
}
}
}

View File

@@ -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))
}
}

View File

@@ -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
View File

@@ -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