sigs.k8s.io/cluster-api@v1.7.1/internal/topology/variables/cluster_variable_validation_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 variables 18 19 import ( 20 "testing" 21 22 . "github.com/onsi/gomega" 23 apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 24 "k8s.io/apimachinery/pkg/util/validation/field" 25 "k8s.io/utils/ptr" 26 27 clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" 28 ) 29 30 func Test_ValidateClusterVariables(t *testing.T) { 31 tests := []struct { 32 name string 33 definitions []clusterv1.ClusterClassStatusVariable 34 values []clusterv1.ClusterVariable 35 validateRequired bool 36 wantErr bool 37 }{ 38 { 39 name: "Pass for a number of valid values.", 40 definitions: []clusterv1.ClusterClassStatusVariable{ 41 { 42 Name: "cpu", 43 Definitions: []clusterv1.ClusterClassStatusVariableDefinition{ 44 { 45 Required: true, 46 From: clusterv1.VariableDefinitionFromInline, 47 Schema: clusterv1.VariableSchema{ 48 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 49 Type: "integer", 50 Minimum: ptr.To[int64](1), 51 }, 52 }, 53 }, 54 }, 55 }, 56 { 57 Name: "zone", 58 Definitions: []clusterv1.ClusterClassStatusVariableDefinition{ 59 { 60 Required: true, 61 From: clusterv1.VariableDefinitionFromInline, 62 Schema: clusterv1.VariableSchema{ 63 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 64 Type: "string", 65 MinLength: ptr.To[int64](1), 66 }, 67 }, 68 }, 69 }, 70 }, 71 { 72 Name: "location", 73 Definitions: []clusterv1.ClusterClassStatusVariableDefinition{ 74 { 75 Required: true, 76 From: clusterv1.VariableDefinitionFromInline, 77 Schema: clusterv1.VariableSchema{ 78 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 79 Type: "string", 80 Enum: []apiextensionsv1.JSON{ 81 {Raw: []byte(`"us-east-1"`)}, 82 {Raw: []byte(`"us-east-2"`)}, 83 }, 84 }, 85 }, 86 }, 87 }, 88 }, 89 }, 90 values: []clusterv1.ClusterVariable{ 91 { 92 Name: "cpu", 93 Value: apiextensionsv1.JSON{ 94 Raw: []byte(`1`), 95 }, 96 }, 97 { 98 Name: "zone", 99 Value: apiextensionsv1.JSON{ 100 Raw: []byte(`"longerThanOneCharacter"`), 101 }, 102 }, 103 { 104 Name: "location", 105 Value: apiextensionsv1.JSON{ 106 Raw: []byte(`"us-east-1"`), 107 }, 108 }, 109 }, 110 validateRequired: true, 111 }, 112 { 113 name: "Error when no value for required definition.", 114 wantErr: true, 115 definitions: []clusterv1.ClusterClassStatusVariable{ 116 { 117 Name: "cpu", 118 Definitions: []clusterv1.ClusterClassStatusVariableDefinition{ 119 { 120 Required: true, 121 From: clusterv1.VariableDefinitionFromInline, 122 Schema: clusterv1.VariableSchema{ 123 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 124 Type: "integer", 125 Minimum: ptr.To[int64](1), 126 }, 127 }, 128 }, 129 }, 130 }, 131 { 132 Name: "zone", 133 Definitions: []clusterv1.ClusterClassStatusVariableDefinition{ 134 { 135 Required: true, 136 From: clusterv1.VariableDefinitionFromInline, 137 Schema: clusterv1.VariableSchema{ 138 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 139 Type: "string", 140 MinLength: ptr.To[int64](1), 141 }, 142 }, 143 }, 144 }, 145 }, 146 }, 147 values: []clusterv1.ClusterVariable{ 148 // cpu is missing in the values but is required in definition. 149 { 150 Name: "zone", 151 Value: apiextensionsv1.JSON{ 152 Raw: []byte(`"longerThanOneCharacter"`), 153 }, 154 }, 155 }, 156 validateRequired: true, 157 }, 158 { 159 name: "Pass if validateRequired='false' and no value for required definition.", 160 definitions: []clusterv1.ClusterClassStatusVariable{ 161 { 162 Name: "cpu", 163 Definitions: []clusterv1.ClusterClassStatusVariableDefinition{ 164 { 165 Required: true, 166 From: clusterv1.VariableDefinitionFromInline, 167 Schema: clusterv1.VariableSchema{ 168 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 169 Type: "integer", 170 Minimum: ptr.To[int64](1), 171 }, 172 }, 173 }, 174 }, 175 }, 176 { 177 Name: "zone", 178 Definitions: []clusterv1.ClusterClassStatusVariableDefinition{ 179 { 180 Required: true, 181 From: clusterv1.VariableDefinitionFromInline, 182 Schema: clusterv1.VariableSchema{ 183 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 184 Type: "string", 185 MinLength: ptr.To[int64](1), 186 }, 187 }, 188 }, 189 }, 190 }, 191 }, 192 193 values: []clusterv1.ClusterVariable{ 194 // cpu is missing in the values and is required in definitions, 195 // but required validation is disabled. 196 { 197 Name: "zone", 198 Value: apiextensionsv1.JSON{ 199 Raw: []byte(`"longerThanOneCharacter"`), 200 }, 201 }, 202 }, 203 validateRequired: false, 204 }, 205 { 206 name: "Error if value has no definition.", 207 wantErr: true, 208 definitions: []clusterv1.ClusterClassStatusVariable{}, 209 values: []clusterv1.ClusterVariable{ 210 // location has a value but no definition. 211 { 212 Name: "location", 213 Value: apiextensionsv1.JSON{ 214 Raw: []byte(`"us-east-1"`), 215 }, 216 }, 217 }, 218 validateRequired: true, 219 }, 220 // Non-conflicting definition tests. 221 { 222 name: "Pass if a value with empty definitionFrom set for a non-conflicting definition", 223 definitions: []clusterv1.ClusterClassStatusVariable{ 224 { 225 Name: "cpu", 226 Definitions: []clusterv1.ClusterClassStatusVariableDefinition{ 227 { 228 Schema: clusterv1.VariableSchema{ 229 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 230 Type: "integer", 231 }, 232 }, 233 From: clusterv1.VariableDefinitionFromInline, 234 Required: true, 235 }, 236 { 237 Schema: clusterv1.VariableSchema{ 238 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 239 Type: "integer", 240 }, 241 }, 242 From: "somepatch", 243 Required: true, 244 }, 245 }, 246 }, 247 }, 248 values: []clusterv1.ClusterVariable{ 249 { 250 Name: "cpu", 251 Value: apiextensionsv1.JSON{ 252 Raw: []byte(`1`), 253 }, 254 }, 255 }, 256 validateRequired: true, 257 }, 258 { 259 name: "Pass when there is a separate value for each required definition with no definition conflicts.", 260 definitions: []clusterv1.ClusterClassStatusVariable{ 261 { 262 Name: "cpu", 263 Definitions: []clusterv1.ClusterClassStatusVariableDefinition{ 264 { 265 Schema: clusterv1.VariableSchema{ 266 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 267 Type: "integer", 268 }, 269 }, 270 From: clusterv1.VariableDefinitionFromInline, 271 Required: true, 272 }, 273 { 274 Schema: clusterv1.VariableSchema{ 275 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 276 Type: "integer", 277 }, 278 }, 279 From: "somepatch", 280 Required: true, 281 }, 282 }, 283 }, 284 }, 285 values: []clusterv1.ClusterVariable{ 286 // Each value is set individually. 287 { 288 Name: "cpu", 289 Value: apiextensionsv1.JSON{ 290 Raw: []byte(`1`), 291 }, 292 DefinitionFrom: "somepatch", 293 }, 294 { 295 Name: "cpu", 296 Value: apiextensionsv1.JSON{ 297 Raw: []byte(`2`), 298 }, 299 DefinitionFrom: "inline", 300 }, 301 }, 302 validateRequired: true, 303 }, 304 { 305 name: "Fail if value DefinitionFrom field does not match any definition.", 306 wantErr: true, 307 definitions: []clusterv1.ClusterClassStatusVariable{ 308 { 309 Name: "cpu", 310 Definitions: []clusterv1.ClusterClassStatusVariableDefinition{ 311 { 312 From: clusterv1.VariableDefinitionFromInline, 313 Schema: clusterv1.VariableSchema{ 314 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 315 Type: "string", 316 }, 317 }, 318 }, 319 }, 320 }, 321 }, 322 values: []clusterv1.ClusterVariable{ 323 { 324 Name: "cpu", 325 Value: apiextensionsv1.JSON{ 326 Raw: []byte(`1`), 327 }, 328 // This definition does not exist. 329 DefinitionFrom: "non-existent-patch", 330 }, 331 }, 332 validateRequired: true, 333 }, 334 { 335 name: "Fail if a value is set twice with the same definitionFrom.", 336 wantErr: true, 337 definitions: []clusterv1.ClusterClassStatusVariable{ 338 { 339 Name: "cpu", 340 Definitions: []clusterv1.ClusterClassStatusVariableDefinition{ 341 { 342 Schema: clusterv1.VariableSchema{ 343 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 344 Type: "integer", 345 }, 346 }, 347 From: "somepatch", 348 }, 349 }, 350 }, 351 }, 352 values: []clusterv1.ClusterVariable{ 353 { 354 Name: "cpu", 355 Value: apiextensionsv1.JSON{ 356 Raw: []byte(`1`), 357 }, 358 DefinitionFrom: "somepatch", 359 }, 360 { 361 Name: "cpu", 362 Value: apiextensionsv1.JSON{ 363 Raw: []byte(`2`), 364 }, 365 DefinitionFrom: "somepatch", 366 }, 367 }, 368 validateRequired: true, 369 }, 370 { 371 name: "Fail if a value is set with empty and non-empty definitionFrom.", 372 wantErr: true, 373 definitions: []clusterv1.ClusterClassStatusVariable{ 374 { 375 Name: "cpu", 376 Definitions: []clusterv1.ClusterClassStatusVariableDefinition{ 377 { 378 Schema: clusterv1.VariableSchema{ 379 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 380 Type: "integer", 381 }, 382 }, 383 From: "somepatch", 384 }, 385 { 386 Schema: clusterv1.VariableSchema{ 387 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 388 Type: "integer", 389 }, 390 }, 391 From: clusterv1.VariableDefinitionFromInline, 392 }, 393 }, 394 }, 395 }, 396 values: []clusterv1.ClusterVariable{ 397 { 398 Name: "cpu", 399 Value: apiextensionsv1.JSON{ 400 Raw: []byte(`1`), 401 }, 402 // Mix of empty and non-empty definitionFrom is not valid. 403 DefinitionFrom: "", 404 }, 405 { 406 Name: "cpu", 407 Value: apiextensionsv1.JSON{ 408 Raw: []byte(`2`), 409 }, 410 DefinitionFrom: "somepatch", 411 }, 412 }, 413 validateRequired: true, 414 }, 415 { 416 name: "Fail when values invalid by their definition schema.", 417 wantErr: true, 418 definitions: []clusterv1.ClusterClassStatusVariable{ 419 { 420 Name: "cpu", 421 DefinitionsConflict: true, 422 Definitions: []clusterv1.ClusterClassStatusVariableDefinition{ 423 { 424 From: clusterv1.VariableDefinitionFromInline, 425 Schema: clusterv1.VariableSchema{ 426 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 427 Type: "string", 428 }, 429 }, 430 }, 431 { 432 From: "somepatch", 433 Schema: clusterv1.VariableSchema{ 434 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 435 Type: "integer", 436 }, 437 }, 438 }, 439 }, 440 }, 441 }, 442 values: []clusterv1.ClusterVariable{ 443 { 444 Name: "cpu", 445 Value: apiextensionsv1.JSON{ 446 Raw: []byte(`1`), 447 }, 448 DefinitionFrom: "inline", 449 }, 450 { 451 Name: "cpu", 452 Value: apiextensionsv1.JSON{ 453 Raw: []byte(`"one"`), 454 }, 455 DefinitionFrom: "somepatch", 456 }, 457 }, 458 validateRequired: true, 459 }, 460 // Conflicting definition tests. 461 { 462 name: "Pass with a value provided for each conflicting definition.", 463 definitions: []clusterv1.ClusterClassStatusVariable{ 464 { 465 Name: "cpu", 466 DefinitionsConflict: true, 467 Definitions: []clusterv1.ClusterClassStatusVariableDefinition{ 468 { 469 From: clusterv1.VariableDefinitionFromInline, 470 Schema: clusterv1.VariableSchema{ 471 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 472 Type: "string", 473 }, 474 }, 475 }, 476 { 477 From: "somepatch", 478 Schema: clusterv1.VariableSchema{ 479 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 480 Type: "integer", 481 }, 482 }, 483 }, 484 }, 485 }, 486 }, 487 values: []clusterv1.ClusterVariable{ 488 { 489 Name: "cpu", 490 Value: apiextensionsv1.JSON{ 491 Raw: []byte(`"one"`), 492 }, 493 DefinitionFrom: "inline", 494 }, 495 { 496 Name: "cpu", 497 Value: apiextensionsv1.JSON{ 498 Raw: []byte(`1`), 499 }, 500 DefinitionFrom: "somepatch", 501 }, 502 }, 503 validateRequired: true, 504 }, 505 { 506 name: "Pass if non-required definition value doesn't include definitionFrom for each required definition when definitions conflict.", 507 definitions: []clusterv1.ClusterClassStatusVariable{ 508 { 509 Name: "cpu", 510 DefinitionsConflict: true, 511 // There are conflicting definitions which means values should include a `definitionFrom` field. 512 Definitions: []clusterv1.ClusterClassStatusVariableDefinition{ 513 { 514 Schema: clusterv1.VariableSchema{ 515 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 516 Type: "integer", 517 }, 518 }, 519 From: "somepatch", 520 Required: true, 521 }, 522 // This variable is not required so it does not need a value. 523 { 524 Schema: clusterv1.VariableSchema{ 525 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 526 Type: "integer", 527 }, 528 }, 529 From: "anotherpatch", 530 Required: false, 531 }, 532 }, 533 }, 534 }, 535 values: []clusterv1.ClusterVariable{ 536 { 537 Name: "cpu", 538 Value: apiextensionsv1.JSON{ 539 Raw: []byte(`1`), 540 }, 541 DefinitionFrom: "somepatch", 542 }, 543 }, 544 validateRequired: true, 545 }, 546 { 547 name: "Fail if value doesn't include definitionFrom when definitions conflict.", 548 wantErr: true, 549 definitions: []clusterv1.ClusterClassStatusVariable{ 550 { 551 Name: "cpu", 552 // There are conflicting definitions which means values should include a `definitionFrom` field. 553 DefinitionsConflict: true, 554 Definitions: []clusterv1.ClusterClassStatusVariableDefinition{ 555 { 556 From: clusterv1.VariableDefinitionFromInline, 557 Schema: clusterv1.VariableSchema{ 558 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 559 Type: "string", 560 }, 561 }, 562 }, 563 { 564 From: "somepatch", 565 Schema: clusterv1.VariableSchema{ 566 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 567 Type: "integer", 568 }, 569 }, 570 }, 571 }, 572 }, 573 }, 574 values: []clusterv1.ClusterVariable{ 575 { 576 Name: "cpu", 577 Value: apiextensionsv1.JSON{ 578 Raw: []byte(`1`), 579 }, 580 // No definitionFrom 581 }, 582 }, 583 validateRequired: true, 584 }, 585 { 586 name: "Fail if value doesn't include definitionFrom for each required definition when definitions conflict.", 587 wantErr: true, 588 definitions: []clusterv1.ClusterClassStatusVariable{ 589 { 590 Name: "cpu", 591 DefinitionsConflict: true, 592 // There are conflicting definitions which means values should include a `definitionFrom` field. 593 Definitions: []clusterv1.ClusterClassStatusVariableDefinition{ 594 { 595 596 Schema: clusterv1.VariableSchema{ 597 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 598 Type: "string", 599 }, 600 }, 601 From: clusterv1.VariableDefinitionFromInline, 602 Required: true, 603 }, 604 { 605 Schema: clusterv1.VariableSchema{ 606 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 607 Type: "integer", 608 }, 609 }, 610 From: "somepatch", 611 Required: true, 612 }, 613 }, 614 }, 615 }, 616 values: []clusterv1.ClusterVariable{ 617 { 618 Name: "cpu", 619 Value: apiextensionsv1.JSON{ 620 Raw: []byte(`1`), 621 }, 622 DefinitionFrom: "somepatch", 623 }, 624 }, 625 validateRequired: true, 626 }, 627 } 628 for _, tt := range tests { 629 t.Run(tt.name, func(t *testing.T) { 630 g := NewWithT(t) 631 632 errList := validateClusterVariables(tt.values, tt.definitions, 633 tt.validateRequired, field.NewPath("spec", "topology", "variables")) 634 635 if tt.wantErr { 636 g.Expect(errList).NotTo(BeEmpty()) 637 return 638 } 639 g.Expect(errList).To(BeEmpty()) 640 }) 641 } 642 } 643 644 func Test_ValidateClusterVariable(t *testing.T) { 645 tests := []struct { 646 name string 647 clusterClassVariable *clusterv1.ClusterClassVariable 648 clusterVariable *clusterv1.ClusterVariable 649 wantErr bool 650 }{ 651 { 652 name: "Valid integer", 653 clusterClassVariable: &clusterv1.ClusterClassVariable{ 654 Name: "cpu", 655 Required: true, 656 Schema: clusterv1.VariableSchema{ 657 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 658 Type: "integer", 659 Minimum: ptr.To[int64](1), 660 }, 661 }, 662 }, 663 clusterVariable: &clusterv1.ClusterVariable{ 664 Name: "cpu", 665 Value: apiextensionsv1.JSON{ 666 Raw: []byte(`1`), 667 }, 668 }, 669 }, 670 { 671 name: "Error if integer is above Maximum", 672 wantErr: true, 673 clusterClassVariable: &clusterv1.ClusterClassVariable{ 674 Name: "cpu", 675 Required: true, 676 Schema: clusterv1.VariableSchema{ 677 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 678 Type: "integer", 679 Maximum: ptr.To[int64](10), 680 }, 681 }, 682 }, 683 clusterVariable: &clusterv1.ClusterVariable{ 684 Name: "cpu", 685 Value: apiextensionsv1.JSON{ 686 Raw: []byte(`99`), 687 }, 688 }, 689 }, 690 { 691 name: "Error if integer is below Minimum", 692 wantErr: true, 693 clusterClassVariable: &clusterv1.ClusterClassVariable{ 694 Name: "cpu", 695 Required: true, 696 Schema: clusterv1.VariableSchema{ 697 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 698 Type: "integer", 699 Minimum: ptr.To[int64](1), 700 }, 701 }, 702 }, 703 clusterVariable: &clusterv1.ClusterVariable{ 704 Name: "cpu", 705 Value: apiextensionsv1.JSON{ 706 Raw: []byte(`0`), 707 }, 708 }, 709 }, 710 711 { 712 name: "Fails, expected integer got string", 713 wantErr: true, 714 clusterClassVariable: &clusterv1.ClusterClassVariable{ 715 Name: "cpu", 716 Required: true, 717 Schema: clusterv1.VariableSchema{ 718 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 719 Type: "integer", 720 Minimum: ptr.To[int64](1), 721 }, 722 }, 723 }, 724 clusterVariable: &clusterv1.ClusterVariable{ 725 Name: "cpu", 726 Value: apiextensionsv1.JSON{ 727 Raw: []byte(`"1"`), 728 }, 729 }, 730 }, 731 { 732 name: "Valid string", 733 clusterClassVariable: &clusterv1.ClusterClassVariable{ 734 Name: "location", 735 Required: true, 736 Schema: clusterv1.VariableSchema{ 737 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 738 Type: "string", 739 MinLength: ptr.To[int64](1), 740 }, 741 }, 742 }, 743 clusterVariable: &clusterv1.ClusterVariable{ 744 Name: "location", 745 Value: apiextensionsv1.JSON{ 746 Raw: []byte(`"us-east"`), 747 }, 748 }, 749 }, 750 { 751 name: "Error if string doesn't match pattern ", 752 wantErr: true, 753 clusterClassVariable: &clusterv1.ClusterClassVariable{ 754 Name: "location", 755 Required: true, 756 Schema: clusterv1.VariableSchema{ 757 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 758 Type: "string", 759 Pattern: "^[0-9]+$", 760 }, 761 }, 762 }, 763 clusterVariable: &clusterv1.ClusterVariable{ 764 Name: "location", 765 Value: apiextensionsv1.JSON{ 766 Raw: []byte(`"000000a"`), 767 }, 768 }, 769 }, 770 { 771 name: "Error if string doesn't match format ", 772 wantErr: true, 773 clusterClassVariable: &clusterv1.ClusterClassVariable{ 774 Name: "location", 775 Required: true, 776 Schema: clusterv1.VariableSchema{ 777 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 778 Type: "string", 779 Format: "uri", 780 }, 781 }, 782 }, 783 clusterVariable: &clusterv1.ClusterVariable{ 784 Name: "location", 785 Value: apiextensionsv1.JSON{ 786 Raw: []byte(`"not a URI"`), 787 }, 788 }, 789 }, 790 { 791 name: "Valid enum string", 792 clusterClassVariable: &clusterv1.ClusterClassVariable{ 793 Name: "location", 794 Required: true, 795 Schema: clusterv1.VariableSchema{ 796 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 797 Type: "string", 798 Enum: []apiextensionsv1.JSON{ 799 {Raw: []byte(`"us-east-1"`)}, 800 {Raw: []byte(`"us-east-2"`)}, 801 }, 802 }, 803 }, 804 }, 805 clusterVariable: &clusterv1.ClusterVariable{ 806 Name: "location", 807 Value: apiextensionsv1.JSON{ 808 Raw: []byte(`"us-east-1"`), 809 }, 810 }, 811 }, 812 { 813 name: "Fails, value does not match one of the enum string values", 814 wantErr: true, 815 clusterClassVariable: &clusterv1.ClusterClassVariable{ 816 Name: "location", 817 Required: true, 818 Schema: clusterv1.VariableSchema{ 819 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 820 Type: "string", 821 Enum: []apiextensionsv1.JSON{ 822 {Raw: []byte(`"us-east-1"`)}, 823 {Raw: []byte(`"us-east-2"`)}, 824 }, 825 }, 826 }, 827 }, 828 clusterVariable: &clusterv1.ClusterVariable{ 829 Name: "location", 830 Value: apiextensionsv1.JSON{ 831 Raw: []byte(`"us-east-invalid"`), 832 }, 833 }, 834 }, 835 { 836 name: "Valid enum integer", 837 clusterClassVariable: &clusterv1.ClusterClassVariable{ 838 Name: "location", 839 Required: true, 840 Schema: clusterv1.VariableSchema{ 841 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 842 Type: "integer", 843 Enum: []apiextensionsv1.JSON{ 844 {Raw: []byte(`1`)}, 845 {Raw: []byte(`2`)}, 846 }, 847 }, 848 }, 849 }, 850 clusterVariable: &clusterv1.ClusterVariable{ 851 Name: "location", 852 Value: apiextensionsv1.JSON{ 853 Raw: []byte(`1`), 854 }, 855 }, 856 }, 857 { 858 name: "Fails, value does not match one of the enum integer values", 859 wantErr: true, 860 clusterClassVariable: &clusterv1.ClusterClassVariable{ 861 Name: "location", 862 Required: true, 863 Schema: clusterv1.VariableSchema{ 864 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 865 Type: "string", 866 Enum: []apiextensionsv1.JSON{ 867 {Raw: []byte(`1`)}, 868 {Raw: []byte(`2`)}, 869 }, 870 }, 871 }, 872 }, 873 clusterVariable: &clusterv1.ClusterVariable{ 874 Name: "location", 875 Value: apiextensionsv1.JSON{ 876 Raw: []byte(`3`), 877 }, 878 }, 879 }, 880 { 881 name: "Valid object", 882 clusterClassVariable: &clusterv1.ClusterClassVariable{ 883 Name: "httpProxy", 884 Required: true, 885 Schema: clusterv1.VariableSchema{ 886 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 887 Type: "object", 888 Properties: map[string]clusterv1.JSONSchemaProps{ 889 "enabled": { 890 Type: "boolean", 891 }, 892 }, 893 }, 894 }, 895 }, 896 clusterVariable: &clusterv1.ClusterVariable{ 897 Name: "httpProxy", 898 Value: apiextensionsv1.JSON{ 899 Raw: []byte(`{"enabled":false}`), 900 }, 901 }, 902 }, 903 { 904 name: "Error if nested field is invalid", 905 wantErr: true, 906 clusterClassVariable: &clusterv1.ClusterClassVariable{ 907 Name: "httpProxy", 908 Required: true, 909 Schema: clusterv1.VariableSchema{ 910 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 911 Type: "object", 912 Properties: map[string]clusterv1.JSONSchemaProps{ 913 "enabled": { 914 Type: "boolean", 915 }, 916 }, 917 }, 918 }, 919 }, 920 clusterVariable: &clusterv1.ClusterVariable{ 921 Name: "httpProxy", 922 Value: apiextensionsv1.JSON{ 923 Raw: []byte(`{"enabled":"not-a-bool"}`), 924 }, 925 }, 926 }, 927 { 928 name: "Error if object is a bool instead", 929 wantErr: true, 930 clusterClassVariable: &clusterv1.ClusterClassVariable{ 931 Name: "httpProxy", 932 Required: true, 933 Schema: clusterv1.VariableSchema{ 934 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 935 Type: "object", 936 Properties: map[string]clusterv1.JSONSchemaProps{ 937 "enabled": { 938 Type: "boolean", 939 }, 940 }, 941 }, 942 }, 943 }, 944 clusterVariable: &clusterv1.ClusterVariable{ 945 Name: "httpProxy", 946 Value: apiextensionsv1.JSON{ 947 Raw: []byte(`"not-a-object"`), 948 }, 949 }, 950 }, 951 { 952 name: "Error if object is missing required field", 953 wantErr: true, 954 clusterClassVariable: &clusterv1.ClusterClassVariable{ 955 Name: "httpProxy", 956 Required: true, 957 Schema: clusterv1.VariableSchema{ 958 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 959 Type: "object", 960 Properties: map[string]clusterv1.JSONSchemaProps{ 961 "url": { 962 Type: "string", 963 }, 964 "enabled": { 965 Type: "boolean", 966 }, 967 }, 968 Required: []string{ 969 "url", 970 }, 971 }, 972 }, 973 }, 974 clusterVariable: &clusterv1.ClusterVariable{ 975 Name: "httpProxy", 976 Value: apiextensionsv1.JSON{ 977 Raw: []byte(`{"enabled":"true"}`), 978 }, 979 }, 980 }, 981 { 982 name: "Valid object", 983 clusterClassVariable: &clusterv1.ClusterClassVariable{ 984 Name: "testObject", 985 Required: true, 986 Schema: clusterv1.VariableSchema{ 987 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 988 Type: "object", 989 Properties: map[string]clusterv1.JSONSchemaProps{ 990 "requiredProperty": { 991 Type: "boolean", 992 }, 993 "boolProperty": { 994 Type: "boolean", 995 }, 996 "integerProperty": { 997 Type: "integer", 998 Minimum: ptr.To[int64](1), 999 }, 1000 "enumProperty": { 1001 Type: "string", 1002 Enum: []apiextensionsv1.JSON{ 1003 {Raw: []byte(`"enumValue1"`)}, 1004 {Raw: []byte(`"enumValue2"`)}, 1005 }, 1006 }, 1007 }, 1008 Required: []string{"requiredProperty"}, 1009 }, 1010 }, 1011 }, 1012 clusterVariable: &clusterv1.ClusterVariable{ 1013 Name: "testObject", 1014 Value: apiextensionsv1.JSON{ 1015 Raw: []byte(`{"requiredProperty":false,"boolProperty":true,"integerProperty":1,"enumProperty":"enumValue2"}`), 1016 }, 1017 }, 1018 }, 1019 { 1020 name: "Valid enum object", 1021 clusterClassVariable: &clusterv1.ClusterClassVariable{ 1022 Name: "enumObject", 1023 Required: true, 1024 Schema: clusterv1.VariableSchema{ 1025 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 1026 Type: "object", 1027 Properties: map[string]clusterv1.JSONSchemaProps{ 1028 "location": { 1029 Type: "string", 1030 }, 1031 "url": { 1032 Type: "string", 1033 }, 1034 }, 1035 Enum: []apiextensionsv1.JSON{ 1036 {Raw: []byte(`{"location": "us-east-1","url":"us-east-1-url"}`)}, 1037 {Raw: []byte(`{"location": "us-east-2","url":"us-east-2-url"}`)}, 1038 }, 1039 }, 1040 }, 1041 }, 1042 clusterVariable: &clusterv1.ClusterVariable{ 1043 Name: "enumObject", 1044 Value: apiextensionsv1.JSON{ 1045 Raw: []byte(`{"location": "us-east-2","url":"us-east-2-url"}`), 1046 }, 1047 }, 1048 }, 1049 { 1050 name: "Fails, value does not match one of the enum object values", 1051 wantErr: true, 1052 clusterClassVariable: &clusterv1.ClusterClassVariable{ 1053 Name: "enumObject", 1054 Required: true, 1055 Schema: clusterv1.VariableSchema{ 1056 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 1057 Type: "object", 1058 Properties: map[string]clusterv1.JSONSchemaProps{ 1059 "location": { 1060 Type: "string", 1061 }, 1062 "url": { 1063 Type: "string", 1064 }, 1065 }, 1066 Enum: []apiextensionsv1.JSON{ 1067 {Raw: []byte(`{"location": "us-east-1","url":"us-east-1-url"}`)}, 1068 {Raw: []byte(`{"location": "us-east-2","url":"us-east-2-url"}`)}, 1069 }, 1070 }, 1071 }, 1072 }, 1073 clusterVariable: &clusterv1.ClusterVariable{ 1074 Name: "enumObject", 1075 Value: apiextensionsv1.JSON{ 1076 Raw: []byte(`{"location": "us-east-2","url":"wrong-url"}`), 1077 }, 1078 }, 1079 }, 1080 { 1081 name: "Valid map", 1082 clusterClassVariable: &clusterv1.ClusterClassVariable{ 1083 Name: "httpProxy", 1084 Required: true, 1085 Schema: clusterv1.VariableSchema{ 1086 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 1087 Type: "object", 1088 AdditionalProperties: &clusterv1.JSONSchemaProps{ 1089 Type: "object", 1090 Properties: map[string]clusterv1.JSONSchemaProps{ 1091 "enabled": { 1092 Type: "boolean", 1093 }, 1094 }, 1095 }, 1096 }, 1097 }, 1098 }, 1099 clusterVariable: &clusterv1.ClusterVariable{ 1100 Name: "httpProxy", 1101 Value: apiextensionsv1.JSON{ 1102 Raw: []byte(`{"proxy":{"enabled":false}}`), 1103 }, 1104 }, 1105 }, 1106 { 1107 name: "Error if map is missing a required field", 1108 wantErr: true, 1109 clusterClassVariable: &clusterv1.ClusterClassVariable{ 1110 Name: "httpProxy", 1111 Required: true, 1112 Schema: clusterv1.VariableSchema{ 1113 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 1114 Type: "object", 1115 AdditionalProperties: &clusterv1.JSONSchemaProps{ 1116 Type: "object", 1117 Properties: map[string]clusterv1.JSONSchemaProps{ 1118 "enabled": { 1119 Type: "boolean", 1120 }, 1121 "url": { 1122 Type: "string", 1123 }, 1124 }, 1125 Required: []string{"url"}, 1126 }, 1127 }, 1128 }, 1129 }, 1130 clusterVariable: &clusterv1.ClusterVariable{ 1131 Name: "httpProxy", 1132 Value: apiextensionsv1.JSON{ 1133 Raw: []byte(`{"proxy":{"enabled":false}}`), 1134 }, 1135 }, 1136 }, 1137 { 1138 name: "Valid array", 1139 clusterClassVariable: &clusterv1.ClusterClassVariable{ 1140 Name: "testArray", 1141 Required: true, 1142 Schema: clusterv1.VariableSchema{ 1143 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 1144 Type: "array", 1145 Items: &clusterv1.JSONSchemaProps{ 1146 Type: "string", 1147 Enum: []apiextensionsv1.JSON{ 1148 {Raw: []byte(`"enumValue1"`)}, 1149 {Raw: []byte(`"enumValue2"`)}, 1150 }, 1151 }, 1152 }, 1153 }, 1154 }, 1155 clusterVariable: &clusterv1.ClusterVariable{ 1156 Name: "testArray", 1157 Value: apiextensionsv1.JSON{ 1158 Raw: []byte(`["enumValue1","enumValue2"]`), 1159 }, 1160 }, 1161 }, 1162 { 1163 name: "Error if array element is invalid", 1164 wantErr: true, 1165 clusterClassVariable: &clusterv1.ClusterClassVariable{ 1166 Name: "testArray", 1167 Required: true, 1168 Schema: clusterv1.VariableSchema{ 1169 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 1170 Type: "array", 1171 Items: &clusterv1.JSONSchemaProps{ 1172 Type: "string", 1173 Enum: []apiextensionsv1.JSON{ 1174 {Raw: []byte(`"enumValue1"`)}, 1175 {Raw: []byte(`"enumValue2"`)}, 1176 }, 1177 }, 1178 }, 1179 }, 1180 }, 1181 clusterVariable: &clusterv1.ClusterVariable{ 1182 Name: "testArray", 1183 Value: apiextensionsv1.JSON{ 1184 Raw: []byte(`["enumValue1","enumValueInvalid"]`), 1185 }, 1186 }, 1187 }, 1188 { 1189 name: "Error if array is too large", 1190 wantErr: true, 1191 clusterClassVariable: &clusterv1.ClusterClassVariable{ 1192 Name: "testArray", 1193 Required: true, 1194 Schema: clusterv1.VariableSchema{ 1195 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 1196 Type: "array", 1197 Items: &clusterv1.JSONSchemaProps{ 1198 Type: "string", 1199 }, 1200 MaxItems: ptr.To[int64](3), 1201 }, 1202 }, 1203 }, 1204 clusterVariable: &clusterv1.ClusterVariable{ 1205 Name: "testArray", 1206 Value: apiextensionsv1.JSON{ 1207 Raw: []byte(`["value1","value2","value3","value4"]`), 1208 }, 1209 }, 1210 }, 1211 { 1212 name: "Error if array is too small", 1213 wantErr: true, 1214 clusterClassVariable: &clusterv1.ClusterClassVariable{ 1215 Name: "testArray", 1216 Required: true, 1217 Schema: clusterv1.VariableSchema{ 1218 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 1219 Type: "array", 1220 Items: &clusterv1.JSONSchemaProps{ 1221 Type: "string", 1222 }, 1223 MinItems: ptr.To[int64](3), 1224 }, 1225 }, 1226 }, 1227 clusterVariable: &clusterv1.ClusterVariable{ 1228 Name: "testArray", 1229 Value: apiextensionsv1.JSON{ 1230 Raw: []byte(`["value1","value2"]`), 1231 }, 1232 }, 1233 }, 1234 { 1235 name: "Error if array contains duplicate values", 1236 wantErr: true, 1237 clusterClassVariable: &clusterv1.ClusterClassVariable{ 1238 Name: "testArray", 1239 Required: true, 1240 Schema: clusterv1.VariableSchema{ 1241 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 1242 Type: "array", 1243 Items: &clusterv1.JSONSchemaProps{ 1244 Type: "string", 1245 }, 1246 UniqueItems: true, 1247 }, 1248 }, 1249 }, 1250 clusterVariable: &clusterv1.ClusterVariable{ 1251 Name: "testArray", 1252 Value: apiextensionsv1.JSON{ 1253 Raw: []byte(`["value1","value1"]`), 1254 }, 1255 }, 1256 }, 1257 { 1258 name: "Valid array object", 1259 clusterClassVariable: &clusterv1.ClusterClassVariable{ 1260 Name: "enumArray", 1261 Required: true, 1262 Schema: clusterv1.VariableSchema{ 1263 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 1264 Type: "array", 1265 Items: &clusterv1.JSONSchemaProps{ 1266 Type: "string", 1267 }, 1268 Enum: []apiextensionsv1.JSON{ 1269 {Raw: []byte(`["1","2","3"]`)}, 1270 {Raw: []byte(`["4","5","6"]`)}, 1271 }, 1272 }, 1273 }, 1274 }, 1275 clusterVariable: &clusterv1.ClusterVariable{ 1276 Name: "enumArray", 1277 Value: apiextensionsv1.JSON{ 1278 Raw: []byte(`["1","2","3"]`), 1279 }, 1280 }, 1281 }, 1282 { 1283 name: "Fails, value does not match one of the enum array values", 1284 wantErr: true, 1285 clusterClassVariable: &clusterv1.ClusterClassVariable{ 1286 Name: "enumArray", 1287 Required: true, 1288 Schema: clusterv1.VariableSchema{ 1289 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 1290 Type: "array", 1291 Items: &clusterv1.JSONSchemaProps{ 1292 Type: "string", 1293 }, 1294 Enum: []apiextensionsv1.JSON{ 1295 {Raw: []byte(`["1","2","3"]`)}, 1296 {Raw: []byte(`["4","5","6"]`)}, 1297 }, 1298 }, 1299 }, 1300 }, 1301 clusterVariable: &clusterv1.ClusterVariable{ 1302 Name: "enumArray", 1303 Value: apiextensionsv1.JSON{ 1304 Raw: []byte(`["7","8","9"]`), 1305 }, 1306 }, 1307 }, 1308 { 1309 name: "Valid object with x-kubernetes-preserve-unknown-fields", 1310 clusterClassVariable: &clusterv1.ClusterClassVariable{ 1311 Name: "testObject", 1312 Required: true, 1313 Schema: clusterv1.VariableSchema{ 1314 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 1315 Type: "object", 1316 Properties: map[string]clusterv1.JSONSchemaProps{ 1317 "knownProperty": { 1318 Type: "boolean", 1319 }, 1320 }, 1321 // Preserves fields for the current object (in this case unknownProperty). 1322 XPreserveUnknownFields: true, 1323 }, 1324 }, 1325 }, 1326 clusterVariable: &clusterv1.ClusterVariable{ 1327 Name: "testObject", 1328 Value: apiextensionsv1.JSON{ 1329 Raw: []byte(`{"knownProperty":false,"unknownProperty":true}`), 1330 }, 1331 }, 1332 }, 1333 { 1334 name: "Error if undefined field", 1335 wantErr: true, 1336 clusterClassVariable: &clusterv1.ClusterClassVariable{ 1337 Name: "testObject", 1338 Required: true, 1339 Schema: clusterv1.VariableSchema{ 1340 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 1341 Type: "object", 1342 Properties: map[string]clusterv1.JSONSchemaProps{ 1343 "knownProperty": { 1344 Type: "boolean", 1345 }, 1346 }, 1347 }, 1348 }, 1349 }, 1350 clusterVariable: &clusterv1.ClusterVariable{ 1351 Name: "testObject", 1352 Value: apiextensionsv1.JSON{ 1353 // unknownProperty is not defined in the schema. 1354 Raw: []byte(`{"knownProperty":false,"unknownProperty":true}`), 1355 }, 1356 }, 1357 }, 1358 { 1359 name: "Error if undefined field with different casing", 1360 wantErr: true, 1361 clusterClassVariable: &clusterv1.ClusterClassVariable{ 1362 Name: "testObject", 1363 Required: true, 1364 Schema: clusterv1.VariableSchema{ 1365 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 1366 Type: "object", 1367 Properties: map[string]clusterv1.JSONSchemaProps{ 1368 "knownProperty": { 1369 Type: "boolean", 1370 }, 1371 }, 1372 }, 1373 }, 1374 }, 1375 clusterVariable: &clusterv1.ClusterVariable{ 1376 Name: "testObject", 1377 Value: apiextensionsv1.JSON{ 1378 // KnownProperty is only defined with lower case in the schema. 1379 Raw: []byte(`{"KnownProperty":false}`), 1380 }, 1381 }, 1382 }, 1383 { 1384 name: "Valid nested object with x-kubernetes-preserve-unknown-fields", 1385 clusterClassVariable: &clusterv1.ClusterClassVariable{ 1386 Name: "testObject", 1387 Required: true, 1388 Schema: clusterv1.VariableSchema{ 1389 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 1390 Type: "object", 1391 // XPreserveUnknownFields preservers recursively if the object has nested fields 1392 // as no nested Properties are defined. 1393 XPreserveUnknownFields: true, 1394 }, 1395 }, 1396 }, 1397 clusterVariable: &clusterv1.ClusterVariable{ 1398 Name: "testObject", 1399 Value: apiextensionsv1.JSON{ 1400 Raw: []byte(`{"test": {"unknownProperty":false}}`), 1401 }, 1402 }, 1403 }, 1404 { 1405 name: "Valid object with nested fields and x-kubernetes-preserve-unknown-fields", 1406 clusterClassVariable: &clusterv1.ClusterClassVariable{ 1407 Name: "testObject", 1408 Required: true, 1409 Schema: clusterv1.VariableSchema{ 1410 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 1411 Type: "object", 1412 Properties: map[string]clusterv1.JSONSchemaProps{ 1413 "test": { 1414 Type: "object", 1415 Properties: map[string]clusterv1.JSONSchemaProps{ 1416 "knownProperty": { 1417 Type: "boolean", 1418 }, 1419 }, 1420 // Preserves fields on the current level (in this case unknownProperty). 1421 XPreserveUnknownFields: true, 1422 }, 1423 }, 1424 }, 1425 }, 1426 }, 1427 clusterVariable: &clusterv1.ClusterVariable{ 1428 Name: "testObject", 1429 Value: apiextensionsv1.JSON{ 1430 Raw: []byte(`{"test": {"knownProperty":false,"unknownProperty":true}}`), 1431 }, 1432 }, 1433 }, 1434 { 1435 name: "Error if undefined field nested", 1436 wantErr: true, 1437 clusterClassVariable: &clusterv1.ClusterClassVariable{ 1438 Name: "testObject", 1439 Required: true, 1440 Schema: clusterv1.VariableSchema{ 1441 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 1442 Type: "object", 1443 Properties: map[string]clusterv1.JSONSchemaProps{ 1444 "test": { 1445 Type: "object", 1446 Properties: map[string]clusterv1.JSONSchemaProps{ 1447 "knownProperty": { 1448 Type: "boolean", 1449 }, 1450 }, 1451 }, 1452 }, 1453 }, 1454 }, 1455 }, 1456 clusterVariable: &clusterv1.ClusterVariable{ 1457 Name: "testObject", 1458 Value: apiextensionsv1.JSON{ 1459 // unknownProperty is not defined in the schema. 1460 Raw: []byte(`{"test": {"knownProperty":false,"unknownProperty":true}}`), 1461 }, 1462 }, 1463 }, 1464 { 1465 name: "Error if undefined field nested and x-kubernetes-preserve-unknown-fields one level above", 1466 wantErr: true, 1467 clusterClassVariable: &clusterv1.ClusterClassVariable{ 1468 Name: "testObject", 1469 Required: true, 1470 Schema: clusterv1.VariableSchema{ 1471 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 1472 Type: "object", 1473 Properties: map[string]clusterv1.JSONSchemaProps{ 1474 "test": { 1475 Type: "object", 1476 Properties: map[string]clusterv1.JSONSchemaProps{ 1477 "knownProperty": { 1478 Type: "boolean", 1479 }, 1480 }, 1481 }, 1482 }, 1483 // Preserves only on the current level as nested Properties are defined. 1484 XPreserveUnknownFields: true, 1485 }, 1486 }, 1487 }, 1488 clusterVariable: &clusterv1.ClusterVariable{ 1489 Name: "testObject", 1490 Value: apiextensionsv1.JSON{ 1491 Raw: []byte(`{"test": {"knownProperty":false,"unknownProperty":true}}`), 1492 }, 1493 }, 1494 }, 1495 { 1496 name: "Valid object with mid-level unknown fields", 1497 clusterClassVariable: &clusterv1.ClusterClassVariable{ 1498 Name: "testObject", 1499 Required: true, 1500 Schema: clusterv1.VariableSchema{ 1501 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 1502 Type: "object", 1503 Properties: map[string]clusterv1.JSONSchemaProps{ 1504 "test": { 1505 Type: "object", 1506 Properties: map[string]clusterv1.JSONSchemaProps{ 1507 "knownProperty": { 1508 Type: "boolean", 1509 }, 1510 }, 1511 }, 1512 }, 1513 // Preserves only on the current level as nested Properties are defined. 1514 XPreserveUnknownFields: true, 1515 }, 1516 }, 1517 }, 1518 clusterVariable: &clusterv1.ClusterVariable{ 1519 Name: "testObject", 1520 Value: apiextensionsv1.JSON{ 1521 Raw: []byte(`{"test": {"knownProperty":false},"unknownProperty":true}`), 1522 }, 1523 }, 1524 }, 1525 } 1526 for _, tt := range tests { 1527 t.Run(tt.name, func(t *testing.T) { 1528 g := NewWithT(t) 1529 1530 errList := ValidateClusterVariable(tt.clusterVariable, tt.clusterClassVariable, 1531 field.NewPath("spec", "topology", "variables")) 1532 1533 if tt.wantErr { 1534 g.Expect(errList).NotTo(BeEmpty()) 1535 return 1536 } 1537 g.Expect(errList).To(BeEmpty()) 1538 }) 1539 } 1540 }