From 77bfddacfdb3eb7335dfff1762d3917427592595 Mon Sep 17 00:00:00 2001 From: "Dr. Stefan Schimanski" Date: Thu, 2 May 2019 12:04:39 +0200 Subject: [PATCH] apiextensions: add pruning integration tests --- .../test/integration/pruning_test.go | 459 ++++++++++++++++++ test/e2e/apimachinery/webhook.go | 38 +- test/integration/etcd/data.go | 31 ++ 3 files changed, 522 insertions(+), 6 deletions(-) create mode 100644 staging/src/k8s.io/apiextensions-apiserver/test/integration/pruning_test.go diff --git a/staging/src/k8s.io/apiextensions-apiserver/test/integration/pruning_test.go b/staging/src/k8s.io/apiextensions-apiserver/test/integration/pruning_test.go new file mode 100644 index 00000000000..5ed8591ec03 --- /dev/null +++ b/staging/src/k8s.io/apiextensions-apiserver/test/integration/pruning_test.go @@ -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, ".")) + } + } +} diff --git a/test/e2e/apimachinery/webhook.go b/test/e2e/apimachinery/webhook.go index d63d239e121..b795a48ccaf 100644 --- a/test/e2e/apimachinery/webhook.go +++ b/test/e2e/apimachinery/webhook.go @@ -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. diff --git a/test/integration/etcd/data.go b/test/integration/etcd/data.go index db7dc8ecf64..2f96aceaf3e 100644 --- a/test/integration/etcd/data.go +++ b/test/integration/etcd/data.go @@ -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{