...

Source file src/k8s.io/kubernetes/test/integration/apiserver/crd_validation_expressions_test.go

Documentation: k8s.io/kubernetes/test/integration/apiserver

     1  /*
     2  Copyright 2021 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package apiserver
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"strings"
    23  	"testing"
    24  
    25  	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    26  	apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
    27  	"k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
    28  	"k8s.io/apiextensions-apiserver/test/integration/fixtures"
    29  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    30  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    31  	"k8s.io/apimachinery/pkg/runtime/schema"
    32  	"k8s.io/apimachinery/pkg/util/json"
    33  	genericfeatures "k8s.io/apiserver/pkg/features"
    34  	"k8s.io/apiserver/pkg/storage/names"
    35  	utilfeature "k8s.io/apiserver/pkg/util/feature"
    36  	"k8s.io/client-go/dynamic"
    37  	featuregatetesting "k8s.io/component-base/featuregate/testing"
    38  
    39  	apiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
    40  	"k8s.io/kubernetes/test/integration/framework"
    41  )
    42  
    43  // TestCustomResourceValidators tests x-kubernetes-validations compile and validate as expected when the feature gate
    44  // is enabled.
    45  func TestCustomResourceValidators(t *testing.T) {
    46  	defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.CustomResourceValidationExpressions, true)()
    47  
    48  	server, err := apiservertesting.StartTestServer(t, apiservertesting.NewDefaultTestServerOptions(), nil, framework.SharedEtcd())
    49  	if err != nil {
    50  		t.Fatal(err)
    51  	}
    52  	defer server.TearDownFn()
    53  	config := server.ClientConfig
    54  
    55  	apiExtensionClient, err := clientset.NewForConfig(config)
    56  	if err != nil {
    57  		t.Fatal(err)
    58  	}
    59  	dynamicClient, err := dynamic.NewForConfig(config)
    60  	if err != nil {
    61  		t.Fatal(err)
    62  	}
    63  
    64  	t.Run("Structural schema", func(t *testing.T) {
    65  		structuralWithValidators := crdWithSchema(t, "Structural", structuralSchemaWithValidators)
    66  		crd, err := fixtures.CreateNewV1CustomResourceDefinition(structuralWithValidators, apiExtensionClient, dynamicClient)
    67  		if err != nil {
    68  			t.Fatal(err)
    69  		}
    70  		gvr := schema.GroupVersionResource{
    71  			Group:    crd.Spec.Group,
    72  			Version:  crd.Spec.Versions[0].Name,
    73  			Resource: crd.Spec.Names.Plural,
    74  		}
    75  		crClient := dynamicClient.Resource(gvr)
    76  
    77  		t.Run("CRD creation MUST allow data that is valid according to x-kubernetes-validations", func(t *testing.T) {
    78  			name1 := names.SimpleNameGenerator.GenerateName("cr-1")
    79  			_, err = crClient.Create(context.TODO(), &unstructured.Unstructured{Object: map[string]interface{}{
    80  				"apiVersion": gvr.Group + "/" + gvr.Version,
    81  				"kind":       crd.Spec.Names.Kind,
    82  				"metadata": map[string]interface{}{
    83  					"name": name1,
    84  				},
    85  				"spec": map[string]interface{}{
    86  					"x":     int64(2),
    87  					"y":     int64(2),
    88  					"limit": int64(123),
    89  				},
    90  			}}, metav1.CreateOptions{})
    91  			if err != nil {
    92  				t.Errorf("Failed to create custom resource: %v", err)
    93  			}
    94  		})
    95  		t.Run("custom resource create and update MUST NOT allow data that is invalid according to x-kubernetes-validations if the feature gate is enabled", func(t *testing.T) {
    96  			name1 := names.SimpleNameGenerator.GenerateName("cr-1")
    97  
    98  			// a spec create that is invalid MUST fail validation
    99  			cr := &unstructured.Unstructured{Object: map[string]interface{}{
   100  				"apiVersion": gvr.Group + "/" + gvr.Version,
   101  				"kind":       crd.Spec.Names.Kind,
   102  				"metadata": map[string]interface{}{
   103  					"name": name1,
   104  				},
   105  				"spec": map[string]interface{}{
   106  					"x": int64(-1),
   107  					"y": int64(0),
   108  				},
   109  			}}
   110  
   111  			// a spec create that is invalid MUST fail validation
   112  			_, err = crClient.Create(context.TODO(), cr, metav1.CreateOptions{})
   113  			if err == nil {
   114  				t.Fatal("Expected create of invalid custom resource to fail")
   115  			} else {
   116  				if !strings.Contains(err.Error(), "failed rule: self.spec.x + self.spec.y") {
   117  					t.Fatalf("Expected error to contain %s but got %v", "failed rule: self.spec.x + self.spec.y", err.Error())
   118  				}
   119  			}
   120  
   121  			// a spec create that is valid MUST pass validation
   122  			cr.Object["spec"] = map[string]interface{}{
   123  				"x":     int64(2),
   124  				"y":     int64(2),
   125  				"extra": "anything?",
   126  				"floatMap": map[string]interface{}{
   127  					"key1": 0.2,
   128  					"key2": 0.3,
   129  				},
   130  				"assocList": []interface{}{
   131  					map[string]interface{}{
   132  						"k": "a",
   133  						"v": "1",
   134  					},
   135  				},
   136  				"limit": nil,
   137  			}
   138  
   139  			cr, err := crClient.Create(context.TODO(), cr, metav1.CreateOptions{})
   140  			if err != nil {
   141  				t.Fatalf("Unexpected error creating custom resource: %v", err)
   142  			}
   143  
   144  			// spec updates that are invalid MUST fail validation
   145  			cases := []struct {
   146  				name string
   147  				spec map[string]interface{}
   148  			}{
   149  				{
   150  					name: "spec vs. status default value",
   151  					spec: map[string]interface{}{
   152  						"x": 3,
   153  						"y": -4,
   154  					},
   155  				},
   156  				{
   157  					name: "nested string field",
   158  					spec: map[string]interface{}{
   159  						"extra": "something",
   160  					},
   161  				},
   162  				{
   163  					name: "nested array",
   164  					spec: map[string]interface{}{
   165  						"floatMap": map[string]interface{}{
   166  							"key1": 0.1,
   167  							"key2": 0.2,
   168  						},
   169  					},
   170  				},
   171  				{
   172  					name: "nested associative list",
   173  					spec: map[string]interface{}{
   174  						"assocList": []interface{}{
   175  							map[string]interface{}{
   176  								"k": "a",
   177  								"v": "2",
   178  							},
   179  						},
   180  					},
   181  				},
   182  			}
   183  			for _, tc := range cases {
   184  				t.Run(tc.name, func(t *testing.T) {
   185  					cr.Object["spec"] = tc.spec
   186  
   187  					_, err = crClient.Update(context.TODO(), cr, metav1.UpdateOptions{})
   188  					if err == nil {
   189  						t.Fatal("Expected invalid update of custom resource to fail")
   190  					} else {
   191  						if !strings.Contains(err.Error(), "failed rule") {
   192  							t.Fatalf("Expected error to contain %s but got %v", "failed rule", err.Error())
   193  						}
   194  					}
   195  				})
   196  			}
   197  
   198  			// a status update that is invalid MUST fail validation
   199  			cr.Object["status"] = map[string]interface{}{
   200  				"z": int64(5),
   201  			}
   202  			_, err = crClient.UpdateStatus(context.TODO(), cr, metav1.UpdateOptions{})
   203  			if err == nil {
   204  				t.Fatal("Expected invalid update of custom resource status to fail")
   205  			} else {
   206  				if !strings.Contains(err.Error(), "failed rule: self.spec.x + self.spec.y") {
   207  					t.Fatalf("Expected error to contain %s but got %v", "failed rule: self.spec.x + self.spec.y", err.Error())
   208  				}
   209  			}
   210  
   211  			// a status update this is valid MUST pass validation
   212  			cr.Object["status"] = map[string]interface{}{
   213  				"z": int64(3),
   214  			}
   215  
   216  			_, err = crClient.UpdateStatus(context.TODO(), cr, metav1.UpdateOptions{})
   217  			if err != nil {
   218  				t.Fatalf("Unexpected error updating custom resource status: %v", err)
   219  			}
   220  		})
   221  	})
   222  	t.Run("CRD writes MUST fail for a non-structural schema containing x-kubernetes-validations", func(t *testing.T) {
   223  		// The only way for a non-structural schema to exist is for it to already be persisted in etcd as a non-structural CRD.
   224  		nonStructuralCRD, err := fixtures.CreateCRDUsingRemovedAPI(server.EtcdClient, server.EtcdStoragePrefix, nonStructuralCrdWithValidations(), apiExtensionClient, dynamicClient)
   225  		if err != nil {
   226  			t.Fatalf("Unexpected error non-structural CRD by writing directly to etcd: %v", err)
   227  		}
   228  		// Double check that the schema is non-structural
   229  		crd, err := apiExtensionClient.ApiextensionsV1().CustomResourceDefinitions().Get(context.TODO(), nonStructuralCRD.Name, metav1.GetOptions{})
   230  		if err != nil {
   231  			t.Fatalf("Unexpected error: %v", err)
   232  		}
   233  		nonStructural := false
   234  		for _, c := range crd.Status.Conditions {
   235  			if c.Type == apiextensionsv1.NonStructuralSchema {
   236  				nonStructural = true
   237  			}
   238  		}
   239  		if !nonStructural {
   240  			t.Fatal("Expected CRD to be non-structural")
   241  		}
   242  
   243  		//Try to change it
   244  		crd.Spec.Versions[0].Schema.OpenAPIV3Schema.XValidations = apiextensionsv1.ValidationRules{
   245  			{
   246  				Rule: "has(self.foo)",
   247  			},
   248  		}
   249  		_, err = apiExtensionClient.ApiextensionsV1().CustomResourceDefinitions().Update(context.TODO(), crd, metav1.UpdateOptions{})
   250  		if err == nil {
   251  			t.Fatal("Expected error")
   252  		}
   253  	})
   254  	t.Run("CRD creation MUST fail if a x-kubernetes-validations rule accesses a metadata field other than name", func(t *testing.T) {
   255  		structuralWithValidators := crdWithSchema(t, "InvalidStructuralMetadata", structuralSchemaWithInvalidMetadataValidators)
   256  		_, err := fixtures.CreateNewV1CustomResourceDefinition(structuralWithValidators, apiExtensionClient, dynamicClient)
   257  		if err == nil {
   258  			t.Error("Expected error creating custom resource but got none")
   259  		} else if !strings.Contains(err.Error(), "undefined field 'labels'") {
   260  			t.Errorf("Expected error to contain %s but got %v", "undefined field 'labels'", err.Error())
   261  		}
   262  	})
   263  	t.Run("CRD creation MUST pass if a x-kubernetes-validations rule accesses metadata.name", func(t *testing.T) {
   264  		structuralWithValidators := crdWithSchema(t, "ValidStructuralMetadata", structuralSchemaWithValidMetadataValidators)
   265  		_, err := fixtures.CreateNewV1CustomResourceDefinition(structuralWithValidators, apiExtensionClient, dynamicClient)
   266  		if err != nil {
   267  			t.Error("Unexpected error creating custom resource but metadata validation rule")
   268  		}
   269  	})
   270  	t.Run("CRD creation MUST pass for an CRD with empty field", func(t *testing.T) {
   271  		structuralWithValidators := crdWithSchema(t, "WithEmptyObject", structuralSchemaWithEmptyObject)
   272  		_, err := fixtures.CreateNewV1CustomResourceDefinition(structuralWithValidators, apiExtensionClient, dynamicClient)
   273  		if err != nil {
   274  			t.Errorf("unexpected error creating CRD with empty field: %v", err)
   275  		}
   276  	})
   277  	t.Run("CR creation MUST fail if a x-kubernetes-validations rule exceeds the runtime cost limit", func(t *testing.T) {
   278  		structuralWithValidators := crdWithSchema(t, "RuntimeCostLimit", structuralSchemaWithCostLimit)
   279  		crd, err := fixtures.CreateNewV1CustomResourceDefinition(structuralWithValidators, apiExtensionClient, dynamicClient)
   280  		if err != nil {
   281  			t.Errorf("Unexpected error creating custom resource definition: %v", err)
   282  		}
   283  		gvr := schema.GroupVersionResource{
   284  			Group:    crd.Spec.Group,
   285  			Version:  crd.Spec.Versions[0].Name,
   286  			Resource: crd.Spec.Names.Plural,
   287  		}
   288  		crClient := dynamicClient.Resource(gvr)
   289  		name1 := names.SimpleNameGenerator.GenerateName("cr-1")
   290  		cr := &unstructured.Unstructured{Object: map[string]interface{}{
   291  			"apiVersion": gvr.Group + "/" + gvr.Version,
   292  			"kind":       crd.Spec.Names.Kind,
   293  			"metadata": map[string]interface{}{
   294  				"name": name1,
   295  			},
   296  			"spec": map[string]interface{}{
   297  				"list": genLargeArray(725, 20),
   298  			},
   299  		}}
   300  		_, err = crClient.Create(context.TODO(), cr, metav1.CreateOptions{})
   301  		if err == nil {
   302  			t.Fatal("Expected error creating custom resource")
   303  		} else if !strings.Contains(err.Error(), "call cost exceeds limit") {
   304  			t.Errorf("Expected error to contain %s but got %v", "call cost exceeds limit", err.Error())
   305  		}
   306  	})
   307  	t.Run("Schema with valid transition rule", func(t *testing.T) {
   308  		structuralWithValidators := crdWithSchema(t, "ValidTransitionRule", structuralSchemaWithValidTransitionRule)
   309  		crd, err := fixtures.CreateNewV1CustomResourceDefinition(structuralWithValidators, apiExtensionClient, dynamicClient)
   310  		if err != nil {
   311  			t.Fatal(err)
   312  		}
   313  		gvr := schema.GroupVersionResource{
   314  			Group:    crd.Spec.Group,
   315  			Version:  crd.Spec.Versions[0].Name,
   316  			Resource: crd.Spec.Names.Plural,
   317  		}
   318  		crClient := dynamicClient.Resource(gvr)
   319  
   320  		t.Run("custom resource update MUST pass if a x-kubernetes-validations rule contains a valid transition rule", func(t *testing.T) {
   321  			name1 := names.SimpleNameGenerator.GenerateName("cr-1")
   322  			cr := &unstructured.Unstructured{Object: map[string]interface{}{
   323  				"apiVersion": gvr.Group + "/" + gvr.Version,
   324  				"kind":       crd.Spec.Names.Kind,
   325  				"metadata": map[string]interface{}{
   326  					"name": name1,
   327  				},
   328  				"spec": map[string]interface{}{
   329  					"someImmutableThing": "original",
   330  					"somethingElse":      "original",
   331  				},
   332  			}}
   333  			cr, err = crClient.Create(context.TODO(), cr, metav1.CreateOptions{})
   334  			if err != nil {
   335  				t.Fatalf("Unexpected error creating custom resource: %v", err)
   336  			}
   337  			cr.Object["spec"].(map[string]interface{})["somethingElse"] = "new value"
   338  			_, err = crClient.Update(context.TODO(), cr, metav1.UpdateOptions{})
   339  			if err != nil {
   340  				t.Fatalf("Unexpected error updating custom resource: %v", err)
   341  			}
   342  		})
   343  		t.Run("custom resource update MUST fail if a x-kubernetes-validations rule contains an invalid transition rule", func(t *testing.T) {
   344  			name1 := names.SimpleNameGenerator.GenerateName("cr-1")
   345  			cr := &unstructured.Unstructured{Object: map[string]interface{}{
   346  				"apiVersion": gvr.Group + "/" + gvr.Version,
   347  				"kind":       crd.Spec.Names.Kind,
   348  				"metadata": map[string]interface{}{
   349  					"name": name1,
   350  				},
   351  				"spec": map[string]interface{}{
   352  					"someImmutableThing": "original",
   353  					"somethingElse":      "original",
   354  				},
   355  			}}
   356  			cr, err = crClient.Create(context.TODO(), cr, metav1.CreateOptions{})
   357  			if err != nil {
   358  				t.Fatalf("Unexpected error creating custom resource: %v", err)
   359  			}
   360  			cr.Object["spec"].(map[string]interface{})["someImmutableThing"] = "new value"
   361  			_, err = crClient.Update(context.TODO(), cr, metav1.UpdateOptions{})
   362  			if err == nil {
   363  				t.Fatalf("Expected error updating custom resource: %v", err)
   364  			} else if !strings.Contains(err.Error(), "failed rule: self.someImmutableThing == oldSelf.someImmutableThing") {
   365  				t.Errorf("Expected error to contain %s but got %v", "failed rule: self.someImmutableThing == oldSelf.someImmutableThing", err.Error())
   366  			}
   367  		})
   368  	})
   369  
   370  	t.Run("CRD creation MUST fail if a x-kubernetes-validations rule contains invalid transition rule", func(t *testing.T) {
   371  		structuralWithValidators := crdWithSchema(t, "InvalidTransitionRule", structuralSchemaWithInvalidTransitionRule)
   372  		_, err := fixtures.CreateNewV1CustomResourceDefinition(structuralWithValidators, apiExtensionClient, dynamicClient)
   373  		if err == nil {
   374  			t.Error("Expected error creating custom resource but got none")
   375  		} else if !strings.Contains(err.Error(), "oldSelf cannot be used on the uncorrelatable portion of the schema") {
   376  			t.Errorf("Expected error to contain %s but got %v", "oldSelf cannot be used on the uncorrelatable portion of the schema", err.Error())
   377  		}
   378  	})
   379  	t.Run("Schema with default map key transition rule", func(t *testing.T) {
   380  		structuralWithValidators := crdWithSchema(t, "DefaultMapKeyTransitionRule", structuralSchemaWithDefaultMapKeyTransitionRule)
   381  		crd, err := fixtures.CreateNewV1CustomResourceDefinition(structuralWithValidators, apiExtensionClient, dynamicClient)
   382  		if err != nil {
   383  			t.Fatal(err)
   384  		}
   385  		gvr := schema.GroupVersionResource{
   386  			Group:    crd.Spec.Group,
   387  			Version:  crd.Spec.Versions[0].Name,
   388  			Resource: crd.Spec.Names.Plural,
   389  		}
   390  		crClient := dynamicClient.Resource(gvr)
   391  
   392  		t.Run("custom resource update MUST fail if a x-kubernetes-validations if a transition rule contained in a mapList with default map keys fails validation", func(t *testing.T) {
   393  			name1 := names.SimpleNameGenerator.GenerateName("cr-1")
   394  			cr := &unstructured.Unstructured{Object: map[string]interface{}{
   395  				"apiVersion": gvr.Group + "/" + gvr.Version,
   396  				"kind":       crd.Spec.Names.Kind,
   397  				"metadata": map[string]interface{}{
   398  					"name": name1,
   399  				},
   400  				"spec": map[string]interface{}{
   401  					"list": []interface{}{
   402  						map[string]interface{}{
   403  							"k1": "x",
   404  							"v":  "value",
   405  						},
   406  					},
   407  				},
   408  			}}
   409  			cr, err = crClient.Create(context.TODO(), cr, metav1.CreateOptions{})
   410  			if err != nil {
   411  				t.Fatalf("Unexpected error creating custom resource: %v", err)
   412  			}
   413  			item := cr.Object["spec"].(map[string]interface{})["list"].([]interface{})[0].(map[string]interface{})
   414  			item["k2"] = "DEFAULT"
   415  			item["v"] = "new value"
   416  			_, err = crClient.Update(context.TODO(), cr, metav1.UpdateOptions{})
   417  			if err == nil {
   418  				t.Fatalf("Expected error updating custom resource: %v", err)
   419  			} else if !strings.Contains(err.Error(), "failed rule: self.v == oldSelf.v") {
   420  				t.Errorf("Expected error to contain %s but got %v", "failed rule: self.v == oldSelf.v", err.Error())
   421  			}
   422  		})
   423  	})
   424  }
   425  
   426  // TestCustomResourceValidatorsWithBlockingErrors tests x-kubernetes-validations is skipped when
   427  // blocking errors occurred.
   428  func TestCustomResourceValidatorsWithBlockingErrors(t *testing.T) {
   429  	defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.CustomResourceValidationExpressions, true)()
   430  
   431  	server, err := apiservertesting.StartTestServer(t, apiservertesting.NewDefaultTestServerOptions(), nil, framework.SharedEtcd())
   432  	if err != nil {
   433  		t.Fatal(err)
   434  	}
   435  	defer server.TearDownFn()
   436  	config := server.ClientConfig
   437  
   438  	apiExtensionClient, err := clientset.NewForConfig(config)
   439  	if err != nil {
   440  		t.Fatal(err)
   441  	}
   442  	dynamicClient, err := dynamic.NewForConfig(config)
   443  	if err != nil {
   444  		t.Fatal(err)
   445  	}
   446  
   447  	t.Run("Structural schema", func(t *testing.T) {
   448  		structuralWithValidators := crdWithSchema(t, "Structural", structuralSchemaWithBlockingErr)
   449  		crd, err := fixtures.CreateNewV1CustomResourceDefinition(structuralWithValidators, apiExtensionClient, dynamicClient)
   450  		if err != nil {
   451  			t.Fatal(err)
   452  		}
   453  		gvr := schema.GroupVersionResource{
   454  			Group:    crd.Spec.Group,
   455  			Version:  crd.Spec.Versions[0].Name,
   456  			Resource: crd.Spec.Names.Plural,
   457  		}
   458  		crClient := dynamicClient.Resource(gvr)
   459  
   460  		t.Run("CRD creation MUST allow data that is valid according to x-kubernetes-validations", func(t *testing.T) {
   461  			name1 := names.SimpleNameGenerator.GenerateName("cr-1")
   462  			_, err = crClient.Create(context.TODO(), &unstructured.Unstructured{Object: map[string]interface{}{
   463  				"apiVersion": gvr.Group + "/" + gvr.Version,
   464  				"kind":       crd.Spec.Names.Kind,
   465  				"metadata": map[string]interface{}{
   466  					"name": name1,
   467  				},
   468  				"spec": map[string]interface{}{
   469  					"x":     int64(2),
   470  					"y":     int64(2),
   471  					"limit": int64(123),
   472  				},
   473  			}}, metav1.CreateOptions{})
   474  			if err != nil {
   475  				t.Errorf("Failed to create custom resource: %v", err)
   476  			}
   477  		})
   478  		t.Run("custom resource create and update MUST NOT allow data if failed validation", func(t *testing.T) {
   479  			name1 := names.SimpleNameGenerator.GenerateName("cr-1")
   480  
   481  			// a spec create that is invalid MUST fail validation
   482  			cr := &unstructured.Unstructured{Object: map[string]interface{}{
   483  				"apiVersion": gvr.Group + "/" + gvr.Version,
   484  				"kind":       crd.Spec.Names.Kind,
   485  				"metadata": map[string]interface{}{
   486  					"name": name1,
   487  				},
   488  				"spec": map[string]interface{}{
   489  					"x": int64(-1),
   490  					"y": int64(0),
   491  				},
   492  			}}
   493  
   494  			// a spec create that is invalid MUST fail validation
   495  			_, err = crClient.Create(context.TODO(), cr, metav1.CreateOptions{})
   496  			if err == nil {
   497  				t.Fatal("Expected create of invalid custom resource to fail")
   498  			} else {
   499  				if !strings.Contains(err.Error(), "self.spec.x + self.spec.y must be greater than or equal to 0") {
   500  					t.Fatalf("Expected error to contain %s but got %v", "self.spec.x + self.spec.y must be greater than or equal to 0", err.Error())
   501  				}
   502  			}
   503  		})
   504  		t.Run("custom resource create and update MUST NOT allow data if there is blocking error of MaxLength", func(t *testing.T) {
   505  			name2 := names.SimpleNameGenerator.GenerateName("cr-2")
   506  
   507  			// a spec create that has maxLengh err MUST fail validation
   508  			cr := &unstructured.Unstructured{Object: map[string]interface{}{
   509  				"apiVersion": gvr.Group + "/" + gvr.Version,
   510  				"kind":       crd.Spec.Names.Kind,
   511  				"metadata": map[string]interface{}{
   512  					"name": name2,
   513  				},
   514  				"spec": map[string]interface{}{
   515  					"x":     int64(2),
   516  					"y":     int64(2),
   517  					"extra": strings.Repeat("x", 201),
   518  					"floatMap": map[string]interface{}{
   519  						"key1": 0.2,
   520  						"key2": 0.3,
   521  					},
   522  					"limit": nil,
   523  				},
   524  			}}
   525  
   526  			_, err := crClient.Create(context.TODO(), cr, metav1.CreateOptions{})
   527  			if err == nil || !strings.Contains(err.Error(), "some validation rules were not checked because the object was invalid; correct the existing errors to complete validation") {
   528  				t.Fatalf("expect error to contain \"some validation rules were not checked because the object was invalid; correct the existing errors to complete validation\" but get: %v", err)
   529  			}
   530  		})
   531  		t.Run("custom resource create and update MUST NOT allow data if there is blocking error of MaxItems", func(t *testing.T) {
   532  			name2 := names.SimpleNameGenerator.GenerateName("cr-2")
   533  			// a spec create that has maxItem err MUST fail validation
   534  			cr := &unstructured.Unstructured{Object: map[string]interface{}{
   535  				"apiVersion": gvr.Group + "/" + gvr.Version,
   536  				"kind":       crd.Spec.Names.Kind,
   537  				"metadata": map[string]interface{}{
   538  					"name": name2,
   539  				},
   540  				"spec": map[string]interface{}{
   541  					"x": int64(2),
   542  					"y": int64(2),
   543  					"floatMap": map[string]interface{}{
   544  						"key1": 0.2,
   545  						"key2": 0.3,
   546  					},
   547  					"assocList": []interface{}{},
   548  					"limit":     nil,
   549  				},
   550  			}}
   551  			assocList := cr.Object["spec"].(map[string]interface{})["assocList"].([]interface{})
   552  			for i := 1; i <= 101; i++ {
   553  				assocList = append(assocList, map[string]interface{}{
   554  					"k": "a",
   555  					"v": fmt.Sprintf("%d", i),
   556  				})
   557  			}
   558  			cr.Object["spec"].(map[string]interface{})["assocList"] = assocList
   559  
   560  			_, err = crClient.Create(context.TODO(), cr, metav1.CreateOptions{})
   561  			if err == nil || !strings.Contains(err.Error(), "some validation rules were not checked because the object was invalid; correct the existing errors to complete validation") {
   562  				t.Fatalf("expect error to contain \"some validation rules were not checked because the object was invalid; correct the existing errors to complete validation\" but get: %v", err)
   563  			}
   564  		})
   565  		t.Run("custom resource create and update MUST NOT allow data if there is blocking error of MaxProperties", func(t *testing.T) {
   566  			name2 := names.SimpleNameGenerator.GenerateName("cr-2")
   567  			// a spec create that has maxItem err MUST fail validation
   568  			cr := &unstructured.Unstructured{Object: map[string]interface{}{
   569  				"apiVersion": gvr.Group + "/" + gvr.Version,
   570  				"kind":       crd.Spec.Names.Kind,
   571  				"metadata": map[string]interface{}{
   572  					"name": name2,
   573  				},
   574  				"spec": map[string]interface{}{
   575  					"x":        int64(2),
   576  					"y":        int64(2),
   577  					"floatMap": map[string]interface{}{},
   578  					"assocList": []interface{}{
   579  						map[string]interface{}{
   580  							"k": "a",
   581  							"v": "1",
   582  						},
   583  					},
   584  					"limit": nil,
   585  				},
   586  			}}
   587  			floatMap := cr.Object["spec"].(map[string]interface{})["floatMap"].(map[string]interface{})
   588  			for i := 1; i <= 101; i++ {
   589  				floatMap[fmt.Sprintf("key%d", i)] = float64(i) / 10
   590  			}
   591  
   592  			_, err = crClient.Create(context.TODO(), cr, metav1.CreateOptions{})
   593  			if err == nil || !strings.Contains(err.Error(), "some validation rules were not checked because the object was invalid; correct the existing errors to complete validation") {
   594  				t.Fatalf("expect error to contain \"some validation rules were not checked because the object was invalid; correct the existing errors to complete validation\" but get: %v", err)
   595  			}
   596  		})
   597  		t.Run("custom resource create and update MUST NOT allow data if there is blocking error of missing required field", func(t *testing.T) {
   598  			name2 := names.SimpleNameGenerator.GenerateName("cr-2")
   599  			// a spec create that has required err MUST fail validation
   600  			cr := &unstructured.Unstructured{Object: map[string]interface{}{
   601  				"apiVersion": gvr.Group + "/" + gvr.Version,
   602  				"kind":       crd.Spec.Names.Kind,
   603  				"metadata": map[string]interface{}{
   604  					"name": name2,
   605  				},
   606  				"spec": map[string]interface{}{
   607  					"x": int64(2),
   608  					"y": int64(2),
   609  					"floatMap": map[string]interface{}{
   610  						"key1": 0.2,
   611  						"key2": 0.3,
   612  					},
   613  					"assocList": []interface{}{
   614  						map[string]interface{}{
   615  							"k": "1",
   616  						},
   617  					},
   618  					"limit": nil,
   619  				},
   620  			}}
   621  
   622  			_, err = crClient.Create(context.TODO(), cr, metav1.CreateOptions{})
   623  			if err == nil || !strings.Contains(err.Error(), "some validation rules were not checked because the object was invalid; correct the existing errors to complete validation") {
   624  				t.Fatalf("expect error to contain \"some validation rules were not checked because the object was invalid; correct the existing errors to complete validation\" but get: %v", err)
   625  			}
   626  		})
   627  		t.Run("custom resource create and update MUST NOT allow data if there is blocking error of type", func(t *testing.T) {
   628  			name2 := names.SimpleNameGenerator.GenerateName("cr-2")
   629  			// a spec create that has required err MUST fail validation
   630  			cr := &unstructured.Unstructured{Object: map[string]interface{}{
   631  				"apiVersion": gvr.Group + "/" + gvr.Version,
   632  				"kind":       crd.Spec.Names.Kind,
   633  				"metadata": map[string]interface{}{
   634  					"name": name2,
   635  				},
   636  				"spec": map[string]interface{}{
   637  					"x": int64(2),
   638  					"y": int64(2),
   639  					"floatMap": map[string]interface{}{
   640  						"key1": 0.2,
   641  						"key2": 0.3,
   642  					},
   643  					"assocList": []interface{}{
   644  						map[string]interface{}{
   645  							"k": "a",
   646  							"v": true,
   647  						},
   648  					},
   649  					"limit": nil,
   650  				},
   651  			}}
   652  
   653  			_, err = crClient.Create(context.TODO(), cr, metav1.CreateOptions{})
   654  			if err == nil || !strings.Contains(err.Error(), "some validation rules were not checked because the object was invalid; correct the existing errors to complete validation") {
   655  				t.Fatalf("expect error to contain \"some validation rules were not checked because the object was invalid; correct the existing errors to complete validation\" but get: %v", err)
   656  			}
   657  		})
   658  	})
   659  }
   660  
   661  func nonStructuralCrdWithValidations() *apiextensionsv1beta1.CustomResourceDefinition {
   662  	return &apiextensionsv1beta1.CustomResourceDefinition{
   663  		ObjectMeta: metav1.ObjectMeta{
   664  			Name: "foos.nonstructural.cr.bar.com",
   665  		},
   666  		Spec: apiextensionsv1beta1.CustomResourceDefinitionSpec{
   667  			Group:   "nonstructural.cr.bar.com",
   668  			Version: "v1",
   669  			Scope:   apiextensionsv1beta1.NamespaceScoped,
   670  			Names: apiextensionsv1beta1.CustomResourceDefinitionNames{
   671  				Plural: "foos",
   672  				Kind:   "Foo",
   673  			},
   674  			Validation: &apiextensionsv1beta1.CustomResourceValidation{
   675  				OpenAPIV3Schema: &apiextensionsv1beta1.JSONSchemaProps{
   676  					Type: "object",
   677  					Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
   678  						"foo": {},
   679  					},
   680  				},
   681  			},
   682  		},
   683  	}
   684  }
   685  
   686  func genLargeArray(n, x int64) []int64 {
   687  	arr := make([]int64, n)
   688  	for i := int64(0); i < n; i++ {
   689  		arr[i] = x
   690  	}
   691  	return arr
   692  }
   693  
   694  func crdWithSchema(t *testing.T, kind string, schemaJson []byte) *apiextensionsv1.CustomResourceDefinition {
   695  	plural := strings.ToLower(kind) + "s"
   696  	var c apiextensionsv1.CustomResourceValidation
   697  	err := json.Unmarshal(schemaJson, &c)
   698  	if err != nil {
   699  		t.Fatal(err)
   700  	}
   701  
   702  	return &apiextensionsv1.CustomResourceDefinition{
   703  		ObjectMeta: metav1.ObjectMeta{Name: fmt.Sprintf("%s.mygroup.example.com", plural)},
   704  		Spec: apiextensionsv1.CustomResourceDefinitionSpec{
   705  			Group: "mygroup.example.com",
   706  			Versions: []apiextensionsv1.CustomResourceDefinitionVersion{{
   707  				Name:    "v1beta1",
   708  				Served:  true,
   709  				Storage: true,
   710  				Schema:  &c,
   711  				Subresources: &apiextensionsv1.CustomResourceSubresources{
   712  					Status: &apiextensionsv1.CustomResourceSubresourceStatus{},
   713  				},
   714  			}},
   715  			Names: apiextensionsv1.CustomResourceDefinitionNames{
   716  				Plural: plural,
   717  				Kind:   kind,
   718  			},
   719  			Scope: apiextensionsv1.ClusterScoped,
   720  		},
   721  	}
   722  }
   723  
   724  var structuralSchemaWithValidators = []byte(`
   725  {
   726    "openAPIV3Schema": {
   727      "description": "CRD with CEL validators",
   728      "type": "object",
   729  	"x-kubernetes-validations": [
   730  	  {
   731  		"rule": "self.spec.x + self.spec.y >= (has(self.status) ? self.status.z : 0)"
   732  	  }
   733  	],
   734      "properties": {
   735        "spec": {
   736          "type": "object",
   737          "properties": {
   738            "x": {
   739              "type": "integer",
   740  			"default": 0
   741            },
   742            "y": {
   743              "type": "integer",
   744  			"default": 0
   745            },
   746            "extra": {
   747  			"type": "string",
   748  			"x-kubernetes-validations": [
   749  			  {
   750  				"rule": "self.startsWith('anything')"
   751  			  }
   752  			]
   753            },
   754  		  "floatMap": {
   755  			"type": "object",
   756  			"additionalProperties": { "type": "number" },
   757  			"x-kubernetes-validations": [
   758  			  {
   759  				"rule": "self.all(k, self[k] >= 0.2)"
   760  			  }
   761  			]
   762            },
   763  		  "assocList": {
   764  			"type": "array",
   765  			"maxItems": 100,
   766  			"items": {
   767  			  "type": "object",
   768  			  "properties": {
   769  			    "k": { "type": "string", "maxLength": 200},
   770  			    "v": { "type": "string", "maxLength": 200}
   771  			  },
   772  			  "required": ["k"]
   773  			},
   774  			"x-kubernetes-list-type": "map",
   775  			"x-kubernetes-list-map-keys": ["k"],
   776  			"x-kubernetes-validations": [
   777  			  {
   778  				"rule": "self.exists(e, e.k == 'a' && e.v == '1')"
   779  			  }
   780  			]
   781            },
   782            "limit": {
   783  			"nullable": true,
   784  			"x-kubernetes-validations": [
   785  			  {
   786  				"rule": "type(self) == int && self == 123"
   787  			  }
   788  			],
   789  			"x-kubernetes-int-or-string": true
   790            }
   791          }
   792        },
   793        "status": {
   794          "type": "object",
   795  		"properties": {
   796            "z": {
   797              "type": "integer",
   798  			"default": 0
   799            }
   800          }
   801        }
   802      }
   803    }
   804  }`)
   805  
   806  var structuralSchemaWithBlockingErr = []byte(`
   807  {
   808    "openAPIV3Schema": {
   809      "description": "CRD with CEL validators",
   810      "type": "object",
   811  	"x-kubernetes-validations": [
   812  	  {
   813  		"rule": "self.spec.x + self.spec.y >= (has(self.status) ? self.status.z : 0)",
   814  		"messageExpression": "\"self.spec.x + self.spec.y must be greater than or equal to 0\""
   815  	  }
   816  	],
   817      "properties": {
   818        "spec": {
   819          "type": "object",
   820          "properties": {
   821            "x": {
   822              "type": "integer",
   823  			"default": 0
   824            },
   825            "y": {
   826              "type": "integer",
   827  			"default": 0
   828            },
   829            "extra": {
   830  			"type": "string",
   831              "maxLength": 200,
   832  			"x-kubernetes-validations": [
   833  			  {
   834  				"rule": "self.startsWith('anything')"
   835  			  }
   836  			]
   837            },
   838  		  "floatMap": {
   839  			"type": "object",
   840              "maxProperties": 100,
   841  			"additionalProperties": { "type": "number" },
   842  			"x-kubernetes-validations": [
   843  			  {
   844  				"rule": "self.all(k, self[k] >= 0.2)"
   845  			  }
   846  			]
   847            },
   848  		  "assocList": {
   849  			"type": "array",
   850              "maxItems": 100,
   851  			"items": {
   852  			  "type": "object",
   853  			  "properties": {
   854  			    "k": { "type": "string" },
   855  			    "v": { "type": "string" }
   856  			  },
   857  			  "required": ["k", "v"]
   858  			},
   859  			"x-kubernetes-list-type": "map",
   860  			"x-kubernetes-list-map-keys": ["k"],
   861  			"x-kubernetes-validations": [
   862  			  {
   863  				"rule": "self.exists(e, e.k == 'a' && e.v == '1')"
   864  			  }
   865  			]
   866            },
   867            "limit": {
   868  			"nullable": true,
   869  			"x-kubernetes-validations": [
   870  			  {
   871  				"rule": "type(self) == int && self == 123"
   872  			  }
   873  			],
   874  			"x-kubernetes-int-or-string": true
   875            }
   876          }
   877        },
   878        "status": {
   879          "type": "object",
   880  		"properties": {
   881            "z": {
   882              "type": "integer",
   883  			"default": 0
   884            }
   885          }
   886        }
   887      }
   888    }
   889  }`)
   890  
   891  var structuralSchemaWithValidMetadataValidators = []byte(`
   892  {
   893    "openAPIV3Schema": {
   894      "description": "CRD with CEL validators",
   895      "type": "object",
   896  	"x-kubernetes-validations": [
   897  	  {
   898  		"rule": "self.metadata.name.size() > 3"
   899  	  }
   900  	],
   901      "properties": {
   902  	  "metadata": {
   903          "type": "object",
   904          "properties": {
   905  		  "name": { "type": "string" }
   906  	    }
   907        },
   908        "spec": {
   909          "type": "object",
   910          "properties": {}
   911        },
   912        "status": {
   913          "type": "object",
   914          "properties": {}
   915  	  }
   916      }
   917    }
   918  }`)
   919  
   920  var structuralSchemaWithInvalidMetadataValidators = []byte(`
   921  {
   922    "openAPIV3Schema": {
   923      "description": "CRD with CEL validators",
   924      "type": "object",
   925  	"x-kubernetes-validations": [
   926  	  {
   927  		"rule": "self.metadata.labels.size() > 0"
   928  	  }
   929  	],
   930      "properties": {
   931  	  "metadata": {
   932          "type": "object",
   933          "properties": {
   934  		  "name": { "type": "string" }
   935  	    }
   936        },
   937        "spec": {
   938          "type": "object",
   939          "properties": {}
   940        },
   941        "status": {
   942          "type": "object",
   943          "properties": {}
   944  	  }
   945      }
   946    }
   947  }`)
   948  
   949  var structuralSchemaWithValidTransitionRule = []byte(`
   950  {
   951    "openAPIV3Schema": {
   952      "description": "CRD with CEL validators",
   953      "type": "object",
   954      "properties": {
   955        "spec": {
   956          "type": "object",
   957          "properties": {
   958  		  "someImmutableThing": { "type": "string" },
   959            "somethingElse": { "type": "string" }
   960  	    },
   961  		"x-kubernetes-validations": [
   962  		  {
   963  			"rule": "self.someImmutableThing == oldSelf.someImmutableThing"
   964  		  }
   965  		]
   966        },
   967        "status": {
   968          "type": "object",
   969          "properties": {}
   970  	  }
   971      }
   972    }
   973  }`)
   974  
   975  var structuralSchemaWithInvalidTransitionRule = []byte(`
   976  {
   977    "openAPIV3Schema": {
   978      "description": "CRD with CEL validators",
   979      "type": "object",
   980      "properties": {
   981        "spec": {
   982          "type": "object",
   983          "properties": {
   984  		  "list": {
   985              "type": "array",
   986              "items": {
   987                "type": "string",
   988  		      "x-kubernetes-validations": [
   989  		        {
   990  			      "rule": "self == oldSelf"
   991                  }
   992  		      ]
   993              }
   994            }
   995  	    }
   996        },
   997        "status": {
   998          "type": "object",
   999          "properties": {}
  1000  	  }
  1001      }
  1002    }
  1003  }`)
  1004  
  1005  var structuralSchemaWithDefaultMapKeyTransitionRule = []byte(`
  1006  {
  1007    "openAPIV3Schema": {
  1008      "description": "CRD with CEL validators",
  1009      "type": "object",
  1010      "properties": {
  1011        "spec": {
  1012          "type": "object",
  1013          "properties": {
  1014  		  "list": {
  1015              "type": "array",
  1016              "x-kubernetes-list-map-keys": [
  1017                "k1",
  1018                "k2"
  1019              ],
  1020              "x-kubernetes-list-type": "map",
  1021              "maxItems": 1000,
  1022              "items": {
  1023                "type": "object",
  1024                "properties": {
  1025                  "k1": { "type": "string" },
  1026                  "k2": { "type": "string", "default": "DEFAULT" },
  1027                  "v": { "type": "string", "maxLength": 200 }
  1028                },
  1029  			  "required": ["k1"],
  1030  		      "x-kubernetes-validations": [
  1031  		        {
  1032  			      "rule": "self.v == oldSelf.v"
  1033                  }
  1034  		      ]
  1035              }
  1036            }
  1037  	    }
  1038        },
  1039        "status": {
  1040          "type": "object",
  1041          "properties": {}
  1042  	  }
  1043      }
  1044    }
  1045  }`)
  1046  
  1047  var structuralSchemaWithCostLimit = []byte(`
  1048  {
  1049    "openAPIV3Schema": {
  1050      "description": "CRD with CEL validators",
  1051      "type": "object",
  1052      "properties": {
  1053        "spec": {
  1054          "type": "object",
  1055          "properties": {
  1056  		  "list": {
  1057  			"type": "array",
  1058  			"maxItems": 725,
  1059              "items": {
  1060  			  "type": "integer"
  1061  			},
  1062  			"x-kubernetes-validations": [
  1063  		        {
  1064  			      "rule": "self.all(x, self.all(y, x == y))"
  1065                  }
  1066  		      ]
  1067            }
  1068  	    }
  1069        },
  1070        "status": {
  1071          "type": "object",
  1072          "properties": {}
  1073  	  }
  1074      }
  1075    }
  1076  }`)
  1077  
  1078  var structuralSchemaWithEmptyObject = []byte(`
  1079  {
  1080    "openAPIV3Schema": {
  1081      "description": "weird CRD with empty spec, unstructured status. designed to fit test fixtures.",
  1082      "type": "object",
  1083      "x-kubernetes-validations": [
  1084        {
  1085          "rule": "[has(self.spec), has(self.status)].exists_one(x, x)"
  1086        }
  1087      ],
  1088      "properties": {
  1089        "spec": {
  1090          "type": "object"
  1091        },
  1092        "status": {
  1093          "type": "object",
  1094          "additionalProperties": true
  1095        }
  1096      }
  1097    }
  1098  }
  1099  `)
  1100  

View as plain text