sigs.k8s.io/cluster-api@v1.7.1/exp/internal/controllers/machinepool_controller_test.go (about)

     1  /*
     2  Copyright 2019 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 controllers
    18  
    19  import (
    20  	"testing"
    21  
    22  	. "github.com/onsi/gomega"
    23  	corev1 "k8s.io/api/core/v1"
    24  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    25  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    26  	"k8s.io/client-go/kubernetes/scheme"
    27  	"k8s.io/utils/ptr"
    28  	ctrl "sigs.k8s.io/controller-runtime"
    29  	"sigs.k8s.io/controller-runtime/pkg/client"
    30  	"sigs.k8s.io/controller-runtime/pkg/client/fake"
    31  	"sigs.k8s.io/controller-runtime/pkg/reconcile"
    32  
    33  	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
    34  	expv1 "sigs.k8s.io/cluster-api/exp/api/v1beta1"
    35  	"sigs.k8s.io/cluster-api/internal/test/builder"
    36  	"sigs.k8s.io/cluster-api/util"
    37  	"sigs.k8s.io/cluster-api/util/conditions"
    38  )
    39  
    40  func TestMachinePoolFinalizer(t *testing.T) {
    41  	bootstrapData := "some valid machinepool bootstrap data"
    42  	clusterCorrectMeta := &clusterv1.Cluster{
    43  		ObjectMeta: metav1.ObjectMeta{
    44  			Namespace: metav1.NamespaceDefault,
    45  			Name:      "valid-cluster",
    46  		},
    47  	}
    48  
    49  	machinePoolValidCluster := &expv1.MachinePool{
    50  		ObjectMeta: metav1.ObjectMeta{
    51  			Name:      "machinePool1",
    52  			Namespace: metav1.NamespaceDefault,
    53  		},
    54  		Spec: expv1.MachinePoolSpec{
    55  			Replicas: ptr.To[int32](1),
    56  			Template: clusterv1.MachineTemplateSpec{
    57  				Spec: clusterv1.MachineSpec{
    58  					Bootstrap: clusterv1.Bootstrap{
    59  						DataSecretName: &bootstrapData,
    60  					},
    61  				},
    62  			},
    63  			ClusterName: "valid-cluster",
    64  		},
    65  	}
    66  
    67  	machinePoolWithFinalizer := &expv1.MachinePool{
    68  		ObjectMeta: metav1.ObjectMeta{
    69  			Name:       "machinePool2",
    70  			Namespace:  metav1.NamespaceDefault,
    71  			Finalizers: []string{"some-other-finalizer"},
    72  		},
    73  		Spec: expv1.MachinePoolSpec{
    74  			Replicas: ptr.To[int32](1),
    75  			Template: clusterv1.MachineTemplateSpec{
    76  				Spec: clusterv1.MachineSpec{
    77  					Bootstrap: clusterv1.Bootstrap{
    78  						DataSecretName: &bootstrapData,
    79  					},
    80  				},
    81  			},
    82  			ClusterName: "valid-cluster",
    83  		},
    84  	}
    85  
    86  	testCases := []struct {
    87  		name               string
    88  		request            reconcile.Request
    89  		m                  *expv1.MachinePool
    90  		expectedFinalizers []string
    91  	}{
    92  		{
    93  			name: "should add a machinePool finalizer to the machinePool if it doesn't have one",
    94  			request: reconcile.Request{
    95  				NamespacedName: util.ObjectKey(machinePoolValidCluster),
    96  			},
    97  			m:                  machinePoolValidCluster,
    98  			expectedFinalizers: []string{expv1.MachinePoolFinalizer},
    99  		},
   100  		{
   101  			name: "should append the machinePool finalizer to the machinePool if it already has a finalizer",
   102  			request: reconcile.Request{
   103  				NamespacedName: util.ObjectKey(machinePoolWithFinalizer),
   104  			},
   105  			m:                  machinePoolWithFinalizer,
   106  			expectedFinalizers: []string{"some-other-finalizer", expv1.MachinePoolFinalizer},
   107  		},
   108  	}
   109  
   110  	for _, tc := range testCases {
   111  		t.Run(tc.name, func(t *testing.T) {
   112  			g := NewWithT(t)
   113  
   114  			mr := &MachinePoolReconciler{
   115  				Client: fake.NewClientBuilder().WithObjects(
   116  					clusterCorrectMeta,
   117  					machinePoolValidCluster,
   118  					machinePoolWithFinalizer,
   119  				).Build(),
   120  			}
   121  
   122  			_, _ = mr.Reconcile(ctx, tc.request)
   123  
   124  			key := client.ObjectKey{Namespace: tc.m.Namespace, Name: tc.m.Name}
   125  			var actual expv1.MachinePool
   126  			if len(tc.expectedFinalizers) > 0 {
   127  				g.Expect(mr.Client.Get(ctx, key, &actual)).To(Succeed())
   128  				g.Expect(actual.Finalizers).ToNot(BeEmpty())
   129  				g.Expect(actual.Finalizers).To(Equal(tc.expectedFinalizers))
   130  			} else {
   131  				g.Expect(actual.Finalizers).To(BeEmpty())
   132  			}
   133  		})
   134  	}
   135  }
   136  
   137  func TestMachinePoolOwnerReference(t *testing.T) {
   138  	bootstrapData := "some valid machinepool bootstrap data"
   139  	testCluster := &clusterv1.Cluster{
   140  		TypeMeta:   metav1.TypeMeta{Kind: "Cluster", APIVersion: clusterv1.GroupVersion.String()},
   141  		ObjectMeta: metav1.ObjectMeta{Namespace: metav1.NamespaceDefault, Name: "test-cluster"},
   142  	}
   143  
   144  	machinePoolInvalidCluster := &expv1.MachinePool{
   145  		ObjectMeta: metav1.ObjectMeta{
   146  			Name:      "machinePool1",
   147  			Namespace: metav1.NamespaceDefault,
   148  		},
   149  		Spec: expv1.MachinePoolSpec{
   150  			Replicas:    ptr.To[int32](1),
   151  			ClusterName: "invalid",
   152  		},
   153  	}
   154  
   155  	machinePoolValidCluster := &expv1.MachinePool{
   156  		ObjectMeta: metav1.ObjectMeta{
   157  			Name:      "machinePool2",
   158  			Namespace: metav1.NamespaceDefault,
   159  		},
   160  		Spec: expv1.MachinePoolSpec{
   161  			Replicas: ptr.To[int32](1),
   162  			Template: clusterv1.MachineTemplateSpec{
   163  				Spec: clusterv1.MachineSpec{
   164  					Bootstrap: clusterv1.Bootstrap{
   165  						DataSecretName: &bootstrapData,
   166  					},
   167  				},
   168  			},
   169  			ClusterName: "test-cluster",
   170  		},
   171  	}
   172  
   173  	machinePoolValidMachinePool := &expv1.MachinePool{
   174  		ObjectMeta: metav1.ObjectMeta{
   175  			Name:      "machinePool3",
   176  			Namespace: metav1.NamespaceDefault,
   177  			Labels: map[string]string{
   178  				clusterv1.ClusterNameLabel: "valid-cluster",
   179  			},
   180  		},
   181  		Spec: expv1.MachinePoolSpec{
   182  			Replicas: ptr.To[int32](1),
   183  			Template: clusterv1.MachineTemplateSpec{
   184  				Spec: clusterv1.MachineSpec{
   185  					Bootstrap: clusterv1.Bootstrap{
   186  						DataSecretName: &bootstrapData,
   187  					},
   188  				},
   189  			},
   190  			ClusterName: "test-cluster",
   191  		},
   192  	}
   193  
   194  	testCases := []struct {
   195  		name       string
   196  		request    reconcile.Request
   197  		m          *expv1.MachinePool
   198  		expectedOR []metav1.OwnerReference
   199  	}{
   200  		{
   201  			name: "should add owner reference to machinePool referencing a cluster with correct type meta",
   202  			request: reconcile.Request{
   203  				NamespacedName: util.ObjectKey(machinePoolValidCluster),
   204  			},
   205  			m: machinePoolValidCluster,
   206  			expectedOR: []metav1.OwnerReference{
   207  				{
   208  					APIVersion: testCluster.APIVersion,
   209  					Kind:       testCluster.Kind,
   210  					Name:       testCluster.Name,
   211  					UID:        testCluster.UID,
   212  				},
   213  			},
   214  		},
   215  	}
   216  
   217  	for _, tc := range testCases {
   218  		t.Run(tc.name, func(t *testing.T) {
   219  			g := NewWithT(t)
   220  
   221  			fakeClient := fake.NewClientBuilder().WithObjects(
   222  				testCluster,
   223  				machinePoolInvalidCluster,
   224  				machinePoolValidCluster,
   225  				machinePoolValidMachinePool,
   226  			).WithStatusSubresource(&expv1.MachinePool{}).Build()
   227  			mr := &MachinePoolReconciler{
   228  				Client:    fakeClient,
   229  				APIReader: fakeClient,
   230  			}
   231  
   232  			key := client.ObjectKey{Namespace: tc.m.Namespace, Name: tc.m.Name}
   233  			var actual expv1.MachinePool
   234  
   235  			// this first requeue is to add finalizer
   236  			result, err := mr.Reconcile(ctx, tc.request)
   237  			g.Expect(err).ToNot(HaveOccurred())
   238  			g.Expect(result).To(BeComparableTo(ctrl.Result{}))
   239  			g.Expect(mr.Client.Get(ctx, key, &actual)).To(Succeed())
   240  			g.Expect(actual.Finalizers).To(ContainElement(expv1.MachinePoolFinalizer))
   241  
   242  			_, _ = mr.Reconcile(ctx, tc.request)
   243  
   244  			if len(tc.expectedOR) > 0 {
   245  				g.Expect(mr.Client.Get(ctx, key, &actual)).To(Succeed())
   246  				g.Expect(actual.OwnerReferences).To(BeComparableTo(tc.expectedOR))
   247  			} else {
   248  				g.Expect(actual.OwnerReferences).To(BeEmpty())
   249  			}
   250  		})
   251  	}
   252  }
   253  
   254  func TestReconcileMachinePoolRequest(t *testing.T) {
   255  	infraConfig := unstructured.Unstructured{
   256  		Object: map[string]interface{}{
   257  			"kind":       builder.TestInfrastructureMachineTemplateKind,
   258  			"apiVersion": builder.InfrastructureGroupVersion.String(),
   259  			"metadata": map[string]interface{}{
   260  				"name":      "infra-config1",
   261  				"namespace": metav1.NamespaceDefault,
   262  			},
   263  			"spec": map[string]interface{}{
   264  				"providerIDList": []interface{}{
   265  					"test://id-1",
   266  				},
   267  			},
   268  			"status": map[string]interface{}{
   269  				"ready": true,
   270  				"addresses": []interface{}{
   271  					map[string]interface{}{
   272  						"type":    "InternalIP",
   273  						"address": "10.0.0.1",
   274  					},
   275  				},
   276  			},
   277  		},
   278  	}
   279  
   280  	time := metav1.Now()
   281  
   282  	testCluster := clusterv1.Cluster{
   283  		TypeMeta:   metav1.TypeMeta{Kind: "Cluster", APIVersion: clusterv1.GroupVersion.String()},
   284  		ObjectMeta: metav1.ObjectMeta{Namespace: metav1.NamespaceDefault, Name: "test-cluster"},
   285  	}
   286  
   287  	bootstrapConfig := &unstructured.Unstructured{
   288  		Object: map[string]interface{}{
   289  			"kind":       builder.TestBootstrapConfigKind,
   290  			"apiVersion": builder.BootstrapGroupVersion.String(),
   291  			"metadata": map[string]interface{}{
   292  				"name":      "test-bootstrap",
   293  				"namespace": metav1.NamespaceDefault,
   294  			},
   295  		},
   296  	}
   297  
   298  	type expected struct {
   299  		result reconcile.Result
   300  		err    bool
   301  	}
   302  	testCases := []struct {
   303  		machinePool expv1.MachinePool
   304  		expected    expected
   305  	}{
   306  		{
   307  			machinePool: expv1.MachinePool{
   308  				ObjectMeta: metav1.ObjectMeta{
   309  					Name:       "created",
   310  					Namespace:  metav1.NamespaceDefault,
   311  					Finalizers: []string{expv1.MachinePoolFinalizer},
   312  				},
   313  				Spec: expv1.MachinePoolSpec{
   314  					ClusterName:    "test-cluster",
   315  					ProviderIDList: []string{"test://id-1"},
   316  					Replicas:       ptr.To[int32](1),
   317  					Template: clusterv1.MachineTemplateSpec{
   318  						Spec: clusterv1.MachineSpec{
   319  
   320  							InfrastructureRef: corev1.ObjectReference{
   321  								APIVersion: builder.InfrastructureGroupVersion.String(),
   322  								Kind:       builder.TestInfrastructureMachineTemplateKind,
   323  								Name:       "infra-config1",
   324  							},
   325  							Bootstrap: clusterv1.Bootstrap{DataSecretName: ptr.To("data")},
   326  						},
   327  					},
   328  				},
   329  				Status: expv1.MachinePoolStatus{
   330  					Replicas:      1,
   331  					ReadyReplicas: 1,
   332  					NodeRefs: []corev1.ObjectReference{
   333  						{Name: "test"},
   334  					},
   335  					ObservedGeneration: 1,
   336  				},
   337  			},
   338  			expected: expected{
   339  				result: reconcile.Result{},
   340  				err:    false,
   341  			},
   342  		},
   343  		{
   344  			machinePool: expv1.MachinePool{
   345  				ObjectMeta: metav1.ObjectMeta{
   346  					Name:       "updated",
   347  					Namespace:  metav1.NamespaceDefault,
   348  					Finalizers: []string{expv1.MachinePoolFinalizer},
   349  				},
   350  				Spec: expv1.MachinePoolSpec{
   351  					ClusterName:    "test-cluster",
   352  					ProviderIDList: []string{"test://id-1"},
   353  					Replicas:       ptr.To[int32](1),
   354  					Template: clusterv1.MachineTemplateSpec{
   355  						Spec: clusterv1.MachineSpec{
   356  							InfrastructureRef: corev1.ObjectReference{
   357  								APIVersion: builder.InfrastructureGroupVersion.String(),
   358  								Kind:       builder.TestInfrastructureMachineTemplateKind,
   359  								Name:       "infra-config1",
   360  							},
   361  							Bootstrap: clusterv1.Bootstrap{DataSecretName: ptr.To("data")},
   362  						},
   363  					},
   364  				},
   365  				Status: expv1.MachinePoolStatus{
   366  					Replicas:      1,
   367  					ReadyReplicas: 1,
   368  					NodeRefs: []corev1.ObjectReference{
   369  						{Name: "test"},
   370  					},
   371  					ObservedGeneration: 1,
   372  				},
   373  			},
   374  			expected: expected{
   375  				result: reconcile.Result{},
   376  				err:    false,
   377  			},
   378  		},
   379  		{
   380  			machinePool: expv1.MachinePool{
   381  				ObjectMeta: metav1.ObjectMeta{
   382  					Name:      "deleted",
   383  					Namespace: metav1.NamespaceDefault,
   384  					Labels: map[string]string{
   385  						clusterv1.MachineControlPlaneLabel: "",
   386  					},
   387  					Finalizers:        []string{expv1.MachinePoolFinalizer},
   388  					DeletionTimestamp: &time,
   389  				},
   390  				Spec: expv1.MachinePoolSpec{
   391  					ClusterName: "test-cluster",
   392  					Replicas:    ptr.To[int32](1),
   393  					Template: clusterv1.MachineTemplateSpec{
   394  						Spec: clusterv1.MachineSpec{
   395  							InfrastructureRef: corev1.ObjectReference{
   396  								APIVersion: builder.InfrastructureGroupVersion.String(),
   397  								Kind:       builder.TestInfrastructureMachineTemplateKind,
   398  								Name:       "infra-config1",
   399  							},
   400  							Bootstrap: clusterv1.Bootstrap{DataSecretName: ptr.To("data")},
   401  						},
   402  					},
   403  				},
   404  			},
   405  			expected: expected{
   406  				result: reconcile.Result{},
   407  				err:    false,
   408  			},
   409  		},
   410  	}
   411  
   412  	for i := range testCases {
   413  		tc := testCases[i]
   414  		t.Run("machinePool should be "+tc.machinePool.Name, func(t *testing.T) {
   415  			g := NewWithT(t)
   416  
   417  			clientFake := fake.NewClientBuilder().WithObjects(
   418  				&testCluster,
   419  				&tc.machinePool,
   420  				&infraConfig,
   421  				bootstrapConfig,
   422  				builder.TestBootstrapConfigCRD,
   423  				builder.TestInfrastructureMachineTemplateCRD,
   424  			).WithStatusSubresource(&expv1.MachinePool{}).Build()
   425  
   426  			r := &MachinePoolReconciler{
   427  				Client:    clientFake,
   428  				APIReader: clientFake,
   429  			}
   430  
   431  			result, err := r.Reconcile(ctx, reconcile.Request{NamespacedName: util.ObjectKey(&tc.machinePool)})
   432  			if tc.expected.err {
   433  				g.Expect(err).To(HaveOccurred())
   434  			} else {
   435  				g.Expect(err).ToNot(HaveOccurred())
   436  			}
   437  
   438  			g.Expect(result).To(BeComparableTo(tc.expected.result))
   439  		})
   440  	}
   441  }
   442  
   443  func TestReconcileMachinePoolDeleteExternal(t *testing.T) {
   444  	testCluster := &clusterv1.Cluster{
   445  		ObjectMeta: metav1.ObjectMeta{Namespace: metav1.NamespaceDefault, Name: "test-cluster"},
   446  	}
   447  
   448  	bootstrapConfig := &unstructured.Unstructured{
   449  		Object: map[string]interface{}{
   450  			"kind":       builder.TestBootstrapConfigKind,
   451  			"apiVersion": builder.BootstrapGroupVersion.String(),
   452  			"metadata": map[string]interface{}{
   453  				"name":      "delete-bootstrap",
   454  				"namespace": metav1.NamespaceDefault,
   455  			},
   456  		},
   457  	}
   458  
   459  	infraConfig := &unstructured.Unstructured{
   460  		Object: map[string]interface{}{
   461  			"kind":       builder.TestInfrastructureMachineTemplateKind,
   462  			"apiVersion": builder.InfrastructureGroupVersion.String(),
   463  			"metadata": map[string]interface{}{
   464  				"name":      "delete-infra",
   465  				"namespace": metav1.NamespaceDefault,
   466  			},
   467  		},
   468  	}
   469  
   470  	machinePool := &expv1.MachinePool{
   471  		ObjectMeta: metav1.ObjectMeta{
   472  			Name:      "delete",
   473  			Namespace: metav1.NamespaceDefault,
   474  		},
   475  		Spec: expv1.MachinePoolSpec{
   476  			ClusterName: "test-cluster",
   477  			Replicas:    ptr.To[int32](1),
   478  			Template: clusterv1.MachineTemplateSpec{
   479  				Spec: clusterv1.MachineSpec{
   480  					InfrastructureRef: corev1.ObjectReference{
   481  						APIVersion: builder.InfrastructureGroupVersion.String(),
   482  						Kind:       builder.TestInfrastructureMachineTemplateKind,
   483  						Name:       "delete-infra",
   484  					},
   485  					Bootstrap: clusterv1.Bootstrap{
   486  						ConfigRef: &corev1.ObjectReference{
   487  							APIVersion: builder.BootstrapGroupVersion.String(),
   488  							Kind:       builder.TestBootstrapConfigKind,
   489  							Name:       "delete-bootstrap",
   490  						},
   491  					},
   492  				},
   493  			},
   494  		},
   495  	}
   496  
   497  	testCases := []struct {
   498  		name            string
   499  		bootstrapExists bool
   500  		infraExists     bool
   501  		expected        bool
   502  		expectError     bool
   503  	}{
   504  		{
   505  			name:            "should continue to reconcile delete of external refs since both refs exists",
   506  			bootstrapExists: true,
   507  			infraExists:     true,
   508  			expected:        false,
   509  			expectError:     false,
   510  		},
   511  		{
   512  			name:            "should continue to reconcile delete of external refs since infra ref exist",
   513  			bootstrapExists: false,
   514  			infraExists:     true,
   515  			expected:        false,
   516  			expectError:     false,
   517  		},
   518  		{
   519  			name:            "should continue to reconcile delete of external refs since bootstrap ref exist",
   520  			bootstrapExists: true,
   521  			infraExists:     false,
   522  			expected:        false,
   523  			expectError:     false,
   524  		},
   525  		{
   526  			name:            "should no longer reconcile deletion of external refs since both don't exist",
   527  			bootstrapExists: false,
   528  			infraExists:     false,
   529  			expected:        true,
   530  			expectError:     false,
   531  		},
   532  	}
   533  
   534  	for _, tc := range testCases {
   535  		t.Run(tc.name, func(t *testing.T) {
   536  			g := NewWithT(t)
   537  			objs := []client.Object{testCluster, machinePool}
   538  
   539  			if tc.bootstrapExists {
   540  				objs = append(objs, bootstrapConfig)
   541  			}
   542  
   543  			if tc.infraExists {
   544  				objs = append(objs, infraConfig)
   545  			}
   546  
   547  			r := &MachinePoolReconciler{
   548  				Client: fake.NewClientBuilder().WithObjects(objs...).Build(),
   549  			}
   550  
   551  			ok, err := r.reconcileDeleteExternal(ctx, machinePool)
   552  			g.Expect(ok).To(Equal(tc.expected))
   553  			if tc.expectError {
   554  				g.Expect(err).To(HaveOccurred())
   555  			} else {
   556  				g.Expect(err).ToNot(HaveOccurred())
   557  			}
   558  		})
   559  	}
   560  }
   561  
   562  func TestRemoveMachinePoolFinalizerAfterDeleteReconcile(t *testing.T) {
   563  	g := NewWithT(t)
   564  
   565  	dt := metav1.Now()
   566  
   567  	testCluster := &clusterv1.Cluster{
   568  		ObjectMeta: metav1.ObjectMeta{Namespace: metav1.NamespaceDefault, Name: "test-cluster"},
   569  	}
   570  
   571  	m := &expv1.MachinePool{
   572  		ObjectMeta: metav1.ObjectMeta{
   573  			Name:              "delete123",
   574  			Namespace:         metav1.NamespaceDefault,
   575  			Finalizers:        []string{expv1.MachinePoolFinalizer, "test"},
   576  			DeletionTimestamp: &dt,
   577  		},
   578  		Spec: expv1.MachinePoolSpec{
   579  			ClusterName: "test-cluster",
   580  			Replicas:    ptr.To[int32](1),
   581  			Template: clusterv1.MachineTemplateSpec{
   582  				Spec: clusterv1.MachineSpec{
   583  					InfrastructureRef: corev1.ObjectReference{
   584  						APIVersion: builder.InfrastructureGroupVersion.String(),
   585  						Kind:       builder.TestInfrastructureMachineTemplateKind,
   586  						Name:       "infra-config1",
   587  					},
   588  					Bootstrap: clusterv1.Bootstrap{DataSecretName: ptr.To("data")},
   589  				},
   590  			},
   591  		},
   592  	}
   593  	key := client.ObjectKey{Namespace: m.Namespace, Name: m.Name}
   594  	mr := &MachinePoolReconciler{
   595  		Client: fake.NewClientBuilder().WithObjects(testCluster, m).WithStatusSubresource(&expv1.MachinePool{}).Build(),
   596  	}
   597  	_, err := mr.Reconcile(ctx, reconcile.Request{NamespacedName: key})
   598  	g.Expect(err).ToNot(HaveOccurred())
   599  
   600  	var actual expv1.MachinePool
   601  	g.Expect(mr.Client.Get(ctx, key, &actual)).To(Succeed())
   602  	g.Expect(actual.ObjectMeta.Finalizers).To(Equal([]string{"test"}))
   603  }
   604  
   605  func TestMachinePoolConditions(t *testing.T) {
   606  	testCluster := &clusterv1.Cluster{
   607  		ObjectMeta: metav1.ObjectMeta{Namespace: metav1.NamespaceDefault, Name: "test-cluster"},
   608  	}
   609  
   610  	bootstrapConfig := func(ready bool) *unstructured.Unstructured {
   611  		return &unstructured.Unstructured{
   612  			Object: map[string]interface{}{
   613  				"kind":       builder.TestBootstrapConfigKind,
   614  				"apiVersion": builder.BootstrapGroupVersion.String(),
   615  				"metadata": map[string]interface{}{
   616  					"name":      "bootstrap1",
   617  					"namespace": metav1.NamespaceDefault,
   618  				},
   619  				"status": map[string]interface{}{
   620  					"ready":          ready,
   621  					"dataSecretName": "data",
   622  				},
   623  			},
   624  		}
   625  	}
   626  
   627  	infraConfig := func(ready bool) *unstructured.Unstructured {
   628  		return &unstructured.Unstructured{
   629  			Object: map[string]interface{}{
   630  				"kind":       builder.TestInfrastructureMachineTemplateKind,
   631  				"apiVersion": builder.InfrastructureGroupVersion.String(),
   632  				"metadata": map[string]interface{}{
   633  					"name":      "infra1",
   634  					"namespace": metav1.NamespaceDefault,
   635  				},
   636  				"status": map[string]interface{}{
   637  					"ready": ready,
   638  				},
   639  				"spec": map[string]interface{}{
   640  					"providerIDList": []interface{}{
   641  						"azure://westus2/id-node-4",
   642  						"aws://us-east-1/id-node-1",
   643  					},
   644  				},
   645  			},
   646  		}
   647  	}
   648  
   649  	machinePool := &expv1.MachinePool{
   650  		ObjectMeta: metav1.ObjectMeta{
   651  			Name:       "blah",
   652  			Namespace:  metav1.NamespaceDefault,
   653  			Finalizers: []string{expv1.MachinePoolFinalizer},
   654  		},
   655  		Spec: expv1.MachinePoolSpec{
   656  			ClusterName: "test-cluster",
   657  			Replicas:    ptr.To[int32](2),
   658  			Template: clusterv1.MachineTemplateSpec{
   659  				Spec: clusterv1.MachineSpec{
   660  					InfrastructureRef: corev1.ObjectReference{
   661  						APIVersion: builder.InfrastructureGroupVersion.String(),
   662  						Kind:       builder.TestInfrastructureMachineTemplateKind,
   663  						Name:       "infra1",
   664  					},
   665  					Bootstrap: clusterv1.Bootstrap{
   666  						ConfigRef: &corev1.ObjectReference{
   667  							APIVersion: builder.BootstrapGroupVersion.String(),
   668  							Kind:       builder.TestBootstrapConfigKind,
   669  							Name:       "bootstrap1",
   670  						},
   671  					},
   672  				},
   673  			},
   674  		},
   675  	}
   676  
   677  	nodeList := corev1.NodeList{
   678  		Items: []corev1.Node{
   679  			{
   680  				ObjectMeta: metav1.ObjectMeta{
   681  					Name: "node-1",
   682  				},
   683  				Spec: corev1.NodeSpec{
   684  					ProviderID: "aws://us-east-1/id-node-1",
   685  				},
   686  				Status: corev1.NodeStatus{Conditions: []corev1.NodeCondition{{Type: corev1.NodeReady}}},
   687  			},
   688  			{
   689  				ObjectMeta: metav1.ObjectMeta{
   690  					Name: "azure-node-4",
   691  				},
   692  				Spec: corev1.NodeSpec{
   693  					ProviderID: "azure://westus2/id-node-4",
   694  				},
   695  				Status: corev1.NodeStatus{Conditions: []corev1.NodeCondition{{Type: corev1.NodeReady}}},
   696  			},
   697  		},
   698  	}
   699  
   700  	testcases := []struct {
   701  		name                string
   702  		bootstrapReady      bool
   703  		infrastructureReady bool
   704  		expectError         bool
   705  		beforeFunc          func(bootstrap, infra *unstructured.Unstructured, mp *expv1.MachinePool, nodeList *corev1.NodeList)
   706  		conditionAssertFunc func(t *testing.T, getter conditions.Getter)
   707  	}{
   708  		{
   709  			name:                "all conditions true",
   710  			bootstrapReady:      true,
   711  			infrastructureReady: true,
   712  			beforeFunc: func(_, _ *unstructured.Unstructured, mp *expv1.MachinePool, _ *corev1.NodeList) {
   713  				mp.Spec.ProviderIDList = []string{"azure://westus2/id-node-4", "aws://us-east-1/id-node-1"}
   714  				mp.Status = expv1.MachinePoolStatus{
   715  					NodeRefs: []corev1.ObjectReference{
   716  						{Name: "node-1"},
   717  						{Name: "azure-node-4"},
   718  					},
   719  					Replicas:      2,
   720  					ReadyReplicas: 2,
   721  				}
   722  			},
   723  			conditionAssertFunc: func(t *testing.T, getter conditions.Getter) {
   724  				t.Helper()
   725  				g := NewWithT(t)
   726  
   727  				g.Expect(getter.GetConditions()).NotTo(BeEmpty())
   728  				for _, c := range getter.GetConditions() {
   729  					g.Expect(c.Status).To(Equal(corev1.ConditionTrue))
   730  				}
   731  			},
   732  		},
   733  		{
   734  			name:                "boostrap not ready",
   735  			bootstrapReady:      false,
   736  			infrastructureReady: true,
   737  			beforeFunc: func(bootstrap, _ *unstructured.Unstructured, _ *expv1.MachinePool, _ *corev1.NodeList) {
   738  				addConditionsToExternal(bootstrap, clusterv1.Conditions{
   739  					{
   740  						Type:     clusterv1.ReadyCondition,
   741  						Status:   corev1.ConditionFalse,
   742  						Severity: clusterv1.ConditionSeverityInfo,
   743  						Reason:   "Custom reason",
   744  					},
   745  				})
   746  			},
   747  			conditionAssertFunc: func(t *testing.T, getter conditions.Getter) {
   748  				t.Helper()
   749  				g := NewWithT(t)
   750  
   751  				g.Expect(conditions.Has(getter, clusterv1.BootstrapReadyCondition)).To(BeTrue())
   752  				infraReadyCondition := conditions.Get(getter, clusterv1.BootstrapReadyCondition)
   753  				g.Expect(infraReadyCondition.Status).To(Equal(corev1.ConditionFalse))
   754  				g.Expect(infraReadyCondition.Reason).To(Equal("Custom reason"))
   755  			},
   756  		},
   757  		{
   758  			name:                "bootstrap not ready with fallback condition",
   759  			bootstrapReady:      false,
   760  			infrastructureReady: true,
   761  			conditionAssertFunc: func(t *testing.T, getter conditions.Getter) {
   762  				t.Helper()
   763  				g := NewWithT(t)
   764  
   765  				g.Expect(conditions.Has(getter, clusterv1.BootstrapReadyCondition)).To(BeTrue())
   766  				bootstrapReadyCondition := conditions.Get(getter, clusterv1.BootstrapReadyCondition)
   767  				g.Expect(bootstrapReadyCondition.Status).To(Equal(corev1.ConditionFalse))
   768  
   769  				g.Expect(conditions.Has(getter, clusterv1.ReadyCondition)).To(BeTrue())
   770  				readyCondition := conditions.Get(getter, clusterv1.ReadyCondition)
   771  				g.Expect(readyCondition.Status).To(Equal(corev1.ConditionFalse))
   772  			},
   773  		},
   774  		{
   775  			name:                "infrastructure not ready",
   776  			bootstrapReady:      true,
   777  			infrastructureReady: false,
   778  			beforeFunc: func(_, infra *unstructured.Unstructured, _ *expv1.MachinePool, _ *corev1.NodeList) {
   779  				addConditionsToExternal(infra, clusterv1.Conditions{
   780  					{
   781  						Type:     clusterv1.ReadyCondition,
   782  						Status:   corev1.ConditionFalse,
   783  						Severity: clusterv1.ConditionSeverityInfo,
   784  						Reason:   "Custom reason",
   785  					},
   786  				})
   787  			},
   788  			conditionAssertFunc: func(t *testing.T, getter conditions.Getter) {
   789  				t.Helper()
   790  
   791  				g := NewWithT(t)
   792  
   793  				g.Expect(conditions.Has(getter, clusterv1.InfrastructureReadyCondition)).To(BeTrue())
   794  				infraReadyCondition := conditions.Get(getter, clusterv1.InfrastructureReadyCondition)
   795  				g.Expect(infraReadyCondition.Status).To(Equal(corev1.ConditionFalse))
   796  				g.Expect(infraReadyCondition.Reason).To(Equal("Custom reason"))
   797  			},
   798  		},
   799  		{
   800  			name:                "infrastructure not ready with fallback condition",
   801  			bootstrapReady:      true,
   802  			infrastructureReady: false,
   803  			conditionAssertFunc: func(t *testing.T, getter conditions.Getter) {
   804  				t.Helper()
   805  				g := NewWithT(t)
   806  
   807  				g.Expect(conditions.Has(getter, clusterv1.InfrastructureReadyCondition)).To(BeTrue())
   808  				infraReadyCondition := conditions.Get(getter, clusterv1.InfrastructureReadyCondition)
   809  				g.Expect(infraReadyCondition.Status).To(Equal(corev1.ConditionFalse))
   810  
   811  				g.Expect(conditions.Has(getter, clusterv1.ReadyCondition)).To(BeTrue())
   812  				readyCondition := conditions.Get(getter, clusterv1.ReadyCondition)
   813  				g.Expect(readyCondition.Status).To(Equal(corev1.ConditionFalse))
   814  			},
   815  		},
   816  		{
   817  			name:           "incorrect infrastructure reference",
   818  			bootstrapReady: true,
   819  			expectError:    true,
   820  			beforeFunc: func(_, _ *unstructured.Unstructured, mp *expv1.MachinePool, _ *corev1.NodeList) {
   821  				mp.Spec.Template.Spec.InfrastructureRef = corev1.ObjectReference{
   822  					APIVersion: builder.InfrastructureGroupVersion.String(),
   823  					Kind:       builder.TestInfrastructureMachineTemplateKind,
   824  					Name:       "does-not-exist",
   825  				}
   826  			},
   827  			conditionAssertFunc: func(t *testing.T, getter conditions.Getter) {
   828  				t.Helper()
   829  				g := NewWithT(t)
   830  
   831  				g.Expect(conditions.Has(getter, clusterv1.InfrastructureReadyCondition)).To(BeTrue())
   832  				infraReadyCondition := conditions.Get(getter, clusterv1.InfrastructureReadyCondition)
   833  				g.Expect(infraReadyCondition.Status).To(Equal(corev1.ConditionFalse))
   834  			},
   835  		},
   836  	}
   837  
   838  	for _, tt := range testcases {
   839  		t.Run(tt.name, func(t *testing.T) {
   840  			g := NewWithT(t)
   841  
   842  			// setup objects
   843  			bootstrap := bootstrapConfig(tt.bootstrapReady)
   844  			infra := infraConfig(tt.infrastructureReady)
   845  			mp := machinePool.DeepCopy()
   846  			nodes := nodeList.DeepCopy()
   847  			if tt.beforeFunc != nil {
   848  				tt.beforeFunc(bootstrap, infra, mp, nodes)
   849  			}
   850  
   851  			g.Expect(clusterv1.AddToScheme(scheme.Scheme)).To(Succeed())
   852  
   853  			clientFake := fake.NewClientBuilder().WithObjects(
   854  				testCluster,
   855  				mp,
   856  				infra,
   857  				bootstrap,
   858  				&nodes.Items[0],
   859  				&nodes.Items[1],
   860  				builder.TestBootstrapConfigCRD,
   861  				builder.TestInfrastructureMachineTemplateCRD,
   862  			).WithStatusSubresource(&expv1.MachinePool{}).Build()
   863  
   864  			r := &MachinePoolReconciler{
   865  				Client:    clientFake,
   866  				APIReader: clientFake,
   867  			}
   868  
   869  			_, err := r.Reconcile(ctx, reconcile.Request{NamespacedName: util.ObjectKey(machinePool)})
   870  			if !tt.expectError {
   871  				g.Expect(err).ToNot(HaveOccurred())
   872  			}
   873  
   874  			m := &expv1.MachinePool{}
   875  			machinePoolKey := client.ObjectKeyFromObject(machinePool)
   876  			g.Expect(r.Client.Get(ctx, machinePoolKey, m)).ToNot(HaveOccurred())
   877  
   878  			tt.conditionAssertFunc(t, m)
   879  		})
   880  	}
   881  }
   882  
   883  // adds a condition list to an external object.
   884  func addConditionsToExternal(u *unstructured.Unstructured, newConditions clusterv1.Conditions) {
   885  	existingConditions := clusterv1.Conditions{}
   886  	if cs := conditions.UnstructuredGetter(u).GetConditions(); len(cs) != 0 {
   887  		existingConditions = cs
   888  	}
   889  	existingConditions = append(existingConditions, newConditions...)
   890  	conditions.UnstructuredSetter(u).SetConditions(existingConditions)
   891  }