/* Copyright 2017 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 ( "context" "fmt" "strings" "testing" "time" apierrors "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/wait" "k8s.io/apimachinery/pkg/util/yaml" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" clientschema "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/scheme" "k8s.io/apiextensions-apiserver/test/integration/fixtures" ) func TestForProperValidationErrors(t *testing.T) { tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t) if err != nil { t.Fatal(err) } defer tearDown() noxuDefinition := fixtures.NewNoxuCustomResourceDefinition(apiextensionsv1beta1.NamespaceScoped) noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) if err != nil { t.Fatal(err) } ns := "not-the-default" noxuResourceClient := newNamespacedCustomResourceClient(ns, dynamicClient, noxuDefinition) tests := []struct { name string instanceFn func() *unstructured.Unstructured expectedError string }{ { name: "bad version", instanceFn: func() *unstructured.Unstructured { instance := fixtures.NewVersionedNoxuInstance(ns, "foo", "v2") return instance }, expectedError: "the API version in the data (mygroup.example.com/v2) does not match the expected API version (mygroup.example.com/v1beta1)", }, { name: "bad kind", instanceFn: func() *unstructured.Unstructured { instance := fixtures.NewNoxuInstance(ns, "foo") instance.Object["kind"] = "SomethingElse" return instance }, expectedError: `SomethingElse.mygroup.example.com "foo" is invalid: kind: Invalid value: "SomethingElse": must be WishIHadChosenNoxu`, }, } for _, tc := range tests { _, err := noxuResourceClient.Create(context.TODO(), tc.instanceFn(), metav1.CreateOptions{}) if err == nil { t.Errorf("%v: expected %v", tc.name, tc.expectedError) continue } // this only works when status errors contain the expect kind and version, so this effectively tests serializations too if !strings.Contains(err.Error(), tc.expectedError) { t.Errorf("%v: expected %v, got %v", tc.name, tc.expectedError, err) continue } } } func newNoxuValidationCRDs(scope apiextensionsv1beta1.ResourceScope) []*apiextensionsv1beta1.CustomResourceDefinition { validationSchema := &apiextensionsv1beta1.JSONSchemaProps{ Required: []string{"alpha", "beta"}, AdditionalProperties: &apiextensionsv1beta1.JSONSchemaPropsOrBool{ Allows: true, }, Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{ "alpha": { Description: "Alpha is an alphanumeric string with underscores", Type: "string", Pattern: "^[a-zA-Z0-9_]*$", }, "beta": { Description: "Minimum value of beta is 10", Type: "number", Minimum: float64Ptr(10), }, "gamma": { Description: "Gamma is restricted to foo, bar and baz", Type: "string", Enum: []apiextensionsv1beta1.JSON{ { Raw: []byte(`"foo"`), }, { Raw: []byte(`"bar"`), }, { Raw: []byte(`"baz"`), }, }, }, "delta": { Description: "Delta is a string with a maximum length of 5 or a number with a minimum value of 0", AnyOf: []apiextensionsv1beta1.JSONSchemaProps{ { Type: "string", MaxLength: int64Ptr(5), }, { Type: "number", Minimum: float64Ptr(0), }, }, }, }, } validationSchemaWithDescription := validationSchema.DeepCopy() validationSchemaWithDescription.Description = "test" return []*apiextensionsv1beta1.CustomResourceDefinition{ { ObjectMeta: metav1.ObjectMeta{Name: "noxus.mygroup.example.com"}, Spec: apiextensionsv1beta1.CustomResourceDefinitionSpec{ Group: "mygroup.example.com", Version: "v1beta1", Names: apiextensionsv1beta1.CustomResourceDefinitionNames{ Plural: "noxus", Singular: "nonenglishnoxu", Kind: "WishIHadChosenNoxu", ShortNames: []string{"foo", "bar", "abc", "def"}, ListKind: "NoxuItemList", }, Scope: apiextensionsv1beta1.NamespaceScoped, Validation: &apiextensionsv1beta1.CustomResourceValidation{ OpenAPIV3Schema: validationSchema, }, Versions: []apiextensionsv1beta1.CustomResourceDefinitionVersion{ { Name: "v1beta1", Served: true, Storage: true, }, { Name: "v1", Served: true, Storage: false, }, }, }, }, { ObjectMeta: metav1.ObjectMeta{Name: "noxus.mygroup.example.com"}, Spec: apiextensionsv1beta1.CustomResourceDefinitionSpec{ Group: "mygroup.example.com", Version: "v1beta1", Names: apiextensionsv1beta1.CustomResourceDefinitionNames{ Plural: "noxus", Singular: "nonenglishnoxu", Kind: "WishIHadChosenNoxu", ShortNames: []string{"foo", "bar", "abc", "def"}, ListKind: "NoxuItemList", }, Scope: apiextensionsv1beta1.NamespaceScoped, Versions: []apiextensionsv1beta1.CustomResourceDefinitionVersion{ { Name: "v1beta1", Served: true, Storage: true, Schema: &apiextensionsv1beta1.CustomResourceValidation{ OpenAPIV3Schema: validationSchema, }, }, { Name: "v1", Served: true, Storage: false, Schema: &apiextensionsv1beta1.CustomResourceValidation{ OpenAPIV3Schema: validationSchemaWithDescription, }, }, }, }, }, } } func newNoxuValidationInstance(namespace, name string) *unstructured.Unstructured { return &unstructured.Unstructured{ Object: map[string]interface{}{ "apiVersion": "mygroup.example.com/v1beta1", "kind": "WishIHadChosenNoxu", "metadata": map[string]interface{}{ "namespace": namespace, "name": name, }, "alpha": "foo_123", "beta": 10, "gamma": "bar", "delta": "hello", }, } } func TestCustomResourceValidation(t *testing.T) { tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t) if err != nil { t.Fatal(err) } defer tearDown() noxuDefinitions := newNoxuValidationCRDs(apiextensionsv1beta1.NamespaceScoped) for _, noxuDefinition := range noxuDefinitions { noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) if err != nil { t.Fatal(err) } ns := "not-the-default" for _, v := range noxuDefinition.Spec.Versions { noxuResourceClient := newNamespacedCustomResourceVersionedClient(ns, dynamicClient, noxuDefinition, v.Name) instanceToCreate := newNoxuValidationInstance(ns, "foo") instanceToCreate.Object["apiVersion"] = fmt.Sprintf("%s/%s", noxuDefinition.Spec.Group, v.Name) _, err = instantiateVersionedCustomResource(t, instanceToCreate, noxuResourceClient, noxuDefinition, v.Name) if err != nil { t.Fatalf("unable to create noxu instance: %v", err) } noxuResourceClient.Delete(context.TODO(), "foo", metav1.DeleteOptions{}) } if err := fixtures.DeleteCustomResourceDefinition(noxuDefinition, apiExtensionClient); err != nil { t.Fatal(err) } } } func TestCustomResourceItemsValidation(t *testing.T) { tearDown, apiExtensionClient, client, err := fixtures.StartDefaultServerWithClients(t) if err != nil { t.Fatal(err) } defer tearDown() // decode CRD manifest obj, _, err := clientschema.Codecs.UniversalDeserializer().Decode([]byte(fixtureItemsAndType), nil, nil) if err != nil { t.Fatalf("failed decoding of: %v\n\n%s", err, fixtureItemsAndType) } crd := obj.(*apiextensionsv1.CustomResourceDefinition) // create CRDs t.Logf("Creating CRD %s", crd.Name) if _, err = fixtures.CreateNewV1CustomResourceDefinition(crd, apiExtensionClient, client); err != nil { t.Fatalf("unexpected create error: %v", err) } // create CR gvr := schema.GroupVersionResource{ Group: crd.Spec.Group, Version: crd.Spec.Versions[0].Name, Resource: crd.Spec.Names.Plural, } u := unstructured.Unstructured{Object: map[string]interface{}{ "apiVersion": gvr.GroupVersion().String(), "kind": crd.Spec.Names.Kind, "metadata": map[string]interface{}{ "name": "foo", }, "items-no-type": map[string]interface{}{ "items": []interface{}{ map[string]interface{}{}, }, }, "items-items-no-type": map[string]interface{}{ "items": []interface{}{ []interface{}{map[string]interface{}{}}, }, }, "items-properties-items-no-type": map[string]interface{}{ "items": []interface{}{ map[string]interface{}{ "items": []interface{}{ map[string]interface{}{}, }, }, }, }, "type-array-no-items": map[string]interface{}{ "type": "array", }, "items-and-type": map[string]interface{}{ "items": []interface{}{map[string]interface{}{}}, "type": "array", }, "issue-84880": map[string]interface{}{ "volumes": []interface{}{ map[string]interface{}{ "downwardAPI": map[string]interface{}{ "items": []interface{}{ map[string]interface{}{ "path": "annotations", }, }, }, }, }, }, }} _, err = client.Resource(gvr).Create(context.TODO(), &u, metav1.CreateOptions{}) if err != nil { t.Fatalf("unexpected error: %v", err) } } const fixtureItemsAndType = ` apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: name: foos.tests.example.com spec: group: tests.example.com version: v1beta1 names: plural: foos singular: foo kind: Foo listKind: Foolist scope: Cluster versions: - name: v1beta1 served: true storage: true schema: openAPIV3Schema: type: object properties: items-no-type: type: object properties: items: type: array items: type: object items-items-no-type: type: object properties: items: type: array items: type: array items: type: object items-properties-items-no-type: type: object properties: items: type: array items: type: object properties: items: type: array items: type: object type-array-no-items: type: object properties: type: type: string items-and-type: type: object properties: type: type: string items: type: array items: type: object default-with-items-and-no-type: type: object properties: type: type: string items: type: array items: type: object default: {"items": []} issue-84880: type: object properties: volumes: type: array items: type: object properties: downwardAPI: type: object properties: items: items: properties: path: type: string required: - path type: object type: array ` func TestCustomResourceUpdateValidation(t *testing.T) { tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t) if err != nil { t.Fatal(err) } defer tearDown() noxuDefinitions := newNoxuValidationCRDs(apiextensionsv1beta1.NamespaceScoped) for _, noxuDefinition := range noxuDefinitions { noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) if err != nil { t.Fatal(err) } ns := "not-the-default" for _, v := range noxuDefinition.Spec.Versions { noxuResourceClient := newNamespacedCustomResourceVersionedClient(ns, dynamicClient, noxuDefinition, v.Name) instanceToCreate := newNoxuValidationInstance(ns, "foo") instanceToCreate.Object["apiVersion"] = fmt.Sprintf("%s/%s", noxuDefinition.Spec.Group, v.Name) _, err = instantiateVersionedCustomResource(t, instanceToCreate, noxuResourceClient, noxuDefinition, v.Name) if err != nil { t.Fatalf("unable to create noxu instance: %v", err) } gottenNoxuInstance, err := noxuResourceClient.Get(context.TODO(), "foo", metav1.GetOptions{}) if err != nil { t.Fatal(err) } // invalidate the instance gottenNoxuInstance.Object = map[string]interface{}{ "apiVersion": "mygroup.example.com/v1beta1", "kind": "WishIHadChosenNoxu", "metadata": map[string]interface{}{ "namespace": "not-the-default", "name": "foo", }, "gamma": "bar", "delta": "hello", } _, err = noxuResourceClient.Update(context.TODO(), gottenNoxuInstance, metav1.UpdateOptions{}) if err == nil { t.Fatalf("unexpected non-error: alpha and beta should be present while updating %v", gottenNoxuInstance) } noxuResourceClient.Delete(context.TODO(), "foo", metav1.DeleteOptions{}) } if err := fixtures.DeleteCustomResourceDefinition(noxuDefinition, apiExtensionClient); err != nil { t.Fatal(err) } } } func TestCustomResourceValidationErrors(t *testing.T) { tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t) if err != nil { t.Fatal(err) } defer tearDown() noxuDefinitions := newNoxuValidationCRDs(apiextensionsv1beta1.NamespaceScoped) for _, noxuDefinition := range noxuDefinitions { noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) if err != nil { t.Fatal(err) } ns := "not-the-default" tests := []struct { name string instanceFn func() *unstructured.Unstructured expectedErrors []string }{ { name: "bad alpha", instanceFn: func() *unstructured.Unstructured { instance := newNoxuValidationInstance(ns, "foo") instance.Object["alpha"] = "foo_123!" return instance }, expectedErrors: []string{"alpha in body should match '^[a-zA-Z0-9_]*$'"}, }, { name: "bad beta", instanceFn: func() *unstructured.Unstructured { instance := newNoxuValidationInstance(ns, "foo") instance.Object["beta"] = 5 return instance }, expectedErrors: []string{"beta in body should be greater than or equal to 10"}, }, { name: "bad gamma", instanceFn: func() *unstructured.Unstructured { instance := newNoxuValidationInstance(ns, "foo") instance.Object["gamma"] = "qux" return instance }, expectedErrors: []string{`gamma: Unsupported value: "qux": supported values: "foo", "bar", "baz"`}, }, { name: "bad delta", instanceFn: func() *unstructured.Unstructured { instance := newNoxuValidationInstance(ns, "foo") instance.Object["delta"] = "foobarbaz" return instance }, expectedErrors: []string{ "must validate at least one schema (anyOf)", "delta in body should be at most 5 chars long", }, }, { name: "absent alpha and beta", instanceFn: func() *unstructured.Unstructured { instance := newNoxuValidationInstance(ns, "foo") instance.Object = map[string]interface{}{ "apiVersion": "mygroup.example.com/v1beta1", "kind": "WishIHadChosenNoxu", "metadata": map[string]interface{}{ "namespace": "not-the-default", "name": "foo", }, "gamma": "bar", "delta": "hello", } return instance }, expectedErrors: []string{"alpha: Required value", "beta: Required value"}, }, } for _, tc := range tests { for _, v := range noxuDefinition.Spec.Versions { noxuResourceClient := newNamespacedCustomResourceVersionedClient(ns, dynamicClient, noxuDefinition, v.Name) instanceToCreate := tc.instanceFn() instanceToCreate.Object["apiVersion"] = fmt.Sprintf("%s/%s", noxuDefinition.Spec.Group, v.Name) _, err := noxuResourceClient.Create(context.TODO(), instanceToCreate, metav1.CreateOptions{}) if err == nil { t.Errorf("%v: expected %v", tc.name, tc.expectedErrors) continue } // this only works when status errors contain the expect kind and version, so this effectively tests serializations too for _, expectedError := range tc.expectedErrors { if !strings.Contains(err.Error(), expectedError) { t.Errorf("%v: expected %v, got %v", tc.name, expectedError, err) } } } } if err := fixtures.DeleteCustomResourceDefinition(noxuDefinition, apiExtensionClient); err != nil { t.Fatal(err) } } } func TestCRValidationOnCRDUpdate(t *testing.T) { tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t) if err != nil { t.Fatal(err) } defer tearDown() noxuDefinitions := newNoxuValidationCRDs(apiextensionsv1beta1.NamespaceScoped) for i, noxuDefinition := range noxuDefinitions { for _, v := range noxuDefinition.Spec.Versions { // Re-define the CRD to make sure we start with a clean CRD noxuDefinition := newNoxuValidationCRDs(apiextensionsv1beta1.NamespaceScoped)[i] validationSchema, err := getSchemaForVersion(noxuDefinition, v.Name) if err != nil { t.Fatal(err) } // set stricter schema validationSchema.OpenAPIV3Schema.Required = []string{"alpha", "beta", "epsilon"} noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) if err != nil { t.Fatal(err) } ns := "not-the-default" noxuResourceClient := newNamespacedCustomResourceVersionedClient(ns, dynamicClient, noxuDefinition, v.Name) instanceToCreate := newNoxuValidationInstance(ns, "foo") instanceToCreate.Object["apiVersion"] = fmt.Sprintf("%s/%s", noxuDefinition.Spec.Group, v.Name) // CR is rejected _, err = instantiateVersionedCustomResource(t, instanceToCreate, noxuResourceClient, noxuDefinition, v.Name) if err == nil { t.Fatalf("unexpected non-error: CR should be rejected") } // update the CRD to a less stricter schema _, err = UpdateCustomResourceDefinitionWithRetry(apiExtensionClient, "noxus.mygroup.example.com", func(crd *apiextensionsv1beta1.CustomResourceDefinition) { validationSchema, err := getSchemaForVersion(crd, v.Name) if err != nil { t.Fatal(err) } validationSchema.OpenAPIV3Schema.Required = []string{"alpha", "beta"} }) if err != nil { t.Fatal(err) } // CR is now accepted err = wait.Poll(500*time.Millisecond, wait.ForeverTestTimeout, func() (bool, error) { _, err := noxuResourceClient.Create(context.TODO(), instanceToCreate, metav1.CreateOptions{}) if _, isStatus := err.(*apierrors.StatusError); isStatus { if apierrors.IsInvalid(err) { return false, nil } } if err != nil { return false, err } return true, nil }) if err != nil { t.Fatal(err) } noxuResourceClient.Delete(context.TODO(), "foo", metav1.DeleteOptions{}) if err := fixtures.DeleteCustomResourceDefinition(noxuDefinition, apiExtensionClient); err != nil { t.Fatal(err) } } } } func TestForbiddenFieldsInSchema(t *testing.T) { tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t) if err != nil { t.Fatal(err) } defer tearDown() noxuDefinitions := newNoxuValidationCRDs(apiextensionsv1beta1.NamespaceScoped) for i, noxuDefinition := range noxuDefinitions { for _, v := range noxuDefinition.Spec.Versions { // Re-define the CRD to make sure we start with a clean CRD noxuDefinition := newNoxuValidationCRDs(apiextensionsv1beta1.NamespaceScoped)[i] validationSchema, err := getSchemaForVersion(noxuDefinition, v.Name) if err != nil { t.Fatal(err) } validationSchema.OpenAPIV3Schema.AdditionalProperties.Allows = false _, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) if err == nil { t.Fatalf("unexpected non-error: additionalProperties cannot be set to false") } validationSchema.OpenAPIV3Schema.Properties["zeta"] = apiextensionsv1beta1.JSONSchemaProps{ Type: "array", UniqueItems: true, } validationSchema.OpenAPIV3Schema.AdditionalProperties.Allows = true _, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) if err == nil { t.Fatalf("unexpected non-error: uniqueItems cannot be set to true") } validationSchema.OpenAPIV3Schema.Ref = strPtr("#/definition/zeta") validationSchema.OpenAPIV3Schema.Properties["zeta"] = apiextensionsv1beta1.JSONSchemaProps{ Type: "array", UniqueItems: false, } _, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) if err == nil { t.Fatal("unexpected non-error: $ref cannot be non-empty string") } validationSchema.OpenAPIV3Schema.Ref = nil noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) if err != nil { t.Fatal(err) } if err := fixtures.DeleteCustomResourceDefinition(noxuDefinition, apiExtensionClient); err != nil { t.Fatal(err) } } } } func TestNonStructuralSchemaConditionUpdate(t *testing.T) { tearDown, apiExtensionClient, _, err := fixtures.StartDefaultServerWithClients(t) if err != nil { t.Fatal(err) } defer tearDown() manifest := ` apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: name: foos.tests.example.com spec: group: tests.example.com version: v1beta1 names: plural: foos singular: foo kind: Foo listKind: Foolist scope: Namespaced validation: openAPIV3Schema: type: object properties: a: {} versions: - name: v1beta1 served: true storage: true ` // decode CRD manifest obj, _, err := clientschema.Codecs.UniversalDeserializer().Decode([]byte(manifest), nil, nil) if err != nil { t.Fatalf("failed decoding of: %v\n\n%s", err, manifest) } crd := obj.(*apiextensionsv1beta1.CustomResourceDefinition) name := crd.Name // save schema for later origSchema := crd.Spec.Validation.OpenAPIV3Schema // create CRDs t.Logf("Creating CRD %s", crd.Name) if _, err = apiExtensionClient.ApiextensionsV1beta1().CustomResourceDefinitions().Create(context.TODO(), crd, metav1.CreateOptions{}); err != nil { t.Fatalf("unexpected create error: %v", err) } // wait for condition with violations t.Log("Waiting for NonStructuralSchema condition") var cond *apiextensionsv1beta1.CustomResourceDefinitionCondition err = wait.PollImmediate(100*time.Millisecond, 5*time.Second, func() (bool, error) { obj, err := apiExtensionClient.ApiextensionsV1beta1().CustomResourceDefinitions().Get(context.TODO(), name, metav1.GetOptions{}) if err != nil { return false, err } cond = findCRDCondition(obj, apiextensionsv1beta1.NonStructuralSchema) return cond != nil, nil }) if err != nil { t.Fatalf("unexpected error waiting for NonStructuralSchema condition: %v", cond) } if v := "spec.versions[0].schema.openAPIV3Schema.properties[a].type: Required value: must not be empty for specified object fields"; !strings.Contains(cond.Message, v) { t.Fatalf("expected violation %q, but got: %v", v, cond.Message) } // remove schema t.Log("Remove schema") for retry := 0; retry < 5; retry++ { crd, err = apiExtensionClient.ApiextensionsV1beta1().CustomResourceDefinitions().Get(context.TODO(), name, metav1.GetOptions{}) if err != nil { t.Fatalf("unexpected get error: %v", err) } crd.Spec.Validation = nil if _, err = apiExtensionClient.ApiextensionsV1beta1().CustomResourceDefinitions().Update(context.TODO(), crd, metav1.UpdateOptions{}); apierrors.IsConflict(err) { continue } if err != nil { t.Fatalf("unexpected update error: %v", err) } } if err != nil { t.Fatalf("unexpected update error: %v", err) } // wait for condition to go away t.Log("Wait for condition to disappear") err = wait.PollImmediate(100*time.Millisecond, 5*time.Second, func() (bool, error) { obj, err := apiExtensionClient.ApiextensionsV1beta1().CustomResourceDefinitions().Get(context.TODO(), name, metav1.GetOptions{}) if err != nil { return false, err } cond = findCRDCondition(obj, apiextensionsv1beta1.NonStructuralSchema) return cond == nil, nil }) if err != nil { t.Fatalf("unexpected error waiting for NonStructuralSchema condition: %v", cond) } // readd schema t.Log("Readd schema") for retry := 0; retry < 5; retry++ { crd, err = apiExtensionClient.ApiextensionsV1beta1().CustomResourceDefinitions().Get(context.TODO(), name, metav1.GetOptions{}) if err != nil { t.Fatalf("unexpected get error: %v", err) } crd.Spec.Validation = &apiextensionsv1beta1.CustomResourceValidation{OpenAPIV3Schema: origSchema} if _, err = apiExtensionClient.ApiextensionsV1beta1().CustomResourceDefinitions().Update(context.TODO(), crd, metav1.UpdateOptions{}); apierrors.IsConflict(err) { continue } if err != nil { t.Fatalf("unexpected update error: %v", err) } } if err != nil { t.Fatalf("unexpected update error: %v", err) } // wait for condition with violations t.Log("Wait for condition to reappear") err = wait.PollImmediate(100*time.Millisecond, 5*time.Second, func() (bool, error) { obj, err := apiExtensionClient.ApiextensionsV1beta1().CustomResourceDefinitions().Get(context.TODO(), name, metav1.GetOptions{}) if err != nil { return false, err } cond = findCRDCondition(obj, apiextensionsv1beta1.NonStructuralSchema) return cond != nil, nil }) if err != nil { t.Fatalf("unexpected error waiting for NonStructuralSchema condition: %v", cond) } if v := "spec.versions[0].schema.openAPIV3Schema.properties[a].type: Required value: must not be empty for specified object fields"; !strings.Contains(cond.Message, v) { t.Fatalf("expected violation %q, but got: %v", v, cond.Message) } } func TestNonStructuralSchemaCondition(t *testing.T) { tearDown, apiExtensionClient, _, err := fixtures.StartDefaultServerWithClients(t) if err != nil { t.Fatal(err) } defer tearDown() tmpl := ` apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition spec: version: v1beta1 names: plural: foos singular: foo kind: Foo listKind: Foolist scope: Namespaced validation: GLOBAL_SCHEMA versions: - name: v1beta1 served: true storage: true schema: V1BETA1_SCHEMA - name: v1 served: true schema: V1_SCHEMA ` type Test struct { desc string globalSchema, v1Schema, v1beta1Schema string expectedCreateErrors []string unexpectedCreateErrors []string expectedViolations []string unexpectedViolations []string } tests := []Test{ {"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 `, expectedCreateErrors: []string{ "spec.validation.openAPIV3Schema.x-kubernetes-preserve-unknown-fields: Invalid value: true: must be false if x-kubernetes-int-or-string is true", }, }, { desc: "int-or-string and embedded-resource true", globalSchema: ` type: object x-kubernetes-embedded-resource: true x-kubernetes-int-or-string: true `, expectedCreateErrors: []string{ "spec.validation.openAPIV3Schema.x-kubernetes-embedded-resource: Invalid value: true: must be false if x-kubernetes-int-or-string is true", }, }, { desc: "embedded-resource without preserve-unknown-fields", globalSchema: ` type: object x-kubernetes-embedded-resource: true `, 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", }, }, { desc: "embedded-resource without preserve-unknown-fields, but properties", globalSchema: ` type: object x-kubernetes-embedded-resource: true properties: apiVersion: type: string kind: type: string metadata: type: object `, expectedViolations: []string{}, }, { desc: "embedded-resource with preserve-unknown-fields", globalSchema: ` type: object x-kubernetes-embedded-resource: true x-kubernetes-preserve-unknown-fields: true `, expectedViolations: []string{}, }, { desc: "embedded-resource with wrong type", globalSchema: ` type: array x-kubernetes-embedded-resource: true x-kubernetes-preserve-unknown-fields: true `, expectedCreateErrors: []string{ "spec.validation.openAPIV3Schema.type: Invalid value: \"array\": must be object if x-kubernetes-embedded-resource is true", }, }, { desc: "embedded-resource with empty type", globalSchema: ` type: "" x-kubernetes-embedded-resource: true x-kubernetes-preserve-unknown-fields: true `, expectedCreateErrors: []string{ "spec.validation.openAPIV3Schema.type: Required value: must be object if x-kubernetes-embedded-resource is true", }, }, { desc: "no top-level type", globalSchema: ` type: "" `, expectedViolations: []string{ "spec.versions[0].schema.openAPIV3Schema.type: Required value: must not be empty at the root", }, }, { desc: "non-object top-level type", globalSchema: ` type: "integer" `, expectedViolations: []string{ "spec.versions[0].schema.openAPIV3Schema.type: Invalid value: \"integer\": must be object at the root", }, }, { desc: "forbidden in nested value validation", globalSchema: ` type: object properties: foo: type: string not: type: string additionalProperties: true title: hello description: world nullable: true allOf: - properties: foo: type: string additionalProperties: true title: hello description: world nullable: true anyOf: - items: type: string additionalProperties: true title: hello description: world nullable: true oneOf: - properties: foo: type: string additionalProperties: true title: hello description: world nullable: true `, expectedViolations: []string{ "spec.versions[0].schema.openAPIV3Schema.anyOf[0].items.type: Forbidden: must be empty to be structural", "spec.versions[0].schema.openAPIV3Schema.anyOf[0].items.additionalProperties: Forbidden: must be undefined to be structural", "spec.versions[0].schema.openAPIV3Schema.anyOf[0].items.title: Forbidden: must be empty to be structural", "spec.versions[0].schema.openAPIV3Schema.anyOf[0].items.description: Forbidden: must be empty to be structural", "spec.versions[0].schema.openAPIV3Schema.anyOf[0].items.nullable: Forbidden: must be false to be structural", "spec.versions[0].schema.openAPIV3Schema.allOf[0].properties[foo].type: Forbidden: must be empty to be structural", "spec.versions[0].schema.openAPIV3Schema.allOf[0].properties[foo].additionalProperties: Forbidden: must be undefined to be structural", "spec.versions[0].schema.openAPIV3Schema.allOf[0].properties[foo].title: Forbidden: must be empty to be structural", "spec.versions[0].schema.openAPIV3Schema.allOf[0].properties[foo].description: Forbidden: must be empty to be structural", "spec.versions[0].schema.openAPIV3Schema.allOf[0].properties[foo].nullable: Forbidden: must be false to be structural", "spec.versions[0].schema.openAPIV3Schema.oneOf[0].properties[foo].type: Forbidden: must be empty to be structural", "spec.versions[0].schema.openAPIV3Schema.oneOf[0].properties[foo].additionalProperties: Forbidden: must be undefined to be structural", "spec.versions[0].schema.openAPIV3Schema.oneOf[0].properties[foo].title: Forbidden: must be empty to be structural", "spec.versions[0].schema.openAPIV3Schema.oneOf[0].properties[foo].description: Forbidden: must be empty to be structural", "spec.versions[0].schema.openAPIV3Schema.oneOf[0].properties[foo].nullable: Forbidden: must be false to be structural", "spec.versions[0].schema.openAPIV3Schema.not.type: Forbidden: must be empty to be structural", "spec.versions[0].schema.openAPIV3Schema.not.additionalProperties: Forbidden: must be undefined to be structural", "spec.versions[0].schema.openAPIV3Schema.not.title: Forbidden: must be empty to be structural", "spec.versions[0].schema.openAPIV3Schema.not.description: Forbidden: must be empty to be structural", "spec.versions[0].schema.openAPIV3Schema.not.nullable: Forbidden: must be false to be structural", "spec.versions[0].schema.openAPIV3Schema.items: Required value: because it is defined in spec.versions[0].schema.openAPIV3Schema.anyOf[0].items", }, unexpectedViolations: []string{ "spec.versions[0].schema.openAPIV3Schema.not.default", }, }, { desc: "invalid regex pattern", globalSchema: ` type: object properties: foo: type: string pattern: "+" `, expectedViolations: []string{ "spec.versions[0].schema.openAPIV3Schema.properties[foo].pattern: Invalid value: \"+\": must be a valid regular expression, but isn't: error parsing regexp: missing argument to repetition operator: `+`", }, }, { desc: "forbidden vendor extensions in nested value validation", globalSchema: ` type: object properties: int-or-string: x-kubernetes-int-or-string: true embedded-resource: type: object x-kubernetes-embedded-resource: true x-kubernetes-preserve-unknown-fields: true not: properties: int-or-string: x-kubernetes-int-or-string: true embedded-resource: x-kubernetes-embedded-resource: true x-kubernetes-preserve-unknown-fields: true allOf: - properties: int-or-string: x-kubernetes-int-or-string: true embedded-resource: x-kubernetes-embedded-resource: true x-kubernetes-preserve-unknown-fields: true anyOf: - properties: int-or-string: x-kubernetes-int-or-string: true embedded-resource: x-kubernetes-embedded-resource: true x-kubernetes-preserve-unknown-fields: true oneOf: - properties: int-or-string: x-kubernetes-int-or-string: true embedded-resource: x-kubernetes-embedded-resource: true x-kubernetes-preserve-unknown-fields: true `, 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", "spec.validation.openAPIV3Schema.anyOf[0].properties[embedded-resource].x-kubernetes-preserve-unknown-fields: Forbidden: must be false to be structural", "spec.validation.openAPIV3Schema.anyOf[0].properties[embedded-resource].x-kubernetes-embedded-resource: Forbidden: must be false to be structural", "spec.validation.openAPIV3Schema.anyOf[0].properties[int-or-string].x-kubernetes-int-or-string: Forbidden: must be false to be structural", "spec.validation.openAPIV3Schema.oneOf[0].properties[embedded-resource].x-kubernetes-preserve-unknown-fields: Forbidden: must be false to be structural", "spec.validation.openAPIV3Schema.oneOf[0].properties[embedded-resource].x-kubernetes-embedded-resource: Forbidden: must be false to be structural", "spec.validation.openAPIV3Schema.oneOf[0].properties[int-or-string].x-kubernetes-int-or-string: Forbidden: must be false to be structural", "spec.validation.openAPIV3Schema.not.properties[embedded-resource].x-kubernetes-preserve-unknown-fields: Forbidden: must be false to be structural", "spec.validation.openAPIV3Schema.not.properties[embedded-resource].x-kubernetes-embedded-resource: Forbidden: must be false to be structural", "spec.validation.openAPIV3Schema.not.properties[int-or-string].x-kubernetes-int-or-string: Forbidden: must be false to be structural", }, }, { desc: "missing types with extensions", globalSchema: ` properties: foo: properties: a: {} bar: items: additionalProperties: properties: a: {} items: {} abc: additionalProperties: properties: a: items: additionalProperties: items: json: x-kubernetes-preserve-unknown-fields: true properties: a: {} int-or-string: x-kubernetes-int-or-string: true properties: a: {} `, 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", "spec.validation.openAPIV3Schema.properties[json].properties[a].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: "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.versions[0].schema.openAPIV3Schema.properties[foo].properties[a].type: Required value: must not be empty for specified object fields", "spec.versions[0].schema.openAPIV3Schema.properties[foo].type: Required value: must not be empty for specified object fields", "spec.versions[0].schema.openAPIV3Schema.properties[abc].additionalProperties.properties[a].items.additionalProperties.type: Required value: must not be empty for specified object fields", "spec.versions[0].schema.openAPIV3Schema.properties[abc].additionalProperties.properties[a].items.type: Required value: must not be empty for specified array items", "spec.versions[0].schema.openAPIV3Schema.properties[abc].additionalProperties.properties[a].type: Required value: must not be empty for specified object fields", "spec.versions[0].schema.openAPIV3Schema.properties[abc].additionalProperties.type: Required value: must not be empty for specified object fields", "spec.versions[0].schema.openAPIV3Schema.properties[abc].type: Required value: must not be empty for specified object fields", "spec.versions[0].schema.openAPIV3Schema.properties[bar].items.additionalProperties.items.type: Required value: must not be empty for specified array items", "spec.versions[0].schema.openAPIV3Schema.properties[bar].items.additionalProperties.properties[a].type: Required value: must not be empty for specified object fields", "spec.versions[0].schema.openAPIV3Schema.properties[bar].items.additionalProperties.type: Required value: must not be empty for specified object fields", "spec.versions[0].schema.openAPIV3Schema.properties[bar].items.type: Required value: must not be empty for specified array items", "spec.versions[0].schema.openAPIV3Schema.properties[bar].type: Required value: must not be empty for specified object fields", "spec.versions[0].schema.openAPIV3Schema.type: Required value: must not be empty at the root", }, }, { desc: "int-or-string variants", globalSchema: ` type: object properties: a: x-kubernetes-int-or-string: true b: x-kubernetes-int-or-string: true anyOf: - type: integer - type: string allOf: - pattern: abc c: x-kubernetes-int-or-string: true allOf: - anyOf: - type: integer - type: string - pattern: abc - pattern: abc d: x-kubernetes-int-or-string: true anyOf: - type: integer - type: string pattern: abc e: x-kubernetes-int-or-string: true allOf: - anyOf: - type: integer - type: string pattern: abc - pattern: abc f: x-kubernetes-int-or-string: true anyOf: - type: integer - type: string - pattern: abc g: x-kubernetes-int-or-string: true anyOf: - type: string - type: integer `, 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", "spec.validation.openAPIV3Schema.properties[e].allOf[0].anyOf[1].type: Forbidden: must be empty to be structural", "spec.validation.openAPIV3Schema.properties[f].anyOf[0].type: Forbidden: must be empty to be structural", "spec.validation.openAPIV3Schema.properties[f].anyOf[1].type: Forbidden: must be empty to be structural", "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", }, unexpectedCreateErrors: []string{ "spec.validation.openAPIV3Schema.properties[a]", "spec.validation.openAPIV3Schema.properties[b]", "spec.validation.openAPIV3Schema.properties[c]", }, }, { desc: "forbidden additionalProperties at the root", globalSchema: ` type: object additionalProperties: false `, expectedViolations: []string{ "spec.versions[0].schema.openAPIV3Schema.additionalProperties: Forbidden: must not be used at the root", }, }, { desc: "structural incomplete", globalSchema: ` type: object properties: b: type: object properties: b: type: array c: type: array items: type: object d: type: array not: properties: a: {} b: not: properties: a: {} b: items: {} c: items: not: items: properties: a: {} d: items: {} allOf: - properties: e: {} anyOf: - properties: f: {} oneOf: - properties: g: {} `, expectedViolations: []string{ "spec.versions[0].schema.openAPIV3Schema.properties[d].items: Required value: because it is defined in spec.versions[0].schema.openAPIV3Schema.not.properties[d].items", "spec.versions[0].schema.openAPIV3Schema.properties[a]: Required value: because it is defined in spec.versions[0].schema.openAPIV3Schema.not.properties[a]", "spec.versions[0].schema.openAPIV3Schema.properties[b].properties[a]: Required value: because it is defined in spec.versions[0].schema.openAPIV3Schema.not.properties[b].not.properties[a]", "spec.versions[0].schema.openAPIV3Schema.properties[b].properties[b].items: Required value: because it is defined in spec.versions[0].schema.openAPIV3Schema.not.properties[b].not.properties[b].items", "spec.versions[0].schema.openAPIV3Schema.properties[c].items.items: Required value: because it is defined in spec.versions[0].schema.openAPIV3Schema.not.properties[c].items.not.items", "spec.versions[0].schema.openAPIV3Schema.properties[e]: Required value: because it is defined in spec.versions[0].schema.openAPIV3Schema.allOf[0].properties[e]", "spec.versions[0].schema.openAPIV3Schema.properties[f]: Required value: because it is defined in spec.versions[0].schema.openAPIV3Schema.anyOf[0].properties[f]", "spec.versions[0].schema.openAPIV3Schema.properties[g]: Required value: because it is defined in spec.versions[0].schema.openAPIV3Schema.oneOf[0].properties[g]", }, }, { desc: "structural complete", globalSchema: ` type: object properties: a: type: string b: type: object properties: a: type: string b: type: array items: type: string c: type: array items: type: array items: type: object properties: a: type: string d: type: array items: type: string e: type: string f: type: string g: type: string not: properties: a: {} b: not: properties: a: {} b: items: {} c: items: not: items: properties: a: {} d: items: {} allOf: - properties: e: {} anyOf: - properties: f: {} oneOf: - properties: g: {} `, expectedViolations: nil, }, { desc: "invalid v1beta1 schema", v1beta1Schema: ` type: object properties: a: {} not: properties: b: {} `, v1Schema: ` type: object properties: a: type: string `, expectedViolations: []string{ "spec.versions[0].schema.openAPIV3Schema.properties[a].type: Required value: must not be empty for specified object fields", "spec.versions[0].schema.openAPIV3Schema.properties[b]: Required value: because it is defined in spec.versions[0].schema.openAPIV3Schema.not.properties[b]", }, }, { desc: "invalid v1beta1 and v1 schemas", v1beta1Schema: ` type: object properties: a: {} not: properties: b: {} `, v1Schema: ` type: object properties: c: {} not: properties: d: {} `, expectedViolations: []string{ "spec.versions[0].schema.openAPIV3Schema.properties[a].type: Required value: must not be empty for specified object fields", "spec.versions[0].schema.openAPIV3Schema.properties[b]: Required value: because it is defined in spec.versions[0].schema.openAPIV3Schema.not.properties[b]", "spec.versions[1].schema.openAPIV3Schema.properties[c].type: Required value: must not be empty for specified object fields", "spec.versions[1].schema.openAPIV3Schema.properties[d]: Required value: because it is defined in spec.versions[1].schema.openAPIV3Schema.not.properties[d]", }, }, { desc: "metadata with non-properties", globalSchema: ` type: object properties: metadata: minimum: 42.0 `, expectedViolations: []string{ "spec.versions[0].schema.openAPIV3Schema.properties[metadata]: Forbidden: must not specify anything other than name and generateName, but metadata is implicitly specified", "spec.versions[0].schema.openAPIV3Schema.properties[metadata].type: Required value: must not be empty for specified object fields", }, }, { desc: "metadata with other properties", globalSchema: ` type: object properties: metadata: properties: name: pattern: "^[a-z]+$" labels: type: object maxLength: 4 `, expectedViolations: []string{ "spec.versions[0].schema.openAPIV3Schema.properties[metadata]: Forbidden: must not specify anything other than name and generateName, but metadata is implicitly specified", "spec.versions[0].schema.openAPIV3Schema.properties[metadata].type: Required value: must not be empty for specified object fields", "spec.versions[0].schema.openAPIV3Schema.properties[metadata].properties[name].type: Required value: must not be empty for specified object fields", }, }, { desc: "metadata with name property", globalSchema: ` type: object properties: metadata: type: object properties: name: type: string pattern: "^[a-z]+$" `, expectedViolations: []string{}, }, { desc: "metadata with generateName property", globalSchema: ` type: object properties: metadata: type: object properties: generateName: type: string pattern: "^[a-z]+$" `, expectedViolations: []string{}, }, { desc: "metadata with name and generateName property", globalSchema: ` type: object properties: metadata: type: object properties: name: type: string pattern: "^[a-z]+$" generateName: type: string pattern: "^[a-z]+$" `, expectedViolations: []string{}, }, { desc: "metadata under junctors", globalSchema: ` type: object properties: metadata: type: object properties: name: type: string pattern: "^[a-z]+$" allOf: - properties: metadata: {} anyOf: - properties: metadata: {} oneOf: - properties: metadata: {} not: properties: metadata: {} `, expectedViolations: []string{ "spec.versions[0].schema.openAPIV3Schema.anyOf[0].properties[metadata]: Forbidden: must not be specified in a nested context", "spec.versions[0].schema.openAPIV3Schema.allOf[0].properties[metadata]: Forbidden: must not be specified in a nested context", "spec.versions[0].schema.openAPIV3Schema.oneOf[0].properties[metadata]: Forbidden: must not be specified in a nested context", "spec.versions[0].schema.openAPIV3Schema.not.properties[metadata]: Forbidden: must not be specified in a nested context", }, }, { desc: "missing items for array", globalSchema: ` type: object properties: slice: type: array `, expectedViolations: []string{ "spec.versions[0].schema.openAPIV3Schema.properties[slice].items: Required value: must be specified", }, }, { desc: "items slice", globalSchema: ` type: object properties: slice: type: array items: - type: string - type: integer `, 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", globalSchema: ` type: object properties: slice: type: array items: type: string not: items: - type: string `, expectedCreateErrors: []string{"spec.validation.openAPIV3Schema.properties[slice].not.items: Forbidden: items must be a schema object and not an array"}, }, } for i := range tests { tst := tests[i] t.Run(tst.desc, func(t *testing.T) { // plug in schemas manifest := strings.NewReplacer( "GLOBAL_SCHEMA", toValidationJSON(tst.globalSchema), "V1BETA1_SCHEMA", toValidationJSON(tst.v1beta1Schema), "V1_SCHEMA", toValidationJSON(tst.v1Schema), ).Replace(tmpl) // decode CRD manifest obj, _, err := clientschema.Codecs.UniversalDeserializer().Decode([]byte(manifest), nil, nil) if err != nil { t.Fatalf("failed decoding of: %v\n\n%s", err, manifest) } crd := obj.(*apiextensionsv1beta1.CustomResourceDefinition) crd.Spec.Group = fmt.Sprintf("tests-%d.apiextension.k8s.io", i) crd.Name = fmt.Sprintf("foos.%s", crd.Spec.Group) // create CRDs crd, err = apiExtensionClient.ApiextensionsV1beta1().CustomResourceDefinitions().Create(context.TODO(), crd, metav1.CreateOptions{}) 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 } if len(tst.expectedViolations) == 0 { // wait for condition to not appear var cond *apiextensionsv1beta1.CustomResourceDefinitionCondition err := wait.PollImmediate(100*time.Millisecond, 5*time.Second, func() (bool, error) { obj, err := apiExtensionClient.ApiextensionsV1beta1().CustomResourceDefinitions().Get(context.TODO(), crd.Name, metav1.GetOptions{}) if err != nil { return false, err } cond = findCRDCondition(obj, apiextensionsv1beta1.NonStructuralSchema) if cond == nil { return false, nil } return true, nil }) if err != wait.ErrWaitTimeout { t.Fatalf("expected no NonStructuralSchema condition, but got one: %v", cond) } return } // wait for condition to appear with the given violations var cond *apiextensionsv1beta1.CustomResourceDefinitionCondition err = wait.PollImmediate(100*time.Millisecond, wait.ForeverTestTimeout, func() (bool, error) { obj, err := apiExtensionClient.ApiextensionsV1beta1().CustomResourceDefinitions().Get(context.TODO(), crd.Name, metav1.GetOptions{}) if err != nil { return false, err } cond = findCRDCondition(obj, apiextensionsv1beta1.NonStructuralSchema) if cond != nil { return true, nil } return false, nil }) if err != nil { t.Fatalf("unexpected error waiting for violations in NonStructuralSchema condition: %v", err) } // check that the condition looks good if cond.Reason != "Violations" { t.Errorf("expected reason Violations, got: %v", cond.Reason) } if cond.Status != apiextensionsv1beta1.ConditionTrue { t.Errorf("expected reason True, got: %v", cond.Status) } // check that we got all violations t.Logf("Got violations: %q", cond.Message) for _, v := range tst.expectedViolations { if strings.Index(cond.Message, v) == -1 { t.Errorf("expected violation %q, but didn't get it", v) } } for _, v := range tst.unexpectedViolations { if strings.Index(cond.Message, v) != -1 { t.Errorf("unexpected violation %q", v) } } }) } } // findCRDCondition returns the condition you're looking for or nil. func findCRDCondition(crd *apiextensionsv1beta1.CustomResourceDefinition, conditionType apiextensionsv1beta1.CustomResourceDefinitionConditionType) *apiextensionsv1beta1.CustomResourceDefinitionCondition { for i := range crd.Status.Conditions { if crd.Status.Conditions[i].Type == conditionType { return &crd.Status.Conditions[i] } } return nil } func toValidationJSON(yml string) string { if len(yml) == 0 { return "null" } bs, err := yaml.ToJSON([]byte(yml)) if err != nil { panic(err) } return fmt.Sprintf("{\"openAPIV3Schema\": %s}", string(bs)) } func float64Ptr(f float64) *float64 { return &f } func int64Ptr(f int64) *int64 { return &f } func strPtr(str string) *string { return &str }