mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-24 20:24:09 +00:00
apiextension: prune default values in storage
This commit is contained in:
parent
135902b0f4
commit
4fd200c148
@ -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 {
|
||||
|
@ -4,6 +4,7 @@ go_library(
|
||||
name = "go_default_library",
|
||||
srcs = [
|
||||
"algorithm.go",
|
||||
"prune.go",
|
||||
"surroundingobject.go",
|
||||
"validation.go",
|
||||
],
|
||||
|
@ -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
|
||||
}
|
@ -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",
|
||||
|
@ -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 {
|
||||
|
@ -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")
|
||||
|
Loading…
Reference in New Issue
Block a user