sigs.k8s.io/cluster-api@v1.7.1/internal/controllers/topology/cluster/current_state_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 cluster
    18  
    19  import (
    20  	"testing"
    21  	"time"
    22  
    23  	"github.com/google/go-cmp/cmp"
    24  	. "github.com/onsi/gomega"
    25  	corev1 "k8s.io/api/core/v1"
    26  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    27  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    28  	"sigs.k8s.io/controller-runtime/pkg/client"
    29  	"sigs.k8s.io/controller-runtime/pkg/client/fake"
    30  	. "sigs.k8s.io/controller-runtime/pkg/envtest/komega"
    31  
    32  	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
    33  	"sigs.k8s.io/cluster-api/exp/topology/scope"
    34  	"sigs.k8s.io/cluster-api/internal/test/builder"
    35  	"sigs.k8s.io/cluster-api/internal/topology/selectors"
    36  )
    37  
    38  func TestGetCurrentState(t *testing.T) {
    39  	crds := []client.Object{
    40  		builder.GenericControlPlaneCRD,
    41  		builder.GenericInfrastructureClusterCRD,
    42  		builder.GenericControlPlaneTemplateCRD,
    43  		builder.GenericInfrastructureClusterTemplateCRD,
    44  		builder.GenericBootstrapConfigTemplateCRD,
    45  		builder.GenericInfrastructureMachineTemplateCRD,
    46  		builder.GenericInfrastructureMachineCRD,
    47  		builder.GenericInfrastructureMachinePoolTemplateCRD,
    48  		builder.GenericInfrastructureMachinePoolCRD,
    49  	}
    50  
    51  	// The following is a block creating a number of objects for use in the test cases.
    52  
    53  	// InfrastructureCluster objects.
    54  	infraCluster := builder.InfrastructureCluster(metav1.NamespaceDefault, "infraOne").
    55  		Build()
    56  	infraCluster.SetLabels(map[string]string{clusterv1.ClusterTopologyOwnedLabel: ""})
    57  	infraClusterNotTopologyOwned := builder.InfrastructureCluster(metav1.NamespaceDefault, "infraOne").
    58  		Build()
    59  	infraClusterTemplate := builder.InfrastructureClusterTemplate(metav1.NamespaceDefault, "infraTemplateOne").
    60  		Build()
    61  
    62  	// ControlPlane and ControlPlaneInfrastructureMachineTemplate objects.
    63  	controlPlaneInfrastructureMachineTemplate := builder.InfrastructureMachineTemplate(metav1.NamespaceDefault, "cpInfraTemplate").
    64  		Build()
    65  	controlPlaneInfrastructureMachineTemplate.SetLabels(map[string]string{clusterv1.ClusterTopologyOwnedLabel: ""})
    66  	controlPlaneInfrastructureMachineTemplateNotTopologyOwned := builder.InfrastructureMachineTemplate(metav1.NamespaceDefault, "cpInfraTemplate").
    67  		Build()
    68  	controlPlaneTemplateWithInfrastructureMachine := builder.ControlPlaneTemplate(metav1.NamespaceDefault, "cpTemplateWithInfra1").
    69  		WithInfrastructureMachineTemplate(controlPlaneInfrastructureMachineTemplate).
    70  		Build()
    71  	controlPlaneTemplateWithInfrastructureMachineNotTopologyOwned := builder.ControlPlaneTemplate(metav1.NamespaceDefault, "cpTemplateWithInfra1").
    72  		WithInfrastructureMachineTemplate(controlPlaneInfrastructureMachineTemplateNotTopologyOwned).
    73  		Build()
    74  	controlPlane := builder.ControlPlane(metav1.NamespaceDefault, "cp1").
    75  		Build()
    76  	controlPlane.SetLabels(map[string]string{clusterv1.ClusterTopologyOwnedLabel: ""})
    77  	controlPlaneWithInfra := builder.ControlPlane(metav1.NamespaceDefault, "cp1").
    78  		WithInfrastructureMachineTemplate(controlPlaneInfrastructureMachineTemplate).
    79  		Build()
    80  	controlPlaneWithInfra.SetLabels(map[string]string{clusterv1.ClusterTopologyOwnedLabel: ""})
    81  	controlPlaneWithInfraNotTopologyOwned := builder.ControlPlane(metav1.NamespaceDefault, "cp1").
    82  		WithInfrastructureMachineTemplate(controlPlaneInfrastructureMachineTemplateNotTopologyOwned).
    83  		Build()
    84  	controlPlaneNotTopologyOwned := builder.ControlPlane(metav1.NamespaceDefault, "cp1").
    85  		Build()
    86  
    87  	// ClusterClass  objects.
    88  	clusterClassWithControlPlaneInfra := builder.ClusterClass(metav1.NamespaceDefault, "class1").
    89  		WithControlPlaneTemplate(controlPlaneTemplateWithInfrastructureMachine).
    90  		WithControlPlaneInfrastructureMachineTemplate(controlPlaneInfrastructureMachineTemplate).
    91  		Build()
    92  	clusterClassWithControlPlaneInfraNotTopologyOwned := builder.ClusterClass(metav1.NamespaceDefault, "class1").
    93  		WithControlPlaneTemplate(controlPlaneTemplateWithInfrastructureMachineNotTopologyOwned).
    94  		WithControlPlaneInfrastructureMachineTemplate(controlPlaneInfrastructureMachineTemplateNotTopologyOwned).
    95  		Build()
    96  	clusterClassWithNoControlPlaneInfra := builder.ClusterClass(metav1.NamespaceDefault, "class2").
    97  		Build()
    98  
    99  	// MachineDeployment and related objects.
   100  	emptyMachineDeployments := make(map[string]*scope.MachineDeploymentState)
   101  
   102  	machineDeploymentInfrastructure := builder.InfrastructureMachineTemplate(metav1.NamespaceDefault, "infra1").
   103  		Build()
   104  	machineDeploymentInfrastructure.SetLabels(map[string]string{clusterv1.ClusterTopologyOwnedLabel: ""})
   105  	machineDeploymentBootstrap := builder.BootstrapTemplate(metav1.NamespaceDefault, "bootstrap1").
   106  		Build()
   107  	machineDeploymentBootstrap.SetLabels(map[string]string{clusterv1.ClusterTopologyOwnedLabel: ""})
   108  
   109  	machineDeployment := builder.MachineDeployment(metav1.NamespaceDefault, "md1").
   110  		WithLabels(map[string]string{
   111  			clusterv1.ClusterNameLabel:                          "cluster1",
   112  			clusterv1.ClusterTopologyOwnedLabel:                 "",
   113  			clusterv1.ClusterTopologyMachineDeploymentNameLabel: "md1",
   114  		}).
   115  		WithBootstrapTemplate(machineDeploymentBootstrap).
   116  		WithInfrastructureTemplate(machineDeploymentInfrastructure).
   117  		Build()
   118  	machineDeployment2 := builder.MachineDeployment(metav1.NamespaceDefault, "md2").
   119  		WithLabels(map[string]string{
   120  			clusterv1.ClusterNameLabel:                          "cluster1",
   121  			clusterv1.ClusterTopologyOwnedLabel:                 "",
   122  			clusterv1.ClusterTopologyMachineDeploymentNameLabel: "md2",
   123  		}).
   124  		WithBootstrapTemplate(machineDeploymentBootstrap).
   125  		WithInfrastructureTemplate(machineDeploymentInfrastructure).
   126  		Build()
   127  
   128  	// MachineHealthChecks for the MachineDeployment and the ControlPlane.
   129  	machineHealthCheckForMachineDeployment := builder.MachineHealthCheck(machineDeployment.Namespace, machineDeployment.Name).
   130  		WithSelector(*selectors.ForMachineDeploymentMHC(machineDeployment)).
   131  		WithUnhealthyConditions([]clusterv1.UnhealthyCondition{
   132  			{
   133  				Type:    corev1.NodeReady,
   134  				Status:  corev1.ConditionUnknown,
   135  				Timeout: metav1.Duration{Duration: 5 * time.Minute},
   136  			},
   137  			{
   138  				Type:    corev1.NodeReady,
   139  				Status:  corev1.ConditionFalse,
   140  				Timeout: metav1.Duration{Duration: 5 * time.Minute},
   141  			},
   142  		}).
   143  		WithClusterName("cluster1").
   144  		Build()
   145  
   146  	machineHealthCheckForControlPlane := builder.MachineHealthCheck(controlPlane.GetNamespace(), controlPlane.GetName()).
   147  		WithSelector(*selectors.ForControlPlaneMHC()).
   148  		WithUnhealthyConditions([]clusterv1.UnhealthyCondition{
   149  			{
   150  				Type:    corev1.NodeReady,
   151  				Status:  corev1.ConditionUnknown,
   152  				Timeout: metav1.Duration{Duration: 5 * time.Minute},
   153  			},
   154  			{
   155  				Type:    corev1.NodeReady,
   156  				Status:  corev1.ConditionFalse,
   157  				Timeout: metav1.Duration{Duration: 5 * time.Minute},
   158  			},
   159  		}).
   160  		WithClusterName("cluster1").
   161  		Build()
   162  
   163  	// MachinePool and related objects.
   164  	emptyMachinePools := make(map[string]*scope.MachinePoolState)
   165  
   166  	machinePoolInfrastructureTemplate := builder.InfrastructureMachinePoolTemplate(metav1.NamespaceDefault, "infra2").
   167  		Build()
   168  	machinePoolInfrastructure := builder.InfrastructureMachinePool(metav1.NamespaceDefault, "infra2").
   169  		Build()
   170  	machinePoolInfrastructure.SetLabels(map[string]string{clusterv1.ClusterTopologyOwnedLabel: ""})
   171  	machinePoolBootstrapTemplate := builder.BootstrapTemplate(metav1.NamespaceDefault, "bootstrap2").
   172  		Build()
   173  	machinePoolBootstrap := builder.BootstrapConfig(metav1.NamespaceDefault, "bootstrap2").
   174  		Build()
   175  	machinePoolBootstrap.SetLabels(map[string]string{clusterv1.ClusterTopologyOwnedLabel: ""})
   176  
   177  	machinePool := builder.MachinePool(metav1.NamespaceDefault, "mp1").
   178  		WithLabels(map[string]string{
   179  			clusterv1.ClusterNameLabel:                    "cluster1",
   180  			clusterv1.ClusterTopologyOwnedLabel:           "",
   181  			clusterv1.ClusterTopologyMachinePoolNameLabel: "mp1",
   182  		}).
   183  		WithBootstrap(machinePoolBootstrap).
   184  		WithInfrastructure(machinePoolInfrastructure).
   185  		Build()
   186  	machinePool2 := builder.MachinePool(metav1.NamespaceDefault, "mp2").
   187  		WithLabels(map[string]string{
   188  			clusterv1.ClusterNameLabel:                    "cluster1",
   189  			clusterv1.ClusterTopologyOwnedLabel:           "",
   190  			clusterv1.ClusterTopologyMachinePoolNameLabel: "mp2",
   191  		}).
   192  		WithBootstrap(machinePoolBootstrap).
   193  		WithInfrastructure(machinePoolInfrastructure).
   194  		Build()
   195  
   196  	tests := []struct {
   197  		name      string
   198  		cluster   *clusterv1.Cluster
   199  		blueprint *scope.ClusterBlueprint
   200  		objects   []client.Object
   201  		want      *scope.ClusterState
   202  		wantErr   bool
   203  	}{
   204  		{
   205  			name:      "Should read a Cluster when being processed by the topology controller for the first time (without references)",
   206  			cluster:   builder.Cluster(metav1.NamespaceDefault, "cluster1").Build(),
   207  			blueprint: &scope.ClusterBlueprint{},
   208  			// Expecting valid return with no ControlPlane or Infrastructure state defined and empty MachineDeployment state list
   209  			want: &scope.ClusterState{
   210  				Cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").
   211  					// No InfrastructureCluster or ControlPlane references!
   212  					Build(),
   213  				ControlPlane:          &scope.ControlPlaneState{},
   214  				InfrastructureCluster: nil,
   215  				MachineDeployments:    emptyMachineDeployments,
   216  				MachinePools:          emptyMachinePools,
   217  			},
   218  		},
   219  		{
   220  			name: "Fails if the Cluster references an InfrastructureCluster that does not exist",
   221  			cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").
   222  				WithInfrastructureCluster(infraCluster).
   223  				Build(),
   224  			blueprint: &scope.ClusterBlueprint{
   225  				InfrastructureClusterTemplate: infraClusterTemplate,
   226  			},
   227  			objects: []client.Object{
   228  				// InfrastructureCluster is missing!
   229  			},
   230  			wantErr: true, // this test fails as partial reconcile is undefined.
   231  		},
   232  		{
   233  			name: "Fails if the Cluster references an InfrastructureCluster that is not topology owned",
   234  			cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").
   235  				WithInfrastructureCluster(infraClusterNotTopologyOwned).
   236  				Build(),
   237  			blueprint: &scope.ClusterBlueprint{
   238  				InfrastructureClusterTemplate: infraClusterTemplate,
   239  			},
   240  			objects: []client.Object{
   241  				infraClusterNotTopologyOwned,
   242  			},
   243  			wantErr: true, // this test fails as partial reconcile is undefined.
   244  		},
   245  		{
   246  			name: "Fails if the Cluster references an Control Plane that is not topology owned",
   247  			cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").
   248  				WithControlPlane(controlPlaneNotTopologyOwned).
   249  				Build(),
   250  			blueprint: &scope.ClusterBlueprint{
   251  				ClusterClass: clusterClassWithControlPlaneInfra,
   252  				ControlPlane: &scope.ControlPlaneBlueprint{
   253  					Template: controlPlaneTemplateWithInfrastructureMachine,
   254  				},
   255  			},
   256  			objects: []client.Object{
   257  				controlPlaneNotTopologyOwned,
   258  			},
   259  			wantErr: true, // this test fails as partial reconcile is undefined.
   260  		},
   261  		{
   262  			name: "Should read  a partial Cluster (with InfrastructureCluster only)",
   263  			cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").
   264  				WithInfrastructureCluster(infraCluster).
   265  				// No ControlPlane reference!
   266  				Build(),
   267  			blueprint: &scope.ClusterBlueprint{
   268  				InfrastructureClusterTemplate: infraClusterTemplate,
   269  			},
   270  			objects: []client.Object{
   271  				infraCluster,
   272  			},
   273  			// Expecting valid return with no ControlPlane, MachineDeployment, or MachinePool state defined but with a valid Infrastructure state.
   274  			want: &scope.ClusterState{
   275  				Cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").
   276  					WithInfrastructureCluster(infraCluster).
   277  					Build(),
   278  				ControlPlane:          &scope.ControlPlaneState{},
   279  				InfrastructureCluster: infraCluster,
   280  				MachineDeployments:    emptyMachineDeployments,
   281  				MachinePools:          emptyMachinePools,
   282  			},
   283  		},
   284  		{
   285  			name: "Should read a partial Cluster (with InfrastructureCluster, ControlPlane, but without workers)",
   286  			cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").
   287  				WithControlPlane(controlPlane).
   288  				WithInfrastructureCluster(infraCluster).
   289  				Build(),
   290  			blueprint: &scope.ClusterBlueprint{
   291  				ClusterClass:                  clusterClassWithNoControlPlaneInfra,
   292  				InfrastructureClusterTemplate: infraClusterTemplate,
   293  				ControlPlane: &scope.ControlPlaneBlueprint{
   294  					Template: controlPlaneTemplateWithInfrastructureMachine,
   295  				},
   296  			},
   297  			objects: []client.Object{
   298  				controlPlane,
   299  				infraCluster,
   300  				clusterClassWithNoControlPlaneInfra,
   301  				// Workers are missing!
   302  			},
   303  			// Expecting valid return with ControlPlane, no ControlPlane Infrastructure state, InfrastructureCluster state, and no defined MachineDeployment or MachinePool state.
   304  			want: &scope.ClusterState{
   305  				Cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").
   306  					WithControlPlane(controlPlane).
   307  					WithInfrastructureCluster(infraCluster).
   308  					Build(),
   309  				ControlPlane:          &scope.ControlPlaneState{Object: controlPlane, InfrastructureMachineTemplate: nil},
   310  				InfrastructureCluster: infraCluster,
   311  				MachineDeployments:    emptyMachineDeployments,
   312  				MachinePools:          emptyMachinePools,
   313  			},
   314  		},
   315  		{
   316  			name: "Fails if the ClusterClass requires InfrastructureMachine for the ControlPlane, but the ControlPlane object does not have a reference for it",
   317  			cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").
   318  				WithControlPlane(controlPlane).
   319  				WithInfrastructureCluster(infraCluster).
   320  				Build(),
   321  			blueprint: &scope.ClusterBlueprint{
   322  				ClusterClass:                  clusterClassWithControlPlaneInfra,
   323  				InfrastructureClusterTemplate: infraClusterTemplate,
   324  				ControlPlane: &scope.ControlPlaneBlueprint{
   325  					Template: controlPlaneTemplateWithInfrastructureMachine,
   326  				},
   327  			},
   328  			objects: []client.Object{
   329  				controlPlane,
   330  				infraCluster,
   331  				clusterClassWithControlPlaneInfra,
   332  			},
   333  			// Expecting error from ControlPlane having no valid ControlPlane Infrastructure with ClusterClass requiring ControlPlane Infrastructure.
   334  			wantErr: true,
   335  		},
   336  		{
   337  			name: "Should read a partial Cluster (with ControlPlane and ControlPlane InfrastructureMachineTemplate, but without InfrastructureCluster and workers)",
   338  			cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").
   339  				// No InfrastructureCluster!
   340  				WithControlPlane(controlPlaneWithInfra).
   341  				Build(),
   342  			blueprint: &scope.ClusterBlueprint{
   343  				ClusterClass: clusterClassWithControlPlaneInfra,
   344  				ControlPlane: &scope.ControlPlaneBlueprint{
   345  					Template:                      controlPlaneTemplateWithInfrastructureMachine,
   346  					InfrastructureMachineTemplate: controlPlaneInfrastructureMachineTemplate,
   347  				},
   348  			},
   349  			objects: []client.Object{
   350  				controlPlaneWithInfra,
   351  				controlPlaneInfrastructureMachineTemplate,
   352  				// Workers are missing!
   353  			},
   354  			// Expecting valid return with valid ControlPlane state, but no ControlPlane Infrastructure, InfrastructureCluster, MachineDeployment, or MachinePool state defined.
   355  			want: &scope.ClusterState{
   356  				Cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").
   357  					WithControlPlane(controlPlaneWithInfra).
   358  					Build(),
   359  				ControlPlane:          &scope.ControlPlaneState{Object: controlPlaneWithInfra, InfrastructureMachineTemplate: controlPlaneInfrastructureMachineTemplate},
   360  				InfrastructureCluster: nil,
   361  				MachineDeployments:    emptyMachineDeployments,
   362  				MachinePools:          emptyMachinePools,
   363  			},
   364  		},
   365  		{
   366  			name: "Should read a partial Cluster (with InfrastructureCluster ControlPlane and ControlPlane InfrastructureMachineTemplate, but without workers)",
   367  			cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").
   368  				WithInfrastructureCluster(infraCluster).
   369  				WithControlPlane(controlPlaneWithInfra).
   370  				Build(),
   371  			blueprint: &scope.ClusterBlueprint{
   372  				ClusterClass:                  clusterClassWithControlPlaneInfra,
   373  				InfrastructureClusterTemplate: infraClusterTemplate,
   374  				ControlPlane: &scope.ControlPlaneBlueprint{
   375  					Template:                      controlPlaneTemplateWithInfrastructureMachine,
   376  					InfrastructureMachineTemplate: controlPlaneInfrastructureMachineTemplate,
   377  				},
   378  			},
   379  			objects: []client.Object{
   380  				infraCluster,
   381  				clusterClassWithControlPlaneInfra,
   382  				controlPlaneInfrastructureMachineTemplate,
   383  				controlPlaneWithInfra,
   384  				// Workers are missing!
   385  			},
   386  			// Expecting valid return with valid ControlPlane state, ControlPlane Infrastructure state and InfrastructureCluster state, but no defined MachineDeployment or MachinePool state.
   387  			want: &scope.ClusterState{
   388  				Cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").
   389  					WithInfrastructureCluster(infraCluster).
   390  					WithControlPlane(controlPlaneWithInfra).
   391  					Build(),
   392  				ControlPlane:          &scope.ControlPlaneState{Object: controlPlaneWithInfra, InfrastructureMachineTemplate: controlPlaneInfrastructureMachineTemplate},
   393  				InfrastructureCluster: infraCluster,
   394  				MachineDeployments:    emptyMachineDeployments,
   395  				MachinePools:          emptyMachinePools,
   396  			},
   397  		},
   398  		{
   399  			name: "Should read a Cluster (with InfrastructureCluster, ControlPlane and ControlPlane InfrastructureMachineTemplate, workers)",
   400  			cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").
   401  				WithTopology(builder.ClusterTopology().
   402  					WithMachineDeployment(clusterv1.MachineDeploymentTopology{
   403  						Class: "mdClass",
   404  						Name:  "md1",
   405  					}).
   406  					WithMachinePool(clusterv1.MachinePoolTopology{
   407  						Class: "mpClass",
   408  						Name:  "mp1",
   409  					}).
   410  					Build()).
   411  				Build(),
   412  			blueprint: &scope.ClusterBlueprint{
   413  				ClusterClass:                  clusterClassWithControlPlaneInfra,
   414  				InfrastructureClusterTemplate: infraClusterTemplate,
   415  				ControlPlane: &scope.ControlPlaneBlueprint{
   416  					Template:                      controlPlaneTemplateWithInfrastructureMachine,
   417  					InfrastructureMachineTemplate: controlPlaneInfrastructureMachineTemplate,
   418  				},
   419  				MachineDeployments: map[string]*scope.MachineDeploymentBlueprint{
   420  					"mdClass": {
   421  						BootstrapTemplate:             machineDeploymentBootstrap,
   422  						InfrastructureMachineTemplate: machineDeploymentInfrastructure,
   423  					},
   424  				},
   425  				MachinePools: map[string]*scope.MachinePoolBlueprint{
   426  					"mpClass": {
   427  						BootstrapTemplate:                 machinePoolBootstrap,
   428  						InfrastructureMachinePoolTemplate: machinePoolInfrastructure,
   429  					},
   430  				},
   431  			},
   432  			objects: []client.Object{
   433  				infraCluster,
   434  				clusterClassWithControlPlaneInfra,
   435  				controlPlaneInfrastructureMachineTemplate,
   436  				controlPlaneWithInfra,
   437  				machineDeploymentInfrastructure,
   438  				machineDeploymentBootstrap,
   439  				machineDeployment,
   440  				machinePoolInfrastructure,
   441  				machinePoolBootstrap,
   442  				machinePool,
   443  			},
   444  			// Expecting valid return with valid ControlPlane, ControlPlane Infrastructure, InfrastructureCluster, MachineDeployment and MachinePool state.
   445  			want: &scope.ClusterState{
   446  				Cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").
   447  					WithTopology(builder.ClusterTopology().
   448  						WithMachineDeployment(clusterv1.MachineDeploymentTopology{
   449  							Class: "mdClass",
   450  							Name:  "md1",
   451  						}).
   452  						WithMachinePool(clusterv1.MachinePoolTopology{
   453  							Class: "mpClass",
   454  							Name:  "mp1",
   455  						}).
   456  						Build()).
   457  					Build(),
   458  				ControlPlane:          &scope.ControlPlaneState{},
   459  				InfrastructureCluster: nil,
   460  				MachineDeployments: map[string]*scope.MachineDeploymentState{
   461  					"md1": {Object: machineDeployment, BootstrapTemplate: machineDeploymentBootstrap, InfrastructureMachineTemplate: machineDeploymentInfrastructure}},
   462  				MachinePools: map[string]*scope.MachinePoolState{
   463  					"mp1": {Object: machinePool, BootstrapObject: machinePoolBootstrap, InfrastructureMachinePoolObject: machinePoolInfrastructure}},
   464  			},
   465  		},
   466  		{
   467  			name: "Fails if the ControlPlane references an InfrastructureMachineTemplate that is not topology owned",
   468  			cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").
   469  				// No InfrastructureCluster!
   470  				WithControlPlane(controlPlaneWithInfraNotTopologyOwned).
   471  				Build(),
   472  			blueprint: &scope.ClusterBlueprint{
   473  				ClusterClass: clusterClassWithControlPlaneInfraNotTopologyOwned,
   474  				ControlPlane: &scope.ControlPlaneBlueprint{
   475  					Template:                      controlPlaneTemplateWithInfrastructureMachine,
   476  					InfrastructureMachineTemplate: controlPlaneInfrastructureMachineTemplate,
   477  				},
   478  			},
   479  			objects: []client.Object{
   480  				controlPlaneWithInfraNotTopologyOwned,
   481  				controlPlaneInfrastructureMachineTemplateNotTopologyOwned,
   482  			},
   483  			wantErr: true,
   484  		},
   485  		{
   486  			name: "Fails if the ControlPlane references an InfrastructureMachineTemplate that does not exist",
   487  			cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").
   488  				WithControlPlane(controlPlane).
   489  				Build(),
   490  			blueprint: &scope.ClusterBlueprint{
   491  				ClusterClass: clusterClassWithControlPlaneInfra,
   492  				ControlPlane: &scope.ControlPlaneBlueprint{
   493  					Template:                      controlPlaneTemplateWithInfrastructureMachine,
   494  					InfrastructureMachineTemplate: controlPlaneInfrastructureMachineTemplate,
   495  				},
   496  			},
   497  			objects: []client.Object{
   498  				clusterClassWithControlPlaneInfra,
   499  				controlPlane,
   500  				// InfrastructureMachineTemplate is missing!
   501  			},
   502  			// Expecting error as ClusterClass references ControlPlane Infrastructure, but ControlPlane Infrastructure is missing in the cluster.
   503  			wantErr: true,
   504  		},
   505  		{
   506  			name: "Should ignore unmanaged MachineDeployments/MachinePools and MachineDeployments/MachinePools belonging to other clusters",
   507  			cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").
   508  				Build(),
   509  			blueprint: &scope.ClusterBlueprint{ClusterClass: clusterClassWithControlPlaneInfra},
   510  			objects: []client.Object{
   511  				clusterClassWithControlPlaneInfra,
   512  				builder.MachineDeployment(metav1.NamespaceDefault, "no-managed-label").
   513  					WithLabels(map[string]string{
   514  						clusterv1.ClusterNameLabel: "cluster1",
   515  						// topology.cluster.x-k8s.io/owned label is missing (unmanaged)!
   516  					}).
   517  					WithBootstrapTemplate(machineDeploymentBootstrap).
   518  					WithInfrastructureTemplate(machineDeploymentInfrastructure).
   519  					Build(),
   520  				builder.MachineDeployment(metav1.NamespaceDefault, "wrong-cluster-label").
   521  					WithLabels(map[string]string{
   522  						clusterv1.ClusterNameLabel:                          "another-cluster",
   523  						clusterv1.ClusterTopologyOwnedLabel:                 "",
   524  						clusterv1.ClusterTopologyMachineDeploymentNameLabel: "md1",
   525  					}).
   526  					WithBootstrapTemplate(machineDeploymentBootstrap).
   527  					WithInfrastructureTemplate(machineDeploymentInfrastructure).
   528  					Build(),
   529  				builder.MachinePool(metav1.NamespaceDefault, "no-managed-label").
   530  					WithLabels(map[string]string{
   531  						clusterv1.ClusterNameLabel: "cluster1",
   532  						// topology.cluster.x-k8s.io/owned label is missing (unmanaged)!
   533  					}).
   534  					WithBootstrap(machinePoolBootstrap).
   535  					WithInfrastructure(machinePoolInfrastructure).
   536  					Build(),
   537  				builder.MachinePool(metav1.NamespaceDefault, "wrong-cluster-label").
   538  					WithLabels(map[string]string{
   539  						clusterv1.ClusterNameLabel:                    "another-cluster",
   540  						clusterv1.ClusterTopologyOwnedLabel:           "",
   541  						clusterv1.ClusterTopologyMachinePoolNameLabel: "mp1",
   542  					}).
   543  					WithBootstrap(machinePoolBootstrap).
   544  					WithInfrastructure(machinePoolInfrastructure).
   545  					Build(),
   546  			},
   547  			// Expect valid return with empty MachineDeployments and MachinePools properly filtered by label.
   548  			want: &scope.ClusterState{
   549  				Cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").
   550  					Build(),
   551  				ControlPlane:          &scope.ControlPlaneState{},
   552  				InfrastructureCluster: nil,
   553  				MachineDeployments:    emptyMachineDeployments,
   554  				MachinePools:          emptyMachinePools,
   555  			},
   556  		},
   557  		{
   558  			name: "Fails if there are MachineDeployments without the topology.cluster.x-k8s.io/deployment-name",
   559  			cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").
   560  				Build(),
   561  			blueprint: &scope.ClusterBlueprint{ClusterClass: clusterClassWithControlPlaneInfra},
   562  			objects: []client.Object{
   563  				clusterClassWithControlPlaneInfra,
   564  				builder.MachineDeployment(metav1.NamespaceDefault, "missing-topology-md-labelName").
   565  					WithLabels(map[string]string{
   566  						clusterv1.ClusterNameLabel:          "cluster1",
   567  						clusterv1.ClusterTopologyOwnedLabel: "",
   568  						// topology.cluster.x-k8s.io/deployment-name label is missing!
   569  					}).
   570  					WithBootstrapTemplate(machineDeploymentBootstrap).
   571  					WithInfrastructureTemplate(machineDeploymentInfrastructure).
   572  					Build(),
   573  			},
   574  			// Expect error to be thrown as no managed MachineDeployment is reconcilable unless it has a ClusterTopologyMachineDeploymentNameLabel.
   575  			wantErr: true,
   576  		},
   577  		{
   578  			name: "Fails if there are MachineDeployments with the same topology.cluster.x-k8s.io/deployment-name",
   579  			cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").
   580  				WithTopology(builder.ClusterTopology().
   581  					WithMachineDeployment(clusterv1.MachineDeploymentTopology{
   582  						Class: "mdClass",
   583  						Name:  "md1",
   584  					}).
   585  					Build()).
   586  				Build(),
   587  			blueprint: &scope.ClusterBlueprint{
   588  				ClusterClass: clusterClassWithControlPlaneInfra,
   589  				ControlPlane: &scope.ControlPlaneBlueprint{
   590  					Template:                      controlPlaneTemplateWithInfrastructureMachine,
   591  					InfrastructureMachineTemplate: controlPlaneInfrastructureMachineTemplate,
   592  				},
   593  			},
   594  			objects: []client.Object{
   595  				clusterClassWithControlPlaneInfra,
   596  				machineDeploymentInfrastructure,
   597  				machineDeploymentBootstrap,
   598  				machineDeployment,
   599  				builder.MachineDeployment(metav1.NamespaceDefault, "duplicate-labels").
   600  					WithLabels(machineDeployment.Labels). // Another machine deployment with the same labels.
   601  					WithBootstrapTemplate(machineDeploymentBootstrap).
   602  					WithInfrastructureTemplate(machineDeploymentInfrastructure).
   603  					Build(),
   604  			},
   605  			// Expect error as two MachineDeployments with the same ClusterTopologyOwnedLabel should not exist for one cluster
   606  			wantErr: true,
   607  		},
   608  		{
   609  			name: "Fails if there are MachinePools without the topology.cluster.x-k8s.io/pool-name",
   610  			cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").
   611  				Build(),
   612  			blueprint: &scope.ClusterBlueprint{ClusterClass: clusterClassWithControlPlaneInfra},
   613  			objects: []client.Object{
   614  				clusterClassWithControlPlaneInfra,
   615  				builder.MachinePool(metav1.NamespaceDefault, "missing-topology-mp-labelName").
   616  					WithLabels(map[string]string{
   617  						clusterv1.ClusterNameLabel:          "cluster1",
   618  						clusterv1.ClusterTopologyOwnedLabel: "",
   619  						// topology.cluster.x-k8s.io/pool-name label is missing!
   620  					}).
   621  					WithBootstrap(machinePoolBootstrap).
   622  					WithInfrastructure(machinePoolInfrastructure).
   623  					Build(),
   624  			},
   625  			// Expect error to be thrown as no managed MachinePool is reconcilable unless it has a ClusterTopologyMachinePoolNameLabel.
   626  			wantErr: true,
   627  		},
   628  		{
   629  			name: "Fails if there are MachinePools with the same topology.cluster.x-k8s.io/pool-name",
   630  			cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").
   631  				WithTopology(builder.ClusterTopology().
   632  					WithMachinePool(clusterv1.MachinePoolTopology{
   633  						Class: "mpClass",
   634  						Name:  "mp1",
   635  					}).
   636  					Build()).
   637  				Build(),
   638  			blueprint: &scope.ClusterBlueprint{
   639  				ClusterClass: clusterClassWithControlPlaneInfra,
   640  				ControlPlane: &scope.ControlPlaneBlueprint{
   641  					Template:                      controlPlaneTemplateWithInfrastructureMachine,
   642  					InfrastructureMachineTemplate: controlPlaneInfrastructureMachineTemplate,
   643  				},
   644  			},
   645  			objects: []client.Object{
   646  				clusterClassWithControlPlaneInfra,
   647  				machinePoolInfrastructure,
   648  				machinePoolBootstrap,
   649  				machinePool,
   650  				builder.MachinePool(metav1.NamespaceDefault, "duplicate-labels").
   651  					WithLabels(machinePool.Labels). // Another machine pool with the same labels.
   652  					WithBootstrap(machinePoolBootstrap).
   653  					WithInfrastructure(machinePoolInfrastructure).
   654  					Build(),
   655  			},
   656  			// Expect error as two MachinePools with the same ClusterTopologyOwnedLabel should not exist for one cluster
   657  			wantErr: true,
   658  		},
   659  		{
   660  			name: "Should read a full Cluster (With InfrastructureCluster, ControlPlane and ControlPlane Infrastructure, MachineDeployments, MachinePools)",
   661  			cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").
   662  				WithInfrastructureCluster(infraCluster).
   663  				WithControlPlane(controlPlaneWithInfra).
   664  				WithTopology(builder.ClusterTopology().
   665  					WithMachineDeployment(clusterv1.MachineDeploymentTopology{
   666  						Class: "mdClass",
   667  						Name:  "md1",
   668  					}).
   669  					WithMachinePool(clusterv1.MachinePoolTopology{
   670  						Class: "mpClass",
   671  						Name:  "mp1",
   672  					}).
   673  					Build()).
   674  				Build(),
   675  			blueprint: &scope.ClusterBlueprint{
   676  				ClusterClass:                  clusterClassWithControlPlaneInfra,
   677  				InfrastructureClusterTemplate: infraClusterTemplate,
   678  				ControlPlane: &scope.ControlPlaneBlueprint{
   679  					Template:                      controlPlaneTemplateWithInfrastructureMachine,
   680  					InfrastructureMachineTemplate: controlPlaneInfrastructureMachineTemplate,
   681  				},
   682  				MachineDeployments: map[string]*scope.MachineDeploymentBlueprint{
   683  					"mdClass": {
   684  						BootstrapTemplate:             machineDeploymentBootstrap,
   685  						InfrastructureMachineTemplate: machineDeploymentInfrastructure,
   686  					},
   687  				},
   688  				MachinePools: map[string]*scope.MachinePoolBlueprint{
   689  					"mpClass": {
   690  						BootstrapTemplate:                 machinePoolBootstrapTemplate,
   691  						InfrastructureMachinePoolTemplate: machinePoolInfrastructureTemplate,
   692  					},
   693  				},
   694  			},
   695  			objects: []client.Object{
   696  				infraCluster,
   697  				clusterClassWithControlPlaneInfra,
   698  				controlPlaneInfrastructureMachineTemplate,
   699  				controlPlaneWithInfra,
   700  				machineDeploymentInfrastructure,
   701  				machineDeploymentBootstrap,
   702  				machineDeployment,
   703  				machinePoolInfrastructure,
   704  				machinePoolBootstrap,
   705  				machinePool,
   706  			},
   707  			// Expect valid return of full ClusterState with MachineDeployments and MachinePools properly filtered by label.
   708  			want: &scope.ClusterState{
   709  				Cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").
   710  					WithInfrastructureCluster(infraCluster).
   711  					WithControlPlane(controlPlaneWithInfra).
   712  					WithTopology(builder.ClusterTopology().
   713  						WithMachineDeployment(clusterv1.MachineDeploymentTopology{
   714  							Class: "mdClass",
   715  							Name:  "md1",
   716  						}).
   717  						WithMachinePool(clusterv1.MachinePoolTopology{
   718  							Class: "mpClass",
   719  							Name:  "mp1",
   720  						}).
   721  						Build()).
   722  					Build(),
   723  				ControlPlane:          &scope.ControlPlaneState{Object: controlPlaneWithInfra, InfrastructureMachineTemplate: controlPlaneInfrastructureMachineTemplate},
   724  				InfrastructureCluster: infraCluster,
   725  				MachineDeployments: map[string]*scope.MachineDeploymentState{
   726  					"md1": {
   727  						Object:                        machineDeployment,
   728  						BootstrapTemplate:             machineDeploymentBootstrap,
   729  						InfrastructureMachineTemplate: machineDeploymentInfrastructure,
   730  					},
   731  				},
   732  				MachinePools: map[string]*scope.MachinePoolState{
   733  					"mp1": {
   734  						Object:                          machinePool,
   735  						BootstrapObject:                 machinePoolBootstrap,
   736  						InfrastructureMachinePoolObject: machinePoolInfrastructure,
   737  					},
   738  				},
   739  			},
   740  		},
   741  		{
   742  			name: "Should read a full Cluster, even if a MachineDeployment & MachinePool topology has been deleted and the MachineDeployment & MachinePool still exists",
   743  			cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").
   744  				WithInfrastructureCluster(infraCluster).
   745  				WithControlPlane(controlPlaneWithInfra).
   746  				WithTopology(builder.ClusterTopology().
   747  					WithMachineDeployment(clusterv1.MachineDeploymentTopology{
   748  						Class: "mdClass",
   749  						Name:  "md1",
   750  					}).
   751  					WithMachinePool(clusterv1.MachinePoolTopology{
   752  						Class: "mpClass",
   753  						Name:  "mp1",
   754  					}).
   755  					Build()).
   756  				Build(),
   757  			blueprint: &scope.ClusterBlueprint{
   758  				ClusterClass:                  clusterClassWithControlPlaneInfra,
   759  				InfrastructureClusterTemplate: infraClusterTemplate,
   760  				ControlPlane: &scope.ControlPlaneBlueprint{
   761  					Template:                      controlPlaneTemplateWithInfrastructureMachine,
   762  					InfrastructureMachineTemplate: controlPlaneInfrastructureMachineTemplate,
   763  				},
   764  				MachineDeployments: map[string]*scope.MachineDeploymentBlueprint{
   765  					"mdClass": {
   766  						BootstrapTemplate:             machineDeploymentBootstrap,
   767  						InfrastructureMachineTemplate: machineDeploymentInfrastructure,
   768  					},
   769  				},
   770  				MachinePools: map[string]*scope.MachinePoolBlueprint{
   771  					"mpClass": {
   772  						BootstrapTemplate:                 machinePoolBootstrapTemplate,
   773  						InfrastructureMachinePoolTemplate: machinePoolInfrastructureTemplate,
   774  					},
   775  				},
   776  			},
   777  			objects: []client.Object{
   778  				infraCluster,
   779  				clusterClassWithControlPlaneInfra,
   780  				controlPlaneInfrastructureMachineTemplate,
   781  				controlPlaneWithInfra,
   782  				machineDeploymentInfrastructure,
   783  				machineDeploymentBootstrap,
   784  				machineDeployment,
   785  				machineDeployment2,
   786  				machinePoolInfrastructure,
   787  				machinePoolBootstrap,
   788  				machinePool,
   789  				machinePool2,
   790  			},
   791  			// Expect valid return of full ClusterState with MachineDeployments and MachinePools properly filtered by label.
   792  			want: &scope.ClusterState{
   793  				Cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").
   794  					WithInfrastructureCluster(infraCluster).
   795  					WithControlPlane(controlPlaneWithInfra).
   796  					WithTopology(builder.ClusterTopology().
   797  						WithMachineDeployment(clusterv1.MachineDeploymentTopology{
   798  							Class: "mdClass",
   799  							Name:  "md1",
   800  						}).
   801  						WithMachinePool(clusterv1.MachinePoolTopology{
   802  							Class: "mpClass",
   803  							Name:  "mp1",
   804  						}).
   805  						Build()).
   806  					Build(),
   807  				ControlPlane:          &scope.ControlPlaneState{Object: controlPlaneWithInfra, InfrastructureMachineTemplate: controlPlaneInfrastructureMachineTemplate},
   808  				InfrastructureCluster: infraCluster,
   809  				MachineDeployments: map[string]*scope.MachineDeploymentState{
   810  					"md1": {
   811  						Object:                        machineDeployment,
   812  						BootstrapTemplate:             machineDeploymentBootstrap,
   813  						InfrastructureMachineTemplate: machineDeploymentInfrastructure,
   814  					},
   815  					"md2": {
   816  						Object:                        machineDeployment2,
   817  						BootstrapTemplate:             machineDeploymentBootstrap,
   818  						InfrastructureMachineTemplate: machineDeploymentInfrastructure,
   819  					},
   820  				},
   821  				MachinePools: map[string]*scope.MachinePoolState{
   822  					"mp1": {
   823  						Object:                          machinePool,
   824  						BootstrapObject:                 machinePoolBootstrap,
   825  						InfrastructureMachinePoolObject: machinePoolInfrastructure,
   826  					},
   827  					"mp2": {
   828  						Object:                          machinePool2,
   829  						BootstrapObject:                 machinePoolBootstrap,
   830  						InfrastructureMachinePoolObject: machinePoolInfrastructure,
   831  					},
   832  				},
   833  			},
   834  		},
   835  		{
   836  			name: "Fails if a Cluster has a MachineDeployment without the Bootstrap Template ref",
   837  			cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").
   838  				WithTopology(builder.ClusterTopology().
   839  					WithMachineDeployment(clusterv1.MachineDeploymentTopology{
   840  						Class: "mdClass",
   841  						Name:  "md1",
   842  					}).
   843  					Build()).
   844  				Build(),
   845  			blueprint: &scope.ClusterBlueprint{
   846  				ClusterClass:                  clusterClassWithControlPlaneInfra,
   847  				InfrastructureClusterTemplate: infraClusterTemplate,
   848  				ControlPlane: &scope.ControlPlaneBlueprint{
   849  					Template:                      controlPlaneTemplateWithInfrastructureMachine,
   850  					InfrastructureMachineTemplate: controlPlaneInfrastructureMachineTemplate,
   851  				},
   852  				MachineDeployments: map[string]*scope.MachineDeploymentBlueprint{
   853  					"mdClass": {
   854  						BootstrapTemplate:             machineDeploymentBootstrap,
   855  						InfrastructureMachineTemplate: machineDeploymentInfrastructure,
   856  					},
   857  				},
   858  			},
   859  			objects: []client.Object{
   860  				infraCluster,
   861  				clusterClassWithControlPlaneInfra,
   862  				controlPlaneInfrastructureMachineTemplate,
   863  				controlPlaneWithInfra,
   864  				machineDeploymentInfrastructure,
   865  				builder.MachineDeployment(metav1.NamespaceDefault, "no-bootstrap").
   866  					WithLabels(machineDeployment.Labels).
   867  					// No BootstrapConfigTemplate reference!
   868  					WithInfrastructureTemplate(machineDeploymentInfrastructure).
   869  					Build(),
   870  			},
   871  			// Expect error as Bootstrap Template not defined for MachineDeployments relevant to the Cluster.
   872  			wantErr: true,
   873  		},
   874  		{
   875  			name: "Fails if a Cluster has a MachinePool without the Bootstrap Template ref",
   876  			cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").
   877  				WithTopology(builder.ClusterTopology().
   878  					WithMachinePool(clusterv1.MachinePoolTopology{
   879  						Class: "mpClass",
   880  						Name:  "mp1",
   881  					}).
   882  					Build()).
   883  				Build(),
   884  			blueprint: &scope.ClusterBlueprint{
   885  				ClusterClass:                  clusterClassWithControlPlaneInfra,
   886  				InfrastructureClusterTemplate: infraClusterTemplate,
   887  				ControlPlane: &scope.ControlPlaneBlueprint{
   888  					Template:                      controlPlaneTemplateWithInfrastructureMachine,
   889  					InfrastructureMachineTemplate: controlPlaneInfrastructureMachineTemplate,
   890  				},
   891  				MachinePools: map[string]*scope.MachinePoolBlueprint{
   892  					"mpClass": {
   893  						BootstrapTemplate:                 machinePoolBootstrapTemplate,
   894  						InfrastructureMachinePoolTemplate: machinePoolInfrastructureTemplate,
   895  					},
   896  				},
   897  			},
   898  			objects: []client.Object{
   899  				infraCluster,
   900  				clusterClassWithControlPlaneInfra,
   901  				controlPlaneInfrastructureMachineTemplate,
   902  				controlPlaneWithInfra,
   903  				machinePoolInfrastructure,
   904  				builder.MachinePool(metav1.NamespaceDefault, "no-bootstrap").
   905  					WithLabels(machinePool.Labels).
   906  					// No BootstrapConfig reference!
   907  					WithInfrastructure(machinePoolInfrastructure).
   908  					Build(),
   909  			},
   910  			// Expect error as BootstrapConfig not defined for MachinePools relevant to the Cluster.
   911  			wantErr: true,
   912  		},
   913  		{
   914  			name: "Fails if a Cluster has a MachineDeployments without the InfrastructureMachineTemplate ref",
   915  			cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").
   916  				WithTopology(builder.ClusterTopology().
   917  					WithMachineDeployment(clusterv1.MachineDeploymentTopology{
   918  						Class: "mdClass",
   919  						Name:  "md1",
   920  					}).
   921  					Build()).
   922  				Build(),
   923  			blueprint: &scope.ClusterBlueprint{
   924  				ClusterClass:                  clusterClassWithControlPlaneInfra,
   925  				InfrastructureClusterTemplate: infraClusterTemplate,
   926  				ControlPlane: &scope.ControlPlaneBlueprint{
   927  					Template:                      controlPlaneTemplateWithInfrastructureMachine,
   928  					InfrastructureMachineTemplate: controlPlaneInfrastructureMachineTemplate,
   929  				},
   930  				MachineDeployments: map[string]*scope.MachineDeploymentBlueprint{
   931  					"mdClass": {
   932  						BootstrapTemplate:             machineDeploymentBootstrap,
   933  						InfrastructureMachineTemplate: machineDeploymentInfrastructure,
   934  					},
   935  				},
   936  			},
   937  			objects: []client.Object{
   938  				infraCluster,
   939  				clusterClassWithControlPlaneInfra,
   940  				controlPlaneInfrastructureMachineTemplate,
   941  				controlPlaneWithInfra,
   942  				machineDeploymentBootstrap,
   943  				builder.MachineDeployment(metav1.NamespaceDefault, "no-infra").
   944  					WithLabels(machineDeployment.Labels).
   945  					WithBootstrapTemplate(machineDeploymentBootstrap).
   946  					// No InfrastructureMachineTemplate reference!
   947  					Build(),
   948  			},
   949  			// Expect error as Infrastructure Template not defined for MachineDeployment relevant to the Cluster.
   950  			wantErr: true,
   951  		},
   952  		{
   953  			name: "Fails if a Cluster has a MachinePools without the InfrastructureMachinePool ref",
   954  			cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").
   955  				WithTopology(builder.ClusterTopology().
   956  					WithMachinePool(clusterv1.MachinePoolTopology{
   957  						Class: "mpClass",
   958  						Name:  "mp1",
   959  					}).
   960  					Build()).
   961  				Build(),
   962  			blueprint: &scope.ClusterBlueprint{
   963  				ClusterClass:                  clusterClassWithControlPlaneInfra,
   964  				InfrastructureClusterTemplate: infraClusterTemplate,
   965  				ControlPlane: &scope.ControlPlaneBlueprint{
   966  					Template:                      controlPlaneTemplateWithInfrastructureMachine,
   967  					InfrastructureMachineTemplate: controlPlaneInfrastructureMachineTemplate,
   968  				},
   969  				MachinePools: map[string]*scope.MachinePoolBlueprint{
   970  					"mpClass": {
   971  						BootstrapTemplate:                 machinePoolBootstrapTemplate,
   972  						InfrastructureMachinePoolTemplate: machinePoolInfrastructureTemplate,
   973  					},
   974  				},
   975  			},
   976  			objects: []client.Object{
   977  				infraCluster,
   978  				clusterClassWithControlPlaneInfra,
   979  				controlPlaneInfrastructureMachineTemplate,
   980  				controlPlaneWithInfra,
   981  				machinePoolBootstrap,
   982  				builder.MachinePool(metav1.NamespaceDefault, "no-infra").
   983  					WithLabels(machinePool.Labels).
   984  					WithBootstrap(machinePoolBootstrap).
   985  					// No InfrastructureMachinePool reference!
   986  					Build(),
   987  			},
   988  			// Expect error as InfrastructureMachinePool not defined for MachinePool relevant to the Cluster.
   989  			wantErr: true,
   990  		},
   991  		{
   992  			name: "Pass reading a full Cluster with MachineHealthChecks for ControlPlane and MachineDeployment)",
   993  			cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").
   994  				WithInfrastructureCluster(infraCluster).
   995  				WithControlPlane(controlPlaneWithInfra).
   996  				WithTopology(builder.ClusterTopology().
   997  					WithMachineDeployment(clusterv1.MachineDeploymentTopology{
   998  						Class: "mdClass",
   999  						Name:  "md1",
  1000  					}).
  1001  					Build()).
  1002  				Build(),
  1003  			blueprint: &scope.ClusterBlueprint{
  1004  				ClusterClass:                  clusterClassWithControlPlaneInfra,
  1005  				InfrastructureClusterTemplate: infraClusterTemplate,
  1006  				ControlPlane: &scope.ControlPlaneBlueprint{
  1007  					Template:                      controlPlaneTemplateWithInfrastructureMachine,
  1008  					InfrastructureMachineTemplate: controlPlaneInfrastructureMachineTemplate,
  1009  				},
  1010  				MachineDeployments: map[string]*scope.MachineDeploymentBlueprint{
  1011  					"mdClass": {
  1012  						BootstrapTemplate:             machineDeploymentBootstrap,
  1013  						InfrastructureMachineTemplate: machineDeploymentInfrastructure,
  1014  					},
  1015  				},
  1016  			},
  1017  			objects: []client.Object{
  1018  				infraCluster,
  1019  				clusterClassWithControlPlaneInfra,
  1020  				controlPlaneInfrastructureMachineTemplate,
  1021  				controlPlaneWithInfra,
  1022  				machineDeploymentInfrastructure,
  1023  				machineDeploymentBootstrap,
  1024  				machineDeployment,
  1025  				machineHealthCheckForMachineDeployment,
  1026  				machineHealthCheckForControlPlane,
  1027  			},
  1028  			// Expect valid return of full ClusterState with MachineHealthChecks for both ControlPlane and MachineDeployment.
  1029  			want: &scope.ClusterState{
  1030  				Cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").
  1031  					WithInfrastructureCluster(infraCluster).
  1032  					WithControlPlane(controlPlaneWithInfra).
  1033  					WithTopology(builder.ClusterTopology().
  1034  						WithMachineDeployment(clusterv1.MachineDeploymentTopology{
  1035  							Class: "mdClass",
  1036  							Name:  "md1",
  1037  						}).
  1038  						Build()).
  1039  					Build(),
  1040  				ControlPlane: &scope.ControlPlaneState{
  1041  					Object:                        controlPlaneWithInfra,
  1042  					InfrastructureMachineTemplate: controlPlaneInfrastructureMachineTemplate,
  1043  					MachineHealthCheck:            machineHealthCheckForControlPlane,
  1044  				},
  1045  				InfrastructureCluster: infraCluster,
  1046  				MachineDeployments: map[string]*scope.MachineDeploymentState{
  1047  					"md1": {
  1048  						Object:                        machineDeployment,
  1049  						BootstrapTemplate:             machineDeploymentBootstrap,
  1050  						InfrastructureMachineTemplate: machineDeploymentInfrastructure,
  1051  						MachineHealthCheck:            machineHealthCheckForMachineDeployment,
  1052  					},
  1053  				},
  1054  				MachinePools: emptyMachinePools,
  1055  			},
  1056  		},
  1057  	}
  1058  	for _, tt := range tests {
  1059  		t.Run(tt.name, func(t *testing.T) {
  1060  			g := NewWithT(t)
  1061  
  1062  			// Sets up a scope with a Blueprint.
  1063  			s := scope.New(tt.cluster)
  1064  			s.Blueprint = tt.blueprint
  1065  
  1066  			// Sets up the fakeClient for the test case.
  1067  			objs := []client.Object{}
  1068  			objs = append(objs, crds...)
  1069  			objs = append(objs, tt.objects...)
  1070  			if tt.cluster != nil {
  1071  				objs = append(objs, tt.cluster)
  1072  			}
  1073  			fakeClient := fake.NewClientBuilder().
  1074  				WithScheme(fakeScheme).
  1075  				WithObjects(objs...).
  1076  				Build()
  1077  
  1078  			// Calls getCurrentState.
  1079  			r := &Reconciler{
  1080  				Client:                    fakeClient,
  1081  				APIReader:                 fakeClient,
  1082  				UnstructuredCachingClient: fakeClient,
  1083  				patchHelperFactory:        dryRunPatchHelperFactory(fakeClient),
  1084  			}
  1085  			got, err := r.getCurrentState(ctx, s)
  1086  
  1087  			// Checks the return error.
  1088  			if tt.wantErr {
  1089  				g.Expect(err).To(HaveOccurred())
  1090  			} else {
  1091  				g.Expect(err).ToNot(HaveOccurred())
  1092  			}
  1093  			if tt.want == nil {
  1094  				g.Expect(got).To(BeNil())
  1095  				return
  1096  			}
  1097  
  1098  			// Use EqualObject where the compared object is passed through the fakeClient. Elsewhere the Equal method is
  1099  			// good enough to establish equality.
  1100  			g.Expect(got.Cluster).To(EqualObject(tt.want.Cluster, IgnoreAutogeneratedMetadata))
  1101  			g.Expect(got.InfrastructureCluster).To(EqualObject(tt.want.InfrastructureCluster))
  1102  			g.Expect(got.ControlPlane).To(BeComparableTo(tt.want.ControlPlane), cmp.Diff(got.ControlPlane, tt.want.ControlPlane))
  1103  			g.Expect(got.MachineDeployments).To(BeComparableTo(tt.want.MachineDeployments), cmp.Diff(got.MachineDeployments, tt.want.MachineDeployments))
  1104  			g.Expect(got.MachinePools).To(BeComparableTo(tt.want.MachinePools), cmp.Diff(got.MachinePools, tt.want.MachinePools))
  1105  		})
  1106  	}
  1107  }
  1108  
  1109  func TestAlignRefAPIVersion(t *testing.T) {
  1110  	tests := []struct {
  1111  		name                     string
  1112  		templateFromClusterClass *unstructured.Unstructured
  1113  		currentRef               *corev1.ObjectReference
  1114  		want                     *corev1.ObjectReference
  1115  		wantErr                  bool
  1116  	}{
  1117  		{
  1118  			name: "Error for invalid apiVersion",
  1119  			templateFromClusterClass: &unstructured.Unstructured{Object: map[string]interface{}{
  1120  				"apiVersion": "infrastructure.cluster.x-k8s.io/v1beta1",
  1121  				"kind":       "DockerCluster",
  1122  			}},
  1123  			currentRef: &corev1.ObjectReference{
  1124  				APIVersion: "invalid/api/version",
  1125  				Kind:       "DockerCluster",
  1126  				Name:       "my-cluster-abc",
  1127  				Namespace:  metav1.NamespaceDefault,
  1128  			},
  1129  			wantErr: true,
  1130  		},
  1131  		{
  1132  			name: "Use apiVersion from ClusterClass: group and kind is the same",
  1133  			templateFromClusterClass: &unstructured.Unstructured{Object: map[string]interface{}{
  1134  				"apiVersion": "infrastructure.cluster.x-k8s.io/v1beta1",
  1135  				"kind":       "DockerCluster",
  1136  			}},
  1137  			currentRef: &corev1.ObjectReference{
  1138  				APIVersion: "infrastructure.cluster.x-k8s.io/v1beta2",
  1139  				Kind:       "DockerCluster",
  1140  				Name:       "my-cluster-abc",
  1141  				Namespace:  metav1.NamespaceDefault,
  1142  			},
  1143  			want: &corev1.ObjectReference{
  1144  				// Group & kind is the same => apiVersion is taken from ClusterClass.
  1145  				APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1",
  1146  				Kind:       "DockerCluster",
  1147  				Name:       "my-cluster-abc",
  1148  				Namespace:  metav1.NamespaceDefault,
  1149  			},
  1150  		},
  1151  		{
  1152  			name: "Use apiVersion from currentRef: kind is different",
  1153  			templateFromClusterClass: &unstructured.Unstructured{Object: map[string]interface{}{
  1154  				"apiVersion": "bootstrap.cluster.x-k8s.io/v1alpha4",
  1155  				"kind":       "DifferentConfigTemplate",
  1156  			}},
  1157  			currentRef: &corev1.ObjectReference{
  1158  				APIVersion: "bootstrap.cluster.x-k8s.io/v1beta1",
  1159  				Kind:       "KubeadmConfigTemplate",
  1160  				Name:       "my-cluster-abc",
  1161  				Namespace:  metav1.NamespaceDefault,
  1162  			},
  1163  			want: &corev1.ObjectReference{
  1164  				// kind is different => apiVersion is taken from currentRef.
  1165  				APIVersion: "bootstrap.cluster.x-k8s.io/v1beta1",
  1166  				Kind:       "KubeadmConfigTemplate",
  1167  				Name:       "my-cluster-abc",
  1168  				Namespace:  metav1.NamespaceDefault,
  1169  			},
  1170  		},
  1171  		{
  1172  			name: "Use apiVersion from currentRef: group is different",
  1173  			templateFromClusterClass: &unstructured.Unstructured{Object: map[string]interface{}{
  1174  				"apiVersion": "bootstrap2.cluster.x-k8s.io/v1beta1",
  1175  				"kind":       "KubeadmConfigTemplate",
  1176  			}},
  1177  			currentRef: &corev1.ObjectReference{
  1178  				APIVersion: "bootstrap.cluster.x-k8s.io/v1beta1",
  1179  				Kind:       "KubeadmConfigTemplate",
  1180  				Name:       "my-cluster-abc",
  1181  				Namespace:  metav1.NamespaceDefault,
  1182  			},
  1183  			want: &corev1.ObjectReference{
  1184  				// group is different => apiVersion is taken from currentRef.
  1185  				APIVersion: "bootstrap.cluster.x-k8s.io/v1beta1",
  1186  				Kind:       "KubeadmConfigTemplate",
  1187  				Name:       "my-cluster-abc",
  1188  				Namespace:  metav1.NamespaceDefault,
  1189  			},
  1190  		},
  1191  	}
  1192  	for _, tt := range tests {
  1193  		t.Run(tt.name, func(t *testing.T) {
  1194  			g := NewWithT(t)
  1195  
  1196  			got, err := alignRefAPIVersion(tt.templateFromClusterClass, tt.currentRef)
  1197  			if tt.wantErr {
  1198  				g.Expect(err).To(HaveOccurred())
  1199  				return
  1200  			}
  1201  			g.Expect(err).ToNot(HaveOccurred())
  1202  
  1203  			g.Expect(got).To(BeComparableTo(tt.want))
  1204  		})
  1205  	}
  1206  }