...

Source file src/k8s.io/kubernetes/test/e2e/apimachinery/crd_validation_rules.go

Documentation: k8s.io/kubernetes/test/e2e/apimachinery

     1  /*
     2  Copyright 2021 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package apimachinery
    18  
    19  import (
    20  	"context"
    21  	"strings"
    22  
    23  	"github.com/onsi/ginkgo/v2"
    24  	"github.com/onsi/gomega"
    25  
    26  	"k8s.io/apimachinery/pkg/runtime/schema"
    27  
    28  	v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    29  	"k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
    30  	"k8s.io/apiextensions-apiserver/test/integration/fixtures"
    31  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    32  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    33  	"k8s.io/apimachinery/pkg/util/json"
    34  	"k8s.io/apiserver/pkg/storage/names"
    35  	"k8s.io/client-go/dynamic"
    36  	"k8s.io/kubernetes/test/e2e/framework"
    37  	admissionapi "k8s.io/pod-security-admission/api"
    38  )
    39  
    40  var _ = SIGDescribe("CustomResourceValidationRules [Privileged:ClusterAdmin]", func() {
    41  	f := framework.NewDefaultFramework("crd-validation-expressions")
    42  	f.NamespacePodSecurityLevel = admissionapi.LevelPrivileged
    43  
    44  	var apiExtensionClient *clientset.Clientset
    45  	ginkgo.BeforeEach(func() {
    46  		var err error
    47  		apiExtensionClient, err = clientset.NewForConfig(f.ClientConfig())
    48  		framework.ExpectNoError(err, "initializing apiExtensionClient")
    49  	})
    50  
    51  	customResourceClient := func(crd *v1.CustomResourceDefinition) (dynamic.NamespaceableResourceInterface, schema.GroupVersionResource) {
    52  		gvrs := fixtures.GetGroupVersionResourcesOfCustomResource(crd)
    53  		if len(gvrs) != 1 {
    54  			ginkgo.Fail("Expected one version in custom resource definition")
    55  		}
    56  		gvr := gvrs[0]
    57  		return f.DynamicClient.Resource(gvr), gvr
    58  	}
    59  	unmarshallSchema := func(schemaJson []byte) *v1.JSONSchemaProps {
    60  		var c v1.JSONSchemaProps
    61  		err := json.Unmarshal(schemaJson, &c)
    62  		framework.ExpectNoError(err, "unmarshalling OpenAPIv3 schema")
    63  		return &c
    64  	}
    65  
    66  	// for all new CRD validation features that should be E2E-tested, add them
    67  	// into this schema and then add CR requests to the end of the test
    68  	// below ("MUST NOT fail validation...") instead of writing a new and
    69  	// separate test
    70  	var schemaWithValidationExpression = unmarshallSchema([]byte(`{
    71  	   "type":"object",
    72  	   "properties":{
    73  		  "spec":{
    74  			 "type":"object",
    75  			 "x-kubernetes-validations":[
    76  			   { "rule":"self.x + self.y > 0" },
    77  			   { "rule":"self.firstArray.isSorted() && self.secondArray.isSorted() && ((self.firstArray.sum() + self.secondArray.sum()) % 2 == 0)" },
    78  			   { "rule":"self.largeArray.all(x, self.largeArray.all(y, y == x))" }
    79  	         ],
    80  			 "properties":{
    81  				"x":{ "type":"integer" },
    82  				"y":{ "type":"integer" },
    83  				"firstArray":{ "type":"array", "maxItems": 1000, "items":{ "type": "integer"} },
    84  				"secondArray":{ "type":"array", "maxItems": 1000, "items":{ "type": "integer"} },
    85  				"largeArray":{ "type":"array", "maxItems": 725, "items":{ "type": "integer"} }
    86  			 }
    87  		  },
    88  		  "status":{
    89  			 "type":"object",
    90  			 "x-kubernetes-validations":[
    91  				{ "rule":"self.health == 'ok' || self.health == 'unhealthy'" }
    92  			 ],
    93  			 "properties":{
    94  				"health":{ "type":"string" }
    95  			 }
    96  		  }
    97  	   }
    98  	}`))
    99  	ginkgo.It("MUST NOT fail validation for create of a custom resource that satisfies the x-kubernetes-validations rules", func(ctx context.Context) {
   100  		ginkgo.By("Creating a custom resource definition with validation rules")
   101  		crd := fixtures.NewRandomNameV1CustomResourceDefinitionWithSchema(v1.NamespaceScoped, schemaWithValidationExpression, false)
   102  		crd, err := fixtures.CreateNewV1CustomResourceDefinitionWatchUnsafe(crd, apiExtensionClient)
   103  		framework.ExpectNoError(err, "creating CustomResourceDefinition")
   104  		defer func() {
   105  			err = fixtures.DeleteV1CustomResourceDefinition(crd, apiExtensionClient)
   106  			framework.ExpectNoError(err, "deleting CustomResourceDefinition")
   107  		}()
   108  
   109  		ginkgo.By("Creating a custom resource with values that are allowed by the validation rules set on the custom resource definition")
   110  		crClient, gvr := customResourceClient(crd)
   111  		name1 := names.SimpleNameGenerator.GenerateName("cr-1")
   112  		_, err = crClient.Namespace(f.Namespace.Name).Create(ctx, &unstructured.Unstructured{Object: map[string]interface{}{
   113  			"apiVersion": gvr.Group + "/" + gvr.Version,
   114  			"kind":       crd.Spec.Names.Kind,
   115  			"metadata": map[string]interface{}{
   116  				"name":      name1,
   117  				"namespace": f.Namespace.Name,
   118  			},
   119  			"spec": map[string]interface{}{
   120  				"x":           int64(1),
   121  				"y":           int64(0),
   122  				"firstArray":  []int64{3, 4},
   123  				"secondArray": []int64{5, 10},
   124  				"largeArray":  []int64{2, 2},
   125  			},
   126  		}}, metav1.CreateOptions{})
   127  		framework.ExpectNoError(err, "validation rules satisfied")
   128  	})
   129  	ginkgo.It("MUST fail validation for create of a custom resource that does not satisfy the x-kubernetes-validations rules", func(ctx context.Context) {
   130  		ginkgo.By("Creating a custom resource definition with validation rules")
   131  		crd := fixtures.NewRandomNameV1CustomResourceDefinitionWithSchema(v1.NamespaceScoped, schemaWithValidationExpression, false)
   132  		crd, err := fixtures.CreateNewV1CustomResourceDefinitionWatchUnsafe(crd, apiExtensionClient)
   133  		framework.ExpectNoError(err, "creating CustomResourceDefinition")
   134  		defer func() {
   135  			err = fixtures.DeleteV1CustomResourceDefinition(crd, apiExtensionClient)
   136  			framework.ExpectNoError(err, "deleting CustomResourceDefinition")
   137  		}()
   138  
   139  		ginkgo.By("Creating a custom resource with values that fail the validation rules set on the custom resource definition")
   140  		crClient, gvr := customResourceClient(crd)
   141  		name1 := names.SimpleNameGenerator.GenerateName("cr-1")
   142  		_, err = crClient.Namespace(f.Namespace.Name).Create(ctx, &unstructured.Unstructured{Object: map[string]interface{}{
   143  			"apiVersion": gvr.Group + "/" + gvr.Version,
   144  			"kind":       crd.Spec.Names.Kind,
   145  			"metadata": map[string]interface{}{
   146  				"name":      name1,
   147  				"namespace": f.Namespace.Name,
   148  			},
   149  			"spec": map[string]interface{}{
   150  				"x": int64(0),
   151  				"y": int64(0),
   152  			},
   153  		}}, metav1.CreateOptions{})
   154  		gomega.Expect(err).To(gomega.HaveOccurred(), "validation rules not satisfied")
   155  		expectedErrMsg := "failed rule"
   156  		if !strings.Contains(err.Error(), expectedErrMsg) {
   157  			framework.Failf("expect error contains %q, got %q", expectedErrMsg, err.Error())
   158  		}
   159  	})
   160  
   161  	ginkgo.It("MUST fail create of a custom resource definition that contains a x-kubernetes-validations rule that refers to a property that do not exist", func(ctx context.Context) {
   162  		ginkgo.By("Defining a custom resource definition with a validation rule that refers to a property that do not exist")
   163  		var schemaWithInvalidValidationRule = unmarshallSchema([]byte(`{
   164  		   "type":"object",
   165  		   "properties":{
   166  			  "spec":{
   167  				 "type":"object",
   168  				 "x-kubernetes-validations":[
   169  				   { "rule":"self.z == 100" }
   170  				 ],
   171  				 "properties":{
   172  					"x":{ "type":"integer" }
   173  				 }
   174  			  }
   175  		   }
   176  		}`))
   177  		crd := fixtures.NewRandomNameV1CustomResourceDefinitionWithSchema(v1.NamespaceScoped, schemaWithInvalidValidationRule, false)
   178  		_, err := fixtures.CreateNewV1CustomResourceDefinitionWatchUnsafe(crd, apiExtensionClient)
   179  		gomega.Expect(err).To(gomega.HaveOccurred(), "creating CustomResourceDefinition with a validation rule that refers to a property that do not exist")
   180  		expectedErrMsg := "undefined field 'z'"
   181  		if !strings.Contains(err.Error(), expectedErrMsg) {
   182  			framework.Failf("expect error contains %q, got %q", expectedErrMsg, err.Error())
   183  		}
   184  	})
   185  
   186  	ginkgo.It("MUST fail create of a custom resource definition that contains an x-kubernetes-validations rule that contains a syntax error", func(ctx context.Context) {
   187  		ginkgo.By("Defining a custom resource definition that contains a validation rule with a syntax error")
   188  		var schemaWithSyntaxErrorRule = unmarshallSchema([]byte(`{
   189  		   "type":"object",
   190  		   "properties":{
   191  		      "spec":{
   192  			    "type":"object",
   193  				"x-kubernetes-validations":[
   194  				  { "rule":"self = 42" }
   195  				]
   196  			  }
   197  			}
   198  		}`))
   199  		crd := fixtures.NewRandomNameV1CustomResourceDefinitionWithSchema(v1.NamespaceScoped, schemaWithSyntaxErrorRule, false)
   200  		_, err := fixtures.CreateNewV1CustomResourceDefinitionWatchUnsafe(crd, apiExtensionClient)
   201  		gomega.Expect(err).To(gomega.HaveOccurred(), "creating a CustomResourceDefinition with a validation rule that contains a syntax error")
   202  		expectedErrMsg := "Syntax error"
   203  		if !strings.Contains(err.Error(), expectedErrMsg) {
   204  			framework.Failf("expected error message to contain %q, got %q", expectedErrMsg, err.Error())
   205  		}
   206  	})
   207  
   208  	ginkgo.It("MUST fail create of a custom resource definition that contains an x-kubernetes-validations rule that exceeds the estimated cost limit", func(ctx context.Context) {
   209  		ginkgo.By("Defining a custom resource definition that contains a validation rule that exceeds the cost limit")
   210  		var schemaWithExpensiveRule = unmarshallSchema([]byte(`{
   211  		   "type":"object",
   212  		   "properties":{
   213  			  "spec":{
   214  			    "type":"object",
   215  			    "properties":{
   216  				  "x":{
   217  				    "type":"array",
   218  				    "items":{
   219  				      "type":"array",
   220  					  "items":{
   221  					    "type":"string"
   222  					  },
   223  					  "x-kubernetes-validations":[
   224  					    { "rule":"self.all(s, s == 'string constant')" }
   225  					  ]
   226  				    }
   227  				  }
   228  			    }
   229  			  }
   230  		    }
   231  		}`))
   232  		crd := fixtures.NewRandomNameV1CustomResourceDefinitionWithSchema(v1.NamespaceScoped, schemaWithExpensiveRule, false)
   233  		_, err := fixtures.CreateNewV1CustomResourceDefinitionWatchUnsafe(crd, apiExtensionClient)
   234  		gomega.Expect(err).To(gomega.HaveOccurred(), "creating a CustomResourceDefinition with a validation rule that exceeds the cost limit")
   235  		expectedErrMsg := "exceeds budget"
   236  		if !strings.Contains(err.Error(), expectedErrMsg) {
   237  			framework.Failf("expected error message to contain %q, got %q", expectedErrMsg, err.Error())
   238  		}
   239  	})
   240  
   241  	ginkgo.It("MUST fail create of a custom resource that exceeds the runtime cost limit for x-kubernetes-validations rule execution", func(ctx context.Context) {
   242  		ginkgo.By("Defining a custom resource definition including an expensive rule on a large amount of data")
   243  		crd := fixtures.NewRandomNameV1CustomResourceDefinitionWithSchema(v1.NamespaceScoped, schemaWithValidationExpression, false)
   244  		_, err := fixtures.CreateNewV1CustomResourceDefinitionWatchUnsafe(crd, apiExtensionClient)
   245  		framework.ExpectNoError(err, "creating CustomResourceDefinition including an expensive rule on a large amount of data")
   246  		defer func() {
   247  			err = fixtures.DeleteV1CustomResourceDefinition(crd, apiExtensionClient)
   248  			framework.ExpectNoError(err, "deleting CustomResourceDefinition")
   249  		}()
   250  		ginkgo.By("Attempting to create a custom resource that will exceed the runtime cost limit")
   251  		crClient, gvr := customResourceClient(crd)
   252  		name1 := names.SimpleNameGenerator.GenerateName("cr-1")
   253  		_, err = crClient.Namespace(f.Namespace.Name).Create(ctx, &unstructured.Unstructured{Object: map[string]interface{}{
   254  			"apiVersion": gvr.Group + "/" + gvr.Version,
   255  			"kind":       crd.Spec.Names.Kind,
   256  			"metadata": map[string]interface{}{
   257  				"name":      name1,
   258  				"namespace": f.Namespace.Name,
   259  			},
   260  			"spec": map[string]interface{}{
   261  				"largeArray": genLargeArray(725, 20),
   262  			},
   263  		}}, metav1.CreateOptions{})
   264  		gomega.Expect(err).To(gomega.HaveOccurred(), "custom resource creation should be prohibited by runtime cost limit")
   265  		expectedErrMsg := "call cost exceeds limit"
   266  		if !strings.Contains(err.Error(), expectedErrMsg) {
   267  			framework.Failf("expect error contains %q, got %q", expectedErrMsg, err.Error())
   268  		}
   269  	})
   270  
   271  	ginkgo.It("MUST fail update of a custom resource that does not satisfy a x-kubernetes-validations transition rule", func(ctx context.Context) {
   272  		ginkgo.By("Defining a custom resource definition with a x-kubernetes-validations transition rule")
   273  		var schemaWithTransitionRule = unmarshallSchema([]byte(`{
   274  		   "type":"object",
   275  		   "properties":{
   276  			  "spec":{
   277  			    "type":"object",
   278  			    "properties":{
   279  				  "num":{
   280  				    "type":"integer",
   281  					  "x-kubernetes-validations":[
   282  					    { "rule":"self > oldSelf" }
   283  					  ]
   284  				    }
   285  				  }
   286  			    }
   287  			  }
   288  		}`))
   289  		crd := fixtures.NewRandomNameV1CustomResourceDefinitionWithSchema(v1.NamespaceScoped, schemaWithTransitionRule, false)
   290  		_, err := fixtures.CreateNewV1CustomResourceDefinitionWatchUnsafe(crd, apiExtensionClient)
   291  		framework.ExpectNoError(err, "creating CustomResourceDefinition including an x-kubernetes-validations transition rule")
   292  		defer func() {
   293  			err = fixtures.DeleteV1CustomResourceDefinition(crd, apiExtensionClient)
   294  			framework.ExpectNoError(err, "deleting CustomResourceDefinition")
   295  		}()
   296  		ginkgo.By("Attempting to create a custom resource")
   297  		crClient, gvr := customResourceClient(crd)
   298  		name1 := names.SimpleNameGenerator.GenerateName("cr-1")
   299  		unstruct, err := crClient.Namespace(f.Namespace.Name).Create(ctx, &unstructured.Unstructured{Object: map[string]interface{}{
   300  			"apiVersion": gvr.Group + "/" + gvr.Version,
   301  			"kind":       crd.Spec.Names.Kind,
   302  			"metadata": map[string]interface{}{
   303  				"name":      name1,
   304  				"namespace": f.Namespace.Name,
   305  			},
   306  			"spec": map[string]interface{}{
   307  				"num": int64(10),
   308  			},
   309  		}}, metav1.CreateOptions{})
   310  		framework.ExpectNoError(err, "transition rules do not apply to create operations")
   311  		ginkgo.By("Updating a custom resource with a value that does not satisfy an x-kubernetes-validations transition rule")
   312  		_, err = crClient.Namespace(f.Namespace.Name).Update(ctx, &unstructured.Unstructured{Object: map[string]interface{}{
   313  			"apiVersion": gvr.Group + "/" + gvr.Version,
   314  			"kind":       crd.Spec.Names.Kind,
   315  			"metadata": map[string]interface{}{
   316  				"name":            name1,
   317  				"namespace":       f.Namespace.Name,
   318  				"resourceVersion": unstruct.GetResourceVersion(),
   319  			},
   320  			"spec": map[string]interface{}{
   321  				"num": int64(9),
   322  			},
   323  		}}, metav1.UpdateOptions{})
   324  		gomega.Expect(err).To(gomega.HaveOccurred(), "custom resource update should be prohibited by transition rule")
   325  		expectedErrMsg := "failed rule"
   326  		if !strings.Contains(err.Error(), expectedErrMsg) {
   327  			framework.Failf("expect error contains %q, got %q", expectedErrMsg, err.Error())
   328  		}
   329  	})
   330  })
   331  
   332  func genLargeArray(n, x int64) []int64 {
   333  	arr := make([]int64, n)
   334  	for i := int64(0); i < n; i++ {
   335  		arr[i] = x
   336  	}
   337  	return arr
   338  }
   339  

View as plain text