sigs.k8s.io/cluster-api@v1.7.1/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/blang/semver/v4" 24 . "github.com/onsi/gomega" 25 "github.com/pkg/errors" 26 corev1 "k8s.io/api/core/v1" 27 apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 28 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 29 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 30 "k8s.io/apimachinery/pkg/labels" 31 "k8s.io/apimachinery/pkg/types" 32 utilfeature "k8s.io/component-base/featuregate/testing" 33 "k8s.io/utils/ptr" 34 "sigs.k8s.io/controller-runtime/pkg/client" 35 "sigs.k8s.io/controller-runtime/pkg/client/fake" 36 "sigs.k8s.io/controller-runtime/pkg/client/interceptor" 37 38 clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" 39 expv1 "sigs.k8s.io/cluster-api/exp/api/v1beta1" 40 "sigs.k8s.io/cluster-api/feature" 41 "sigs.k8s.io/cluster-api/internal/test/builder" 42 "sigs.k8s.io/cluster-api/internal/webhooks/util" 43 "sigs.k8s.io/cluster-api/util/conditions" 44 ) 45 46 func TestClusterDefaultNamespaces(t *testing.T) { 47 g := NewWithT(t) 48 49 c := &clusterv1.Cluster{ 50 ObjectMeta: metav1.ObjectMeta{ 51 Namespace: "fooboo", 52 }, 53 Spec: clusterv1.ClusterSpec{ 54 InfrastructureRef: &corev1.ObjectReference{}, 55 ControlPlaneRef: &corev1.ObjectReference{}, 56 }, 57 } 58 webhook := &Cluster{} 59 t.Run("for Cluster", util.CustomDefaultValidateTest(ctx, c, webhook)) 60 61 g.Expect(webhook.Default(ctx, c)).To(Succeed()) 62 63 g.Expect(c.Spec.InfrastructureRef.Namespace).To(Equal(c.Namespace)) 64 g.Expect(c.Spec.ControlPlaneRef.Namespace).To(Equal(c.Namespace)) 65 } 66 67 // TestClusterDefaultAndValidateVariables cases where cluster.spec.topology.class is altered. 68 func TestClusterDefaultAndValidateVariables(t *testing.T) { 69 defer utilfeature.SetFeatureGateDuringTest(t, feature.Gates, feature.ClusterTopology, true)() 70 71 tests := []struct { 72 name string 73 clusterClass *clusterv1.ClusterClass 74 topology *clusterv1.Topology 75 expect *clusterv1.Topology 76 wantErr bool 77 }{ 78 { 79 name: "default a single variable to its correct values", 80 clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). 81 WithStatusVariables(clusterv1.ClusterClassStatusVariable{ 82 Name: "location", 83 Definitions: []clusterv1.ClusterClassStatusVariableDefinition{ 84 { 85 Required: true, 86 From: clusterv1.VariableDefinitionFromInline, 87 Schema: clusterv1.VariableSchema{ 88 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 89 Type: "string", 90 Default: &apiextensionsv1.JSON{Raw: []byte(`"us-east"`)}, 91 }, 92 }, 93 }, 94 }, 95 }, 96 ). 97 Build(), 98 topology: &clusterv1.Topology{}, 99 expect: &clusterv1.Topology{ 100 Variables: []clusterv1.ClusterVariable{ 101 { 102 Name: "location", 103 Value: apiextensionsv1.JSON{ 104 Raw: []byte(`"us-east"`), 105 }, 106 }, 107 }, 108 }, 109 }, 110 { 111 name: "don't change a variable if it is already set", 112 clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). 113 WithStatusVariables(clusterv1.ClusterClassStatusVariable{ 114 Name: "location", 115 Definitions: []clusterv1.ClusterClassStatusVariableDefinition{ 116 { 117 Required: true, 118 From: clusterv1.VariableDefinitionFromInline, 119 Schema: clusterv1.VariableSchema{ 120 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 121 Type: "string", 122 Default: &apiextensionsv1.JSON{Raw: []byte(`"us-east"`)}, 123 }, 124 }, 125 }, 126 }, 127 }, 128 ). 129 Build(), 130 topology: &clusterv1.Topology{ 131 Variables: []clusterv1.ClusterVariable{ 132 { 133 Name: "location", 134 Value: apiextensionsv1.JSON{ 135 Raw: []byte(`"A different location"`), 136 }, 137 }, 138 }, 139 }, 140 expect: &clusterv1.Topology{ 141 Variables: []clusterv1.ClusterVariable{ 142 { 143 Name: "location", 144 Value: apiextensionsv1.JSON{ 145 Raw: []byte(`"A different location"`), 146 }, 147 }, 148 }, 149 }, 150 }, 151 { 152 name: "default many variables to their correct values", 153 clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). 154 WithStatusVariables([]clusterv1.ClusterClassStatusVariable{ 155 { 156 Name: "location", 157 Definitions: []clusterv1.ClusterClassStatusVariableDefinition{ 158 { 159 Required: true, 160 From: clusterv1.VariableDefinitionFromInline, 161 Schema: clusterv1.VariableSchema{ 162 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 163 Type: "string", 164 Default: &apiextensionsv1.JSON{Raw: []byte(`"us-east"`)}, 165 }, 166 }, 167 }, 168 }, 169 }, 170 { 171 Name: "count", 172 Definitions: []clusterv1.ClusterClassStatusVariableDefinition{ 173 { 174 Required: true, 175 From: clusterv1.VariableDefinitionFromInline, 176 Schema: clusterv1.VariableSchema{ 177 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 178 Type: "number", 179 Default: &apiextensionsv1.JSON{Raw: []byte(`0.1`)}, 180 }, 181 }, 182 }, 183 }, 184 }, 185 }...). 186 Build(), 187 topology: &clusterv1.Topology{}, 188 expect: &clusterv1.Topology{ 189 Variables: []clusterv1.ClusterVariable{ 190 { 191 Name: "location", 192 Value: apiextensionsv1.JSON{ 193 Raw: []byte(`"us-east"`), 194 }, 195 }, 196 { 197 Name: "count", 198 Value: apiextensionsv1.JSON{ 199 Raw: []byte(`0.1`), 200 }, 201 }, 202 }, 203 }, 204 }, 205 { 206 name: "don't add new variable overrides", 207 clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). 208 WithWorkerMachineDeploymentClasses( 209 *builder.MachineDeploymentClass("default-worker").Build(), 210 ). 211 WithWorkerMachinePoolClasses( 212 *builder.MachinePoolClass("default-worker").Build(), 213 ). 214 WithStatusVariables(clusterv1.ClusterClassStatusVariable{ 215 Name: "location", 216 Definitions: []clusterv1.ClusterClassStatusVariableDefinition{ 217 { 218 Required: true, 219 From: clusterv1.VariableDefinitionFromInline, 220 Schema: clusterv1.VariableSchema{ 221 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 222 Type: "string", 223 Default: &apiextensionsv1.JSON{Raw: []byte(`"us-east"`)}, 224 }, 225 }, 226 }, 227 }, 228 }). 229 Build(), 230 topology: &clusterv1.Topology{ 231 Workers: &clusterv1.WorkersTopology{ 232 MachineDeployments: []clusterv1.MachineDeploymentTopology{ 233 { 234 Class: "default-worker", 235 Name: "md-1", 236 }, 237 }, 238 MachinePools: []clusterv1.MachinePoolTopology{ 239 { 240 Class: "default-worker", 241 Name: "mp-1", 242 }, 243 }, 244 }, 245 }, 246 expect: &clusterv1.Topology{ 247 Workers: &clusterv1.WorkersTopology{ 248 MachineDeployments: []clusterv1.MachineDeploymentTopology{ 249 { 250 Class: "default-worker", 251 Name: "md-1", 252 // "location" has not been added to .variables.overrides. 253 }, 254 }, 255 MachinePools: []clusterv1.MachinePoolTopology{ 256 { 257 Class: "default-worker", 258 Name: "mp-1", 259 // "location" has not been added to .variables.overrides. 260 }, 261 }, 262 }, 263 Variables: []clusterv1.ClusterVariable{ 264 { 265 Name: "location", 266 Value: apiextensionsv1.JSON{ 267 Raw: []byte(`"us-east"`), 268 }, 269 }, 270 }, 271 }, 272 }, 273 { 274 name: "default nested fields of variable overrides", 275 clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). 276 WithWorkerMachineDeploymentClasses( 277 *builder.MachineDeploymentClass("default-worker").Build(), 278 ). 279 WithWorkerMachinePoolClasses( 280 *builder.MachinePoolClass("default-worker").Build(), 281 ). 282 WithStatusVariables(clusterv1.ClusterClassStatusVariable{ 283 Name: "httpProxy", 284 Definitions: []clusterv1.ClusterClassStatusVariableDefinition{ 285 { 286 Required: true, 287 From: clusterv1.VariableDefinitionFromInline, 288 Schema: clusterv1.VariableSchema{ 289 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 290 Type: "object", 291 Properties: map[string]clusterv1.JSONSchemaProps{ 292 "enabled": { 293 Type: "boolean", 294 }, 295 "url": { 296 Type: "string", 297 Default: &apiextensionsv1.JSON{Raw: []byte(`"http://localhost:3128"`)}, 298 }, 299 }, 300 }, 301 }, 302 }, 303 }, 304 }). 305 Build(), 306 topology: &clusterv1.Topology{ 307 Workers: &clusterv1.WorkersTopology{ 308 MachineDeployments: []clusterv1.MachineDeploymentTopology{ 309 { 310 Class: "default-worker", 311 Name: "md-1", 312 Variables: &clusterv1.MachineDeploymentVariables{ 313 Overrides: []clusterv1.ClusterVariable{ 314 { 315 Name: "httpProxy", 316 Value: apiextensionsv1.JSON{Raw: []byte(`{"enabled":true}`)}, 317 }, 318 }, 319 }, 320 }, 321 }, 322 MachinePools: []clusterv1.MachinePoolTopology{ 323 { 324 Class: "default-worker", 325 Name: "md-1", 326 Variables: &clusterv1.MachinePoolVariables{ 327 Overrides: []clusterv1.ClusterVariable{ 328 { 329 Name: "httpProxy", 330 Value: apiextensionsv1.JSON{Raw: []byte(`{"enabled":true}`)}, 331 }, 332 }, 333 }, 334 }, 335 }, 336 }, 337 Variables: []clusterv1.ClusterVariable{ 338 { 339 Name: "httpProxy", 340 Value: apiextensionsv1.JSON{Raw: []byte(`{"enabled":true}`)}, 341 }, 342 }, 343 }, 344 expect: &clusterv1.Topology{ 345 Workers: &clusterv1.WorkersTopology{ 346 MachineDeployments: []clusterv1.MachineDeploymentTopology{ 347 { 348 Class: "default-worker", 349 Name: "md-1", 350 Variables: &clusterv1.MachineDeploymentVariables{ 351 Overrides: []clusterv1.ClusterVariable{ 352 { 353 Name: "httpProxy", 354 // url has been added by defaulting. 355 Value: apiextensionsv1.JSON{Raw: []byte(`{"enabled":true,"url":"http://localhost:3128"}`)}, 356 }, 357 }, 358 }, 359 }, 360 }, 361 MachinePools: []clusterv1.MachinePoolTopology{ 362 { 363 Class: "default-worker", 364 Name: "md-1", 365 Variables: &clusterv1.MachinePoolVariables{ 366 Overrides: []clusterv1.ClusterVariable{ 367 { 368 Name: "httpProxy", 369 // url has been added by defaulting. 370 Value: apiextensionsv1.JSON{Raw: []byte(`{"enabled":true,"url":"http://localhost:3128"}`)}, 371 }, 372 }, 373 }, 374 }, 375 }, 376 }, 377 Variables: []clusterv1.ClusterVariable{ 378 { 379 Name: "httpProxy", 380 Value: apiextensionsv1.JSON{ 381 // url has been added by defaulting. 382 Raw: []byte(`{"enabled":true,"url":"http://localhost:3128"}`), 383 }, 384 }, 385 }, 386 }, 387 }, 388 { 389 name: "Use one value for multiple definitions when variables don't conflict", 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 expect: &clusterv1.Topology{ 430 Variables: []clusterv1.ClusterVariable{ 431 { 432 Name: "location", 433 Value: apiextensionsv1.JSON{ 434 Raw: []byte(`"us-east"`), 435 }, 436 }, 437 }, 438 }, 439 }, 440 { 441 name: "Add defaults for each definitionFrom if variable is defined for some definitionFrom", 442 clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). 443 WithStatusVariables(clusterv1.ClusterClassStatusVariable{ 444 Name: "location", 445 Definitions: []clusterv1.ClusterClassStatusVariableDefinition{ 446 { 447 Required: true, 448 From: clusterv1.VariableDefinitionFromInline, 449 Schema: clusterv1.VariableSchema{ 450 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 451 Type: "string", 452 Default: &apiextensionsv1.JSON{Raw: []byte(`"us-east"`)}, 453 }, 454 }, 455 }, 456 { 457 Required: true, 458 From: "somepatch", 459 Schema: clusterv1.VariableSchema{ 460 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 461 Type: "string", 462 Default: &apiextensionsv1.JSON{Raw: []byte(`"us-east"`)}, 463 }, 464 }, 465 }, 466 { 467 Required: true, 468 From: "anotherpatch", 469 Schema: clusterv1.VariableSchema{ 470 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 471 Type: "string", 472 Default: &apiextensionsv1.JSON{Raw: []byte(`"us-east"`)}, 473 }, 474 }, 475 }, 476 }, 477 }, 478 ). 479 Build(), 480 topology: &clusterv1.Topology{ 481 Variables: []clusterv1.ClusterVariable{ 482 { 483 Name: "location", 484 Value: apiextensionsv1.JSON{ 485 Raw: []byte(`"us-west"`), 486 }, 487 DefinitionFrom: "somepatch", 488 }, 489 }, 490 }, 491 expect: &clusterv1.Topology{ 492 Variables: []clusterv1.ClusterVariable{ 493 { 494 Name: "location", 495 Value: apiextensionsv1.JSON{ 496 Raw: []byte(`"us-west"`), 497 }, 498 DefinitionFrom: "somepatch", 499 }, 500 { 501 Name: "location", 502 Value: apiextensionsv1.JSON{ 503 Raw: []byte(`"us-east"`), 504 }, 505 DefinitionFrom: clusterv1.VariableDefinitionFromInline, 506 }, 507 { 508 Name: "location", 509 Value: apiextensionsv1.JSON{ 510 Raw: []byte(`"us-east"`), 511 }, 512 DefinitionFrom: "anotherpatch", 513 }, 514 }, 515 }, 516 }, 517 { 518 name: "set definitionFrom on defaults when variables conflict", 519 clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). 520 WithStatusVariables(clusterv1.ClusterClassStatusVariable{ 521 Name: "location", 522 DefinitionsConflict: true, 523 Definitions: []clusterv1.ClusterClassStatusVariableDefinition{ 524 { 525 Required: true, 526 From: clusterv1.VariableDefinitionFromInline, 527 Schema: clusterv1.VariableSchema{ 528 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 529 Type: "string", 530 Default: &apiextensionsv1.JSON{Raw: []byte(`"first-region"`)}, 531 }, 532 }, 533 }, 534 { 535 Required: true, 536 From: "somepatch", 537 Schema: clusterv1.VariableSchema{ 538 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 539 Type: "string", 540 Default: &apiextensionsv1.JSON{Raw: []byte(`"another-region"`)}, 541 }, 542 }, 543 }, 544 { 545 Required: true, 546 From: "anotherpatch", 547 Schema: clusterv1.VariableSchema{ 548 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 549 Type: "string", 550 Default: &apiextensionsv1.JSON{Raw: []byte(`"us-east"`)}, 551 }, 552 }, 553 }, 554 }, 555 }, 556 ). 557 Build(), 558 topology: &clusterv1.Topology{}, 559 expect: &clusterv1.Topology{ 560 Variables: []clusterv1.ClusterVariable{ 561 { 562 Name: "location", 563 Value: apiextensionsv1.JSON{ 564 Raw: []byte(`"first-region"`), 565 }, 566 DefinitionFrom: clusterv1.VariableDefinitionFromInline, 567 }, 568 { 569 Name: "location", 570 Value: apiextensionsv1.JSON{ 571 Raw: []byte(`"another-region"`), 572 }, 573 DefinitionFrom: "somepatch", 574 }, 575 { 576 Name: "location", 577 Value: apiextensionsv1.JSON{ 578 Raw: []byte(`"us-east"`), 579 }, 580 DefinitionFrom: "anotherpatch", 581 }, 582 }, 583 }, 584 }, 585 // Testing validation of variables. 586 { 587 name: "should fail when required variable is missing top-level", 588 clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). 589 WithStatusVariables(clusterv1.ClusterClassStatusVariable{ 590 Name: "cpu", 591 Definitions: []clusterv1.ClusterClassStatusVariableDefinition{ 592 { 593 Required: true, 594 From: clusterv1.VariableDefinitionFromInline, 595 Schema: clusterv1.VariableSchema{ 596 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 597 Type: "integer", 598 }, 599 }, 600 }, 601 }, 602 }).Build(), 603 topology: builder.ClusterTopology().Build(), 604 expect: builder.ClusterTopology().Build(), 605 wantErr: true, 606 }, 607 { 608 name: "should fail when top-level variable is invalid", 609 clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). 610 WithStatusVariables(clusterv1.ClusterClassStatusVariable{ 611 Name: "cpu", 612 Definitions: []clusterv1.ClusterClassStatusVariableDefinition{ 613 { 614 Required: true, 615 From: clusterv1.VariableDefinitionFromInline, 616 Schema: clusterv1.VariableSchema{ 617 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 618 Type: "integer", 619 }, 620 }, 621 }, 622 }}, 623 ).Build(), 624 topology: builder.ClusterTopology(). 625 WithVariables(clusterv1.ClusterVariable{ 626 Name: "cpu", 627 Value: apiextensionsv1.JSON{Raw: []byte(`"text"`)}, 628 }). 629 Build(), 630 expect: builder.ClusterTopology().Build(), 631 wantErr: true, 632 }, 633 { 634 name: "should fail when variable override is invalid", 635 clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). 636 WithStatusVariables(clusterv1.ClusterClassStatusVariable{ 637 Name: "cpu", 638 Definitions: []clusterv1.ClusterClassStatusVariableDefinition{ 639 { 640 Required: true, 641 From: clusterv1.VariableDefinitionFromInline, 642 Schema: clusterv1.VariableSchema{ 643 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 644 Type: "integer", 645 }, 646 }, 647 }, 648 }}).Build(), 649 topology: builder.ClusterTopology(). 650 WithVariables(clusterv1.ClusterVariable{ 651 Name: "cpu", 652 Value: apiextensionsv1.JSON{Raw: []byte(`2`)}, 653 }). 654 WithMachineDeployment(builder.MachineDeploymentTopology("workers1"). 655 WithClass("aa"). 656 WithVariables(clusterv1.ClusterVariable{ 657 Name: "cpu", 658 Value: apiextensionsv1.JSON{Raw: []byte(`"text"`)}, 659 }). 660 Build()). 661 WithMachinePool(builder.MachinePoolTopology("workers1"). 662 WithClass("aa"). 663 WithVariables(clusterv1.ClusterVariable{ 664 Name: "cpu", 665 Value: apiextensionsv1.JSON{Raw: []byte(`"text"`)}, 666 }). 667 Build()). 668 Build(), 669 expect: builder.ClusterTopology().Build(), 670 wantErr: true, 671 }, 672 { 673 name: "should pass when required variable exists top-level", 674 clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). 675 WithStatusVariables(clusterv1.ClusterClassStatusVariable{ 676 Name: "cpu", 677 Definitions: []clusterv1.ClusterClassStatusVariableDefinition{ 678 { 679 Required: true, 680 From: clusterv1.VariableDefinitionFromInline, 681 Schema: clusterv1.VariableSchema{ 682 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 683 Type: "integer", 684 }, 685 }, 686 }, 687 }}).Build(), 688 topology: builder.ClusterTopology(). 689 WithClass("foo"). 690 WithVersion("v1.19.1"). 691 WithVariables(clusterv1.ClusterVariable{ 692 Name: "cpu", 693 Value: apiextensionsv1.JSON{Raw: []byte(`2`)}, 694 }). 695 // Variable is not required in MachineDeployment or MachinePool topologies. 696 Build(), 697 expect: builder.ClusterTopology(). 698 WithClass("foo"). 699 WithVersion("v1.19.1"). 700 WithVariables(clusterv1.ClusterVariable{ 701 Name: "cpu", 702 Value: apiextensionsv1.JSON{Raw: []byte(`2`)}, 703 }). 704 // Variable is not required in MachineDeployment or MachinePool topologies. 705 Build(), 706 }, 707 { 708 name: "should pass when top-level variable and override are valid", 709 clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). 710 WithWorkerMachineDeploymentClasses(*builder.MachineDeploymentClass("md1").Build()). 711 WithWorkerMachinePoolClasses(*builder.MachinePoolClass("mp1").Build()). 712 WithStatusVariables(clusterv1.ClusterClassStatusVariable{ 713 Name: "cpu", 714 Definitions: []clusterv1.ClusterClassStatusVariableDefinition{ 715 { 716 Required: true, 717 From: clusterv1.VariableDefinitionFromInline, 718 Schema: clusterv1.VariableSchema{ 719 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 720 Type: "integer", 721 }, 722 }, 723 }, 724 }}).Build(), 725 topology: builder.ClusterTopology(). 726 WithClass("foo"). 727 WithVersion("v1.19.1"). 728 WithVariables(clusterv1.ClusterVariable{ 729 Name: "cpu", 730 Value: apiextensionsv1.JSON{Raw: []byte(`2`)}, 731 }). 732 WithMachineDeployment(builder.MachineDeploymentTopology("workers1"). 733 WithClass("md1"). 734 WithVariables(clusterv1.ClusterVariable{ 735 Name: "cpu", 736 Value: apiextensionsv1.JSON{Raw: []byte(`2`)}, 737 }). 738 Build()). 739 WithMachinePool(builder.MachinePoolTopology("workers1"). 740 WithClass("mp1"). 741 WithVariables(clusterv1.ClusterVariable{ 742 Name: "cpu", 743 Value: apiextensionsv1.JSON{Raw: []byte(`2`)}, 744 }). 745 Build()). 746 Build(), 747 expect: builder.ClusterTopology(). 748 WithClass("foo"). 749 WithVersion("v1.19.1"). 750 WithVariables(clusterv1.ClusterVariable{ 751 Name: "cpu", 752 Value: apiextensionsv1.JSON{Raw: []byte(`2`)}, 753 }). 754 WithMachineDeployment(builder.MachineDeploymentTopology("workers1"). 755 WithClass("md1"). 756 WithVariables(clusterv1.ClusterVariable{ 757 Name: "cpu", 758 Value: apiextensionsv1.JSON{Raw: []byte(`2`)}, 759 }). 760 Build()). 761 WithMachinePool(builder.MachinePoolTopology("workers1"). 762 WithClass("mp1"). 763 WithVariables(clusterv1.ClusterVariable{ 764 Name: "cpu", 765 Value: apiextensionsv1.JSON{Raw: []byte(`2`)}, 766 }). 767 Build()). 768 Build(), 769 }, 770 { 771 name: "should pass even when variable override is missing the corresponding top-level variable", 772 clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). 773 WithWorkerMachineDeploymentClasses(*builder.MachineDeploymentClass("md1").Build()). 774 WithWorkerMachinePoolClasses(*builder.MachinePoolClass("mp1").Build()). 775 WithStatusVariables(clusterv1.ClusterClassStatusVariable{ 776 Name: "cpu", 777 Definitions: []clusterv1.ClusterClassStatusVariableDefinition{ 778 { 779 Required: false, 780 From: clusterv1.VariableDefinitionFromInline, 781 Schema: clusterv1.VariableSchema{ 782 OpenAPIV3Schema: clusterv1.JSONSchemaProps{ 783 Type: "integer", 784 }, 785 }, 786 }, 787 }}).Build(), 788 topology: builder.ClusterTopology(). 789 WithClass("foo"). 790 WithVersion("v1.19.1"). 791 WithMachineDeployment(builder.MachineDeploymentTopology("workers1"). 792 WithClass("md1"). 793 WithVariables(clusterv1.ClusterVariable{ 794 Name: "cpu", 795 Value: apiextensionsv1.JSON{Raw: []byte(`2`)}, 796 }). 797 Build()). 798 WithMachinePool(builder.MachinePoolTopology("workers1"). 799 WithClass("mp1"). 800 WithVariables(clusterv1.ClusterVariable{ 801 Name: "cpu", 802 Value: apiextensionsv1.JSON{Raw: []byte(`2`)}, 803 }). 804 Build()). 805 Build(), 806 expect: builder.ClusterTopology(). 807 WithClass("foo"). 808 WithVersion("v1.19.1"). 809 WithVariables([]clusterv1.ClusterVariable{}...). 810 WithMachineDeployment(builder.MachineDeploymentTopology("workers1"). 811 WithClass("md1"). 812 WithVariables(clusterv1.ClusterVariable{ 813 Name: "cpu", 814 Value: apiextensionsv1.JSON{Raw: []byte(`2`)}, 815 }). 816 Build()). 817 WithMachinePool(builder.MachinePoolTopology("workers1"). 818 WithClass("mp1"). 819 WithVariables(clusterv1.ClusterVariable{ 820 Name: "cpu", 821 Value: apiextensionsv1.JSON{Raw: []byte(`2`)}, 822 }). 823 Build()). 824 Build(), 825 }, 826 } 827 for _, tt := range tests { 828 t.Run(tt.name, func(t *testing.T) { 829 // Setting Class and Version here to avoid obfuscating the test cases above. 830 tt.topology.Class = "class1" 831 tt.topology.Version = "v1.22.2" 832 tt.expect.Class = "class1" 833 tt.expect.Version = "v1.22.2" 834 835 cluster := builder.Cluster(metav1.NamespaceDefault, "cluster1"). 836 WithTopology(tt.topology). 837 Build() 838 839 // Mark this condition to true so the webhook sees the ClusterClass as up to date. 840 conditions.MarkTrue(tt.clusterClass, clusterv1.ClusterClassVariablesReconciledCondition) 841 fakeClient := fake.NewClientBuilder(). 842 WithObjects(tt.clusterClass). 843 WithScheme(fakeScheme). 844 Build() 845 // Create the webhook and add the fakeClient as its client. This is required because the test uses a Managed Topology. 846 webhook := &Cluster{Client: fakeClient} 847 848 // Test defaulting. 849 t.Run("default", func(t *testing.T) { 850 g := NewWithT(t) 851 if tt.wantErr { 852 g.Expect(webhook.Default(ctx, cluster)).To(Not(Succeed())) 853 return 854 } 855 g.Expect(webhook.Default(ctx, cluster)).To(Succeed()) 856 g.Expect(cluster.Spec.Topology).To(BeEquivalentTo(tt.expect)) 857 }) 858 859 // Test if defaulting works in combination with validation. 860 // Note this test is not run for the case where the webhook should fail. 861 if tt.wantErr { 862 t.Skip("skipping test for combination of defaulting and validation (not supported by the test)") 863 } 864 util.CustomDefaultValidateTest(ctx, cluster, webhook)(t) 865 }) 866 } 867 } 868 869 func TestClusterDefaultTopologyVersion(t *testing.T) { 870 // NOTE: ClusterTopology feature flag is disabled by default, thus preventing to set Cluster.Topologies. 871 // Enabling the feature flag temporarily for this test. 872 defer utilfeature.SetFeatureGateDuringTest(t, feature.Gates, feature.ClusterTopology, true)() 873 874 g := NewWithT(t) 875 876 c := builder.Cluster("fooboo", "cluster1"). 877 WithTopology(builder.ClusterTopology(). 878 WithClass("foo"). 879 WithVersion("1.19.1"). 880 Build()). 881 Build() 882 883 clusterClass := builder.ClusterClass("fooboo", "foo").Build() 884 conditions.MarkTrue(clusterClass, clusterv1.ClusterClassVariablesReconciledCondition) 885 // Sets up the fakeClient for the test case. This is required because the test uses a Managed Topology. 886 fakeClient := fake.NewClientBuilder(). 887 WithObjects(clusterClass). 888 WithScheme(fakeScheme). 889 Build() 890 891 // Create the webhook and add the fakeClient as its client. 892 webhook := &Cluster{Client: fakeClient} 893 t.Run("for Cluster", util.CustomDefaultValidateTest(ctx, c, webhook)) 894 895 g.Expect(webhook.Default(ctx, c)).To(Succeed()) 896 897 g.Expect(c.Spec.Topology.Version).To(HavePrefix("v")) 898 } 899 900 func TestClusterValidation(t *testing.T) { 901 // NOTE: ClusterTopology feature flag is disabled by default, thus preventing to set Cluster.Topologies. 902 903 var ( 904 tests = []struct { 905 name string 906 in *clusterv1.Cluster 907 old *clusterv1.Cluster 908 expectErr bool 909 }{ 910 { 911 name: "should return error when cluster namespace and infrastructure ref namespace mismatch", 912 expectErr: true, 913 in: builder.Cluster("fooNamespace", "cluster1"). 914 WithInfrastructureCluster( 915 builder.InfrastructureClusterTemplate("barNamespace", "infra1").Build()). 916 WithControlPlane( 917 builder.ControlPlane("fooNamespace", "cp1").Build()). 918 Build(), 919 }, 920 { 921 name: "should return error when cluster namespace and controlPlane ref namespace mismatch", 922 expectErr: true, 923 in: builder.Cluster("fooNamespace", "cluster1"). 924 WithInfrastructureCluster( 925 builder.InfrastructureClusterTemplate("fooNamespace", "infra1").Build()). 926 WithControlPlane( 927 builder.ControlPlane("barNamespace", "cp1").Build()). 928 Build(), 929 }, 930 { 931 name: "should succeed when namespaces match", 932 expectErr: false, 933 in: builder.Cluster("fooNamespace", "cluster1"). 934 WithInfrastructureCluster( 935 builder.InfrastructureClusterTemplate("fooNamespace", "infra1").Build()). 936 WithControlPlane( 937 builder.ControlPlane("fooNamespace", "cp1").Build()). 938 Build(), 939 }, 940 { 941 name: "fails if topology is set but feature flag is disabled", 942 expectErr: true, 943 in: builder.Cluster("fooNamespace", "cluster1"). 944 WithInfrastructureCluster( 945 builder.InfrastructureClusterTemplate("fooNamespace", "infra1").Build()). 946 WithControlPlane( 947 builder.ControlPlane("fooNamespace", "cp1").Build()). 948 WithTopology(&clusterv1.Topology{}). 949 Build(), 950 }, 951 { 952 name: "pass with undefined CIDR ranges", 953 expectErr: false, 954 in: builder.Cluster("fooNamespace", "cluster1"). 955 WithClusterNetwork(&clusterv1.ClusterNetwork{ 956 Services: &clusterv1.NetworkRanges{ 957 CIDRBlocks: []string{}}, 958 Pods: &clusterv1.NetworkRanges{ 959 CIDRBlocks: []string{}}, 960 }). 961 Build(), 962 }, 963 { 964 name: "pass with nil CIDR ranges", 965 expectErr: false, 966 in: builder.Cluster("fooNamespace", "cluster1"). 967 WithClusterNetwork(&clusterv1.ClusterNetwork{ 968 Services: &clusterv1.NetworkRanges{ 969 CIDRBlocks: nil}, 970 Pods: &clusterv1.NetworkRanges{ 971 CIDRBlocks: nil}, 972 }). 973 Build(), 974 }, 975 { 976 name: "pass with valid IPv4 CIDR ranges", 977 expectErr: false, 978 in: builder.Cluster("fooNamespace", "cluster1"). 979 WithClusterNetwork(&clusterv1.ClusterNetwork{ 980 Services: &clusterv1.NetworkRanges{ 981 CIDRBlocks: []string{"10.10.10.10/24"}}, 982 Pods: &clusterv1.NetworkRanges{ 983 CIDRBlocks: []string{"10.10.10.10/24"}}, 984 }). 985 Build(), 986 }, 987 { 988 name: "pass with valid IPv6 CIDR ranges", 989 expectErr: false, 990 in: builder.Cluster("fooNamespace", "cluster1"). 991 WithClusterNetwork(&clusterv1.ClusterNetwork{ 992 Services: &clusterv1.NetworkRanges{ 993 CIDRBlocks: []string{"2004::1234:abcd:ffff:c0a8:101/64"}}, 994 Pods: &clusterv1.NetworkRanges{ 995 CIDRBlocks: []string{"2004::1234:abcd:ffff:c0a8:101/64"}}, 996 }). 997 Build(), 998 }, 999 { 1000 name: "pass with valid dualstack CIDR ranges", 1001 expectErr: false, 1002 in: builder.Cluster("fooNamespace", "cluster1"). 1003 WithClusterNetwork(&clusterv1.ClusterNetwork{ 1004 Services: &clusterv1.NetworkRanges{ 1005 CIDRBlocks: []string{"2004::1234:abcd:ffff:c0a8:101/64", "10.10.10.10/24"}}, 1006 Pods: &clusterv1.NetworkRanges{ 1007 CIDRBlocks: []string{"2004::1234:abcd:ffff:c0a8:101/64", "10.10.10.10/24"}}, 1008 }). 1009 Build(), 1010 }, 1011 { 1012 name: "pass if multiple CIDR ranges of IPv4 are passed", 1013 expectErr: false, 1014 in: builder.Cluster("fooNamespace", "cluster1"). 1015 WithClusterNetwork(&clusterv1.ClusterNetwork{ 1016 Services: &clusterv1.NetworkRanges{ 1017 CIDRBlocks: []string{"10.10.10.10/24", "11.11.11.11/24"}}, 1018 }). 1019 Build(), 1020 }, 1021 { 1022 name: "pass if multiple CIDR ranges of IPv6 are passed", 1023 expectErr: false, 1024 in: builder.Cluster("fooNamespace", "cluster1"). 1025 WithClusterNetwork(&clusterv1.ClusterNetwork{ 1026 Services: &clusterv1.NetworkRanges{ 1027 CIDRBlocks: []string{"2002::1234:abcd:ffff:c0a8:101/64", "2004::1234:abcd:ffff:c0a8:101/64"}}, 1028 }). 1029 Build(), 1030 }, 1031 { 1032 name: "pass if too many cidr ranges are specified in the clusterNetwork pods field", 1033 expectErr: false, 1034 in: builder.Cluster("fooNamespace", "cluster1"). 1035 WithClusterNetwork(&clusterv1.ClusterNetwork{ 1036 Pods: &clusterv1.NetworkRanges{ 1037 CIDRBlocks: []string{"10.10.10.10/24", "11.11.11.11/24", "12.12.12.12/24"}}}). 1038 Build(), 1039 }, 1040 { 1041 name: "fails if service cidr ranges are not valid", 1042 expectErr: true, 1043 in: builder.Cluster("fooNamespace", "cluster1"). 1044 WithClusterNetwork(&clusterv1.ClusterNetwork{ 1045 Services: &clusterv1.NetworkRanges{ 1046 // Invalid ranges: missing network suffix 1047 CIDRBlocks: []string{"10.10.10.10", "11.11.11.11"}}}). 1048 Build(), 1049 }, 1050 { 1051 name: "fails if pod cidr ranges are not valid", 1052 expectErr: true, 1053 in: builder.Cluster("fooNamespace", "cluster1"). 1054 WithClusterNetwork(&clusterv1.ClusterNetwork{ 1055 Pods: &clusterv1.NetworkRanges{ 1056 // Invalid ranges: missing network suffix 1057 CIDRBlocks: []string{"10.10.10.10", "11.11.11.11"}}}). 1058 Build(), 1059 }, 1060 { 1061 name: "pass with name of under 63 characters", 1062 expectErr: false, 1063 in: builder.Cluster("fooNamespace", "short-name").Build(), 1064 }, 1065 { 1066 name: "pass with _, -, . characters in name", 1067 in: builder.Cluster("fooNamespace", "thisNameContains.A_Non-Alphanumeric").Build(), 1068 expectErr: false, 1069 }, 1070 { 1071 name: "fails if cluster name is longer than 63 characters", 1072 in: builder.Cluster("fooNamespace", "thisNameIsReallyMuchLongerThanTheMaximumLengthOfSixtyThreeCharacters").Build(), 1073 expectErr: true, 1074 }, 1075 { 1076 name: "error when name starts with NonAlphanumeric character", 1077 in: builder.Cluster("fooNamespace", "-thisNameStartsWithANonAlphanumeric").Build(), 1078 expectErr: true, 1079 }, 1080 { 1081 name: "error when name ends with NonAlphanumeric character", 1082 in: builder.Cluster("fooNamespace", "thisNameEndsWithANonAlphanumeric.").Build(), 1083 expectErr: true, 1084 }, 1085 { 1086 name: "error when name contains invalid NonAlphanumeric character", 1087 in: builder.Cluster("fooNamespace", "thisNameContainsInvalid!@NonAlphanumerics").Build(), 1088 expectErr: true, 1089 }, 1090 } 1091 ) 1092 for _, tt := range tests { 1093 t.Run(tt.name, func(t *testing.T) { 1094 g := NewWithT(t) 1095 1096 // Create the webhook. 1097 webhook := &Cluster{} 1098 1099 warnings, err := webhook.validate(ctx, tt.old, tt.in) 1100 g.Expect(warnings).To(BeEmpty()) 1101 if tt.expectErr { 1102 g.Expect(err).To(HaveOccurred()) 1103 return 1104 } 1105 g.Expect(err).ToNot(HaveOccurred()) 1106 }) 1107 } 1108 } 1109 1110 func TestClusterTopologyValidation(t *testing.T) { 1111 // NOTE: ClusterTopology feature flag is disabled by default, thus preventing to set Cluster.Topologies. 1112 // Enabling the feature flag temporarily for this test. 1113 defer utilfeature.SetFeatureGateDuringTest(t, feature.Gates, feature.ClusterTopology, true)() 1114 1115 tests := []struct { 1116 name string 1117 clusterClassStatusVariables []clusterv1.ClusterClassStatusVariable 1118 in *clusterv1.Cluster 1119 old *clusterv1.Cluster 1120 additionalObjects []client.Object 1121 expectErr bool 1122 expectWarning bool 1123 }{ 1124 { 1125 name: "should return error when topology does not have class", 1126 expectErr: true, 1127 in: builder.Cluster("fooboo", "cluster1"). 1128 WithTopology(&clusterv1.Topology{}). 1129 Build(), 1130 }, 1131 { 1132 name: "should return error when topology does not have valid version", 1133 expectErr: true, 1134 in: builder.Cluster("fooboo", "cluster1"). 1135 WithTopology(builder.ClusterTopology(). 1136 WithClass("foo"). 1137 WithVersion("invalid").Build()). 1138 Build(), 1139 }, 1140 { 1141 name: "should return error when downgrading topology version - major", 1142 expectErr: true, 1143 old: builder.Cluster("fooboo", "cluster1"). 1144 WithTopology(builder.ClusterTopology(). 1145 WithClass("foo"). 1146 WithVersion("v2.2.3"). 1147 Build()). 1148 Build(), 1149 in: builder.Cluster("fooboo", "cluster1"). 1150 WithTopology(builder.ClusterTopology(). 1151 WithClass("foo"). 1152 WithVersion("v1.2.3"). 1153 Build()). 1154 Build(), 1155 }, 1156 { 1157 name: "should return error when downgrading topology version - minor", 1158 expectErr: true, 1159 old: builder.Cluster("fooboo", "cluster1"). 1160 WithTopology(builder.ClusterTopology(). 1161 WithClass("foo"). 1162 WithVersion("v1.2.3"). 1163 Build()). 1164 Build(), 1165 in: builder.Cluster("fooboo", "cluster1"). 1166 WithTopology(builder.ClusterTopology(). 1167 WithClass("foo"). 1168 WithVersion("v1.1.3"). 1169 Build()). 1170 Build(), 1171 }, 1172 { 1173 name: "should return error when downgrading topology version - patch", 1174 expectErr: true, 1175 old: builder.Cluster("fooboo", "cluster1"). 1176 WithTopology(builder.ClusterTopology(). 1177 WithClass("foo"). 1178 WithVersion("v1.2.3"). 1179 Build()). 1180 Build(), 1181 in: builder.Cluster("fooboo", "cluster1"). 1182 WithTopology(builder.ClusterTopology(). 1183 WithClass("foo"). 1184 WithVersion("v1.2.2"). 1185 Build()). 1186 Build(), 1187 }, 1188 { 1189 name: "should return error when downgrading topology version - pre-release", 1190 expectErr: true, 1191 old: builder.Cluster("fooboo", "cluster1"). 1192 WithTopology(builder.ClusterTopology(). 1193 WithClass("foo"). 1194 WithVersion("v1.2.3-xyz.2"). 1195 Build()). 1196 Build(), 1197 in: builder.Cluster("fooboo", "cluster1"). 1198 WithTopology(builder.ClusterTopology(). 1199 WithClass("foo"). 1200 WithVersion("v1.2.3-xyz.1"). 1201 Build()). 1202 Build(), 1203 }, 1204 { 1205 name: "should return error when downgrading topology version - build tag", 1206 expectErr: true, 1207 old: builder.Cluster("fooboo", "cluster1"). 1208 WithTopology(builder.ClusterTopology(). 1209 WithClass("foo"). 1210 WithVersion("v1.2.3+xyz.2"). 1211 Build()). 1212 Build(), 1213 in: builder.Cluster("fooboo", "cluster1"). 1214 WithTopology(builder.ClusterTopology(). 1215 WithClass("foo"). 1216 WithVersion("v1.2.3+xyz.1"). 1217 Build()). 1218 Build(), 1219 }, 1220 { 1221 name: "should return error when upgrading +2 minor version", 1222 expectErr: true, 1223 old: builder.Cluster("fooboo", "cluster1"). 1224 WithTopology(builder.ClusterTopology(). 1225 WithClass("foo"). 1226 WithVersion("v1.2.3"). 1227 Build()). 1228 Build(), 1229 in: builder.Cluster("fooboo", "cluster1"). 1230 WithTopology(builder.ClusterTopology(). 1231 WithClass("foo"). 1232 WithVersion("v1.4.0"). 1233 Build()). 1234 Build(), 1235 }, 1236 { 1237 name: "should return error when duplicated MachineDeployments names exists in a Topology", 1238 expectErr: true, 1239 in: builder.Cluster("fooboo", "cluster1"). 1240 WithTopology(builder.ClusterTopology(). 1241 WithClass("foo"). 1242 WithVersion("v1.19.1"). 1243 WithMachineDeployment( 1244 builder.MachineDeploymentTopology("workers1"). 1245 WithClass("aa"). 1246 Build()). 1247 WithMachineDeployment( 1248 builder.MachineDeploymentTopology("workers1"). 1249 WithClass("bb"). 1250 Build()). 1251 Build()). 1252 Build(), 1253 }, 1254 { 1255 name: "should return error when duplicated MachinePools names exists in a Topology", 1256 expectErr: true, 1257 in: builder.Cluster("fooboo", "cluster1"). 1258 WithTopology(builder.ClusterTopology(). 1259 WithClass("foo"). 1260 WithVersion("v1.19.1"). 1261 WithMachinePool( 1262 builder.MachinePoolTopology("workers1"). 1263 WithClass("aa"). 1264 Build()). 1265 WithMachinePool( 1266 builder.MachinePoolTopology("workers1"). 1267 WithClass("bb"). 1268 Build()). 1269 Build()). 1270 Build(), 1271 }, 1272 { 1273 name: "should pass when MachineDeployments names in a Topology are unique", 1274 expectErr: false, 1275 in: builder.Cluster("fooboo", "cluster1"). 1276 WithTopology(builder.ClusterTopology(). 1277 WithClass("foo"). 1278 WithVersion("v1.19.1"). 1279 WithMachineDeployment( 1280 builder.MachineDeploymentTopology("workers1"). 1281 WithClass("aa"). 1282 Build()). 1283 WithMachineDeployment( 1284 builder.MachineDeploymentTopology("workers2"). 1285 WithClass("bb"). 1286 Build()). 1287 Build()). 1288 Build(), 1289 }, 1290 { 1291 name: "should pass when MachinePools names in a Topology are unique", 1292 expectErr: false, 1293 in: builder.Cluster("fooboo", "cluster1"). 1294 WithTopology(builder.ClusterTopology(). 1295 WithClass("foo"). 1296 WithVersion("v1.19.1"). 1297 WithMachinePool( 1298 builder.MachinePoolTopology("workers1"). 1299 WithClass("aa"). 1300 Build()). 1301 WithMachinePool( 1302 builder.MachinePoolTopology("workers2"). 1303 WithClass("bb"). 1304 Build()). 1305 Build()). 1306 Build(), 1307 }, 1308 { 1309 name: "should update", 1310 expectErr: false, 1311 old: builder.Cluster("fooboo", "cluster1"). 1312 WithTopology(builder.ClusterTopology(). 1313 WithClass("foo"). 1314 WithVersion("v1.19.1"). 1315 WithMachineDeployment( 1316 builder.MachineDeploymentTopology("workers1"). 1317 WithClass("aa"). 1318 Build()). 1319 WithMachineDeployment( 1320 builder.MachineDeploymentTopology("workers2"). 1321 WithClass("bb"). 1322 Build()). 1323 WithMachinePool( 1324 builder.MachinePoolTopology("workers1"). 1325 WithClass("aa"). 1326 Build()). 1327 WithMachinePool( 1328 builder.MachinePoolTopology("workers2"). 1329 WithClass("bb"). 1330 Build()). 1331 Build()). 1332 Build(), 1333 in: builder.Cluster("fooboo", "cluster1"). 1334 WithTopology(builder.ClusterTopology(). 1335 WithClass("foo"). 1336 WithVersion("v1.19.2"). 1337 WithMachineDeployment( 1338 builder.MachineDeploymentTopology("workers1"). 1339 WithClass("aa"). 1340 Build()). 1341 WithMachineDeployment( 1342 builder.MachineDeploymentTopology("workers2"). 1343 WithClass("bb"). 1344 Build()). 1345 WithMachinePool( 1346 builder.MachinePoolTopology("workers1"). 1347 WithClass("aa"). 1348 Build()). 1349 WithMachinePool( 1350 builder.MachinePoolTopology("workers2"). 1351 WithClass("bb"). 1352 Build()). 1353 Build()). 1354 Build(), 1355 }, 1356 { 1357 name: "should return error when upgrade concurrency annotation value is < 1", 1358 expectErr: true, 1359 in: builder.Cluster("fooboo", "cluster1"). 1360 WithAnnotations(map[string]string{ 1361 clusterv1.ClusterTopologyUpgradeConcurrencyAnnotation: "-1", 1362 }). 1363 WithTopology(builder.ClusterTopology(). 1364 WithClass("foo"). 1365 WithVersion("v1.19.2"). 1366 Build()). 1367 Build(), 1368 }, 1369 { 1370 name: "should return error when upgrade concurrency annotation value is not numeric", 1371 expectErr: true, 1372 in: builder.Cluster("fooboo", "cluster1"). 1373 WithAnnotations(map[string]string{ 1374 clusterv1.ClusterTopologyUpgradeConcurrencyAnnotation: "abc", 1375 }). 1376 WithTopology(builder.ClusterTopology(). 1377 WithClass("foo"). 1378 WithVersion("v1.19.2"). 1379 Build()). 1380 Build(), 1381 }, 1382 { 1383 name: "should pass upgrade concurrency annotation value is >= 1", 1384 expectErr: false, 1385 in: builder.Cluster("fooboo", "cluster1"). 1386 WithAnnotations(map[string]string{ 1387 clusterv1.ClusterTopologyUpgradeConcurrencyAnnotation: "2", 1388 }). 1389 WithTopology(builder.ClusterTopology(). 1390 WithClass("foo"). 1391 WithVersion("v1.19.2"). 1392 Build()). 1393 Build(), 1394 }, 1395 { 1396 name: "should update if cluster is fully upgraded and up to date", 1397 expectErr: false, 1398 old: builder.Cluster("fooboo", "cluster1"). 1399 WithControlPlane(builder.ControlPlane("fooboo", "cluster1-cp").Build()). 1400 WithTopology(builder.ClusterTopology(). 1401 WithClass("foo"). 1402 WithVersion("v1.19.1"). 1403 WithMachineDeployment( 1404 builder.MachineDeploymentTopology("workers1"). 1405 WithClass("aa"). 1406 Build()). 1407 WithMachinePool( 1408 builder.MachinePoolTopology("pool1"). 1409 WithClass("aa"). 1410 Build()). 1411 Build()). 1412 Build(), 1413 in: builder.Cluster("fooboo", "cluster1"). 1414 WithControlPlane(builder.ControlPlane("fooboo", "cluster1-cp").Build()). 1415 WithTopology(builder.ClusterTopology(). 1416 WithClass("foo"). 1417 WithVersion("v1.20.2"). 1418 Build()). 1419 Build(), 1420 additionalObjects: []client.Object{ 1421 builder.ControlPlane("fooboo", "cluster1-cp").WithVersion("v1.19.1"). 1422 WithStatusFields(map[string]interface{}{"status.version": "v1.19.1"}). 1423 Build(), 1424 builder.MachineDeployment("fooboo", "cluster1-workers1").WithLabels(map[string]string{ 1425 clusterv1.ClusterNameLabel: "cluster1", 1426 clusterv1.ClusterTopologyOwnedLabel: "", 1427 clusterv1.ClusterTopologyMachineDeploymentNameLabel: "workers1", 1428 }).WithVersion("v1.19.1").Build(), 1429 builder.MachinePool("fooboo", "cluster1-pool1").WithLabels(map[string]string{ 1430 clusterv1.ClusterNameLabel: "cluster1", 1431 clusterv1.ClusterTopologyOwnedLabel: "", 1432 clusterv1.ClusterTopologyMachinePoolNameLabel: "pool1", 1433 }).WithVersion("v1.19.1").Build(), 1434 }, 1435 }, 1436 { 1437 name: "should skip validation if cluster kcp is not yet provisioned but annotation is set", 1438 expectErr: false, 1439 expectWarning: true, 1440 old: builder.Cluster("fooboo", "cluster1"). 1441 WithControlPlane(builder.ControlPlane("fooboo", "cluster1-cp").Build()). 1442 WithTopology(builder.ClusterTopology(). 1443 WithClass("foo"). 1444 WithVersion("v1.19.1"). 1445 Build()). 1446 Build(), 1447 in: builder.Cluster("fooboo", "cluster1"). 1448 WithControlPlane(builder.ControlPlane("fooboo", "cluster1-cp").Build()). 1449 WithAnnotations(map[string]string{clusterv1.ClusterTopologyUnsafeUpdateVersionAnnotation: "true"}). 1450 WithTopology(builder.ClusterTopology(). 1451 WithClass("foo"). 1452 WithVersion("v1.20.2"). 1453 Build()). 1454 Build(), 1455 additionalObjects: []client.Object{ 1456 builder.ControlPlane("fooboo", "cluster1-cp").WithVersion("v1.18.1").Build(), 1457 }, 1458 }, 1459 { 1460 name: "should block update if cluster kcp is not yet provisioned", 1461 expectErr: true, 1462 old: builder.Cluster("fooboo", "cluster1"). 1463 WithControlPlane(builder.ControlPlane("fooboo", "cluster1-cp").Build()). 1464 WithTopology(builder.ClusterTopology(). 1465 WithClass("foo"). 1466 WithVersion("v1.19.1"). 1467 Build()). 1468 Build(), 1469 in: builder.Cluster("fooboo", "cluster1"). 1470 WithControlPlane(builder.ControlPlane("fooboo", "cluster1-cp").Build()). 1471 WithTopology(builder.ClusterTopology(). 1472 WithClass("foo"). 1473 WithVersion("v1.20.2"). 1474 Build()). 1475 Build(), 1476 additionalObjects: []client.Object{ 1477 builder.ControlPlane("fooboo", "cluster1-cp").WithVersion("v1.18.1").Build(), 1478 }, 1479 }, 1480 { 1481 name: "should block update if md is not yet upgraded", 1482 expectErr: true, 1483 old: builder.Cluster("fooboo", "cluster1"). 1484 WithControlPlane(builder.ControlPlane("fooboo", "cluster1-cp").Build()). 1485 WithTopology(builder.ClusterTopology(). 1486 WithClass("foo"). 1487 WithVersion("v1.19.1"). 1488 WithMachineDeployment( 1489 builder.MachineDeploymentTopology("workers1"). 1490 WithClass("aa"). 1491 Build()). 1492 Build()). 1493 Build(), 1494 in: builder.Cluster("fooboo", "cluster1"). 1495 WithControlPlane(builder.ControlPlane("fooboo", "cluster1-cp").Build()). 1496 WithTopology(builder.ClusterTopology(). 1497 WithClass("foo"). 1498 WithVersion("v1.20.2"). 1499 Build()). 1500 Build(), 1501 additionalObjects: []client.Object{ 1502 builder.ControlPlane("fooboo", "cluster1-cp").WithVersion("v1.19.1"). 1503 WithStatusFields(map[string]interface{}{"status.version": "v1.19.1"}). 1504 Build(), 1505 builder.MachineDeployment("fooboo", "cluster1-workers1").WithLabels(map[string]string{ 1506 clusterv1.ClusterNameLabel: "cluster1", 1507 clusterv1.ClusterTopologyOwnedLabel: "", 1508 clusterv1.ClusterTopologyMachineDeploymentNameLabel: "workers1", 1509 }).WithVersion("v1.18.1").Build(), 1510 }, 1511 }, 1512 { 1513 name: "should block update if mp is not yet upgraded", 1514 expectErr: true, 1515 old: builder.Cluster("fooboo", "cluster1"). 1516 WithControlPlane(builder.ControlPlane("fooboo", "cluster1-cp").Build()). 1517 WithTopology(builder.ClusterTopology(). 1518 WithClass("foo"). 1519 WithVersion("v1.19.1"). 1520 WithMachinePool( 1521 builder.MachinePoolTopology("pool1"). 1522 WithClass("aa"). 1523 Build()). 1524 Build()). 1525 Build(), 1526 in: builder.Cluster("fooboo", "cluster1"). 1527 WithControlPlane(builder.ControlPlane("fooboo", "cluster1-cp").Build()). 1528 WithTopology(builder.ClusterTopology(). 1529 WithClass("foo"). 1530 WithVersion("v1.20.2"). 1531 Build()). 1532 Build(), 1533 additionalObjects: []client.Object{ 1534 builder.ControlPlane("fooboo", "cluster1-cp").WithVersion("v1.19.1"). 1535 WithStatusFields(map[string]interface{}{"status.version": "v1.19.1"}). 1536 Build(), 1537 builder.MachinePool("fooboo", "cluster1-pool1").WithLabels(map[string]string{ 1538 clusterv1.ClusterNameLabel: "cluster1", 1539 clusterv1.ClusterTopologyOwnedLabel: "", 1540 clusterv1.ClusterTopologyMachinePoolNameLabel: "pool1", 1541 }).WithVersion("v1.18.1").Build(), 1542 }, 1543 }, 1544 } 1545 for _, tt := range tests { 1546 t.Run(tt.name, func(t *testing.T) { 1547 g := NewWithT(t) 1548 class := builder.ClusterClass("fooboo", "foo"). 1549 WithWorkerMachineDeploymentClasses( 1550 *builder.MachineDeploymentClass("bb").Build(), 1551 *builder.MachineDeploymentClass("aa").Build(), 1552 ). 1553 WithWorkerMachinePoolClasses( 1554 *builder.MachinePoolClass("bb").Build(), 1555 *builder.MachinePoolClass("aa").Build(), 1556 ). 1557 WithStatusVariables(tt.clusterClassStatusVariables...). 1558 Build() 1559 1560 // Mark this condition to true so the webhook sees the ClusterClass as up to date. 1561 conditions.MarkTrue(class, clusterv1.ClusterClassVariablesReconciledCondition) 1562 // Sets up the fakeClient for the test case. 1563 fakeClient := fake.NewClientBuilder(). 1564 WithObjects(class). 1565 WithObjects(tt.additionalObjects...). 1566 WithScheme(fakeScheme). 1567 Build() 1568 1569 // Use an empty fakeClusterCacheTracker here because the real cases are tested in Test_validateTopologyMachinePoolVersions. 1570 fakeClusterCacheTrackerReader := &fakeClusterCacheTracker{client: fake.NewFakeClient()} 1571 1572 // Create the webhook and add the fakeClient as its client. This is required because the test uses a Managed Topology. 1573 webhook := &Cluster{Client: fakeClient, Tracker: fakeClusterCacheTrackerReader} 1574 1575 warnings, err := webhook.validate(ctx, tt.old, tt.in) 1576 if tt.expectErr { 1577 g.Expect(err).To(HaveOccurred()) 1578 } else { 1579 g.Expect(err).ToNot(HaveOccurred()) 1580 } 1581 if tt.expectWarning { 1582 g.Expect(warnings).ToNot(BeEmpty()) 1583 } else { 1584 g.Expect(warnings).To(BeEmpty()) 1585 } 1586 }) 1587 } 1588 } 1589 1590 // TestClusterTopologyValidationWithClient tests the additional cases introduced in new validation in the webhook package. 1591 func TestClusterTopologyValidationWithClient(t *testing.T) { 1592 defer utilfeature.SetFeatureGateDuringTest(t, feature.Gates, feature.ClusterTopology, true)() 1593 g := NewWithT(t) 1594 1595 tests := []struct { 1596 name string 1597 cluster *clusterv1.Cluster 1598 class *clusterv1.ClusterClass 1599 classReconciled bool 1600 objects []client.Object 1601 wantErr bool 1602 wantWarnings bool 1603 }{ 1604 { 1605 name: "Accept a cluster with an existing ClusterClass named in cluster.spec.topology.class", 1606 cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1"). 1607 WithTopology( 1608 builder.ClusterTopology(). 1609 WithClass("clusterclass"). 1610 WithVersion("v1.22.2"). 1611 WithControlPlaneReplicas(3). 1612 Build()). 1613 Build(), 1614 class: builder.ClusterClass(metav1.NamespaceDefault, "clusterclass"). 1615 Build(), 1616 classReconciled: true, 1617 wantErr: false, 1618 }, 1619 { 1620 name: "Warning for a cluster with non-existent ClusterClass referenced cluster.spec.topology.class", 1621 cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1"). 1622 WithTopology( 1623 builder.ClusterTopology(). 1624 WithClass("wrongName"). 1625 WithVersion("v1.22.2"). 1626 WithControlPlaneReplicas(3). 1627 Build()). 1628 Build(), 1629 class: builder.ClusterClass(metav1.NamespaceDefault, "clusterclass"). 1630 Build(), 1631 // There should be a warning for a ClusterClass which can not be found. 1632 wantWarnings: true, 1633 wantErr: false, 1634 }, 1635 { 1636 name: "Warning for a cluster with an unreconciled ClusterClass named in cluster.spec.topology.class", 1637 cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1"). 1638 WithTopology( 1639 builder.ClusterTopology(). 1640 WithClass("clusterclass"). 1641 WithVersion("v1.22.2"). 1642 WithControlPlaneReplicas(3). 1643 Build()). 1644 Build(), 1645 class: builder.ClusterClass(metav1.NamespaceDefault, "clusterclass"). 1646 Build(), 1647 classReconciled: false, 1648 // There should be a warning for a ClusterClass which is not yet reconciled. 1649 wantWarnings: true, 1650 wantErr: false, 1651 }, 1652 { 1653 name: "Reject a cluster that has MHC enabled for control plane but is missing MHC definition in cluster topology and clusterclass", 1654 cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1"). 1655 WithTopology( 1656 builder.ClusterTopology(). 1657 WithClass("clusterclass"). 1658 WithVersion("v1.22.2"). 1659 WithControlPlaneReplicas(3). 1660 WithControlPlaneMachineHealthCheck(&clusterv1.MachineHealthCheckTopology{ 1661 Enable: ptr.To(true), 1662 }). 1663 Build()). 1664 Build(), 1665 class: builder.ClusterClass(metav1.NamespaceDefault, "clusterclass"). 1666 Build(), 1667 classReconciled: true, 1668 wantErr: true, 1669 }, 1670 { 1671 name: "Reject a cluster that MHC override defined for control plane but is missing unhealthy conditions", 1672 cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1"). 1673 WithTopology( 1674 builder.ClusterTopology(). 1675 WithClass("clusterclass"). 1676 WithVersion("v1.22.2"). 1677 WithControlPlaneReplicas(3). 1678 WithControlPlaneMachineHealthCheck(&clusterv1.MachineHealthCheckTopology{ 1679 MachineHealthCheckClass: clusterv1.MachineHealthCheckClass{ 1680 UnhealthyConditions: []clusterv1.UnhealthyCondition{}, 1681 }, 1682 }). 1683 Build()). 1684 Build(), 1685 class: builder.ClusterClass(metav1.NamespaceDefault, "clusterclass"). 1686 Build(), 1687 classReconciled: true, 1688 wantErr: true, 1689 }, 1690 { 1691 name: "Reject a cluster that MHC override defined for control plane but is set when control plane is missing machineInfrastructure", 1692 cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1"). 1693 WithTopology( 1694 builder.ClusterTopology(). 1695 WithClass("clusterclass"). 1696 WithVersion("v1.22.2"). 1697 WithControlPlaneReplicas(3). 1698 WithControlPlaneMachineHealthCheck(&clusterv1.MachineHealthCheckTopology{ 1699 MachineHealthCheckClass: clusterv1.MachineHealthCheckClass{ 1700 UnhealthyConditions: []clusterv1.UnhealthyCondition{ 1701 { 1702 Type: corev1.NodeReady, 1703 Status: corev1.ConditionFalse, 1704 }, 1705 }, 1706 }, 1707 }). 1708 Build()). 1709 Build(), 1710 class: builder.ClusterClass(metav1.NamespaceDefault, "clusterclass"). 1711 Build(), 1712 classReconciled: true, 1713 wantErr: true, 1714 }, 1715 { 1716 name: "Accept a cluster that has MHC enabled for control plane with control plane MHC defined in ClusterClass", 1717 cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1"). 1718 WithTopology( 1719 builder.ClusterTopology(). 1720 WithClass("clusterclass"). 1721 WithVersion("v1.22.2"). 1722 WithControlPlaneReplicas(3). 1723 WithControlPlaneMachineHealthCheck(&clusterv1.MachineHealthCheckTopology{ 1724 Enable: ptr.To(true), 1725 }). 1726 Build()). 1727 Build(), 1728 class: builder.ClusterClass(metav1.NamespaceDefault, "clusterclass"). 1729 WithControlPlaneMachineHealthCheck(&clusterv1.MachineHealthCheckClass{}). 1730 Build(), 1731 classReconciled: true, 1732 wantErr: false, 1733 }, 1734 { 1735 name: "Accept a cluster that has MHC enabled for control plane with control plane MHC defined in cluster topology", 1736 cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1"). 1737 WithTopology( 1738 builder.ClusterTopology(). 1739 WithClass("clusterclass"). 1740 WithVersion("v1.22.2"). 1741 WithControlPlaneReplicas(3). 1742 WithControlPlaneMachineHealthCheck(&clusterv1.MachineHealthCheckTopology{ 1743 Enable: ptr.To(true), 1744 MachineHealthCheckClass: clusterv1.MachineHealthCheckClass{ 1745 UnhealthyConditions: []clusterv1.UnhealthyCondition{ 1746 { 1747 Type: corev1.NodeReady, 1748 Status: corev1.ConditionFalse, 1749 }, 1750 }, 1751 }, 1752 }). 1753 Build()). 1754 Build(), 1755 class: builder.ClusterClass(metav1.NamespaceDefault, "clusterclass"). 1756 WithControlPlaneInfrastructureMachineTemplate(&unstructured.Unstructured{}). 1757 Build(), 1758 classReconciled: true, 1759 wantErr: false, 1760 }, 1761 { 1762 name: "Reject a cluster that has MHC enabled for machine deployment but is missing MHC definition in cluster topology and ClusterClass", 1763 cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1"). 1764 WithTopology( 1765 builder.ClusterTopology(). 1766 WithClass("clusterclass"). 1767 WithVersion("v1.22.2"). 1768 WithControlPlaneReplicas(3). 1769 WithMachineDeployment( 1770 builder.MachineDeploymentTopology("md1"). 1771 WithClass("worker-class"). 1772 WithMachineHealthCheck(&clusterv1.MachineHealthCheckTopology{ 1773 Enable: ptr.To(true), 1774 }). 1775 Build(), 1776 ). 1777 Build()). 1778 Build(), 1779 class: builder.ClusterClass(metav1.NamespaceDefault, "clusterclass"). 1780 WithWorkerMachineDeploymentClasses( 1781 *builder.MachineDeploymentClass("worker-class").Build(), 1782 ). 1783 Build(), 1784 classReconciled: true, 1785 wantErr: true, 1786 }, 1787 { 1788 name: "Reject a cluster that has MHC override defined for machine deployment but is missing unhealthy conditions", 1789 cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1"). 1790 WithTopology( 1791 builder.ClusterTopology(). 1792 WithClass("clusterclass"). 1793 WithVersion("v1.22.2"). 1794 WithControlPlaneReplicas(3). 1795 WithMachineDeployment( 1796 builder.MachineDeploymentTopology("md1"). 1797 WithClass("worker-class"). 1798 WithMachineHealthCheck(&clusterv1.MachineHealthCheckTopology{ 1799 MachineHealthCheckClass: clusterv1.MachineHealthCheckClass{ 1800 UnhealthyConditions: []clusterv1.UnhealthyCondition{}, 1801 }, 1802 }). 1803 Build(), 1804 ). 1805 Build()). 1806 Build(), 1807 class: builder.ClusterClass(metav1.NamespaceDefault, "clusterclass"). 1808 WithWorkerMachineDeploymentClasses( 1809 *builder.MachineDeploymentClass("worker-class").Build(), 1810 ). 1811 Build(), 1812 classReconciled: true, 1813 wantErr: true, 1814 }, 1815 { 1816 name: "Accept a cluster that has MHC enabled for machine deployment with machine deployment MHC defined in ClusterClass", 1817 cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1"). 1818 WithTopology( 1819 builder.ClusterTopology(). 1820 WithClass("clusterclass"). 1821 WithVersion("v1.22.2"). 1822 WithControlPlaneReplicas(3). 1823 WithMachineDeployment( 1824 builder.MachineDeploymentTopology("md1"). 1825 WithClass("worker-class"). 1826 WithMachineHealthCheck(&clusterv1.MachineHealthCheckTopology{ 1827 Enable: ptr.To(true), 1828 }). 1829 Build(), 1830 ). 1831 Build()). 1832 Build(), 1833 class: builder.ClusterClass(metav1.NamespaceDefault, "clusterclass"). 1834 WithWorkerMachineDeploymentClasses( 1835 *builder.MachineDeploymentClass("worker-class"). 1836 WithMachineHealthCheckClass(&clusterv1.MachineHealthCheckClass{}). 1837 Build(), 1838 ). 1839 Build(), 1840 classReconciled: true, 1841 wantErr: false, 1842 }, 1843 { 1844 name: "Accept a cluster that has MHC enabled for machine deployment with machine deployment MHC defined in cluster topology", 1845 cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1"). 1846 WithTopology( 1847 builder.ClusterTopology(). 1848 WithClass("clusterclass"). 1849 WithVersion("v1.22.2"). 1850 WithControlPlaneReplicas(3). 1851 WithMachineDeployment( 1852 builder.MachineDeploymentTopology("md1"). 1853 WithClass("worker-class"). 1854 WithMachineHealthCheck(&clusterv1.MachineHealthCheckTopology{ 1855 Enable: ptr.To(true), 1856 MachineHealthCheckClass: clusterv1.MachineHealthCheckClass{ 1857 UnhealthyConditions: []clusterv1.UnhealthyCondition{ 1858 { 1859 Type: corev1.NodeReady, 1860 Status: corev1.ConditionFalse, 1861 }, 1862 }, 1863 }, 1864 }). 1865 Build(), 1866 ). 1867 Build()). 1868 Build(), 1869 class: builder.ClusterClass(metav1.NamespaceDefault, "clusterclass"). 1870 WithWorkerMachineDeploymentClasses( 1871 *builder.MachineDeploymentClass("worker-class").Build(), 1872 ). 1873 Build(), 1874 classReconciled: true, 1875 wantErr: false, 1876 }, 1877 } 1878 for _, tt := range tests { 1879 t.Run(tt.name, func(*testing.T) { 1880 // Mark this condition to true so the webhook sees the ClusterClass as up to date. 1881 if tt.classReconciled { 1882 conditions.MarkTrue(tt.class, clusterv1.ClusterClassVariablesReconciledCondition) 1883 } 1884 // Sets up the fakeClient for the test case. 1885 fakeClient := fake.NewClientBuilder(). 1886 WithObjects(tt.class). 1887 WithScheme(fakeScheme). 1888 Build() 1889 1890 // Create the webhook and add the fakeClient as its client. This is required because the test uses a Managed Topology. 1891 c := &Cluster{Client: fakeClient} 1892 1893 // Checks the return error. 1894 warnings, err := c.ValidateCreate(ctx, tt.cluster) 1895 if tt.wantErr { 1896 g.Expect(err).To(HaveOccurred()) 1897 } else { 1898 g.Expect(err).ToNot(HaveOccurred()) 1899 } 1900 if tt.wantWarnings { 1901 g.Expect(warnings).ToNot(BeEmpty()) 1902 } else { 1903 g.Expect(warnings).To(BeEmpty()) 1904 } 1905 }) 1906 } 1907 } 1908 1909 // TestClusterTopologyValidationForTopologyClassChange cases where cluster.spec.topology.class is altered. 1910 func TestClusterTopologyValidationForTopologyClassChange(t *testing.T) { 1911 defer utilfeature.SetFeatureGateDuringTest(t, feature.Gates, feature.ClusterTopology, true)() 1912 g := NewWithT(t) 1913 1914 cluster := builder.Cluster(metav1.NamespaceDefault, "cluster1"). 1915 WithTopology( 1916 builder.ClusterTopology(). 1917 WithClass("class1"). 1918 WithVersion("v1.22.2"). 1919 WithControlPlaneReplicas(3). 1920 Build()). 1921 Build() 1922 1923 ref := &corev1.ObjectReference{ 1924 APIVersion: "group.test.io/foo", 1925 Kind: "barTemplate", 1926 Name: "baz", 1927 Namespace: "default", 1928 } 1929 compatibleNameChangeRef := &corev1.ObjectReference{ 1930 APIVersion: "group.test.io/foo", 1931 Kind: "barTemplate", 1932 Name: "differentbaz", 1933 Namespace: "default", 1934 } 1935 compatibleAPIVersionChangeRef := &corev1.ObjectReference{ 1936 APIVersion: "group.test.io/foo2", 1937 Kind: "barTemplate", 1938 Name: "differentbaz", 1939 Namespace: "default", 1940 } 1941 incompatibleKindRef := &corev1.ObjectReference{ 1942 APIVersion: "group.test.io/foo", 1943 Kind: "another-barTemplate", 1944 Name: "another-baz", 1945 Namespace: "default", 1946 } 1947 incompatibleAPIGroupRef := &corev1.ObjectReference{ 1948 APIVersion: "group.nottest.io/foo", 1949 Kind: "barTemplate", 1950 Name: "another-baz", 1951 Namespace: "default", 1952 } 1953 1954 tests := []struct { 1955 name string 1956 cluster *clusterv1.Cluster 1957 firstClass *clusterv1.ClusterClass 1958 secondClass *clusterv1.ClusterClass 1959 wantErr bool 1960 }{ 1961 // InfrastructureCluster changes. 1962 { 1963 name: "Accept cluster.topology.class change with a compatible infrastructureCluster Kind ref change", 1964 firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). 1965 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 1966 WithControlPlaneTemplate(refToUnstructured(ref)). 1967 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). 1968 Build(), 1969 secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2"). 1970 WithInfrastructureClusterTemplate(refToUnstructured(compatibleNameChangeRef)). 1971 WithControlPlaneTemplate(refToUnstructured(ref)). 1972 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). 1973 Build(), 1974 wantErr: false, 1975 }, 1976 { 1977 name: "Accept cluster.topology.class change with a compatible infrastructureCluster APIVersion ref change", 1978 firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). 1979 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 1980 WithControlPlaneTemplate(refToUnstructured(ref)). 1981 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). 1982 Build(), 1983 secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2"). 1984 WithInfrastructureClusterTemplate(refToUnstructured(compatibleAPIVersionChangeRef)). 1985 WithControlPlaneTemplate(refToUnstructured(ref)). 1986 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). 1987 Build(), 1988 wantErr: false, 1989 }, 1990 1991 { 1992 name: "Reject cluster.topology.class change with an incompatible infrastructureCluster Kind ref change", 1993 firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). 1994 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 1995 WithControlPlaneTemplate(refToUnstructured(ref)). 1996 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). 1997 Build(), 1998 secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2"). 1999 WithInfrastructureClusterTemplate(refToUnstructured(incompatibleKindRef)). 2000 WithControlPlaneTemplate(refToUnstructured(ref)). 2001 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). 2002 Build(), 2003 wantErr: true, 2004 }, 2005 { 2006 name: "Reject cluster.topology.class change with an incompatible infrastructureCluster APIGroup ref change", 2007 firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). 2008 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 2009 WithControlPlaneTemplate(refToUnstructured(ref)). 2010 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). 2011 Build(), 2012 secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2"). 2013 WithInfrastructureClusterTemplate(refToUnstructured(incompatibleAPIGroupRef)). 2014 WithControlPlaneTemplate(refToUnstructured(ref)). 2015 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). 2016 Build(), 2017 wantErr: true, 2018 }, 2019 2020 // ControlPlane changes. 2021 { 2022 name: "Accept cluster.topology.class change with a compatible controlPlaneTemplate ref change", 2023 firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). 2024 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 2025 WithControlPlaneTemplate(refToUnstructured(ref)). 2026 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). 2027 Build(), 2028 secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2"). 2029 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 2030 WithControlPlaneTemplate(refToUnstructured(compatibleNameChangeRef)). 2031 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). 2032 Build(), 2033 wantErr: false, 2034 }, 2035 { 2036 name: "Accept cluster.topology.class change with a compatible controlPlaneTemplate ref change", 2037 firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). 2038 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 2039 WithControlPlaneTemplate(refToUnstructured(ref)). 2040 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). 2041 Build(), 2042 secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2"). 2043 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 2044 WithControlPlaneTemplate(refToUnstructured(compatibleAPIVersionChangeRef)). 2045 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). 2046 Build(), 2047 wantErr: false, 2048 }, 2049 2050 { 2051 name: "Reject cluster.topology.class change with an incompatible controlPlane Kind ref change", 2052 firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). 2053 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 2054 WithControlPlaneTemplate(refToUnstructured(ref)). 2055 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). 2056 Build(), 2057 secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2"). 2058 WithInfrastructureClusterTemplate(refToUnstructured(incompatibleKindRef)). 2059 WithControlPlaneTemplate(refToUnstructured(ref)). 2060 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). 2061 Build(), 2062 wantErr: true, 2063 }, 2064 { 2065 name: "Reject cluster.topology.class change with an incompatible controlPlane APIVersion ref change", 2066 firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). 2067 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 2068 WithControlPlaneTemplate(refToUnstructured(ref)). 2069 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). 2070 Build(), 2071 secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2"). 2072 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 2073 WithControlPlaneTemplate(refToUnstructured(incompatibleAPIGroupRef)). 2074 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(compatibleNameChangeRef)). 2075 Build(), 2076 wantErr: true, 2077 }, 2078 { 2079 name: "Accept cluster.topology.class change with a compatible controlPlane.MachineInfrastructure ref change", 2080 firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). 2081 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 2082 WithControlPlaneTemplate(refToUnstructured(ref)). 2083 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). 2084 Build(), 2085 secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2"). 2086 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 2087 WithControlPlaneTemplate(refToUnstructured(ref)). 2088 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(compatibleNameChangeRef)). 2089 Build(), 2090 wantErr: false, 2091 }, 2092 { 2093 name: "Accept cluster.topology.class change with a compatible controlPlane.MachineInfrastructure ref change", 2094 firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). 2095 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 2096 WithControlPlaneTemplate(refToUnstructured(ref)). 2097 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). 2098 Build(), 2099 secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2"). 2100 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 2101 WithControlPlaneTemplate(refToUnstructured(ref)). 2102 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(compatibleAPIVersionChangeRef)). 2103 Build(), 2104 wantErr: false, 2105 }, 2106 { 2107 name: "Reject cluster.topology.class change with an incompatible controlPlane.MachineInfrastructure Kind ref change", 2108 firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). 2109 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 2110 WithControlPlaneTemplate(refToUnstructured(ref)). 2111 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). 2112 Build(), 2113 secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2"). 2114 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 2115 WithControlPlaneTemplate(refToUnstructured(ref)). 2116 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(incompatibleKindRef)). 2117 Build(), 2118 wantErr: true, 2119 }, 2120 { 2121 name: "Reject cluster.topology.class change with an incompatible controlPlane.MachineInfrastructure APIVersion ref change", 2122 firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). 2123 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 2124 WithControlPlaneTemplate(refToUnstructured(ref)). 2125 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). 2126 Build(), 2127 secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2"). 2128 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 2129 WithControlPlaneTemplate(refToUnstructured(ref)). 2130 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(incompatibleAPIGroupRef)). 2131 Build(), 2132 wantErr: true, 2133 }, 2134 2135 // MachineDeploymentClass & MachinePoolClass changes 2136 { 2137 name: "Accept cluster.topology.class change with a compatible MachineDeploymentClass and MachinePoolClass InfrastructureTemplate", 2138 firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). 2139 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 2140 WithControlPlaneTemplate(refToUnstructured(ref)). 2141 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). 2142 WithWorkerMachineDeploymentClasses( 2143 *builder.MachineDeploymentClass("aa"). 2144 WithInfrastructureTemplate(refToUnstructured(ref)). 2145 WithBootstrapTemplate(refToUnstructured(ref)). 2146 Build(), 2147 ). 2148 WithWorkerMachinePoolClasses( 2149 *builder.MachinePoolClass("aa"). 2150 WithInfrastructureTemplate(refToUnstructured(ref)). 2151 WithBootstrapTemplate(refToUnstructured(ref)). 2152 Build(), 2153 ). 2154 Build(), 2155 secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2"). 2156 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 2157 WithControlPlaneTemplate(refToUnstructured(ref)). 2158 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). 2159 WithWorkerMachineDeploymentClasses( 2160 *builder.MachineDeploymentClass("aa"). 2161 WithInfrastructureTemplate(refToUnstructured(compatibleNameChangeRef)). 2162 WithBootstrapTemplate(refToUnstructured(ref)). 2163 Build(), 2164 ). 2165 WithWorkerMachinePoolClasses( 2166 *builder.MachinePoolClass("aa"). 2167 WithInfrastructureTemplate(refToUnstructured(compatibleNameChangeRef)). 2168 WithBootstrapTemplate(refToUnstructured(ref)). 2169 Build(), 2170 ). 2171 Build(), 2172 wantErr: false, 2173 }, 2174 { 2175 name: "Accept cluster.topology.class change with an incompatible MachineDeploymentClass and MachinePoolClass BootstrapTemplate", 2176 firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). 2177 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 2178 WithControlPlaneTemplate(refToUnstructured(ref)). 2179 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). 2180 WithWorkerMachineDeploymentClasses( 2181 *builder.MachineDeploymentClass("aa"). 2182 WithInfrastructureTemplate(refToUnstructured(ref)). 2183 WithBootstrapTemplate(refToUnstructured(ref)). 2184 Build(), 2185 ). 2186 WithWorkerMachinePoolClasses( 2187 *builder.MachinePoolClass("aa"). 2188 WithInfrastructureTemplate(refToUnstructured(ref)). 2189 WithBootstrapTemplate(refToUnstructured(ref)). 2190 Build(), 2191 ). 2192 Build(), 2193 secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2"). 2194 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 2195 WithControlPlaneTemplate(refToUnstructured(ref)). 2196 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). 2197 WithWorkerMachineDeploymentClasses( 2198 *builder.MachineDeploymentClass("aa"). 2199 WithInfrastructureTemplate(refToUnstructured(compatibleNameChangeRef)). 2200 WithBootstrapTemplate(refToUnstructured(incompatibleKindRef)). 2201 Build(), 2202 ). 2203 WithWorkerMachinePoolClasses( 2204 *builder.MachinePoolClass("aa"). 2205 WithInfrastructureTemplate(refToUnstructured(compatibleNameChangeRef)). 2206 WithBootstrapTemplate(refToUnstructured(incompatibleKindRef)). 2207 Build(), 2208 ). 2209 Build(), 2210 wantErr: false, 2211 }, 2212 { 2213 name: "Accept cluster.topology.class change with a deleted MachineDeploymentClass and MachinePoolClass", 2214 firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). 2215 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 2216 WithControlPlaneTemplate(refToUnstructured(ref)). 2217 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). 2218 WithWorkerMachineDeploymentClasses( 2219 *builder.MachineDeploymentClass("aa"). 2220 WithInfrastructureTemplate(refToUnstructured(ref)). 2221 WithBootstrapTemplate(refToUnstructured(ref)). 2222 Build(), 2223 *builder.MachineDeploymentClass("bb"). 2224 WithInfrastructureTemplate(refToUnstructured(ref)). 2225 WithBootstrapTemplate(refToUnstructured(ref)). 2226 Build(), 2227 ). 2228 WithWorkerMachinePoolClasses( 2229 *builder.MachinePoolClass("aa"). 2230 WithInfrastructureTemplate(refToUnstructured(ref)). 2231 WithBootstrapTemplate(refToUnstructured(ref)). 2232 Build(), 2233 *builder.MachinePoolClass("bb"). 2234 WithInfrastructureTemplate(refToUnstructured(ref)). 2235 WithBootstrapTemplate(refToUnstructured(ref)). 2236 Build(), 2237 ). 2238 Build(), 2239 secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2"). 2240 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 2241 WithControlPlaneTemplate(refToUnstructured(ref)). 2242 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). 2243 WithWorkerMachineDeploymentClasses( 2244 *builder.MachineDeploymentClass("aa"). 2245 WithInfrastructureTemplate(refToUnstructured(ref)). 2246 WithBootstrapTemplate(refToUnstructured(ref)). 2247 Build(), 2248 ). 2249 WithWorkerMachinePoolClasses( 2250 *builder.MachinePoolClass("aa"). 2251 WithInfrastructureTemplate(refToUnstructured(ref)). 2252 WithBootstrapTemplate(refToUnstructured(ref)). 2253 Build(), 2254 ). 2255 Build(), 2256 wantErr: false, 2257 }, 2258 { 2259 name: "Accept cluster.topology.class change with an added MachineDeploymentClass and MachinePoolClass", 2260 firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). 2261 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 2262 WithControlPlaneTemplate(refToUnstructured(ref)). 2263 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). 2264 WithWorkerMachineDeploymentClasses( 2265 *builder.MachineDeploymentClass("aa"). 2266 WithInfrastructureTemplate(refToUnstructured(ref)). 2267 WithBootstrapTemplate(refToUnstructured(ref)). 2268 Build(), 2269 ). 2270 WithWorkerMachinePoolClasses( 2271 *builder.MachinePoolClass("aa"). 2272 WithInfrastructureTemplate(refToUnstructured(ref)). 2273 WithBootstrapTemplate(refToUnstructured(ref)). 2274 Build(), 2275 ). 2276 Build(), 2277 secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2"). 2278 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 2279 WithControlPlaneTemplate(refToUnstructured(ref)). 2280 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). 2281 WithWorkerMachineDeploymentClasses( 2282 *builder.MachineDeploymentClass("aa"). 2283 WithInfrastructureTemplate(refToUnstructured(ref)). 2284 WithBootstrapTemplate(refToUnstructured(ref)). 2285 Build(), 2286 *builder.MachineDeploymentClass("bb"). 2287 WithInfrastructureTemplate(refToUnstructured(ref)). 2288 WithBootstrapTemplate(refToUnstructured(ref)). 2289 Build(), 2290 ). 2291 WithWorkerMachinePoolClasses( 2292 *builder.MachinePoolClass("aa"). 2293 WithInfrastructureTemplate(refToUnstructured(ref)). 2294 WithBootstrapTemplate(refToUnstructured(ref)). 2295 Build(), 2296 *builder.MachinePoolClass("bb"). 2297 WithInfrastructureTemplate(refToUnstructured(ref)). 2298 WithBootstrapTemplate(refToUnstructured(ref)). 2299 Build(), 2300 ). 2301 Build(), 2302 wantErr: false, 2303 }, 2304 { 2305 name: "Reject cluster.topology.class change with an incompatible Kind change to MachineDeploymentClass InfrastructureTemplate", 2306 firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). 2307 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 2308 WithControlPlaneTemplate(refToUnstructured(ref)). 2309 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). 2310 WithWorkerMachineDeploymentClasses( 2311 *builder.MachineDeploymentClass("aa"). 2312 WithInfrastructureTemplate(refToUnstructured(ref)). 2313 WithBootstrapTemplate(refToUnstructured(ref)). 2314 Build(), 2315 ). 2316 Build(), 2317 secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2"). 2318 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 2319 WithControlPlaneTemplate(refToUnstructured(ref)). 2320 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). 2321 WithWorkerMachineDeploymentClasses( 2322 *builder.MachineDeploymentClass("aa"). 2323 WithInfrastructureTemplate(refToUnstructured(incompatibleKindRef)). 2324 WithBootstrapTemplate(refToUnstructured(ref)). 2325 Build(), 2326 ). 2327 Build(), 2328 wantErr: true, 2329 }, 2330 { 2331 name: "Reject cluster.topology.class change with an incompatible APIGroup change to MachineDeploymentClass InfrastructureTemplate", 2332 firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). 2333 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 2334 WithControlPlaneTemplate(refToUnstructured(ref)). 2335 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). 2336 WithWorkerMachineDeploymentClasses( 2337 *builder.MachineDeploymentClass("aa"). 2338 WithInfrastructureTemplate(refToUnstructured(ref)). 2339 WithBootstrapTemplate(refToUnstructured(ref)). 2340 Build(), 2341 ). 2342 Build(), 2343 secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2"). 2344 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 2345 WithControlPlaneTemplate(refToUnstructured(ref)). 2346 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). 2347 WithWorkerMachineDeploymentClasses( 2348 *builder.MachineDeploymentClass("aa"). 2349 WithInfrastructureTemplate(refToUnstructured(incompatibleAPIGroupRef)). 2350 WithBootstrapTemplate(refToUnstructured(ref)). 2351 Build(), 2352 ). 2353 Build(), 2354 wantErr: true, 2355 }, 2356 2357 // MachinePoolClass reject changes 2358 { 2359 name: "Reject cluster.topology.class change with an incompatible Kind change to MachinePoolClass InfrastructureTemplate", 2360 firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). 2361 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 2362 WithControlPlaneTemplate(refToUnstructured(ref)). 2363 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). 2364 WithWorkerMachinePoolClasses( 2365 *builder.MachinePoolClass("aa"). 2366 WithInfrastructureTemplate(refToUnstructured(ref)). 2367 WithBootstrapTemplate(refToUnstructured(ref)). 2368 Build(), 2369 ). 2370 Build(), 2371 secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2"). 2372 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 2373 WithControlPlaneTemplate(refToUnstructured(ref)). 2374 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). 2375 WithWorkerMachinePoolClasses( 2376 *builder.MachinePoolClass("aa"). 2377 WithInfrastructureTemplate(refToUnstructured(incompatibleKindRef)). 2378 WithBootstrapTemplate(refToUnstructured(ref)). 2379 Build(), 2380 ). 2381 Build(), 2382 wantErr: true, 2383 }, 2384 { 2385 name: "Reject cluster.topology.class change with an incompatible APIGroup change to MachinePoolClass InfrastructureTemplate", 2386 firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). 2387 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 2388 WithControlPlaneTemplate(refToUnstructured(ref)). 2389 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). 2390 WithWorkerMachinePoolClasses( 2391 *builder.MachinePoolClass("aa"). 2392 WithInfrastructureTemplate(refToUnstructured(ref)). 2393 WithBootstrapTemplate(refToUnstructured(ref)). 2394 Build(), 2395 ). 2396 Build(), 2397 secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2"). 2398 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 2399 WithControlPlaneTemplate(refToUnstructured(ref)). 2400 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). 2401 WithWorkerMachinePoolClasses( 2402 *builder.MachinePoolClass("aa"). 2403 WithInfrastructureTemplate(refToUnstructured(incompatibleAPIGroupRef)). 2404 WithBootstrapTemplate(refToUnstructured(ref)). 2405 Build(), 2406 ). 2407 Build(), 2408 wantErr: true, 2409 }, 2410 } 2411 for _, tt := range tests { 2412 t.Run(tt.name, func(*testing.T) { 2413 // Mark this condition to true so the webhook sees the ClusterClass as up to date. 2414 conditions.MarkTrue(tt.firstClass, clusterv1.ClusterClassVariablesReconciledCondition) 2415 conditions.MarkTrue(tt.secondClass, clusterv1.ClusterClassVariablesReconciledCondition) 2416 2417 // Sets up the fakeClient for the test case. 2418 fakeClient := fake.NewClientBuilder(). 2419 WithObjects(tt.firstClass, tt.secondClass). 2420 WithScheme(fakeScheme). 2421 Build() 2422 2423 // Create the webhook and add the fakeClient as its client. This is required because the test uses a Managed Topology. 2424 c := &Cluster{Client: fakeClient} 2425 2426 // Create and updated cluster which uses the name of the second class from the test definition in its '.spec.topology.' 2427 secondCluster := cluster.DeepCopy() 2428 secondCluster.Spec.Topology.Class = tt.secondClass.Name 2429 2430 // Checks the return error. 2431 warnings, err := c.ValidateUpdate(ctx, cluster, secondCluster) 2432 if tt.wantErr { 2433 g.Expect(err).To(HaveOccurred()) 2434 } else { 2435 g.Expect(err).ToNot(HaveOccurred()) 2436 } 2437 g.Expect(warnings).To(BeEmpty()) 2438 }) 2439 } 2440 } 2441 2442 // TestMovingBetweenManagedAndUnmanaged cluster tests cases where a clusterClass is added or removed during a cluster update. 2443 func TestMovingBetweenManagedAndUnmanaged(t *testing.T) { 2444 defer utilfeature.SetFeatureGateDuringTest(t, feature.Gates, feature.ClusterTopology, true)() 2445 ref := &corev1.ObjectReference{ 2446 APIVersion: "group.test.io/foo", 2447 Kind: "barTemplate", 2448 Name: "baz", 2449 Namespace: "default", 2450 } 2451 2452 g := NewWithT(t) 2453 2454 tests := []struct { 2455 name string 2456 cluster *clusterv1.Cluster 2457 clusterClass *clusterv1.ClusterClass 2458 updatedTopology *clusterv1.Topology 2459 wantErr bool 2460 }{ 2461 { 2462 name: "Reject cluster moving from Unmanaged to Managed i.e. adding the spec.topology.class field on update", 2463 cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1"). 2464 Build(), 2465 clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). 2466 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 2467 WithControlPlaneTemplate(refToUnstructured(ref)). 2468 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). 2469 Build(), 2470 updatedTopology: builder.ClusterTopology(). 2471 WithClass("class1"). 2472 WithVersion("v1.22.2"). 2473 WithControlPlaneReplicas(3). 2474 Build(), 2475 wantErr: true, 2476 }, 2477 { 2478 name: "Allow cluster moving from Unmanaged to Managed i.e. adding the spec.topology.class field on update " + 2479 "if and only if ClusterTopologyUnsafeUpdateClassNameAnnotation is set", 2480 cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1"). 2481 WithAnnotations(map[string]string{clusterv1.ClusterTopologyUnsafeUpdateClassNameAnnotation: ""}). 2482 Build(), 2483 clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). 2484 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 2485 WithControlPlaneTemplate(refToUnstructured(ref)). 2486 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). 2487 Build(), 2488 updatedTopology: builder.ClusterTopology(). 2489 WithClass("class1"). 2490 WithVersion("v1.22.2"). 2491 WithControlPlaneReplicas(3). 2492 Build(), 2493 wantErr: false, 2494 }, 2495 { 2496 name: "Reject cluster moving from Managed to Unmanaged i.e. removing the spec.topology.class field on update", 2497 cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1"). 2498 WithTopology(builder.ClusterTopology(). 2499 WithClass("class1"). 2500 WithVersion("v1.22.2"). 2501 WithControlPlaneReplicas(3). 2502 Build()). 2503 Build(), 2504 clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). 2505 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 2506 WithControlPlaneTemplate(refToUnstructured(ref)). 2507 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). 2508 Build(), 2509 updatedTopology: nil, 2510 wantErr: true, 2511 }, 2512 { 2513 name: "Reject cluster update if ClusterClass does not exist", 2514 cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1"). 2515 WithTopology(builder.ClusterTopology(). 2516 WithClass("class1"). 2517 WithVersion("v1.22.2"). 2518 WithControlPlaneReplicas(3). 2519 Build()). 2520 Build(), 2521 clusterClass: 2522 // ClusterClass name is different to that in the Cluster `.spec.topology.class` 2523 builder.ClusterClass(metav1.NamespaceDefault, "completely-different-class"). 2524 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 2525 WithControlPlaneTemplate(refToUnstructured(ref)). 2526 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). 2527 Build(), 2528 updatedTopology: builder.ClusterTopology(). 2529 WithClass("class1"). 2530 WithVersion("v1.22.2"). 2531 WithControlPlaneReplicas(3). 2532 Build(), 2533 wantErr: true, 2534 }, 2535 } 2536 for _, tt := range tests { 2537 t.Run(tt.name, func(*testing.T) { 2538 // Mark this condition to true so the webhook sees the ClusterClass as up to date. 2539 conditions.MarkTrue(tt.clusterClass, clusterv1.ClusterClassVariablesReconciledCondition) 2540 // Sets up the fakeClient for the test case. 2541 fakeClient := fake.NewClientBuilder(). 2542 WithObjects(tt.clusterClass, tt.cluster). 2543 WithScheme(fakeScheme). 2544 Build() 2545 2546 // Create the webhook and add the fakeClient as its client. This is required because the test uses a Managed Topology. 2547 c := &Cluster{Client: fakeClient} 2548 2549 // Create and updated cluster which uses the name of the second class from the test definition in its '.spec.topology.' 2550 updatedCluster := tt.cluster.DeepCopy() 2551 updatedCluster.Spec.Topology = tt.updatedTopology 2552 2553 // Checks the return error. 2554 warnings, err := c.ValidateUpdate(ctx, tt.cluster, updatedCluster) 2555 if tt.wantErr { 2556 g.Expect(err).To(HaveOccurred()) 2557 } else { 2558 g.Expect(err).NotTo(HaveOccurred()) 2559 // Errors may be duplicated as warnings. There should be no warnings in this case if there are no errors. 2560 g.Expect(warnings).To(BeEmpty()) 2561 } 2562 }) 2563 } 2564 } 2565 2566 // TestClusterClassPollingErrors tests when a Cluster can be reconciled given different reconcile states of the ClusterClass. 2567 func TestClusterClassPollingErrors(t *testing.T) { 2568 defer utilfeature.SetFeatureGateDuringTest(t, feature.Gates, feature.ClusterTopology, true)() 2569 g := NewWithT(t) 2570 ref := &corev1.ObjectReference{ 2571 APIVersion: "group.test.io/foo", 2572 Kind: "barTemplate", 2573 Name: "baz", 2574 Namespace: "default", 2575 } 2576 2577 topology := builder.ClusterTopology().WithClass("class1").WithVersion("v1.24.3").Build() 2578 secondTopology := builder.ClusterTopology().WithClass("class2").WithVersion("v1.24.3").Build() 2579 notFoundTopology := builder.ClusterTopology().WithClass("doesnotexist").WithVersion("v1.24.3").Build() 2580 2581 baseClusterClass := builder.ClusterClass(metav1.NamespaceDefault, "class1"). 2582 WithInfrastructureClusterTemplate(refToUnstructured(ref)). 2583 WithControlPlaneTemplate(refToUnstructured(ref)). 2584 WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)) 2585 2586 // ccFullyReconciled is a ClusterClass with a matching generation and observed generation, and VariablesReconciled=True. 2587 ccFullyReconciled := baseClusterClass.DeepCopy().Build() 2588 ccFullyReconciled.Generation = 1 2589 ccFullyReconciled.Status.ObservedGeneration = 1 2590 conditions.MarkTrue(ccFullyReconciled, clusterv1.ClusterClassVariablesReconciledCondition) 2591 2592 // secondFullyReconciled is a second ClusterClass with a matching generation and observed generation, and VariablesReconciled=True. 2593 secondFullyReconciled := ccFullyReconciled.DeepCopy() 2594 secondFullyReconciled.SetName("class2") 2595 2596 // ccGenerationMismatch is a ClusterClass with a mismatched generation and observed generation, but VariablesReconciledCondition=True. 2597 ccGenerationMismatch := baseClusterClass.DeepCopy().Build() 2598 ccGenerationMismatch.Generation = 999 2599 ccGenerationMismatch.Status.ObservedGeneration = 1 2600 conditions.MarkTrue(ccGenerationMismatch, clusterv1.ClusterClassVariablesReconciledCondition) 2601 2602 // ccVariablesReconciledFalse with VariablesReconciled=False. 2603 ccVariablesReconciledFalse := baseClusterClass.DeepCopy().Build() 2604 conditions.MarkFalse(ccGenerationMismatch, clusterv1.ClusterClassVariablesReconciledCondition, "", clusterv1.ConditionSeverityError, "") 2605 2606 tests := []struct { 2607 name string 2608 cluster *clusterv1.Cluster 2609 oldCluster *clusterv1.Cluster 2610 clusterClasses []*clusterv1.ClusterClass 2611 injectedErr interceptor.Funcs 2612 wantErr bool 2613 wantWarnings bool 2614 }{ 2615 { 2616 name: "Pass on create if ClusterClass is fully reconciled", 2617 cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").WithTopology(topology).Build(), 2618 clusterClasses: []*clusterv1.ClusterClass{ccFullyReconciled}, 2619 wantErr: false, 2620 }, 2621 { 2622 name: "Pass on create if ClusterClass generation does not match observedGeneration", 2623 cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").WithTopology(topology).Build(), 2624 clusterClasses: []*clusterv1.ClusterClass{ccGenerationMismatch}, 2625 wantErr: false, 2626 wantWarnings: true, 2627 }, 2628 { 2629 name: "Pass on create if ClusterClass generation matches observedGeneration but VariablesReconciled=False", 2630 cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").WithTopology(topology).Build(), 2631 clusterClasses: []*clusterv1.ClusterClass{ccVariablesReconciledFalse}, 2632 wantErr: false, 2633 wantWarnings: true, 2634 }, 2635 { 2636 name: "Pass on create if ClusterClass is not found", 2637 cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").WithTopology(notFoundTopology).Build(), 2638 clusterClasses: []*clusterv1.ClusterClass{ccFullyReconciled}, 2639 wantErr: false, 2640 wantWarnings: true, 2641 }, 2642 { 2643 name: "Pass on update if oldCluster ClusterClass is fully reconciled", 2644 cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").WithTopology(secondTopology).Build(), 2645 oldCluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").WithTopology(topology).Build(), 2646 clusterClasses: []*clusterv1.ClusterClass{ccFullyReconciled, secondFullyReconciled}, 2647 wantErr: false, 2648 }, 2649 { 2650 name: "Fail on update if oldCluster ClusterClass generation does not match observedGeneration", 2651 cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").WithTopology(secondTopology).Build(), 2652 oldCluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").WithTopology(topology).Build(), 2653 clusterClasses: []*clusterv1.ClusterClass{ccGenerationMismatch, secondFullyReconciled}, 2654 wantErr: true, 2655 }, 2656 { 2657 name: "Fail on update if old Cluster ClusterClass is not found", 2658 cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").WithTopology(topology).Build(), 2659 oldCluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").WithTopology(notFoundTopology).Build(), 2660 clusterClasses: []*clusterv1.ClusterClass{ccFullyReconciled}, 2661 wantErr: true, 2662 }, 2663 { 2664 name: "Fail on update if new Cluster ClusterClass is not found", 2665 cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").WithTopology(notFoundTopology).Build(), 2666 oldCluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").WithTopology(topology).Build(), 2667 clusterClasses: []*clusterv1.ClusterClass{ccFullyReconciled}, 2668 wantErr: true, 2669 wantWarnings: true, 2670 }, 2671 { 2672 name: "Fail on update if new ClusterClass returns connection error", 2673 cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").WithTopology(secondTopology).Build(), 2674 oldCluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").WithTopology(topology).Build(), 2675 clusterClasses: []*clusterv1.ClusterClass{ccFullyReconciled, secondFullyReconciled}, 2676 injectedErr: interceptor.Funcs{ 2677 Get: func(ctx context.Context, client client.WithWatch, key client.ObjectKey, obj client.Object, _ ...client.GetOption) error { 2678 // Throw an error if the second ClusterClass `class2` used as the new ClusterClass is being retrieved. 2679 if key.Name == secondTopology.Class { 2680 return errors.New("connection error") 2681 } 2682 return client.Get(ctx, key, obj) 2683 }, 2684 }, 2685 wantErr: true, 2686 wantWarnings: false, 2687 }, 2688 { 2689 name: "Fail on update if old ClusterClass returns connection error", 2690 cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").WithTopology(secondTopology).Build(), 2691 oldCluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").WithTopology(topology).Build(), 2692 clusterClasses: []*clusterv1.ClusterClass{ccFullyReconciled, secondFullyReconciled}, 2693 injectedErr: interceptor.Funcs{ 2694 Get: func(ctx context.Context, client client.WithWatch, key client.ObjectKey, obj client.Object, _ ...client.GetOption) error { 2695 // Throw an error if the ClusterClass `class1` used as the old ClusterClass is being retrieved. 2696 if key.Name == topology.Class { 2697 return errors.New("connection error") 2698 } 2699 return client.Get(ctx, key, obj) 2700 }, 2701 }, 2702 wantErr: true, 2703 wantWarnings: false, 2704 }, 2705 } 2706 2707 for _, tt := range tests { 2708 t.Run(tt.name, func(*testing.T) { 2709 // Sets up a reconcile with a fakeClient for the test case. 2710 objs := []client.Object{} 2711 for _, cc := range tt.clusterClasses { 2712 objs = append(objs, cc) 2713 } 2714 c := &Cluster{Client: fake.NewClientBuilder(). 2715 WithInterceptorFuncs(tt.injectedErr). 2716 WithScheme(fakeScheme). 2717 WithObjects(objs...). 2718 Build(), 2719 } 2720 2721 // Checks the return error. 2722 warnings, err := c.validate(ctx, tt.oldCluster, tt.cluster) 2723 if tt.wantErr { 2724 g.Expect(err).To(HaveOccurred()) 2725 } else { 2726 g.Expect(err).ToNot(HaveOccurred()) 2727 } 2728 if tt.wantWarnings { 2729 g.Expect(warnings).NotTo(BeEmpty()) 2730 } else { 2731 g.Expect(warnings).To(BeEmpty()) 2732 } 2733 }) 2734 } 2735 } 2736 2737 func Test_validateTopologyControlPlaneVersion(t *testing.T) { 2738 tests := []struct { 2739 name string 2740 expectErr bool 2741 old *clusterv1.Cluster 2742 additionalObjects []client.Object 2743 }{ 2744 { 2745 name: "should update if kcp is fully upgraded and up to date", 2746 expectErr: false, 2747 old: builder.Cluster("fooboo", "cluster1"). 2748 WithControlPlane(builder.ControlPlane("fooboo", "cluster1-cp").Build()). 2749 WithTopology(builder.ClusterTopology(). 2750 WithClass("foo"). 2751 WithVersion("v1.19.1"). 2752 Build()). 2753 Build(), 2754 additionalObjects: []client.Object{ 2755 builder.ControlPlane("fooboo", "cluster1-cp").WithVersion("v1.19.1"). 2756 WithStatusFields(map[string]interface{}{"status.version": "v1.19.1"}). 2757 Build(), 2758 }, 2759 }, 2760 { 2761 name: "should block update if kcp is provisioning", 2762 expectErr: true, 2763 old: builder.Cluster("fooboo", "cluster1"). 2764 WithControlPlane(builder.ControlPlane("fooboo", "cluster1-cp").Build()). 2765 WithTopology(builder.ClusterTopology(). 2766 WithClass("foo"). 2767 WithVersion("v1.19.1"). 2768 Build()). 2769 Build(), 2770 additionalObjects: []client.Object{ 2771 builder.ControlPlane("fooboo", "cluster1-cp").WithVersion("v1.19.1"). 2772 Build(), 2773 }, 2774 }, 2775 { 2776 name: "should block update if kcp is upgrading", 2777 expectErr: true, 2778 old: builder.Cluster("fooboo", "cluster1"). 2779 WithControlPlane(builder.ControlPlane("fooboo", "cluster1-cp").Build()). 2780 WithTopology(builder.ClusterTopology(). 2781 WithClass("foo"). 2782 WithVersion("v1.19.1"). 2783 Build()). 2784 Build(), 2785 additionalObjects: []client.Object{ 2786 builder.ControlPlane("fooboo", "cluster1-cp").WithVersion("v1.18.1"). 2787 WithStatusFields(map[string]interface{}{"status.version": "v1.17.1"}). 2788 Build(), 2789 }, 2790 }, 2791 { 2792 name: "should block update if kcp is not yet upgraded", 2793 expectErr: true, 2794 old: builder.Cluster("fooboo", "cluster1"). 2795 WithControlPlane(builder.ControlPlane("fooboo", "cluster1-cp").Build()). 2796 WithTopology(builder.ClusterTopology(). 2797 WithClass("foo"). 2798 WithVersion("v1.19.1"). 2799 Build()). 2800 Build(), 2801 additionalObjects: []client.Object{ 2802 builder.ControlPlane("fooboo", "cluster1-cp").WithVersion("v1.18.1"). 2803 WithStatusFields(map[string]interface{}{"status.version": "v1.18.1"}). 2804 Build(), 2805 }, 2806 }, 2807 } 2808 for _, tt := range tests { 2809 t.Run(tt.name, func(t *testing.T) { 2810 g := NewWithT(t) 2811 2812 fakeClient := fake.NewClientBuilder(). 2813 WithObjects(tt.additionalObjects...). 2814 WithScheme(fakeScheme). 2815 Build() 2816 2817 oldVersion, err := semver.ParseTolerant(tt.old.Spec.Topology.Version) 2818 g.Expect(err).ToNot(HaveOccurred()) 2819 2820 err = validateTopologyControlPlaneVersion(ctx, fakeClient, tt.old, oldVersion) 2821 if tt.expectErr { 2822 g.Expect(err).To(HaveOccurred()) 2823 return 2824 } 2825 g.Expect(err).ToNot(HaveOccurred()) 2826 }) 2827 } 2828 } 2829 2830 func Test_validateTopologyMachineDeploymentVersions(t *testing.T) { 2831 tests := []struct { 2832 name string 2833 expectErr bool 2834 old *clusterv1.Cluster 2835 additionalObjects []client.Object 2836 }{ 2837 { 2838 name: "should update if no machine deployment is exists", 2839 expectErr: false, 2840 old: builder.Cluster("fooboo", "cluster1"). 2841 WithTopology(builder.ClusterTopology(). 2842 WithClass("foo"). 2843 WithVersion("v1.19.1"). 2844 Build()). 2845 Build(), 2846 additionalObjects: []client.Object{}, 2847 }, 2848 { 2849 name: "should update if machine deployments are fully upgraded and up to date", 2850 expectErr: false, 2851 old: builder.Cluster("fooboo", "cluster1"). 2852 WithTopology(builder.ClusterTopology(). 2853 WithClass("foo"). 2854 WithVersion("v1.19.1"). 2855 WithMachineDeployment( 2856 builder.MachineDeploymentTopology("workers1"). 2857 WithClass("aa"). 2858 Build()). 2859 Build()). 2860 Build(), 2861 additionalObjects: []client.Object{ 2862 builder.MachineDeployment("fooboo", "cluster1-workers1").WithLabels(map[string]string{ 2863 clusterv1.ClusterNameLabel: "cluster1", 2864 clusterv1.ClusterTopologyOwnedLabel: "", 2865 clusterv1.ClusterTopologyMachineDeploymentNameLabel: "workers1", 2866 }).WithVersion("v1.19.1").Build(), 2867 }, 2868 }, 2869 { 2870 name: "should block update if machine deployment is not yet upgraded", 2871 expectErr: true, 2872 old: builder.Cluster("fooboo", "cluster1"). 2873 WithTopology(builder.ClusterTopology(). 2874 WithClass("foo"). 2875 WithVersion("v1.19.1"). 2876 WithMachineDeployment( 2877 builder.MachineDeploymentTopology("workers1"). 2878 WithClass("aa"). 2879 Build()). 2880 Build()). 2881 Build(), 2882 additionalObjects: []client.Object{ 2883 builder.MachineDeployment("fooboo", "cluster1-workers1").WithLabels(map[string]string{ 2884 clusterv1.ClusterNameLabel: "cluster1", 2885 clusterv1.ClusterTopologyOwnedLabel: "", 2886 clusterv1.ClusterTopologyMachineDeploymentNameLabel: "workers1", 2887 }).WithVersion("v1.18.1").Build(), 2888 }, 2889 }, 2890 { 2891 name: "should block update if machine deployment is upgrading", 2892 expectErr: true, 2893 old: builder.Cluster("fooboo", "cluster1"). 2894 WithTopology(builder.ClusterTopology(). 2895 WithClass("foo"). 2896 WithVersion("v1.19.1"). 2897 WithMachineDeployment( 2898 builder.MachineDeploymentTopology("workers1"). 2899 WithClass("aa"). 2900 Build()). 2901 Build()). 2902 Build(), 2903 additionalObjects: []client.Object{ 2904 builder.MachineDeployment("fooboo", "cluster1-workers1").WithLabels(map[string]string{ 2905 clusterv1.ClusterNameLabel: "cluster1", 2906 clusterv1.ClusterTopologyOwnedLabel: "", 2907 clusterv1.ClusterTopologyMachineDeploymentNameLabel: "workers1", 2908 }).WithVersion("v1.19.1").WithSelector(*metav1.SetAsLabelSelector(labels.Set{clusterv1.ClusterTopologyMachineDeploymentNameLabel: "workers1"})).Build(), 2909 builder.Machine("fooboo", "cluster1-workers1-1").WithLabels(map[string]string{ 2910 clusterv1.ClusterNameLabel: "cluster1", 2911 // clusterv1.ClusterTopologyOwnedLabel: "", 2912 clusterv1.ClusterTopologyMachineDeploymentNameLabel: "workers1", 2913 }).WithVersion("v1.18.1").Build(), 2914 }, 2915 }, 2916 } 2917 for _, tt := range tests { 2918 t.Run(tt.name, func(t *testing.T) { 2919 g := NewWithT(t) 2920 2921 fakeClient := fake.NewClientBuilder(). 2922 WithObjects(tt.additionalObjects...). 2923 WithScheme(fakeScheme). 2924 Build() 2925 2926 oldVersion, err := semver.ParseTolerant(tt.old.Spec.Topology.Version) 2927 g.Expect(err).ToNot(HaveOccurred()) 2928 2929 err = validateTopologyMachineDeploymentVersions(ctx, fakeClient, tt.old, oldVersion) 2930 if tt.expectErr { 2931 g.Expect(err).To(HaveOccurred()) 2932 return 2933 } 2934 g.Expect(err).ToNot(HaveOccurred()) 2935 }) 2936 } 2937 } 2938 2939 func Test_validateTopologyMachinePoolVersions(t *testing.T) { 2940 tests := []struct { 2941 name string 2942 expectErr bool 2943 old *clusterv1.Cluster 2944 additionalObjects []client.Object 2945 workloadObjects []client.Object 2946 }{ 2947 { 2948 name: "should update if no machine pool is exists", 2949 expectErr: false, 2950 old: builder.Cluster("fooboo", "cluster1"). 2951 WithTopology(builder.ClusterTopology(). 2952 WithClass("foo"). 2953 WithVersion("v1.19.1"). 2954 Build()). 2955 Build(), 2956 additionalObjects: []client.Object{}, 2957 workloadObjects: []client.Object{}, 2958 }, 2959 { 2960 name: "should update if machine pools are fully upgraded and up to date", 2961 expectErr: false, 2962 old: builder.Cluster("fooboo", "cluster1"). 2963 WithTopology(builder.ClusterTopology(). 2964 WithClass("foo"). 2965 WithVersion("v1.19.1"). 2966 WithMachinePool( 2967 builder.MachinePoolTopology("pool1"). 2968 WithClass("aa"). 2969 Build()). 2970 Build()). 2971 Build(), 2972 additionalObjects: []client.Object{ 2973 builder.MachinePool("fooboo", "cluster1-pool1").WithLabels(map[string]string{ 2974 clusterv1.ClusterNameLabel: "cluster1", 2975 clusterv1.ClusterTopologyOwnedLabel: "", 2976 clusterv1.ClusterTopologyMachinePoolNameLabel: "pool1", 2977 }).WithVersion("v1.19.1").Build(), 2978 }, 2979 workloadObjects: []client.Object{}, 2980 }, 2981 { 2982 name: "should block update if machine pool is not yet upgraded", 2983 expectErr: true, 2984 old: builder.Cluster("fooboo", "cluster1"). 2985 WithTopology(builder.ClusterTopology(). 2986 WithClass("foo"). 2987 WithVersion("v1.19.1"). 2988 WithMachinePool( 2989 builder.MachinePoolTopology("pool1"). 2990 WithClass("aa"). 2991 Build()). 2992 Build()). 2993 Build(), 2994 additionalObjects: []client.Object{ 2995 builder.MachinePool("fooboo", "cluster1-pool1").WithLabels(map[string]string{ 2996 clusterv1.ClusterNameLabel: "cluster1", 2997 clusterv1.ClusterTopologyOwnedLabel: "", 2998 clusterv1.ClusterTopologyMachinePoolNameLabel: "pool1", 2999 }).WithVersion("v1.18.1").Build(), 3000 }, 3001 workloadObjects: []client.Object{}, 3002 }, 3003 { 3004 name: "should block update machine pool is upgrading", 3005 expectErr: true, 3006 old: builder.Cluster("fooboo", "cluster1"). 3007 WithTopology(builder.ClusterTopology(). 3008 WithClass("foo"). 3009 WithVersion("v1.19.1"). 3010 WithMachinePool( 3011 builder.MachinePoolTopology("pool1"). 3012 WithClass("aa"). 3013 Build()). 3014 Build()). 3015 Build(), 3016 additionalObjects: []client.Object{ 3017 builder.MachinePool("fooboo", "cluster1-pool1").WithLabels(map[string]string{ 3018 clusterv1.ClusterNameLabel: "cluster1", 3019 clusterv1.ClusterTopologyOwnedLabel: "", 3020 clusterv1.ClusterTopologyMachinePoolNameLabel: "pool1", 3021 }).WithVersion("v1.19.1").WithStatus(expv1.MachinePoolStatus{NodeRefs: []corev1.ObjectReference{{Name: "mp-node-1"}}}).Build(), 3022 }, 3023 workloadObjects: []client.Object{ 3024 &corev1.Node{ 3025 ObjectMeta: metav1.ObjectMeta{Name: "mp-node-1"}, 3026 Status: corev1.NodeStatus{NodeInfo: corev1.NodeSystemInfo{KubeletVersion: "v1.18.1"}}, 3027 }, 3028 }, 3029 }, 3030 { 3031 name: "should block update if it cannot get the node of a machine pool", 3032 expectErr: true, 3033 old: builder.Cluster("fooboo", "cluster1"). 3034 WithTopology(builder.ClusterTopology(). 3035 WithClass("foo"). 3036 WithVersion("v1.19.1"). 3037 WithMachinePool( 3038 builder.MachinePoolTopology("pool1"). 3039 WithClass("aa"). 3040 Build()). 3041 Build()). 3042 Build(), 3043 additionalObjects: []client.Object{ 3044 builder.MachinePool("fooboo", "cluster1-pool1").WithLabels(map[string]string{ 3045 clusterv1.ClusterNameLabel: "cluster1", 3046 clusterv1.ClusterTopologyOwnedLabel: "", 3047 clusterv1.ClusterTopologyMachinePoolNameLabel: "pool1", 3048 }).WithVersion("v1.19.1").WithStatus(expv1.MachinePoolStatus{NodeRefs: []corev1.ObjectReference{{Name: "mp-node-1"}}}).Build(), 3049 }, 3050 workloadObjects: []client.Object{}, 3051 }, 3052 } 3053 for _, tt := range tests { 3054 t.Run(tt.name, func(t *testing.T) { 3055 g := NewWithT(t) 3056 3057 fakeClient := fake.NewClientBuilder(). 3058 WithObjects(tt.additionalObjects...). 3059 WithScheme(fakeScheme). 3060 Build() 3061 3062 oldVersion, err := semver.ParseTolerant(tt.old.Spec.Topology.Version) 3063 g.Expect(err).ToNot(HaveOccurred()) 3064 3065 fakeClusterCacheTracker := &fakeClusterCacheTracker{ 3066 client: fake.NewClientBuilder(). 3067 WithObjects(tt.workloadObjects...). 3068 Build(), 3069 } 3070 3071 err = validateTopologyMachinePoolVersions(ctx, fakeClient, fakeClusterCacheTracker, tt.old, oldVersion) 3072 if tt.expectErr { 3073 g.Expect(err).To(HaveOccurred()) 3074 return 3075 } 3076 g.Expect(err).ToNot(HaveOccurred()) 3077 }) 3078 } 3079 } 3080 3081 func refToUnstructured(ref *corev1.ObjectReference) *unstructured.Unstructured { 3082 gvk := ref.GetObjectKind().GroupVersionKind() 3083 output := &unstructured.Unstructured{} 3084 output.SetKind(gvk.Kind) 3085 output.SetAPIVersion(gvk.GroupVersion().String()) 3086 output.SetName(ref.Name) 3087 output.SetNamespace(ref.Namespace) 3088 return output 3089 } 3090 3091 type fakeClusterCacheTracker struct { 3092 client client.Reader 3093 } 3094 3095 func (f *fakeClusterCacheTracker) GetReader(_ context.Context, _ types.NamespacedName) (client.Reader, error) { 3096 return f.client, nil 3097 }