sigs.k8s.io/cluster-api-provider-aws@v1.5.5/exp/controllers/awsmachinepool_controller_test.go (about)

     1  /*
     2  Copyright 2020 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  	"bytes"
    21  	"context"
    22  	"flag"
    23  	"fmt"
    24  	"testing"
    25  
    26  	"github.com/go-logr/logr"
    27  	"github.com/golang/mock/gomock"
    28  	. "github.com/onsi/gomega"
    29  	"github.com/pkg/errors"
    30  	corev1 "k8s.io/api/core/v1"
    31  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    32  	"k8s.io/apimachinery/pkg/runtime"
    33  	"k8s.io/client-go/tools/record"
    34  	"k8s.io/klog/v2"
    35  	"k8s.io/utils/pointer"
    36  	"sigs.k8s.io/controller-runtime/pkg/client/fake"
    37  
    38  	infrav1 "sigs.k8s.io/cluster-api-provider-aws/api/v1beta1"
    39  	expinfrav1 "sigs.k8s.io/cluster-api-provider-aws/exp/api/v1beta1"
    40  	"sigs.k8s.io/cluster-api-provider-aws/pkg/cloud"
    41  	"sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/scope"
    42  	"sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/services"
    43  	"sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/services/mock_services"
    44  	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
    45  	"sigs.k8s.io/cluster-api/controllers/noderefutil"
    46  	capierrors "sigs.k8s.io/cluster-api/errors"
    47  	expclusterv1 "sigs.k8s.io/cluster-api/exp/api/v1beta1"
    48  	"sigs.k8s.io/cluster-api/util/conditions"
    49  	"sigs.k8s.io/cluster-api/util/patch"
    50  )
    51  
    52  func TestAWSMachinePoolReconciler(t *testing.T) {
    53  	var (
    54  		reconciler     AWSMachinePoolReconciler
    55  		cs             *scope.ClusterScope
    56  		ms             *scope.MachinePoolScope
    57  		mockCtrl       *gomock.Controller
    58  		ec2Svc         *mock_services.MockEC2Interface
    59  		asgSvc         *mock_services.MockASGInterface
    60  		recorder       *record.FakeRecorder
    61  		awsMachinePool *expinfrav1.AWSMachinePool
    62  		secret         *corev1.Secret
    63  	)
    64  	setup := func(t *testing.T, g *WithT) {
    65  		t.Helper()
    66  
    67  		var err error
    68  
    69  		if err := flag.Set("logtostderr", "false"); err != nil {
    70  			_ = fmt.Errorf("Error setting logtostderr flag")
    71  		}
    72  		if err := flag.Set("v", "2"); err != nil {
    73  			_ = fmt.Errorf("Error setting v flag")
    74  		}
    75  		ctx := context.TODO()
    76  
    77  		awsMachinePool = &expinfrav1.AWSMachinePool{
    78  			ObjectMeta: metav1.ObjectMeta{
    79  				Name:      "test",
    80  				Namespace: "default",
    81  			},
    82  			Spec: expinfrav1.AWSMachinePoolSpec{
    83  				MinSize: int32(0),
    84  				MaxSize: int32(1),
    85  			},
    86  		}
    87  
    88  		secret = &corev1.Secret{
    89  			ObjectMeta: metav1.ObjectMeta{
    90  				Name:      "bootstrap-data",
    91  				Namespace: "default",
    92  			},
    93  			Data: map[string][]byte{
    94  				"value": []byte("shell-script"),
    95  			},
    96  		}
    97  
    98  		g.Expect(testEnv.Create(ctx, awsMachinePool)).To(Succeed())
    99  		g.Expect(testEnv.Create(ctx, secret)).To(Succeed())
   100  
   101  		ms, err = scope.NewMachinePoolScope(
   102  			scope.MachinePoolScopeParams{
   103  				Client: testEnv.Client,
   104  				Cluster: &clusterv1.Cluster{
   105  					Status: clusterv1.ClusterStatus{
   106  						InfrastructureReady: true,
   107  					},
   108  				},
   109  				MachinePool: &expclusterv1.MachinePool{
   110  					ObjectMeta: metav1.ObjectMeta{
   111  						Name:      "mp",
   112  						Namespace: "default",
   113  					},
   114  					Spec: expclusterv1.MachinePoolSpec{
   115  						ClusterName: "test",
   116  						Template: clusterv1.MachineTemplateSpec{
   117  							Spec: clusterv1.MachineSpec{
   118  								ClusterName: "test",
   119  								Bootstrap: clusterv1.Bootstrap{
   120  									DataSecretName: pointer.StringPtr("bootstrap-data"),
   121  								},
   122  							},
   123  						},
   124  					},
   125  				},
   126  				InfraCluster:   cs,
   127  				AWSMachinePool: awsMachinePool,
   128  			},
   129  		)
   130  		g.Expect(err).To(BeNil())
   131  
   132  		cs, err = setupCluster("test-cluster")
   133  		g.Expect(err).To(BeNil())
   134  
   135  		mockCtrl = gomock.NewController(t)
   136  		ec2Svc = mock_services.NewMockEC2Interface(mockCtrl)
   137  		asgSvc = mock_services.NewMockASGInterface(mockCtrl)
   138  
   139  		// If the test hangs for 9 minutes, increase the value here to the number of events during a reconciliation loop
   140  		recorder = record.NewFakeRecorder(2)
   141  
   142  		reconciler = AWSMachinePoolReconciler{
   143  			ec2ServiceFactory: func(scope.EC2Scope) services.EC2Interface {
   144  				return ec2Svc
   145  			},
   146  			asgServiceFactory: func(cloud.ClusterScoper) services.ASGInterface {
   147  				return asgSvc
   148  			},
   149  			Recorder: recorder,
   150  		}
   151  	}
   152  
   153  	teardown := func(t *testing.T, g *WithT) {
   154  		t.Helper()
   155  
   156  		ctx := context.TODO()
   157  		mpPh, err := patch.NewHelper(awsMachinePool, testEnv)
   158  		g.Expect(err).ShouldNot(HaveOccurred())
   159  		awsMachinePool.SetFinalizers([]string{})
   160  		g.Expect(mpPh.Patch(ctx, awsMachinePool)).To(Succeed())
   161  		g.Expect(testEnv.Delete(ctx, awsMachinePool)).To(Succeed())
   162  		g.Expect(testEnv.Delete(ctx, secret)).To(Succeed())
   163  		mockCtrl.Finish()
   164  	}
   165  
   166  	t.Run("Reconciling an AWSMachinePool", func(t *testing.T) {
   167  		t.Run("when can't reach amazon", func(t *testing.T) {
   168  			expectedErr := errors.New("no connection available ")
   169  			getASG := func(t *testing.T, g *WithT) {
   170  				t.Helper()
   171  
   172  				ec2Svc.EXPECT().GetLaunchTemplate(gomock.Any()).Return(nil, "", expectedErr).AnyTimes()
   173  				asgSvc.EXPECT().GetASGByName(gomock.Any()).Return(nil, expectedErr).AnyTimes()
   174  			}
   175  			t.Run("should exit immediately on an error state", func(t *testing.T) {
   176  				g := NewWithT(t)
   177  				setup(t, g)
   178  				defer teardown(t, g)
   179  				getASG(t, g)
   180  
   181  				er := capierrors.CreateMachineError
   182  				ms.AWSMachinePool.Status.FailureReason = &er
   183  				ms.AWSMachinePool.Status.FailureMessage = pointer.StringPtr("Couldn't create machine pool")
   184  
   185  				buf := new(bytes.Buffer)
   186  				klog.SetOutput(buf)
   187  
   188  				_, _ = reconciler.reconcileNormal(context.Background(), ms, cs, cs)
   189  				g.Expect(buf).To(ContainSubstring("Error state detected, skipping reconciliation"))
   190  			})
   191  			t.Run("should add our finalizer to the machinepool", func(t *testing.T) {
   192  				g := NewWithT(t)
   193  				setup(t, g)
   194  				defer teardown(t, g)
   195  				getASG(t, g)
   196  
   197  				_, _ = reconciler.reconcileNormal(context.Background(), ms, cs, cs)
   198  
   199  				g.Expect(ms.AWSMachinePool.Finalizers).To(ContainElement(expinfrav1.MachinePoolFinalizer))
   200  			})
   201  			t.Run("should exit immediately if cluster infra isn't ready", func(t *testing.T) {
   202  				g := NewWithT(t)
   203  				setup(t, g)
   204  				defer teardown(t, g)
   205  				getASG(t, g)
   206  
   207  				ms.Cluster.Status.InfrastructureReady = false
   208  
   209  				buf := new(bytes.Buffer)
   210  				klog.SetOutput(buf)
   211  
   212  				_, err := reconciler.reconcileNormal(context.Background(), ms, cs, cs)
   213  				g.Expect(err).To(BeNil())
   214  				g.Expect(buf.String()).To(ContainSubstring("Cluster infrastructure is not ready yet"))
   215  				expectConditions(g, ms.AWSMachinePool, []conditionAssertion{{expinfrav1.ASGReadyCondition, corev1.ConditionFalse, clusterv1.ConditionSeverityInfo, infrav1.WaitingForClusterInfrastructureReason}})
   216  			})
   217  			t.Run("should exit immediately if bootstrap data secret reference isn't available", func(t *testing.T) {
   218  				g := NewWithT(t)
   219  				setup(t, g)
   220  				defer teardown(t, g)
   221  				getASG(t, g)
   222  
   223  				ms.MachinePool.Spec.Template.Spec.Bootstrap.DataSecretName = nil
   224  				buf := new(bytes.Buffer)
   225  				klog.SetOutput(buf)
   226  
   227  				_, err := reconciler.reconcileNormal(context.Background(), ms, cs, cs)
   228  
   229  				g.Expect(err).To(BeNil())
   230  				g.Expect(buf.String()).To(ContainSubstring("Bootstrap data secret reference is not yet available"))
   231  				expectConditions(g, ms.AWSMachinePool, []conditionAssertion{{expinfrav1.ASGReadyCondition, corev1.ConditionFalse, clusterv1.ConditionSeverityInfo, infrav1.WaitingForBootstrapDataReason}})
   232  			})
   233  		})
   234  		t.Run("there's a provider ID", func(t *testing.T) {
   235  			id := "<cloudProvider>://<optional>/<segments>/<providerid>"
   236  			setProviderID := func(t *testing.T, g *WithT) {
   237  				t.Helper()
   238  
   239  				_, err := noderefutil.NewProviderID(id)
   240  				g.Expect(err).To(BeNil())
   241  
   242  				ms.AWSMachinePool.Spec.ProviderID = id
   243  			}
   244  			t.Run("should look up by provider ID when one exists", func(t *testing.T) {
   245  				g := NewWithT(t)
   246  				setup(t, g)
   247  				defer teardown(t, g)
   248  				setProviderID(t, g)
   249  
   250  				expectedErr := errors.New("no connection available ")
   251  				var launchtemplate *expinfrav1.AWSLaunchTemplate
   252  				ec2Svc.EXPECT().GetLaunchTemplate(gomock.Any()).Return(launchtemplate, "", expectedErr)
   253  				_, err := reconciler.reconcileNormal(context.Background(), ms, cs, cs)
   254  				g.Expect(errors.Cause(err)).To(MatchError(expectedErr))
   255  			})
   256  			t.Run("should try to create a new machinepool if none exists", func(t *testing.T) {
   257  				g := NewWithT(t)
   258  				setup(t, g)
   259  				defer teardown(t, g)
   260  				setProviderID(t, g)
   261  
   262  				expectedErr := errors.New("Invalid instance")
   263  				asgSvc.EXPECT().ASGIfExists(gomock.Any()).Return(nil, nil).AnyTimes()
   264  				ec2Svc.EXPECT().GetLaunchTemplate(gomock.Any()).Return(nil, "", nil)
   265  				ec2Svc.EXPECT().DiscoverLaunchTemplateAMI(gomock.Any()).Return(nil, nil)
   266  				ec2Svc.EXPECT().CreateLaunchTemplate(gomock.Any(), gomock.Any(), gomock.Any()).Return("", expectedErr).AnyTimes()
   267  
   268  				_, err := reconciler.reconcileNormal(context.Background(), ms, cs, cs)
   269  				g.Expect(errors.Cause(err)).To(MatchError(expectedErr))
   270  			})
   271  		})
   272  
   273  		t.Run("externally managed annotation", func(t *testing.T) {
   274  			g := NewWithT(t)
   275  			setup(t, g)
   276  			defer teardown(t, g)
   277  
   278  			asg := expinfrav1.AutoScalingGroup{
   279  				Name:            "an-asg",
   280  				DesiredCapacity: pointer.Int32(1),
   281  			}
   282  			asgSvc.EXPECT().GetASGByName(gomock.Any()).Return(&asg, nil).AnyTimes()
   283  			asgSvc.EXPECT().UpdateASG(gomock.Any()).Return(nil).AnyTimes()
   284  			ec2Svc.EXPECT().GetLaunchTemplate(gomock.Any()).Return(nil, "", nil).AnyTimes()
   285  			ec2Svc.EXPECT().DiscoverLaunchTemplateAMI(gomock.Any()).Return(nil, nil).AnyTimes()
   286  			ec2Svc.EXPECT().CreateLaunchTemplate(gomock.Any(), gomock.Any(), gomock.Any()).Return("", nil).AnyTimes()
   287  			ms.MachinePool.Annotations = map[string]string{
   288  				scope.ReplicasManagedByAnnotation: scope.ExternalAutoscalerReplicasManagedByAnnotationValue,
   289  			}
   290  			ms.MachinePool.Spec.Replicas = pointer.Int32(0)
   291  
   292  			g.Expect(testEnv.Create(ctx, ms.MachinePool)).To(Succeed())
   293  
   294  			_, _ = reconciler.reconcileNormal(context.Background(), ms, cs, cs)
   295  			g.Expect(*ms.MachinePool.Spec.Replicas).To(Equal(int32(1)))
   296  		})
   297  	})
   298  
   299  	t.Run("Deleting an AWSMachinePool", func(t *testing.T) {
   300  		finalizer := func(t *testing.T, g *WithT) {
   301  			t.Helper()
   302  
   303  			ms.AWSMachinePool.Finalizers = []string{
   304  				expinfrav1.MachinePoolFinalizer,
   305  				metav1.FinalizerDeleteDependents,
   306  			}
   307  		}
   308  		t.Run("should exit immediately on an error state", func(t *testing.T) {
   309  			g := NewWithT(t)
   310  			setup(t, g)
   311  			defer teardown(t, g)
   312  			finalizer(t, g)
   313  
   314  			expectedErr := errors.New("no connection available ")
   315  			asgSvc.EXPECT().GetASGByName(gomock.Any()).Return(nil, expectedErr).AnyTimes()
   316  
   317  			_, err := reconciler.reconcileDelete(ms, cs, cs)
   318  			g.Expect(errors.Cause(err)).To(MatchError(expectedErr))
   319  		})
   320  		t.Run("should log and remove finalizer when no machinepool exists", func(t *testing.T) {
   321  			g := NewWithT(t)
   322  			setup(t, g)
   323  			defer teardown(t, g)
   324  			finalizer(t, g)
   325  
   326  			asgSvc.EXPECT().GetASGByName(gomock.Any()).Return(nil, nil)
   327  			ec2Svc.EXPECT().GetLaunchTemplate(gomock.Any()).Return(nil, "", nil).AnyTimes()
   328  
   329  			buf := new(bytes.Buffer)
   330  			klog.SetOutput(buf)
   331  
   332  			_, err := reconciler.reconcileDelete(ms, cs, cs)
   333  			g.Expect(err).To(BeNil())
   334  			g.Expect(buf.String()).To(ContainSubstring("Unable to locate ASG"))
   335  			g.Expect(ms.AWSMachinePool.Finalizers).To(ConsistOf(metav1.FinalizerDeleteDependents))
   336  			g.Eventually(recorder.Events).Should(Receive(ContainSubstring("NoASGFound")))
   337  		})
   338  		t.Run("should cause AWSMachinePool to go into NotReady", func(t *testing.T) {
   339  			g := NewWithT(t)
   340  			setup(t, g)
   341  			defer teardown(t, g)
   342  			finalizer(t, g)
   343  
   344  			inProgressASG := expinfrav1.AutoScalingGroup{
   345  				Name:   "an-asg-that-is-currently-being-deleted",
   346  				Status: expinfrav1.ASGStatusDeleteInProgress,
   347  			}
   348  			asgSvc.EXPECT().GetASGByName(gomock.Any()).Return(&inProgressASG, nil)
   349  			ec2Svc.EXPECT().GetLaunchTemplate(gomock.Any()).Return(nil, "", nil).AnyTimes()
   350  
   351  			buf := new(bytes.Buffer)
   352  			klog.SetOutput(buf)
   353  			_, err := reconciler.reconcileDelete(ms, cs, cs)
   354  			g.Expect(err).To(BeNil())
   355  			g.Expect(ms.AWSMachinePool.Status.Ready).To(Equal(false))
   356  			g.Eventually(recorder.Events).Should(Receive(ContainSubstring("DeletionInProgress")))
   357  		})
   358  	})
   359  }
   360  
   361  //TODO: This was taken from awsmachine_controller_test, i think it should be moved to elsewhere in both locations like test/helpers
   362  
   363  type conditionAssertion struct {
   364  	conditionType clusterv1.ConditionType
   365  	status        corev1.ConditionStatus
   366  	severity      clusterv1.ConditionSeverity
   367  	reason        string
   368  }
   369  
   370  func expectConditions(g *WithT, m *expinfrav1.AWSMachinePool, expected []conditionAssertion) {
   371  	g.Expect(len(m.Status.Conditions)).To(BeNumerically(">=", len(expected)), "number of conditions")
   372  	for _, c := range expected {
   373  		actual := conditions.Get(m, c.conditionType)
   374  		g.Expect(actual).To(Not(BeNil()))
   375  		g.Expect(actual.Type).To(Equal(c.conditionType))
   376  		g.Expect(actual.Status).To(Equal(c.status))
   377  		g.Expect(actual.Severity).To(Equal(c.severity))
   378  		g.Expect(actual.Reason).To(Equal(c.reason))
   379  	}
   380  }
   381  
   382  func setupCluster(clusterName string) (*scope.ClusterScope, error) {
   383  	scheme := runtime.NewScheme()
   384  	_ = infrav1.AddToScheme(scheme)
   385  	awsCluster := &infrav1.AWSCluster{
   386  		ObjectMeta: metav1.ObjectMeta{Name: "test"},
   387  		Spec:       infrav1.AWSClusterSpec{},
   388  	}
   389  	client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(awsCluster).Build()
   390  	return scope.NewClusterScope(scope.ClusterScopeParams{
   391  		Cluster: &clusterv1.Cluster{
   392  			ObjectMeta: metav1.ObjectMeta{Name: clusterName},
   393  		},
   394  		AWSCluster: awsCluster,
   395  		Client:     client,
   396  	})
   397  }
   398  
   399  func Test_asgNeedsUpdates(t *testing.T) {
   400  	type args struct {
   401  		machinePoolScope *scope.MachinePoolScope
   402  		existingASG      *expinfrav1.AutoScalingGroup
   403  	}
   404  	tests := []struct {
   405  		name string
   406  		args args
   407  		want bool
   408  	}{
   409  		{
   410  			name: "replicas != asg.desiredCapacity",
   411  			args: args{
   412  				machinePoolScope: &scope.MachinePoolScope{
   413  					MachinePool: &expclusterv1.MachinePool{
   414  						Spec: expclusterv1.MachinePoolSpec{
   415  							Replicas: pointer.Int32(0),
   416  						},
   417  					},
   418  				},
   419  				existingASG: &expinfrav1.AutoScalingGroup{
   420  					DesiredCapacity: pointer.Int32(1),
   421  				},
   422  			},
   423  			want: true,
   424  		},
   425  		{
   426  			name: "replicas (nil) != asg.desiredCapacity",
   427  			args: args{
   428  				machinePoolScope: &scope.MachinePoolScope{
   429  					MachinePool: &expclusterv1.MachinePool{
   430  						Spec: expclusterv1.MachinePoolSpec{
   431  							Replicas: nil,
   432  						},
   433  					},
   434  				},
   435  				existingASG: &expinfrav1.AutoScalingGroup{
   436  					DesiredCapacity: pointer.Int32(1),
   437  				},
   438  			},
   439  			want: true,
   440  		},
   441  		{
   442  			name: "replicas != asg.desiredCapacity (nil)",
   443  			args: args{
   444  				machinePoolScope: &scope.MachinePoolScope{
   445  					MachinePool: &expclusterv1.MachinePool{
   446  						Spec: expclusterv1.MachinePoolSpec{
   447  							Replicas: pointer.Int32(0),
   448  						},
   449  					},
   450  				},
   451  				existingASG: &expinfrav1.AutoScalingGroup{
   452  					DesiredCapacity: nil,
   453  				},
   454  			},
   455  			want: true,
   456  		},
   457  		{
   458  			name: "maxSize != asg.maxSize",
   459  			args: args{
   460  				machinePoolScope: &scope.MachinePoolScope{
   461  					MachinePool: &expclusterv1.MachinePool{
   462  						Spec: expclusterv1.MachinePoolSpec{
   463  							Replicas: pointer.Int32(1),
   464  						},
   465  					},
   466  					AWSMachinePool: &expinfrav1.AWSMachinePool{
   467  						Spec: expinfrav1.AWSMachinePoolSpec{
   468  							MaxSize: 1,
   469  						},
   470  					},
   471  				},
   472  				existingASG: &expinfrav1.AutoScalingGroup{
   473  					DesiredCapacity: pointer.Int32(1),
   474  					MaxSize:         2,
   475  				},
   476  			},
   477  			want: true,
   478  		},
   479  		{
   480  			name: "minSize != asg.minSize",
   481  			args: args{
   482  				machinePoolScope: &scope.MachinePoolScope{
   483  					MachinePool: &expclusterv1.MachinePool{
   484  						Spec: expclusterv1.MachinePoolSpec{
   485  							Replicas: pointer.Int32(1),
   486  						},
   487  					},
   488  					AWSMachinePool: &expinfrav1.AWSMachinePool{
   489  						Spec: expinfrav1.AWSMachinePoolSpec{
   490  							MaxSize: 2,
   491  							MinSize: 0,
   492  						},
   493  					},
   494  				},
   495  				existingASG: &expinfrav1.AutoScalingGroup{
   496  					DesiredCapacity: pointer.Int32(1),
   497  					MaxSize:         2,
   498  					MinSize:         1,
   499  				},
   500  			},
   501  			want: true,
   502  		},
   503  		{
   504  			name: "capacityRebalance != asg.capacityRebalance",
   505  			args: args{
   506  				machinePoolScope: &scope.MachinePoolScope{
   507  					MachinePool: &expclusterv1.MachinePool{
   508  						Spec: expclusterv1.MachinePoolSpec{
   509  							Replicas: pointer.Int32(1),
   510  						},
   511  					},
   512  					AWSMachinePool: &expinfrav1.AWSMachinePool{
   513  						Spec: expinfrav1.AWSMachinePoolSpec{
   514  							MaxSize:           2,
   515  							MinSize:           0,
   516  							CapacityRebalance: true,
   517  						},
   518  					},
   519  				},
   520  				existingASG: &expinfrav1.AutoScalingGroup{
   521  					DesiredCapacity:   pointer.Int32(1),
   522  					MaxSize:           2,
   523  					MinSize:           0,
   524  					CapacityRebalance: false,
   525  				},
   526  			},
   527  			want: true,
   528  		},
   529  		{
   530  			name: "MixedInstancesPolicy != asg.MixedInstancesPolicy",
   531  			args: args{
   532  				machinePoolScope: &scope.MachinePoolScope{
   533  					MachinePool: &expclusterv1.MachinePool{
   534  						Spec: expclusterv1.MachinePoolSpec{
   535  							Replicas: pointer.Int32(1),
   536  						},
   537  					},
   538  					AWSMachinePool: &expinfrav1.AWSMachinePool{
   539  						Spec: expinfrav1.AWSMachinePoolSpec{
   540  							MaxSize:           2,
   541  							MinSize:           0,
   542  							CapacityRebalance: true,
   543  							MixedInstancesPolicy: &expinfrav1.MixedInstancesPolicy{
   544  								InstancesDistribution: &expinfrav1.InstancesDistribution{
   545  									OnDemandAllocationStrategy: expinfrav1.OnDemandAllocationStrategyPrioritized,
   546  								},
   547  								Overrides: nil,
   548  							},
   549  						},
   550  					},
   551  					Logger: logr.Discard(),
   552  				},
   553  				existingASG: &expinfrav1.AutoScalingGroup{
   554  					DesiredCapacity:      pointer.Int32(1),
   555  					MaxSize:              2,
   556  					MinSize:              0,
   557  					CapacityRebalance:    true,
   558  					MixedInstancesPolicy: &expinfrav1.MixedInstancesPolicy{},
   559  				},
   560  			},
   561  			want: true,
   562  		},
   563  		{
   564  			name: "all matches",
   565  			args: args{
   566  				machinePoolScope: &scope.MachinePoolScope{
   567  					MachinePool: &expclusterv1.MachinePool{
   568  						Spec: expclusterv1.MachinePoolSpec{
   569  							Replicas: pointer.Int32(1),
   570  						},
   571  					},
   572  					AWSMachinePool: &expinfrav1.AWSMachinePool{
   573  						Spec: expinfrav1.AWSMachinePoolSpec{
   574  							MaxSize:           2,
   575  							MinSize:           0,
   576  							CapacityRebalance: true,
   577  							MixedInstancesPolicy: &expinfrav1.MixedInstancesPolicy{
   578  								InstancesDistribution: &expinfrav1.InstancesDistribution{
   579  									OnDemandAllocationStrategy: expinfrav1.OnDemandAllocationStrategyPrioritized,
   580  								},
   581  								Overrides: nil,
   582  							},
   583  						},
   584  					},
   585  				},
   586  				existingASG: &expinfrav1.AutoScalingGroup{
   587  					DesiredCapacity:   pointer.Int32(1),
   588  					MaxSize:           2,
   589  					MinSize:           0,
   590  					CapacityRebalance: true,
   591  					MixedInstancesPolicy: &expinfrav1.MixedInstancesPolicy{
   592  						InstancesDistribution: &expinfrav1.InstancesDistribution{
   593  							OnDemandAllocationStrategy: expinfrav1.OnDemandAllocationStrategyPrioritized,
   594  						},
   595  						Overrides: nil,
   596  					},
   597  				},
   598  			},
   599  			want: false,
   600  		},
   601  		{
   602  			name: "externally managed annotation ignores difference between desiredCapacity and replicas",
   603  			args: args{
   604  				machinePoolScope: &scope.MachinePoolScope{
   605  					MachinePool: &expclusterv1.MachinePool{
   606  						ObjectMeta: metav1.ObjectMeta{
   607  							Annotations: map[string]string{
   608  								scope.ReplicasManagedByAnnotation: scope.ExternalAutoscalerReplicasManagedByAnnotationValue,
   609  							},
   610  						},
   611  						Spec: expclusterv1.MachinePoolSpec{
   612  							Replicas: pointer.Int32(0),
   613  						},
   614  					},
   615  					AWSMachinePool: &expinfrav1.AWSMachinePool{
   616  						Spec: expinfrav1.AWSMachinePoolSpec{},
   617  					},
   618  				},
   619  				existingASG: &expinfrav1.AutoScalingGroup{
   620  					DesiredCapacity: pointer.Int32(1),
   621  				},
   622  			},
   623  			want: false,
   624  		},
   625  		{
   626  			name: "without externally managed annotation ignores difference between desiredCapacity and replicas",
   627  			args: args{
   628  				machinePoolScope: &scope.MachinePoolScope{
   629  					MachinePool: &expclusterv1.MachinePool{
   630  						Spec: expclusterv1.MachinePoolSpec{
   631  							Replicas: pointer.Int32(0),
   632  						},
   633  					},
   634  					AWSMachinePool: &expinfrav1.AWSMachinePool{
   635  						Spec: expinfrav1.AWSMachinePoolSpec{},
   636  					},
   637  				},
   638  				existingASG: &expinfrav1.AutoScalingGroup{
   639  					DesiredCapacity: pointer.Int32(1),
   640  				},
   641  			},
   642  			want: true,
   643  		},
   644  	}
   645  	for _, tt := range tests {
   646  		t.Run(tt.name, func(t *testing.T) {
   647  			g := NewWithT(t)
   648  			g.Expect(asgNeedsUpdates(tt.args.machinePoolScope, tt.args.existingASG)).To(Equal(tt.want))
   649  		})
   650  	}
   651  }