sigs.k8s.io/cluster-api@v1.6.3/internal/webhooks/cluster_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 webhooks 18 19 import ( 20 "context" 21 "testing" 22 23 . "github.com/onsi/gomega" 24 "github.com/pkg/errors" 25 corev1 "k8s.io/api/core/v1" 26 apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 27 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 28 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 29 utilfeature "k8s.io/component-base/featuregate/testing" 30 "k8s.io/utils/pointer" 31 "sigs.k8s.io/controller-runtime/pkg/client" 32 "sigs.k8s.io/controller-runtime/pkg/client/fake" 33 "sigs.k8s.io/controller-runtime/pkg/client/interceptor" 34 35 clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" 36 "sigs.k8s.io/cluster-api/feature" 37 "sigs.k8s.io/cluster-api/internal/test/builder" 38 "sigs.k8s.io/cluster-api/internal/webhooks/util" 39 "sigs.k8s.io/cluster-api/util/conditions" 40 ) 41 42 func TestClusterDefaultNamespaces(t *testing.T) { 43 g := NewWithT(t) 44 45 c := &clusterv1.Cluster{ 46 ObjectMeta: metav1.ObjectMeta{ 47 Namespace: "fooboo", 48 }, 49 Spec: clusterv1.ClusterSpec{ 50 InfrastructureRef: &corev1.ObjectReference{}, 51 ControlPlaneRef: &corev1.ObjectReference{}, 52 }, 53 } 54 webhook := &Cluster{} 55 t.Run("for Cluster", util.CustomDefaultValidateTest(ctx, c, webhook)) 56 57 g.Expect(webhook.Default(ctx, c)).To(Succeed()) 58 59 g.Expect(c.Spec.InfrastructureRef.Namespace).To(Equal(c.Namespace)) 60 g.Expect(c.Spec.ControlPlaneRef.Namespace).To(Equal(c.Namespace)) 61 } 62 63 // TestClusterDefaultAndValidateVariables cases where cluster.spec.topology.class is altered. 64 func TestClusterDefaultAndValidateVariables(t *testing.T) { 65 defer utilfeature.SetFeatureGateDuringTest(t, feature.Gates, feature.ClusterTopology, true)() 66 67 tests := []struct { 68 name string 69 clusterClass *clusterv1.ClusterClass 70 topology *clusterv1.Topology 71 expect *clusterv1.Topology 72 wantErr bool 73 }{ 74 { 75 name: "default a single variable to its correct values", 76 clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). 77 WithStatusVariables(clusterv1.ClusterClassStatusVariable{ 78 Name: "location", 79 Definitions: []clusterv1.ClusterClassStatusVariableDefinition{ 80 { 81 Required: true, 82 From: clusterv1.VariableDefinitionFromInline, 83 Schema: clusterv1.VariableSchema{ 84 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 85 Type: "string", 86 Default: &apiextensionsv1.JSON{Raw: []byte(`"us-east"`)}, 87 }, 88 }, 89 }, 90 }, 91 }, 92 ). 93 Build(), 94 topology: &clusterv1.Topology{}, 95 expect: &clusterv1.Topology{ 96 Variables: []clusterv1.ClusterVariable{ 97 { 98 Name: "location", 99 Value: apiextensionsv1.JSON{ 100 Raw: []byte(`"us-east"`), 101 }, 102 }, 103 }, 104 }, 105 }, 106 { 107 name: "don't change a variable if it is already set", 108 clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). 109 WithStatusVariables(clusterv1.ClusterClassStatusVariable{ 110 Name: "location", 111 Definitions: []clusterv1.ClusterClassStatusVariableDefinition{ 112 { 113 Required: true, 114 From: clusterv1.VariableDefinitionFromInline, 115 Schema: clusterv1.VariableSchema{ 116 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 117 Type: "string", 118 Default: &apiextensionsv1.JSON{Raw: []byte(`"us-east"`)}, 119 }, 120 }, 121 }, 122 }, 123 }, 124 ). 125 Build(), 126 topology: &clusterv1.Topology{ 127 Variables: []clusterv1.ClusterVariable{ 128 { 129 Name: "location", 130 Value: apiextensionsv1.JSON{ 131 Raw: []byte(`"A different location"`), 132 }, 133 }, 134 }, 135 }, 136 expect: &clusterv1.Topology{ 137 Variables: []clusterv1.ClusterVariable{ 138 { 139 Name: "location", 140 Value: apiextensionsv1.JSON{ 141 Raw: []byte(`"A different location"`), 142 }, 143 }, 144 }, 145 }, 146 }, 147 { 148 name: "default many variables to their correct values", 149 clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). 150 WithStatusVariables([]clusterv1.ClusterClassStatusVariable{ 151 { 152 Name: "location", 153 Definitions: []clusterv1.ClusterClassStatusVariableDefinition{ 154 { 155 Required: true, 156 From: clusterv1.VariableDefinitionFromInline, 157 Schema: clusterv1.VariableSchema{ 158 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 159 Type: "string", 160 Default: &apiextensionsv1.JSON{Raw: []byte(`"us-east"`)}, 161 }, 162 }, 163 }, 164 }, 165 }, 166 { 167 Name: "count", 168 Definitions: []clusterv1.ClusterClassStatusVariableDefinition{ 169 { 170 Required: true, 171 From: clusterv1.VariableDefinitionFromInline, 172 Schema: clusterv1.VariableSchema{ 173 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 174 Type: "number", 175 Default: &apiextensionsv1.JSON{Raw: []byte(`0.1`)}, 176 }, 177 }, 178 }, 179 }, 180 }, 181 }...). 182 Build(), 183 topology: &clusterv1.Topology{}, 184 expect: &clusterv1.Topology{ 185 Variables: []clusterv1.ClusterVariable{ 186 { 187 Name: "location", 188 Value: apiextensionsv1.JSON{ 189 Raw: []byte(`"us-east"`), 190 }, 191 }, 192 { 193 Name: "count", 194 Value: apiextensionsv1.JSON{ 195 Raw: []byte(`0.1`), 196 }, 197 }, 198 }, 199 }, 200 }, 201 { 202 name: "don't add new variable overrides", 203 clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). 204 WithWorkerMachineDeploymentClasses( 205 *builder.MachineDeploymentClass("default-worker").Build(), 206 ). 207 WithStatusVariables(clusterv1.ClusterClassStatusVariable{ 208 Name: "location", 209 Definitions: []clusterv1.ClusterClassStatusVariableDefinition{ 210 { 211 Required: true, 212 From: clusterv1.VariableDefinitionFromInline, 213 Schema: clusterv1.VariableSchema{ 214 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 215 Type: "string", 216 Default: &apiextensionsv1.JSON{Raw: []byte(`"us-east"`)}, 217 }, 218 }, 219 }, 220 }, 221 }). 222 Build(), 223 topology: &clusterv1.Topology{ 224 Workers: &clusterv1.WorkersTopology{ 225 MachineDeployments: []clusterv1.MachineDeploymentTopology{ 226 { 227 Class: "default-worker", 228 Name: "md-1", 229 }, 230 }, 231 }, 232 }, 233 expect: &clusterv1.Topology{ 234 Workers: &clusterv1.WorkersTopology{ 235 MachineDeployments: []clusterv1.MachineDeploymentTopology{ 236 { 237 Class: "default-worker", 238 Name: "md-1", 239 // "location" has not been added to .variables.overrides. 240 }, 241 }, 242 }, 243 Variables: []clusterv1.ClusterVariable{ 244 { 245 Name: "location", 246 Value: apiextensionsv1.JSON{ 247 Raw: []byte(`"us-east"`), 248 }, 249 }, 250 }, 251 }, 252 }, 253 { 254 name: "default nested fields of variable overrides", 255 clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). 256 WithWorkerMachineDeploymentClasses( 257 *builder.MachineDeploymentClass("default-worker").Build(), 258 ). 259 WithStatusVariables(clusterv1.ClusterClassStatusVariable{ 260 Name: "httpProxy", 261 Definitions: []clusterv1.ClusterClassStatusVariableDefinition{ 262 { 263 Required: true, 264 From: clusterv1.VariableDefinitionFromInline, 265 Schema: clusterv1.VariableSchema{ 266 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 267 Type: "object", 268 Properties: map[string]clusterv1.JSONSchemaProps{ 269 "enabled": { 270 Type: "boolean", 271 }, 272 "url": { 273 Type: "string", 274 Default: &apiextensionsv1.JSON{Raw: []byte(`"http://localhost:3128"`)}, 275 }, 276 }, 277 }, 278 }, 279 }, 280 }, 281 }). 282 Build(), 283 topology: &clusterv1.Topology{ 284 Workers: &clusterv1.WorkersTopology{ 285 MachineDeployments: []clusterv1.MachineDeploymentTopology{ 286 { 287 Class: "default-worker", 288 Name: "md-1", 289 Variables: &clusterv1.MachineDeploymentVariables{ 290 Overrides: []clusterv1.ClusterVariable{ 291 { 292 Name: "httpProxy", 293 Value: apiextensionsv1.JSON{Raw: []byte(`{"enabled":true}`)}, 294 }, 295 }, 296 }, 297 }, 298 }, 299 }, 300 Variables: []clusterv1.ClusterVariable{ 301 { 302 Name: "httpProxy", 303 Value: apiextensionsv1.JSON{Raw: []byte(`{"enabled":true}`)}, 304 }, 305 }, 306 }, 307 expect: &clusterv1.Topology{ 308 Workers: &clusterv1.WorkersTopology{ 309 MachineDeployments: []clusterv1.MachineDeploymentTopology{ 310 { 311 Class: "default-worker", 312 Name: "md-1", 313 Variables: &clusterv1.MachineDeploymentVariables{ 314 Overrides: []clusterv1.ClusterVariable{ 315 { 316 Name: "httpProxy", 317 // url has been added by defaulting. 318 Value: apiextensionsv1.JSON{Raw: []byte(`{"enabled":true,"url":"http://localhost:3128"}`)}, 319 }, 320 }, 321 }, 322 }, 323 }, 324 }, 325 Variables: []clusterv1.ClusterVariable{ 326 { 327 Name: "httpProxy", 328 Value: apiextensionsv1.JSON{ 329 // url has been added by defaulting. 330 Raw: []byte(`{"enabled":true,"url":"http://localhost:3128"}`), 331 }, 332 }, 333 }, 334 }, 335 }, 336 { 337 name: "Use one value for multiple definitions when variables don't conflict", 338 clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). 339 WithStatusVariables(clusterv1.ClusterClassStatusVariable{ 340 Name: "location", 341 Definitions: []clusterv1.ClusterClassStatusVariableDefinition{ 342 { 343 Required: true, 344 From: clusterv1.VariableDefinitionFromInline, 345 Schema: clusterv1.VariableSchema{ 346 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 347 Type: "string", 348 Default: &apiextensionsv1.JSON{Raw: []byte(`"us-east"`)}, 349 }, 350 }, 351 }, 352 { 353 Required: true, 354 From: "somepatch", 355 Schema: clusterv1.VariableSchema{ 356 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 357 Type: "string", 358 Default: &apiextensionsv1.JSON{Raw: []byte(`"us-east"`)}, 359 }, 360 }, 361 }, 362 { 363 Required: true, 364 From: "anotherpatch", 365 Schema: clusterv1.VariableSchema{ 366 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 367 Type: "string", 368 Default: &apiextensionsv1.JSON{Raw: []byte(`"us-east"`)}, 369 }, 370 }, 371 }, 372 }, 373 }, 374 ). 375 Build(), 376 topology: &clusterv1.Topology{}, 377 expect: &clusterv1.Topology{ 378 Variables: []clusterv1.ClusterVariable{ 379 { 380 Name: "location", 381 Value: apiextensionsv1.JSON{ 382 Raw: []byte(`"us-east"`), 383 }, 384 }, 385 }, 386 }, 387 }, 388 { 389 name: "Add defaults for each definitionFrom if variable is defined for some definitionFrom", 390 clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). 391 WithStatusVariables(clusterv1.ClusterClassStatusVariable{ 392 Name: "location", 393 Definitions: []clusterv1.ClusterClassStatusVariableDefinition{ 394 { 395 Required: true, 396 From: clusterv1.VariableDefinitionFromInline, 397 Schema: clusterv1.VariableSchema{ 398 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 399 Type: "string", 400 Default: &apiextensionsv1.JSON{Raw: []byte(`"us-east"`)}, 401 }, 402 }, 403 }, 404 { 405 Required: true, 406 From: "somepatch", 407 Schema: clusterv1.VariableSchema{ 408 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 409 Type: "string", 410 Default: &apiextensionsv1.JSON{Raw: []byte(`"us-east"`)}, 411 }, 412 }, 413 }, 414 { 415 Required: true, 416 From: "anotherpatch", 417 Schema: clusterv1.VariableSchema{ 418 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 419 Type: "string", 420 Default: &apiextensionsv1.JSON{Raw: []byte(`"us-east"`)}, 421 }, 422 }, 423 }, 424 }, 425 }, 426 ). 427 Build(), 428 topology: &clusterv1.Topology{ 429 Variables: []clusterv1.ClusterVariable{ 430 { 431 Name: "location", 432 Value: apiextensionsv1.JSON{ 433 Raw: []byte(`"us-west"`), 434 }, 435 DefinitionFrom: "somepatch", 436 }, 437 }, 438 }, 439 expect: &clusterv1.Topology{ 440 Variables: []clusterv1.ClusterVariable{ 441 { 442 Name: "location", 443 Value: apiextensionsv1.JSON{ 444 Raw: []byte(`"us-west"`), 445 }, 446 DefinitionFrom: "somepatch", 447 }, 448 { 449 Name: "location", 450 Value: apiextensionsv1.JSON{ 451 Raw: []byte(`"us-east"`), 452 }, 453 DefinitionFrom: clusterv1.VariableDefinitionFromInline, 454 }, 455 { 456 Name: "location", 457 Value: apiextensionsv1.JSON{ 458 Raw: []byte(`"us-east"`), 459 }, 460 DefinitionFrom: "anotherpatch", 461 }, 462 }, 463 }, 464 }, 465 { 466 name: "set definitionFrom on defaults when variables conflict", 467 clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). 468 WithStatusVariables(clusterv1.ClusterClassStatusVariable{ 469 Name: "location", 470 DefinitionsConflict: true, 471 Definitions: []clusterv1.ClusterClassStatusVariableDefinition{ 472 { 473 Required: true, 474 From: clusterv1.VariableDefinitionFromInline, 475 Schema: clusterv1.VariableSchema{ 476 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 477 Type: "string", 478 Default: &apiextensionsv1.JSON{Raw: []byte(`"first-region"`)}, 479 }, 480 }, 481 }, 482 { 483 Required: true, 484 From: "somepatch", 485 Schema: clusterv1.VariableSchema{ 486 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 487 Type: "string", 488 Default: &apiextensionsv1.JSON{Raw: []byte(`"another-region"`)}, 489 }, 490 }, 491 }, 492 { 493 Required: true, 494 From: "anotherpatch", 495 Schema: clusterv1.VariableSchema{ 496 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 497 Type: "string", 498 Default: &apiextensionsv1.JSON{Raw: []byte(`"us-east"`)}, 499 }, 500 }, 501 }, 502 }, 503 }, 504 ). 505 Build(), 506 topology: &clusterv1.Topology{}, 507 expect: &clusterv1.Topology{ 508 Variables: []clusterv1.ClusterVariable{ 509 { 510 Name: "location", 511 Value: apiextensionsv1.JSON{ 512 Raw: []byte(`"first-region"`), 513 }, 514 DefinitionFrom: clusterv1.VariableDefinitionFromInline, 515 }, 516 { 517 Name: "location", 518 Value: apiextensionsv1.JSON{ 519 Raw: []byte(`"another-region"`), 520 }, 521 DefinitionFrom: "somepatch", 522 }, 523 { 524 Name: "location", 525 Value: apiextensionsv1.JSON{ 526 Raw: []byte(`"us-east"`), 527 }, 528 DefinitionFrom: "anotherpatch", 529 }, 530 }, 531 }, 532 }, 533 // Testing validation of variables. 534 { 535 name: "should fail when required variable is missing top-level", 536 clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). 537 WithStatusVariables(clusterv1.ClusterClassStatusVariable{ 538 Name: "cpu", 539 Definitions: []clusterv1.ClusterClassStatusVariableDefinition{ 540 { 541 Required: true, 542 From: clusterv1.VariableDefinitionFromInline, 543 Schema: clusterv1.VariableSchema{ 544 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 545 Type: "integer", 546 }, 547 }, 548 }, 549 }, 550 }).Build(), 551 topology: builder.ClusterTopology().Build(), 552 expect: builder.ClusterTopology().Build(), 553 wantErr: true, 554 }, 555 { 556 name: "should fail when top-level variable is invalid", 557 clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). 558 WithStatusVariables(clusterv1.ClusterClassStatusVariable{ 559 Name: "cpu", 560 Definitions: []clusterv1.ClusterClassStatusVariableDefinition{ 561 { 562 Required: true, 563 From: clusterv1.VariableDefinitionFromInline, 564 Schema: clusterv1.VariableSchema{ 565 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 566 Type: "integer", 567 }, 568 }, 569 }, 570 }}, 571 ).Build(), 572 topology: builder.ClusterTopology(). 573 WithVariables(clusterv1.ClusterVariable{ 574 Name: "cpu", 575 Value: apiextensionsv1.JSON{Raw: []byte(`"text"`)}, 576 }). 577 Build(), 578 expect: builder.ClusterTopology().Build(), 579 wantErr: true, 580 }, 581 { 582 name: "should fail when variable override is invalid", 583 clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). 584 WithStatusVariables(clusterv1.ClusterClassStatusVariable{ 585 Name: "cpu", 586 Definitions: []clusterv1.ClusterClassStatusVariableDefinition{ 587 { 588 Required: true, 589 From: clusterv1.VariableDefinitionFromInline, 590 Schema: clusterv1.VariableSchema{ 591 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 592 Type: "integer", 593 }, 594 }, 595 }, 596 }}).Build(), 597 topology: builder.ClusterTopology(). 598 WithVariables(clusterv1.ClusterVariable{ 599 Name: "cpu", 600 Value: apiextensionsv1.JSON{Raw: []byte(`2`)}, 601 }). 602 WithMachineDeployment(builder.MachineDeploymentTopology("workers1"). 603 WithClass("aa"). 604 WithVariables(clusterv1.ClusterVariable{ 605 Name: "cpu", 606 Value: apiextensionsv1.JSON{Raw: []byte(`"text"`)}, 607 }). 608 Build()). 609 Build(), 610 expect: builder.ClusterTopology().Build(), 611 wantErr: true, 612 }, 613 { 614 name: "should pass when required variable exists top-level", 615 clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). 616 WithStatusVariables(clusterv1.ClusterClassStatusVariable{ 617 Name: "cpu", 618 Definitions: []clusterv1.ClusterClassStatusVariableDefinition{ 619 { 620 Required: true, 621 From: clusterv1.VariableDefinitionFromInline, 622 Schema: clusterv1.VariableSchema{ 623 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 624 Type: "integer", 625 }, 626 }, 627 }, 628 }}).Build(), 629 topology: builder.ClusterTopology(). 630 WithClass("foo"). 631 WithVersion("v1.19.1"). 632 WithVariables(clusterv1.ClusterVariable{ 633 Name: "cpu", 634 Value: apiextensionsv1.JSON{Raw: []byte(`2`)}, 635 }). 636 // Variable is not required in MachineDeployment topologies. 637 Build(), 638 expect: builder.ClusterTopology(). 639 WithClass("foo"). 640 WithVersion("v1.19.1"). 641 WithVariables(clusterv1.ClusterVariable{ 642 Name: "cpu", 643 Value: apiextensionsv1.JSON{Raw: []byte(`2`)}, 644 }). 645 // Variable is not required in MachineDeployment topologies. 646 Build(), 647 }, 648 { 649 name: "should pass when top-level variable and override are valid", 650 clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). 651 WithWorkerMachineDeploymentClasses(*builder.MachineDeploymentClass("md1").Build()). 652 WithStatusVariables(clusterv1.ClusterClassStatusVariable{ 653 Name: "cpu", 654 Definitions: []clusterv1.ClusterClassStatusVariableDefinition{ 655 { 656 Required: true, 657 From: clusterv1.VariableDefinitionFromInline, 658 Schema: clusterv1.VariableSchema{ 659 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 660 Type: "integer", 661 }, 662 }, 663 }, 664 }}).Build(), 665 topology: builder.ClusterTopology(). 666 WithClass("foo"). 667 WithVersion("v1.19.1"). 668 WithVariables(clusterv1.ClusterVariable{ 669 Name: "cpu", 670 Value: apiextensionsv1.JSON{Raw: []byte(`2`)}, 671 }). 672 WithMachineDeployment(builder.MachineDeploymentTopology("workers1"). 673 WithClass("md1"). 674 WithVariables(clusterv1.ClusterVariable{ 675 Name: "cpu", 676 Value: apiextensionsv1.JSON{Raw: []byte(`2`)}, 677 }). 678 Build()). 679 Build(), 680 expect: builder.ClusterTopology(). 681 WithClass("foo"). 682 WithVersion("v1.19.1"). 683 WithVariables(clusterv1.ClusterVariable{ 684 Name: "cpu", 685 Value: apiextensionsv1.JSON{Raw: []byte(`2`)}, 686 }). 687 WithMachineDeployment(builder.MachineDeploymentTopology("workers1"). 688 WithClass("md1"). 689 WithVariables(clusterv1.ClusterVariable{ 690 Name: "cpu", 691 Value: apiextensionsv1.JSON{Raw: []byte(`2`)}, 692 }). 693 Build()). 694 Build(), 695 }, 696 { 697 name: "should pass even when variable override is missing the corresponding top-level variable", 698 clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). 699 WithWorkerMachineDeploymentClasses(*builder.MachineDeploymentClass("md1").Build()). 700 WithStatusVariables(clusterv1.ClusterClassStatusVariable{ 701 Name: "cpu", 702 Definitions: []clusterv1.ClusterClassStatusVariableDefinition{ 703 { 704 Required: false, 705 From: clusterv1.VariableDefinitionFromInline, 706 Schema: clusterv1.VariableSchema{ 707 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 708 Type: "integer", 709 }, 710 }, 711 }, 712 }}).Build(), 713 topology: builder.ClusterTopology(). 714 WithClass("foo"). 715 WithVersion("v1.19.1"). 716 WithMachineDeployment(builder.MachineDeploymentTopology("workers1"). 717 WithClass("md1"). 718 WithVariables(clusterv1.ClusterVariable{ 719 Name: "cpu", 720 Value: apiextensionsv1.JSON{Raw: []byte(`2`)}, 721 }). 722 Build()). 723 Build(), 724 expect: builder.ClusterTopology(). 725 WithClass("foo"). 726 WithVersion("v1.19.1"). 727 WithVariables([]clusterv1.ClusterVariable{}...). 728 WithMachineDeployment(builder.MachineDeploymentTopology("workers1"). 729 WithClass("md1"). 730 WithVariables(clusterv1.ClusterVariable{ 731 Name: "cpu", 732 Value: apiextensionsv1.JSON{Raw: []byte(`2`)}, 733 }). 734 Build()). 735 Build(), 736 }, 737 } 738 for _, tt := range tests { 739 t.Run(tt.name, func(t *testing.T) { 740 // Setting Class and Version here to avoid obfuscating the test cases above. 741 tt.topology.Class = "class1" 742 tt.topology.Version = "v1.22.2" 743 tt.expect.Class = "class1" 744 tt.expect.Version = "v1.22.2" 745 746 cluster := builder.Cluster(metav1.NamespaceDefault, "cluster1"). 747 WithTopology(tt.topology). 748 Build() 749 750 // Mark this condition to true so the webhook sees the ClusterClass as up to date. 751 conditions.MarkTrue(tt.clusterClass, clusterv1.ClusterClassVariablesReconciledCondition) 752 fakeClient := fake.NewClientBuilder(). 753 WithObjects(tt.clusterClass). 754 WithScheme(fakeScheme). 755 Build() 756 // Create the webhook and add the fakeClient as its client. This is required because the test uses a Managed Topology. 757 webhook := &Cluster{Client: fakeClient} 758 759 // Test defaulting. 760 t.Run("default", func(t *testing.T) { 761 g := NewWithT(t) 762 if tt.wantErr { 763 g.Expect(webhook.Default(ctx, cluster)).To(Not(Succeed())) 764 return 765 } 766 g.Expect(webhook.Default(ctx, cluster)).To(Succeed()) 767 g.Expect(cluster.Spec.Topology).To(BeEquivalentTo(tt.expect)) 768 }) 769 770 // Test if defaulting works in combination with validation. 771 // Note this test is not run for the case where the webhook should fail. 772 if tt.wantErr { 773 t.Skip("skipping test for combination of defaulting and validation (not supported by the test)") 774 } 775 util.CustomDefaultValidateTest(ctx, cluster, webhook)(t) 776 }) 777 } 778 } 779 780 func TestClusterDefaultTopologyVersion(t *testing.T) { 781 // NOTE: ClusterTopology feature flag is disabled by default, thus preventing to set Cluster.Topologies. 782 // Enabling the feature flag temporarily for this test. 783 defer utilfeature.SetFeatureGateDuringTest(t, feature.Gates, feature.ClusterTopology, true)() 784 785 g := NewWithT(t) 786 787 c := builder.Cluster("fooboo", "cluster1"). 788 WithTopology(builder.ClusterTopology(). 789 WithClass("foo"). 790 WithVersion("1.19.1"). 791 Build()). 792 Build() 793 794 clusterClass := builder.ClusterClass("fooboo", "foo").Build() 795 conditions.MarkTrue(clusterClass, clusterv1.ClusterClassVariablesReconciledCondition) 796 // Sets up the fakeClient for the test case. This is required because the test uses a Managed Topology. 797 fakeClient := fake.NewClientBuilder(). 798 WithObjects(clusterClass). 799 WithScheme(fakeScheme). 800 Build() 801 802 // Create the webhook and add the fakeClient as its client. 803 webhook := &Cluster{Client: fakeClient} 804 t.Run("for Cluster", util.CustomDefaultValidateTest(ctx, c, webhook)) 805 806 g.Expect(webhook.Default(ctx, c)).To(Succeed()) 807 808 g.Expect(c.Spec.Topology.Version).To(HavePrefix("v")) 809 } 810 811 func TestClusterValidation(t *testing.T) { 812 // NOTE: ClusterTopology feature flag is disabled by default, thus preventing to set Cluster.Topologies. 813 814 var ( 815 tests = []struct { 816 name string 817 in *clusterv1.Cluster 818 old *clusterv1.Cluster 819 expectErr bool 820 }{ 821 { 822 name: "should return error when cluster namespace and infrastructure ref namespace mismatch", 823 expectErr: true, 824 in: builder.Cluster("fooNamespace", "cluster1"). 825 WithInfrastructureCluster( 826 builder.InfrastructureClusterTemplate("barNamespace", "infra1").Build()). 827 WithControlPlane( 828 builder.ControlPlane("fooNamespace", "cp1").Build()). 829 Build(), 830 }, 831 { 832 name: "should return error when cluster namespace and controlPlane ref namespace mismatch", 833 expectErr: true, 834 in: builder.Cluster("fooNamespace", "cluster1"). 835 WithInfrastructureCluster( 836 builder.InfrastructureClusterTemplate("fooNamespace", "infra1").Build()). 837 WithControlPlane( 838 builder.ControlPlane("barNamespace", "cp1").Build()). 839 Build(), 840 }, 841 { 842 name: "should succeed when namespaces match", 843 expectErr: false, 844 in: builder.Cluster("fooNamespace", "cluster1"). 845 WithInfrastructureCluster( 846 builder.InfrastructureClusterTemplate("fooNamespace", "infra1").Build()). 847 WithControlPlane( 848 builder.ControlPlane("fooNamespace", "cp1").Build()). 849 Build(), 850 }, 851 { 852 name: "fails if topology is set but feature flag is disabled", 853 expectErr: true, 854 in: builder.Cluster("fooNamespace", "cluster1"). 855 WithInfrastructureCluster( 856 builder.InfrastructureClusterTemplate("fooNamespace", "infra1").Build()). 857 WithControlPlane( 858 builder.ControlPlane("fooNamespace", "cp1").Build()). 859 WithTopology(&clusterv1.Topology{}). 860 Build(), 861 }, 862 { 863 name: "pass with undefined CIDR ranges", 864 expectErr: false, 865 in: builder.Cluster("fooNamespace", "cluster1"). 866 WithClusterNetwork(&clusterv1.ClusterNetwork{ 867 Services: &clusterv1.NetworkRanges{ 868 CIDRBlocks: []string{}}, 869 Pods: &clusterv1.NetworkRanges{ 870 CIDRBlocks: []string{}}, 871 }). 872 Build(), 873 }, 874 { 875 name: "pass with nil CIDR ranges", 876 expectErr: false, 877 in: builder.Cluster("fooNamespace", "cluster1"). 878 WithClusterNetwork(&clusterv1.ClusterNetwork{ 879 Services: &clusterv1.NetworkRanges{ 880 CIDRBlocks: nil}, 881 Pods: &clusterv1.NetworkRanges{ 882 CIDRBlocks: nil}, 883 }). 884 Build(), 885 }, 886 { 887 name: "pass with valid IPv4 CIDR ranges", 888 expectErr: false, 889 in: builder.Cluster("fooNamespace", "cluster1"). 890 WithClusterNetwork(&clusterv1.ClusterNetwork{ 891 Services: &clusterv1.NetworkRanges{ 892 CIDRBlocks: []string{"10.10.10.10/24"}}, 893 Pods: &clusterv1.NetworkRanges{ 894 CIDRBlocks: []string{"10.10.10.10/24"}}, 895 }). 896 Build(), 897 }, 898 { 899 name: "pass with valid IPv6 CIDR ranges", 900 expectErr: false, 901 in: builder.Cluster("fooNamespace", "cluster1"). 902 WithClusterNetwork(&clusterv1.ClusterNetwork{ 903 Services: &clusterv1.NetworkRanges{ 904 CIDRBlocks: []string{"2004::1234:abcd:ffff:c0a8:101/64"}}, 905 Pods: &clusterv1.NetworkRanges{ 906 CIDRBlocks: []string{"2004::1234:abcd:ffff:c0a8:101/64"}}, 907 }). 908 Build(), 909 }, 910 { 911 name: "pass with valid dualstack CIDR ranges", 912 expectErr: false, 913 in: builder.Cluster("fooNamespace", "cluster1"). 914 WithClusterNetwork(&clusterv1.ClusterNetwork{ 915 Services: &clusterv1.NetworkRanges{ 916 CIDRBlocks: []string{"2004::1234:abcd:ffff:c0a8:101/64", "10.10.10.10/24"}}, 917 Pods: &clusterv1.NetworkRanges{ 918 CIDRBlocks: []string{"2004::1234:abcd:ffff:c0a8:101/64", "10.10.10.10/24"}}, 919 }). 920 Build(), 921 }, 922 { 923 name: "pass if multiple CIDR ranges of IPv4 are passed", 924 expectErr: false, 925 in: builder.Cluster("fooNamespace", "cluster1"). 926 WithClusterNetwork(&clusterv1.ClusterNetwork{ 927 Services: &clusterv1.NetworkRanges{ 928 CIDRBlocks: []string{"10.10.10.10/24", "11.11.11.11/24"}}, 929 }). 930 Build(), 931 }, 932 { 933 name: "pass if multiple CIDR ranges of IPv6 are passed", 934 expectErr: false, 935 in: builder.Cluster("fooNamespace", "cluster1"). 936 WithClusterNetwork(&clusterv1.ClusterNetwork{ 937 Services: &clusterv1.NetworkRanges{ 938 CIDRBlocks: []string{"2002::1234:abcd:ffff:c0a8:101/64", "2004::1234:abcd:ffff:c0a8:101/64"}}, 939 }). 940 Build(), 941 }, 942 { 943 name: "pass if too many cidr ranges are specified in the clusterNetwork pods field", 944 expectErr: false, 945 in: builder.Cluster("fooNamespace", "cluster1"). 946 WithClusterNetwork(&clusterv1.ClusterNetwork{ 947 Pods: &clusterv1.NetworkRanges{ 948 CIDRBlocks: []string{"10.10.10.10/24", "11.11.11.11/24", "12.12.12.12/24"}}}). 949 Build(), 950 }, 951 { 952 name: "fails if service cidr ranges are not valid", 953 expectErr: true, 954 in: builder.Cluster("fooNamespace", "cluster1"). 955 WithClusterNetwork(&clusterv1.ClusterNetwork{ 956 Services: &clusterv1.NetworkRanges{ 957 // Invalid ranges: missing network suffix 958 CIDRBlocks: []string{"10.10.10.10", "11.11.11.11"}}}). 959 Build(), 960 }, 961 { 962 name: "fails if pod cidr ranges are not valid", 963 expectErr: true, 964 in: builder.Cluster("fooNamespace", "cluster1"). 965 WithClusterNetwork(&clusterv1.ClusterNetwork{ 966 Pods: &clusterv1.NetworkRanges{ 967 // Invalid ranges: missing network suffix 968 CIDRBlocks: []string{"10.10.10.10", "11.11.11.11"}}}). 969 Build(), 970 }, 971 { 972 name: "pass with name of under 63 characters", 973 expectErr: false, 974 in: builder.Cluster("fooNamespace", "short-name").Build(), 975 }, 976 { 977 name: "pass with _, -, . characters in name", 978 in: builder.Cluster("fooNamespace", "thisNameContains.A_Non-Alphanumeric").Build(), 979 expectErr: false, 980 }, 981 { 982 name: "fails if cluster name is longer than 63 characters", 983 in: builder.Cluster("fooNamespace", "thisNameIsReallyMuchLongerThanTheMaximumLengthOfSixtyThreeCharacters").Build(), 984 expectErr: true, 985 }, 986 { 987 name: "error when name starts with NonAlphanumeric character", 988 in: builder.Cluster("fooNamespace", "-thisNameStartsWithANonAlphanumeric").Build(), 989 expectErr: true, 990 }, 991 { 992 name: "error when name ends with NonAlphanumeric character", 993 in: builder.Cluster("fooNamespace", "thisNameEndsWithANonAlphanumeric.").Build(), 994 expectErr: true, 995 }, 996 { 997 name: "error when name contains invalid NonAlphanumeric character", 998 in: builder.Cluster("fooNamespace", "thisNameContainsInvalid!@NonAlphanumerics").Build(), 999 expectErr: true, 1000 }, 1001 } 1002 ) 1003 for _, tt := range tests { 1004 t.Run(tt.name, func(t *testing.T) { 1005 g := NewWithT(t) 1006 1007 // Create the webhook. 1008 webhook := &Cluster{} 1009 1010 warnings, err := webhook.validate(ctx, tt.old, tt.in) 1011 g.Expect(warnings).To(BeEmpty()) 1012 if tt.expectErr { 1013 g.Expect(err).To(HaveOccurred()) 1014 return 1015 } 1016 g.Expect(err).ToNot(HaveOccurred()) 1017 }) 1018 } 1019 } 1020 1021 func TestClusterTopologyValidation(t *testing.T) { 1022 // NOTE: ClusterTopology feature flag is disabled by default, thus preventing to set Cluster.Topologies. 1023 // Enabling the feature flag temporarily for this test. 1024 defer utilfeature.SetFeatureGateDuringTest(t, feature.Gates, feature.ClusterTopology, true)() 1025 1026 tests := []struct { 1027 name string 1028 clusterClassStatusVariables []clusterv1.ClusterClassStatusVariable 1029 in *clusterv1.Cluster 1030 old *clusterv1.Cluster 1031 expectErr bool 1032 }{ 1033 { 1034 name: "should return error when topology does not have class", 1035 expectErr: true, 1036 in: builder.Cluster("fooboo", "cluster1"). 1037 WithTopology(&clusterv1.Topology{}). 1038 Build(), 1039 }, 1040 { 1041 name: "should return error when topology does not have valid version", 1042 expectErr: true, 1043 in: builder.Cluster("fooboo", "cluster1"). 1044 WithTopology(builder.ClusterTopology(). 1045 WithClass("foo"). 1046 WithVersion("invalid").Build()). 1047 Build(), 1048 }, 1049 { 1050 name: "should return error when downgrading topology version - major", 1051 expectErr: true, 1052 old: builder.Cluster("fooboo", "cluster1"). 1053 WithTopology(builder.ClusterTopology(). 1054 WithClass("foo"). 1055 WithVersion("v2.2.3"). 1056 Build()). 1057 Build(), 1058 in: builder.Cluster("fooboo", "cluster1"). 1059 WithTopology(builder.ClusterTopology(). 1060 WithClass("foo"). 1061 WithVersion("v1.2.3"). 1062 Build()). 1063 Build(), 1064 }, 1065 { 1066 name: "should return error when downgrading topology version - minor", 1067 expectErr: true, 1068 old: builder.Cluster("fooboo", "cluster1"). 1069 WithTopology(builder.ClusterTopology(). 1070 WithClass("foo"). 1071 WithVersion("v1.2.3"). 1072 Build()). 1073 Build(), 1074 in: builder.Cluster("fooboo", "cluster1"). 1075 WithTopology(builder.ClusterTopology(). 1076 WithClass("foo"). 1077 WithVersion("v1.1.3"). 1078 Build()). 1079 Build(), 1080 }, 1081 { 1082 name: "should return error when downgrading topology version - patch", 1083 expectErr: true, 1084 old: builder.Cluster("fooboo", "cluster1"). 1085 WithTopology(builder.ClusterTopology(). 1086 WithClass("foo"). 1087 WithVersion("v1.2.3"). 1088 Build()). 1089 Build(), 1090 in: builder.Cluster("fooboo", "cluster1"). 1091 WithTopology(builder.ClusterTopology(). 1092 WithClass("foo"). 1093 WithVersion("v1.2.2"). 1094 Build()). 1095 Build(), 1096 }, 1097 { 1098 name: "should return error when downgrading topology version - pre-release", 1099 expectErr: true, 1100 old: builder.Cluster("fooboo", "cluster1"). 1101 WithTopology(builder.ClusterTopology(). 1102 WithClass("foo"). 1103 WithVersion("v1.2.3-xyz.2"). 1104 Build()). 1105 Build(), 1106 in: builder.Cluster("fooboo", "cluster1"). 1107 WithTopology(builder.ClusterTopology(). 1108 WithClass("foo"). 1109 WithVersion("v1.2.3-xyz.1"). 1110 Build()). 1111 Build(), 1112 }, 1113 { 1114 name: "should return error when downgrading topology version - build tag", 1115 expectErr: true, 1116 old: builder.Cluster("fooboo", "cluster1"). 1117 WithTopology(builder.ClusterTopology(). 1118 WithClass("foo"). 1119 WithVersion("v1.2.3+xyz.2"). 1120 Build()). 1121 Build(), 1122 in: builder.Cluster("fooboo", "cluster1"). 1123 WithTopology(builder.ClusterTopology(). 1124 WithClass("foo"). 1125 WithVersion("v1.2.3+xyz.1"). 1126 Build()). 1127 Build(), 1128 }, 1129 { 1130 name: "should return error when upgrading +2 minor version", 1131 expectErr: true, 1132 old: builder.Cluster("fooboo", "cluster1"). 1133 WithTopology(builder.ClusterTopology(). 1134 WithClass("foo"). 1135 WithVersion("v1.2.3"). 1136 Build()). 1137 Build(), 1138 in: builder.Cluster("fooboo", "cluster1"). 1139 WithTopology(builder.ClusterTopology(). 1140 WithClass("foo"). 1141 WithVersion("v1.4.0"). 1142 Build()). 1143 Build(), 1144 }, 1145 { 1146 name: "should return error when duplicated MachineDeployments names exists in a Topology", 1147 expectErr: true, 1148 in: builder.Cluster("fooboo", "cluster1"). 1149 WithTopology(builder.ClusterTopology(). 1150 WithClass("foo"). 1151 WithVersion("v1.19.1"). 1152 WithMachineDeployment( 1153 builder.MachineDeploymentTopology("workers1"). 1154 WithClass("aa"). 1155 Build()). 1156 WithMachineDeployment( 1157 builder.MachineDeploymentTopology("workers1"). 1158 WithClass("bb"). 1159 Build()). 1160 Build()). 1161 Build(), 1162 }, 1163 { 1164 name: "should pass when MachineDeployments names in a Topology are unique", 1165 expectErr: false, 1166 in: builder.Cluster("fooboo", "cluster1"). 1167 WithTopology(builder.ClusterTopology(). 1168 WithClass("foo"). 1169 WithVersion("v1.19.1"). 1170 WithMachineDeployment( 1171 builder.MachineDeploymentTopology("workers1"). 1172 WithClass("aa"). 1173 Build()). 1174 WithMachineDeployment( 1175 builder.MachineDeploymentTopology("workers2"). 1176 WithClass("bb"). 1177 Build()). 1178 Build()). 1179 Build(), 1180 }, 1181 { 1182 name: "should update", 1183 expectErr: false, 1184 old: builder.Cluster("fooboo", "cluster1"). 1185 WithTopology(builder.ClusterTopology(). 1186 WithClass("foo"). 1187 WithVersion("v1.19.1"). 1188 WithMachineDeployment( 1189 builder.MachineDeploymentTopology("workers1"). 1190 WithClass("aa"). 1191 Build()). 1192 WithMachineDeployment( 1193 builder.MachineDeploymentTopology("workers2"). 1194 WithClass("bb"). 1195 Build()). 1196 Build()). 1197 Build(), 1198 in: builder.Cluster("fooboo", "cluster1"). 1199 WithTopology(builder.ClusterTopology(). 1200 WithClass("foo"). 1201 WithVersion("v1.19.2"). 1202 WithMachineDeployment( 1203 builder.MachineDeploymentTopology("workers1"). 1204 WithClass("aa"). 1205 Build()). 1206 WithMachineDeployment( 1207 builder.MachineDeploymentTopology("workers2"). 1208 WithClass("bb"). 1209 Build()). 1210 Build()). 1211 Build(), 1212 }, 1213 { 1214 name: "should return error when upgrade concurrency annotation value is < 1", 1215 expectErr: true, 1216 in: builder.Cluster("fooboo", "cluster1"). 1217 WithAnnotations(map[string]string{ 1218 clusterv1.ClusterTopologyUpgradeConcurrencyAnnotation: "-1", 1219 }). 1220 WithTopology(builder.ClusterTopology(). 1221 WithClass("foo"). 1222 WithVersion("v1.19.2"). 1223 Build()). 1224 Build(), 1225 }, 1226 { 1227 name: "should return error when upgrade concurrency annotation value is not numeric", 1228 expectErr: true, 1229 in: builder.Cluster("fooboo", "cluster1"). 1230 WithAnnotations(map[string]string{ 1231 clusterv1.ClusterTopologyUpgradeConcurrencyAnnotation: "abc", 1232 }). 1233 WithTopology(builder.ClusterTopology(). 1234 WithClass("foo"). 1235 WithVersion("v1.19.2"). 1236 Build()). 1237 Build(), 1238 }, 1239 { 1240 name: "should pass upgrade concurrency annotation value is >= 1", 1241 expectErr: false, 1242 in: builder.Cluster("fooboo", "cluster1"). 1243 WithAnnotations(map[string]string{ 1244 clusterv1.ClusterTopologyUpgradeConcurrencyAnnotation: "2", 1245 }). 1246 WithTopology(builder.ClusterTopology(). 1247 WithClass("foo"). 1248 WithVersion("v1.19.2"). 1249 Build()). 1250 Build(), 1251 }, 1252 } 1253 for _, tt := range tests { 1254 t.Run(tt.name, func(t *testing.T) { 1255 g := NewWithT(t) 1256 class := builder.ClusterClass("fooboo", "foo"). 1257 WithWorkerMachineDeploymentClasses( 1258 *builder.MachineDeploymentClass("bb").Build(), 1259 *builder.MachineDeploymentClass("aa").Build(), 1260 ). 1261 WithStatusVariables(tt.clusterClassStatusVariables...). 1262 Build() 1263 1264 // Mark this condition to true so the webhook sees the ClusterClass as up to date. 1265 conditions.MarkTrue(class, clusterv1.ClusterClassVariablesReconciledCondition) 1266 // Sets up the fakeClient for the test case. 1267 fakeClient := fake.NewClientBuilder(). 1268 WithObjects(class). 1269 WithScheme(fakeScheme). 1270 Build() 1271 1272 // Create the webhook and add the fakeClient as its client. This is required because the test uses a Managed Topology. 1273 webhook := &Cluster{Client: fakeClient} 1274 1275 warnings, err := webhook.validate(ctx, tt.old, tt.in) 1276 if tt.expectErr { 1277 g.Expect(err).To(HaveOccurred()) 1278 g.Expect(warnings).To(BeEmpty()) 1279 return 1280 } 1281 g.Expect(err).ToNot(HaveOccurred()) 1282 g.Expect(warnings).To(BeEmpty()) 1283 }) 1284 } 1285 } 1286 1287 // TestClusterTopologyValidationWithClient tests the additional cases introduced in new validation in the webhook package. 1288 func TestClusterTopologyValidationWithClient(t *testing.T) { 1289 defer utilfeature.SetFeatureGateDuringTest(t, feature.Gates, feature.ClusterTopology, true)() 1290 g := NewWithT(t) 1291 1292 tests := []struct { 1293 name string 1294 cluster *clusterv1.Cluster 1295 class *clusterv1.ClusterClass 1296 classReconciled bool 1297 objects []client.Object 1298 wantErr bool 1299 wantWarnings bool 1300 }{ 1301 { 1302 name: "Accept a cluster with an existing ClusterClass named in cluster.spec.topology.class", 1303 cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1"). 1304 WithTopology( 1305 builder.ClusterTopology(). 1306 WithClass("clusterclass"). 1307 WithVersion("v1.22.2"). 1308 WithControlPlaneReplicas(3). 1309 Build()). 1310 Build(), 1311 class: builder.ClusterClass(metav1.NamespaceDefault, "clusterclass"). 1312 Build(), 1313 classReconciled: true, 1314 wantErr: false, 1315 }, 1316 { 1317 name: "Warning for a cluster with non-existent ClusterClass referenced cluster.spec.topology.class", 1318 cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1"). 1319 WithTopology( 1320 builder.ClusterTopology(). 1321 WithClass("wrongName"). 1322 WithVersion("v1.22.2"). 1323 WithControlPlaneReplicas(3). 1324 Build()). 1325 Build(), 1326 class: builder.ClusterClass(metav1.NamespaceDefault, "clusterclass"). 1327 Build(), 1328 // There should be a warning for a ClusterClass which can not be found. 1329 wantWarnings: true, 1330 wantErr: false, 1331 }, 1332 { 1333 name: "Warning for a cluster with an unreconciled ClusterClass named in cluster.spec.topology.class", 1334 cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1"). 1335 WithTopology( 1336 builder.ClusterTopology(). 1337 WithClass("clusterclass"). 1338 WithVersion("v1.22.2"). 1339 WithControlPlaneReplicas(3). 1340 Build()). 1341 Build(), 1342 class: builder.ClusterClass(metav1.NamespaceDefault, "clusterclass"). 1343 Build(), 1344 classReconciled: false, 1345 // There should be a warning for a ClusterClass which is not yet reconciled. 1346 wantWarnings: true, 1347 wantErr: false, 1348 }, 1349 { 1350 name: "Reject a cluster that has MHC enabled for control plane but is missing MHC definition in cluster topology and clusterclass", 1351 cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1"). 1352 WithTopology( 1353 builder.ClusterTopology(). 1354 WithClass("clusterclass"). 1355 WithVersion("v1.22.2"). 1356 WithControlPlaneReplicas(3). 1357 WithControlPlaneMachineHealthCheck(&clusterv1.MachineHealthCheckTopology{ 1358 Enable: pointer.Bool(true), 1359 }). 1360 Build()). 1361 Build(), 1362 class: builder.ClusterClass(metav1.NamespaceDefault, "clusterclass"). 1363 Build(), 1364 classReconciled: true, 1365 wantErr: true, 1366 }, 1367 { 1368 name: "Reject a cluster that MHC override defined for control plane but is missing unhealthy conditions", 1369 cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1"). 1370 WithTopology( 1371 builder.ClusterTopology(). 1372 WithClass("clusterclass"). 1373 WithVersion("v1.22.2"). 1374 WithControlPlaneReplicas(3). 1375 WithControlPlaneMachineHealthCheck(&clusterv1.MachineHealthCheckTopology{ 1376 MachineHealthCheckClass: clusterv1.MachineHealthCheckClass{ 1377 UnhealthyConditions: []clusterv1.UnhealthyCondition{}, 1378 }, 1379 }). 1380 Build()). 1381 Build(), 1382 class: builder.ClusterClass(metav1.NamespaceDefault, "clusterclass"). 1383 Build(), 1384 classReconciled: true, 1385 wantErr: true, 1386 }, 1387 { 1388 name: "Reject a cluster that MHC override defined for control plane but is set when control plane is missing machineInfrastructure", 1389 cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1"). 1390 WithTopology( 1391 builder.ClusterTopology(). 1392 WithClass("clusterclass"). 1393 WithVersion("v1.22.2"). 1394 WithControlPlaneReplicas(3). 1395 WithControlPlaneMachineHealthCheck(&clusterv1.MachineHealthCheckTopology{ 1396 MachineHealthCheckClass: clusterv1.MachineHealthCheckClass{ 1397 UnhealthyConditions: []clusterv1.UnhealthyCondition{ 1398 { 1399 Type: corev1.NodeReady, 1400 Status: corev1.ConditionFalse, 1401 }, 1402 }, 1403 }, 1404 }). 1405 Build()). 1406 Build(), 1407 class: builder.ClusterClass(metav1.NamespaceDefault, "clusterclass"). 1408 Build(), 1409 classReconciled: true, 1410 wantErr: true, 1411 }, 1412 { 1413 name: "Accept a cluster that has MHC enabled for control plane with control plane MHC defined in ClusterClass", 1414 cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1"). 1415 WithTopology( 1416 builder.ClusterTopology(). 1417 WithClass("clusterclass"). 1418 WithVersion("v1.22.2"). 1419 WithControlPlaneReplicas(3). 1420 WithControlPlaneMachineHealthCheck(&clusterv1.MachineHealthCheckTopology{ 1421 Enable: pointer.Bool(true), 1422 }). 1423 Build()). 1424 Build(), 1425 class: builder.ClusterClass(metav1.NamespaceDefault, "clusterclass"). 1426 WithControlPlaneMachineHealthCheck(&clusterv1.MachineHealthCheckClass{}). 1427 Build(), 1428 classReconciled: true, 1429 wantErr: false, 1430 }, 1431 { 1432 name: "Accept a cluster that has MHC enabled for control plane with control plane MHC defined in cluster topology", 1433 cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1"). 1434 WithTopology( 1435 builder.ClusterTopology(). 1436 WithClass("clusterclass"). 1437 WithVersion("v1.22.2"). 1438 WithControlPlaneReplicas(3). 1439 WithControlPlaneMachineHealthCheck(&clusterv1.MachineHealthCheckTopology{ 1440 Enable: pointer.Bool(true), 1441 MachineHealthCheckClass: clusterv1.MachineHealthCheckClass{ 1442 UnhealthyConditions: []clusterv1.UnhealthyCondition{ 1443 { 1444 Type: corev1.NodeReady, 1445 Status: corev1.ConditionFalse, 1446 }, 1447 }, 1448 }, 1449 }). 1450 Build()). 1451 Build(), 1452 class: builder.ClusterClass(metav1.NamespaceDefault, "clusterclass"). 1453 WithControlPlaneInfrastructureMachineTemplate(&unstructured.Unstructured{}). 1454 Build(), 1455 classReconciled: true, 1456 wantErr: false, 1457 }, 1458 { 1459 name: "Reject a cluster that has MHC enabled for machine deployment but is missing MHC definition in cluster topology and ClusterClass", 1460 cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1"). 1461 WithTopology( 1462 builder.ClusterTopology(). 1463 WithClass("clusterclass"). 1464 WithVersion("v1.22.2"). 1465 WithControlPlaneReplicas(3). 1466 WithMachineDeployment( 1467 builder.MachineDeploymentTopology("md1"). 1468 WithClass("worker-class"). 1469 WithMachineHealthCheck(&clusterv1.MachineHealthCheckTopology{ 1470 Enable: pointer.Bool(true), 1471 }). 1472 Build(), 1473 ). 1474 Build()). 1475 Build(), 1476 class: builder.ClusterClass(metav1.NamespaceDefault, "clusterclass"). 1477 WithWorkerMachineDeploymentClasses( 1478 *builder.MachineDeploymentClass("worker-class").Build(), 1479 ). 1480 Build(), 1481 classReconciled: true, 1482 wantErr: true, 1483 }, 1484 { 1485 name: "Reject a cluster that has MHC override defined for machine deployment but is missing unhealthy conditions", 1486 cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1"). 1487 WithTopology( 1488 builder.ClusterTopology(). 1489 WithClass("clusterclass"). 1490 WithVersion("v1.22.2"). 1491 WithControlPlaneReplicas(3). 1492 WithMachineDeployment( 1493 builder.MachineDeploymentTopology("md1"). 1494 WithClass("worker-class"). 1495 WithMachineHealthCheck(&clusterv1.MachineHealthCheckTopology{ 1496 MachineHealthCheckClass: clusterv1.MachineHealthCheckClass{ 1497 UnhealthyConditions: []clusterv1.UnhealthyCondition{}, 1498 }, 1499 }). 1500 Build(), 1501 ). 1502 Build()). 1503 Build(), 1504 class: builder.ClusterClass(metav1.NamespaceDefault, "clusterclass"). 1505 WithWorkerMachineDeploymentClasses( 1506 *builder.MachineDeploymentClass("worker-class").Build(), 1507 ). 1508 Build(), 1509 classReconciled: true, 1510 wantErr: true, 1511 }, 1512 { 1513 name: "Accept a cluster that has MHC enabled for machine deployment with machine deployment MHC defined in ClusterClass", 1514 cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1"). 1515 WithTopology( 1516 builder.ClusterTopology(). 1517 WithClass("clusterclass"). 1518 WithVersion("v1.22.2"). 1519 WithControlPlaneReplicas(3). 1520 WithMachineDeployment( 1521 builder.MachineDeploymentTopology("md1"). 1522 WithClass("worker-class"). 1523 WithMachineHealthCheck(&clusterv1.MachineHealthCheckTopology{ 1524 Enable: pointer.Bool(true), 1525 }). 1526 Build(), 1527 ). 1528 Build()). 1529 Build(), 1530 class: builder.ClusterClass(metav1.NamespaceDefault, "clusterclass"). 1531 WithWorkerMachineDeploymentClasses( 1532 *builder.MachineDeploymentClass("worker-class"). 1533 WithMachineHealthCheckClass(&clusterv1.MachineHealthCheckClass{}). 1534 Build(), 1535 ). 1536 Build(), 1537 classReconciled: true, 1538 wantErr: false, 1539 }, 1540 { 1541 name: "Accept a cluster that has MHC enabled for machine deployment with machine deployment MHC defined in cluster topology", 1542 cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1"). 1543 WithTopology( 1544 builder.ClusterTopology(). 1545 WithClass("clusterclass"). 1546 WithVersion("v1.22.2"). 1547 WithControlPlaneReplicas(3). 1548 WithMachineDeployment( 1549 builder.MachineDeploymentTopology("md1"). 1550 WithClass("worker-class"). 1551 WithMachineHealthCheck(&clusterv1.MachineHealthCheckTopology{ 1552 Enable: pointer.Bool(true), 1553 MachineHealthCheckClass: clusterv1.MachineHealthCheckClass{ 1554 UnhealthyConditions: []clusterv1.UnhealthyCondition{ 1555 { 1556 Type: corev1.NodeReady, 1557 Status: corev1.ConditionFalse, 1558 }, 1559 }, 1560 }, 1561 }). 1562 Build(), 1563 ). 1564 Build()). 1565 Build(), 1566 class: builder.ClusterClass(metav1.NamespaceDefault, "clusterclass"). 1567 WithWorkerMachineDeploymentClasses( 1568 *builder.MachineDeploymentClass("worker-class").Build(), 1569 ). 1570 Build(), 1571 classReconciled: true, 1572 wantErr: false, 1573 }, 1574 } 1575 for _, tt := range tests { 1576 t.Run(tt.name, func(t *testing.T) { 1577 // Mark this condition to true so the webhook sees the ClusterClass as up to date. 1578 if tt.classReconciled { 1579 conditions.MarkTrue(tt.class, clusterv1.ClusterClassVariablesReconciledCondition) 1580 } 1581 // Sets up the fakeClient for the test case. 1582 fakeClient := fake.NewClientBuilder(). 1583 WithObjects(tt.class). 1584 WithScheme(fakeScheme). 1585 Build() 1586 1587 // Create the webhook and add the fakeClient as its client. This is required because the test uses a Managed Topology. 1588 c := &Cluster{Client: fakeClient} 1589 1590 // Checks the return error. 1591 warnings, err := c.ValidateCreate(ctx, tt.cluster) 1592 if tt.wantErr { 1593 g.Expect(err).To(HaveOccurred()) 1594 } else { 1595 g.Expect(err).ToNot(HaveOccurred()) 1596 } 1597 if tt.wantWarnings { 1598 g.Expect(warnings).ToNot(BeEmpty()) 1599 } else { 1600 g.Expect(warnings).To(BeEmpty()) 1601 } 1602 }) 1603 } 1604 } 1605 1606 // TestClusterTopologyValidationForTopologyClassChange cases where cluster.spec.topology.class is altered. 1607 func TestClusterTopologyValidationForTopologyClassChange(t *testing.T) { 1608 defer utilfeature.SetFeatureGateDuringTest(t, feature.Gates, feature.ClusterTopology, true)() 1609 g := NewWithT(t) 1610 1611 cluster := builder.Cluster(metav1.NamespaceDefault, "cluster1"). 1612 WithTopology( 1613 builder.ClusterTopology(). 1614 WithClass("class1"). 1615 WithVersion("v1.22.2"). 1616 WithControlPlaneReplicas(3). 1617 Build()). 1618 Build() 1619 1620 ref := &corev1.ObjectReference{ 1621 APIVersion: "group.test.io/foo", 1622 Kind: "barTemplate", 1623 Name: "baz", 1624 Namespace: "default", 1625 } 1626 compatibleNameChangeRef := &corev1.ObjectReference{ 1627 APIVersion: "group.test.io/foo", 1628 Kind: "barTemplate", 1629 Name: "differentbaz", 1630 Namespace: "default", 1631 } 1632 compatibleAPIVersionChangeRef := &corev1.ObjectReference{ 1633 APIVersion: "group.test.io/foo2", 1634 Kind: "barTemplate", 1635 Name: "differentbaz", 1636 Namespace: "default", 1637 } 1638 incompatibleKindRef := &corev1.ObjectReference{ 1639 APIVersion: "group.test.io/foo", 1640 Kind: "another-barTemplate", 1641 Name: "another-baz", 1642 Namespace: "default", 1643 } 1644 incompatibleAPIGroupRef := &corev1.ObjectReference{ 1645 APIVersion: "group.nottest.io/foo", 1646 Kind: "barTemplate", 1647 Name: "another-baz", 1648 Namespace: "default", 1649 } 1650 1651 tests := []struct { 1652 name string 1653 cluster *clusterv1.Cluster 1654 firstClass *clusterv1.ClusterClass 1655 secondClass *clusterv1.ClusterClass 1656 wantErr bool 1657 }{ 1658 // InfrastructureCluster changes. 1659 { 1660 name: "Accept cluster.topology.class change with a compatible infrastructureCluster Kind ref change", 1661 firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). 1662 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 1663 WithControlPlaneTemplate(refToUnstructured(ref)). 1664 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). 1665 Build(), 1666 secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2"). 1667 WithInfrastructureClusterTemplate(refToUnstructured(compatibleNameChangeRef)). 1668 WithControlPlaneTemplate(refToUnstructured(ref)). 1669 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). 1670 Build(), 1671 wantErr: false, 1672 }, 1673 { 1674 name: "Accept cluster.topology.class change with a compatible infrastructureCluster APIVersion ref change", 1675 firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). 1676 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 1677 WithControlPlaneTemplate(refToUnstructured(ref)). 1678 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). 1679 Build(), 1680 secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2"). 1681 WithInfrastructureClusterTemplate(refToUnstructured(compatibleAPIVersionChangeRef)). 1682 WithControlPlaneTemplate(refToUnstructured(ref)). 1683 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). 1684 Build(), 1685 wantErr: false, 1686 }, 1687 1688 { 1689 name: "Reject cluster.topology.class change with an incompatible infrastructureCluster Kind ref change", 1690 firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). 1691 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 1692 WithControlPlaneTemplate(refToUnstructured(ref)). 1693 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). 1694 Build(), 1695 secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2"). 1696 WithInfrastructureClusterTemplate(refToUnstructured(incompatibleKindRef)). 1697 WithControlPlaneTemplate(refToUnstructured(ref)). 1698 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). 1699 Build(), 1700 wantErr: true, 1701 }, 1702 { 1703 name: "Reject cluster.topology.class change with an incompatible infrastructureCluster APIGroup ref change", 1704 firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). 1705 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 1706 WithControlPlaneTemplate(refToUnstructured(ref)). 1707 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). 1708 Build(), 1709 secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2"). 1710 WithInfrastructureClusterTemplate(refToUnstructured(incompatibleAPIGroupRef)). 1711 WithControlPlaneTemplate(refToUnstructured(ref)). 1712 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). 1713 Build(), 1714 wantErr: true, 1715 }, 1716 1717 // ControlPlane changes. 1718 { 1719 name: "Accept cluster.topology.class change with a compatible controlPlaneTemplate ref change", 1720 firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). 1721 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 1722 WithControlPlaneTemplate(refToUnstructured(ref)). 1723 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). 1724 Build(), 1725 secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2"). 1726 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 1727 WithControlPlaneTemplate(refToUnstructured(compatibleNameChangeRef)). 1728 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). 1729 Build(), 1730 wantErr: false, 1731 }, 1732 { 1733 name: "Accept cluster.topology.class change with a compatible controlPlaneTemplate ref change", 1734 firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). 1735 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 1736 WithControlPlaneTemplate(refToUnstructured(ref)). 1737 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). 1738 Build(), 1739 secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2"). 1740 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 1741 WithControlPlaneTemplate(refToUnstructured(compatibleAPIVersionChangeRef)). 1742 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). 1743 Build(), 1744 wantErr: false, 1745 }, 1746 1747 { 1748 name: "Reject cluster.topology.class change with an incompatible controlPlane Kind ref change", 1749 firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). 1750 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 1751 WithControlPlaneTemplate(refToUnstructured(ref)). 1752 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). 1753 Build(), 1754 secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2"). 1755 WithInfrastructureClusterTemplate(refToUnstructured(incompatibleKindRef)). 1756 WithControlPlaneTemplate(refToUnstructured(ref)). 1757 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). 1758 Build(), 1759 wantErr: true, 1760 }, 1761 { 1762 name: "Reject cluster.topology.class change with an incompatible controlPlane APIVersion ref change", 1763 firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). 1764 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 1765 WithControlPlaneTemplate(refToUnstructured(ref)). 1766 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). 1767 Build(), 1768 secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2"). 1769 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 1770 WithControlPlaneTemplate(refToUnstructured(incompatibleAPIGroupRef)). 1771 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(compatibleNameChangeRef)). 1772 Build(), 1773 wantErr: true, 1774 }, 1775 { 1776 name: "Accept cluster.topology.class change with a compatible controlPlane.MachineInfrastructure ref change", 1777 firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). 1778 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 1779 WithControlPlaneTemplate(refToUnstructured(ref)). 1780 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). 1781 Build(), 1782 secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2"). 1783 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 1784 WithControlPlaneTemplate(refToUnstructured(ref)). 1785 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(compatibleNameChangeRef)). 1786 Build(), 1787 wantErr: false, 1788 }, 1789 { 1790 name: "Accept cluster.topology.class change with a compatible controlPlane.MachineInfrastructure ref change", 1791 firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). 1792 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 1793 WithControlPlaneTemplate(refToUnstructured(ref)). 1794 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). 1795 Build(), 1796 secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2"). 1797 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 1798 WithControlPlaneTemplate(refToUnstructured(ref)). 1799 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(compatibleAPIVersionChangeRef)). 1800 Build(), 1801 wantErr: false, 1802 }, 1803 { 1804 name: "Reject cluster.topology.class change with an incompatible controlPlane.MachineInfrastructure Kind ref change", 1805 firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). 1806 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 1807 WithControlPlaneTemplate(refToUnstructured(ref)). 1808 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). 1809 Build(), 1810 secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2"). 1811 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 1812 WithControlPlaneTemplate(refToUnstructured(ref)). 1813 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(incompatibleKindRef)). 1814 Build(), 1815 wantErr: true, 1816 }, 1817 { 1818 name: "Reject cluster.topology.class change with an incompatible controlPlane.MachineInfrastructure APIVersion ref change", 1819 firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). 1820 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 1821 WithControlPlaneTemplate(refToUnstructured(ref)). 1822 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). 1823 Build(), 1824 secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2"). 1825 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 1826 WithControlPlaneTemplate(refToUnstructured(ref)). 1827 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(incompatibleAPIGroupRef)). 1828 Build(), 1829 wantErr: true, 1830 }, 1831 1832 // MachineDeploymentClass changes 1833 { 1834 name: "Accept cluster.topology.class change with a compatible MachineDeploymentClass InfrastructureTemplate", 1835 firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). 1836 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 1837 WithControlPlaneTemplate(refToUnstructured(ref)). 1838 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). 1839 WithWorkerMachineDeploymentClasses( 1840 *builder.MachineDeploymentClass("aa"). 1841 WithInfrastructureTemplate(refToUnstructured(ref)). 1842 WithBootstrapTemplate(refToUnstructured(ref)). 1843 Build(), 1844 ). 1845 Build(), 1846 secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2"). 1847 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 1848 WithControlPlaneTemplate(refToUnstructured(ref)). 1849 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). 1850 WithWorkerMachineDeploymentClasses( 1851 *builder.MachineDeploymentClass("aa"). 1852 WithInfrastructureTemplate(refToUnstructured(compatibleNameChangeRef)). 1853 WithBootstrapTemplate(refToUnstructured(ref)). 1854 Build(), 1855 ). 1856 Build(), 1857 wantErr: false, 1858 }, 1859 { 1860 name: "Accept cluster.topology.class change with an incompatible MachineDeploymentClass BootstrapTemplate", 1861 firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). 1862 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 1863 WithControlPlaneTemplate(refToUnstructured(ref)). 1864 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). 1865 WithWorkerMachineDeploymentClasses( 1866 *builder.MachineDeploymentClass("aa"). 1867 WithInfrastructureTemplate(refToUnstructured(ref)). 1868 WithBootstrapTemplate(refToUnstructured(ref)). 1869 Build(), 1870 ). 1871 Build(), 1872 secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2"). 1873 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 1874 WithControlPlaneTemplate(refToUnstructured(ref)). 1875 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). 1876 WithWorkerMachineDeploymentClasses( 1877 *builder.MachineDeploymentClass("aa"). 1878 WithInfrastructureTemplate(refToUnstructured(compatibleNameChangeRef)). 1879 WithBootstrapTemplate(refToUnstructured(incompatibleKindRef)). 1880 Build(), 1881 ). 1882 Build(), 1883 wantErr: false, 1884 }, 1885 { 1886 name: "Accept cluster.topology.class change with a deleted MachineDeploymentClass", 1887 firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). 1888 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 1889 WithControlPlaneTemplate(refToUnstructured(ref)). 1890 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). 1891 WithWorkerMachineDeploymentClasses( 1892 *builder.MachineDeploymentClass("aa"). 1893 WithInfrastructureTemplate(refToUnstructured(ref)). 1894 WithBootstrapTemplate(refToUnstructured(ref)). 1895 Build(), 1896 *builder.MachineDeploymentClass("bb"). 1897 WithInfrastructureTemplate(refToUnstructured(ref)). 1898 WithBootstrapTemplate(refToUnstructured(ref)). 1899 Build(), 1900 ). 1901 Build(), 1902 secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2"). 1903 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 1904 WithControlPlaneTemplate(refToUnstructured(ref)). 1905 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). 1906 WithWorkerMachineDeploymentClasses( 1907 *builder.MachineDeploymentClass("aa"). 1908 WithInfrastructureTemplate(refToUnstructured(ref)). 1909 WithBootstrapTemplate(refToUnstructured(ref)). 1910 Build(), 1911 ). 1912 Build(), 1913 wantErr: false, 1914 }, 1915 { 1916 name: "Accept cluster.topology.class change with an added MachineDeploymentClass", 1917 firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). 1918 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 1919 WithControlPlaneTemplate(refToUnstructured(ref)). 1920 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). 1921 WithWorkerMachineDeploymentClasses( 1922 *builder.MachineDeploymentClass("aa"). 1923 WithInfrastructureTemplate(refToUnstructured(ref)). 1924 WithBootstrapTemplate(refToUnstructured(ref)). 1925 Build(), 1926 ). 1927 Build(), 1928 secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2"). 1929 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 1930 WithControlPlaneTemplate(refToUnstructured(ref)). 1931 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). 1932 WithWorkerMachineDeploymentClasses( 1933 *builder.MachineDeploymentClass("aa"). 1934 WithInfrastructureTemplate(refToUnstructured(ref)). 1935 WithBootstrapTemplate(refToUnstructured(ref)). 1936 Build(), 1937 *builder.MachineDeploymentClass("bb"). 1938 WithInfrastructureTemplate(refToUnstructured(ref)). 1939 WithBootstrapTemplate(refToUnstructured(ref)). 1940 Build(), 1941 ). 1942 Build(), 1943 wantErr: false, 1944 }, 1945 { 1946 name: "Reject cluster.topology.class change with an incompatible Kind change to MachineDeploymentClass InfrastructureTemplate", 1947 firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). 1948 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 1949 WithControlPlaneTemplate(refToUnstructured(ref)). 1950 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). 1951 WithWorkerMachineDeploymentClasses( 1952 *builder.MachineDeploymentClass("aa"). 1953 WithInfrastructureTemplate(refToUnstructured(ref)). 1954 WithBootstrapTemplate(refToUnstructured(ref)). 1955 Build(), 1956 ). 1957 Build(), 1958 secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2"). 1959 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 1960 WithControlPlaneTemplate(refToUnstructured(ref)). 1961 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). 1962 WithWorkerMachineDeploymentClasses( 1963 *builder.MachineDeploymentClass("aa"). 1964 WithInfrastructureTemplate(refToUnstructured(incompatibleKindRef)). 1965 WithBootstrapTemplate(refToUnstructured(ref)). 1966 Build(), 1967 ). 1968 Build(), 1969 wantErr: true, 1970 }, 1971 { 1972 name: "Reject cluster.topology.class change with an incompatible APIGroup change to MachineDeploymentClass InfrastructureTemplate", 1973 firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). 1974 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 1975 WithControlPlaneTemplate(refToUnstructured(ref)). 1976 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). 1977 WithWorkerMachineDeploymentClasses( 1978 *builder.MachineDeploymentClass("aa"). 1979 WithInfrastructureTemplate(refToUnstructured(ref)). 1980 WithBootstrapTemplate(refToUnstructured(ref)). 1981 Build(), 1982 ). 1983 Build(), 1984 secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2"). 1985 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 1986 WithControlPlaneTemplate(refToUnstructured(ref)). 1987 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). 1988 WithWorkerMachineDeploymentClasses( 1989 *builder.MachineDeploymentClass("aa"). 1990 WithInfrastructureTemplate(refToUnstructured(incompatibleAPIGroupRef)). 1991 WithBootstrapTemplate(refToUnstructured(ref)). 1992 Build(), 1993 ). 1994 Build(), 1995 wantErr: true, 1996 }, 1997 } 1998 for _, tt := range tests { 1999 t.Run(tt.name, func(t *testing.T) { 2000 // Mark this condition to true so the webhook sees the ClusterClass as up to date. 2001 conditions.MarkTrue(tt.firstClass, clusterv1.ClusterClassVariablesReconciledCondition) 2002 conditions.MarkTrue(tt.secondClass, clusterv1.ClusterClassVariablesReconciledCondition) 2003 2004 // Sets up the fakeClient for the test case. 2005 fakeClient := fake.NewClientBuilder(). 2006 WithObjects(tt.firstClass, tt.secondClass). 2007 WithScheme(fakeScheme). 2008 Build() 2009 2010 // Create the webhook and add the fakeClient as its client. This is required because the test uses a Managed Topology. 2011 c := &Cluster{Client: fakeClient} 2012 2013 // Create and updated cluster which uses the name of the second class from the test definition in its '.spec.topology.' 2014 secondCluster := cluster.DeepCopy() 2015 secondCluster.Spec.Topology.Class = tt.secondClass.Name 2016 2017 // Checks the return error. 2018 warnings, err := c.ValidateUpdate(ctx, cluster, secondCluster) 2019 if tt.wantErr { 2020 g.Expect(err).To(HaveOccurred()) 2021 } else { 2022 g.Expect(err).ToNot(HaveOccurred()) 2023 } 2024 g.Expect(warnings).To(BeEmpty()) 2025 }) 2026 } 2027 } 2028 2029 // TestMovingBetweenManagedAndUnmanaged cluster tests cases where a clusterClass is added or removed during a cluster update. 2030 func TestMovingBetweenManagedAndUnmanaged(t *testing.T) { 2031 defer utilfeature.SetFeatureGateDuringTest(t, feature.Gates, feature.ClusterTopology, true)() 2032 ref := &corev1.ObjectReference{ 2033 APIVersion: "group.test.io/foo", 2034 Kind: "barTemplate", 2035 Name: "baz", 2036 Namespace: "default", 2037 } 2038 2039 g := NewWithT(t) 2040 2041 tests := []struct { 2042 name string 2043 cluster *clusterv1.Cluster 2044 clusterClass *clusterv1.ClusterClass 2045 updatedTopology *clusterv1.Topology 2046 wantErr bool 2047 }{ 2048 { 2049 name: "Reject cluster moving from Unmanaged to Managed i.e. adding the spec.topology.class field on update", 2050 cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1"). 2051 Build(), 2052 clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). 2053 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 2054 WithControlPlaneTemplate(refToUnstructured(ref)). 2055 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). 2056 Build(), 2057 updatedTopology: builder.ClusterTopology(). 2058 WithClass("class1"). 2059 WithVersion("v1.22.2"). 2060 WithControlPlaneReplicas(3). 2061 Build(), 2062 wantErr: true, 2063 }, 2064 { 2065 name: "Allow cluster moving from Unmanaged to Managed i.e. adding the spec.topology.class field on update " + 2066 "if and only if ClusterTopologyUnsafeUpdateClassNameAnnotation is set", 2067 cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1"). 2068 WithAnnotations(map[string]string{clusterv1.ClusterTopologyUnsafeUpdateClassNameAnnotation: ""}). 2069 Build(), 2070 clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). 2071 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 2072 WithControlPlaneTemplate(refToUnstructured(ref)). 2073 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). 2074 Build(), 2075 updatedTopology: builder.ClusterTopology(). 2076 WithClass("class1"). 2077 WithVersion("v1.22.2"). 2078 WithControlPlaneReplicas(3). 2079 Build(), 2080 wantErr: false, 2081 }, 2082 { 2083 name: "Reject cluster moving from Managed to Unmanaged i.e. removing the spec.topology.class field on update", 2084 cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1"). 2085 WithTopology(builder.ClusterTopology(). 2086 WithClass("class1"). 2087 WithVersion("v1.22.2"). 2088 WithControlPlaneReplicas(3). 2089 Build()). 2090 Build(), 2091 clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). 2092 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 2093 WithControlPlaneTemplate(refToUnstructured(ref)). 2094 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). 2095 Build(), 2096 updatedTopology: nil, 2097 wantErr: true, 2098 }, 2099 { 2100 name: "Reject cluster update if ClusterClass does not exist", 2101 cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1"). 2102 WithTopology(builder.ClusterTopology(). 2103 WithClass("class1"). 2104 WithVersion("v1.22.2"). 2105 WithControlPlaneReplicas(3). 2106 Build()). 2107 Build(), 2108 clusterClass: 2109 // ClusterClass name is different to that in the Cluster `.spec.topology.class` 2110 builder.ClusterClass(metav1.NamespaceDefault, "completely-different-class"). 2111 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 2112 WithControlPlaneTemplate(refToUnstructured(ref)). 2113 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). 2114 Build(), 2115 updatedTopology: builder.ClusterTopology(). 2116 WithClass("class1"). 2117 WithVersion("v1.22.2"). 2118 WithControlPlaneReplicas(3). 2119 Build(), 2120 wantErr: true, 2121 }, 2122 } 2123 for _, tt := range tests { 2124 t.Run(tt.name, func(t *testing.T) { 2125 // Mark this condition to true so the webhook sees the ClusterClass as up to date. 2126 conditions.MarkTrue(tt.clusterClass, clusterv1.ClusterClassVariablesReconciledCondition) 2127 // Sets up the fakeClient for the test case. 2128 fakeClient := fake.NewClientBuilder(). 2129 WithObjects(tt.clusterClass, tt.cluster). 2130 WithScheme(fakeScheme). 2131 Build() 2132 2133 // Create the webhook and add the fakeClient as its client. This is required because the test uses a Managed Topology. 2134 c := &Cluster{Client: fakeClient} 2135 2136 // Create and updated cluster which uses the name of the second class from the test definition in its '.spec.topology.' 2137 updatedCluster := tt.cluster.DeepCopy() 2138 updatedCluster.Spec.Topology = tt.updatedTopology 2139 2140 // Checks the return error. 2141 warnings, err := c.ValidateUpdate(ctx, tt.cluster, updatedCluster) 2142 if tt.wantErr { 2143 g.Expect(err).To(HaveOccurred()) 2144 } else { 2145 g.Expect(err).NotTo(HaveOccurred()) 2146 // Errors may be duplicated as warnings. There should be no warnings in this case if there are no errors. 2147 g.Expect(warnings).To(BeEmpty()) 2148 } 2149 }) 2150 } 2151 } 2152 2153 // TestClusterClassPollingErrors tests when a Cluster can be reconciled given different reconcile states of the ClusterClass. 2154 func TestClusterClassPollingErrors(t *testing.T) { 2155 defer utilfeature.SetFeatureGateDuringTest(t, feature.Gates, feature.ClusterTopology, true)() 2156 g := NewWithT(t) 2157 ref := &corev1.ObjectReference{ 2158 APIVersion: "group.test.io/foo", 2159 Kind: "barTemplate", 2160 Name: "baz", 2161 Namespace: "default", 2162 } 2163 2164 topology := builder.ClusterTopology().WithClass("class1").WithVersion("v1.24.3").Build() 2165 secondTopology := builder.ClusterTopology().WithClass("class2").WithVersion("v1.24.3").Build() 2166 notFoundTopology := builder.ClusterTopology().WithClass("doesnotexist").WithVersion("v1.24.3").Build() 2167 2168 baseClusterClass := builder.ClusterClass(metav1.NamespaceDefault, "class1"). 2169 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 2170 WithControlPlaneTemplate(refToUnstructured(ref)). 2171 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)) 2172 2173 // ccFullyReconciled is a ClusterClass with a matching generation and observed generation, and VariablesReconciled=True. 2174 ccFullyReconciled := baseClusterClass.DeepCopy().Build() 2175 ccFullyReconciled.Generation = 1 2176 ccFullyReconciled.Status.ObservedGeneration = 1 2177 conditions.MarkTrue(ccFullyReconciled, clusterv1.ClusterClassVariablesReconciledCondition) 2178 2179 // secondFullyReconciled is a second ClusterClass with a matching generation and observed generation, and VariablesReconciled=True. 2180 secondFullyReconciled := ccFullyReconciled.DeepCopy() 2181 secondFullyReconciled.SetName("class2") 2182 2183 // ccGenerationMismatch is a ClusterClass with a mismatched generation and observed generation, but VariablesReconciledCondition=True. 2184 ccGenerationMismatch := baseClusterClass.DeepCopy().Build() 2185 ccGenerationMismatch.Generation = 999 2186 ccGenerationMismatch.Status.ObservedGeneration = 1 2187 conditions.MarkTrue(ccGenerationMismatch, clusterv1.ClusterClassVariablesReconciledCondition) 2188 2189 // ccVariablesReconciledFalse with VariablesReconciled=False. 2190 ccVariablesReconciledFalse := baseClusterClass.DeepCopy().Build() 2191 conditions.MarkFalse(ccGenerationMismatch, clusterv1.ClusterClassVariablesReconciledCondition, "", clusterv1.ConditionSeverityError, "") 2192 2193 tests := []struct { 2194 name string 2195 cluster *clusterv1.Cluster 2196 oldCluster *clusterv1.Cluster 2197 clusterClasses []*clusterv1.ClusterClass 2198 injectedErr interceptor.Funcs 2199 wantErr bool 2200 wantWarnings bool 2201 }{ 2202 { 2203 name: "Pass on create if ClusterClass is fully reconciled", 2204 cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").WithTopology(topology).Build(), 2205 clusterClasses: []*clusterv1.ClusterClass{ccFullyReconciled}, 2206 wantErr: false, 2207 }, 2208 { 2209 name: "Pass on create if ClusterClass generation does not match observedGeneration", 2210 cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").WithTopology(topology).Build(), 2211 clusterClasses: []*clusterv1.ClusterClass{ccGenerationMismatch}, 2212 wantErr: false, 2213 wantWarnings: true, 2214 }, 2215 { 2216 name: "Pass on create if ClusterClass generation matches observedGeneration but VariablesReconciled=False", 2217 cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").WithTopology(topology).Build(), 2218 clusterClasses: []*clusterv1.ClusterClass{ccVariablesReconciledFalse}, 2219 wantErr: false, 2220 wantWarnings: true, 2221 }, 2222 { 2223 name: "Pass on create if ClusterClass is not found", 2224 cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").WithTopology(notFoundTopology).Build(), 2225 clusterClasses: []*clusterv1.ClusterClass{ccFullyReconciled}, 2226 wantErr: false, 2227 wantWarnings: true, 2228 }, 2229 { 2230 name: "Pass on update if oldCluster ClusterClass is fully reconciled", 2231 cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").WithTopology(secondTopology).Build(), 2232 oldCluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").WithTopology(topology).Build(), 2233 clusterClasses: []*clusterv1.ClusterClass{ccFullyReconciled, secondFullyReconciled}, 2234 wantErr: false, 2235 }, 2236 { 2237 name: "Fail on update if oldCluster ClusterClass generation does not match observedGeneration", 2238 cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").WithTopology(secondTopology).Build(), 2239 oldCluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").WithTopology(topology).Build(), 2240 clusterClasses: []*clusterv1.ClusterClass{ccGenerationMismatch, secondFullyReconciled}, 2241 wantErr: true, 2242 }, 2243 { 2244 name: "Fail on update if old Cluster ClusterClass is not found", 2245 cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").WithTopology(topology).Build(), 2246 oldCluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").WithTopology(notFoundTopology).Build(), 2247 clusterClasses: []*clusterv1.ClusterClass{ccFullyReconciled}, 2248 wantErr: true, 2249 }, 2250 { 2251 name: "Fail on update if new Cluster ClusterClass is not found", 2252 cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").WithTopology(notFoundTopology).Build(), 2253 oldCluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").WithTopology(topology).Build(), 2254 clusterClasses: []*clusterv1.ClusterClass{ccFullyReconciled}, 2255 wantErr: true, 2256 wantWarnings: true, 2257 }, 2258 { 2259 name: "Fail on update if new ClusterClass returns connection error", 2260 cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").WithTopology(secondTopology).Build(), 2261 oldCluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").WithTopology(topology).Build(), 2262 clusterClasses: []*clusterv1.ClusterClass{ccFullyReconciled, secondFullyReconciled}, 2263 injectedErr: interceptor.Funcs{ 2264 Get: func(ctx context.Context, client client.WithWatch, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error { 2265 // Throw an error if the second ClusterClass `class2` used as the new ClusterClass is being retrieved. 2266 if key.Name == secondTopology.Class { 2267 return errors.New("connection error") 2268 } 2269 return client.Get(ctx, key, obj) 2270 }, 2271 }, 2272 wantErr: true, 2273 wantWarnings: false, 2274 }, 2275 { 2276 name: "Fail on update if old ClusterClass returns connection error", 2277 cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").WithTopology(secondTopology).Build(), 2278 oldCluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").WithTopology(topology).Build(), 2279 clusterClasses: []*clusterv1.ClusterClass{ccFullyReconciled, secondFullyReconciled}, 2280 injectedErr: interceptor.Funcs{ 2281 Get: func(ctx context.Context, client client.WithWatch, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error { 2282 // Throw an error if the ClusterClass `class1` used as the old ClusterClass is being retrieved. 2283 if key.Name == topology.Class { 2284 return errors.New("connection error") 2285 } 2286 return client.Get(ctx, key, obj) 2287 }, 2288 }, 2289 wantErr: true, 2290 wantWarnings: false, 2291 }, 2292 } 2293 2294 for _, tt := range tests { 2295 t.Run(tt.name, func(t *testing.T) { 2296 // Sets up a reconcile with a fakeClient for the test case. 2297 objs := []client.Object{} 2298 for _, cc := range tt.clusterClasses { 2299 objs = append(objs, cc) 2300 } 2301 c := &Cluster{Client: fake.NewClientBuilder(). 2302 WithInterceptorFuncs(tt.injectedErr). 2303 WithScheme(fakeScheme). 2304 WithObjects(objs...). 2305 Build(), 2306 } 2307 2308 // Checks the return error. 2309 warnings, err := c.validate(ctx, tt.oldCluster, tt.cluster) 2310 if tt.wantErr { 2311 g.Expect(err).To(HaveOccurred()) 2312 } else { 2313 g.Expect(err).ToNot(HaveOccurred()) 2314 } 2315 if tt.wantWarnings { 2316 g.Expect(warnings).NotTo(BeNil()) 2317 } else { 2318 g.Expect(warnings).To(BeNil()) 2319 } 2320 }) 2321 } 2322 } 2323 2324 func refToUnstructured(ref *corev1.ObjectReference) *unstructured.Unstructured { 2325 gvk := ref.GetObjectKind().GroupVersionKind() 2326 output := &unstructured.Unstructured{} 2327 output.SetKind(gvk.Kind) 2328 output.SetAPIVersion(gvk.GroupVersion().String()) 2329 output.SetName(ref.Name) 2330 output.SetNamespace(ref.Namespace) 2331 return output 2332 }