apiextension: prune default values in storage

This commit is contained in:
Dr. Stefan Schimanski 2019-08-22 12:00:11 +02:00
parent 135902b0f4
commit 4fd200c148
6 changed files with 397 additions and 10 deletions

View File

@ -614,11 +614,24 @@ func (r *crdHandler) getOrCreateServingInfoFor(uid types.UID, name string) (*crd
if val == nil {
continue
}
structuralSchemas[v.Name], err = structuralschema.NewStructural(val.OpenAPIV3Schema)
s, err := structuralschema.NewStructural(val.OpenAPIV3Schema)
if *crd.Spec.PreserveUnknownFields == false && err != nil {
utilruntime.HandleError(err)
// This should never happen. If it does, it is a programming error.
utilruntime.HandleError(fmt.Errorf("failed to convert schema to structural: %v", err))
return nil, fmt.Errorf("the server could not properly serve the CR schema") // validation should avoid this
}
if *crd.Spec.PreserveUnknownFields == false {
// we don't own s completely, e.g. defaults are not deep-copied. So better make a copy here.
s = s.DeepCopy()
if err := structuraldefaulting.PruneDefaults(s); err != nil {
// This should never happen. If it does, it is a programming error.
utilruntime.HandleError(fmt.Errorf("failed to prune defaults: %v", err))
return nil, fmt.Errorf("the server could not properly serve the CR schema") // validation should avoid this
}
}
structuralSchemas[v.Name] = s
}
for _, v := range crd.Spec.Versions {

View File

@ -4,6 +4,7 @@ go_library(
name = "go_default_library",
srcs = [
"algorithm.go",
"prune.go",
"surroundingobject.go",
"validation.go",
],

View File

@ -0,0 +1,91 @@
/*
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 defaulting
import (
"fmt"
"reflect"
structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
structuralobjectmeta "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/objectmeta"
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema/pruning"
"k8s.io/apimachinery/pkg/runtime"
)
// PruneDefaults prunes default values according to the schema and according to
// the ObjectMeta definition of the running server. It mutates the passed schema.
func PruneDefaults(s *structuralschema.Structural) error {
p := pruner{s}
_, err := p.pruneDefaults(s, NewRootObjectFunc())
return err
}
type pruner struct {
rootSchema *structuralschema.Structural
}
func (p *pruner) pruneDefaults(s *structuralschema.Structural, f SurroundingObjectFunc) (changed bool, err error) {
if s == nil {
return false, nil
}
if s.Default.Object != nil {
orig := runtime.DeepCopyJSONValue(s.Default.Object)
obj, acc, err := f(s.Default.Object)
if err != nil {
return false, fmt.Errorf("failed to prune default value: %v", err)
}
if err := structuralobjectmeta.Coerce(nil, obj, p.rootSchema, true, true); err != nil {
return false, fmt.Errorf("failed to prune default value: %v", err)
}
pruning.Prune(obj, p.rootSchema, true)
s.Default.Object, _, err = acc(obj)
if err != nil {
return false, fmt.Errorf("failed to prune default value: %v", err)
}
changed = changed || !reflect.DeepEqual(orig, s.Default.Object)
}
if s.AdditionalProperties != nil && s.AdditionalProperties.Structural != nil {
c, err := p.pruneDefaults(s.AdditionalProperties.Structural, f.Child("*"))
if err != nil {
return false, err
}
changed = changed || c
}
if s.Items != nil {
c, err := p.pruneDefaults(s.Items, f.Index())
if err != nil {
return false, err
}
changed = changed || c
}
for k, subSchema := range s.Properties {
c, err := p.pruneDefaults(&subSchema, f.Child(k))
if err != nil {
return false, err
}
if c {
s.Properties[k] = subSchema
changed = true
}
}
return changed, nil
}

View File

@ -36,6 +36,7 @@ go_test(
"//staging/src/k8s.io/apiextensions-apiserver/pkg/cmd/server/options:go_default_library",
"//staging/src/k8s.io/apiextensions-apiserver/pkg/features:go_default_library",
"//staging/src/k8s.io/apiextensions-apiserver/test/integration/fixtures:go_default_library",
"//staging/src/k8s.io/apiextensions-apiserver/test/integration/storage:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/api/errors:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/api/meta:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",

View File

@ -18,6 +18,7 @@ package integration
import (
"fmt"
"reflect"
"strings"
"testing"
"time"
@ -32,11 +33,15 @@ import (
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apimachinery/pkg/watch"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/client-go/dynamic"
utilfeaturetesting "k8s.io/component-base/featuregate/testing"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"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/apiextensions-apiserver/test/integration/storage"
)
var defaultingFixture = &apiextensionsv1.CustomResourceDefinition{
@ -146,6 +151,13 @@ properties:
default: "v1beta2"
`
const defaultingFooInstance = `
kind: Foo
apiVersion: tests.example.com/v1beta1
metadata:
name: foo
`
func TestCustomResourceDefaultingWithWatchCache(t *testing.T) {
testDefaulting(t, true)
}
@ -252,7 +264,7 @@ func testDefaulting(t *testing.T, watchCache bool) {
t.Logf("Creating CR and expecting defaulted fields in spec, but status does not exist at all")
fooClient := dynamicClient.Resource(schema.GroupVersionResource{crd.Spec.Group, crd.Spec.Versions[0].Name, crd.Spec.Names.Plural})
foo := &unstructured.Unstructured{}
if err := yaml.Unmarshal([]byte(fooInstance), &foo.Object); err != nil {
if err := yaml.Unmarshal([]byte(defaultingFooInstance), &foo.Object); err != nil {
t.Fatal(err)
}
unstructured.SetNestedField(foo.Object, "a", "spec", "a")
@ -400,6 +412,275 @@ func testDefaulting(t *testing.T, watchCache bool) {
mustNotExist(foo.Object, [][]string{{"spec", "c"}})
}
var metaDefaultingFixture = &apiextensionsv1.CustomResourceDefinition{
ObjectMeta: metav1.ObjectMeta{Name: "foos.tests.example.com"},
Spec: apiextensionsv1.CustomResourceDefinitionSpec{
Group: "tests.example.com",
Versions: []apiextensionsv1.CustomResourceDefinitionVersion{
{
Name: "v1beta1",
Storage: true,
Served: true,
Subresources: &apiextensionsv1.CustomResourceSubresources{
Status: &apiextensionsv1.CustomResourceSubresourceStatus{},
},
},
},
Names: apiextensionsv1.CustomResourceDefinitionNames{
Plural: "foos",
Singular: "foo",
Kind: "Foo",
ListKind: "FooList",
},
Scope: apiextensionsv1.ClusterScoped,
PreserveUnknownFields: false,
},
}
const metaDefaultingFooV1beta1Schema = `
type: object
properties:
fields:
type: object
x-kubernetes-embedded-resource: true
properties:
apiVersion:
type: string
default: foos/v1
kind:
type: string
default: Foo
metadata:
type: object
properties:
name:
type: string
default: Bar
unknown:
type: string
default: unknown
fullMetadata:
type: object
x-kubernetes-embedded-resource: true
properties:
apiVersion:
type: string
default: foos/v1
kind:
type: string
default: Foo
metadata:
type: object
default:
name: Bar
unknown: unknown
fullObject:
type: object
x-kubernetes-embedded-resource: true
properties:
foo:
type: string
default:
apiVersion: foos/v1
kind: Foo
metadata:
name: Bar
unknown: unknown
spanning:
type: object
properties:
embedded:
type: object
properties:
foo:
type: string
x-kubernetes-embedded-resource: true
default:
embedded:
apiVersion: foos/v1
kind: Foo
metadata:
name: Bar
unknown: unknown
preserve-fields:
type: object
x-kubernetes-embedded-resource: true
x-kubernetes-preserve-unknown-fields: true
properties:
apiVersion:
type: string
default: foos/v1
kind:
type: string
default: Foo
metadata:
type: object
properties:
name:
type: string
default: Bar
unknown:
type: string
default: unknown
preserve-fullMetadata:
type: object
x-kubernetes-embedded-resource: true
x-kubernetes-preserve-unknown-fields: true
properties:
apiVersion:
type: string
default: foos/v1
kind:
type: string
default: Foo
metadata:
type: object
default:
name: Bar
unknown: unknown
preserve-fullObject:
type: object
x-kubernetes-embedded-resource: true
x-kubernetes-preserve-unknown-fields: true
default:
apiVersion: foos/v1
kind: Foo
metadata:
name: Bar
unknown: unknown
preserve-spanning:
type: object
properties:
embedded:
type: object
x-kubernetes-embedded-resource: true
x-kubernetes-preserve-unknown-fields: true
default:
embedded:
apiVersion: foos/v1
kind: Foo
metadata:
name: Bar
unknown: unknown
`
const metaDefaultingFooInstance = `
kind: Foo
apiVersion: tests.example.com/v1beta1
metadata:
name: foo
`
func TestCustomResourceDefaultingOfMetaFields(t *testing.T) {
defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.CustomResourceDefaulting, true)()
tearDown, config, options, err := fixtures.StartDefaultServer(t)
if err != nil {
t.Fatal(err)
}
apiExtensionClient, err := clientset.NewForConfig(config)
if err != nil {
tearDown()
t.Fatal(err)
}
dynamicClient, err := dynamic.NewForConfig(config)
if err != nil {
tearDown()
t.Fatal(err)
}
defer tearDown()
crd := metaDefaultingFixture.DeepCopy()
crd.Spec.Versions[0].Schema = &apiextensionsv1.CustomResourceValidation{}
if err := yaml.Unmarshal([]byte(metaDefaultingFooV1beta1Schema), &crd.Spec.Versions[0].Schema.OpenAPIV3Schema); err != nil {
t.Fatal(err)
}
crd, err = fixtures.CreateNewV1CustomResourceDefinition(crd, apiExtensionClient, dynamicClient)
if err != nil {
t.Fatal(err)
}
t.Logf("Creating CR and expecting defaulted, embedded objects, with the unknown ObjectMeta fields pruned")
fooClient := dynamicClient.Resource(schema.GroupVersionResource{Group: crd.Spec.Group, Version: crd.Spec.Versions[0].Name, Resource: crd.Spec.Names.Plural})
tests := []struct {
path []string
value interface{}
}{
{[]string{"fields"}, map[string]interface{}{"metadata": map[string]interface{}{}}},
{[]string{"fullMetadata"}, map[string]interface{}{}},
{[]string{"fullObject"}, nil},
{[]string{"spanning", "embedded"}, nil},
{[]string{"preserve-fields"}, map[string]interface{}{"metadata": map[string]interface{}{}}},
{[]string{"preserve-fullMetadata"}, map[string]interface{}{}},
{[]string{"preserve-fullObject"}, nil},
{[]string{"preserve-spanning", "embedded"}, nil},
}
returnedFoo := &unstructured.Unstructured{}
if err := yaml.Unmarshal([]byte(metaDefaultingFooInstance), &returnedFoo.Object); err != nil {
t.Fatal(err)
}
for _, tst := range tests {
if tst.value != nil {
if err := unstructured.SetNestedField(returnedFoo.Object, tst.value, tst.path...); err != nil {
t.Fatal(err)
}
}
}
returnedFoo, err = fooClient.Create(returnedFoo, metav1.CreateOptions{})
if err != nil {
t.Fatalf("Unable to create CR: %v", err)
}
t.Logf("CR created: %#v", returnedFoo.UnstructuredContent())
// get persisted object
RESTOptionsGetter := serveroptions.NewCRDRESTOptionsGetter(*options.RecommendedOptions.Etcd)
restOptions, err := RESTOptionsGetter.GetRESTOptions(schema.GroupResource{Group: crd.Spec.Group, Resource: crd.Spec.Names.Plural})
if err != nil {
t.Fatal(err)
}
etcdClient, _, err := storage.GetEtcdClients(restOptions.StorageConfig.Transport)
if err != nil {
t.Fatal(err)
}
defer etcdClient.Close()
etcdObjectReader := storage.NewEtcdObjectReader(etcdClient, &restOptions, crd)
persistedFoo, err := etcdObjectReader.GetStoredCustomResource("", returnedFoo.GetName())
if err != nil {
t.Fatalf("Unable read CR from stored: %v", err)
}
// check that the returned and persisted object is pruned
for _, tst := range tests {
for _, foo := range []*unstructured.Unstructured{returnedFoo, persistedFoo} {
source := "request"
if foo == persistedFoo {
source = "persisted"
}
t.Run(fmt.Sprintf("%s of %s object", strings.Join(tst.path, "."), source), func(t *testing.T) {
obj, found, err := unstructured.NestedMap(foo.Object, tst.path...)
if err != nil {
t.Fatal(err)
}
if !found {
t.Errorf("expected defaulted objected, didn't find any")
} else if expected := map[string]interface{}{
"apiVersion": "foos/v1",
"kind": "Foo",
"metadata": map[string]interface{}{
"name": "Bar",
},
}; !reflect.DeepEqual(obj, expected) {
t.Errorf("unexpected defaulted object\n expected: %v\n got: %v", expected, obj)
}
})
}
}
}
func jsonPtr(x interface{}) *apiextensionsv1.JSON {
bs, err := json.Marshal(x)
if err != nil {

View File

@ -137,7 +137,7 @@ properties:
type: string
`
fooSchemaEmbeddedResourceInstance = fooInstance + `
fooSchemaEmbeddedResourceInstance = pruningFooInstance + `
embeddedPruning:
apiVersion: foo/v1
kind: Foo
@ -170,7 +170,7 @@ embeddedNested:
specified: bar
`
fooInstance = `
pruningFooInstance = `
kind: Foo
apiVersion: tests.example.com/v1beta1
metadata:
@ -199,7 +199,7 @@ func TestPruningCreate(t *testing.T) {
t.Logf("Creating CR and expect 'unspecified' fields 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(fooInstance), &foo.Object); err != nil {
if err := yaml.Unmarshal([]byte(pruningFooInstance), &foo.Object); err != nil {
t.Fatal(err)
}
unstructured.SetNestedField(foo.Object, "bar", "unspecified")
@ -251,7 +251,7 @@ func TestPruningStatus(t *testing.T) {
t.Logf("Creating CR and expect 'unspecified' fields 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(fooInstance), &foo.Object); err != nil {
if err := yaml.Unmarshal([]byte(pruningFooInstance), &foo.Object); err != nil {
t.Fatal(err)
}
foo, err = fooClient.Create(foo, metav1.CreateOptions{})
@ -342,7 +342,7 @@ func TestPruningFromStorage(t *testing.T) {
t.Logf("Creating object with unknown field manually in etcd")
original := &unstructured.Unstructured{}
if err := yaml.Unmarshal([]byte(fooInstance), &original.Object); err != nil {
if err := yaml.Unmarshal([]byte(pruningFooInstance), &original.Object); err != nil {
t.Fatal(err)
}
unstructured.SetNestedField(original.Object, "bar", "unspecified")
@ -404,7 +404,7 @@ func TestPruningPatch(t *testing.T) {
fooClient := dynamicClient.Resource(schema.GroupVersionResource{crd.Spec.Group, crd.Spec.Version, crd.Spec.Names.Plural})
foo := &unstructured.Unstructured{}
if err := yaml.Unmarshal([]byte(fooInstance), &foo.Object); err != nil {
if err := yaml.Unmarshal([]byte(pruningFooInstance), &foo.Object); err != nil {
t.Fatal(err)
}
foo, err = fooClient.Create(foo, metav1.CreateOptions{})
@ -457,7 +457,7 @@ func TestPruningCreatePreservingUnknownFields(t *testing.T) {
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(fooInstance), &foo.Object); err != nil {
if err := yaml.Unmarshal([]byte(pruningFooInstance), &foo.Object); err != nil {
t.Fatal(err)
}
unstructured.SetNestedField(foo.Object, "bar", "unspecified")