From 591f00e9b3f8440e1d92c47d74ad819e47aa0a44 Mon Sep 17 00:00:00 2001 From: Haowei Cai Date: Fri, 1 Mar 2019 13:58:13 -0800 Subject: [PATCH] test client-side validation behavior for CRD support version-skew testing --- test/e2e/framework/crd_util.go | 13 ++++ test/e2e/framework/util.go | 5 ++ test/e2e/kubectl/kubectl.go | 132 +++++++++++++++++++++++++++++++++ 3 files changed, 150 insertions(+) diff --git a/test/e2e/framework/crd_util.go b/test/e2e/framework/crd_util.go index 0bd0365dfff..5b465a82643 100644 --- a/test/e2e/framework/crd_util.go +++ b/test/e2e/framework/crd_util.go @@ -24,6 +24,8 @@ import ( "k8s.io/apiextensions-apiserver/test/integration/fixtures" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + utilyaml "k8s.io/apimachinery/pkg/util/yaml" "k8s.io/client-go/dynamic" ) @@ -185,3 +187,14 @@ func (c *TestCrd) GetAPIVersions() []string { func (c *TestCrd) GetV1DynamicClient() dynamic.ResourceInterface { return c.DynamicClients["v1"] } + +// PatchSchema takes validation schema in YAML and patches it to given CRD +func (c *TestCrd) PatchSchema(schema []byte) error { + s, err := utilyaml.ToJSON(schema) + if err != nil { + return fmt.Errorf("failed to create json patch: %v", err) + } + patch := []byte(fmt.Sprintf(`{"spec":{"validation":{"openAPIV3Schema":%s}}}`, string(s))) + c.Crd, err = c.ApiExtensionClient.ApiextensionsV1beta1().CustomResourceDefinitions().Patch(c.GetMetaName(), types.MergePatchType, patch) + return err +} diff --git a/test/e2e/framework/util.go b/test/e2e/framework/util.go index 72ce584f770..f6281dc573a 100644 --- a/test/e2e/framework/util.go +++ b/test/e2e/framework/util.go @@ -2371,6 +2371,11 @@ func RunKubectlOrDieInput(data string, args ...string) string { return NewKubectlCommand(args...).WithStdinData(data).ExecOrDie() } +// RunKubectlInput is a convenience wrapper over kubectlBuilder that takes input to stdin +func RunKubectlInput(data string, args ...string) (string, error) { + return NewKubectlCommand(args...).WithStdinData(data).Exec() +} + // RunKubemciWithKubeconfig is a convenience wrapper over RunKubemciCmd func RunKubemciWithKubeconfig(args ...string) (string, error) { if TestContext.KubeConfig != "" { diff --git a/test/e2e/kubectl/kubectl.go b/test/e2e/kubectl/kubectl.go index 59c617c628f..ef0c5dcb41e 100644 --- a/test/e2e/kubectl/kubectl.go +++ b/test/e2e/kubectl/kubectl.go @@ -88,6 +88,7 @@ const ( nginxDeployment1Filename = "nginx-deployment1.yaml.in" nginxDeployment2Filename = "nginx-deployment2.yaml.in" nginxDeployment3Filename = "nginx-deployment3.yaml.in" + metaPattern = `"kind":"%s","apiVersion":"%s/%s","metadata":{"name":"%s"}` ) var ( @@ -819,6 +820,69 @@ metadata: }) }) + framework.KubeDescribe("Kubectl client-side validation", func() { + ginkgo.It("should create/apply a CR with unknown fields for CRD with no validation schema", func() { + ginkgo.By("create CRD with no validation schema") + crd, err := framework.CreateTestCRD(f) + if err != nil { + framework.Failf("failed to create test CRD: %v", err) + } + defer crd.CleanUp() + + ginkgo.By("sleep for 10s to wait for potential crd openapi publishing alpha feature") + time.Sleep(10 * time.Second) + + meta := fmt.Sprintf(metaPattern, crd.Kind, crd.ApiGroup, crd.Versions[0].Name, "test-cr") + randomCR := fmt.Sprintf(`{%s,"a":{"b":[{"c":"d"}]}}`, meta) + if err := createApplyCustomResource(randomCR, f.Namespace.Name, "test-cr", crd); err != nil { + framework.Failf("%v", err) + } + }) + + ginkgo.It("should create/apply a valid CR for CRD with validation schema", func() { + ginkgo.By("prepare CRD with validation schema") + crd, err := framework.CreateTestCRD(f) + if err != nil { + framework.Failf("failed to create test CRD: %v", err) + } + defer crd.CleanUp() + if err := crd.PatchSchema(schemaFoo); err != nil { + framework.Failf("%v", err) + } + + ginkgo.By("sleep for 10s to wait for potential crd openapi publishing alpha feature") + time.Sleep(10 * time.Second) + + meta := fmt.Sprintf(metaPattern, crd.Kind, crd.ApiGroup, crd.Versions[0].Name, "test-cr") + validCR := fmt.Sprintf(`{%s,"spec":{"bars":[{"name":"test-bar"}]}}`, meta) + if err := createApplyCustomResource(validCR, f.Namespace.Name, "test-cr", crd); err != nil { + framework.Failf("%v", err) + } + }) + + ginkgo.It("should create/apply a valid CR with arbitrary-extra properties for CRD with partially-specified validation schema", func() { + ginkgo.By("prepare CRD with partially-specified validation schema") + crd, err := framework.CreateTestCRD(f) + if err != nil { + framework.Failf("failed to create test CRD: %v", err) + } + defer crd.CleanUp() + if err := crd.PatchSchema(schemaFoo); err != nil { + framework.Failf("%v", err) + } + + ginkgo.By("sleep for 10s to wait for potential crd openapi publishing alpha feature") + time.Sleep(10 * time.Second) + + meta := fmt.Sprintf(metaPattern, crd.Kind, crd.ApiGroup, crd.Versions[0].Name, "test-cr") + validArbitraryCR := fmt.Sprintf(`{%s,"spec":{"bars":[{"name":"test-bar"}],"extraProperty":"arbitrary-value"}}`, meta) + if err := createApplyCustomResource(validArbitraryCR, f.Namespace.Name, "test-cr", crd); err != nil { + framework.Failf("%v", err) + } + }) + + }) + framework.KubeDescribe("Kubectl cluster-info", func() { /* Release : v1.9 @@ -2159,3 +2223,71 @@ func startLocalProxy() (srv *httptest.Server, logs *bytes.Buffer) { p.Logger = log.New(logs, "", 0) return httptest.NewServer(p), logs } + +// createApplyCustomResource asserts that given CustomResource be created and applied +// without being rejected by client-side validation +func createApplyCustomResource(resource, namespace, name string, crd *framework.TestCrd) error { + ns := fmt.Sprintf("--namespace=%v", namespace) + ginkgo.By("successfully create CR") + if _, err := framework.RunKubectlInput(resource, ns, "create", "-f", "-"); err != nil { + return fmt.Errorf("failed to create CR %s in namespace %s: %v", resource, ns, err) + } + if _, err := framework.RunKubectl(ns, "delete", crd.GetPluralName(), name); err != nil { + return fmt.Errorf("failed to delete CR %s: %v", name, err) + } + ginkgo.By("successfully apply CR") + if _, err := framework.RunKubectlInput(resource, ns, "apply", "-f", "-"); err != nil { + return fmt.Errorf("failed to apply CR %s in namespace %s: %v", resource, ns, err) + } + if _, err := framework.RunKubectl(ns, "delete", crd.GetPluralName(), name); err != nil { + return fmt.Errorf("failed to delete CR %s: %v", name, err) + } + return nil +} + +var schemaFoo = []byte(`description: Foo CRD for Testing +type: object +properties: + spec: + type: object + description: Specification of Foo + properties: + bars: + description: List of Bars and their specs. + type: array + items: + type: object + required: + - name + properties: + name: + description: Name of Bar. + type: string + age: + description: Age of Bar. + type: string + bazs: + description: List of Bazs. + items: + type: string + type: array + status: + description: Status of Foo + type: object + properties: + bars: + description: List of Bars and their statuses. + type: array + items: + type: object + properties: + name: + description: Name of Bar. + type: string + available: + description: Whether the Bar is installed. + type: boolean + quxType: + description: Indicates to external qux type. + pattern: in-tree|out-of-tree + type: string`)