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  }