sigs.k8s.io/cluster-api-provider-azure@v1.14.3/controllers/azuremachine_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  	"context"
    21  	"testing"
    22  	"time"
    23  
    24  	. "github.com/onsi/gomega"
    25  	"github.com/pkg/errors"
    26  	corev1 "k8s.io/api/core/v1"
    27  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    28  	"k8s.io/apimachinery/pkg/runtime"
    29  	"k8s.io/apimachinery/pkg/types"
    30  	"k8s.io/client-go/tools/record"
    31  	"k8s.io/utils/ptr"
    32  	infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1"
    33  	"sigs.k8s.io/cluster-api-provider-azure/azure"
    34  	"sigs.k8s.io/cluster-api-provider-azure/azure/scope"
    35  	"sigs.k8s.io/cluster-api-provider-azure/azure/services/resourceskus"
    36  	"sigs.k8s.io/cluster-api-provider-azure/util/reconciler"
    37  	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
    38  	capierrors "sigs.k8s.io/cluster-api/errors"
    39  	ctrl "sigs.k8s.io/controller-runtime"
    40  	"sigs.k8s.io/controller-runtime/pkg/client"
    41  	"sigs.k8s.io/controller-runtime/pkg/client/fake"
    42  	"sigs.k8s.io/controller-runtime/pkg/reconcile"
    43  )
    44  
    45  type TestMachineReconcileInput struct {
    46  	createAzureMachineService func(*scope.MachineScope) (*azureMachineService, error)
    47  	azureMachineOptions       func(am *infrav1.AzureMachine)
    48  	expectedErr               string
    49  	machineScopeFailureReason capierrors.MachineStatusError
    50  	ready                     bool
    51  	cache                     *scope.MachineCache
    52  	skuCache                  scope.SKUCacher
    53  	expectedResult            reconcile.Result
    54  }
    55  
    56  func TestAzureMachineReconcile(t *testing.T) {
    57  	g := NewWithT(t)
    58  	scheme, err := newScheme()
    59  	g.Expect(err).NotTo(HaveOccurred())
    60  
    61  	defaultCluster := getFakeCluster()
    62  	defaultAzureCluster := getFakeAzureCluster()
    63  	defaultAzureMachine := getFakeAzureMachine()
    64  	defaultMachine := getFakeMachine(defaultAzureMachine)
    65  	defaultAzureClusterIdentity := getFakeAzureClusterIdentity()
    66  	defaultSecret := &corev1.Secret{Data: map[string][]byte{"clientSecret": []byte("fooSecret")}}
    67  
    68  	cases := map[string]struct {
    69  		objects []runtime.Object
    70  		fail    bool
    71  		err     string
    72  		event   string
    73  	}{
    74  		"should reconcile normally": {
    75  			objects: []runtime.Object{
    76  				defaultCluster,
    77  				defaultAzureCluster,
    78  				defaultAzureMachine,
    79  				defaultMachine,
    80  				defaultAzureClusterIdentity,
    81  				defaultSecret,
    82  			},
    83  		},
    84  		"should not fail if the azure machine is not found": {
    85  			objects: []runtime.Object{
    86  				defaultCluster,
    87  				defaultAzureCluster,
    88  				defaultMachine,
    89  				defaultAzureClusterIdentity,
    90  			},
    91  		},
    92  		"should fail if machine is not found": {
    93  			objects: []runtime.Object{
    94  				defaultCluster,
    95  				defaultAzureCluster,
    96  				defaultAzureMachine,
    97  				defaultAzureClusterIdentity,
    98  			},
    99  			fail: true,
   100  			err:  "machines.cluster.x-k8s.io \"my-machine\" not found",
   101  		},
   102  		"should return if azureMachine has not yet set ownerref": {
   103  			objects: []runtime.Object{
   104  				defaultCluster,
   105  				defaultAzureCluster,
   106  				getFakeAzureMachine(func(am *infrav1.AzureMachine) {
   107  					am.OwnerReferences = nil
   108  				}),
   109  				defaultMachine,
   110  				defaultAzureClusterIdentity,
   111  			},
   112  			event: "Machine controller dependency not yet met",
   113  		},
   114  		"should return if cluster does not exist": {
   115  			objects: []runtime.Object{
   116  				defaultAzureCluster,
   117  				defaultAzureMachine,
   118  				defaultMachine,
   119  				defaultAzureClusterIdentity,
   120  			},
   121  			event: "Unable to get cluster from metadata",
   122  		},
   123  		"should return if azureCluster does not yet available": {
   124  			objects: []runtime.Object{
   125  				defaultCluster,
   126  				defaultAzureMachine,
   127  				defaultMachine,
   128  				defaultAzureClusterIdentity,
   129  			},
   130  			event: "AzureCluster unavailable",
   131  		},
   132  	}
   133  
   134  	for name, tc := range cases {
   135  		t.Run(name, func(t *testing.T) {
   136  			fakeClient := fake.NewClientBuilder().
   137  				WithScheme(scheme).
   138  				WithRuntimeObjects(tc.objects...).
   139  				WithStatusSubresource(
   140  					&infrav1.AzureMachine{},
   141  				).
   142  				Build()
   143  
   144  			resultIdentity := &infrav1.AzureClusterIdentity{}
   145  			key := client.ObjectKey{Name: defaultAzureClusterIdentity.Name, Namespace: defaultAzureClusterIdentity.Namespace}
   146  			g.Expect(fakeClient.Get(context.TODO(), key, resultIdentity)).To(Succeed())
   147  
   148  			reconciler := &AzureMachineReconciler{
   149  				Client:   fakeClient,
   150  				Recorder: record.NewFakeRecorder(128),
   151  			}
   152  
   153  			_, err := reconciler.Reconcile(context.Background(), ctrl.Request{
   154  				NamespacedName: types.NamespacedName{
   155  					Namespace: "default",
   156  					Name:      "my-machine",
   157  				},
   158  			})
   159  			if tc.event != "" {
   160  				g.Expect(reconciler.Recorder.(*record.FakeRecorder).Events).To(Receive(ContainSubstring(tc.event)))
   161  			}
   162  			if tc.fail {
   163  				g.Expect(err).To(MatchError(tc.err))
   164  			} else {
   165  				g.Expect(err).NotTo(HaveOccurred())
   166  			}
   167  		})
   168  	}
   169  }
   170  
   171  type fakeSKUCacher struct{}
   172  
   173  func (f fakeSKUCacher) Get(context.Context, string, resourceskus.ResourceType) (resourceskus.SKU, error) {
   174  	return resourceskus.SKU{}, errors.New("not implemented")
   175  }
   176  
   177  func TestAzureMachineReconcileNormal(t *testing.T) {
   178  	cases := map[string]TestMachineReconcileInput{
   179  		"should reconcile normally": {
   180  			createAzureMachineService: getFakeAzureMachineService,
   181  			cache:                     &scope.MachineCache{},
   182  			ready:                     true,
   183  		},
   184  		"should skip reconciliation if error state is detected on azure machine": {
   185  			azureMachineOptions: func(am *infrav1.AzureMachine) {
   186  				updateError := capierrors.UpdateMachineError
   187  				am.Status.FailureReason = &updateError
   188  			},
   189  			createAzureMachineService: getFakeAzureMachineService,
   190  		},
   191  		"should fail if failed to initialize machine cache": {
   192  			createAzureMachineService: getFakeAzureMachineService,
   193  			cache:                     nil,
   194  			skuCache:                  fakeSKUCacher{},
   195  			expectedErr:               "failed to init machine scope cache",
   196  		},
   197  		"should fail if identities are not ready": {
   198  			azureMachineOptions: func(am *infrav1.AzureMachine) {
   199  				am.Status.Conditions = clusterv1.Conditions{
   200  					{
   201  						Type:   infrav1.VMIdentitiesReadyCondition,
   202  						Reason: infrav1.UserAssignedIdentityMissingReason,
   203  						Status: corev1.ConditionFalse,
   204  					},
   205  				}
   206  			},
   207  			createAzureMachineService: getFakeAzureMachineService,
   208  			cache:                     &scope.MachineCache{},
   209  			expectedErr:               "VM identities are not ready",
   210  		},
   211  		"should fail if azure machine service creator fails": {
   212  			createAzureMachineService: func(*scope.MachineScope) (*azureMachineService, error) {
   213  				return nil, errors.New("failed to create azure machine service")
   214  			},
   215  			cache:       &scope.MachineCache{},
   216  			expectedErr: "failed to create azure machine service",
   217  		},
   218  		"should fail if VM is deleted": {
   219  			createAzureMachineService: getFakeAzureMachineServiceWithVMDeleted,
   220  			machineScopeFailureReason: capierrors.UpdateMachineError,
   221  			cache:                     &scope.MachineCache{},
   222  			expectedErr:               "failed to reconcile AzureMachine",
   223  		},
   224  		"should reconcile if terminal error is received": {
   225  			createAzureMachineService: getFakeAzureMachineServiceWithTerminalError,
   226  			machineScopeFailureReason: capierrors.CreateMachineError,
   227  			cache:                     &scope.MachineCache{},
   228  		},
   229  		"should requeue if transient error is received": {
   230  			createAzureMachineService: getFakeAzureMachineServiceWithTransientError,
   231  			cache:                     &scope.MachineCache{},
   232  			expectedResult:            reconcile.Result{RequeueAfter: 10 * time.Second},
   233  		},
   234  		"should return error for general failures": {
   235  			createAzureMachineService: getFakeAzureMachineServiceWithGeneralError,
   236  			cache:                     &scope.MachineCache{},
   237  			expectedErr:               "failed to reconcile AzureMachine",
   238  		},
   239  	}
   240  
   241  	for name, c := range cases {
   242  		tc := c
   243  		t.Run(name, func(t *testing.T) {
   244  			g := NewWithT(t)
   245  
   246  			reconciler, machineScope, clusterScope, err := getMachineReconcileInputs(tc)
   247  			g.Expect(err).NotTo(HaveOccurred())
   248  
   249  			result, err := reconciler.reconcileNormal(context.Background(), machineScope, clusterScope)
   250  			g.Expect(result).To(Equal(tc.expectedResult))
   251  
   252  			if tc.ready {
   253  				g.Expect(machineScope.AzureMachine.Status.Ready).To(BeTrue())
   254  			}
   255  			if tc.machineScopeFailureReason != "" {
   256  				g.Expect(machineScope.AzureMachine.Status.FailureReason).NotTo(BeNil())
   257  				g.Expect(*machineScope.AzureMachine.Status.FailureReason).To(Equal(tc.machineScopeFailureReason))
   258  			}
   259  			if tc.expectedErr != "" {
   260  				g.Expect(err).To(HaveOccurred())
   261  				g.Expect(err.Error()).To(ContainSubstring(tc.expectedErr))
   262  			} else {
   263  				g.Expect(err).NotTo(HaveOccurred())
   264  			}
   265  		})
   266  	}
   267  }
   268  
   269  func TestAzureMachineReconcilePause(t *testing.T) {
   270  	cases := map[string]TestMachineReconcileInput{
   271  		"should pause successfully": {
   272  			createAzureMachineService: getFakeAzureMachineService,
   273  			cache:                     &scope.MachineCache{},
   274  		},
   275  		"should fail if failed to create azure machine service": {
   276  			createAzureMachineService: getFakeAzureMachineServiceWithFailure,
   277  			cache:                     &scope.MachineCache{},
   278  			expectedErr:               "failed to create AzureMachineService",
   279  		},
   280  		"should fail to pause for errors": {
   281  			createAzureMachineService: getFakeAzureMachineServiceWithGeneralError,
   282  			cache:                     &scope.MachineCache{},
   283  			expectedErr:               "failed to pause azure machine service",
   284  		},
   285  	}
   286  
   287  	for name, c := range cases {
   288  		tc := c
   289  		t.Run(name, func(t *testing.T) {
   290  			g := NewWithT(t)
   291  
   292  			reconciler, machineScope, _, err := getMachineReconcileInputs(tc)
   293  			g.Expect(err).NotTo(HaveOccurred())
   294  
   295  			result, err := reconciler.reconcilePause(context.Background(), machineScope)
   296  			g.Expect(result).To(Equal(tc.expectedResult))
   297  
   298  			if tc.expectedErr != "" {
   299  				g.Expect(err).To(HaveOccurred())
   300  				g.Expect(err.Error()).To(ContainSubstring(tc.expectedErr))
   301  			} else {
   302  				g.Expect(err).NotTo(HaveOccurred())
   303  			}
   304  		})
   305  	}
   306  }
   307  
   308  func TestAzureMachineReconcileDelete(t *testing.T) {
   309  	cases := map[string]TestMachineReconcileInput{
   310  		"should delete successfully": {
   311  			createAzureMachineService: getFakeAzureMachineService,
   312  			cache:                     &scope.MachineCache{},
   313  		},
   314  		"should fail if failed to create azure machine service": {
   315  			createAzureMachineService: getFakeAzureMachineServiceWithFailure,
   316  			cache:                     &scope.MachineCache{},
   317  			expectedErr:               "failed to create AzureMachineService",
   318  		},
   319  		"should requeue if transient error is received": {
   320  			createAzureMachineService: getFakeAzureMachineServiceWithTransientError,
   321  			cache:                     &scope.MachineCache{},
   322  			expectedResult:            reconcile.Result{RequeueAfter: 10 * time.Second},
   323  		},
   324  		"should fail to delete for non-transient errors": {
   325  			createAzureMachineService: getFakeAzureMachineServiceWithGeneralError,
   326  			cache:                     &scope.MachineCache{},
   327  			expectedErr:               "error deleting AzureMachine",
   328  		},
   329  	}
   330  
   331  	for name, c := range cases {
   332  		tc := c
   333  		t.Run(name, func(t *testing.T) {
   334  			g := NewWithT(t)
   335  
   336  			reconciler, machineScope, clusterScope, err := getMachineReconcileInputs(tc)
   337  			g.Expect(err).NotTo(HaveOccurred())
   338  
   339  			result, err := reconciler.reconcileDelete(context.Background(), machineScope, clusterScope)
   340  			g.Expect(result).To(Equal(tc.expectedResult))
   341  
   342  			if tc.expectedErr != "" {
   343  				g.Expect(err).To(HaveOccurred())
   344  				g.Expect(err.Error()).To(ContainSubstring(tc.expectedErr))
   345  			} else {
   346  				g.Expect(err).NotTo(HaveOccurred())
   347  			}
   348  		})
   349  	}
   350  }
   351  
   352  func getMachineReconcileInputs(tc TestMachineReconcileInput) (*AzureMachineReconciler, *scope.MachineScope, *scope.ClusterScope, error) {
   353  	scheme, err := newScheme()
   354  	if err != nil {
   355  		return nil, nil, nil, err
   356  	}
   357  
   358  	var azureMachine *infrav1.AzureMachine
   359  	if tc.azureMachineOptions != nil {
   360  		azureMachine = getFakeAzureMachine(tc.azureMachineOptions)
   361  	} else {
   362  		azureMachine = getFakeAzureMachine()
   363  	}
   364  
   365  	cluster := getFakeCluster()
   366  	azureCluster := getFakeAzureCluster(func(ac *infrav1.AzureCluster) {
   367  		ac.Spec.Location = "westus2"
   368  	})
   369  	machine := getFakeMachine(azureMachine, func(m *clusterv1.Machine) {
   370  		m.Spec.Bootstrap = clusterv1.Bootstrap{
   371  			DataSecretName: ptr.To("fooSecret"),
   372  		}
   373  	})
   374  	azureClusterIdentity := getFakeAzureClusterIdentity(func(identity *infrav1.AzureClusterIdentity) {
   375  		identity.Spec.ClientSecret.Name = "fooSecret"
   376  		identity.Spec.ClientSecret.Namespace = "default"
   377  	})
   378  
   379  	objects := []runtime.Object{
   380  		cluster,
   381  		azureCluster,
   382  		machine,
   383  		azureMachine,
   384  		azureClusterIdentity,
   385  		&corev1.Secret{
   386  			ObjectMeta: metav1.ObjectMeta{
   387  				Name:      "fooSecret",
   388  				Namespace: "default",
   389  			},
   390  			Data: map[string][]byte{
   391  				"clientSecret": []byte("fooSecret"),
   392  			},
   393  		},
   394  	}
   395  
   396  	client := fake.NewClientBuilder().
   397  		WithScheme(scheme).
   398  		WithRuntimeObjects(objects...).
   399  		WithStatusSubresource(
   400  			&infrav1.AzureMachine{},
   401  		).
   402  		Build()
   403  
   404  	reconciler := &AzureMachineReconciler{
   405  		Client:                    client,
   406  		Recorder:                  record.NewFakeRecorder(128),
   407  		createAzureMachineService: tc.createAzureMachineService,
   408  	}
   409  
   410  	clusterScope, err := scope.NewClusterScope(context.Background(), scope.ClusterScopeParams{
   411  		Client:       client,
   412  		Cluster:      cluster,
   413  		AzureCluster: azureCluster,
   414  	})
   415  	if err != nil {
   416  		return nil, nil, nil, err
   417  	}
   418  
   419  	machineScope, err := scope.NewMachineScope(scope.MachineScopeParams{
   420  		Client:       client,
   421  		Machine:      machine,
   422  		AzureMachine: azureMachine,
   423  		ClusterScope: clusterScope,
   424  		Cache:        tc.cache,
   425  		SKUCache:     tc.skuCache,
   426  	})
   427  	if err != nil {
   428  		return nil, nil, nil, err
   429  	}
   430  
   431  	return reconciler, machineScope, clusterScope, nil
   432  }
   433  
   434  func getFakeAzureMachineService(machineScope *scope.MachineScope) (*azureMachineService, error) {
   435  	cache, err := resourceskus.GetCache(machineScope, machineScope.Location())
   436  	if err != nil {
   437  		return nil, errors.Wrap(err, "failed creating a NewCache")
   438  	}
   439  
   440  	return getDefaultAzureMachineService(machineScope, cache), nil
   441  }
   442  
   443  func getFakeAzureMachineServiceWithFailure(_ *scope.MachineScope) (*azureMachineService, error) {
   444  	return nil, errors.New("failed to create AzureMachineService")
   445  }
   446  
   447  func getFakeAzureMachineServiceWithVMDeleted(machineScope *scope.MachineScope) (*azureMachineService, error) {
   448  	cache, err := resourceskus.GetCache(machineScope, machineScope.Location())
   449  	if err != nil {
   450  		return nil, errors.Wrap(err, "failed creating a NewCache")
   451  	}
   452  
   453  	ams := getDefaultAzureMachineService(machineScope, cache)
   454  	ams.Reconcile = func(context.Context) error {
   455  		return azure.VMDeletedError{}
   456  	}
   457  
   458  	return ams, nil
   459  }
   460  
   461  func getFakeAzureMachineServiceWithTerminalError(machineScope *scope.MachineScope) (*azureMachineService, error) {
   462  	cache, err := resourceskus.GetCache(machineScope, machineScope.Location())
   463  	if err != nil {
   464  		return nil, errors.Wrap(err, "failed creating a NewCache")
   465  	}
   466  
   467  	ams := getDefaultAzureMachineService(machineScope, cache)
   468  	ams.Reconcile = func(context.Context) error {
   469  		return azure.WithTerminalError(errors.New("failed to reconcile AzureMachine"))
   470  	}
   471  
   472  	return ams, nil
   473  }
   474  
   475  func getFakeAzureMachineServiceWithTransientError(machineScope *scope.MachineScope) (*azureMachineService, error) {
   476  	cache, err := resourceskus.GetCache(machineScope, machineScope.Location())
   477  	if err != nil {
   478  		return nil, errors.Wrap(err, "failed creating a NewCache")
   479  	}
   480  
   481  	ams := getDefaultAzureMachineService(machineScope, cache)
   482  	ams.Reconcile = func(context.Context) error {
   483  		return azure.WithTransientError(errors.New("failed to reconcile AzureMachine"), 10*time.Second)
   484  	}
   485  	ams.Delete = func(context.Context) error {
   486  		return azure.WithTransientError(errors.New("failed to reconcile AzureMachine"), 10*time.Second)
   487  	}
   488  
   489  	return ams, nil
   490  }
   491  
   492  func getFakeAzureMachineServiceWithGeneralError(machineScope *scope.MachineScope) (*azureMachineService, error) {
   493  	cache, err := resourceskus.GetCache(machineScope, machineScope.Location())
   494  	if err != nil {
   495  		return nil, errors.Wrap(err, "failed creating a NewCache")
   496  	}
   497  
   498  	ams := getDefaultAzureMachineService(machineScope, cache)
   499  	ams.Reconcile = func(context.Context) error {
   500  		return errors.New("foo error")
   501  	}
   502  	ams.Pause = func(context.Context) error {
   503  		return errors.New("foo error")
   504  	}
   505  	ams.Delete = func(context.Context) error {
   506  		return errors.New("foo error")
   507  	}
   508  
   509  	return ams, nil
   510  }
   511  
   512  func getDefaultAzureMachineService(machineScope *scope.MachineScope, cache *resourceskus.Cache) *azureMachineService {
   513  	return &azureMachineService{
   514  		scope:    machineScope,
   515  		services: []azure.ServiceReconciler{},
   516  		skuCache: cache,
   517  		Reconcile: func(context.Context) error {
   518  			return nil
   519  		},
   520  		Pause: func(context.Context) error {
   521  			return nil
   522  		},
   523  		Delete: func(context.Context) error {
   524  			return nil
   525  		},
   526  	}
   527  }
   528  
   529  func getFakeCluster() *clusterv1.Cluster {
   530  	return &clusterv1.Cluster{
   531  		ObjectMeta: metav1.ObjectMeta{
   532  			Name:      "my-cluster",
   533  			Namespace: "default",
   534  		},
   535  		Spec: clusterv1.ClusterSpec{
   536  			InfrastructureRef: &corev1.ObjectReference{
   537  				APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1",
   538  				Kind:       infrav1.AzureClusterKind,
   539  				Name:       "my-azure-cluster",
   540  			},
   541  		},
   542  		Status: clusterv1.ClusterStatus{
   543  			InfrastructureReady: true,
   544  		},
   545  	}
   546  }
   547  
   548  func getFakeAzureCluster(changes ...func(*infrav1.AzureCluster)) *infrav1.AzureCluster {
   549  	input := &infrav1.AzureCluster{
   550  		ObjectMeta: metav1.ObjectMeta{
   551  			Name:      "my-azure-cluster",
   552  			Namespace: "default",
   553  		},
   554  		Spec: infrav1.AzureClusterSpec{
   555  			AzureClusterClassSpec: infrav1.AzureClusterClassSpec{
   556  				SubscriptionID: "123",
   557  				IdentityRef: &corev1.ObjectReference{
   558  					Name:      "fake-identity",
   559  					Namespace: "default",
   560  					Kind:      "AzureClusterIdentity",
   561  				},
   562  			},
   563  			NetworkSpec: infrav1.NetworkSpec{
   564  				Subnets: infrav1.Subnets{
   565  					{
   566  						SubnetClassSpec: infrav1.SubnetClassSpec{
   567  							Name: "node",
   568  							Role: infrav1.SubnetNode,
   569  						},
   570  					},
   571  				},
   572  				APIServerLB: infrav1.LoadBalancerSpec{
   573  					Name: "my-cluster-public-lb",
   574  					FrontendIPs: []infrav1.FrontendIP{
   575  						{
   576  							PublicIP: &infrav1.PublicIPSpec{
   577  								Name:    "my-cluster-public-lb-frontEnd",
   578  								DNSName: "my-cluster-fb560e20.westus2.cloudapp.azure.com",
   579  							},
   580  						},
   581  					},
   582  				},
   583  			},
   584  			ControlPlaneEndpoint: clusterv1.APIEndpoint{
   585  				Port: 6443,
   586  			},
   587  		},
   588  	}
   589  	for _, change := range changes {
   590  		change(input)
   591  	}
   592  
   593  	return input
   594  }
   595  
   596  func getFakeAzureMachine(changes ...func(*infrav1.AzureMachine)) *infrav1.AzureMachine {
   597  	input := &infrav1.AzureMachine{
   598  		ObjectMeta: metav1.ObjectMeta{
   599  			Name:      "my-machine",
   600  			Namespace: "default",
   601  			Labels: map[string]string{
   602  				clusterv1.ClusterNameLabel: "my-cluster",
   603  			},
   604  			OwnerReferences: []metav1.OwnerReference{
   605  				{
   606  					APIVersion: "cluster.x-k8s.io/v1beta1",
   607  					Kind:       "Machine",
   608  					Name:       "my-machine",
   609  				},
   610  			},
   611  		},
   612  		Spec: infrav1.AzureMachineSpec{
   613  			VMSize: "Standard_D2s_v3",
   614  		},
   615  	}
   616  	for _, change := range changes {
   617  		change(input)
   618  	}
   619  
   620  	return input
   621  }
   622  
   623  func getFakeAzureClusterIdentity(changes ...func(*infrav1.AzureClusterIdentity)) *infrav1.AzureClusterIdentity {
   624  	input := &infrav1.AzureClusterIdentity{
   625  		ObjectMeta: metav1.ObjectMeta{
   626  			Name:      "fake-identity",
   627  			Namespace: "default",
   628  		},
   629  		Spec: infrav1.AzureClusterIdentitySpec{
   630  			Type:     infrav1.ServicePrincipal,
   631  			ClientID: "fake-client-id",
   632  			TenantID: "fake-tenant-id",
   633  		},
   634  	}
   635  
   636  	for _, change := range changes {
   637  		change(input)
   638  	}
   639  
   640  	return input
   641  }
   642  
   643  func getFakeMachine(azureMachine *infrav1.AzureMachine, changes ...func(*clusterv1.Machine)) *clusterv1.Machine {
   644  	input := &clusterv1.Machine{
   645  		ObjectMeta: metav1.ObjectMeta{
   646  			Name:      "my-machine",
   647  			Namespace: "default",
   648  			Labels: map[string]string{
   649  				clusterv1.ClusterNameLabel: "my-cluster",
   650  			},
   651  		},
   652  		TypeMeta: metav1.TypeMeta{
   653  			APIVersion: "cluster.x-k8s.io/v1beta1",
   654  			Kind:       "Machine",
   655  		},
   656  		Spec: clusterv1.MachineSpec{
   657  			InfrastructureRef: corev1.ObjectReference{
   658  				APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1",
   659  				Kind:       "AzureMachine",
   660  				Name:       azureMachine.Name,
   661  				Namespace:  azureMachine.Namespace,
   662  			},
   663  			Version: ptr.To("v1.22.0"),
   664  		},
   665  	}
   666  	for _, change := range changes {
   667  		change(input)
   668  	}
   669  
   670  	return input
   671  }
   672  
   673  func TestConditions(t *testing.T) {
   674  	g := NewWithT(t)
   675  	scheme, err := newScheme()
   676  	g.Expect(err).NotTo(HaveOccurred())
   677  
   678  	testcases := []struct {
   679  		name               string
   680  		clusterStatus      clusterv1.ClusterStatus
   681  		machine            *clusterv1.Machine
   682  		azureMachine       *infrav1.AzureMachine
   683  		expectedConditions []clusterv1.Condition
   684  	}{
   685  		{
   686  			name: "cluster infrastructure is not ready yet",
   687  			clusterStatus: clusterv1.ClusterStatus{
   688  				InfrastructureReady: false,
   689  			},
   690  			machine: &clusterv1.Machine{
   691  				ObjectMeta: metav1.ObjectMeta{
   692  					Labels: map[string]string{
   693  						clusterv1.ClusterNameLabel: "my-cluster",
   694  					},
   695  					Name: "my-machine",
   696  				},
   697  			},
   698  			azureMachine: &infrav1.AzureMachine{
   699  				ObjectMeta: metav1.ObjectMeta{
   700  					Name: "azure-test1",
   701  					OwnerReferences: []metav1.OwnerReference{
   702  						{
   703  							APIVersion: clusterv1.GroupVersion.String(),
   704  							Kind:       "Machine",
   705  							Name:       "test1",
   706  						},
   707  					},
   708  				},
   709  			},
   710  			expectedConditions: []clusterv1.Condition{{
   711  				Type:     "VMRunning",
   712  				Status:   corev1.ConditionFalse,
   713  				Severity: clusterv1.ConditionSeverityInfo,
   714  				Reason:   "WaitingForClusterInfrastructure",
   715  			}},
   716  		},
   717  		{
   718  			name: "bootstrap data secret reference is not yet available",
   719  			clusterStatus: clusterv1.ClusterStatus{
   720  				InfrastructureReady: true,
   721  			},
   722  			machine: &clusterv1.Machine{
   723  				ObjectMeta: metav1.ObjectMeta{
   724  					Labels: map[string]string{
   725  						clusterv1.ClusterNameLabel: "my-cluster",
   726  					},
   727  					Name: "my-machine",
   728  				},
   729  			},
   730  			azureMachine: &infrav1.AzureMachine{
   731  				ObjectMeta: metav1.ObjectMeta{
   732  					Name: "azure-test1",
   733  					OwnerReferences: []metav1.OwnerReference{
   734  						{
   735  							APIVersion: clusterv1.GroupVersion.String(),
   736  							Kind:       "Machine",
   737  							Name:       "test1",
   738  						},
   739  					},
   740  				},
   741  			},
   742  			expectedConditions: []clusterv1.Condition{{
   743  				Type:     "VMRunning",
   744  				Status:   corev1.ConditionFalse,
   745  				Severity: clusterv1.ConditionSeverityInfo,
   746  				Reason:   "WaitingForBootstrapData",
   747  			}},
   748  		},
   749  	}
   750  	for _, tc := range testcases {
   751  		t.Run(tc.name, func(t *testing.T) {
   752  			cluster := &clusterv1.Cluster{
   753  				ObjectMeta: metav1.ObjectMeta{
   754  					Name: "my-cluster",
   755  				},
   756  				Status: tc.clusterStatus,
   757  			}
   758  			azureCluster := &infrav1.AzureCluster{
   759  				Spec: infrav1.AzureClusterSpec{
   760  					AzureClusterClassSpec: infrav1.AzureClusterClassSpec{
   761  						SubscriptionID: "123",
   762  						IdentityRef: &corev1.ObjectReference{
   763  							Name:      "fake-identity",
   764  							Namespace: "default",
   765  							Kind:      "AzureClusterIdentity",
   766  						},
   767  					},
   768  				},
   769  			}
   770  			azureClusterIdentity := getFakeAzureClusterIdentity()
   771  			defaultSecret := &corev1.Secret{Data: map[string][]byte{"clientSecret": []byte("fooSecret")}}
   772  
   773  			initObjects := []runtime.Object{
   774  				cluster,
   775  				tc.machine,
   776  				azureCluster,
   777  				tc.azureMachine,
   778  				azureClusterIdentity,
   779  				defaultSecret,
   780  			}
   781  			fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(initObjects...).Build()
   782  			resultIdentity := &infrav1.AzureClusterIdentity{}
   783  			key := client.ObjectKey{Name: azureClusterIdentity.Name, Namespace: azureClusterIdentity.Namespace}
   784  			g.Expect(fakeClient.Get(context.TODO(), key, resultIdentity)).To(Succeed())
   785  			recorder := record.NewFakeRecorder(10)
   786  
   787  			reconciler := NewAzureMachineReconciler(fakeClient, recorder, reconciler.Timeouts{}, "")
   788  
   789  			clusterScope, err := scope.NewClusterScope(context.TODO(), scope.ClusterScopeParams{
   790  				Client:       fakeClient,
   791  				Cluster:      cluster,
   792  				AzureCluster: azureCluster,
   793  			})
   794  			g.Expect(err).NotTo(HaveOccurred())
   795  
   796  			machineScope, err := scope.NewMachineScope(scope.MachineScopeParams{
   797  				Client:       fakeClient,
   798  				ClusterScope: clusterScope,
   799  				Machine:      tc.machine,
   800  				AzureMachine: tc.azureMachine,
   801  				Cache:        &scope.MachineCache{},
   802  			})
   803  			g.Expect(err).NotTo(HaveOccurred())
   804  
   805  			_, err = reconciler.reconcileNormal(context.TODO(), machineScope, clusterScope)
   806  			g.Expect(err).NotTo(HaveOccurred())
   807  
   808  			g.Expect(machineScope.AzureMachine.GetConditions()).To(HaveLen(len(tc.expectedConditions)))
   809  			for i, c := range machineScope.AzureMachine.GetConditions() {
   810  				g.Expect(conditionsMatch(c, tc.expectedConditions[i])).To(BeTrue())
   811  			}
   812  		})
   813  	}
   814  }
   815  
   816  func conditionsMatch(i, j clusterv1.Condition) bool {
   817  	return i.Type == j.Type &&
   818  		i.Status == j.Status &&
   819  		i.Reason == j.Reason &&
   820  		i.Severity == j.Severity
   821  }