...

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

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

     1  /*
     2  Copyright 2022 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 contains e2e tests owned by SIG-API-Machinery.
    18  package apimachinery
    19  
    20  import (
    21  	"bytes"
    22  	"context"
    23  	"fmt"
    24  	"strings"
    25  	"time"
    26  
    27  	"github.com/onsi/ginkgo/v2"
    28  	"github.com/onsi/gomega"
    29  	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    30  	"k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
    31  	apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features"
    32  	"k8s.io/apimachinery/pkg/api/meta"
    33  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    34  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    35  	"k8s.io/apimachinery/pkg/runtime/schema"
    36  	"k8s.io/apimachinery/pkg/util/uuid"
    37  	"k8s.io/apimachinery/pkg/util/wait"
    38  	utilyaml "k8s.io/apimachinery/pkg/util/yaml"
    39  	"k8s.io/client-go/dynamic"
    40  	"k8s.io/kubernetes/test/e2e/framework"
    41  	"k8s.io/kubernetes/test/utils/crd"
    42  )
    43  
    44  var _ = SIGDescribe("CRDValidationRatcheting [Privileged:ClusterAdmin]", framework.WithFeatureGate(apiextensionsfeatures.CRDValidationRatcheting), func() {
    45  	f := framework.NewDefaultFramework("crd-validation-ratcheting")
    46  	var apiExtensionClient *clientset.Clientset
    47  	var dynamicClient dynamic.Interface
    48  	var restmapper meta.RESTMapper
    49  	var ctx context.Context
    50  	var testCRD *crd.TestCrd
    51  	var testCRDGVR schema.GroupVersionResource
    52  
    53  	ginkgo.BeforeEach(func() {
    54  		var err error
    55  		ctx = context.TODO()
    56  
    57  		apiExtensionClient, err = clientset.NewForConfig(f.ClientConfig())
    58  		framework.ExpectNoError(err, "initializing apiExtensionClient")
    59  
    60  		dynamicClient, err = dynamic.NewForConfig(f.ClientConfig())
    61  		framework.ExpectNoError(err, "initializing dynamicClient")
    62  
    63  		testCRD, err = crd.CreateTestCRD(f)
    64  		framework.ExpectNoError(err, "creating test CRD")
    65  
    66  		testCRDGVR = schema.GroupVersionResource{
    67  			Group:    testCRD.Crd.Spec.Group,
    68  			Version:  testCRD.Crd.Spec.Versions[0].Name,
    69  			Resource: testCRD.Crd.Spec.Names.Plural,
    70  		}
    71  
    72  		// Full discovery restmapper pretty heavy handed for this test, just
    73  		// use hardcoded mappings
    74  		restmapper = &fakeRESTMapper{
    75  			m: map[schema.GroupVersionResource]schema.GroupVersionKind{
    76  				testCRDGVR: {
    77  					Group:   testCRDGVR.Group,
    78  					Version: testCRDGVR.Version,
    79  					Kind:    testCRD.Crd.Spec.Names.Kind,
    80  				},
    81  			},
    82  		}
    83  	})
    84  
    85  	ginkgo.AfterEach(func() {
    86  		framework.ExpectNoError(testCRD.CleanUp(ctx), "cleaning up test CRD")
    87  	})
    88  
    89  	// Applies the given patch to the given GVR. The patch can be a string or a
    90  	// map[string]interface{}. If it is a string, it will be parsed as YAML or
    91  	// JSON. If it is a map, it will be used as-is.
    92  	applyPatch := func(gvr schema.GroupVersionResource, name string, patchObj map[string]interface{}) error {
    93  		gvk, err := restmapper.KindFor(gvr)
    94  		if err != nil {
    95  			return fmt.Errorf("no mapping for %s", gvr)
    96  		}
    97  		patch := &unstructured.Unstructured{
    98  			Object: patchObj,
    99  		}
   100  		patch = patch.DeepCopy()
   101  
   102  		patch.SetKind(gvk.Kind)
   103  		patch.SetAPIVersion(gvk.GroupVersion().Identifier())
   104  		patch.SetName(name)
   105  		patch.SetNamespace("default")
   106  
   107  		_, err = dynamicClient.
   108  			Resource(gvr).
   109  			Namespace(patch.GetNamespace()).
   110  			Apply(
   111  				context.TODO(),
   112  				patch.GetName(),
   113  				patch,
   114  				metav1.ApplyOptions{
   115  					FieldManager: "manager",
   116  				})
   117  
   118  		return err
   119  	}
   120  
   121  	// Updates the CRD schema for the given GVR. Waits for the CRD to be properly
   122  	// updated by attempting a create using a sentinel error before returning.
   123  	updateCRDSchema := func(gvr schema.GroupVersionResource, props apiextensionsv1.JSONSchemaProps) error {
   124  		myCRD, err := apiExtensionClient.
   125  			ApiextensionsV1().
   126  			CustomResourceDefinitions().
   127  			Get(
   128  				context.TODO(),
   129  				gvr.Resource+"."+gvr.Group,
   130  				metav1.GetOptions{},
   131  			)
   132  		if err != nil {
   133  			return fmt.Errorf("getting CRD %s: %v", gvr, err)
   134  		}
   135  
   136  		// Inject a special field that will throw a unique error string so we know
   137  		// when the schema as been updated on the server side.
   138  		uniqueErrorUUID := string(uuid.NewUUID())
   139  		sentinelName := "__update_schema_sentinel_field__"
   140  		props.Properties[sentinelName] = apiextensionsv1.JSONSchemaProps{
   141  			Type: "string",
   142  			Enum: []apiextensionsv1.JSON{
   143  				{Raw: []byte(`"` + uniqueErrorUUID + `"`)},
   144  			},
   145  		}
   146  
   147  		for i, v := range myCRD.Spec.Versions {
   148  			if v.Name == gvr.Version {
   149  				myCRD.Spec.Versions[i].Schema.OpenAPIV3Schema = &props
   150  			}
   151  		}
   152  
   153  		_, err = apiExtensionClient.ApiextensionsV1().CustomResourceDefinitions().Update(context.TODO(), myCRD, metav1.UpdateOptions{
   154  			FieldManager: "manager",
   155  		})
   156  		if err != nil {
   157  			return fmt.Errorf("updating CRD %s: %v", gvr, err)
   158  		}
   159  
   160  		// Keep trying to create an invalid instance of the CRD until we
   161  		// get an error containing the ResourceVersion we are looking for
   162  		//
   163  		counter := 0
   164  		err = wait.PollUntilContextCancel(context.TODO(), 100*time.Millisecond, true, func(_ context.Context) (done bool, err error) {
   165  			counter += 1
   166  			err = applyPatch(gvr, "sentinel-resource", map[string]interface{}{
   167  				"metadata": map[string]interface{}{
   168  					"finalizers": []interface{}{
   169  						"unqualified-finalizer",
   170  					},
   171  					"labels": map[string]interface{}{
   172  						"#inv/($%)/alid=": ">htt$://",
   173  					},
   174  				},
   175  				// Just keep using different values
   176  				sentinelName: fmt.Sprintf("%v", counter),
   177  			})
   178  
   179  			if err == nil {
   180  				return false, fmt.Errorf("expected error when creating sentinel resource")
   181  			}
   182  			// Check to see if the returned error message contains our
   183  			// unique string. UUID should be unique enough to just check
   184  			// simple existence in the error.
   185  			if strings.Contains(err.Error(), uniqueErrorUUID) {
   186  				return true, nil
   187  			}
   188  			return false, nil
   189  
   190  		})
   191  		if err == nil {
   192  			return nil
   193  		}
   194  		return fmt.Errorf("waiting for CRD %s to be updated: %v", gvr, err)
   195  	}
   196  
   197  	ginkgo.It("MUST NOT fail to update a resource due to JSONSchema errors on unchanged correlatable fields", func() {
   198  		sch, err := parseSchema(`
   199  			type: object
   200  			properties:
   201  				field: {type: string, enum: ["notfoo"]}
   202  				struct:
   203  					type: object
   204  					properties:
   205  						field: {type: string, enum: ["notfoo"]}
   206  				list:
   207  					type: array
   208  					x-kubernetes-list-type: map
   209  					x-kubernetes-list-map-keys: ["key"]
   210  					items:
   211  						type: object
   212  						properties:
   213  							key: {type: string}
   214  							field: {type: string, enum: ["notfoo"]}
   215  						required:
   216  						- key
   217  				map:
   218  					type: object
   219  					additionalProperties:
   220  						type: object
   221  						properties:
   222  							field: {type: string, enum: ["notfoo"]}
   223  		`)
   224  		framework.ExpectNoError(err, "parsing schema")
   225  
   226  		instance, err := parseUnstructured(`
   227  			field: "foo"
   228  			struct:
   229  				field: "foo"
   230  			list:
   231  			- key: "first"
   232  			  field: "foo"
   233  			map:
   234  				foo:
   235  					field: "foo"
   236  		`)
   237  		framework.ExpectNoError(err, "parsing test resource")
   238  
   239  		ginkgo.By("creating test resource with correlatable fields")
   240  		framework.ExpectNoError(applyPatch(testCRDGVR, "test-resource", instance.Object), "failed creating test resource")
   241  		ginkgo.By("updating CRD schema with constraints on correlatable fields to make instance invalid")
   242  		framework.ExpectNoError(updateCRDSchema(testCRDGVR, *sch), "failed to update schema")
   243  
   244  		// Make an update to a label. The unchanged fields should be allowed
   245  		// to pass through.
   246  		ginkgo.By("updating label on now-invalid test resource")
   247  		instance.SetLabels(map[string]string{
   248  			"foo": "bar",
   249  		})
   250  		framework.ExpectNoError(applyPatch(testCRDGVR, "test-resource", instance.Object), "update label on test resource")
   251  	})
   252  
   253  	ginkgo.It("MUST fail to update a resource due to JSONSchema errors on unchanged uncorrelatable fields", func() {
   254  		ginkgo.By("creating test resource with correlatable fields")
   255  		instance, err := parseUnstructured(`
   256  			setArray:
   257  			- "foo"
   258  			- "bar"
   259  			- "baz"
   260  			atomicArray:
   261  			- "foo"
   262  			- "bar"
   263  			- "baz"
   264  		`)
   265  		framework.ExpectNoError(err, "parsing test resource")
   266  		framework.ExpectNoError(applyPatch(testCRDGVR, "test-resource", instance.Object), "failed creating test resource")
   267  
   268  		ginkgo.By("updating CRD schema with constraints on uncorrelatable fields to make instance invalid")
   269  		sch, err := parseSchema(`
   270  			type: object
   271  			properties:
   272  				atomicArray:
   273  					type: array
   274  					items:
   275  						type: string
   276  						enum: ["notfoo", "notbar", "notbaz"]
   277  				setArray:
   278  					type: array
   279  					x-kubernetes-list-type: set
   280  					items:
   281  						type: string
   282  						enum: ["notfoo", "notbar", "notbaz"]
   283  		`)
   284  		framework.ExpectNoError(err, "parsing schema")
   285  		framework.ExpectNoError(updateCRDSchema(testCRDGVR, *sch), "failed to update schema")
   286  
   287  		ginkgo.By("updating label on now-invalid test resource")
   288  		instance, err = parseUnstructured(`
   289  			setArray:
   290  			- "foo"
   291  			- "bar"
   292  			- "baz"
   293  			- "notfoo"
   294  			atomicArray:
   295  			- "foo"
   296  			- "bar"
   297  			- "baz"
   298  			- "notfoo"
   299  		`)
   300  		framework.ExpectNoError(err, "parsing modified resource")
   301  		instance.SetLabels(map[string]string{
   302  			"foo": "bar",
   303  		})
   304  		err = applyPatch(testCRDGVR, "test-resource", instance.Object)
   305  		gomega.Expect(err).To(gomega.MatchError(gomega.ContainSubstring("atomicArray")))
   306  		gomega.Expect(err).To(gomega.MatchError(gomega.ContainSubstring("setArray")))
   307  	})
   308  
   309  	ginkgo.It("MUST fail to update a resource due to JSONSchema errors on changed fields", func() {
   310  		ginkgo.By("creating an initial object with many correlatable fields")
   311  		instance, err := parseUnstructured(`
   312  			field: "foo"
   313  			struct:
   314  				field: "foo"
   315  			list:
   316  			- key: "foo"
   317  			  field: "foo"
   318  			- key: "bar"
   319  			  field: "foo"
   320  			map:
   321  				foo:
   322  					field: "foo"
   323  				bar:
   324  					field: "foo"
   325  		`)
   326  		framework.ExpectNoError(err, "parsing test resource")
   327  		framework.ExpectNoError(applyPatch(testCRDGVR, "test-resource", instance.Object), "failed creating test resource")
   328  
   329  		ginkgo.By("updating CRD schema with constraints on correlatable fields to make instance invalid")
   330  		sch, err := parseSchema(`
   331  			type: object
   332  			properties:
   333  				field: {type: string, enum: ["foo"]}
   334  				struct:
   335  					type: object
   336  					properties:
   337  						field: {type: string, enum: ["foo"]}
   338  				list:
   339  					type: array
   340  					x-kubernetes-list-type: map
   341  					x-kubernetes-list-map-keys: ["key"]
   342  					items:
   343  						type: object
   344  						properties:
   345  							key: {type: string}
   346  							field: {type: string, enum: ["foo"]}
   347  						required:
   348  							- key
   349  				map:
   350  					type: object
   351  					additionalProperties:
   352  						type: object
   353  						properties:
   354  							field: {type: string, enum: ["foo"]}
   355  		`)
   356  
   357  		framework.ExpectNoError(err, "parsing schema")
   358  		framework.ExpectNoError(updateCRDSchema(testCRDGVR, *sch), "failed to update schema")
   359  
   360  		ginkgo.By("changing every field to invalid value")
   361  		modifiedInstance, err := parseUnstructured(`
   362  			field: "notfoo"
   363  			struct:
   364  				field: "notfoo"
   365  			list:
   366  			- key: "foo"
   367  			  field: "notfoo"
   368  			- key: "bar"
   369  			  field: "notfoo"
   370  			map:
   371  				foo:
   372  					field: "notfoo"
   373  				bar:
   374  					field: "notfoo"
   375  		`)
   376  		framework.ExpectNoError(err, "parsing modified resource")
   377  		err = applyPatch(testCRDGVR, "test-resource", modifiedInstance.Object)
   378  		for _, fieldPath := range []string{
   379  			"field",
   380  			"struct.field",
   381  			"list[0].field",
   382  			"list[1].field",
   383  			"map.foo.field",
   384  			"map.bar.field",
   385  		} {
   386  			gomega.Expect(err).To(gomega.MatchError(gomega.ContainSubstring(fieldPath)))
   387  		}
   388  	})
   389  
   390  	ginkgo.It("MUST NOT fail to update a resource due to CRD Validation Rule errors on unchanged correlatable fields", func() {
   391  		ginkgo.By("creating an initial object with many correlatable fields")
   392  		instance, err := parseUnstructured(`
   393  			field: "notfoo"
   394  			struct:
   395  				field: "notfoo"
   396  			list:
   397  			- key: "foo"
   398  			  field: "notfoo"
   399  			- key: "bar"
   400  			  field: "notfoo"
   401  			map:
   402  				foo:
   403  					field: "notfoo"
   404  				bar:
   405  					field: "notfoo"
   406  		`)
   407  		framework.ExpectNoError(err, "parsing test resource")
   408  		framework.ExpectNoError(applyPatch(testCRDGVR, "test-resource", instance.Object), "failed creating test resource")
   409  
   410  		ginkgo.By("updating CRD schema with constraints on correlatable fields to make instance invalid")
   411  		sch, err := parseSchema(`
   412  			type: object
   413  			properties:
   414  				field:
   415  					type: string
   416  					x-kubernetes-validations:
   417  					- rule: self == "foo"
   418  				otherField:
   419  					type: string
   420  				struct:
   421  					type: object
   422  					properties:
   423  						field:
   424  							type: string
   425  							x-kubernetes-validations:
   426  							- rule: self == "foo"
   427  						otherField:
   428  							type: string
   429  				list:
   430  					type: array
   431  					x-kubernetes-list-type: map
   432  					x-kubernetes-list-map-keys: ["key"]
   433  					items:
   434  						type: object
   435  						properties:
   436  							key:
   437  								type: string
   438  							field:
   439  								type: string
   440  								x-kubernetes-validations:
   441  								- rule: self == "foo"
   442  							otherField:
   443  								type: string
   444  						required:
   445  						- key
   446  				map:
   447  					type: object
   448  					additionalProperties:
   449  						type: object
   450  						properties:
   451  							field:
   452  								type: string
   453  								x-kubernetes-validations:
   454  								- rule: self == "foo"
   455  							otherField:
   456  								type: string
   457  		`)
   458  
   459  		framework.ExpectNoError(err, "parsing schema")
   460  		framework.ExpectNoError(updateCRDSchema(testCRDGVR, *sch), "failed to update schema")
   461  
   462  		ginkgo.By("introducing new values, but leaving invalid old correlatable values untouched")
   463  		modifiedInstance, err := parseUnstructured(`
   464  			field: "notfoo"
   465  			otherField: "doesntmatter"
   466  			struct:
   467  				field: "notfoo"
   468  				otherField: "doesntmatter"
   469  			list:
   470  			- key: "foo"
   471  			  field: "notfoo"
   472  			  otherField: "doesntmatter"
   473  			- key: "bar"
   474  			  field: "notfoo"
   475  			  otherField: "doesntmatter"
   476  			- key: "baz"
   477  			  field: "foo"
   478  			  otherField: "doesntmatter"
   479  			map:
   480  				foo:
   481  					field: "notfoo"
   482  					otherField: "doesntmatter"
   483  				bar:
   484  					field: "notfoo"
   485  					otherField: "doesntmatter"
   486  		`)
   487  		framework.ExpectNoError(err, "parsing test resource")
   488  		framework.ExpectNoError(applyPatch(testCRDGVR, "test-resource", modifiedInstance.Object), "failed updating test resource")
   489  	})
   490  
   491  	ginkgo.It("MUST fail to update a resource due to CRD Validation Rule errors on unchanged uncorrelatable fields", func() {
   492  		ginkgo.By("creating test resource with correlatable fields")
   493  		instance, err := parseUnstructured(`
   494  			setArray:
   495  			- "foo"
   496  			- "bar"
   497  			- "baz"
   498  			atomicArray:
   499  			- "foo"
   500  			- "bar"
   501  			- "baz"
   502  		`)
   503  		framework.ExpectNoError(err, "parsing test resource")
   504  		framework.ExpectNoError(applyPatch(testCRDGVR, "test-resource", instance.Object), "failed creating test resource")
   505  
   506  		ginkgo.By("updating CRD schema with constraints on uncorrelatable fields to make instance invalid")
   507  		sch, err := parseSchema(`
   508  			type: object
   509  			properties:
   510  				atomicArray:
   511  					type: array
   512  					items:
   513  						type: string
   514  						x-kubernetes-validations:
   515  						- rule: self != "foo"
   516  				setArray:
   517  					type: array
   518  					x-kubernetes-list-type: set
   519  					items:
   520  						type: string
   521  						x-kubernetes-validations:
   522  						- rule: self != "foo"
   523  		`)
   524  		framework.ExpectNoError(err, "parsing schema")
   525  		framework.ExpectNoError(updateCRDSchema(testCRDGVR, *sch), "failed to update schema")
   526  
   527  		ginkgo.By("updating label and adding valid elements to invalid lists")
   528  		instance, err = parseUnstructured(`
   529  			setArray:
   530  			- "foo"
   531  			- "bar"
   532  			- "baz"
   533  			- "notfoo"
   534  			atomicArray:
   535  			- "foo"
   536  			- "bar"
   537  			- "baz"
   538  			- "notfoo"
   539  		`)
   540  		framework.ExpectNoError(err, "parsing modified resource")
   541  		instance.SetLabels(map[string]string{
   542  			"foo": "bar",
   543  		})
   544  		err = applyPatch(testCRDGVR, "test-resource", instance.Object)
   545  		gomega.Expect(err).To(gomega.MatchError(gomega.ContainSubstring("atomicArray")))
   546  		gomega.Expect(err).To(gomega.MatchError(gomega.ContainSubstring("setArray")))
   547  	})
   548  
   549  	ginkgo.It("MUST fail to update a resource due to CRD Validation Rule errors on changed fields", func() {
   550  		ginkgo.By("creating an initial object with many correlatable fields")
   551  		instance, err := parseUnstructured(`
   552  			field: "foo"
   553  			struct:
   554  				field: "foo"
   555  			list:
   556  			- key: "foo"
   557  			  field: "foo"
   558  			- key: "bar"
   559  			  field: "foo"
   560  			map:
   561  				foo:
   562  					field: "foo"
   563  				bar:
   564  					field: "foo"
   565  		`)
   566  		framework.ExpectNoError(err, "parsing test resource")
   567  		framework.ExpectNoError(applyPatch(testCRDGVR, "test-resource", instance.Object), "failed creating test resource")
   568  
   569  		ginkgo.By("updating CRD schema with constraints on correlatable fields to make instance invalid")
   570  		sch, err := parseSchema(`
   571  			type: object
   572  			properties:
   573  				field: 
   574  					type: string
   575  					x-kubernetes-validations:
   576  					- rule: self == "foo"
   577  				struct:
   578  					type: object
   579  					properties:
   580  						field:
   581  							type: string
   582  							x-kubernetes-validations:
   583  							- rule: self == "foo"
   584  				list:
   585  					type: array
   586  					x-kubernetes-list-type: map
   587  					x-kubernetes-list-map-keys:
   588  					- key
   589  					items:
   590  						type: object
   591  						properties:
   592  							key:
   593  								type: string
   594  							field:
   595  								type: string
   596  								x-kubernetes-validations:
   597  								- rule: self == "foo"
   598  						required:
   599  						- key
   600  				map:
   601  					type: object
   602  					additionalProperties:
   603  						type: object
   604  						properties:
   605  							field:
   606  								type: string
   607  								x-kubernetes-validations:
   608  								- rule: self == "foo"
   609  		`)
   610  
   611  		framework.ExpectNoError(err, "parsing schema")
   612  		framework.ExpectNoError(updateCRDSchema(testCRDGVR, *sch), "failed to update schema")
   613  
   614  		ginkgo.By("changing every field to invalid value")
   615  		modifiedInstance, err := parseUnstructured(`
   616  			field: "notfoo"
   617  			struct:
   618  				field: "notfoo"
   619  			list:
   620  			- key: "foo"
   621  			  field: "notfoo"
   622  			- key: "bar"
   623  			  field: "notfoo"
   624  			map:
   625  				foo:
   626  					field: "notfoo"
   627  				bar:
   628  					field: "notfoo"
   629  		`)
   630  		framework.ExpectNoError(err, "parsing modified resource")
   631  		err = applyPatch(testCRDGVR, "test-resource", modifiedInstance.Object)
   632  		for _, fieldPath := range []string{
   633  			"field",
   634  			"struct.field",
   635  			"list[0].field",
   636  			"list[1].field",
   637  			"map[foo].field",
   638  			"map[bar].field",
   639  		} {
   640  			gomega.Expect(err).To(gomega.MatchError(gomega.ContainSubstring(fieldPath)))
   641  		}
   642  	})
   643  
   644  	ginkgo.It("MUST NOT ratchet errors raised by transition rules", func() {
   645  		ginkgo.By("creating an initial object with many correlatable fields")
   646  		instance, err := parseUnstructured(`
   647  			field: "foo"
   648  			struct:
   649  				field: "foo"
   650  			list:
   651  			- key: "foo"
   652  			  field: "foo"
   653  			- key: "bar"
   654  			  field: "foo"
   655  			map:
   656  				foo:
   657  					field: "foo"
   658  				bar:
   659  					field: "foo"
   660  		`)
   661  		framework.ExpectNoError(err, "parsing test resource")
   662  		framework.ExpectNoError(applyPatch(testCRDGVR, "test-resource", instance.Object), "failed creating test resource")
   663  
   664  		ginkgo.By("updating CRD schema with constraints on correlatable fields to make instance invalid")
   665  		sch, err := parseSchema(`
   666  			type: object
   667  			properties:
   668  				field: 
   669  					type: string
   670  					maxLength: 5
   671  					x-kubernetes-validations:
   672  					- rule: self != oldSelf
   673  				struct:
   674  					type: object
   675  					properties:
   676  						field:
   677  							type: string
   678  							maxLength: 5
   679  							x-kubernetes-validations:
   680  							- rule: self != oldSelf
   681  				list:
   682  					type: array
   683  					maxItems: 5
   684  					x-kubernetes-list-type: map
   685  					x-kubernetes-list-map-keys: [key]
   686  					items:
   687  						type: object
   688  						properties:
   689  							key: {type: string}
   690  							field:
   691  								type: string
   692  								maxLength: 5
   693  								x-kubernetes-validations:
   694  								- rule: self != oldSelf
   695  						required:
   696  						- key
   697  				map:
   698  					type: object
   699  					maxProperties: 5
   700  					additionalProperties:
   701  						type: object
   702  						properties:
   703  							field:
   704  								type: string
   705  								maxLength: 5
   706  								x-kubernetes-validations:
   707  								- rule: self != oldSelf
   708  		`)
   709  
   710  		framework.ExpectNoError(err, "parsing schema")
   711  		framework.ExpectNoError(updateCRDSchema(testCRDGVR, *sch), "failed to update schema")
   712  
   713  		ginkgo.By("updating a label on the test resource")
   714  		instance.SetLabels(map[string]string{
   715  			"foo": "bar",
   716  		})
   717  		err = applyPatch(testCRDGVR, "test-resource", instance.Object)
   718  		for _, fieldPath := range []string{
   719  			"field",
   720  			"struct.field",
   721  			"list[0].field",
   722  			"list[1].field",
   723  			"map[foo].field",
   724  			"map[bar].field",
   725  		} {
   726  			gomega.Expect(err).To(gomega.MatchError(gomega.ContainSubstring(fieldPath)))
   727  		}
   728  	})
   729  
   730  	ginkgo.It("MUST evaluate a CRD Validation Rule with oldSelf = nil for new values when optionalOldSelf is true", func() {
   731  		ginkgo.By("updating CRD schema to use optionalOldSelf")
   732  		sch, err := parseSchema(`
   733  			type: object
   734  			properties:
   735  				field: 
   736  					type: string
   737  					maxLength: 5
   738  					x-kubernetes-validations:
   739  					- rule: "!oldSelf.hasValue() || self != oldSelf.value()"
   740  					  optionalOldSelf: true
   741  				struct:
   742  					type: object
   743  					properties:
   744  						field:
   745  							type: string
   746  							maxLength: 5
   747  							x-kubernetes-validations:
   748  							- rule: "!oldSelf.hasValue() || self != oldSelf.value()"
   749  							  optionalOldSelf: true
   750  				list:
   751  					type: array
   752  					maxItems: 5
   753  					x-kubernetes-list-type: map
   754  					x-kubernetes-list-map-keys: [key]
   755  					items:
   756  						type: object
   757  						properties:
   758  							key: {type: string}
   759  							field:
   760  								type: string
   761  								maxLength: 5
   762  								x-kubernetes-validations:
   763  								- rule: "!oldSelf.hasValue() || self != oldSelf.value()"
   764  								  optionalOldSelf: true
   765  						required:
   766  						- key
   767  				map:
   768  					type: object
   769  					maxProperties: 5
   770  					additionalProperties:
   771  						type: object
   772  						properties:
   773  							field:
   774  								type: string
   775  								maxLength: 5
   776  								x-kubernetes-validations:
   777  								- rule: "!oldSelf.hasValue() || self != oldSelf.value()"
   778  								  optionalOldSelf: true
   779  		`)
   780  		framework.ExpectNoError(err, "parsing schema")
   781  		framework.ExpectNoError(updateCRDSchema(testCRDGVR, *sch), "failed to update schema")
   782  
   783  		ginkgo.By("creating an object")
   784  		instance, err := parseUnstructured(`
   785  			field: "foo"
   786  			struct:
   787  				field: "foo"
   788  			list:
   789  			- key: "foo"
   790  			  field: "foo"
   791  			- key: "bar"
   792  			  field: "foo"
   793  			map:
   794  				foo:
   795  					field: "foo"
   796  				bar:
   797  					field: "foo"
   798  		`)
   799  		framework.ExpectNoError(err, "parsing test resource")
   800  		framework.ExpectNoError(applyPatch(testCRDGVR, "test-resource", instance.Object), "failed creating test resource")
   801  
   802  		ginkgo.By("updating a label on the test resource")
   803  		instance.SetLabels(map[string]string{
   804  			"foo": "bar",
   805  		})
   806  		err = applyPatch(testCRDGVR, "test-resource", instance.Object)
   807  		for _, fieldPath := range []string{
   808  			"field",
   809  			"struct.field",
   810  			"list[0].field",
   811  			"list[1].field",
   812  			"map[foo].field",
   813  			"map[bar].field",
   814  		} {
   815  			gomega.Expect(err).To(gomega.MatchError(gomega.ContainSubstring(fieldPath)))
   816  		}
   817  
   818  		ginkgo.By("updating all fields of the object to show the condition is checked")
   819  		instance, err = parseUnstructured(`
   820  			field: "new"
   821  			struct:
   822  				field: "new"
   823  			list:
   824  			- key: "foo"
   825  			  field: "new"
   826  			- key: "bar"
   827  			  field: "new"
   828  			map:
   829  				foo:
   830  					field: "new"
   831  				bar:
   832  					field: "new"
   833  		`)
   834  		framework.ExpectNoError(err, "parsing test resource")
   835  		framework.ExpectNoError(applyPatch(testCRDGVR, "test-resource", instance.Object), "failed updating test resource")
   836  	})
   837  
   838  })
   839  
   840  func parseSchema(source string) (*apiextensionsv1.JSONSchemaProps, error) {
   841  	source, err := fixTabs(source)
   842  	if err != nil {
   843  		return nil, err
   844  	}
   845  
   846  	d := utilyaml.NewYAMLOrJSONDecoder(strings.NewReader(source), 4096)
   847  	props := &apiextensionsv1.JSONSchemaProps{}
   848  	return props, d.Decode(props)
   849  }
   850  
   851  func parseUnstructured(source string) (*unstructured.Unstructured, error) {
   852  	source, err := fixTabs(source)
   853  	if err != nil {
   854  		return nil, err
   855  	}
   856  
   857  	d := utilyaml.NewYAMLOrJSONDecoder(strings.NewReader(source), 4096)
   858  	obj := &unstructured.Unstructured{}
   859  	return obj, d.Decode(&obj.Object)
   860  }
   861  
   862  // fixTabs counts the number of tab characters preceding the first
   863  // line in the given yaml object. It removes that many tabs from every
   864  // line. It returns error (it's a test function) if some line has fewer tabs
   865  // than the first line.
   866  //
   867  // The purpose of this is to make it easier to read tests.
   868  func fixTabs(in string) (string, error) {
   869  	lines := bytes.Split([]byte(in), []byte{'\n'})
   870  	if len(lines[0]) == 0 && len(lines) > 1 {
   871  		lines = lines[1:]
   872  	}
   873  	// Create prefix made of tabs that we want to remove.
   874  	var prefix []byte
   875  	for _, c := range lines[0] {
   876  		if c != '\t' {
   877  			break
   878  		}
   879  		prefix = append(prefix, byte('\t'))
   880  	}
   881  	// Remove prefix from all tabs, fail otherwise.
   882  	for i := range lines {
   883  		line := lines[i]
   884  		// It's OK for the last line to be blank (trailing \n)
   885  		if i == len(lines)-1 && len(line) <= len(prefix) && bytes.TrimSpace(line) == nil {
   886  			lines[i] = []byte{}
   887  			break
   888  		}
   889  		if !bytes.HasPrefix(line, prefix) {
   890  			minRange := i - 5
   891  			maxRange := i + 5
   892  			if minRange < 0 {
   893  				minRange = 0
   894  			}
   895  			if maxRange > len(lines) {
   896  				maxRange = len(lines)
   897  			}
   898  			return "", fmt.Errorf("line %d doesn't start with expected number (%d) of tabs (%v-%v):\n%v", i, len(prefix), minRange, maxRange, string(bytes.Join(lines[minRange:maxRange], []byte{'\n'})))
   899  		}
   900  		lines[i] = line[len(prefix):]
   901  	}
   902  	joined := string(bytes.Join(lines, []byte{'\n'}))
   903  
   904  	// Convert rest of tabs to spaces since yaml doesnt like tabs
   905  	// (assuming 2 space alignment)
   906  	return strings.ReplaceAll(joined, "\t", "  "), nil
   907  }
   908  
   909  type fakeRESTMapper struct {
   910  	m map[schema.GroupVersionResource]schema.GroupVersionKind
   911  }
   912  
   913  func (f *fakeRESTMapper) KindFor(resource schema.GroupVersionResource) (schema.GroupVersionKind, error) {
   914  	gvk, ok := f.m[resource]
   915  	if !ok {
   916  		return schema.GroupVersionKind{}, fmt.Errorf("no mapping for %s", resource)
   917  	}
   918  	return gvk, nil
   919  }
   920  
   921  func (f *fakeRESTMapper) KindsFor(resource schema.GroupVersionResource) ([]schema.GroupVersionKind, error) {
   922  	return nil, nil
   923  }
   924  
   925  func (f *fakeRESTMapper) ResourceFor(input schema.GroupVersionResource) (schema.GroupVersionResource, error) {
   926  	return schema.GroupVersionResource{}, nil
   927  }
   928  
   929  func (f *fakeRESTMapper) ResourcesFor(input schema.GroupVersionResource) ([]schema.GroupVersionResource, error) {
   930  	return nil, nil
   931  }
   932  
   933  func (f *fakeRESTMapper) RESTMapping(gk schema.GroupKind, versions ...string) (*meta.RESTMapping, error) {
   934  	return nil, nil
   935  }
   936  
   937  func (f *fakeRESTMapper) RESTMappings(gk schema.GroupKind, versions ...string) ([]*meta.RESTMapping, error) {
   938  	return nil, nil
   939  }
   940  
   941  func (f *fakeRESTMapper) ResourceSingularizer(resource string) (singular string, err error) {
   942  	return "", nil
   943  }
   944  

View as plain text