apiextensions: add pruning integration tests

This commit is contained in:
Dr. Stefan Schimanski 2019-05-02 12:04:39 +02:00
parent 3f3ed79484
commit 77bfddacfd
3 changed files with 522 additions and 6 deletions

View File

@ -0,0 +1,459 @@
/*
Copyright 2018 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 integration
import (
"path"
"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/schema"
types "k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/json"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/client-go/dynamic"
"k8s.io/utils/pointer"
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
"k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
"k8s.io/apiextensions-apiserver/test/integration/fixtures"
)
var pruningFixture = &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 (
fooSchema = `
type: object
properties:
alpha:
type: string
beta:
type: number
`
fooStatusSchema = `
type: object
properties:
status:
type: object
properties:
alpha:
type: string
beta:
type: number
`
fooSchemaPreservingUnknownFields = `
type: object
properties:
alpha:
type: string
beta:
type: number
preserving:
type: object
x-kubernetes-preserve-unknown-fields: true
properties:
preserving:
type: object
x-kubernetes-preserve-unknown-fields: true
pruning:
type: object
pruning:
type: object
properties:
preserving:
type: object
x-kubernetes-preserve-unknown-fields: true
pruning:
type: object
x-kubernetes-preserve-unknown-fields: true
`
fooInstance = `
kind: Foo
apiVersion: tests.apiextensions.k8s.io/v1beta1
metadata:
name: foo
`
)
func TestPruningCreate(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(fooSchema), &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")
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 {
t.Fatal(err)
}
unstructured.SetNestedField(foo.Object, "bar", "unspecified")
unstructured.SetNestedField(foo.Object, "abc", "alpha")
unstructured.SetNestedField(foo.Object, float64(42.0), "beta")
unstructured.SetNestedField(foo.Object, "bar", "metadata", "unspecified")
unstructured.SetNestedField(foo.Object, "bar", "metadata", "labels", "foo")
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())
if _, found, _ := unstructured.NestedFieldNoCopy(foo.Object, "unspecified"); found {
t.Errorf("Expected 'unspecified' field to be pruned, but it was not")
}
if _, found, _ := unstructured.NestedFieldNoCopy(foo.Object, "alpha"); !found {
t.Errorf("Expected specified 'alpha' field to stay, but it was pruned")
}
if _, found, _ := unstructured.NestedFieldNoCopy(foo.Object, "beta"); !found {
t.Errorf("Expected specified 'beta' field to stay, but it was pruned")
}
if _, found, _ := unstructured.NestedFieldNoCopy(foo.Object, "metadata", "unspecified"); found {
t.Errorf("Expected 'metadata.unspecified' field to be pruned, but it was not")
}
if _, found, _ := unstructured.NestedFieldNoCopy(foo.Object, "metadata", "labels", "foo"); !found {
t.Errorf("Expected specified 'metadata.labels[foo]' field to stay, but it was pruned")
}
}
func TestPruningStatus(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(fooStatusSchema), &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")
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 {
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())
unstructured.SetNestedField(foo.Object, "bar", "status", "unspecified")
unstructured.SetNestedField(foo.Object, "abc", "status", "alpha")
unstructured.SetNestedField(foo.Object, float64(42.0), "status", "beta")
unstructured.SetNestedField(foo.Object, "bar", "metadata", "unspecified")
foo, err = fooClient.UpdateStatus(foo, metav1.UpdateOptions{})
if err != nil {
t.Fatalf("Unable to update status: %v", err)
}
if _, found, _ := unstructured.NestedFieldNoCopy(foo.Object, "unspecified"); found {
t.Errorf("Expected 'status.unspecified' field to be pruned, but it was not")
}
if _, found, _ := unstructured.NestedFieldNoCopy(foo.Object, "status", "alpha"); !found {
t.Errorf("Expected specified 'status.alpha' field to stay, but it was pruned")
}
if _, found, _ := unstructured.NestedFieldNoCopy(foo.Object, "status", "beta"); !found {
t.Errorf("Expected specified 'status.beta' field to stay, but it was pruned")
}
if _, found, _ := unstructured.NestedFieldNoCopy(foo.Object, "metadata", "unspecified"); found {
t.Errorf("Expected 'metadata.unspecified' field to be pruned, but it was not")
}
}
func TestPruningFromStorage(t *testing.T) {
tearDown, config, options, err := fixtures.StartDefaultServer(t)
if err != nil {
t.Fatal(err)
}
defer tearDown()
apiExtensionClient, err := clientset.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
dynamicClient, err := dynamic.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
serverConfig, err := options.Config()
if err != nil {
t.Fatal(err)
}
crd := pruningFixture.DeepCopy()
crd.Spec.Validation = &apiextensionsv1beta1.CustomResourceValidation{}
if err := yaml.Unmarshal([]byte(fooSchema), &crd.Spec.Validation.OpenAPIV3Schema); err != nil {
t.Fatal(err)
}
crd, err = fixtures.CreateNewCustomResourceDefinition(crd, apiExtensionClient, dynamicClient)
if err != nil {
t.Fatal(err)
}
restOptions, err := serverConfig.GenericConfig.RESTOptionsGetter.GetRESTOptions(schema.GroupResource{Group: crd.Spec.Group, Resource: crd.Spec.Names.Plural})
if err != nil {
t.Fatal(err)
}
tlsInfo := transport.TLSInfo{
CertFile: restOptions.StorageConfig.Transport.CertFile,
KeyFile: restOptions.StorageConfig.Transport.KeyFile,
CAFile: restOptions.StorageConfig.Transport.CAFile,
}
tlsConfig, err := tlsInfo.ClientConfig()
if err != nil {
t.Fatal(err)
}
etcdConfig := clientv3.Config{
Endpoints: restOptions.StorageConfig.Transport.ServerList,
TLS: tlsConfig,
}
etcdclient, err := clientv3.New(etcdConfig)
if err != nil {
t.Fatal(err)
}
t.Logf("Creating object with unknown field manually in etcd")
original := &unstructured.Unstructured{}
if err := yaml.Unmarshal([]byte(fooInstance), &original.Object); err != nil {
t.Fatal(err)
}
unstructured.SetNestedField(original.Object, "bar", "unspecified")
unstructured.SetNestedField(original.Object, "abc", "alpha")
unstructured.SetNestedField(original.Object, float64(42), "beta")
unstructured.SetNestedField(original.Object, "bar", "metadata", "labels", "foo")
// Note: we don't add metadata.unspecified as in the other tests. ObjectMeta pruning is independent of the generic pruning
// and we do not guarantee that we prune ObjectMeta on read from etcd.
ctx := genericapirequest.WithNamespace(genericapirequest.NewContext(), metav1.NamespaceDefault)
key := path.Join("/", restOptions.StorageConfig.Prefix, crd.Spec.Group, "foos/foo")
val, _ := json.Marshal(original.UnstructuredContent())
if _, err := etcdclient.Put(ctx, key, string(val)); err != nil {
t.Fatalf("unexpected error: %v", err)
}
t.Logf("Checking that CustomResource is pruned from unknown fields")
fooClient := dynamicClient.Resource(schema.GroupVersionResource{crd.Spec.Group, crd.Spec.Version, crd.Spec.Names.Plural})
foo, err := fooClient.Get("foo", metav1.GetOptions{})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if _, found, _ := unstructured.NestedFieldNoCopy(foo.Object, "unspecified"); found {
t.Errorf("Expected 'unspecified' field to be pruned, but it was not")
}
if _, found, _ := unstructured.NestedFieldNoCopy(foo.Object, "alpha"); !found {
t.Errorf("Expected specified 'alpha' field to stay, but it was pruned")
}
if _, found, _ := unstructured.NestedFieldNoCopy(foo.Object, "beta"); !found {
t.Errorf("Expected specified 'beta' field to stay, but it was pruned")
}
// Note: we don't check metadata.foo as in the other tests. ObjectMeta pruning is independent of the generic pruning
// and we do not guarantee that we prune ObjectMeta on read from etcd.
if _, found, _ := unstructured.NestedFieldNoCopy(foo.Object, "metadata", "labels", "foo"); !found {
t.Errorf("Expected specified 'metadata.labels[foo]' field to stay, but it was pruned")
}
}
func TestPruningPatch(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(fooSchema), &crd.Spec.Validation.OpenAPIV3Schema); err != nil {
t.Fatal(err)
}
crd, err = fixtures.CreateNewCustomResourceDefinition(crd, apiExtensionClient, dynamicClient)
if err != nil {
t.Fatal(err)
}
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 {
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())
// a patch with a change
patch := []byte(`{"alpha": "abc", "beta": 42.0, "unspecified": "bar", "metadata": {"unspecified": "bar", "labels":{"foo":"bar"}}}`)
if foo, err = fooClient.Patch("foo", types.MergePatchType, patch, metav1.PatchOptions{}); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if _, found, _ := unstructured.NestedFieldNoCopy(foo.Object, "unspecified"); found {
t.Errorf("Expected 'unspecified' field to be pruned, but it was not")
}
if _, found, _ := unstructured.NestedFieldNoCopy(foo.Object, "alpha"); !found {
t.Errorf("Expected specified 'alpha' field to stay, but it was pruned")
}
if _, found, _ := unstructured.NestedFieldNoCopy(foo.Object, "beta"); !found {
t.Errorf("Expected specified 'beta' field to stay, but it was pruned")
}
if _, found, _ := unstructured.NestedFieldNoCopy(foo.Object, "metadata", "unspecified"); found {
t.Errorf("Expected 'metadata.unspecified' field to be pruned, but it was not")
}
if _, found, _ := unstructured.NestedFieldNoCopy(foo.Object, "metadata", "labels", "foo"); !found {
t.Errorf("Expected specified 'metadata.labels[foo]' field to stay, but it was pruned")
}
}
func TestPruningCreatePreservingUnknownFields(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(fooSchemaPreservingUnknownFields), &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(fooInstance), &foo.Object); err != nil {
t.Fatal(err)
}
unstructured.SetNestedField(foo.Object, "bar", "unspecified")
unstructured.SetNestedField(foo.Object, "abc", "alpha")
unstructured.SetNestedField(foo.Object, float64(42.0), "beta")
unstructured.SetNestedField(foo.Object, "bar", "metadata", "unspecified")
unstructured.SetNestedField(foo.Object, "bar", "metadata", "labels", "foo")
unstructured.SetNestedField(foo.Object, map[string]interface{}{
"unspecified": "bar",
"unspecifiedObject": map[string]interface{}{"unspecified": "bar"},
"pruning": map[string]interface{}{"unspecified": "bar"},
"preserving": map[string]interface{}{"unspecified": "bar"},
}, "pruning")
unstructured.SetNestedField(foo.Object, map[string]interface{}{
"unspecified": "bar",
"unspecifiedObject": map[string]interface{}{"unspecified": "bar"},
"pruning": map[string]interface{}{"unspecified": "bar"},
"preserving": map[string]interface{}{"unspecified": "bar"},
}, "preserving")
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())
for _, pth := range [][]string{
{"unspecified"},
{"alpha"},
{"beta"},
{"metadata", "labels", "foo"},
{"pruning", "pruning"},
{"pruning", "preserving"},
{"pruning", "preserving", "unspecified"},
{"preserving", "unspecified"},
{"preserving", "unspecifiedObject"},
{"preserving", "unspecifiedObject", "unspecified"},
{"preserving", "pruning"},
{"preserving", "preserving"},
{"preserving", "preserving", "unspecified"},
} {
if _, found, _ := unstructured.NestedFieldNoCopy(foo.Object, pth...); !found {
t.Errorf("Expected '%s' field to stay, but it was pruned", strings.Join(pth, "."))
}
}
for _, pth := range [][]string{
{"metadata", "unspecified"},
{"pruning", "unspecified"},
{"pruning", "unspecifiedObject"},
{"pruning", "unspecifiedObject", "unspecified"},
{"pruning", "pruning", "unspecified"},
{"preserving", "pruning", "unspecified"},
} {
if _, found, _ := unstructured.NestedFieldNoCopy(foo.Object, pth...); found {
t.Errorf("Expected '%s' field to be pruned, but it was not", strings.Join(pth, "."))
}
}
}

View File

@ -181,7 +181,7 @@ var _ = SIGDescribe("AdmissionWebhook", func() {
defer testcrd.CleanUp()
webhookCleanup := registerMutatingWebhookForCustomResource(f, context, testcrd)
defer webhookCleanup()
testMutatingCustomResourceWebhook(f, testcrd.Crd, testcrd.DynamicClients["v1"])
testMutatingCustomResourceWebhook(f, testcrd.Crd, testcrd.DynamicClients["v1"], false)
})
ginkgo.It("Should deny crd creation", func() {
@ -202,6 +202,30 @@ var _ = SIGDescribe("AdmissionWebhook", func() {
testMultiVersionCustomResourceWebhook(f, testcrd)
})
ginkgo.It("Should mutate custom resource with pruning", func() {
const prune = true
testcrd, err := createAdmissionWebhookMultiVersionTestCRDWithV1Storage(f, func(crd *apiextensionsv1beta1.CustomResourceDefinition) {
crd.Spec.PreserveUnknownFields = pointer.BoolPtr(false)
crd.Spec.Validation = &apiextensionsv1beta1.CustomResourceValidation{
OpenAPIV3Schema: &apiextensionsv1beta1.JSONSchemaProps{
Type: "object",
Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
"mutation-start": {Type: "string"},
"mutation-stage-1": {Type: "string"},
// mutation-stage-2 is intentionally missing such that it is pruned
},
},
}
})
if err != nil {
return
}
defer testcrd.CleanUp()
webhookCleanup := registerMutatingWebhookForCustomResource(f, context, testcrd)
defer webhookCleanup()
testMutatingCustomResourceWebhook(f, testcrd.Crd, testcrd.DynamicClients["v1"], prune)
})
ginkgo.It("Should deny crd creation", func() {
crdWebhookCleanup := registerValidatingWebhookForCRD(f, context)
defer crdWebhookCleanup()
@ -1329,7 +1353,7 @@ func testCustomResourceWebhook(f *framework.Framework, crd *apiextensionsv1beta1
}
}
func testMutatingCustomResourceWebhook(f *framework.Framework, crd *apiextensionsv1beta1.CustomResourceDefinition, customResourceClient dynamic.ResourceInterface) {
func testMutatingCustomResourceWebhook(f *framework.Framework, crd *apiextensionsv1beta1.CustomResourceDefinition, customResourceClient dynamic.ResourceInterface, prune bool) {
ginkgo.By("Creating a custom resource that should be mutated by the webhook")
crName := "cr-instance-1"
cr := &unstructured.Unstructured{
@ -1350,7 +1374,9 @@ func testMutatingCustomResourceWebhook(f *framework.Framework, crd *apiextension
expectedCRData := map[string]interface{}{
"mutation-start": "yes",
"mutation-stage-1": "yes",
"mutation-stage-2": "yes",
}
if !prune {
expectedCRData["mutation-stage-2"] = "yes"
}
if !reflect.DeepEqual(expectedCRData, mutatedCR.Object["data"]) {
framework.Failf("\nexpected %#v\n, got %#v\n", expectedCRData, mutatedCR.Object["data"])
@ -1571,9 +1597,9 @@ func testSlowWebhookTimeoutNoError(f *framework.Framework) {
// createAdmissionWebhookMultiVersionTestCRDWithV1Storage creates a new CRD specifically
// for the admissin webhook calling test.
func createAdmissionWebhookMultiVersionTestCRDWithV1Storage(f *framework.Framework) (*crd.TestCrd, error) {
func createAdmissionWebhookMultiVersionTestCRDWithV1Storage(f *framework.Framework, opts ...crd.Option) (*crd.TestCrd, error) {
group := fmt.Sprintf("%s-multiversion-crd-test.k8s.io", f.BaseName)
return crd.CreateMultiVersionTestCRD(f, group, func(crd *apiextensionsv1beta1.CustomResourceDefinition) {
return crd.CreateMultiVersionTestCRD(f, group, append([]crd.Option{func(crd *apiextensionsv1beta1.CustomResourceDefinition) {
crd.Spec.Versions = []apiextensionsv1beta1.CustomResourceDefinitionVersion{
{
Name: "v1",
@ -1586,7 +1612,7 @@ func createAdmissionWebhookMultiVersionTestCRDWithV1Storage(f *framework.Framewo
Storage: false,
},
}
})
}}, opts...)...)
}
// servedAPIVersions returns the API versions served by the CRD.

View File

@ -22,6 +22,7 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/kubernetes/pkg/features"
"k8s.io/utils/pointer"
)
// GetEtcdStorageData returns etcd data for all persisted objects.
@ -485,6 +486,10 @@ func GetEtcdStorageDataForNamespace(namespace string) map[schema.GroupVersionRes
ExpectedEtcdPath: "/registry/awesome.bears.com/pandas/cr4panda",
ExpectedGVK: gvkP("awesome.bears.com", "v1", "Panda"),
},
gvr("random.numbers.com", "v1", "integers"): {
Stub: `{"kind": "Integer", "apiVersion": "random.numbers.com/v1", "metadata": {"name": "fortytwo"}, "value": 42, "garbage": "oiujnasdf"}`, // requires TypeMeta due to CRD scheme's UnstructuredObjectTyper
ExpectedEtcdPath: "/registry/random.numbers.com/integers/fortytwo",
},
// --
// k8s.io/kubernetes/pkg/apis/auditregistration/v1alpha1
@ -580,6 +585,32 @@ func GetCustomResourceDefinitionData() []*apiextensionsv1beta1.CustomResourceDef
},
},
},
// cluster scoped with legacy version field and pruning.
{
ObjectMeta: metav1.ObjectMeta{
Name: "integers.random.numbers.com",
},
Spec: apiextensionsv1beta1.CustomResourceDefinitionSpec{
Group: "random.numbers.com",
Version: "v1",
Scope: apiextensionsv1beta1.ClusterScoped,
Names: apiextensionsv1beta1.CustomResourceDefinitionNames{
Plural: "integers",
Kind: "Integer",
},
Validation: &apiextensionsv1beta1.CustomResourceValidation{
OpenAPIV3Schema: &apiextensionsv1beta1.JSONSchemaProps{
Type: "object",
Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
"value": {
Type: "number",
},
},
},
},
PreserveUnknownFields: pointer.BoolPtr(false),
},
},
// cluster scoped with versions field
{
ObjectMeta: metav1.ObjectMeta{