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