sigs.k8s.io/cluster-api@v1.7.1/internal/controllers/topology/cluster/patches/engine_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 patches 18 19 import ( 20 "context" 21 "encoding/json" 22 "fmt" 23 "strings" 24 "testing" 25 26 . "github.com/onsi/gomega" 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/types" 31 utilfeature "k8s.io/component-base/featuregate/testing" 32 "k8s.io/utils/ptr" 33 . "sigs.k8s.io/controller-runtime/pkg/envtest/komega" 34 35 clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" 36 runtimecatalog "sigs.k8s.io/cluster-api/exp/runtime/catalog" 37 runtimehooksv1 "sigs.k8s.io/cluster-api/exp/runtime/hooks/api/v1alpha1" 38 "sigs.k8s.io/cluster-api/exp/topology/scope" 39 "sigs.k8s.io/cluster-api/feature" 40 fakeruntimeclient "sigs.k8s.io/cluster-api/internal/runtime/client/fake" 41 "sigs.k8s.io/cluster-api/internal/test/builder" 42 ) 43 44 func TestApply(t *testing.T) { 45 defer utilfeature.SetFeatureGateDuringTest(t, feature.Gates, feature.RuntimeSDK, true)() 46 type expectedFields struct { 47 infrastructureCluster map[string]interface{} 48 controlPlane map[string]interface{} 49 controlPlaneInfrastructureMachineTemplate map[string]interface{} 50 machineDeploymentBootstrapTemplate map[string]map[string]interface{} 51 machineDeploymentInfrastructureMachineTemplate map[string]map[string]interface{} 52 machinePoolBootstrapConfig map[string]map[string]interface{} 53 machinePoolInfrastructureMachinePool map[string]map[string]interface{} 54 } 55 56 tests := []struct { 57 name string 58 patches []clusterv1.ClusterClassPatch 59 varDefinitions []clusterv1.ClusterClassStatusVariable 60 externalPatchResponses map[string]runtimehooksv1.ResponseObject 61 expectedFields expectedFields 62 wantErr bool 63 }{ 64 { 65 name: "Should preserve desired state, if there are no patches", 66 // No changes expected. 67 expectedFields: expectedFields{}, 68 }, 69 { 70 name: "Should apply JSON patches to InfraCluster, ControlPlane and ControlPlaneInfrastructureMachineTemplate", 71 patches: []clusterv1.ClusterClassPatch{ 72 { 73 Name: "fake-patch1", 74 Definitions: []clusterv1.PatchDefinition{ 75 { 76 Selector: clusterv1.PatchSelector{ 77 APIVersion: builder.InfrastructureGroupVersion.String(), 78 Kind: builder.GenericInfrastructureClusterTemplateKind, 79 MatchResources: clusterv1.PatchSelectorMatch{ 80 InfrastructureCluster: true, 81 }, 82 }, 83 JSONPatches: []clusterv1.JSONPatch{ 84 { 85 Op: "add", 86 Path: "/spec/template/spec/resource", 87 Value: &apiextensionsv1.JSON{Raw: []byte(`"infraCluster"`)}, 88 }, 89 }, 90 }, 91 { 92 Selector: clusterv1.PatchSelector{ 93 APIVersion: builder.ControlPlaneGroupVersion.String(), 94 Kind: builder.GenericControlPlaneTemplateKind, 95 MatchResources: clusterv1.PatchSelectorMatch{ 96 ControlPlane: true, 97 }, 98 }, 99 JSONPatches: []clusterv1.JSONPatch{ 100 { 101 Op: "add", 102 Path: "/spec/template/spec/resource", 103 Value: &apiextensionsv1.JSON{Raw: []byte(`"controlPlane"`)}, 104 }, 105 }, 106 }, 107 { 108 Selector: clusterv1.PatchSelector{ 109 APIVersion: builder.InfrastructureGroupVersion.String(), 110 Kind: builder.GenericInfrastructureMachineTemplateKind, 111 MatchResources: clusterv1.PatchSelectorMatch{ 112 ControlPlane: true, 113 }, 114 }, 115 JSONPatches: []clusterv1.JSONPatch{ 116 { 117 Op: "add", 118 Path: "/spec/template/spec/resource", 119 Value: &apiextensionsv1.JSON{Raw: []byte(`"controlPlaneInfrastructureMachineTemplate"`)}, 120 }, 121 }, 122 }, 123 }, 124 }, 125 }, 126 expectedFields: expectedFields{ 127 infrastructureCluster: map[string]interface{}{ 128 "spec.resource": "infraCluster", 129 }, 130 controlPlane: map[string]interface{}{ 131 "spec.resource": "controlPlane", 132 }, 133 controlPlaneInfrastructureMachineTemplate: map[string]interface{}{ 134 "spec.template.spec.resource": "controlPlaneInfrastructureMachineTemplate", 135 }, 136 }, 137 }, 138 { 139 name: "Should apply JSON patches to MachineDeployment and MachinePool templates", 140 patches: []clusterv1.ClusterClassPatch{ 141 { 142 Name: "fake-patch1", 143 Definitions: []clusterv1.PatchDefinition{ 144 { 145 Selector: clusterv1.PatchSelector{ 146 APIVersion: builder.InfrastructureGroupVersion.String(), 147 Kind: builder.GenericInfrastructureMachineTemplateKind, 148 MatchResources: clusterv1.PatchSelectorMatch{ 149 MachineDeploymentClass: &clusterv1.PatchSelectorMatchMachineDeploymentClass{ 150 Names: []string{"default-worker"}, 151 }, 152 }, 153 }, 154 JSONPatches: []clusterv1.JSONPatch{ 155 { 156 Op: "add", 157 Path: "/spec/template/spec/resource", 158 Value: &apiextensionsv1.JSON{Raw: []byte(`"default-worker-infra"`)}, 159 }, 160 }, 161 }, 162 { 163 Selector: clusterv1.PatchSelector{ 164 APIVersion: builder.BootstrapGroupVersion.String(), 165 Kind: builder.GenericBootstrapConfigTemplateKind, 166 MatchResources: clusterv1.PatchSelectorMatch{ 167 MachineDeploymentClass: &clusterv1.PatchSelectorMatchMachineDeploymentClass{ 168 Names: []string{"default-worker"}, 169 }, 170 }, 171 }, 172 JSONPatches: []clusterv1.JSONPatch{ 173 { 174 Op: "add", 175 Path: "/spec/template/spec/resource", 176 Value: &apiextensionsv1.JSON{Raw: []byte(`"default-worker-bootstrap"`)}, 177 }, 178 }, 179 }, 180 { 181 Selector: clusterv1.PatchSelector{ 182 APIVersion: builder.InfrastructureGroupVersion.String(), 183 Kind: builder.GenericInfrastructureMachinePoolTemplateKind, 184 MatchResources: clusterv1.PatchSelectorMatch{ 185 MachinePoolClass: &clusterv1.PatchSelectorMatchMachinePoolClass{ 186 Names: []string{"default-mp-worker"}, 187 }, 188 }, 189 }, 190 JSONPatches: []clusterv1.JSONPatch{ 191 { 192 Op: "add", 193 Path: "/spec/template/spec/resource", 194 Value: &apiextensionsv1.JSON{Raw: []byte(`"default-mp-worker-infra"`)}, 195 }, 196 }, 197 }, 198 { 199 Selector: clusterv1.PatchSelector{ 200 APIVersion: builder.BootstrapGroupVersion.String(), 201 Kind: builder.GenericBootstrapConfigTemplateKind, 202 MatchResources: clusterv1.PatchSelectorMatch{ 203 MachinePoolClass: &clusterv1.PatchSelectorMatchMachinePoolClass{ 204 Names: []string{"default-mp-worker"}, 205 }, 206 }, 207 }, 208 JSONPatches: []clusterv1.JSONPatch{ 209 { 210 Op: "add", 211 Path: "/spec/template/spec/resource", 212 Value: &apiextensionsv1.JSON{Raw: []byte(`"default-mp-worker-bootstrap"`)}, 213 }, 214 }, 215 }, 216 }, 217 }, 218 }, 219 expectedFields: expectedFields{ 220 machineDeploymentBootstrapTemplate: map[string]map[string]interface{}{ 221 "default-worker-topo1": {"spec.template.spec.resource": "default-worker-bootstrap"}, 222 "default-worker-topo2": {"spec.template.spec.resource": "default-worker-bootstrap"}, 223 }, 224 machineDeploymentInfrastructureMachineTemplate: map[string]map[string]interface{}{ 225 "default-worker-topo1": {"spec.template.spec.resource": "default-worker-infra"}, 226 "default-worker-topo2": {"spec.template.spec.resource": "default-worker-infra"}, 227 }, 228 machinePoolBootstrapConfig: map[string]map[string]interface{}{ 229 "default-mp-worker-topo1": {"spec.resource": "default-mp-worker-bootstrap"}, 230 "default-mp-worker-topo2": {"spec.resource": "default-mp-worker-bootstrap"}, 231 }, 232 machinePoolInfrastructureMachinePool: map[string]map[string]interface{}{ 233 "default-mp-worker-topo1": {"spec.resource": "default-mp-worker-infra"}, 234 "default-mp-worker-topo2": {"spec.resource": "default-mp-worker-infra"}, 235 }, 236 }, 237 }, 238 { 239 name: "Should apply JSON patches in the correct order", 240 patches: []clusterv1.ClusterClassPatch{ 241 { 242 Name: "fake-patch1", 243 Definitions: []clusterv1.PatchDefinition{ 244 { 245 Selector: clusterv1.PatchSelector{ 246 APIVersion: builder.ControlPlaneGroupVersion.String(), 247 Kind: builder.GenericControlPlaneTemplateKind, 248 MatchResources: clusterv1.PatchSelectorMatch{ 249 ControlPlane: true, 250 }, 251 }, 252 JSONPatches: []clusterv1.JSONPatch{ 253 { 254 Op: "add", 255 Path: "/spec/template/spec/clusterName", 256 Value: &apiextensionsv1.JSON{Raw: []byte(`"cluster1"`)}, 257 }, 258 { 259 Op: "add", 260 Path: "/spec/template/spec/files", 261 Value: &apiextensionsv1.JSON{Raw: []byte(`[{"key1":"value1"}]`)}, 262 }, 263 }, 264 }, 265 }, 266 }, 267 { 268 Name: "fake-patch2", 269 Definitions: []clusterv1.PatchDefinition{ 270 { 271 Selector: clusterv1.PatchSelector{ 272 APIVersion: builder.ControlPlaneGroupVersion.String(), 273 Kind: builder.GenericControlPlaneTemplateKind, 274 MatchResources: clusterv1.PatchSelectorMatch{ 275 ControlPlane: true, 276 }, 277 }, 278 JSONPatches: []clusterv1.JSONPatch{ 279 { 280 Op: "replace", 281 Path: "/spec/template/spec/clusterName", 282 Value: &apiextensionsv1.JSON{Raw: []byte(`"cluster1-overwritten"`)}, 283 }, 284 }, 285 }, 286 }, 287 }, 288 }, 289 expectedFields: expectedFields{ 290 controlPlane: map[string]interface{}{ 291 "spec.clusterName": "cluster1-overwritten", 292 "spec.files": []interface{}{ 293 map[string]interface{}{ 294 "key1": "value1", 295 }, 296 }, 297 }, 298 }, 299 }, 300 { 301 name: "Should apply JSON patches and preserve ControlPlane fields", 302 patches: []clusterv1.ClusterClassPatch{ 303 { 304 Name: "fake-patch1", 305 Definitions: []clusterv1.PatchDefinition{ 306 { 307 Selector: clusterv1.PatchSelector{ 308 APIVersion: builder.ControlPlaneGroupVersion.String(), 309 Kind: builder.GenericControlPlaneTemplateKind, 310 MatchResources: clusterv1.PatchSelectorMatch{ 311 ControlPlane: true, 312 }, 313 }, 314 JSONPatches: []clusterv1.JSONPatch{ 315 { 316 Op: "add", 317 Path: "/spec/template/spec/replicas", 318 Value: &apiextensionsv1.JSON{Raw: []byte(`1`)}, 319 }, 320 { 321 Op: "add", 322 Path: "/spec/template/spec/version", 323 Value: &apiextensionsv1.JSON{Raw: []byte(`"v1.15.0"`)}, 324 }, 325 { 326 Op: "add", 327 Path: "/spec/template/spec/machineTemplate/infrastructureRef", 328 Value: &apiextensionsv1.JSON{Raw: []byte(`{"apiVersion":"invalid","kind":"invalid","namespace":"invalid","name":"invalid"}`)}, 329 }, 330 }, 331 }, 332 }, 333 }, 334 }, 335 }, 336 { 337 name: "Should apply JSON patches without metadata", 338 patches: []clusterv1.ClusterClassPatch{ 339 { 340 Name: "fake-patch1", 341 Definitions: []clusterv1.PatchDefinition{ 342 { 343 Selector: clusterv1.PatchSelector{ 344 APIVersion: builder.InfrastructureGroupVersion.String(), 345 Kind: builder.GenericInfrastructureClusterTemplateKind, 346 MatchResources: clusterv1.PatchSelectorMatch{ 347 InfrastructureCluster: true, 348 }, 349 }, 350 JSONPatches: []clusterv1.JSONPatch{ 351 { 352 Op: "add", 353 Path: "/spec/template/spec/clusterName", 354 Value: &apiextensionsv1.JSON{Raw: []byte(`"cluster1"`)}, 355 }, 356 { 357 Op: "replace", 358 Path: "/metadata/name", 359 Value: &apiextensionsv1.JSON{Raw: []byte(`"overwrittenName"`)}, 360 }, 361 }, 362 }, 363 }, 364 }, 365 }, 366 expectedFields: expectedFields{ 367 infrastructureCluster: map[string]interface{}{ 368 "spec.clusterName": "cluster1", 369 }, 370 }, 371 }, 372 { 373 name: "Should apply JSON merge patches", 374 patches: []clusterv1.ClusterClassPatch{ 375 { 376 Name: "fake-patch1", 377 Definitions: []clusterv1.PatchDefinition{ 378 { 379 Selector: clusterv1.PatchSelector{ 380 APIVersion: builder.InfrastructureGroupVersion.String(), 381 Kind: builder.GenericInfrastructureClusterTemplateKind, 382 MatchResources: clusterv1.PatchSelectorMatch{ 383 InfrastructureCluster: true, 384 }, 385 }, 386 JSONPatches: []clusterv1.JSONPatch{ 387 { 388 Op: "add", 389 Path: "/spec/template/spec/resource", 390 Value: &apiextensionsv1.JSON{Raw: []byte(`"infraCluster"`)}, 391 }, 392 }, 393 }, 394 }, 395 }, 396 }, 397 expectedFields: expectedFields{ 398 infrastructureCluster: map[string]interface{}{ 399 "spec.resource": "infraCluster", 400 }, 401 }, 402 }, 403 { 404 name: "Successfully apply external jsonPatch with generate and validate", 405 patches: []clusterv1.ClusterClassPatch{ 406 { 407 Name: "fake-patch1", 408 External: &clusterv1.ExternalPatchDefinition{ 409 GenerateExtension: ptr.To("patch-infrastructureCluster"), 410 ValidateExtension: ptr.To("validate-infrastructureCluster"), 411 }, 412 }, 413 }, 414 externalPatchResponses: map[string]runtimehooksv1.ResponseObject{ 415 "patch-infrastructureCluster": &runtimehooksv1.GeneratePatchesResponse{ 416 Items: []runtimehooksv1.GeneratePatchesResponseItem{ 417 { 418 UID: "1", 419 PatchType: runtimehooksv1.JSONPatchType, 420 Patch: bytesPatch([]jsonPatchRFC6902{{ 421 Op: "add", 422 Path: "/spec/template/spec/resource", 423 Value: &apiextensionsv1.JSON{Raw: []byte(`"infraCluster"`)}}}), 424 }, 425 }, 426 }, 427 "validate-infrastructureCluster": &runtimehooksv1.ValidateTopologyResponse{ 428 CommonResponse: runtimehooksv1.CommonResponse{ 429 Status: runtimehooksv1.ResponseStatusSuccess, 430 }, 431 }, 432 }, 433 expectedFields: expectedFields{ 434 infrastructureCluster: map[string]interface{}{ 435 "spec.resource": "infraCluster", 436 }, 437 }, 438 }, 439 { 440 name: "error on failed validation with external jsonPatch", 441 patches: []clusterv1.ClusterClassPatch{ 442 { 443 Name: "fake-patch1", 444 External: &clusterv1.ExternalPatchDefinition{ 445 GenerateExtension: ptr.To("patch-infrastructureCluster"), 446 ValidateExtension: ptr.To("validate-infrastructureCluster"), 447 }, 448 }, 449 }, 450 externalPatchResponses: map[string]runtimehooksv1.ResponseObject{ 451 "patch-infrastructureCluster": &runtimehooksv1.GeneratePatchesResponse{ 452 Items: []runtimehooksv1.GeneratePatchesResponseItem{ 453 { 454 UID: "1", 455 PatchType: runtimehooksv1.JSONPatchType, 456 Patch: bytesPatch([]jsonPatchRFC6902{{ 457 Op: "add", 458 Path: "/spec/template/spec/resource", 459 Value: &apiextensionsv1.JSON{Raw: []byte(`"invalid-infraCluster"`)}}}), 460 }, 461 }, 462 }, 463 "validate-infrastructureCluster": &runtimehooksv1.ValidateTopologyResponse{ 464 CommonResponse: runtimehooksv1.CommonResponse{ 465 Status: runtimehooksv1.ResponseStatusFailure, 466 Message: "not a valid infrastructureCluster", 467 }, 468 }, 469 }, 470 wantErr: true, 471 }, 472 { 473 name: "Successfully apply multiple external jsonPatch", 474 patches: []clusterv1.ClusterClassPatch{ 475 { 476 Name: "fake-patch1", 477 External: &clusterv1.ExternalPatchDefinition{ 478 GenerateExtension: ptr.To("patch-infrastructureCluster"), 479 }, 480 }, 481 { 482 Name: "fake-patch2", 483 External: &clusterv1.ExternalPatchDefinition{ 484 GenerateExtension: ptr.To("patch-controlPlane"), 485 }, 486 }, 487 }, 488 489 externalPatchResponses: map[string]runtimehooksv1.ResponseObject{ 490 "patch-infrastructureCluster": &runtimehooksv1.GeneratePatchesResponse{ 491 Items: []runtimehooksv1.GeneratePatchesResponseItem{ 492 { 493 UID: "1", 494 PatchType: runtimehooksv1.JSONPatchType, 495 Patch: bytesPatch([]jsonPatchRFC6902{{ 496 Op: "add", 497 Path: "/spec/template/spec/resource", 498 Value: &apiextensionsv1.JSON{Raw: []byte(`"infraCluster"`)}}}), 499 }, 500 { 501 UID: "1", 502 PatchType: runtimehooksv1.JSONPatchType, 503 Patch: bytesPatch([]jsonPatchRFC6902{{ 504 Op: "add", 505 Path: "/spec/template/spec/another", 506 Value: &apiextensionsv1.JSON{Raw: []byte(`"resource"`)}}}), 507 }, 508 }, 509 }, 510 "patch-controlPlane": &runtimehooksv1.GeneratePatchesResponse{ 511 Items: []runtimehooksv1.GeneratePatchesResponseItem{ 512 { 513 UID: "2", 514 PatchType: runtimehooksv1.JSONMergePatchType, 515 Patch: []byte(`{"spec":{"template":{"spec":{"resource": "controlPlane"}}}}`)}, 516 }, 517 }, 518 }, 519 expectedFields: expectedFields{ 520 infrastructureCluster: map[string]interface{}{ 521 "spec.resource": "infraCluster", 522 "spec.another": "resource", 523 }, 524 controlPlane: map[string]interface{}{ 525 "spec.resource": "controlPlane", 526 }, 527 }, 528 }, 529 { 530 name: "Should correctly apply patches with builtin variables", 531 patches: []clusterv1.ClusterClassPatch{ 532 { 533 Name: "fake-patch1", 534 Definitions: []clusterv1.PatchDefinition{ 535 { 536 Selector: clusterv1.PatchSelector{ 537 APIVersion: builder.InfrastructureGroupVersion.String(), 538 Kind: builder.GenericInfrastructureClusterTemplateKind, 539 MatchResources: clusterv1.PatchSelectorMatch{ 540 InfrastructureCluster: true, 541 }, 542 }, 543 JSONPatches: []clusterv1.JSONPatch{ 544 { 545 Op: "add", 546 Path: "/spec/template/spec/clusterName", 547 ValueFrom: &clusterv1.JSONPatchValue{ 548 Variable: ptr.To("builtin.cluster.name"), 549 }, 550 }, 551 }, 552 }, 553 { 554 Selector: clusterv1.PatchSelector{ 555 APIVersion: builder.ControlPlaneGroupVersion.String(), 556 Kind: builder.GenericControlPlaneTemplateKind, 557 MatchResources: clusterv1.PatchSelectorMatch{ 558 ControlPlane: true, 559 }, 560 }, 561 JSONPatches: []clusterv1.JSONPatch{ 562 { 563 Op: "add", 564 Path: "/spec/template/spec/controlPlaneName", 565 ValueFrom: &clusterv1.JSONPatchValue{ 566 Variable: ptr.To("builtin.controlPlane.name"), 567 }, 568 }, 569 }, 570 }, 571 { 572 Selector: clusterv1.PatchSelector{ 573 APIVersion: builder.InfrastructureGroupVersion.String(), 574 Kind: builder.GenericInfrastructureMachineTemplateKind, 575 MatchResources: clusterv1.PatchSelectorMatch{ 576 ControlPlane: true, 577 }, 578 }, 579 JSONPatches: []clusterv1.JSONPatch{ 580 { 581 Op: "add", 582 Path: "/spec/template/spec/controlPlaneName", 583 ValueFrom: &clusterv1.JSONPatchValue{ 584 Variable: ptr.To("builtin.controlPlane.name"), 585 }, 586 }, 587 }, 588 }, 589 { 590 Selector: clusterv1.PatchSelector{ 591 APIVersion: builder.BootstrapGroupVersion.String(), 592 Kind: builder.GenericBootstrapConfigTemplateKind, 593 MatchResources: clusterv1.PatchSelectorMatch{ 594 MachineDeploymentClass: &clusterv1.PatchSelectorMatchMachineDeploymentClass{ 595 Names: []string{"default-worker"}, 596 }, 597 }, 598 }, 599 JSONPatches: []clusterv1.JSONPatch{ 600 { 601 Op: "add", 602 Path: "/spec/template/spec/machineDeploymentTopologyName", 603 ValueFrom: &clusterv1.JSONPatchValue{ 604 Variable: ptr.To("builtin.machineDeployment.topologyName"), 605 }, 606 }, 607 }, 608 }, 609 { 610 Selector: clusterv1.PatchSelector{ 611 APIVersion: builder.InfrastructureGroupVersion.String(), 612 Kind: builder.GenericInfrastructureMachineTemplateKind, 613 MatchResources: clusterv1.PatchSelectorMatch{ 614 MachineDeploymentClass: &clusterv1.PatchSelectorMatchMachineDeploymentClass{ 615 Names: []string{"default-worker"}, 616 }, 617 }, 618 }, 619 JSONPatches: []clusterv1.JSONPatch{ 620 { 621 Op: "add", 622 Path: "/spec/template/spec/machineDeploymentTopologyName", 623 ValueFrom: &clusterv1.JSONPatchValue{ 624 Variable: ptr.To("builtin.machineDeployment.topologyName"), 625 }, 626 }, 627 }, 628 }, 629 { 630 Selector: clusterv1.PatchSelector{ 631 APIVersion: builder.BootstrapGroupVersion.String(), 632 Kind: builder.GenericBootstrapConfigTemplateKind, 633 MatchResources: clusterv1.PatchSelectorMatch{ 634 MachinePoolClass: &clusterv1.PatchSelectorMatchMachinePoolClass{ 635 Names: []string{"default-mp-worker"}, 636 }, 637 }, 638 }, 639 JSONPatches: []clusterv1.JSONPatch{ 640 { 641 Op: "add", 642 Path: "/spec/template/spec/machinePoolTopologyName", 643 ValueFrom: &clusterv1.JSONPatchValue{ 644 Variable: ptr.To("builtin.machinePool.topologyName"), 645 }, 646 }, 647 }, 648 }, 649 { 650 Selector: clusterv1.PatchSelector{ 651 APIVersion: builder.InfrastructureGroupVersion.String(), 652 Kind: builder.GenericInfrastructureMachinePoolTemplateKind, 653 MatchResources: clusterv1.PatchSelectorMatch{ 654 MachinePoolClass: &clusterv1.PatchSelectorMatchMachinePoolClass{ 655 Names: []string{"default-mp-worker"}, 656 }, 657 }, 658 }, 659 JSONPatches: []clusterv1.JSONPatch{ 660 { 661 Op: "add", 662 Path: "/spec/template/spec/machinePoolTopologyName", 663 ValueFrom: &clusterv1.JSONPatchValue{ 664 Variable: ptr.To("builtin.machinePool.topologyName"), 665 }, 666 }, 667 }, 668 }, 669 }, 670 }, 671 }, 672 expectedFields: expectedFields{ 673 infrastructureCluster: map[string]interface{}{ 674 "spec.clusterName": "cluster1", 675 }, 676 controlPlane: map[string]interface{}{ 677 "spec.controlPlaneName": "controlPlane1", 678 }, 679 controlPlaneInfrastructureMachineTemplate: map[string]interface{}{ 680 "spec.template.spec.controlPlaneName": "controlPlane1", 681 }, 682 machineDeploymentInfrastructureMachineTemplate: map[string]map[string]interface{}{ 683 "default-worker-topo1": {"spec.template.spec.machineDeploymentTopologyName": "default-worker-topo1"}, 684 "default-worker-topo2": {"spec.template.spec.machineDeploymentTopologyName": "default-worker-topo2"}, 685 }, 686 machineDeploymentBootstrapTemplate: map[string]map[string]interface{}{ 687 "default-worker-topo1": {"spec.template.spec.machineDeploymentTopologyName": "default-worker-topo1"}, 688 "default-worker-topo2": {"spec.template.spec.machineDeploymentTopologyName": "default-worker-topo2"}, 689 }, 690 machinePoolInfrastructureMachinePool: map[string]map[string]interface{}{ 691 "default-mp-worker-topo1": {"spec.machinePoolTopologyName": "default-mp-worker-topo1"}, 692 "default-mp-worker-topo2": {"spec.machinePoolTopologyName": "default-mp-worker-topo2"}, 693 }, 694 machinePoolBootstrapConfig: map[string]map[string]interface{}{ 695 "default-mp-worker-topo1": {"spec.machinePoolTopologyName": "default-mp-worker-topo1"}, 696 "default-mp-worker-topo2": {"spec.machinePoolTopologyName": "default-mp-worker-topo2"}, 697 }, 698 }, 699 }, 700 { 701 name: "Should correctly apply variables for a given patch definitionFrom", 702 varDefinitions: []clusterv1.ClusterClassStatusVariable{ 703 { 704 Name: "default-worker-infra", 705 Definitions: []clusterv1.ClusterClassStatusVariableDefinition{ 706 { 707 From: "inline", 708 }, 709 }, 710 }, 711 { 712 Name: "default-mp-worker-infra", 713 Definitions: []clusterv1.ClusterClassStatusVariableDefinition{ 714 { 715 From: "inline", 716 }, 717 }, 718 }, 719 { 720 Name: "infraCluster", 721 DefinitionsConflict: true, 722 Definitions: []clusterv1.ClusterClassStatusVariableDefinition{ 723 { 724 From: "inline", 725 }, 726 { 727 From: "not-used-patch", 728 }, 729 }, 730 }, 731 }, 732 patches: []clusterv1.ClusterClassPatch{ 733 { 734 Name: "fake-patch1", 735 Definitions: []clusterv1.PatchDefinition{ 736 { 737 Selector: clusterv1.PatchSelector{ 738 APIVersion: builder.InfrastructureGroupVersion.String(), 739 Kind: builder.GenericInfrastructureClusterTemplateKind, 740 MatchResources: clusterv1.PatchSelectorMatch{ 741 InfrastructureCluster: true, 742 }, 743 }, 744 JSONPatches: []clusterv1.JSONPatch{ 745 { 746 Op: "add", 747 Path: "/spec/template/spec/resource", 748 ValueFrom: &clusterv1.JSONPatchValue{ 749 Variable: ptr.To("infraCluster"), 750 }, 751 }, 752 }, 753 }, 754 { 755 Selector: clusterv1.PatchSelector{ 756 APIVersion: builder.BootstrapGroupVersion.String(), 757 Kind: builder.GenericBootstrapConfigTemplateKind, 758 MatchResources: clusterv1.PatchSelectorMatch{ 759 MachineDeploymentClass: &clusterv1.PatchSelectorMatchMachineDeploymentClass{ 760 Names: []string{"default-worker"}, 761 }, 762 }, 763 }, 764 JSONPatches: []clusterv1.JSONPatch{ 765 { 766 Op: "add", 767 Path: "/spec/template/spec/resource", 768 ValueFrom: &clusterv1.JSONPatchValue{ 769 Variable: ptr.To("default-worker-infra"), 770 }, 771 }, 772 }, 773 }, 774 { 775 Selector: clusterv1.PatchSelector{ 776 APIVersion: builder.InfrastructureGroupVersion.String(), 777 Kind: builder.GenericInfrastructureMachineTemplateKind, 778 MatchResources: clusterv1.PatchSelectorMatch{ 779 MachineDeploymentClass: &clusterv1.PatchSelectorMatchMachineDeploymentClass{ 780 Names: []string{"default-worker"}, 781 }, 782 }, 783 }, 784 JSONPatches: []clusterv1.JSONPatch{ 785 { 786 Op: "add", 787 Path: "/spec/template/spec/resource", 788 ValueFrom: &clusterv1.JSONPatchValue{ 789 Variable: ptr.To("default-worker-infra"), 790 }, 791 }, 792 }, 793 }, 794 { 795 Selector: clusterv1.PatchSelector{ 796 APIVersion: builder.BootstrapGroupVersion.String(), 797 Kind: builder.GenericBootstrapConfigTemplateKind, 798 MatchResources: clusterv1.PatchSelectorMatch{ 799 MachinePoolClass: &clusterv1.PatchSelectorMatchMachinePoolClass{ 800 Names: []string{"default-mp-worker"}, 801 }, 802 }, 803 }, 804 JSONPatches: []clusterv1.JSONPatch{ 805 { 806 Op: "add", 807 Path: "/spec/template/spec/resource", 808 ValueFrom: &clusterv1.JSONPatchValue{ 809 Variable: ptr.To("default-mp-worker-infra"), 810 }, 811 }, 812 }, 813 }, 814 { 815 Selector: clusterv1.PatchSelector{ 816 APIVersion: builder.InfrastructureGroupVersion.String(), 817 Kind: builder.GenericInfrastructureMachinePoolTemplateKind, 818 MatchResources: clusterv1.PatchSelectorMatch{ 819 MachinePoolClass: &clusterv1.PatchSelectorMatchMachinePoolClass{ 820 Names: []string{"default-mp-worker"}, 821 }, 822 }, 823 }, 824 JSONPatches: []clusterv1.JSONPatch{ 825 { 826 Op: "add", 827 Path: "/spec/template/spec/resource", 828 ValueFrom: &clusterv1.JSONPatchValue{ 829 Variable: ptr.To("default-mp-worker-infra"), 830 }, 831 }, 832 }, 833 }, 834 }, 835 }, 836 }, 837 expectedFields: expectedFields{ 838 infrastructureCluster: map[string]interface{}{ 839 "spec.resource": "value99", 840 }, 841 machineDeploymentInfrastructureMachineTemplate: map[string]map[string]interface{}{ 842 "default-worker-topo1": {"spec.template.spec.resource": "value1"}, 843 "default-worker-topo2": {"spec.template.spec.resource": "default-worker-topo2"}, 844 }, 845 machineDeploymentBootstrapTemplate: map[string]map[string]interface{}{ 846 "default-worker-topo1": {"spec.template.spec.resource": "value1"}, 847 "default-worker-topo2": {"spec.template.spec.resource": "default-worker-topo2"}, 848 }, 849 machinePoolInfrastructureMachinePool: map[string]map[string]interface{}{ 850 "default-mp-worker-topo1": {"spec.resource": "value2"}, 851 "default-mp-worker-topo2": {"spec.resource": "default-mp-worker-topo2"}, 852 }, 853 machinePoolBootstrapConfig: map[string]map[string]interface{}{ 854 "default-mp-worker-topo1": {"spec.resource": "value2"}, 855 "default-mp-worker-topo2": {"spec.resource": "default-mp-worker-topo2"}, 856 }, 857 }, 858 }, 859 } 860 for _, tt := range tests { 861 t.Run(tt.name, func(t *testing.T) { 862 g := NewWithT(t) 863 864 // Set up test objects, which are: 865 // * blueprint: 866 // * A ClusterClass with its corresponding templates: 867 // * ControlPlaneTemplate with a corresponding ControlPlane InfrastructureMachineTemplate. 868 // * MachineDeploymentClass "default-worker" with corresponding BootstrapTemplate and InfrastructureMachineTemplate. 869 // * MachinePoolClass "default-mp-worker" with corresponding BootstrapTemplate and InfrastructureMachinePoolTemplate. 870 // * The corresponding Cluster.spec.topology: 871 // * with 3 ControlPlane replicas 872 // * with a "default-worker-topo1" MachineDeploymentTopology without replicas (based on "default-worker") 873 // * with a "default-mp-worker-topo1" MachinePoolTopology without replicas (based on "default-mp-worker") 874 // * with a "default-worker-topo2" MachineDeploymentTopology with 3 replicas (based on "default-worker") 875 // * with a "default-mp-worker-topo2" MachinePoolTopology with 3 replicas (based on "default-mp-worker") 876 // * desired: essentially the corresponding desired objects. 877 blueprint, desired := setupTestObjects() 878 879 // If there are patches, set up patch generators. 880 cat := runtimecatalog.New() 881 g.Expect(runtimehooksv1.AddToCatalog(cat)).To(Succeed()) 882 883 runtimeClient := fakeruntimeclient.NewRuntimeClientBuilder().WithCatalog(cat).Build() 884 885 if tt.externalPatchResponses != nil { 886 // replace the package variable uuidGenerator with one that returns an incremented integer. 887 // each patch will have a new uuid in the order in which they're defined and called. 888 var uuid int32 889 uuidGenerator = func() types.UID { 890 uuid++ 891 return types.UID(fmt.Sprintf("%d", uuid)) 892 } 893 runtimeClient = fakeruntimeclient.NewRuntimeClientBuilder(). 894 WithCallExtensionResponses(tt.externalPatchResponses). 895 WithCatalog(cat). 896 Build() 897 } 898 patchEngine := NewEngine(runtimeClient) 899 900 if len(tt.patches) > 0 { 901 // Add the patches. 902 blueprint.ClusterClass.Spec.Patches = tt.patches 903 } 904 if len(tt.varDefinitions) > 0 { 905 // If there are variable definitions in the test add them to the ClusterClass. 906 blueprint.ClusterClass.Status.Variables = tt.varDefinitions 907 } 908 909 // Copy the desired objects before applying patches. 910 expectedCluster := desired.Cluster.DeepCopy() 911 expectedInfrastructureCluster := desired.InfrastructureCluster.DeepCopy() 912 expectedControlPlane := desired.ControlPlane.Object.DeepCopy() 913 expectedControlPlaneInfrastructureMachineTemplate := desired.ControlPlane.InfrastructureMachineTemplate.DeepCopy() 914 expectedBootstrapTemplates := map[string]*unstructured.Unstructured{} 915 expectedInfrastructureMachineTemplate := map[string]*unstructured.Unstructured{} 916 for mdTopology, md := range desired.MachineDeployments { 917 expectedBootstrapTemplates[mdTopology] = md.BootstrapTemplate.DeepCopy() 918 expectedInfrastructureMachineTemplate[mdTopology] = md.InfrastructureMachineTemplate.DeepCopy() 919 } 920 expectedBootstrapConfig := map[string]*unstructured.Unstructured{} 921 expectedInfrastructureMachinePool := map[string]*unstructured.Unstructured{} 922 for mpTopology, mp := range desired.MachinePools { 923 expectedBootstrapConfig[mpTopology] = mp.BootstrapObject.DeepCopy() 924 expectedInfrastructureMachinePool[mpTopology] = mp.InfrastructureMachinePoolObject.DeepCopy() 925 } 926 927 // Set expected fields on the copy of the objects, so they can be used for comparison with the result of Apply. 928 if tt.expectedFields.infrastructureCluster != nil { 929 setSpecFields(expectedInfrastructureCluster, tt.expectedFields.infrastructureCluster) 930 } 931 if tt.expectedFields.controlPlane != nil { 932 setSpecFields(expectedControlPlane, tt.expectedFields.controlPlane) 933 } 934 if tt.expectedFields.controlPlaneInfrastructureMachineTemplate != nil { 935 setSpecFields(expectedControlPlaneInfrastructureMachineTemplate, tt.expectedFields.controlPlaneInfrastructureMachineTemplate) 936 } 937 for mdTopology, expectedFields := range tt.expectedFields.machineDeploymentBootstrapTemplate { 938 setSpecFields(expectedBootstrapTemplates[mdTopology], expectedFields) 939 } 940 for mdTopology, expectedFields := range tt.expectedFields.machineDeploymentInfrastructureMachineTemplate { 941 setSpecFields(expectedInfrastructureMachineTemplate[mdTopology], expectedFields) 942 } 943 for mpTopology, expectedFields := range tt.expectedFields.machinePoolBootstrapConfig { 944 setSpecFields(expectedBootstrapConfig[mpTopology], expectedFields) 945 } 946 for mpTopology, expectedFields := range tt.expectedFields.machinePoolInfrastructureMachinePool { 947 setSpecFields(expectedInfrastructureMachinePool[mpTopology], expectedFields) 948 } 949 950 // Apply patches. 951 if err := patchEngine.Apply(context.Background(), blueprint, desired); err != nil { 952 if !tt.wantErr { 953 t.Fatal(err) 954 } 955 return 956 } 957 958 // Compare the patched desired objects with the expected desired objects. 959 g.Expect(desired.Cluster).To(EqualObject(expectedCluster)) 960 g.Expect(desired.InfrastructureCluster).To(EqualObject(expectedInfrastructureCluster)) 961 g.Expect(desired.ControlPlane.Object).To(EqualObject(expectedControlPlane)) 962 g.Expect(desired.ControlPlane.InfrastructureMachineTemplate).To(EqualObject(expectedControlPlaneInfrastructureMachineTemplate)) 963 for mdTopology, bootstrapTemplate := range expectedBootstrapTemplates { 964 g.Expect(desired.MachineDeployments[mdTopology].BootstrapTemplate).To(EqualObject(bootstrapTemplate)) 965 } 966 for mdTopology, infrastructureMachineTemplate := range expectedInfrastructureMachineTemplate { 967 g.Expect(desired.MachineDeployments[mdTopology].InfrastructureMachineTemplate).To(EqualObject(infrastructureMachineTemplate)) 968 } 969 for mpTopology, bootstrapConfig := range expectedBootstrapConfig { 970 g.Expect(desired.MachinePools[mpTopology].BootstrapObject).To(EqualObject(bootstrapConfig)) 971 } 972 for mpTopology, infrastructureMachinePool := range expectedInfrastructureMachinePool { 973 g.Expect(desired.MachinePools[mpTopology].InfrastructureMachinePoolObject).To(EqualObject(infrastructureMachinePool)) 974 } 975 }) 976 } 977 } 978 979 func setupTestObjects() (*scope.ClusterBlueprint, *scope.ClusterState) { 980 infrastructureClusterTemplate := builder.InfrastructureClusterTemplate(metav1.NamespaceDefault, "infraClusterTemplate1"). 981 Build() 982 983 controlPlaneInfrastructureMachineTemplate := builder.InfrastructureMachineTemplate(metav1.NamespaceDefault, "controlplaneinframachinetemplate1"). 984 Build() 985 controlPlaneTemplate := builder.ControlPlaneTemplate(metav1.NamespaceDefault, "controlPlaneTemplate1"). 986 WithInfrastructureMachineTemplate(controlPlaneInfrastructureMachineTemplate). 987 Build() 988 989 workerInfrastructureMachineTemplate := builder.InfrastructureMachineTemplate(metav1.NamespaceDefault, "linux-worker-inframachinetemplate"). 990 Build() 991 workerInfrastructureMachinePoolTemplate := builder.InfrastructureMachinePoolTemplate(metav1.NamespaceDefault, "linux-worker-inframachinepooltemplate"). 992 Build() 993 workerInfrastructureMachinePool := builder.InfrastructureMachinePool(metav1.NamespaceDefault, "linux-worker-inframachinepool"). 994 Build() 995 workerBootstrapTemplate := builder.BootstrapTemplate(metav1.NamespaceDefault, "linux-worker-bootstrapconfigtemplate"). 996 Build() 997 workerBootstrapConfig := builder.BootstrapConfig(metav1.NamespaceDefault, "linux-worker-bootstrapconfig"). 998 Build() 999 mdClass1 := builder.MachineDeploymentClass("default-worker"). 1000 WithInfrastructureTemplate(workerInfrastructureMachineTemplate). 1001 WithBootstrapTemplate(workerBootstrapTemplate). 1002 Build() 1003 mpClass1 := builder.MachinePoolClass("default-mp-worker"). 1004 WithInfrastructureTemplate(workerInfrastructureMachinePoolTemplate). 1005 WithBootstrapTemplate(workerBootstrapTemplate). 1006 Build() 1007 1008 clusterClass := builder.ClusterClass(metav1.NamespaceDefault, "clusterClass1"). 1009 WithInfrastructureClusterTemplate(infrastructureClusterTemplate). 1010 WithControlPlaneTemplate(controlPlaneTemplate). 1011 WithControlPlaneInfrastructureMachineTemplate(controlPlaneInfrastructureMachineTemplate). 1012 WithWorkerMachineDeploymentClasses(*mdClass1). 1013 WithWorkerMachinePoolClasses(*mpClass1). 1014 Build() 1015 1016 // Note: we depend on TypeMeta being set to calculate HolderReferences correctly. 1017 // We also set TypeMeta explicitly in the topology/cluster/cluster_controller.go. 1018 cluster := &clusterv1.Cluster{ 1019 TypeMeta: metav1.TypeMeta{ 1020 APIVersion: clusterv1.GroupVersion.String(), 1021 Kind: "Cluster", 1022 }, 1023 ObjectMeta: metav1.ObjectMeta{ 1024 Name: "cluster1", 1025 Namespace: metav1.NamespaceDefault, 1026 }, 1027 Spec: clusterv1.ClusterSpec{ 1028 Paused: false, 1029 ClusterNetwork: &clusterv1.ClusterNetwork{ 1030 APIServerPort: ptr.To[int32](8), 1031 Services: &clusterv1.NetworkRanges{ 1032 CIDRBlocks: []string{"10.10.10.1/24"}, 1033 }, 1034 Pods: &clusterv1.NetworkRanges{ 1035 CIDRBlocks: []string{"11.10.10.1/24"}, 1036 }, 1037 ServiceDomain: "lark", 1038 }, 1039 ControlPlaneRef: nil, 1040 InfrastructureRef: nil, 1041 Topology: &clusterv1.Topology{ 1042 Version: "v1.21.2", 1043 Class: clusterClass.Name, 1044 ControlPlane: clusterv1.ControlPlaneTopology{ 1045 Replicas: ptr.To[int32](3), 1046 }, 1047 Variables: []clusterv1.ClusterVariable{ 1048 { 1049 Name: "infraCluster", 1050 Value: apiextensionsv1.JSON{Raw: []byte(`"value99"`)}, 1051 DefinitionFrom: "inline", 1052 }, 1053 { 1054 Name: "infraCluster", 1055 // This variable should not be used as it is from "non-used-patch" which is not applied in any test. 1056 Value: apiextensionsv1.JSON{Raw: []byte(`"should-never-be-used"`)}, 1057 DefinitionFrom: "not-used-patch", 1058 }, 1059 { 1060 Name: "default-worker-infra", 1061 // This value should be overwritten for the default-worker-topo1 MachineDeployment. 1062 Value: apiextensionsv1.JSON{Raw: []byte(`"default-worker-topo2"`)}, 1063 DefinitionFrom: "inline", 1064 }, 1065 { 1066 Name: "default-mp-worker-infra", 1067 // This value should be overwritten for the default-mp-worker-topo1 MachinePool. 1068 Value: apiextensionsv1.JSON{Raw: []byte(`"default-mp-worker-topo2"`)}, 1069 DefinitionFrom: "inline", 1070 }, 1071 }, 1072 Workers: &clusterv1.WorkersTopology{ 1073 MachineDeployments: []clusterv1.MachineDeploymentTopology{ 1074 { 1075 Metadata: clusterv1.ObjectMeta{}, 1076 Class: "default-worker", 1077 Name: "default-worker-topo1", 1078 Variables: &clusterv1.MachineDeploymentVariables{ 1079 Overrides: []clusterv1.ClusterVariable{ 1080 { 1081 Name: "default-worker-infra", 1082 DefinitionFrom: "inline", 1083 Value: apiextensionsv1.JSON{Raw: []byte(`"value1"`)}, 1084 }, 1085 }, 1086 }, 1087 }, 1088 { 1089 Metadata: clusterv1.ObjectMeta{}, 1090 Class: "default-worker", 1091 Name: "default-worker-topo2", 1092 Replicas: ptr.To[int32](5), 1093 }, 1094 }, 1095 MachinePools: []clusterv1.MachinePoolTopology{ 1096 { 1097 Metadata: clusterv1.ObjectMeta{}, 1098 Class: "default-mp-worker", 1099 Name: "default-mp-worker-topo1", 1100 Variables: &clusterv1.MachinePoolVariables{ 1101 Overrides: []clusterv1.ClusterVariable{ 1102 { 1103 Name: "default-mp-worker-infra", 1104 DefinitionFrom: "inline", 1105 Value: apiextensionsv1.JSON{Raw: []byte(`"value2"`)}, 1106 }, 1107 }, 1108 }, 1109 }, 1110 { 1111 Metadata: clusterv1.ObjectMeta{}, 1112 Class: "default-mp-worker", 1113 Name: "default-mp-worker-topo2", 1114 Replicas: ptr.To[int32](5), 1115 }, 1116 }, 1117 }, 1118 }, 1119 }, 1120 } 1121 1122 // Aggregating Cluster, Templates and ClusterClass into a blueprint. 1123 blueprint := &scope.ClusterBlueprint{ 1124 Topology: cluster.Spec.Topology, 1125 ClusterClass: clusterClass, 1126 InfrastructureClusterTemplate: infrastructureClusterTemplate, 1127 ControlPlane: &scope.ControlPlaneBlueprint{ 1128 Template: controlPlaneTemplate, 1129 InfrastructureMachineTemplate: controlPlaneInfrastructureMachineTemplate, 1130 }, 1131 MachineDeployments: map[string]*scope.MachineDeploymentBlueprint{ 1132 "default-worker": { 1133 InfrastructureMachineTemplate: workerInfrastructureMachineTemplate, 1134 BootstrapTemplate: workerBootstrapTemplate, 1135 }, 1136 }, 1137 MachinePools: map[string]*scope.MachinePoolBlueprint{ 1138 "default-mp-worker": { 1139 InfrastructureMachinePoolTemplate: workerInfrastructureMachinePoolTemplate, 1140 BootstrapTemplate: workerBootstrapTemplate, 1141 }, 1142 }, 1143 } 1144 1145 // Create a Cluster using the ClusterClass from above with multiple MachineDeployments 1146 // using the same MachineDeployment class. 1147 desiredCluster := cluster.DeepCopy() 1148 1149 infrastructureCluster := builder.InfrastructureCluster(metav1.NamespaceDefault, "infraClusterTemplate1"). 1150 WithSpecFields(map[string]interface{}{ 1151 // Add an empty spec field, to make sure the InfrastructureCluster matches 1152 // the one calculated by computeInfrastructureCluster. 1153 "spec": map[string]interface{}{}, 1154 }). 1155 Build() 1156 1157 controlPlane := builder.ControlPlane(metav1.NamespaceDefault, "controlPlane1"). 1158 WithVersion("v1.21.2"). 1159 WithReplicas(3). 1160 // Make sure we're using an independent instance of the template. 1161 WithInfrastructureMachineTemplate(controlPlaneInfrastructureMachineTemplate.DeepCopy()). 1162 Build() 1163 1164 desired := &scope.ClusterState{ 1165 Cluster: desiredCluster, 1166 InfrastructureCluster: infrastructureCluster, 1167 ControlPlane: &scope.ControlPlaneState{ 1168 Object: controlPlane, 1169 // Make sure we're using an independent instance of the template. 1170 InfrastructureMachineTemplate: controlPlaneInfrastructureMachineTemplate.DeepCopy(), 1171 }, 1172 MachineDeployments: map[string]*scope.MachineDeploymentState{ 1173 "default-worker-topo1": { 1174 Object: builder.MachineDeployment(metav1.NamespaceDefault, "md1"). 1175 WithLabels(map[string]string{clusterv1.ClusterTopologyMachineDeploymentNameLabel: "default-worker-topo1"}). 1176 WithVersion("v1.21.2"). 1177 Build(), 1178 // Make sure we're using an independent instance of the template. 1179 InfrastructureMachineTemplate: workerInfrastructureMachineTemplate.DeepCopy(), 1180 BootstrapTemplate: workerBootstrapTemplate.DeepCopy(), 1181 }, 1182 "default-worker-topo2": { 1183 Object: builder.MachineDeployment(metav1.NamespaceDefault, "md2"). 1184 WithLabels(map[string]string{clusterv1.ClusterTopologyMachineDeploymentNameLabel: "default-worker-topo2"}). 1185 WithVersion("v1.20.6"). 1186 WithReplicas(5). 1187 Build(), 1188 // Make sure we're using an independent instance of the template. 1189 InfrastructureMachineTemplate: workerInfrastructureMachineTemplate.DeepCopy(), 1190 BootstrapTemplate: workerBootstrapTemplate.DeepCopy(), 1191 }, 1192 }, 1193 MachinePools: map[string]*scope.MachinePoolState{ 1194 "default-mp-worker-topo1": { 1195 Object: builder.MachinePool(metav1.NamespaceDefault, "mp1"). 1196 WithLabels(map[string]string{clusterv1.ClusterTopologyMachinePoolNameLabel: "default-mp-worker-topo1"}). 1197 WithVersion("v1.21.2"). 1198 Build(), 1199 // Make sure we're using an independent instance of the template. 1200 InfrastructureMachinePoolObject: workerInfrastructureMachinePool.DeepCopy(), 1201 BootstrapObject: workerBootstrapConfig.DeepCopy(), 1202 }, 1203 "default-mp-worker-topo2": { 1204 Object: builder.MachinePool(metav1.NamespaceDefault, "mp2"). 1205 WithLabels(map[string]string{clusterv1.ClusterTopologyMachinePoolNameLabel: "default-mp-worker-topo2"}). 1206 WithVersion("v1.20.6"). 1207 WithReplicas(5). 1208 Build(), 1209 // Make sure we're using an independent instance of the template. 1210 InfrastructureMachinePoolObject: workerInfrastructureMachinePool.DeepCopy(), 1211 BootstrapObject: workerBootstrapConfig.DeepCopy(), 1212 }, 1213 }, 1214 } 1215 return blueprint, desired 1216 } 1217 1218 // setSpecFields sets fields on an unstructured object from a map. 1219 func setSpecFields(obj *unstructured.Unstructured, fields map[string]interface{}) { 1220 for k, v := range fields { 1221 fieldParts := strings.Split(k, ".") 1222 if len(fieldParts) == 0 { 1223 panic(fmt.Errorf("fieldParts invalid")) 1224 } 1225 if fieldParts[0] != "spec" { 1226 panic(fmt.Errorf("can not set fields outside spec")) 1227 } 1228 if err := unstructured.SetNestedField(obj.UnstructuredContent(), v, strings.Split(k, ".")...); err != nil { 1229 panic(err) 1230 } 1231 } 1232 } 1233 1234 // jsonPatchRFC6902 represents a jsonPatch. 1235 type jsonPatchRFC6902 struct { 1236 Op string `json:"op"` 1237 Path string `json:"path"` 1238 Value *apiextensionsv1.JSON `json:"value,omitempty"` 1239 } 1240 1241 func bytesPatch(patch []jsonPatchRFC6902) []byte { 1242 out, err := json.Marshal(patch) 1243 if err != nil { 1244 panic(err) 1245 } 1246 return out 1247 }