diff --git a/exp/controllers/awsmachinepool_controller_test.go b/exp/controllers/awsmachinepool_controller_test.go index af03c76e90..028e9ad524 100644 --- a/exp/controllers/awsmachinepool_controller_test.go +++ b/exp/controllers/awsmachinepool_controller_test.go @@ -749,60 +749,6 @@ func TestAWSMachinePoolReconciler(t *testing.T) { g.Expect(err).To(Succeed()) }) - t.Run("launch template and ASG exist and only AMI ID changed", func(t *testing.T) { - g := NewWithT(t) - setup(t, g) - reconciler.reconcileServiceFactory = nil // use real implementation, but keep EC2 calls mocked (`ec2ServiceFactory`) - reconSvc = nil // not used - defer teardown(t, g) - - // Latest ID and version already stored, no need to retrieve it - ms.AWSMachinePool.Status.LaunchTemplateID = launchTemplateIDExisting - ms.AWSMachinePool.Status.LaunchTemplateVersion = ptr.To[string]("1") - - ec2Svc.EXPECT().GetLaunchTemplate(gomock.Eq("test")).Return( - &expinfrav1.AWSLaunchTemplate{ - Name: "test", - AMI: infrav1.AMIReference{ - ID: ptr.To[string]("ami-existing"), - }, - }, - // No change to user data - userdata.ComputeHash([]byte("shell-script")), - &userDataSecretKey, - nil) - ec2Svc.EXPECT().DiscoverLaunchTemplateAMI(gomock.Any()).Return(ptr.To[string]("ami-different"), nil) - ec2Svc.EXPECT().LaunchTemplateNeedsUpdate(gomock.Any(), gomock.Any(), gomock.Any()).Return(false, nil) - asgSvc.EXPECT().CanStartASGInstanceRefresh(gomock.Any()).Return(true, nil) - ec2Svc.EXPECT().PruneLaunchTemplateVersions(gomock.Any()).Return(nil) - ec2Svc.EXPECT().CreateLaunchTemplateVersion(gomock.Any(), gomock.Any(), gomock.Eq(ptr.To[string]("ami-different")), gomock.Eq(apimachinerytypes.NamespacedName{Namespace: "default", Name: "bootstrap-data"}), gomock.Any()).Return(nil) - ec2Svc.EXPECT().GetLaunchTemplateLatestVersion(gomock.Any()).Return("2", nil) - // AMI change should trigger rolling out new nodes - asgSvc.EXPECT().StartASGInstanceRefresh(gomock.Any()) - - asgSvc.EXPECT().GetASGByName(gomock.Any()).DoAndReturn(func(scope *scope.MachinePoolScope) (*expinfrav1.AutoScalingGroup, error) { - g.Expect(scope.Name()).To(Equal("test")) - - // No difference to `AWSMachinePool.spec` - return &expinfrav1.AutoScalingGroup{ - Name: scope.Name(), - Subnets: []string{ - "subnet-1", - }, - MinSize: awsMachinePool.Spec.MinSize, - MaxSize: awsMachinePool.Spec.MaxSize, - MixedInstancesPolicy: awsMachinePool.Spec.MixedInstancesPolicy.DeepCopy(), - }, nil - }) - asgSvc.EXPECT().DescribeLifecycleHooks(gomock.Any()).Return(nil, nil) - asgSvc.EXPECT().SubnetIDs(gomock.Any()).Return([]string{"subnet-1"}, nil) // no change - // No changes, so there must not be an ASG update! - asgSvc.EXPECT().UpdateASG(gomock.Any()).Times(0) - - _, err := reconciler.reconcileNormal(context.Background(), ms, cs, cs) - g.Expect(err).To(Succeed()) - }) - t.Run("launch template and ASG exist and only bootstrap data secret name changed", func(t *testing.T) { g := NewWithT(t) setup(t, g) diff --git a/pkg/cloud/services/ec2/launchtemplate.go b/pkg/cloud/services/ec2/launchtemplate.go index ead242003a..9e0ff783a5 100644 --- a/pkg/cloud/services/ec2/launchtemplate.go +++ b/pkg/cloud/services/ec2/launchtemplate.go @@ -71,21 +71,21 @@ func (s *Service) ReconcileLaunchTemplate( bootstrapDataHash := userdata.ComputeHash(bootstrapData) scope.Info("checking for existing launch template") - launchTemplate, launchTemplateUserDataHash, launchTemplateUserDataSecretKey, err := ec2svc.GetLaunchTemplate(scope.LaunchTemplateName()) + existingLaunchTemplate, launchTemplateUserDataHash, launchTemplateUserDataSecretKey, err := ec2svc.GetLaunchTemplate(scope.LaunchTemplateName()) if err != nil { conditions.MarkUnknown(scope.GetSetter(), expinfrav1.LaunchTemplateReadyCondition, expinfrav1.LaunchTemplateNotFoundReason, "%s", err.Error()) return err } - imageID, err := ec2svc.DiscoverLaunchTemplateAMI(scope) + currentlyUsedAMIID, err := ec2svc.DiscoverLaunchTemplateAMI(scope) if err != nil { conditions.MarkFalse(scope.GetSetter(), expinfrav1.LaunchTemplateReadyCondition, expinfrav1.LaunchTemplateCreateFailedReason, clusterv1.ConditionSeverityError, "%s", err.Error()) return err } - if launchTemplate == nil { + if existingLaunchTemplate == nil { scope.Info("no existing launch template found, creating") - launchTemplateID, err := ec2svc.CreateLaunchTemplate(scope, imageID, *bootstrapDataSecretKey, bootstrapData) + launchTemplateID, err := ec2svc.CreateLaunchTemplate(scope, currentlyUsedAMIID, *bootstrapDataSecretKey, bootstrapData) if err != nil { conditions.MarkFalse(scope.GetSetter(), expinfrav1.LaunchTemplateReadyCondition, expinfrav1.LaunchTemplateCreateFailedReason, clusterv1.ConditionSeverityError, "%s", err.Error()) return err @@ -119,21 +119,11 @@ func (s *Service) ReconcileLaunchTemplate( } } - annotation, err := MachinePoolAnnotationJSON(scope, TagsLastAppliedAnnotation) - if err != nil { - return err - } - - // Check if the instance tags were changed. If they were, create a new LaunchTemplate. - tagsChanged, _, _, _ := tagsChanged(annotation, scope.AdditionalTags()) //nolint:dogsled - - needsUpdate, err := ec2svc.LaunchTemplateNeedsUpdate(scope, scope.GetLaunchTemplate(), launchTemplate) + needsUpdate, err := ec2svc.LaunchTemplateNeedsUpdate(scope, existingLaunchTemplate, currentlyUsedAMIID) if err != nil { return err } - amiChanged := *imageID != *launchTemplate.AMI.ID - // `launchTemplateUserDataSecretKey` can be nil since it comes from a tag on the launch template // which may not exist in older launch templates created by older CAPA versions. // On change, we trigger instance refresh (rollout of new nodes). Therefore, do not consider it a change if the @@ -142,7 +132,7 @@ func (s *Service) ReconcileLaunchTemplate( userDataSecretKeyChanged := launchTemplateUserDataSecretKey != nil && bootstrapDataSecretKey.String() != launchTemplateUserDataSecretKey.String() launchTemplateNeedsUserDataSecretKeyTag := launchTemplateUserDataSecretKey == nil - if needsUpdate || tagsChanged || amiChanged || userDataSecretKeyChanged { + if needsUpdate || userDataSecretKeyChanged { canUpdate, err := canUpdateLaunchTemplate() if err != nil { return err @@ -157,14 +147,14 @@ func (s *Service) ReconcileLaunchTemplate( // Create a new launch template version if there's a difference in configuration, tags, // userdata, OR we've discovered a new AMI ID. - if needsUpdate || tagsChanged || amiChanged || userDataHashChanged || userDataSecretKeyChanged || launchTemplateNeedsUserDataSecretKeyTag { - scope.Info("creating new version for launch template", "existing", launchTemplate, "incoming", scope.GetLaunchTemplate(), "needsUpdate", needsUpdate, "tagsChanged", tagsChanged, "amiChanged", amiChanged, "userDataHashChanged", userDataHashChanged, "userDataSecretKeyChanged", userDataSecretKeyChanged) + if needsUpdate || userDataHashChanged || userDataSecretKeyChanged || launchTemplateNeedsUserDataSecretKeyTag { + scope.Info("creating new version for launch template", "existing", existingLaunchTemplate, "incoming", scope.GetLaunchTemplate(), "needsUpdate", needsUpdate, "userDataHashChanged", userDataHashChanged, "userDataSecretKeyChanged", userDataSecretKeyChanged) // There is a limit to the number of Launch Template Versions. // We ensure that the number of versions does not grow without bound by following a simple rule: Before we create a new version, we delete one old version, if there is at least one old version that is not in use. if err := ec2svc.PruneLaunchTemplateVersions(scope.GetLaunchTemplateIDStatus()); err != nil { return err } - if err := ec2svc.CreateLaunchTemplateVersion(scope.GetLaunchTemplateIDStatus(), scope, imageID, *bootstrapDataSecretKey, bootstrapData); err != nil { + if err := ec2svc.CreateLaunchTemplateVersion(scope.GetLaunchTemplateIDStatus(), scope, currentlyUsedAMIID, *bootstrapDataSecretKey, bootstrapData); err != nil { return err } version, err := ec2svc.GetLaunchTemplateLatestVersion(scope.GetLaunchTemplateIDStatus()) @@ -178,7 +168,7 @@ func (s *Service) ReconcileLaunchTemplate( } } - if needsUpdate || tagsChanged || amiChanged || userDataSecretKeyChanged { + if needsUpdate || userDataSecretKeyChanged { if err := runPostLaunchTemplateUpdateOperation(); err != nil { conditions.MarkFalse(scope.GetSetter(), expinfrav1.PostLaunchTemplateUpdateOperationCondition, expinfrav1.PostLaunchTemplateUpdateOperationFailedReason, clusterv1.ConditionSeverityError, "%s", err.Error()) return err @@ -788,39 +778,42 @@ func (s *Service) SDKToLaunchTemplate(d *ec2.LaunchTemplateVersion) (*expinfrav1 } // LaunchTemplateNeedsUpdate checks if a new launch template version is needed. -// -// FIXME(dlipovetsky): This check should account for changed userdata, but does not yet do so. -// Although userdata is stored in an EC2 Launch Template, it is not a field of AWSLaunchTemplate. -func (s *Service) LaunchTemplateNeedsUpdate(scope scope.LaunchTemplateScope, incoming *expinfrav1.AWSLaunchTemplate, existing *expinfrav1.AWSLaunchTemplate) (bool, error) { - if incoming.IamInstanceProfile != existing.IamInstanceProfile { +func (s *Service) LaunchTemplateNeedsUpdate(scope scope.LaunchTemplateScope, existingLaunchTemplate *expinfrav1.AWSLaunchTemplate, currentlyUsedAMIID *string) (bool, error) { + incomingLaunchTemplate := scope.GetLaunchTemplate() + + if incomingLaunchTemplate.IamInstanceProfile != existingLaunchTemplate.IamInstanceProfile { return true, nil } - if incoming.InstanceType != existing.InstanceType { + if incomingLaunchTemplate.InstanceType != existingLaunchTemplate.InstanceType { return true, nil } - if !cmp.Equal(incoming.InstanceMetadataOptions, existing.InstanceMetadataOptions) { + if !cmp.Equal(incomingLaunchTemplate.InstanceMetadataOptions, existingLaunchTemplate.InstanceMetadataOptions) { return true, nil } - if !cmp.Equal(incoming.SpotMarketOptions, existing.SpotMarketOptions) { + if !cmp.Equal(incomingLaunchTemplate.SpotMarketOptions, existingLaunchTemplate.SpotMarketOptions) { return true, nil } - if !cmp.Equal(incoming.CapacityReservationID, existing.CapacityReservationID) { + if incomingLaunchTemplate.AMI.ID != nil && *incomingLaunchTemplate.AMI.ID != *currentlyUsedAMIID { return true, nil } - if !cmp.Equal(incoming.PrivateDNSName, existing.PrivateDNSName) { + if !cmp.Equal(incomingLaunchTemplate.CapacityReservationID, existingLaunchTemplate.CapacityReservationID) { return true, nil } - if !cmp.Equal(incoming.SSHKeyName, existing.SSHKeyName) { + if !cmp.Equal(incomingLaunchTemplate.PrivateDNSName, existingLaunchTemplate.PrivateDNSName) { return true, nil } - incomingIDs, err := s.GetAdditionalSecurityGroupsIDs(incoming.AdditionalSecurityGroups) + if !cmp.Equal(incomingLaunchTemplate.SSHKeyName, existingLaunchTemplate.SSHKeyName) { + return true, nil + } + + incomingIDs, err := s.GetAdditionalSecurityGroupsIDs(incomingLaunchTemplate.AdditionalSecurityGroups) if err != nil { return false, err } @@ -831,7 +824,7 @@ func (s *Service) LaunchTemplateNeedsUpdate(scope scope.LaunchTemplateScope, inc } incomingIDs = append(incomingIDs, coreIDs...) - existingIDs, err := s.GetAdditionalSecurityGroupsIDs(existing.AdditionalSecurityGroups) + existingIDs, err := s.GetAdditionalSecurityGroupsIDs(existingLaunchTemplate.AdditionalSecurityGroups) if err != nil { return false, err } @@ -842,6 +835,16 @@ func (s *Service) LaunchTemplateNeedsUpdate(scope scope.LaunchTemplateScope, inc return true, nil } + annotation, err := MachinePoolAnnotationJSON(scope, TagsLastAppliedAnnotation) + if err != nil { + return false, err + } + //nolint:dogsled + tagsHaveChanged, _, _, _ := tagsChanged(annotation, scope.AdditionalTags()) + if tagsHaveChanged { + return true, nil + } + return false, nil } diff --git a/pkg/cloud/services/ec2/launchtemplate_test.go b/pkg/cloud/services/ec2/launchtemplate_test.go index 99ed68bf45..22b547a336 100644 --- a/pkg/cloud/services/ec2/launchtemplate_test.go +++ b/pkg/cloud/services/ec2/launchtemplate_test.go @@ -566,19 +566,42 @@ func TestServiceLaunchTemplateNeedsUpdate(t *testing.T) { defer mockCtrl.Finish() tests := []struct { - name string - incoming *expinfrav1.AWSLaunchTemplate - existing *expinfrav1.AWSLaunchTemplate - expect func(m *mocks.MockEC2APIMockRecorder) - want bool - wantErr bool + name string + machinePoolScope *scope.MachinePoolScope + existingLaunchTemplate *expinfrav1.AWSLaunchTemplate + expect func(m *mocks.MockEC2APIMockRecorder) + want bool + wantErr bool }{ { name: "only core security groups, order shouldn't matter", - incoming: &expinfrav1.AWSLaunchTemplate{ - AdditionalSecurityGroups: []infrav1.AWSResourceReference{}, + machinePoolScope: &scope.MachinePoolScope{ + AWSMachinePool: &expinfrav1.AWSMachinePool{ + Spec: expinfrav1.AWSMachinePoolSpec{ + AWSLaunchTemplate: expinfrav1.AWSLaunchTemplate{ + AdditionalSecurityGroups: []infrav1.AWSResourceReference{}, + }, + }, + }, + InfraCluster: &scope.ClusterScope{ + AWSCluster: &infrav1.AWSCluster{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Status: infrav1.AWSClusterStatus{ + Network: infrav1.NetworkStatus{ + SecurityGroups: map[infrav1.SecurityGroupRole]infrav1.SecurityGroup{ + infrav1.SecurityGroupNode: { + ID: "sg-111", + }, + infrav1.SecurityGroupLB: { + ID: "sg-222", + }, + }, + }, + }, + }, + }, }, - existing: &expinfrav1.AWSLaunchTemplate{ + existingLaunchTemplate: &expinfrav1.AWSLaunchTemplate{ AdditionalSecurityGroups: []infrav1.AWSResourceReference{ {ID: aws.String("sg-222")}, {ID: aws.String("sg-111")}, @@ -589,12 +612,35 @@ func TestServiceLaunchTemplateNeedsUpdate(t *testing.T) { }, { name: "the same security groups", - incoming: &expinfrav1.AWSLaunchTemplate{ - AdditionalSecurityGroups: []infrav1.AWSResourceReference{ - {ID: aws.String("sg-999")}, + machinePoolScope: &scope.MachinePoolScope{ + AWSMachinePool: &expinfrav1.AWSMachinePool{ + Spec: expinfrav1.AWSMachinePoolSpec{ + AWSLaunchTemplate: expinfrav1.AWSLaunchTemplate{ + AdditionalSecurityGroups: []infrav1.AWSResourceReference{ + {ID: aws.String("sg-999")}, + }, + }, + }, + }, + InfraCluster: &scope.ClusterScope{ + AWSCluster: &infrav1.AWSCluster{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Status: infrav1.AWSClusterStatus{ + Network: infrav1.NetworkStatus{ + SecurityGroups: map[infrav1.SecurityGroupRole]infrav1.SecurityGroup{ + infrav1.SecurityGroupNode: { + ID: "sg-111", + }, + infrav1.SecurityGroupLB: { + ID: "sg-222", + }, + }, + }, + }, + }, }, }, - existing: &expinfrav1.AWSLaunchTemplate{ + existingLaunchTemplate: &expinfrav1.AWSLaunchTemplate{ AdditionalSecurityGroups: []infrav1.AWSResourceReference{ {ID: aws.String("sg-111")}, {ID: aws.String("sg-222")}, @@ -606,12 +652,35 @@ func TestServiceLaunchTemplateNeedsUpdate(t *testing.T) { }, { name: "core security group removed externally", - incoming: &expinfrav1.AWSLaunchTemplate{ - AdditionalSecurityGroups: []infrav1.AWSResourceReference{ - {ID: aws.String("sg-999")}, + machinePoolScope: &scope.MachinePoolScope{ + AWSMachinePool: &expinfrav1.AWSMachinePool{ + Spec: expinfrav1.AWSMachinePoolSpec{ + AWSLaunchTemplate: expinfrav1.AWSLaunchTemplate{ + AdditionalSecurityGroups: []infrav1.AWSResourceReference{ + {ID: aws.String("sg-999")}, + }, + }, + }, + }, + InfraCluster: &scope.ClusterScope{ + AWSCluster: &infrav1.AWSCluster{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Status: infrav1.AWSClusterStatus{ + Network: infrav1.NetworkStatus{ + SecurityGroups: map[infrav1.SecurityGroupRole]infrav1.SecurityGroup{ + infrav1.SecurityGroupNode: { + ID: "sg-111", + }, + infrav1.SecurityGroupLB: { + ID: "sg-222", + }, + }, + }, + }, + }, }, }, - existing: &expinfrav1.AWSLaunchTemplate{ + existingLaunchTemplate: &expinfrav1.AWSLaunchTemplate{ AdditionalSecurityGroups: []infrav1.AWSResourceReference{ {ID: aws.String("sg-222")}, {ID: aws.String("sg-999")}, @@ -622,13 +691,36 @@ func TestServiceLaunchTemplateNeedsUpdate(t *testing.T) { }, { name: "new additional security group", - incoming: &expinfrav1.AWSLaunchTemplate{ - AdditionalSecurityGroups: []infrav1.AWSResourceReference{ - {ID: aws.String("sg-999")}, - {ID: aws.String("sg-000")}, + machinePoolScope: &scope.MachinePoolScope{ + AWSMachinePool: &expinfrav1.AWSMachinePool{ + Spec: expinfrav1.AWSMachinePoolSpec{ + AWSLaunchTemplate: expinfrav1.AWSLaunchTemplate{ + AdditionalSecurityGroups: []infrav1.AWSResourceReference{ + {ID: aws.String("sg-999")}, + {ID: aws.String("sg-000")}, + }, + }, + }, + }, + InfraCluster: &scope.ClusterScope{ + AWSCluster: &infrav1.AWSCluster{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Status: infrav1.AWSClusterStatus{ + Network: infrav1.NetworkStatus{ + SecurityGroups: map[infrav1.SecurityGroupRole]infrav1.SecurityGroup{ + infrav1.SecurityGroupNode: { + ID: "sg-111", + }, + infrav1.SecurityGroupLB: { + ID: "sg-222", + }, + }, + }, + }, + }, }, }, - existing: &expinfrav1.AWSLaunchTemplate{ + existingLaunchTemplate: &expinfrav1.AWSLaunchTemplate{ AdditionalSecurityGroups: []infrav1.AWSResourceReference{ {ID: aws.String("sg-111")}, {ID: aws.String("sg-222")}, @@ -640,10 +732,33 @@ func TestServiceLaunchTemplateNeedsUpdate(t *testing.T) { }, { name: "Should return true if incoming IamInstanceProfile is not same as existing IamInstanceProfile", - incoming: &expinfrav1.AWSLaunchTemplate{ - IamInstanceProfile: DefaultAmiNameFormat, + machinePoolScope: &scope.MachinePoolScope{ + AWSMachinePool: &expinfrav1.AWSMachinePool{ + Spec: expinfrav1.AWSMachinePoolSpec{ + AWSLaunchTemplate: expinfrav1.AWSLaunchTemplate{ + IamInstanceProfile: DefaultAmiNameFormat, + }, + }, + }, + InfraCluster: &scope.ClusterScope{ + AWSCluster: &infrav1.AWSCluster{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Status: infrav1.AWSClusterStatus{ + Network: infrav1.NetworkStatus{ + SecurityGroups: map[infrav1.SecurityGroupRole]infrav1.SecurityGroup{ + infrav1.SecurityGroupNode: { + ID: "sg-111", + }, + infrav1.SecurityGroupLB: { + ID: "sg-222", + }, + }, + }, + }, + }, + }, }, - existing: &expinfrav1.AWSLaunchTemplate{ + existingLaunchTemplate: &expinfrav1.AWSLaunchTemplate{ AdditionalSecurityGroups: []infrav1.AWSResourceReference{ {ID: aws.String("sg-111")}, {ID: aws.String("sg-222")}, @@ -654,10 +769,33 @@ func TestServiceLaunchTemplateNeedsUpdate(t *testing.T) { }, { name: "Should return true if incoming InstanceType is not same as existing InstanceType", - incoming: &expinfrav1.AWSLaunchTemplate{ - InstanceType: "t3.micro", + machinePoolScope: &scope.MachinePoolScope{ + AWSMachinePool: &expinfrav1.AWSMachinePool{ + Spec: expinfrav1.AWSMachinePoolSpec{ + AWSLaunchTemplate: expinfrav1.AWSLaunchTemplate{ + InstanceType: "t3.micro", + }, + }, + }, + InfraCluster: &scope.ClusterScope{ + AWSCluster: &infrav1.AWSCluster{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Status: infrav1.AWSClusterStatus{ + Network: infrav1.NetworkStatus{ + SecurityGroups: map[infrav1.SecurityGroupRole]infrav1.SecurityGroup{ + infrav1.SecurityGroupNode: { + ID: "sg-111", + }, + infrav1.SecurityGroupLB: { + ID: "sg-222", + }, + }, + }, + }, + }, + }, }, - existing: &expinfrav1.AWSLaunchTemplate{ + existingLaunchTemplate: &expinfrav1.AWSLaunchTemplate{ AdditionalSecurityGroups: []infrav1.AWSResourceReference{ {ID: aws.String("sg-111")}, {ID: aws.String("sg-222")}, @@ -668,12 +806,35 @@ func TestServiceLaunchTemplateNeedsUpdate(t *testing.T) { }, { name: "new additional security group with filters", - incoming: &expinfrav1.AWSLaunchTemplate{ - AdditionalSecurityGroups: []infrav1.AWSResourceReference{ - {Filters: []infrav1.Filter{{Name: "sg-1", Values: []string{"test-1"}}}}, + machinePoolScope: &scope.MachinePoolScope{ + AWSMachinePool: &expinfrav1.AWSMachinePool{ + Spec: expinfrav1.AWSMachinePoolSpec{ + AWSLaunchTemplate: expinfrav1.AWSLaunchTemplate{ + AdditionalSecurityGroups: []infrav1.AWSResourceReference{ + {Filters: []infrav1.Filter{{Name: "sg-1", Values: []string{"test-1"}}}}, + }, + }, + }, + }, + InfraCluster: &scope.ClusterScope{ + AWSCluster: &infrav1.AWSCluster{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Status: infrav1.AWSClusterStatus{ + Network: infrav1.NetworkStatus{ + SecurityGroups: map[infrav1.SecurityGroupRole]infrav1.SecurityGroup{ + infrav1.SecurityGroupNode: { + ID: "sg-111", + }, + infrav1.SecurityGroupLB: { + ID: "sg-222", + }, + }, + }, + }, + }, }, }, - existing: &expinfrav1.AWSLaunchTemplate{ + existingLaunchTemplate: &expinfrav1.AWSLaunchTemplate{ AdditionalSecurityGroups: []infrav1.AWSResourceReference{ {Filters: []infrav1.Filter{{Name: "sg-2", Values: []string{"test-2"}}}}, }, @@ -689,13 +850,36 @@ func TestServiceLaunchTemplateNeedsUpdate(t *testing.T) { }, { name: "new launch template instance metadata options, requiring IMDSv2", - incoming: &expinfrav1.AWSLaunchTemplate{ - InstanceMetadataOptions: &infrav1.InstanceMetadataOptions{ - HTTPPutResponseHopLimit: 1, - HTTPTokens: infrav1.HTTPTokensStateRequired, + machinePoolScope: &scope.MachinePoolScope{ + AWSMachinePool: &expinfrav1.AWSMachinePool{ + Spec: expinfrav1.AWSMachinePoolSpec{ + AWSLaunchTemplate: expinfrav1.AWSLaunchTemplate{ + InstanceMetadataOptions: &infrav1.InstanceMetadataOptions{ + HTTPPutResponseHopLimit: 1, + HTTPTokens: infrav1.HTTPTokensStateRequired, + }, + }, + }, + }, + InfraCluster: &scope.ClusterScope{ + AWSCluster: &infrav1.AWSCluster{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Status: infrav1.AWSClusterStatus{ + Network: infrav1.NetworkStatus{ + SecurityGroups: map[infrav1.SecurityGroupRole]infrav1.SecurityGroup{ + infrav1.SecurityGroupNode: { + ID: "sg-111", + }, + infrav1.SecurityGroupLB: { + ID: "sg-222", + }, + }, + }, + }, + }, }, }, - existing: &expinfrav1.AWSLaunchTemplate{ + existingLaunchTemplate: &expinfrav1.AWSLaunchTemplate{ AdditionalSecurityGroups: []infrav1.AWSResourceReference{ {ID: aws.String("sg-111")}, {ID: aws.String("sg-222")}, @@ -705,9 +889,32 @@ func TestServiceLaunchTemplateNeedsUpdate(t *testing.T) { wantErr: false, }, { - name: "new launch template instance metadata options, removing IMDSv2 requirement", - incoming: &expinfrav1.AWSLaunchTemplate{}, - existing: &expinfrav1.AWSLaunchTemplate{ + name: "new launch template instance metadata options, removing IMDSv2 requirement", + machinePoolScope: &scope.MachinePoolScope{ + AWSMachinePool: &expinfrav1.AWSMachinePool{ + Spec: expinfrav1.AWSMachinePoolSpec{ + AWSLaunchTemplate: expinfrav1.AWSLaunchTemplate{}, + }, + }, + InfraCluster: &scope.ClusterScope{ + AWSCluster: &infrav1.AWSCluster{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Status: infrav1.AWSClusterStatus{ + Network: infrav1.NetworkStatus{ + SecurityGroups: map[infrav1.SecurityGroupRole]infrav1.SecurityGroup{ + infrav1.SecurityGroupNode: { + ID: "sg-111", + }, + infrav1.SecurityGroupLB: { + ID: "sg-222", + }, + }, + }, + }, + }, + }, + }, + existingLaunchTemplate: &expinfrav1.AWSLaunchTemplate{ InstanceMetadataOptions: &infrav1.InstanceMetadataOptions{ HTTPPutResponseHopLimit: 1, HTTPTokens: infrav1.HTTPTokensStateRequired, @@ -718,12 +925,35 @@ func TestServiceLaunchTemplateNeedsUpdate(t *testing.T) { }, { name: "Should return true if incoming SpotMarketOptions is different from existing SpotMarketOptions", - incoming: &expinfrav1.AWSLaunchTemplate{ - SpotMarketOptions: &infrav1.SpotMarketOptions{ - MaxPrice: aws.String("0.10"), + machinePoolScope: &scope.MachinePoolScope{ + AWSMachinePool: &expinfrav1.AWSMachinePool{ + Spec: expinfrav1.AWSMachinePoolSpec{ + AWSLaunchTemplate: expinfrav1.AWSLaunchTemplate{ + SpotMarketOptions: &infrav1.SpotMarketOptions{ + MaxPrice: aws.String("0.10"), + }, + }, + }, + }, + InfraCluster: &scope.ClusterScope{ + AWSCluster: &infrav1.AWSCluster{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Status: infrav1.AWSClusterStatus{ + Network: infrav1.NetworkStatus{ + SecurityGroups: map[infrav1.SecurityGroupRole]infrav1.SecurityGroup{ + infrav1.SecurityGroupNode: { + ID: "sg-111", + }, + infrav1.SecurityGroupLB: { + ID: "sg-222", + }, + }, + }, + }, + }, }, }, - existing: &expinfrav1.AWSLaunchTemplate{ + existingLaunchTemplate: &expinfrav1.AWSLaunchTemplate{ AdditionalSecurityGroups: []infrav1.AWSResourceReference{ {ID: aws.String("sg-111")}, {ID: aws.String("sg-222")}, @@ -737,12 +967,35 @@ func TestServiceLaunchTemplateNeedsUpdate(t *testing.T) { }, { name: "Should return true if incoming adds SpotMarketOptions and existing has none", - incoming: &expinfrav1.AWSLaunchTemplate{ - SpotMarketOptions: &infrav1.SpotMarketOptions{ - MaxPrice: aws.String("0.10"), + machinePoolScope: &scope.MachinePoolScope{ + AWSMachinePool: &expinfrav1.AWSMachinePool{ + Spec: expinfrav1.AWSMachinePoolSpec{ + AWSLaunchTemplate: expinfrav1.AWSLaunchTemplate{ + SpotMarketOptions: &infrav1.SpotMarketOptions{ + MaxPrice: aws.String("0.10"), + }, + }, + }, + }, + InfraCluster: &scope.ClusterScope{ + AWSCluster: &infrav1.AWSCluster{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Status: infrav1.AWSClusterStatus{ + Network: infrav1.NetworkStatus{ + SecurityGroups: map[infrav1.SecurityGroupRole]infrav1.SecurityGroup{ + infrav1.SecurityGroupNode: { + ID: "sg-111", + }, + infrav1.SecurityGroupLB: { + ID: "sg-222", + }, + }, + }, + }, + }, }, }, - existing: &expinfrav1.AWSLaunchTemplate{ + existingLaunchTemplate: &expinfrav1.AWSLaunchTemplate{ AdditionalSecurityGroups: []infrav1.AWSResourceReference{ {ID: aws.String("sg-111")}, {ID: aws.String("sg-222")}, @@ -754,10 +1007,33 @@ func TestServiceLaunchTemplateNeedsUpdate(t *testing.T) { }, { name: "Should return true if incoming removes SpotMarketOptions and existing has some", - incoming: &expinfrav1.AWSLaunchTemplate{ - SpotMarketOptions: nil, + machinePoolScope: &scope.MachinePoolScope{ + AWSMachinePool: &expinfrav1.AWSMachinePool{ + Spec: expinfrav1.AWSMachinePoolSpec{ + AWSLaunchTemplate: expinfrav1.AWSLaunchTemplate{ + SpotMarketOptions: nil, + }, + }, + }, + InfraCluster: &scope.ClusterScope{ + AWSCluster: &infrav1.AWSCluster{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Status: infrav1.AWSClusterStatus{ + Network: infrav1.NetworkStatus{ + SecurityGroups: map[infrav1.SecurityGroupRole]infrav1.SecurityGroup{ + infrav1.SecurityGroupNode: { + ID: "sg-111", + }, + infrav1.SecurityGroupLB: { + ID: "sg-222", + }, + }, + }, + }, + }, + }, }, - existing: &expinfrav1.AWSLaunchTemplate{ + existingLaunchTemplate: &expinfrav1.AWSLaunchTemplate{ AdditionalSecurityGroups: []infrav1.AWSResourceReference{ {ID: aws.String("sg-111")}, {ID: aws.String("sg-222")}, @@ -769,12 +1045,113 @@ func TestServiceLaunchTemplateNeedsUpdate(t *testing.T) { want: true, wantErr: false, }, + { + name: "Should return true if AMI has changed", + machinePoolScope: &scope.MachinePoolScope{ + AWSMachinePool: &expinfrav1.AWSMachinePool{ + Spec: expinfrav1.AWSMachinePoolSpec{ + AWSLaunchTemplate: expinfrav1.AWSLaunchTemplate{ + AMI: infrav1.AMIReference{ + ID: aws.String("ami-new"), + }, + }, + }, + }, + InfraCluster: &scope.ClusterScope{ + AWSCluster: &infrav1.AWSCluster{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Status: infrav1.AWSClusterStatus{ + Network: infrav1.NetworkStatus{ + SecurityGroups: map[infrav1.SecurityGroupRole]infrav1.SecurityGroup{ + infrav1.SecurityGroupNode: { + ID: "sg-111", + }, + infrav1.SecurityGroupLB: { + ID: "sg-222", + }, + }, + }, + }, + }, + }, + }, + existingLaunchTemplate: &expinfrav1.AWSLaunchTemplate{ + AdditionalSecurityGroups: []infrav1.AWSResourceReference{ + {ID: aws.String("sg-111")}, + {ID: aws.String("sg-222")}, + }, + }, + want: true, + wantErr: false, + }, + { + name: "Should return false if AMI has not changed", + machinePoolScope: &scope.MachinePoolScope{ + AWSMachinePool: &expinfrav1.AWSMachinePool{ + Spec: expinfrav1.AWSMachinePoolSpec{ + AWSLaunchTemplate: expinfrav1.AWSLaunchTemplate{ + AMI: infrav1.AMIReference{ + ID: aws.String("current-ami-id"), + }, + }, + }, + }, + InfraCluster: &scope.ClusterScope{ + AWSCluster: &infrav1.AWSCluster{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Status: infrav1.AWSClusterStatus{ + Network: infrav1.NetworkStatus{ + SecurityGroups: map[infrav1.SecurityGroupRole]infrav1.SecurityGroup{ + infrav1.SecurityGroupNode: { + ID: "sg-111", + }, + infrav1.SecurityGroupLB: { + ID: "sg-222", + }, + }, + }, + }, + }, + }, + }, + existingLaunchTemplate: &expinfrav1.AWSLaunchTemplate{ + AdditionalSecurityGroups: []infrav1.AWSResourceReference{ + {ID: aws.String("sg-111")}, + {ID: aws.String("sg-222")}, + }, + }, + want: false, + wantErr: false, + }, { name: "Should return true if SSH key names are different", - incoming: &expinfrav1.AWSLaunchTemplate{ - SSHKeyName: aws.String("new-key"), + machinePoolScope: &scope.MachinePoolScope{ + AWSMachinePool: &expinfrav1.AWSMachinePool{ + Spec: expinfrav1.AWSMachinePoolSpec{ + AWSLaunchTemplate: expinfrav1.AWSLaunchTemplate{ + SSHKeyName: aws.String("new-key"), + }, + }, + }, + InfraCluster: &scope.ClusterScope{ + AWSCluster: &infrav1.AWSCluster{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Status: infrav1.AWSClusterStatus{ + Network: infrav1.NetworkStatus{ + SecurityGroups: map[infrav1.SecurityGroupRole]infrav1.SecurityGroup{ + infrav1.SecurityGroupNode: { + ID: "sg-111", + }, + infrav1.SecurityGroupLB: { + ID: "sg-222", + }, + }, + }, + }, + }, + }, }, - existing: &expinfrav1.AWSLaunchTemplate{ + existingLaunchTemplate: &expinfrav1.AWSLaunchTemplate{ AdditionalSecurityGroups: []infrav1.AWSResourceReference{ {ID: aws.String("sg-111")}, {ID: aws.String("sg-222")}, @@ -786,10 +1163,33 @@ func TestServiceLaunchTemplateNeedsUpdate(t *testing.T) { }, { name: "Should return true if one has SSH key name and other doesn't", - incoming: &expinfrav1.AWSLaunchTemplate{ - SSHKeyName: aws.String("new-key"), + machinePoolScope: &scope.MachinePoolScope{ + AWSMachinePool: &expinfrav1.AWSMachinePool{ + Spec: expinfrav1.AWSMachinePoolSpec{ + AWSLaunchTemplate: expinfrav1.AWSLaunchTemplate{ + SSHKeyName: aws.String("new-key"), + }, + }, + }, + InfraCluster: &scope.ClusterScope{ + AWSCluster: &infrav1.AWSCluster{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Status: infrav1.AWSClusterStatus{ + Network: infrav1.NetworkStatus{ + SecurityGroups: map[infrav1.SecurityGroupRole]infrav1.SecurityGroup{ + infrav1.SecurityGroupNode: { + ID: "sg-111", + }, + infrav1.SecurityGroupLB: { + ID: "sg-222", + }, + }, + }, + }, + }, + }, }, - existing: &expinfrav1.AWSLaunchTemplate{ + existingLaunchTemplate: &expinfrav1.AWSLaunchTemplate{ AdditionalSecurityGroups: []infrav1.AWSResourceReference{ {ID: aws.String("sg-111")}, {ID: aws.String("sg-222")}, @@ -801,14 +1201,37 @@ func TestServiceLaunchTemplateNeedsUpdate(t *testing.T) { }, { name: "Should return true if incoming PrivateDNSName is different from existing PrivateDNSName", - incoming: &expinfrav1.AWSLaunchTemplate{ - PrivateDNSName: &infrav1.PrivateDNSName{ - EnableResourceNameDNSARecord: aws.Bool(true), - EnableResourceNameDNSAAAARecord: aws.Bool(false), - HostnameType: aws.String("resource-name"), + machinePoolScope: &scope.MachinePoolScope{ + AWSMachinePool: &expinfrav1.AWSMachinePool{ + Spec: expinfrav1.AWSMachinePoolSpec{ + AWSLaunchTemplate: expinfrav1.AWSLaunchTemplate{ + PrivateDNSName: &infrav1.PrivateDNSName{ + EnableResourceNameDNSARecord: aws.Bool(true), + EnableResourceNameDNSAAAARecord: aws.Bool(false), + HostnameType: aws.String("resource-name"), + }, + }, + }, + }, + InfraCluster: &scope.ClusterScope{ + AWSCluster: &infrav1.AWSCluster{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Status: infrav1.AWSClusterStatus{ + Network: infrav1.NetworkStatus{ + SecurityGroups: map[infrav1.SecurityGroupRole]infrav1.SecurityGroup{ + infrav1.SecurityGroupNode: { + ID: "sg-111", + }, + infrav1.SecurityGroupLB: { + ID: "sg-222", + }, + }, + }, + }, + }, }, }, - existing: &expinfrav1.AWSLaunchTemplate{ + existingLaunchTemplate: &expinfrav1.AWSLaunchTemplate{ AdditionalSecurityGroups: []infrav1.AWSResourceReference{ {ID: aws.String("sg-111")}, {ID: aws.String("sg-222")}, @@ -824,14 +1247,37 @@ func TestServiceLaunchTemplateNeedsUpdate(t *testing.T) { }, { name: "Should return true if incoming adds PrivateDNSName and existing has none", - incoming: &expinfrav1.AWSLaunchTemplate{ - PrivateDNSName: &infrav1.PrivateDNSName{ - EnableResourceNameDNSARecord: aws.Bool(true), - EnableResourceNameDNSAAAARecord: aws.Bool(false), - HostnameType: aws.String("resource-name"), + machinePoolScope: &scope.MachinePoolScope{ + AWSMachinePool: &expinfrav1.AWSMachinePool{ + Spec: expinfrav1.AWSMachinePoolSpec{ + AWSLaunchTemplate: expinfrav1.AWSLaunchTemplate{ + PrivateDNSName: &infrav1.PrivateDNSName{ + EnableResourceNameDNSARecord: aws.Bool(true), + EnableResourceNameDNSAAAARecord: aws.Bool(false), + HostnameType: aws.String("resource-name"), + }, + }, + }, + }, + InfraCluster: &scope.ClusterScope{ + AWSCluster: &infrav1.AWSCluster{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Status: infrav1.AWSClusterStatus{ + Network: infrav1.NetworkStatus{ + SecurityGroups: map[infrav1.SecurityGroupRole]infrav1.SecurityGroup{ + infrav1.SecurityGroupNode: { + ID: "sg-111", + }, + infrav1.SecurityGroupLB: { + ID: "sg-222", + }, + }, + }, + }, + }, }, }, - existing: &expinfrav1.AWSLaunchTemplate{ + existingLaunchTemplate: &expinfrav1.AWSLaunchTemplate{ AdditionalSecurityGroups: []infrav1.AWSResourceReference{ {ID: aws.String("sg-111")}, {ID: aws.String("sg-222")}, @@ -843,10 +1289,33 @@ func TestServiceLaunchTemplateNeedsUpdate(t *testing.T) { }, { name: "Should return true if incoming removes PrivateDNSName and existing has some", - incoming: &expinfrav1.AWSLaunchTemplate{ - PrivateDNSName: nil, + machinePoolScope: &scope.MachinePoolScope{ + AWSMachinePool: &expinfrav1.AWSMachinePool{ + Spec: expinfrav1.AWSMachinePoolSpec{ + AWSLaunchTemplate: expinfrav1.AWSLaunchTemplate{ + PrivateDNSName: nil, + }, + }, + }, + InfraCluster: &scope.ClusterScope{ + AWSCluster: &infrav1.AWSCluster{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Status: infrav1.AWSClusterStatus{ + Network: infrav1.NetworkStatus{ + SecurityGroups: map[infrav1.SecurityGroupRole]infrav1.SecurityGroup{ + infrav1.SecurityGroupNode: { + ID: "sg-111", + }, + infrav1.SecurityGroupLB: { + ID: "sg-222", + }, + }, + }, + }, + }, + }, }, - existing: &expinfrav1.AWSLaunchTemplate{ + existingLaunchTemplate: &expinfrav1.AWSLaunchTemplate{ AdditionalSecurityGroups: []infrav1.AWSResourceReference{ {ID: aws.String("sg-111")}, {ID: aws.String("sg-222")}, @@ -862,10 +1331,33 @@ func TestServiceLaunchTemplateNeedsUpdate(t *testing.T) { }, { name: "Should return true if capacity reservation IDs are different", - incoming: &expinfrav1.AWSLaunchTemplate{ - CapacityReservationID: aws.String("new-reservation"), + machinePoolScope: &scope.MachinePoolScope{ + AWSMachinePool: &expinfrav1.AWSMachinePool{ + Spec: expinfrav1.AWSMachinePoolSpec{ + AWSLaunchTemplate: expinfrav1.AWSLaunchTemplate{ + CapacityReservationID: aws.String("new-reservation"), + }, + }, + }, + InfraCluster: &scope.ClusterScope{ + AWSCluster: &infrav1.AWSCluster{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Status: infrav1.AWSClusterStatus{ + Network: infrav1.NetworkStatus{ + SecurityGroups: map[infrav1.SecurityGroupRole]infrav1.SecurityGroup{ + infrav1.SecurityGroupNode: { + ID: "sg-111", + }, + infrav1.SecurityGroupLB: { + ID: "sg-222", + }, + }, + }, + }, + }, + }, }, - existing: &expinfrav1.AWSLaunchTemplate{ + existingLaunchTemplate: &expinfrav1.AWSLaunchTemplate{ AdditionalSecurityGroups: []infrav1.AWSResourceReference{ {ID: aws.String("sg-111")}, {ID: aws.String("sg-222")}, @@ -877,10 +1369,33 @@ func TestServiceLaunchTemplateNeedsUpdate(t *testing.T) { }, { name: "Should return true if one has capacity reservation ID and other doesn't", - incoming: &expinfrav1.AWSLaunchTemplate{ - CapacityReservationID: aws.String("new-reservation"), + machinePoolScope: &scope.MachinePoolScope{ + AWSMachinePool: &expinfrav1.AWSMachinePool{ + Spec: expinfrav1.AWSMachinePoolSpec{ + AWSLaunchTemplate: expinfrav1.AWSLaunchTemplate{ + CapacityReservationID: aws.String("new-reservation"), + }, + }, + }, + InfraCluster: &scope.ClusterScope{ + AWSCluster: &infrav1.AWSCluster{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Status: infrav1.AWSClusterStatus{ + Network: infrav1.NetworkStatus{ + SecurityGroups: map[infrav1.SecurityGroupRole]infrav1.SecurityGroup{ + infrav1.SecurityGroupNode: { + ID: "sg-111", + }, + infrav1.SecurityGroupLB: { + ID: "sg-222", + }, + }, + }, + }, + }, + }, }, - existing: &expinfrav1.AWSLaunchTemplate{ + existingLaunchTemplate: &expinfrav1.AWSLaunchTemplate{ AdditionalSecurityGroups: []infrav1.AWSResourceReference{ {ID: aws.String("sg-111")}, {ID: aws.String("sg-222")}, @@ -914,11 +1429,6 @@ func TestServiceLaunchTemplateNeedsUpdate(t *testing.T) { AWSCluster: ac, }, } - machinePoolScope := &scope.MachinePoolScope{ - InfraCluster: &scope.ClusterScope{ - AWSCluster: ac, - }, - } mockEC2Client := mocks.NewMockEC2API(mockCtrl) s.EC2Client = mockEC2Client @@ -926,7 +1436,7 @@ func TestServiceLaunchTemplateNeedsUpdate(t *testing.T) { tt.expect(mockEC2Client.EXPECT()) } - got, err := s.LaunchTemplateNeedsUpdate(machinePoolScope, tt.incoming, tt.existing) + got, err := s.LaunchTemplateNeedsUpdate(tt.machinePoolScope, tt.existingLaunchTemplate, aws.String("current-ami-id")) if tt.wantErr { g.Expect(err).To(HaveOccurred()) return diff --git a/pkg/cloud/services/interfaces.go b/pkg/cloud/services/interfaces.go index 96f91815fb..97b8e0f922 100644 --- a/pkg/cloud/services/interfaces.go +++ b/pkg/cloud/services/interfaces.go @@ -82,7 +82,7 @@ type EC2Interface interface { CreateLaunchTemplateVersion(id string, scope scope.LaunchTemplateScope, imageID *string, userDataSecretKey apimachinerytypes.NamespacedName, userData []byte) error PruneLaunchTemplateVersions(id string) error DeleteLaunchTemplate(id string) error - LaunchTemplateNeedsUpdate(scope scope.LaunchTemplateScope, incoming *expinfrav1.AWSLaunchTemplate, existing *expinfrav1.AWSLaunchTemplate) (bool, error) + LaunchTemplateNeedsUpdate(scope scope.LaunchTemplateScope, existingLaunchTemplate *expinfrav1.AWSLaunchTemplate, currentlyUsedAMIID *string) (bool, error) DeleteBastion() error ReconcileBastion() error // ReconcileElasticIPFromPublicPool reconciles the elastic IP from a custom Public IPv4 Pool. diff --git a/pkg/cloud/services/mock_services/ec2_interface_mock.go b/pkg/cloud/services/mock_services/ec2_interface_mock.go index d46dc001e4..bddcb71135 100644 --- a/pkg/cloud/services/mock_services/ec2_interface_mock.go +++ b/pkg/cloud/services/mock_services/ec2_interface_mock.go @@ -277,7 +277,7 @@ func (mr *MockEC2InterfaceMockRecorder) InstanceIfExists(arg0 interface{}) *gomo } // LaunchTemplateNeedsUpdate mocks base method. -func (m *MockEC2Interface) LaunchTemplateNeedsUpdate(arg0 scope.LaunchTemplateScope, arg1, arg2 *v1beta20.AWSLaunchTemplate) (bool, error) { +func (m *MockEC2Interface) LaunchTemplateNeedsUpdate(arg0 scope.LaunchTemplateScope, arg1 *v1beta20.AWSLaunchTemplate, arg2 *string) (bool, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "LaunchTemplateNeedsUpdate", arg0, arg1, arg2) ret0, _ := ret[0].(bool)