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