k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/test/integration/apiserver/crd_validation_expressions_test.go (about) 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 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 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 `)