...

Source file src/k8s.io/apiextensions-apiserver/test/integration/ratcheting_test.go

Documentation: k8s.io/apiextensions-apiserver/test/integration

     1  /*
     2  Copyright 2023 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 integration_test
    18  
    19  import (
    20  	"bytes"
    21  	"context"
    22  	"encoding/json"
    23  	"errors"
    24  	"fmt"
    25  	"io"
    26  	"io/fs"
    27  	"os"
    28  	"path/filepath"
    29  	"strings"
    30  	"testing"
    31  	"time"
    32  
    33  	jsonpatch "github.com/evanphx/json-patch"
    34  
    35  	apiextensionsinternal "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
    36  	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    37  	structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
    38  	apiservervalidation "k8s.io/apiextensions-apiserver/pkg/apiserver/validation"
    39  	"k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
    40  	"k8s.io/apiextensions-apiserver/pkg/features"
    41  	"k8s.io/apiextensions-apiserver/pkg/registry/customresource"
    42  	"k8s.io/apiextensions-apiserver/test/integration/fixtures"
    43  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    44  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    45  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    46  	"k8s.io/apimachinery/pkg/runtime"
    47  	"k8s.io/apimachinery/pkg/runtime/schema"
    48  	"k8s.io/apimachinery/pkg/util/uuid"
    49  	"k8s.io/apimachinery/pkg/util/wait"
    50  	utilyaml "k8s.io/apimachinery/pkg/util/yaml"
    51  	utilfeature "k8s.io/apiserver/pkg/util/feature"
    52  	"k8s.io/client-go/dynamic"
    53  	featuregatetesting "k8s.io/component-base/featuregate/testing"
    54  	"k8s.io/kube-openapi/pkg/validation/spec"
    55  	"k8s.io/kube-openapi/pkg/validation/strfmt"
    56  )
    57  
    58  var stringSchema *apiextensionsv1.JSONSchemaProps = &apiextensionsv1.JSONSchemaProps{
    59  	Type: "string",
    60  }
    61  
    62  var stringMapSchema *apiextensionsv1.JSONSchemaProps = &apiextensionsv1.JSONSchemaProps{
    63  	Type: "object",
    64  	AdditionalProperties: &apiextensionsv1.JSONSchemaPropsOrBool{
    65  		Schema: stringSchema,
    66  	},
    67  }
    68  
    69  var numberSchema *apiextensionsv1.JSONSchemaProps = &apiextensionsv1.JSONSchemaProps{
    70  	Type: "integer",
    71  }
    72  
    73  var numbersMapSchema *apiextensionsv1.JSONSchemaProps = &apiextensionsv1.JSONSchemaProps{
    74  	Type: "object",
    75  	AdditionalProperties: &apiextensionsv1.JSONSchemaPropsOrBool{
    76  		Schema: numberSchema,
    77  	},
    78  }
    79  
    80  type ratchetingTestContext struct {
    81  	*testing.T
    82  	DynamicClient       dynamic.Interface
    83  	APIExtensionsClient clientset.Interface
    84  }
    85  
    86  type ratchetingTestOperation interface {
    87  	Do(ctx *ratchetingTestContext) error
    88  	Description() string
    89  }
    90  
    91  type expectError struct {
    92  	op ratchetingTestOperation
    93  }
    94  
    95  func (e expectError) Do(ctx *ratchetingTestContext) error {
    96  	err := e.op.Do(ctx)
    97  	if err != nil {
    98  		return nil
    99  	}
   100  	return errors.New("expected error")
   101  }
   102  
   103  func (e expectError) Description() string {
   104  	return fmt.Sprintf("Expect Error: %v", e.op.Description())
   105  }
   106  
   107  // apiextensions-apiserver has discovery disabled, so hardcode this mapping
   108  var fakeRESTMapper map[schema.GroupVersionResource]string = map[schema.GroupVersionResource]string{
   109  	myCRDV1Beta1: "MyCoolCRD",
   110  }
   111  
   112  // FixTabsOrDie counts the number of tab characters preceding the first
   113  // line in the given yaml object. It removes that many tabs from every
   114  // line. It panics (it's a test function) if some line has fewer tabs
   115  // than the first line.
   116  //
   117  // The purpose of this is to make it easier to read tests.
   118  func FixTabsOrDie(in string) string {
   119  	lines := bytes.Split([]byte(in), []byte{'\n'})
   120  	if len(lines[0]) == 0 && len(lines) > 1 {
   121  		lines = lines[1:]
   122  	}
   123  	// Create prefix made of tabs that we want to remove.
   124  	var prefix []byte
   125  	for _, c := range lines[0] {
   126  		if c != '\t' {
   127  			break
   128  		}
   129  		prefix = append(prefix, byte('\t'))
   130  	}
   131  	// Remove prefix from all tabs, fail otherwise.
   132  	for i := range lines {
   133  		line := lines[i]
   134  		// It's OK for the last line to be blank (trailing \n)
   135  		if i == len(lines)-1 && len(line) <= len(prefix) && bytes.TrimSpace(line) == nil {
   136  			lines[i] = []byte{}
   137  			break
   138  		}
   139  		if !bytes.HasPrefix(line, prefix) {
   140  			panic(fmt.Errorf("line %d doesn't start with expected number (%d) of tabs: %v", i, len(prefix), string(line)))
   141  		}
   142  		lines[i] = line[len(prefix):]
   143  	}
   144  	joined := string(bytes.Join(lines, []byte{'\n'}))
   145  
   146  	// Convert rest of tabs to spaces since yaml doesnt like yabs
   147  	// (assuming 2 space alignment)
   148  	return strings.ReplaceAll(joined, "\t", "  ")
   149  }
   150  
   151  type applyPatchOperation struct {
   152  	description string
   153  	gvr         schema.GroupVersionResource
   154  	name        string
   155  	patch       interface{}
   156  }
   157  
   158  func (a applyPatchOperation) Do(ctx *ratchetingTestContext) error {
   159  	// Lookup GVK from discovery
   160  	kind, ok := fakeRESTMapper[a.gvr]
   161  	if !ok {
   162  		return fmt.Errorf("no mapping found for Gvr %v, add entry to fakeRESTMapper", a.gvr)
   163  	}
   164  
   165  	patch := &unstructured.Unstructured{}
   166  	if obj, ok := a.patch.(map[string]interface{}); ok {
   167  		patch.Object = obj
   168  	} else if str, ok := a.patch.(string); ok {
   169  		str = FixTabsOrDie(str)
   170  		if err := utilyaml.NewYAMLOrJSONDecoder(strings.NewReader(str), len(str)).Decode(&patch.Object); err != nil {
   171  			return err
   172  		}
   173  	} else {
   174  		return fmt.Errorf("invalid patch type: %T", a.patch)
   175  	}
   176  
   177  	patch.SetKind(kind)
   178  	patch.SetAPIVersion(a.gvr.GroupVersion().String())
   179  	patch.SetName(a.name)
   180  	patch.SetNamespace("default")
   181  
   182  	_, err := ctx.DynamicClient.
   183  		Resource(a.gvr).
   184  		Namespace(patch.GetNamespace()).
   185  		Apply(
   186  			context.TODO(),
   187  			patch.GetName(),
   188  			patch,
   189  			metav1.ApplyOptions{
   190  				FieldManager: "manager",
   191  			})
   192  
   193  	return err
   194  
   195  }
   196  
   197  func (a applyPatchOperation) Description() string {
   198  	return a.description
   199  }
   200  
   201  // Replaces schema used for v1beta1 of crd
   202  type updateMyCRDV1Beta1Schema struct {
   203  	newSchema *apiextensionsv1.JSONSchemaProps
   204  }
   205  
   206  func (u updateMyCRDV1Beta1Schema) Do(ctx *ratchetingTestContext) error {
   207  	var myCRD *apiextensionsv1.CustomResourceDefinition
   208  	var err error = apierrors.NewConflict(schema.GroupResource{}, "", nil)
   209  	for apierrors.IsConflict(err) {
   210  		myCRD, err = ctx.APIExtensionsClient.ApiextensionsV1().CustomResourceDefinitions().Get(context.TODO(), myCRDV1Beta1.Resource+"."+myCRDV1Beta1.Group, metav1.GetOptions{})
   211  		if err != nil {
   212  			return err
   213  		}
   214  
   215  		// Insert a sentinel property that we can probe to detect when the
   216  		// schema takes effect
   217  		sch := u.newSchema.DeepCopy()
   218  		if sch.Properties == nil {
   219  			sch.Properties = map[string]apiextensionsv1.JSONSchemaProps{}
   220  		}
   221  
   222  		uuidString := string(uuid.NewUUID())
   223  		sentinelName := "__ratcheting_sentinel_field__"
   224  		sch.Properties[sentinelName] = apiextensionsv1.JSONSchemaProps{
   225  			Type: "string",
   226  			Enum: []apiextensionsv1.JSON{{
   227  				Raw: []byte(`"` + uuidString + `"`),
   228  			}},
   229  		}
   230  
   231  		for _, v := range myCRD.Spec.Versions {
   232  			if v.Name != myCRDV1Beta1.Version {
   233  				continue
   234  			}
   235  			v.Schema.OpenAPIV3Schema = sch
   236  		}
   237  
   238  		_, err = ctx.APIExtensionsClient.ApiextensionsV1().CustomResourceDefinitions().Update(context.TODO(), myCRD, metav1.UpdateOptions{
   239  			FieldManager: "manager",
   240  		})
   241  		if err != nil {
   242  			return err
   243  		}
   244  
   245  		// Keep trying to create an invalid instance of the CRD until we
   246  		// get an error containing the message we are looking for
   247  		//
   248  		counter := 0
   249  		return wait.PollUntilContextCancel(context.TODO(), 100*time.Millisecond, true, func(_ context.Context) (done bool, err error) {
   250  			counter += 1
   251  			err = applyPatchOperation{
   252  				gvr:  myCRDV1Beta1,
   253  				name: "sentinel-resource",
   254  				patch: map[string]interface{}{
   255  					sentinelName: fmt.Sprintf("invalid-%d", counter),
   256  				}}.Do(ctx)
   257  
   258  			if err == nil {
   259  				return false, errors.New("expected error when creating sentinel resource")
   260  			}
   261  
   262  			// Check to see if the returned error message contains our
   263  			// unique string. UUID should be unique enough to just check
   264  			// simple existence in the error.
   265  			if strings.Contains(err.Error(), uuidString) {
   266  				return true, nil
   267  			}
   268  
   269  			return false, nil
   270  		})
   271  	}
   272  	return err
   273  }
   274  
   275  func (u updateMyCRDV1Beta1Schema) Description() string {
   276  	return "Update CRD schema"
   277  }
   278  
   279  type patchMyCRDV1Beta1Schema struct {
   280  	description string
   281  	patch       map[string]interface{}
   282  }
   283  
   284  func (p patchMyCRDV1Beta1Schema) Do(ctx *ratchetingTestContext) error {
   285  	var err error
   286  	patchJSON, err := json.Marshal(p.patch)
   287  	if err != nil {
   288  		return err
   289  	}
   290  
   291  	myCRD, err := ctx.APIExtensionsClient.ApiextensionsV1().CustomResourceDefinitions().Get(context.TODO(), myCRDV1Beta1.Resource+"."+myCRDV1Beta1.Group, metav1.GetOptions{})
   292  	if err != nil {
   293  		return err
   294  	}
   295  
   296  	for _, v := range myCRD.Spec.Versions {
   297  		if v.Name != myCRDV1Beta1.Version {
   298  			continue
   299  		}
   300  
   301  		jsonSchema, err := json.Marshal(v.Schema.OpenAPIV3Schema)
   302  		if err != nil {
   303  			return err
   304  		}
   305  
   306  		merged, err := jsonpatch.MergePatch(jsonSchema, patchJSON)
   307  		if err != nil {
   308  			return err
   309  		}
   310  
   311  		var parsed apiextensionsv1.JSONSchemaProps
   312  		if err := json.Unmarshal(merged, &parsed); err != nil {
   313  			return err
   314  		}
   315  
   316  		return updateMyCRDV1Beta1Schema{
   317  			newSchema: &parsed,
   318  		}.Do(ctx)
   319  	}
   320  
   321  	return fmt.Errorf("could not find version %v in CRD %v", myCRDV1Beta1.Version, myCRD.Name)
   322  }
   323  
   324  func (p patchMyCRDV1Beta1Schema) Description() string {
   325  	return p.description
   326  }
   327  
   328  type ratchetingTestCase struct {
   329  	Name       string
   330  	Disabled   bool
   331  	Operations []ratchetingTestOperation
   332  }
   333  
   334  func runTests(t *testing.T, cases []ratchetingTestCase) {
   335  	defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.CRDValidationRatcheting, true)()
   336  	tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t)
   337  	if err != nil {
   338  		t.Fatal(err)
   339  	}
   340  	defer tearDown()
   341  
   342  	group := myCRDV1Beta1.Group
   343  	version := myCRDV1Beta1.Version
   344  	resource := myCRDV1Beta1.Resource
   345  	kind := fakeRESTMapper[myCRDV1Beta1]
   346  
   347  	myCRD := &apiextensionsv1.CustomResourceDefinition{
   348  		ObjectMeta: metav1.ObjectMeta{Name: resource + "." + group},
   349  		Spec: apiextensionsv1.CustomResourceDefinitionSpec{
   350  			Group: group,
   351  			Versions: []apiextensionsv1.CustomResourceDefinitionVersion{{
   352  				Name:    version,
   353  				Served:  true,
   354  				Storage: true,
   355  				Schema: &apiextensionsv1.CustomResourceValidation{
   356  					OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{
   357  						Type: "object",
   358  						Properties: map[string]apiextensionsv1.JSONSchemaProps{
   359  							"content": {
   360  								Type: "object",
   361  								AdditionalProperties: &apiextensionsv1.JSONSchemaPropsOrBool{
   362  									Schema: &apiextensionsv1.JSONSchemaProps{
   363  										Type: "string",
   364  									},
   365  								},
   366  							},
   367  							"num": {
   368  								Type: "object",
   369  								AdditionalProperties: &apiextensionsv1.JSONSchemaPropsOrBool{
   370  									Schema: &apiextensionsv1.JSONSchemaProps{
   371  										Type: "integer",
   372  									},
   373  								},
   374  							},
   375  						},
   376  					},
   377  				},
   378  			}},
   379  			Names: apiextensionsv1.CustomResourceDefinitionNames{
   380  				Plural:   resource,
   381  				Kind:     kind,
   382  				ListKind: kind + "List",
   383  			},
   384  			Scope: apiextensionsv1.NamespaceScoped,
   385  		},
   386  	}
   387  
   388  	_, err = fixtures.CreateNewV1CustomResourceDefinition(myCRD, apiExtensionClient, dynamicClient)
   389  	if err != nil {
   390  		t.Fatal(err)
   391  	}
   392  	for _, c := range cases {
   393  		if c.Disabled {
   394  			continue
   395  		}
   396  
   397  		t.Run(c.Name, func(t *testing.T) {
   398  			ctx := &ratchetingTestContext{
   399  				T:                   t,
   400  				DynamicClient:       dynamicClient,
   401  				APIExtensionsClient: apiExtensionClient,
   402  			}
   403  
   404  			for i, op := range c.Operations {
   405  				t.Logf("Performing Operation: %v", op.Description())
   406  				if err := op.Do(ctx); err != nil {
   407  					t.Fatalf("failed %T operation %v: %v\n%v", op, i, err, op)
   408  				}
   409  			}
   410  
   411  			// Reset resources
   412  			err := ctx.DynamicClient.Resource(myCRDV1Beta1).Namespace("default").DeleteCollection(context.TODO(), metav1.DeleteOptions{}, metav1.ListOptions{})
   413  			if err != nil {
   414  				t.Fatal(err)
   415  			}
   416  		})
   417  	}
   418  }
   419  
   420  var myCRDV1Beta1 schema.GroupVersionResource = schema.GroupVersionResource{
   421  	Group:    "mygroup.example.com",
   422  	Version:  "v1beta1",
   423  	Resource: "mycrds",
   424  }
   425  
   426  var myCRDInstanceName string = "mycrdinstance"
   427  
   428  func TestRatchetingFunctionality(t *testing.T) {
   429  	cases := []ratchetingTestCase{
   430  		{
   431  			Name: "Minimum Maximum",
   432  			Operations: []ratchetingTestOperation{
   433  				updateMyCRDV1Beta1Schema{&apiextensionsv1.JSONSchemaProps{
   434  					Type: "object",
   435  					Properties: map[string]apiextensionsv1.JSONSchemaProps{
   436  						"hasMinimum":           *numberSchema,
   437  						"hasMaximum":           *numberSchema,
   438  						"hasMinimumAndMaximum": *numberSchema,
   439  					},
   440  				}},
   441  				applyPatchOperation{
   442  					"Create an object that complies with the schema",
   443  					myCRDV1Beta1,
   444  					myCRDInstanceName,
   445  					map[string]interface{}{
   446  						"hasMinimum":           0,
   447  						"hasMaximum":           1000,
   448  						"hasMinimumAndMaximum": 50,
   449  					}},
   450  				patchMyCRDV1Beta1Schema{
   451  					"Add stricter minimums and maximums that violate the previous object",
   452  					map[string]interface{}{
   453  						"properties": map[string]interface{}{
   454  							"hasMinimum": map[string]interface{}{
   455  								"minimum": 10,
   456  							},
   457  							"hasMaximum": map[string]interface{}{
   458  								"maximum": 20,
   459  							},
   460  							"hasMinimumAndMaximum": map[string]interface{}{
   461  								"minimum": 10,
   462  								"maximum": 20,
   463  							},
   464  							"noRestrictions": map[string]interface{}{
   465  								"type": "integer",
   466  							},
   467  						},
   468  					}},
   469  				applyPatchOperation{
   470  					"Add new fields that validates successfully without changing old ones",
   471  					myCRDV1Beta1,
   472  					myCRDInstanceName,
   473  					map[string]interface{}{
   474  						"noRestrictions": 50,
   475  					}},
   476  				expectError{
   477  					applyPatchOperation{
   478  						"Change a single old field to be invalid",
   479  						myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
   480  							"hasMinimum": 5,
   481  						}},
   482  				},
   483  				expectError{
   484  					applyPatchOperation{
   485  						"Change multiple old fields to be invalid",
   486  						myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
   487  							"hasMinimum": 5,
   488  							"hasMaximum": 21,
   489  						}},
   490  				},
   491  				applyPatchOperation{
   492  					"Change single old field to be valid",
   493  					myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
   494  						"hasMinimum": 11,
   495  					}},
   496  				applyPatchOperation{
   497  					"Change multiple old fields to be valid",
   498  					myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
   499  						"hasMaximum":           19,
   500  						"hasMinimumAndMaximum": 15,
   501  					}},
   502  			},
   503  		},
   504  		{
   505  			Name: "Enum",
   506  			Operations: []ratchetingTestOperation{
   507  				// Create schema with some enum element
   508  				updateMyCRDV1Beta1Schema{&apiextensionsv1.JSONSchemaProps{
   509  					Type: "object",
   510  					Properties: map[string]apiextensionsv1.JSONSchemaProps{
   511  						"enumField": *stringSchema,
   512  					},
   513  				}},
   514  				applyPatchOperation{
   515  					"Create an instance with a soon-to-be-invalid value",
   516  					myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
   517  						"enumField": "okValueNowBadValueLater",
   518  					}},
   519  				patchMyCRDV1Beta1Schema{
   520  					"restrict `enumField` to an enum of A, B, or C",
   521  					map[string]interface{}{
   522  						"properties": map[string]interface{}{
   523  							"enumField": map[string]interface{}{
   524  								"enum": []interface{}{
   525  									"A", "B", "C",
   526  								},
   527  							},
   528  							"otherField": map[string]interface{}{
   529  								"type": "string",
   530  							},
   531  						},
   532  					}},
   533  				applyPatchOperation{
   534  					"An invalid patch with no changes is a noop",
   535  					myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
   536  						"enumField": "okValueNowBadValueLater",
   537  					}},
   538  				applyPatchOperation{
   539  					"Add a new field, and include old value in our patch",
   540  					myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
   541  						"enumField":  "okValueNowBadValueLater",
   542  						"otherField": "anythingGoes",
   543  					}},
   544  				expectError{
   545  					applyPatchOperation{
   546  						"Set enumField to invalid value D",
   547  						myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
   548  							"enumField": "D",
   549  						}},
   550  				},
   551  				applyPatchOperation{
   552  					"Set to a valid value",
   553  					myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
   554  						"enumField": "A",
   555  					}},
   556  				expectError{
   557  					applyPatchOperation{
   558  						"After setting a valid value, return to the old, accepted value",
   559  						myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
   560  							"enumField": "okValueNowBadValueLater",
   561  						}},
   562  				},
   563  			},
   564  		},
   565  		{
   566  			Name: "AdditionalProperties",
   567  			Operations: []ratchetingTestOperation{
   568  				updateMyCRDV1Beta1Schema{&apiextensionsv1.JSONSchemaProps{
   569  					Type: "object",
   570  					Properties: map[string]apiextensionsv1.JSONSchemaProps{
   571  						"nums":    *numbersMapSchema,
   572  						"content": *stringMapSchema,
   573  					},
   574  				}},
   575  				applyPatchOperation{
   576  					"Create an instance",
   577  					myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
   578  						"nums": map[string]interface{}{
   579  							"num1": 1,
   580  							"num2": 1000000,
   581  						},
   582  						"content": map[string]interface{}{
   583  							"k1": "some content",
   584  							"k2": "other content",
   585  						},
   586  					}},
   587  				patchMyCRDV1Beta1Schema{
   588  					"set minimum value for fields with additionalProperties",
   589  					map[string]interface{}{
   590  						"properties": map[string]interface{}{
   591  							"nums": map[string]interface{}{
   592  								"additionalProperties": map[string]interface{}{
   593  									"minimum": 1000,
   594  								},
   595  							},
   596  						},
   597  					}},
   598  				applyPatchOperation{
   599  					"updating validating field num2 to another validating value, but rachet invalid field num1",
   600  					myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
   601  						"nums": map[string]interface{}{
   602  							"num1": 1,
   603  							"num2": 2000,
   604  						},
   605  					}},
   606  				expectError{applyPatchOperation{
   607  					"update field num1 to different invalid value",
   608  					myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
   609  						"nums": map[string]interface{}{
   610  							"num1": 2,
   611  							"num2": 2000,
   612  						},
   613  					}}},
   614  			},
   615  		},
   616  		{
   617  			Name: "MinProperties MaxProperties",
   618  			Operations: []ratchetingTestOperation{
   619  				updateMyCRDV1Beta1Schema{&apiextensionsv1.JSONSchemaProps{
   620  					Type: "object",
   621  					Properties: map[string]apiextensionsv1.JSONSchemaProps{
   622  						"restricted": {
   623  							Type: "object",
   624  							AdditionalProperties: &apiextensionsv1.JSONSchemaPropsOrBool{
   625  								Schema: stringSchema,
   626  							},
   627  						},
   628  						"unrestricted": {
   629  							Type: "object",
   630  							AdditionalProperties: &apiextensionsv1.JSONSchemaPropsOrBool{
   631  								Schema: stringSchema,
   632  							},
   633  						},
   634  					},
   635  				}},
   636  				applyPatchOperation{
   637  					"Create instance",
   638  					myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
   639  						"restricted": map[string]interface{}{
   640  							"key1": "hi",
   641  							"key2": "there",
   642  						},
   643  					}},
   644  				patchMyCRDV1Beta1Schema{
   645  					"set both minProperties and maxProperties to 1 to violate the previous object",
   646  					map[string]interface{}{
   647  						"properties": map[string]interface{}{
   648  							"restricted": map[string]interface{}{
   649  								"minProperties": 1,
   650  								"maxProperties": 1,
   651  							},
   652  						},
   653  					}},
   654  				applyPatchOperation{
   655  					"ratchet violating object 'restricted' around changes to unrelated field",
   656  					myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
   657  						"restricted": map[string]interface{}{
   658  							"key1": "hi",
   659  							"key2": "there",
   660  						},
   661  						"unrestricted": map[string]interface{}{
   662  							"key1": "yo",
   663  						},
   664  					}},
   665  				expectError{applyPatchOperation{
   666  					"make invalid changes to previously ratcheted invalid field",
   667  					myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
   668  						"restricted": map[string]interface{}{
   669  							"key1": "changed",
   670  							"key2": "there",
   671  						},
   672  						"unrestricted": map[string]interface{}{
   673  							"key1": "yo",
   674  						},
   675  					}}},
   676  
   677  				patchMyCRDV1Beta1Schema{
   678  					"remove maxProeprties, set minProperties to 2",
   679  					map[string]interface{}{
   680  						"properties": map[string]interface{}{
   681  							"restricted": map[string]interface{}{
   682  								"minProperties": 2,
   683  								"maxProperties": nil,
   684  							},
   685  						},
   686  					}},
   687  				applyPatchOperation{
   688  					"a new value",
   689  					myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
   690  						"restricted": map[string]interface{}{
   691  							"key1": "hi",
   692  							"key2": "there",
   693  							"key3": "buddy",
   694  						},
   695  					}},
   696  
   697  				expectError{applyPatchOperation{
   698  					"violate new validation by removing keys",
   699  					myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
   700  						"restricted": map[string]interface{}{
   701  							"key1": "hi",
   702  							"key2": nil,
   703  							"key3": nil,
   704  						},
   705  					}}},
   706  				patchMyCRDV1Beta1Schema{
   707  					"remove minProperties, set maxProperties to 1",
   708  					map[string]interface{}{
   709  						"properties": map[string]interface{}{
   710  							"restricted": map[string]interface{}{
   711  								"minProperties": nil,
   712  								"maxProperties": 1,
   713  							},
   714  						},
   715  					}},
   716  				applyPatchOperation{
   717  					"modify only the other key, ratcheting maxProperties for field `restricted`",
   718  					myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
   719  						"restricted": map[string]interface{}{
   720  							"key1": "hi",
   721  							"key2": "there",
   722  							"key3": "buddy",
   723  						},
   724  						"unrestricted": map[string]interface{}{
   725  							"key1": "value",
   726  							"key2": "value",
   727  						},
   728  					}},
   729  				expectError{
   730  					applyPatchOperation{
   731  						"modifying one value in the object with maxProperties restriction, but keeping old fields",
   732  						myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
   733  							"restricted": map[string]interface{}{
   734  								"key1": "hi",
   735  								"key2": "theres",
   736  								"key3": "buddy",
   737  							},
   738  						}}},
   739  			},
   740  		},
   741  		{
   742  			Name: "MinItems",
   743  			Operations: []ratchetingTestOperation{
   744  				updateMyCRDV1Beta1Schema{&apiextensionsv1.JSONSchemaProps{
   745  					Type: "object",
   746  					Properties: map[string]apiextensionsv1.JSONSchemaProps{
   747  						"field": *stringSchema,
   748  						"array": {
   749  							Type: "array",
   750  							Items: &apiextensionsv1.JSONSchemaPropsOrArray{
   751  								Schema: stringSchema,
   752  							},
   753  						},
   754  					},
   755  				}},
   756  				applyPatchOperation{
   757  					"Create instance",
   758  					myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
   759  						"array": []interface{}{"value1", "value2", "value3"},
   760  					}},
   761  				patchMyCRDV1Beta1Schema{
   762  					"change minItems on array to 10, invalidates previous object",
   763  					map[string]interface{}{
   764  						"properties": map[string]interface{}{
   765  							"array": map[string]interface{}{
   766  								"minItems": 10,
   767  							},
   768  						},
   769  					}},
   770  				applyPatchOperation{
   771  					"keep invalid field `array` unchanged, add new field with ratcheting",
   772  					myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
   773  						"array": []interface{}{"value1", "value2", "value3"},
   774  						"field": "value",
   775  					}},
   776  				expectError{
   777  					applyPatchOperation{
   778  						"modify array element without satisfying property",
   779  						myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
   780  							"array": []interface{}{"value2", "value2", "value3"},
   781  						}}},
   782  
   783  				expectError{
   784  					applyPatchOperation{
   785  						"add array element without satisfying proeprty",
   786  						myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
   787  							"array": []interface{}{"value1", "value2", "value3", "value4"},
   788  						}}},
   789  
   790  				applyPatchOperation{
   791  					"make array valid",
   792  					myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
   793  						"array": []interface{}{"value1", "value2", "value3", "4", "5", "6", "7", "8", "9", "10"},
   794  					}},
   795  				expectError{
   796  					applyPatchOperation{
   797  						"revert to original value",
   798  						myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
   799  							"array": []interface{}{"value1", "value2", "value3"},
   800  						}}},
   801  			},
   802  		},
   803  		{
   804  			Name: "MaxItems",
   805  			Operations: []ratchetingTestOperation{
   806  				updateMyCRDV1Beta1Schema{&apiextensionsv1.JSONSchemaProps{
   807  					Type: "object",
   808  					Properties: map[string]apiextensionsv1.JSONSchemaProps{
   809  						"field": *stringSchema,
   810  						"array": {
   811  							Type: "array",
   812  							Items: &apiextensionsv1.JSONSchemaPropsOrArray{
   813  								Schema: stringSchema,
   814  							},
   815  						},
   816  					},
   817  				}},
   818  				applyPatchOperation{
   819  					"create instance",
   820  					myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
   821  						"array": []interface{}{"value1", "value2", "value3"},
   822  					}},
   823  				patchMyCRDV1Beta1Schema{
   824  					"change maxItems on array to 1, invalidates previous object",
   825  					map[string]interface{}{
   826  						"properties": map[string]interface{}{
   827  							"array": map[string]interface{}{
   828  								"maxItems": 1,
   829  							},
   830  						},
   831  					}},
   832  				applyPatchOperation{
   833  					"ratchet old value of array through an update to another field",
   834  					myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
   835  						"array": []interface{}{"value1", "value2", "value3"},
   836  						"field": "value",
   837  					}},
   838  				expectError{
   839  					applyPatchOperation{
   840  						"modify array element without satisfying property",
   841  						myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
   842  							"array": []interface{}{"value2", "value2", "value3"},
   843  						}}},
   844  
   845  				expectError{
   846  					applyPatchOperation{
   847  						"remove array element without satisfying proeprty",
   848  						myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
   849  							"array": []interface{}{"value1", "value2"},
   850  						}}},
   851  
   852  				applyPatchOperation{
   853  					"change array to valid value that satisfies maxItems",
   854  					myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
   855  						"array": []interface{}{"value1"},
   856  					}},
   857  				expectError{
   858  					applyPatchOperation{
   859  						"revert to previous invalid ratcheted value",
   860  						myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
   861  							"array": []interface{}{"value1", "value2", "value3"},
   862  						}}},
   863  			},
   864  		},
   865  		{
   866  			Name: "MinLength MaxLength",
   867  			Operations: []ratchetingTestOperation{
   868  				updateMyCRDV1Beta1Schema{&apiextensionsv1.JSONSchemaProps{
   869  					Type: "object",
   870  					Properties: map[string]apiextensionsv1.JSONSchemaProps{
   871  						"minField":   *stringSchema,
   872  						"maxField":   *stringSchema,
   873  						"otherField": *stringSchema,
   874  					},
   875  				}},
   876  				applyPatchOperation{
   877  					"create instance",
   878  					myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
   879  						"minField": "value",
   880  						"maxField": "valueThatsVeryLongSee",
   881  					}},
   882  				patchMyCRDV1Beta1Schema{
   883  					"set minField maxLength to 10, and maxField's minLength to 15",
   884  					map[string]interface{}{
   885  						"properties": map[string]interface{}{
   886  							"minField": map[string]interface{}{
   887  								"minLength": 10,
   888  							},
   889  							"maxField": map[string]interface{}{
   890  								"maxLength": 15,
   891  							},
   892  						},
   893  					}},
   894  				applyPatchOperation{
   895  					"add new field `otherField`, ratcheting `minField` and `maxField`",
   896  					myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
   897  						"minField":   "value",
   898  						"maxField":   "valueThatsVeryLongSee",
   899  						"otherField": "otherValue",
   900  					}},
   901  				applyPatchOperation{
   902  					"make minField valid, ratcheting old value for maxField",
   903  					myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
   904  						"minField":   "valuelength13",
   905  						"maxField":   "valueThatsVeryLongSee",
   906  						"otherField": "otherValue",
   907  					}},
   908  				applyPatchOperation{
   909  					"make maxField shorter",
   910  					myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
   911  						"maxField": "l2",
   912  					}},
   913  				expectError{
   914  					applyPatchOperation{
   915  						"make maxField too long",
   916  						myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
   917  							"maxField": "valuewithlength17",
   918  						}}},
   919  				expectError{
   920  					applyPatchOperation{
   921  						"revert minFIeld to previously ratcheted value",
   922  						myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
   923  							"minField": "value",
   924  						}}},
   925  				expectError{
   926  					applyPatchOperation{
   927  						"revert maxField to previously ratcheted value",
   928  						myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
   929  							"maxField": "valueThatsVeryLongSee",
   930  						}}},
   931  			},
   932  		},
   933  		{
   934  			Name: "Pattern",
   935  			Operations: []ratchetingTestOperation{
   936  				updateMyCRDV1Beta1Schema{&apiextensionsv1.JSONSchemaProps{
   937  					Type: "object",
   938  					Properties: map[string]apiextensionsv1.JSONSchemaProps{
   939  						"field": *stringSchema,
   940  					},
   941  				}},
   942  				applyPatchOperation{
   943  					"create instance",
   944  					myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
   945  						"field": "doesnt abide pattern",
   946  					}},
   947  				patchMyCRDV1Beta1Schema{
   948  					"add pattern validation on `field`",
   949  					map[string]interface{}{
   950  						"properties": map[string]interface{}{
   951  							"field": map[string]interface{}{
   952  								"pattern": "^[1-9]+$",
   953  							},
   954  							"otherField": map[string]interface{}{
   955  								"type": "string",
   956  							},
   957  						},
   958  					}},
   959  				applyPatchOperation{
   960  					"add unrelated field, ratcheting old invalid field",
   961  					myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
   962  						"field":      "doesnt abide pattern",
   963  						"otherField": "added",
   964  					}},
   965  				expectError{applyPatchOperation{
   966  					"change field to invalid value",
   967  					myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
   968  						"field":      "w123",
   969  						"otherField": "added",
   970  					}}},
   971  				applyPatchOperation{
   972  					"change field to a valid value",
   973  					myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
   974  						"field":      "123",
   975  						"otherField": "added",
   976  					}},
   977  			},
   978  		},
   979  		{
   980  			Name: "Format Addition and Change",
   981  			Operations: []ratchetingTestOperation{
   982  				updateMyCRDV1Beta1Schema{&apiextensionsv1.JSONSchemaProps{
   983  					Type: "object",
   984  					Properties: map[string]apiextensionsv1.JSONSchemaProps{
   985  						"field": *stringSchema,
   986  					},
   987  				}},
   988  				applyPatchOperation{
   989  					"create instance",
   990  					myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
   991  						"field": "doesnt abide any format",
   992  					}},
   993  				patchMyCRDV1Beta1Schema{
   994  					"change `field`'s format to `byte",
   995  					map[string]interface{}{
   996  						"properties": map[string]interface{}{
   997  							"field": map[string]interface{}{
   998  								"format": "byte",
   999  							},
  1000  							"otherField": map[string]interface{}{
  1001  								"type": "string",
  1002  							},
  1003  						},
  1004  					}},
  1005  				applyPatchOperation{
  1006  					"add unrelated otherField, ratchet invalid old field format",
  1007  					myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
  1008  						"field":      "doesnt abide any format",
  1009  						"otherField": "value",
  1010  					}},
  1011  				expectError{applyPatchOperation{
  1012  					"change field to an invalid string",
  1013  					myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
  1014  						"field": "asd",
  1015  					}}},
  1016  				applyPatchOperation{
  1017  					"change field to a valid byte string",
  1018  					myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
  1019  						"field": "dGhpcyBpcyBwYXNzd29yZA==",
  1020  					}},
  1021  				patchMyCRDV1Beta1Schema{
  1022  					"change `field`'s format to date-time",
  1023  					map[string]interface{}{
  1024  						"properties": map[string]interface{}{
  1025  							"field": map[string]interface{}{
  1026  								"format": "date-time",
  1027  							},
  1028  						},
  1029  					}},
  1030  				applyPatchOperation{
  1031  					"change otherField, ratchet `field`'s invalid byte format",
  1032  					myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
  1033  						"field":      "dGhpcyBpcyBwYXNzd29yZA==",
  1034  						"otherField": "value2",
  1035  					}},
  1036  				applyPatchOperation{
  1037  					"change `field` to a valid value",
  1038  					myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
  1039  						"field":      "2018-11-13T20:20:39+00:00",
  1040  						"otherField": "value2",
  1041  					}},
  1042  				expectError{
  1043  					applyPatchOperation{
  1044  						"revert `field` to previously ratcheted value",
  1045  						myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
  1046  							"field":      "dGhpcyBpcyBwYXNzd29yZA==",
  1047  							"otherField": "value2",
  1048  						}}},
  1049  				expectError{
  1050  					applyPatchOperation{
  1051  						"revert `field` to its initial value from creation",
  1052  						myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
  1053  							"field": "doesnt abide any format",
  1054  						}}},
  1055  			},
  1056  		},
  1057  		{
  1058  			Name: "Map Type List Reordering Grandfathers Invalid Key",
  1059  			Operations: []ratchetingTestOperation{
  1060  				updateMyCRDV1Beta1Schema{&apiextensionsv1.JSONSchemaProps{
  1061  					Type: "object",
  1062  					Properties: map[string]apiextensionsv1.JSONSchemaProps{
  1063  						"field": {
  1064  							Type:         "array",
  1065  							XListType:    ptr("map"),
  1066  							XListMapKeys: []string{"name", "port"},
  1067  							Items: &apiextensionsv1.JSONSchemaPropsOrArray{
  1068  								Schema: &apiextensionsv1.JSONSchemaProps{
  1069  									Type:     "object",
  1070  									Required: []string{"name", "port"},
  1071  									Properties: map[string]apiextensionsv1.JSONSchemaProps{
  1072  										"name":  *stringSchema,
  1073  										"port":  *numberSchema,
  1074  										"field": *stringSchema,
  1075  									},
  1076  								},
  1077  							},
  1078  						},
  1079  					},
  1080  				}},
  1081  				applyPatchOperation{
  1082  					"create instance with three soon-to-be-invalid keys",
  1083  					myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
  1084  						"field": []interface{}{
  1085  							map[string]interface{}{
  1086  								"name":  "nginx",
  1087  								"port":  443,
  1088  								"field": "value",
  1089  							},
  1090  							map[string]interface{}{
  1091  								"name":  "etcd",
  1092  								"port":  2379,
  1093  								"field": "value",
  1094  							},
  1095  							map[string]interface{}{
  1096  								"name":  "kube-apiserver",
  1097  								"port":  6443,
  1098  								"field": "value",
  1099  							},
  1100  						},
  1101  					}},
  1102  				patchMyCRDV1Beta1Schema{
  1103  					"set `field`'s maxItems to 2, which is exceeded by all of previous object's elements",
  1104  					map[string]interface{}{
  1105  						"properties": map[string]interface{}{
  1106  							"field": map[string]interface{}{
  1107  								"maxItems": 2,
  1108  							},
  1109  						},
  1110  					}},
  1111  				applyPatchOperation{
  1112  					"reorder invalid objects which have too many properties, but do not modify them or change keys",
  1113  					myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
  1114  						"field": []interface{}{
  1115  							map[string]interface{}{
  1116  								"name":  "kube-apiserver",
  1117  								"port":  6443,
  1118  								"field": "value",
  1119  							},
  1120  							map[string]interface{}{
  1121  								"name":  "nginx",
  1122  								"port":  443,
  1123  								"field": "value",
  1124  							},
  1125  							map[string]interface{}{
  1126  								"name":  "etcd",
  1127  								"port":  2379,
  1128  								"field": "value",
  1129  							},
  1130  						},
  1131  					}},
  1132  				expectError{
  1133  					applyPatchOperation{
  1134  						"attempt to change one of the fields of the items which exceed maxItems",
  1135  						myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
  1136  							"field": []interface{}{
  1137  								map[string]interface{}{
  1138  									"name":  "kube-apiserver",
  1139  									"port":  6443,
  1140  									"field": "value",
  1141  								},
  1142  								map[string]interface{}{
  1143  									"name":  "nginx",
  1144  									"port":  443,
  1145  									"field": "value",
  1146  								},
  1147  								map[string]interface{}{
  1148  									"name":  "etcd",
  1149  									"port":  2379,
  1150  									"field": "value",
  1151  								},
  1152  								map[string]interface{}{
  1153  									"name":  "dev",
  1154  									"port":  8080,
  1155  									"field": "value",
  1156  								},
  1157  							},
  1158  						}}},
  1159  				patchMyCRDV1Beta1Schema{
  1160  					"Require even numbered port in key, remove maxItems requirement",
  1161  					map[string]interface{}{
  1162  						"properties": map[string]interface{}{
  1163  							"field": map[string]interface{}{
  1164  								"maxItems": nil,
  1165  								"items": map[string]interface{}{
  1166  									"properties": map[string]interface{}{
  1167  										"port": map[string]interface{}{
  1168  											"multipleOf": 2,
  1169  										},
  1170  									},
  1171  								},
  1172  							},
  1173  						},
  1174  					}},
  1175  
  1176  				applyPatchOperation{
  1177  					"reorder fields without changing anything",
  1178  					myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
  1179  						"field": []interface{}{
  1180  							map[string]interface{}{
  1181  								"name":  "nginx",
  1182  								"port":  443,
  1183  								"field": "value",
  1184  							},
  1185  							map[string]interface{}{
  1186  								"name":  "etcd",
  1187  								"port":  2379,
  1188  								"field": "value",
  1189  							},
  1190  							map[string]interface{}{
  1191  								"name":  "kube-apiserver",
  1192  								"port":  6443,
  1193  								"field": "value",
  1194  							},
  1195  						},
  1196  					}},
  1197  
  1198  				applyPatchOperation{
  1199  					`use "invalid" keys despite changing order and changing sibling fields to the key`,
  1200  					myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
  1201  						"field": []interface{}{
  1202  							map[string]interface{}{
  1203  								"name":  "nginx",
  1204  								"port":  443,
  1205  								"field": "value",
  1206  							},
  1207  							map[string]interface{}{
  1208  								"name":  "etcd",
  1209  								"port":  2379,
  1210  								"field": "value",
  1211  							},
  1212  							map[string]interface{}{
  1213  								"name":  "kube-apiserver",
  1214  								"port":  6443,
  1215  								"field": "this is a changed value for an an invalid but grandfathered key",
  1216  							},
  1217  							map[string]interface{}{
  1218  								"name":  "dev",
  1219  								"port":  8080,
  1220  								"field": "value",
  1221  							},
  1222  						},
  1223  					}},
  1224  			},
  1225  		},
  1226  		{
  1227  			Name: "ArrayItems do not correlate by index",
  1228  			Operations: []ratchetingTestOperation{
  1229  				updateMyCRDV1Beta1Schema{&apiextensionsv1.JSONSchemaProps{
  1230  					Type: "object",
  1231  					Properties: map[string]apiextensionsv1.JSONSchemaProps{
  1232  						"values": {
  1233  							Type: "array",
  1234  							Items: &apiextensionsv1.JSONSchemaPropsOrArray{
  1235  								Schema: stringMapSchema,
  1236  							},
  1237  						},
  1238  						"otherField": *stringSchema,
  1239  					},
  1240  				}},
  1241  				applyPatchOperation{
  1242  					"create instance with length 5 values",
  1243  					myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
  1244  						"values": []interface{}{
  1245  							map[string]interface{}{
  1246  								"name": "1",
  1247  								"key":  "value",
  1248  							},
  1249  							map[string]interface{}{
  1250  								"name": "2",
  1251  								"key":  "value",
  1252  							},
  1253  						},
  1254  					}},
  1255  				patchMyCRDV1Beta1Schema{
  1256  					"Set minimum length of 6 for values of elements in the items array",
  1257  					map[string]interface{}{
  1258  						"properties": map[string]interface{}{
  1259  							"values": map[string]interface{}{
  1260  								"items": map[string]interface{}{
  1261  									"additionalProperties": map[string]interface{}{
  1262  										"minLength": 6,
  1263  									},
  1264  								},
  1265  							},
  1266  						},
  1267  					}},
  1268  				expectError{
  1269  					applyPatchOperation{
  1270  						"change value to one that exceeds minLength",
  1271  						myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
  1272  							"values": []interface{}{
  1273  								map[string]interface{}{
  1274  									"name": "1",
  1275  									"key":  "value",
  1276  								},
  1277  								map[string]interface{}{
  1278  									"name": "2",
  1279  									"key":  "bad",
  1280  								},
  1281  							},
  1282  						}}},
  1283  				applyPatchOperation{
  1284  					"add new fields without touching the map",
  1285  					myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
  1286  						"values": []interface{}{
  1287  							map[string]interface{}{
  1288  								"name": "1",
  1289  								"key":  "value",
  1290  							},
  1291  							map[string]interface{}{
  1292  								"name": "2",
  1293  								"key":  "value",
  1294  							},
  1295  						},
  1296  						"otherField": "hello world",
  1297  					}},
  1298  				// (This test shows an array cannpt be correlated by index with its old value)
  1299  				expectError{applyPatchOperation{
  1300  					"add new, valid fields to elements of the array, failing to ratchet unchanged old fields within the array elements by correlating by index due to atomic list",
  1301  					myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
  1302  						"values": []interface{}{
  1303  							map[string]interface{}{
  1304  								"name": "1",
  1305  								"key":  "value",
  1306  							},
  1307  							map[string]interface{}{
  1308  								"name": "2",
  1309  								"key":  "value",
  1310  								"key2": "valid value",
  1311  							},
  1312  						},
  1313  					}}},
  1314  				expectError{
  1315  					applyPatchOperation{
  1316  						"reorder the array, preventing index correlation",
  1317  						myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
  1318  							"values": []interface{}{
  1319  								map[string]interface{}{
  1320  									"name": "2",
  1321  									"key":  "value",
  1322  									"key2": "valid value",
  1323  								},
  1324  								map[string]interface{}{
  1325  									"name": "1",
  1326  									"key":  "value",
  1327  								},
  1328  							},
  1329  						}}},
  1330  			},
  1331  		},
  1332  		{
  1333  			Name: "CEL Optional OldSelf",
  1334  			Operations: []ratchetingTestOperation{
  1335  				updateMyCRDV1Beta1Schema{&apiextensionsv1.JSONSchemaProps{
  1336  					Type: "object",
  1337  					Properties: map[string]apiextensionsv1.JSONSchemaProps{
  1338  						"field": {
  1339  							Type: "string",
  1340  							XValidations: []apiextensionsv1.ValidationRule{
  1341  								{
  1342  									Rule:            "!oldSelf.hasValue()",
  1343  									Message:         "oldSelf must be null",
  1344  									OptionalOldSelf: ptr(true),
  1345  								},
  1346  							},
  1347  						},
  1348  					},
  1349  				}},
  1350  
  1351  				applyPatchOperation{
  1352  					"create instance passes since oldself is null",
  1353  					myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
  1354  						"field": "value",
  1355  					}},
  1356  
  1357  				expectError{
  1358  					applyPatchOperation{
  1359  						"update field fails, since oldself is not null",
  1360  						myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
  1361  							"field": "value2",
  1362  						},
  1363  					},
  1364  				},
  1365  
  1366  				expectError{
  1367  					applyPatchOperation{
  1368  						"noop update field fails, since oldself is not null and transition rules are not ratcheted",
  1369  						myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
  1370  							"field": "value",
  1371  						},
  1372  					},
  1373  				},
  1374  			},
  1375  		},
  1376  		// Features that should not ratchet
  1377  		{
  1378  			Name: "AllOf_should_not_ratchet",
  1379  		},
  1380  		{
  1381  			Name: "OneOf_should_not_ratchet",
  1382  		},
  1383  		{
  1384  			Name: "AnyOf_should_not_ratchet",
  1385  		},
  1386  		{
  1387  			Name: "Not_should_not_ratchet",
  1388  		},
  1389  		{
  1390  			Name: "CEL_transition_rules_should_not_ratchet",
  1391  			Operations: []ratchetingTestOperation{
  1392  				updateMyCRDV1Beta1Schema{&apiextensionsv1.JSONSchemaProps{
  1393  					Type:                   "object",
  1394  					XPreserveUnknownFields: ptr(true),
  1395  				}},
  1396  				applyPatchOperation{
  1397  					"create instance with strings that do not start with k8s",
  1398  					myCRDV1Beta1, myCRDInstanceName,
  1399  					`
  1400  						myStringField: myStringValue
  1401  						myOtherField: myOtherField
  1402  					`,
  1403  				},
  1404  				updateMyCRDV1Beta1Schema{&apiextensionsv1.JSONSchemaProps{
  1405  					Type:                   "object",
  1406  					XPreserveUnknownFields: ptr(true),
  1407  					Properties: map[string]apiextensionsv1.JSONSchemaProps{
  1408  						"myStringField": {
  1409  							Type: "string",
  1410  							XValidations: apiextensionsv1.ValidationRules{
  1411  								{
  1412  									Rule: "oldSelf != 'myStringValue' || self == 'validstring'",
  1413  								},
  1414  							},
  1415  						},
  1416  					},
  1417  				}},
  1418  				expectError{applyPatchOperation{
  1419  					"try to change one field to valid value, but unchanged field fails to be ratcheted by transition rule",
  1420  					myCRDV1Beta1, myCRDInstanceName,
  1421  					`
  1422  						myOtherField: myNewOtherField
  1423  						myStringField: myStringValue
  1424  					`,
  1425  				}},
  1426  				applyPatchOperation{
  1427  					"change both fields to valid values",
  1428  					myCRDV1Beta1, myCRDInstanceName,
  1429  					`
  1430  						myStringField: validstring
  1431  						myOtherField: myNewOtherField
  1432  					`,
  1433  				},
  1434  			},
  1435  		},
  1436  		// Future Functionality, disabled tests
  1437  		{
  1438  			Name: "CEL Add Change Rule",
  1439  			Operations: []ratchetingTestOperation{
  1440  				updateMyCRDV1Beta1Schema{&apiextensionsv1.JSONSchemaProps{
  1441  					Type: "object",
  1442  					Properties: map[string]apiextensionsv1.JSONSchemaProps{
  1443  						"field": {
  1444  							Type: "object",
  1445  							AdditionalProperties: &apiextensionsv1.JSONSchemaPropsOrBool{
  1446  								Schema: &apiextensionsv1.JSONSchemaProps{
  1447  									Type: "object",
  1448  									Properties: map[string]apiextensionsv1.JSONSchemaProps{
  1449  										"stringField":   *stringSchema,
  1450  										"intField":      *numberSchema,
  1451  										"otherIntField": *numberSchema,
  1452  									},
  1453  								},
  1454  							},
  1455  						},
  1456  					},
  1457  				}},
  1458  				applyPatchOperation{
  1459  					"create instance with strings that do not start with k8s",
  1460  					myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
  1461  						"field": map[string]interface{}{
  1462  							"object1": map[string]interface{}{
  1463  								"stringField": "a string",
  1464  								"intField":    5,
  1465  							},
  1466  							"object2": map[string]interface{}{
  1467  								"stringField": "another string",
  1468  								"intField":    15,
  1469  							},
  1470  							"object3": map[string]interface{}{
  1471  								"stringField": "a third string",
  1472  								"intField":    7,
  1473  							},
  1474  						},
  1475  					}},
  1476  				patchMyCRDV1Beta1Schema{
  1477  					"require that stringField value start with `k8s`",
  1478  					map[string]interface{}{
  1479  						"properties": map[string]interface{}{
  1480  							"field": map[string]interface{}{
  1481  								"additionalProperties": map[string]interface{}{
  1482  									"properties": map[string]interface{}{
  1483  										"stringField": map[string]interface{}{
  1484  											"x-kubernetes-validations": []interface{}{
  1485  												map[string]interface{}{
  1486  													"rule":    "self.startsWith('k8s')",
  1487  													"message": "strings must have k8s prefix",
  1488  												},
  1489  											},
  1490  										},
  1491  									},
  1492  								},
  1493  							},
  1494  						},
  1495  					}},
  1496  				applyPatchOperation{
  1497  					"add a new entry that follows the new rule, ratchet old values",
  1498  					myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
  1499  						"field": map[string]interface{}{
  1500  							"object1": map[string]interface{}{
  1501  								"stringField": "a string",
  1502  								"intField":    5,
  1503  							},
  1504  							"object2": map[string]interface{}{
  1505  								"stringField": "another string",
  1506  								"intField":    15,
  1507  							},
  1508  							"object3": map[string]interface{}{
  1509  								"stringField": "a third string",
  1510  								"intField":    7,
  1511  							},
  1512  							"object4": map[string]interface{}{
  1513  								"stringField": "k8s third string",
  1514  								"intField":    7,
  1515  							},
  1516  						},
  1517  					}},
  1518  				applyPatchOperation{
  1519  					"modify a sibling to an invalid value, ratcheting the unchanged invalid value",
  1520  					myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
  1521  						"field": map[string]interface{}{
  1522  							"object1": map[string]interface{}{
  1523  								"stringField": "a string",
  1524  								"intField":    15,
  1525  							},
  1526  							"object2": map[string]interface{}{
  1527  								"stringField":   "another string",
  1528  								"intField":      10,
  1529  								"otherIntField": 20,
  1530  							},
  1531  						},
  1532  					}},
  1533  				expectError{
  1534  					applyPatchOperation{
  1535  						"change a previously ratcheted field to an invalid value",
  1536  						myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
  1537  							"field": map[string]interface{}{
  1538  								"object2": map[string]interface{}{
  1539  									"stringField": "a changed string",
  1540  								},
  1541  								"object3": map[string]interface{}{
  1542  									"stringField": "a changed third string",
  1543  								},
  1544  							},
  1545  						}}},
  1546  				patchMyCRDV1Beta1Schema{
  1547  					"require that stringField values are also odd length",
  1548  					map[string]interface{}{
  1549  						"properties": map[string]interface{}{
  1550  							"field": map[string]interface{}{
  1551  								"additionalProperties": map[string]interface{}{
  1552  									"stringField": map[string]interface{}{
  1553  										"x-kubernetes-validations": []interface{}{
  1554  											map[string]interface{}{
  1555  												"rule":    "self.startsWith('k8s')",
  1556  												"message": "strings must have k8s prefix",
  1557  											},
  1558  											map[string]interface{}{
  1559  												"rule":    "len(self) % 2 == 1",
  1560  												"message": "strings must have odd length",
  1561  											},
  1562  										},
  1563  									},
  1564  								},
  1565  							},
  1566  						},
  1567  					}},
  1568  				applyPatchOperation{
  1569  					"have mixed ratcheting of one or two CEL rules, object4 is ratcheted by one rule, object1 is ratcheting 2 rules",
  1570  					myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
  1571  						"field": map[string]interface{}{
  1572  							"object1": map[string]interface{}{
  1573  								"stringField": "a string", // invalid. even number length, no k8s prefix
  1574  								"intField":    1000,
  1575  							},
  1576  							"object4": map[string]interface{}{
  1577  								"stringField": "k8s third string", // invalid. even number length. ratcheted
  1578  								"intField":    7000,
  1579  							},
  1580  						},
  1581  					}},
  1582  				expectError{
  1583  					applyPatchOperation{
  1584  						"swap keys between valuesin the map",
  1585  						myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
  1586  							"field": map[string]interface{}{
  1587  								"object1": map[string]interface{}{
  1588  									"stringField": "k8s third string",
  1589  									"intField":    1000,
  1590  								},
  1591  								"object4": map[string]interface{}{
  1592  									"stringField": "a string",
  1593  									"intField":    7000,
  1594  								},
  1595  							},
  1596  						}}},
  1597  				applyPatchOperation{
  1598  					"fix keys",
  1599  					myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
  1600  						"field": map[string]interface{}{
  1601  							"object1": map[string]interface{}{
  1602  								"stringField": "k8s a stringy",
  1603  								"intField":    1000,
  1604  							},
  1605  							"object4": map[string]interface{}{
  1606  								"stringField": "k8s third stringy",
  1607  								"intField":    7000,
  1608  							},
  1609  						},
  1610  					}},
  1611  			},
  1612  		},
  1613  		{
  1614  			// Changing a list to a set should allow you to keep the items the
  1615  			// same, but if you modify any one item the set must be uniqued
  1616  			//
  1617  			// Possibly a future area of improvement. As it stands now,
  1618  			// SSA implementation is incompatible with ratcheting this field:
  1619  			// https://github.com/kubernetes/kubernetes/blob/ec9a8ffb237e391ce9ccc58de93ba4ecc2fabf42/staging/src/k8s.io/apimachinery/pkg/util/managedfields/internal/structuredmerge.go#L146-L149
  1620  			//
  1621  			// Throws error trying to interpret an invalid existing `liveObj`
  1622  			// as a set.
  1623  			Name:     "Change list to set",
  1624  			Disabled: true,
  1625  			Operations: []ratchetingTestOperation{
  1626  				updateMyCRDV1Beta1Schema{&apiextensionsv1.JSONSchemaProps{
  1627  					Type: "object",
  1628  					Properties: map[string]apiextensionsv1.JSONSchemaProps{
  1629  						"values": {
  1630  							Type: "object",
  1631  							AdditionalProperties: &apiextensionsv1.JSONSchemaPropsOrBool{
  1632  								Schema: &apiextensionsv1.JSONSchemaProps{
  1633  									Type: "array",
  1634  									Items: &apiextensionsv1.JSONSchemaPropsOrArray{
  1635  										Schema: numberSchema,
  1636  									},
  1637  								},
  1638  							},
  1639  						},
  1640  					},
  1641  				}},
  1642  				applyPatchOperation{
  1643  					"reate a list of numbers with duplicates using the old simple schema",
  1644  					myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
  1645  						"values": map[string]interface{}{
  1646  							"dups": []interface{}{1, 2, 2, 3, 1000, 2000},
  1647  						},
  1648  					}},
  1649  				patchMyCRDV1Beta1Schema{
  1650  					"change list type to set",
  1651  					map[string]interface{}{
  1652  						"properties": map[string]interface{}{
  1653  							"values": map[string]interface{}{
  1654  								"additionalProperties": map[string]interface{}{
  1655  									"x-kubernetes-list-type": "set",
  1656  								},
  1657  							},
  1658  						},
  1659  					}},
  1660  				expectError{
  1661  					applyPatchOperation{
  1662  						"change original without removing duplicates",
  1663  						myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
  1664  							"values": map[string]interface{}{
  1665  								"dups": []interface{}{1, 2, 2, 3, 1000, 2000, 3},
  1666  							},
  1667  						}}},
  1668  				expectError{applyPatchOperation{
  1669  					"add another list with duplicates",
  1670  					myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
  1671  						"values": map[string]interface{}{
  1672  							"dups":  []interface{}{1, 2, 2, 3, 1000, 2000},
  1673  							"dups2": []interface{}{1, 2, 2, 3, 1000, 2000},
  1674  						},
  1675  					}}},
  1676  				// Can add a valid sibling field
  1677  				//! Remove this ExpectError if/when we add support for ratcheting
  1678  				// the type of a list
  1679  				applyPatchOperation{
  1680  					"add a valid sibling field",
  1681  					myCRDV1Beta1, myCRDInstanceName, map[string]interface{}{
  1682  						"values": map[string]interface{}{
  1683  							"dups":       []interface{}{1, 2, 2, 3, 1000, 2000},
  1684  							"otherField": []interface{}{1, 2, 3},
  1685  						},
  1686  					}},
  1687  				// Can remove dups to make valid
  1688  				//! Normally this woud be valid, but SSA is unable to interpret
  1689  				// the `liveObj` in the new schema, so fails. Changing
  1690  				// x-kubernetes-list-type from anything to a set is unsupported by SSA.
  1691  				applyPatchOperation{
  1692  					"remove dups to make list valid",
  1693  					myCRDV1Beta1,
  1694  					myCRDInstanceName,
  1695  					map[string]interface{}{
  1696  						"values": map[string]interface{}{
  1697  							"dups":       []interface{}{1, 3, 1000, 2000},
  1698  							"otherField": []interface{}{1, 2, 3},
  1699  						},
  1700  					}},
  1701  			},
  1702  		},
  1703  	}
  1704  
  1705  	runTests(t, cases)
  1706  }
  1707  
  1708  func ptr[T any](v T) *T {
  1709  	return &v
  1710  }
  1711  
  1712  type validator func(new, old *unstructured.Unstructured)
  1713  
  1714  func newValidator(customResourceValidation *apiextensionsinternal.JSONSchemaProps, kind schema.GroupVersionKind, namespaceScoped bool) (validator, error) {
  1715  	// Replicate customResourceStrategy validation
  1716  	openapiSchema := &spec.Schema{}
  1717  	if customResourceValidation != nil {
  1718  		// TODO: replace with NewStructural(...).ToGoOpenAPI
  1719  		if err := apiservervalidation.ConvertJSONSchemaPropsWithPostProcess(customResourceValidation, openapiSchema, apiservervalidation.StripUnsupportedFormatsPostProcess); err != nil {
  1720  			return nil, err
  1721  		}
  1722  	}
  1723  
  1724  	schemaValidator := apiservervalidation.NewRatchetingSchemaValidator(
  1725  		openapiSchema,
  1726  		nil,
  1727  		"",
  1728  		strfmt.Default)
  1729  	sts, err := structuralschema.NewStructural(customResourceValidation)
  1730  	if err != nil {
  1731  		return nil, err
  1732  	}
  1733  
  1734  	strategy := customresource.NewStrategy(
  1735  		nil, // No need for typer, since only using validation
  1736  		namespaceScoped,
  1737  		kind,
  1738  		schemaValidator,
  1739  		nil, // No status schema validator
  1740  		sts,
  1741  		nil, // No need for status
  1742  		nil, // No need for scale
  1743  		nil, // No need for selectable fields
  1744  	)
  1745  
  1746  	return func(new, old *unstructured.Unstructured) {
  1747  		_ = strategy.ValidateUpdate(context.TODO(), new, old)
  1748  	}, nil
  1749  }
  1750  
  1751  // Recursively walks the provided directory and parses the YAML files into
  1752  // unstructured objects. If there are more than one object in a single file,
  1753  // they are all added to the returned slice.
  1754  func loadObjects(dir string) []*unstructured.Unstructured {
  1755  	result := []*unstructured.Unstructured{}
  1756  	err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
  1757  		if err != nil {
  1758  			return err
  1759  		} else if d.IsDir() {
  1760  			return nil
  1761  		} else if filepath.Ext(d.Name()) != ".yaml" {
  1762  			return nil
  1763  		}
  1764  		// Read the file in as []byte
  1765  		data, err := os.ReadFile(path)
  1766  		if err != nil {
  1767  			return err
  1768  		}
  1769  
  1770  		decoder := utilyaml.NewYAMLOrJSONDecoder(bytes.NewReader(data), 4096)
  1771  
  1772  		// Split the data by YAML drame
  1773  		for {
  1774  			parsed := &unstructured.Unstructured{}
  1775  			if err := decoder.Decode(parsed); err != nil {
  1776  				if errors.Is(err, io.EOF) {
  1777  					break
  1778  				}
  1779  				return err
  1780  			}
  1781  
  1782  			result = append(result, parsed)
  1783  		}
  1784  
  1785  		return nil
  1786  	})
  1787  	if err != nil {
  1788  		panic(err)
  1789  	}
  1790  	return result
  1791  }
  1792  
  1793  func BenchmarkRatcheting(b *testing.B) {
  1794  	// Walk directory with CRDs, for each file parse YAML with multiple CRDs in it.
  1795  	// Keep track in a map a validator for each unique gvk
  1796  	crdObjects := loadObjects("ratcheting_test_cases/crds")
  1797  	invalidFiles := loadObjects("ratcheting_test_cases/invalid")
  1798  	validFiles := loadObjects("ratcheting_test_cases/valid")
  1799  
  1800  	// Create a validator for each GVK.
  1801  	validators := map[schema.GroupVersionKind]validator{}
  1802  	for _, crd := range crdObjects {
  1803  		parsed := apiextensionsv1.CustomResourceDefinition{}
  1804  		if err := runtime.DefaultUnstructuredConverter.FromUnstructured(crd.Object, &parsed); err != nil {
  1805  			b.Fatalf("Failed to parse CRD %v", err)
  1806  			return
  1807  		}
  1808  
  1809  		for _, v := range parsed.Spec.Versions {
  1810  			gvk := schema.GroupVersionKind{
  1811  				Group:   parsed.Spec.Group,
  1812  				Version: v.Name,
  1813  				Kind:    parsed.Spec.Names.Kind,
  1814  			}
  1815  
  1816  			// Create structural schema from v.Schema.OpenAPIV3Schema
  1817  			internalValidation := &apiextensionsinternal.CustomResourceValidation{}
  1818  			if err := apiextensionsv1.Convert_v1_CustomResourceValidation_To_apiextensions_CustomResourceValidation(v.Schema, internalValidation, nil); err != nil {
  1819  				b.Fatal(fmt.Errorf("failed converting CRD validation to internal version: %v", err))
  1820  				return
  1821  			}
  1822  
  1823  			validator, err := newValidator(internalValidation.OpenAPIV3Schema, gvk, parsed.Spec.Scope == apiextensionsv1.NamespaceScoped)
  1824  			if err != nil {
  1825  				b.Fatal(err)
  1826  				return
  1827  			}
  1828  			validators[gvk] = validator
  1829  		}
  1830  
  1831  	}
  1832  
  1833  	// Organize all the files by GVK.
  1834  	gvksToValidFiles := map[schema.GroupVersionKind][]*unstructured.Unstructured{}
  1835  	gvksToInvalidFiles := map[schema.GroupVersionKind][]*unstructured.Unstructured{}
  1836  
  1837  	for _, valid := range validFiles {
  1838  		gvk := valid.GroupVersionKind()
  1839  		gvksToValidFiles[gvk] = append(gvksToValidFiles[gvk], valid)
  1840  	}
  1841  
  1842  	for _, invalid := range invalidFiles {
  1843  		gvk := invalid.GroupVersionKind()
  1844  		gvksToInvalidFiles[gvk] = append(gvksToInvalidFiles[gvk], invalid)
  1845  	}
  1846  
  1847  	// Remove any GVKs for which we dont have both valid and invalid files.
  1848  	for gvk := range gvksToValidFiles {
  1849  		if _, ok := gvksToInvalidFiles[gvk]; !ok {
  1850  			delete(gvksToValidFiles, gvk)
  1851  		}
  1852  	}
  1853  
  1854  	for gvk := range gvksToInvalidFiles {
  1855  		if _, ok := gvksToValidFiles[gvk]; !ok {
  1856  			delete(gvksToInvalidFiles, gvk)
  1857  		}
  1858  	}
  1859  
  1860  	type pair struct {
  1861  		old *unstructured.Unstructured
  1862  		new *unstructured.Unstructured
  1863  	}
  1864  
  1865  	// For each valid file, match it with every invalid file of the same GVK
  1866  	validXValidPairs := []pair{}
  1867  	validXInvalidPairs := []pair{}
  1868  	invalidXInvalidPairs := []pair{}
  1869  
  1870  	for gvk, valids := range gvksToValidFiles {
  1871  		for _, validOld := range valids {
  1872  			for _, validNew := range gvksToValidFiles[gvk] {
  1873  				validXValidPairs = append(validXValidPairs, pair{old: validOld, new: validNew})
  1874  			}
  1875  		}
  1876  	}
  1877  
  1878  	for gvk, valids := range gvksToValidFiles {
  1879  		for _, valid := range valids {
  1880  			for _, invalid := range gvksToInvalidFiles[gvk] {
  1881  				validXInvalidPairs = append(validXInvalidPairs, pair{old: valid, new: invalid})
  1882  			}
  1883  		}
  1884  	}
  1885  
  1886  	// For each invalid file, add pair with every other invalid file of the same
  1887  	// GVK including itself
  1888  	for gvk, invalids := range gvksToInvalidFiles {
  1889  		for _, invalid := range invalids {
  1890  			for _, invalid2 := range gvksToInvalidFiles[gvk] {
  1891  				invalidXInvalidPairs = append(invalidXInvalidPairs, pair{old: invalid, new: invalid2})
  1892  			}
  1893  		}
  1894  	}
  1895  
  1896  	// For each pair, run the ratcheting algorithm on the update.
  1897  	//
  1898  	for _, ratchetingEnabled := range []bool{true, false} {
  1899  		name := "RatchetingEnabled"
  1900  		if !ratchetingEnabled {
  1901  			name = "RatchetingDisabled"
  1902  		}
  1903  		b.Run(name, func(b *testing.B) {
  1904  			defer featuregatetesting.SetFeatureGateDuringTest(b, utilfeature.DefaultFeatureGate, features.CRDValidationRatcheting, ratchetingEnabled)()
  1905  			b.ResetTimer()
  1906  
  1907  			do := func(pairs []pair) {
  1908  				for _, pair := range pairs {
  1909  					// Create a validator for the GVK of the valid object.
  1910  					validator, ok := validators[pair.old.GroupVersionKind()]
  1911  					if !ok {
  1912  						b.Log("No validator for GVK", pair.old.GroupVersionKind())
  1913  						continue
  1914  					}
  1915  
  1916  					// Run the ratcheting algorithm on the update.
  1917  					// Don't care about result for benchmark
  1918  					validator(pair.old, pair.new)
  1919  				}
  1920  			}
  1921  
  1922  			b.Run("ValidXValid", func(b *testing.B) {
  1923  				for i := 0; i < b.N; i++ {
  1924  					do(validXValidPairs)
  1925  				}
  1926  			})
  1927  
  1928  			b.Run("ValidXInvalid", func(b *testing.B) {
  1929  				for i := 0; i < b.N; i++ {
  1930  					do(validXInvalidPairs)
  1931  				}
  1932  			})
  1933  
  1934  			b.Run("InvalidXInvalid", func(b *testing.B) {
  1935  				for i := 0; i < b.N; i++ {
  1936  					do(invalidXInvalidPairs)
  1937  				}
  1938  			})
  1939  		})
  1940  	}
  1941  }
  1942  
  1943  func TestRatchetingDropFields(t *testing.T) {
  1944  	// Field dropping only takes effect when feature is disabled
  1945  	defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.CRDValidationRatcheting, false)()
  1946  	tearDown, apiExtensionClient, _, err := fixtures.StartDefaultServerWithClients(t)
  1947  	if err != nil {
  1948  		t.Fatal(err)
  1949  	}
  1950  	defer tearDown()
  1951  
  1952  	group := myCRDV1Beta1.Group
  1953  	version := myCRDV1Beta1.Version
  1954  	resource := myCRDV1Beta1.Resource
  1955  	kind := fakeRESTMapper[myCRDV1Beta1]
  1956  
  1957  	myCRD := &apiextensionsv1.CustomResourceDefinition{
  1958  		ObjectMeta: metav1.ObjectMeta{Name: resource + "." + group},
  1959  		Spec: apiextensionsv1.CustomResourceDefinitionSpec{
  1960  			Group: group,
  1961  			Versions: []apiextensionsv1.CustomResourceDefinitionVersion{{
  1962  				Name:    version,
  1963  				Served:  true,
  1964  				Storage: true,
  1965  				Schema: &apiextensionsv1.CustomResourceValidation{
  1966  					OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{
  1967  						Type: "object",
  1968  						Properties: map[string]apiextensionsv1.JSONSchemaProps{
  1969  							"spec": {
  1970  								Type: "object",
  1971  								Properties: map[string]apiextensionsv1.JSONSchemaProps{
  1972  									"field": {
  1973  										Type: "string",
  1974  										XValidations: []apiextensionsv1.ValidationRule{
  1975  											{
  1976  												// Results in error if field wasn't dropped
  1977  												Rule:            "self == oldSelf",
  1978  												OptionalOldSelf: ptr(true),
  1979  											},
  1980  										},
  1981  									},
  1982  								},
  1983  							},
  1984  						},
  1985  					},
  1986  				},
  1987  			}},
  1988  			Names: apiextensionsv1.CustomResourceDefinitionNames{
  1989  				Plural:   resource,
  1990  				Kind:     kind,
  1991  				ListKind: kind + "List",
  1992  			},
  1993  			Scope: apiextensionsv1.NamespaceScoped,
  1994  		},
  1995  	}
  1996  
  1997  	created, err := apiExtensionClient.ApiextensionsV1().CustomResourceDefinitions().Create(context.TODO(), myCRD, metav1.CreateOptions{})
  1998  	if err != nil {
  1999  		t.Fatal(err)
  2000  	}
  2001  	if created.Spec.Versions[0].Schema.OpenAPIV3Schema.Properties["spec"].Properties["field"].XValidations[0].OptionalOldSelf != nil {
  2002  		t.Errorf("Expected OpeiontalOldSelf field to be dropped for create when feature gate is disabled")
  2003  	}
  2004  
  2005  	var updated *apiextensionsv1.CustomResourceDefinition
  2006  	err = wait.PollUntilContextTimeout(context.TODO(), 100*time.Millisecond, 5*time.Second, true, func(ctx context.Context) (bool, error) {
  2007  		existing, err := apiExtensionClient.ApiextensionsV1().CustomResourceDefinitions().Get(context.TODO(), created.Name, metav1.GetOptions{})
  2008  		if err != nil {
  2009  			return false, err
  2010  		}
  2011  		existing.Spec.Versions[0].Schema.OpenAPIV3Schema.Properties["spec"].Properties["field"].XValidations[0].OptionalOldSelf = ptr(true)
  2012  		updated, err = apiExtensionClient.ApiextensionsV1().CustomResourceDefinitions().Update(context.TODO(), existing, metav1.UpdateOptions{})
  2013  		if err != nil {
  2014  			if apierrors.IsConflict(err) {
  2015  				return false, nil
  2016  			}
  2017  			return false, err
  2018  		}
  2019  		return true, nil
  2020  	})
  2021  	if err != nil {
  2022  		t.Fatalf("unexpected error waiting for CRD update: %v", err)
  2023  	}
  2024  
  2025  	if updated.Spec.Versions[0].Schema.OpenAPIV3Schema.Properties["spec"].Properties["field"].XValidations[0].OptionalOldSelf != nil {
  2026  		t.Errorf("Expected OpeiontalOldSelf field to be dropped for update when feature gate is disabled")
  2027  	}
  2028  }
  2029  

View as plain text