From b1e16bff17a2e3f50ac3af9708559f5031dbc1c4 Mon Sep 17 00:00:00 2001 From: Nikhita Raghunath Date: Fri, 23 Jun 2017 05:04:32 +0530 Subject: [PATCH] Add integration tests Update test schema Add polling for TestCRValidationOnCRDUpdate Add tests for forbidden fields Enable featureGate for CustomResourceValidation --- .../test/integration/BUILD | 1 + .../test/integration/testserver/resources.go | 7 +- .../test/integration/validation_test.go | 372 +++++++++++++++++- 3 files changed, 378 insertions(+), 2 deletions(-) diff --git a/staging/src/k8s.io/apiextensions-apiserver/test/integration/BUILD b/staging/src/k8s.io/apiextensions-apiserver/test/integration/BUILD index 482ab94a39e..bea580200d5 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/test/integration/BUILD +++ b/staging/src/k8s.io/apiextensions-apiserver/test/integration/BUILD @@ -32,6 +32,7 @@ go_test( "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/wait:go_default_library", "//vendor/k8s.io/apimachinery/pkg/watch:go_default_library", + "//vendor/k8s.io/apiserver/pkg/util/feature:go_default_library", "//vendor/k8s.io/client-go/dynamic:go_default_library", ], ) diff --git a/staging/src/k8s.io/apiextensions-apiserver/test/integration/testserver/resources.go b/staging/src/k8s.io/apiextensions-apiserver/test/integration/testserver/resources.go index a0efcb43da7..f80ad8bf6e8 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/test/integration/testserver/resources.go +++ b/staging/src/k8s.io/apiextensions-apiserver/test/integration/testserver/resources.go @@ -213,7 +213,7 @@ func checkForWatchCachePrimed(crd *apiextensionsv1beta1.CustomResourceDefinition return err } - instanceName := "foo" + instanceName := "setup-instance" instance := &unstructured.Unstructured{ Object: map[string]interface{}{ "apiVersion": crd.Spec.Group + "/" + crd.Spec.Version, @@ -222,6 +222,11 @@ func checkForWatchCachePrimed(crd *apiextensionsv1beta1.CustomResourceDefinition "namespace": ns, "name": instanceName, }, + "alpha": "foo_123", + "beta": 10, + "gamma": "bar", + "delta": "hello", + "epsilon": "foobar", }, } if _, err := resourceClient.Create(instance); err != nil { diff --git a/staging/src/k8s.io/apiextensions-apiserver/test/integration/validation_test.go b/staging/src/k8s.io/apiextensions-apiserver/test/integration/validation_test.go index 69ccb58aa4a..3c771ef1825 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/test/integration/validation_test.go +++ b/staging/src/k8s.io/apiextensions-apiserver/test/integration/validation_test.go @@ -19,10 +19,15 @@ package integration import ( "strings" "testing" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/util/wait" + utilfeature "k8s.io/apiserver/pkg/util/feature" apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" "k8s.io/apiextensions-apiserver/test/integration/testserver" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) func TestForProperValidationErrors(t *testing.T) { @@ -79,3 +84,368 @@ func TestForProperValidationErrors(t *testing.T) { } } } + +func newNoxuValidationCRD(scope apiextensionsv1beta1.ResourceScope) *apiextensionsv1beta1.CustomResourceDefinition { + 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: &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), + }, + }, + }, + }, + }, + }, + }, + } +} + +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) { + stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer() + if err != nil { + t.Fatal(err) + } + defer close(stopCh) + + // enable alpha feature CustomResourceValidation + err = utilfeature.DefaultFeatureGate.Set("CustomResourceValidation=true") + if err != nil { + t.Errorf("failed to enable feature gate for CustomResourceValidation: %v", err) + } + + noxuDefinition := newNoxuValidationCRD(apiextensionsv1beta1.NamespaceScoped) + noxuVersionClient, err := testserver.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, clientPool) + if err != nil { + t.Fatal(err) + } + + ns := "not-the-default" + noxuResourceClient := NewNamespacedCustomResourceClient(ns, noxuVersionClient, noxuDefinition) + _, err = instantiateCustomResource(t, newNoxuValidationInstance(ns, "foo"), noxuResourceClient, noxuDefinition) + if err != nil { + t.Fatalf("unable to create noxu instance: %v", err) + } +} + +func TestCustomResourceUpdateValidation(t *testing.T) { + stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer() + if err != nil { + t.Fatal(err) + } + defer close(stopCh) + + // enable alpha feature CustomResourceValidation + err = utilfeature.DefaultFeatureGate.Set("CustomResourceValidation=true") + if err != nil { + t.Errorf("failed to enable feature gate for CustomResourceValidation: %v", err) + } + + noxuDefinition := newNoxuValidationCRD(apiextensionsv1beta1.NamespaceScoped) + noxuVersionClient, err := testserver.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, clientPool) + if err != nil { + t.Fatal(err) + } + + ns := "not-the-default" + noxuResourceClient := NewNamespacedCustomResourceClient(ns, noxuVersionClient, noxuDefinition) + _, err = instantiateCustomResource(t, newNoxuValidationInstance(ns, "foo"), noxuResourceClient, noxuDefinition) + if err != nil { + t.Fatalf("unable to create noxu instance: %v", err) + } + + gottenNoxuInstance, err := noxuResourceClient.Get("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(gottenNoxuInstance) + if err == nil { + t.Fatalf("unexpected non-error: alpha and beta should be present while updating %v", gottenNoxuInstance) + } +} + +func TestCustomResourceValidationErrors(t *testing.T) { + stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer() + if err != nil { + t.Fatal(err) + } + defer close(stopCh) + + // enable alpha feature CustomResourceValidation + err = utilfeature.DefaultFeatureGate.Set("CustomResourceValidation=true") + if err != nil { + t.Errorf("failed to enable feature gate for CustomResourceValidation: %v", err) + } + + noxuDefinition := newNoxuValidationCRD(apiextensionsv1beta1.NamespaceScoped) + noxuVersionClient, err := testserver.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, clientPool) + if err != nil { + t.Fatal(err) + } + + ns := "not-the-default" + noxuResourceClient := NewNamespacedCustomResourceClient(ns, noxuVersionClient, noxuDefinition) + + tests := []struct { + name string + instanceFn func() *unstructured.Unstructured + expectedError string + }{ + { + name: "bad alpha", + instanceFn: func() *unstructured.Unstructured { + instance := newNoxuValidationInstance(ns, "foo") + instance.Object["alpha"] = "foo_123!" + return instance + }, + expectedError: "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 + }, + expectedError: "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 + }, + expectedError: "gamma in body should be one of [foo bar baz]", + }, + { + name: "bad delta", + instanceFn: func() *unstructured.Unstructured { + instance := newNoxuValidationInstance(ns, "foo") + instance.Object["delta"] = "foobarbaz" + return instance + }, + expectedError: "must validate at least one schema (anyOf)\ndelta 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 + }, + expectedError: ".alpha in body is required\n.beta in body is required", + }, + } + + for _, tc := range tests { + _, err := noxuResourceClient.Create(tc.instanceFn()) + 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 TestCRValidationOnCRDUpdate(t *testing.T) { + stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer() + if err != nil { + t.Fatal(err) + } + defer close(stopCh) + + // enable alpha feature CustomResourceValidation + err = utilfeature.DefaultFeatureGate.Set("CustomResourceValidation=true") + if err != nil { + t.Errorf("failed to enable feature gate for CustomResourceValidation: %v", err) + } + + noxuDefinition := newNoxuValidationCRD(apiextensionsv1beta1.NamespaceScoped) + + // set stricter schema + noxuDefinition.Spec.Validation.OpenAPIV3Schema.Required = []string{"alpha", "beta", "epsilon"} + + noxuVersionClient, err := testserver.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, clientPool) + if err != nil { + t.Fatal(err) + } + ns := "not-the-default" + noxuResourceClient := NewNamespacedCustomResourceClient(ns, noxuVersionClient, noxuDefinition) + + // CR is rejected + _, err = instantiateCustomResource(t, newNoxuValidationInstance(ns, "foo"), noxuResourceClient, noxuDefinition) + if err == nil { + t.Fatalf("unexpected non-error: CR should be rejected") + } + + gottenCRD, err := apiExtensionClient.ApiextensionsV1beta1().CustomResourceDefinitions().Get("noxus.mygroup.example.com", metav1.GetOptions{}) + if err != nil { + t.Fatal(err) + } + + // update the CRD to a less stricter schema + gottenCRD.Spec.Validation.OpenAPIV3Schema.Required = []string{"alpha", "beta"} + + updatedCRD, err := apiExtensionClient.ApiextensionsV1beta1().CustomResourceDefinitions().Update(gottenCRD) + if err != nil { + t.Fatal(err) + } + + // CR is now accepted + err = wait.Poll(500*time.Millisecond, wait.ForeverTestTimeout, func() (bool, error) { + _, err = instantiateCustomResource(t, newNoxuValidationInstance(ns, "foo"), noxuResourceClient, updatedCRD) + if err != nil { + return false, err + } + return true, nil + }) + if err != nil { + t.Fatal(err) + } +} + +func TestForbiddenFieldsInSchema(t *testing.T) { + stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer() + if err != nil { + t.Fatal(err) + } + defer close(stopCh) + + // enable alpha feature CustomResourceValidation + err = utilfeature.DefaultFeatureGate.Set("CustomResourceValidation=true") + if err != nil { + t.Errorf("failed to enable feature gate for CustomResourceValidation: %v", err) + } + + noxuDefinition := newNoxuValidationCRD(apiextensionsv1beta1.NamespaceScoped) + noxuDefinition.Spec.Validation.OpenAPIV3Schema.AdditionalProperties.Allows = false + + _, err = testserver.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, clientPool) + if err == nil { + t.Fatalf("unexpected non-error: additionalProperties cannot be set to false") + } + + noxuDefinition.Spec.Validation.OpenAPIV3Schema.Properties["zeta"] = apiextensionsv1beta1.JSONSchemaProps{ + Type: "array", + UniqueItems: true, + } + noxuDefinition.Spec.Validation.OpenAPIV3Schema.AdditionalProperties.Allows = true + + _, err = testserver.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, clientPool) + if err == nil { + t.Fatalf("unexpected non-error: uniqueItems cannot be set to true") + } + + noxuDefinition.Spec.Validation.OpenAPIV3Schema.Properties["zeta"] = apiextensionsv1beta1.JSONSchemaProps{ + Type: "array", + UniqueItems: false, + } + + _, err = testserver.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, clientPool) + if err != nil { + t.Fatal(err) + } + +} + +func float64Ptr(f float64) *float64 { + return &f +} + +func int64Ptr(f int64) *int64 { + return &f +}