Repository: keikoproj/instance-manager (go)

Issue:
LaunchConfiguration pagination and retention
Looks like we are not paginating when we call DescribeLaunchConfigurations, we must paginate to be sure we get all launch configurations.

Also, we should do some work around the retention of launch configurations and possibly make it configurable (either globally or per CR).
Right now we are deferring a deletion but ignore failures, this can cause leak of launch configurations under some conditions

Pull request:
Pagination & Launch config deletion
Fixes #114 

This adds pagination for DescribeLaunchConfigurations and refactors create/update/delete to properly handle launch configuration deletion.

New logic will be called on CloudDiscovery instead of during update, and will delete the oldest launch configurations per instance group outside of the newest ones according to number of default retention (default 2) - we can make this configurable in the future.
IG Deletion now simply deletes all launch configs with the resource prefix.

- [x] BDD

Patch:
diff --git a/controllers/common/utils.go b/controllers/common/utils.go
index 1e4d23d..1925d91 100644
--- a/controllers/common/utils.go
+++ b/controllers/common/utils.go
@@ -90,5 +90,5 @@ func ReadFile(path string) ([]byte, error) {
 
 func GetTimeString() string {
 	n := time.Now().UTC()
-	return fmt.Sprintf("%v%v%v%v%v%v", n.Year(), int(n.Month()), n.Day(), n.Hour(), n.Minute(), n.Second())
+	return n.Format("20060102150405")
 }
diff --git a/controllers/providers/aws/aws.go b/controllers/providers/aws/aws.go
index 6b13f9d..7a86ecc 100644
--- a/controllers/providers/aws/aws.go
+++ b/controllers/providers/aws/aws.go
@@ -494,12 +494,16 @@ func (w *AwsWorker) DescribeAutoscalingGroups() ([]*autoscaling.Group, error) {
 	return scalingGroups, nil
 }
 
-func (w *AwsWorker) DescribeAutoscalingLaunchConfigs() (autoscaling.DescribeLaunchConfigurationsOutput, error) {
-	out, err := w.AsgClient.DescribeLaunchConfigurations(&autoscaling.DescribeLaunchConfigurationsInput{})
+func (w *AwsWorker) DescribeAutoscalingLaunchConfigs() ([]*autoscaling.LaunchConfiguration, error) {
+	launchConfigurations := []*autoscaling.LaunchConfiguration{}
+	err := w.AsgClient.DescribeLaunchConfigurationsPages(&autoscaling.DescribeLaunchConfigurationsInput{}, func(page *autoscaling.DescribeLaunchConfigurationsOutput, lastPage bool) bool {
+		launchConfigurations = append(launchConfigurations, page.LaunchConfigurations...)
+		return page.NextToken != nil
+	})
 	if err != nil {
-		return autoscaling.DescribeLaunchConfigurationsOutput{}, err
+		return launchConfigurations, err
 	}
-	return *out, nil
+	return launchConfigurations, nil
 }
 
 func (w *AwsWorker) GetAutoscalingLaunchConfig(name string) (*autoscaling.LaunchConfiguration, error) {
diff --git a/controllers/provisioners/eks/cloud.go b/controllers/provisioners/eks/cloud.go
index f622560..219d21d 100644
--- a/controllers/provisioners/eks/cloud.go
+++ b/controllers/provisioners/eks/cloud.go
@@ -17,6 +17,7 @@ package eks
 
 import (
 	"fmt"
+	"strings"
 
 	"github.com/keikoproj/instance-manager/api/v1alpha1"
 	kubeprovider "github.com/keikoproj/instance-manager/controllers/providers/kubernetes"
@@ -33,6 +34,7 @@ type DiscoveredState struct {
 	NodesReady                    bool
 	OwnedScalingGroups            []*autoscaling.Group
 	ScalingGroup                  *autoscaling.Group
+	LaunchConfigurations          []*autoscaling.LaunchConfiguration
 	LaunchConfiguration           *autoscaling.LaunchConfiguration
 	ActiveLaunchConfigurationName string
 	IAMRole                       *iam.Role
@@ -84,16 +86,22 @@ func (ctx *EksInstanceGroupContext) CloudDiscovery() error {
 	// find all owned scaling groups
 	ownedScalingGroups := ctx.findOwnedScalingGroups(scalingGroups)
 	state.SetOwnedScalingGroups(ownedScalingGroups)
-
 	// cache the scaling group we are reconciling for if it exists
 	targetScalingGroup := ctx.findTargetScalingGroup(ownedScalingGroups)
 
 	// if there is no scaling group found, it's deprovisioned
 	if targetScalingGroup == nil {
 		state.SetProvisioned(false)
-		// no need to look for launch configurations at this point
 		return nil
 	}
+
+	launchConfigurations, err := ctx.AwsWorker.DescribeAutoscalingLaunchConfigs()
+	if err != nil {
+		return errors.Wrap(err, "failed to describe autoscaling groups")
+	}
+
+	ctx.DiscoveredState.SetLaunchConfigurations(launchConfigurations)
+
 	state.SetProvisioned(true)
 	state.SetScalingGroup(targetScalingGroup)
 
@@ -108,22 +116,33 @@ func (ctx *EksInstanceGroupContext) CloudDiscovery() error {
 	}
 
 	// cache the launch configuration we are reconciling for if it exists
-	launchConfigName := aws.StringValue(targetScalingGroup.LaunchConfigurationName)
-	if launchConfigName != "" {
-		targetLaunchConfig, err := ctx.AwsWorker.GetAutoscalingLaunchConfig(launchConfigName)
-		if err != nil {
-			return errors.Wrap(err, "failed to describe autoscaling launch configurations")
-		}
-
-		if targetLaunchConfig == nil {
-			return nil
+	for _, lc := range launchConfigurations {
+		lcName := aws.StringValue(lc.LaunchConfigurationName)
+		if aws.StringValue(lc.LaunchConfigurationName) == aws.StringValue(targetScalingGroup.LaunchConfigurationName) {
+			state.SetLaunchConfiguration(lc)
+			state.SetActiveLaunchConfigurationName(lcName)
+			status.SetActiveLaunchConfigurationName(lcName)
 		}
+	}
 
-		var lcName = aws.StringValue(targetLaunchConfig.LaunchConfigurationName)
+	// delete old launch configurations
+	sortedConfigs := ctx.GetTimeSortedLaunchConfigurations()
+	var deletable []*autoscaling.LaunchConfiguration
+	if len(sortedConfigs) > defaultLaunchConfigurationRetention {
+		d := len(sortedConfigs) - defaultLaunchConfigurationRetention
+		deletable = sortedConfigs[:d]
+	}
 
-		state.SetLaunchConfiguration(targetLaunchConfig)
-		state.SetActiveLaunchConfigurationName(lcName)
-		status.SetActiveLaunchConfigurationName(lcName)
+	for _, d := range deletable {
+		name := aws.StringValue(d.LaunchConfigurationName)
+		if strings.EqualFold(name, state.GetActiveLaunchConfigurationName()) {
+			// never try to delete the active launch config
+			continue
+		}
+		ctx.Log.Info("deleting old launch configuration", "instancegroup", instanceGroup.GetName(), "name", name)
+		if err := ctx.AwsWorker.DeleteLaunchConfig(name); err != nil {
+			ctx.Log.Error(err, "failed to delete launch configuration", "instancegroup", instanceGroup.GetName(), "name", name)
+		}
 	}
 
 	if status.GetNodesReadyCondition() == corev1.ConditionTrue {
@@ -165,6 +184,12 @@ func (d *DiscoveredState) SetLaunchConfiguration(lc *autoscaling.LaunchConfigura
 func (d *DiscoveredState) GetLaunchConfiguration() *autoscaling.LaunchConfiguration {
 	return d.LaunchConfiguration
 }
+func (d *DiscoveredState) GetLaunchConfigurations() []*autoscaling.LaunchConfiguration {
+	return d.LaunchConfigurations
+}
+func (d *DiscoveredState) SetLaunchConfigurations(configs []*autoscaling.LaunchConfiguration) {
+	d.LaunchConfigurations = configs
+}
 func (d *DiscoveredState) SetActiveLaunchConfigurationName(name string) {
 	d.ActiveLaunchConfigurationName = name
 }
diff --git a/controllers/provisioners/eks/create.go b/controllers/provisioners/eks/create.go
index f5be0fa..e91c6a5 100644
--- a/controllers/provisioners/eks/create.go
+++ b/controllers/provisioners/eks/create.go
@@ -43,7 +43,8 @@ func (ctx *EksInstanceGroupContext) Create() error {
 
 	// create launchconfig
 	if !state.HasLaunchConfiguration() {
-		err := ctx.CreateLaunchConfiguration()
+		lcName := fmt.Sprintf("%v-%v", ctx.ResourcePrefix, common.GetTimeString())
+		err := ctx.CreateLaunchConfiguration(lcName)
 		if err != nil {
 			return errors.Wrap(err, "failed to create launch configuration")
 		}
@@ -64,9 +65,8 @@ func (ctx *EksInstanceGroupContext) CreateScalingGroup() error {
 		instanceGroup = ctx.GetInstanceGroup()
 		spec          = instanceGroup.GetEKSSpec()
 		configuration = instanceGroup.GetEKSConfiguration()
-		clusterName   = configuration.GetClusterName()
 		state         = ctx.GetDiscoveredState()
-		asgName       = fmt.Sprintf("%v-%v-%v", clusterName, instanceGroup.GetNamespace(), instanceGroup.GetName())
+		asgName       = ctx.ResourcePrefix
 		tags          = ctx.GetAddedTags(asgName)
 	)
 
@@ -100,35 +100,30 @@ func (ctx *EksInstanceGroupContext) CreateScalingGroup() error {
 	return nil
 }
 
-func (ctx *EksInstanceGroupContext) CreateLaunchConfiguration() error {
+func (ctx *EksInstanceGroupContext) CreateLaunchConfiguration(name string) error {
 	var (
 		state         = ctx.GetDiscoveredState()
 		instanceGroup = ctx.GetInstanceGroup()
-		configuration = instanceGroup.GetEKSConfiguration()
 		status        = instanceGroup.GetStatus()
-		clusterName   = configuration.GetClusterName()
-		lcName        = fmt.Sprintf("%v-%v-%v-%v", clusterName, instanceGroup.GetNamespace(), instanceGroup.GetName(), common.GetTimeString())
-		lcInput       = ctx.GetLaunchConfigurationInput(lcName)
+		input         = ctx.GetLaunchConfigurationInput(name)
 	)
 
-	if aws.StringValue(lcInput.IamInstanceProfile) == "" {
+	if aws.StringValue(input.IamInstanceProfile) == "" {
 		return errors.Errorf("cannot create a launchconfiguration without iam instance profile")
 	}
 
-	err := ctx.AwsWorker.CreateLaunchConfig(lcInput)
+	err := ctx.AwsWorker.CreateLaunchConfig(input)
 	if err != nil {
 		return err
 	}
 
-	ctx.Log.Info("created launchconfig", "instancegroup", instanceGroup.GetName(), "launchconfig", lcName)
-
-	lc, err := ctx.AwsWorker.GetAutoscalingLaunchConfig(lcName)
+	ctx.Log.Info("created launchconfig", "instancegroup", instanceGroup.GetName(), "launchconfig", name)
+	lc, err := ctx.AwsWorker.GetAutoscalingLaunchConfig(name)
 	if err != nil {
 		return err
 	}
 
 	if lc != nil {
-		name := aws.StringValue(lc.LaunchConfigurationName)
 		status.SetActiveLaunchConfigurationName(name)
 		state.SetActiveLaunchConfigurationName(name)
 		state.SetLaunchConfiguration(lc)
@@ -142,9 +137,8 @@ func (ctx *EksInstanceGroupContext) CreateManagedRole() error {
 		instanceGroup      = ctx.GetInstanceGroup()
 		state              = ctx.GetDiscoveredState()
 		configuration      = instanceGroup.GetEKSConfiguration()
-		clusterName        = configuration.GetClusterName()
 		additionalPolicies = configuration.GetManagedPolicies()
-		roleName           = fmt.Sprintf("%v-%v-%v", clusterName, instanceGroup.GetNamespace(), instanceGroup.GetName())
+		roleName           = ctx.ResourcePrefix
 	)
 
 	if configuration.HasExistingRole() {
diff --git a/controllers/provisioners/eks/delete.go b/controllers/provisioners/eks/delete.go
index 511f715..e4c49cf 100644
--- a/controllers/provisioners/eks/delete.go
+++ b/controllers/provisioners/eks/delete.go
@@ -16,6 +16,8 @@ limitations under the License.
 package eks
 
 import (
+	"strings"
+
 	"github.com/keikoproj/instance-manager/api/v1alpha1"
 	kubeprovider "github.com/keikoproj/instance-manager/controllers/providers/kubernetes"
 	"github.com/pkg/errors"
@@ -83,18 +85,18 @@ func (ctx *EksInstanceGroupContext) DeleteLaunchConfiguration() error {
 	var (
 		state         = ctx.GetDiscoveredState()
 		instanceGroup = ctx.GetInstanceGroup()
-		lcName        = state.GetActiveLaunchConfigurationName()
 	)
 
-	if !state.HasLaunchConfiguration() {
-		return nil
-	}
-
-	err := ctx.AwsWorker.DeleteLaunchConfig(lcName)
-	if err != nil {
-		return err
+	for _, lc := range state.GetLaunchConfigurations() {
+		name := aws.StringValue(lc.LaunchConfigurationName)
+		if strings.HasPrefix(name, ctx.ResourcePrefix) {
+			err := ctx.AwsWorker.DeleteLaunchConfig(name)
+			if err != nil {
+				return err
+			}
+			ctx.Log.Info("deleted launch config", "instancegroup", instanceGroup.GetName(), "launchconfig", name)
+		}
 	}
-	ctx.Log.Info("deleted launch config", "instancegroup", instanceGroup.GetName(), "launchconfig", lcName)
 	return nil
 }
 
diff --git a/controllers/provisioners/eks/eks.go b/controllers/provisioners/eks/eks.go
index 4d1470d..989ff08 100644
--- a/controllers/provisioners/eks/eks.go
+++ b/controllers/provisioners/eks/eks.go
@@ -16,6 +16,7 @@ limitations under the License.
 package eks
 
 import (
+	"fmt"
 	"sync"
 
 	"github.com/go-logr/logr"
@@ -26,7 +27,8 @@ import (
 )
 
 const (
-	ProvisionerName = "eks"
+	ProvisionerName                     = "eks"
+	defaultLaunchConfigurationRetention = 2
 )
 
 var (
@@ -45,6 +47,8 @@ func New(p provisioners.ProvisionerInput) *EksInstanceGroupContext {
 	}
 	instanceGroup := ctx.GetInstanceGroup()
 	configuration := instanceGroup.GetEKSConfiguration()
+	ctx.ResourcePrefix = fmt.Sprintf("%v-%v-%v", configuration.GetClusterName(), instanceGroup.GetNamespace(), instanceGroup.GetName())
+
 	instanceGroup.SetState(v1alpha1.ReconcileInit)
 
 	if len(p.Configuration.DefaultSubnets) != 0 {
@@ -79,6 +83,7 @@ type EksInstanceGroupContext struct {
 	AwsWorker        awsprovider.AwsWorker
 	DiscoveredState  *DiscoveredState
 	Log              logr.Logger
+	ResourcePrefix   string
 }
 
 func (ctx *EksInstanceGroupContext) GetInstanceGroup() *v1alpha1.InstanceGroup {
diff --git a/controllers/provisioners/eks/helpers.go b/controllers/provisioners/eks/helpers.go
index aa9e08f..71b0c01 100644
--- a/controllers/provisioners/eks/helpers.go
+++ b/controllers/provisioners/eks/helpers.go
@@ -375,3 +375,32 @@ func (ctx *EksInstanceGroupContext) RemoveAuthRole(arn string) error {
 
 	return common.RemoveAuthConfigMap(ctx.KubernetesClient.Kubernetes, []string{arn})
 }
+
+func (ctx *EksInstanceGroupContext) GetTimeSortedLaunchConfigurations() []*autoscaling.LaunchConfiguration {
+	var (
+		state = ctx.GetDiscoveredState()
+	)
+
+	configurations := []*autoscaling.LaunchConfiguration{}
+	for _, lc := range state.GetLaunchConfigurations() {
+		name := aws.StringValue(lc.LaunchConfigurationName)
+		if strings.HasPrefix(name, ctx.ResourcePrefix) {
+			configurations = append(configurations, lc)
+		}
+	}
+
+	// sort matching launch configs by created time
+	sort.Slice(configurations, func(i, j int) bool {
+		ti := configurations[i].CreatedTime
+		tj := configurations[j].CreatedTime
+		if tj == nil {
+			return true
+		}
+		if ti == nil {
+			return false
+		}
+		return ti.UnixNano() < tj.UnixNano()
+	})
+
+	return configurations
+}
diff --git a/controllers/provisioners/eks/update.go b/controllers/provisioners/eks/update.go
index 8872591..1d07510 100644
--- a/controllers/provisioners/eks/update.go
+++ b/controllers/provisioners/eks/update.go
@@ -16,6 +16,7 @@ limitations under the License.
 package eks
 
 import (
+	"fmt"
 	"reflect"
 
 	"github.com/pkg/errors"
@@ -29,8 +30,6 @@ import (
 func (ctx *EksInstanceGroupContext) Update() error {
 	var (
 		instanceGroup  = ctx.GetInstanceGroup()
-		state          = ctx.GetDiscoveredState()
-		oldConfigName  string
 		rotationNeeded bool
 	)
 
@@ -45,14 +44,11 @@ func (ctx *EksInstanceGroupContext) Update() error {
 	// create new launchconfig if it has drifted
 	if ctx.LaunchConfigurationDrifted() {
 		rotationNeeded = true
-		oldConfigName = state.GetActiveLaunchConfigurationName()
-		err := ctx.CreateLaunchConfiguration()
+		lcName := fmt.Sprintf("%v-%v", ctx.ResourcePrefix, common.GetTimeString())
+		err := ctx.CreateLaunchConfiguration(lcName)
 		if err != nil {
 			return errors.Wrap(err, "failed to create launch configuration")
 		}
-		if oldConfigName != "" {
-			defer ctx.AwsWorker.DeleteLaunchConfig(oldConfigName)
-		}
 	}
 
 	if ctx.RotationNeeded() {


Test patch:
diff --git a/controllers/provisioners/eks/cloud_test.go b/controllers/provisioners/eks/cloud_test.go
index 300d7f9..8ac3a1d 100644
--- a/controllers/provisioners/eks/cloud_test.go
+++ b/controllers/provisioners/eks/cloud_test.go
@@ -16,6 +16,7 @@ limitations under the License.
 package eks
 
 import (
+	"fmt"
 	"testing"
 	"time"
 
@@ -209,3 +210,67 @@ func TestCloudDiscoverySpotPrice(t *testing.T) {
 	g.Expect(err).NotTo(gomega.HaveOccurred())
 	g.Expect(configuration.GetSpotPrice()).To(gomega.BeEmpty())
 }
+
+func TestLaunchConfigDeletion(t *testing.T) {
+	var (
+		g       = gomega.NewGomegaWithT(t)
+		k       = MockKubernetesClientSet()
+		ig      = MockInstanceGroup()
+		asgMock = NewAutoScalingMocker()
+		iamMock = NewIamMocker()
+	)
+
+	w := MockAwsWorker(asgMock, iamMock)
+	ctx := MockContext(ig, k, w)
+	configuration := ig.GetEKSConfiguration()
+
+	iamMock.Role = &iam.Role{
+		RoleName: aws.String("some-role"),
+		Arn:      aws.String("some-arn"),
+	}
+
+	iamMock.InstanceProfile = &iam.InstanceProfile{
+		InstanceProfileName: aws.String("some-profile"),
+	}
+
+	var (
+		clusterName           = "some-cluster"
+		resourceName          = "some-instance-group"
+		resourceNamespace     = "default"
+		ownedScalingGroupName = "scaling-group-1"
+		ownershipTag          = MockTagDescription(provisioners.TagClusterName, clusterName)
+		nameTag               = MockTagDescription(provisioners.TagInstanceGroupName, resourceName)
+		namespaceTag          = MockTagDescription(provisioners.TagInstanceGroupNamespace, resourceNamespace)
+	)
+
+	ig.SetName(resourceName)
+	ig.SetNamespace(resourceNamespace)
+	configuration.SetClusterName(clusterName)
+
+	asgMock.AutoScalingGroups = []*autoscaling.Group{
+		MockScalingGroup(ownedScalingGroupName, ownershipTag, nameTag, namespaceTag),
+	}
+
+	asgMock.LaunchConfigurations = []*autoscaling.LaunchConfiguration{
+		{
+			LaunchConfigurationName: aws.String(fmt.Sprintf("%v-123456", ctx.ResourcePrefix)),
+			CreatedTime:             aws.Time(time.Now()),
+		},
+		{
+			LaunchConfigurationName: aws.String(fmt.Sprintf("%v-123457", ctx.ResourcePrefix)),
+			CreatedTime:             aws.Time(time.Now().Add(time.Duration(-1) * time.Minute)),
+		},
+		{
+			LaunchConfigurationName: aws.String(fmt.Sprintf("%v-123458", ctx.ResourcePrefix)),
+			CreatedTime:             aws.Time(time.Now().Add(time.Duration(-3) * time.Minute)),
+		},
+		{
+			LaunchConfigurationName: aws.String(fmt.Sprintf("%v-123459", ctx.ResourcePrefix)),
+			CreatedTime:             aws.Time(time.Now().Add(time.Duration(-5) * time.Minute)),
+		},
+	}
+
+	err := ctx.CloudDiscovery()
+	g.Expect(err).NotTo(gomega.HaveOccurred())
+	g.Expect(asgMock.DeleteLaunchConfigurationCallCount).To(gomega.Equal(2))
+}
diff --git a/controllers/provisioners/eks/delete_test.go b/controllers/provisioners/eks/delete_test.go
index ecd5d44..68d6445 100644
--- a/controllers/provisioners/eks/delete_test.go
+++ b/controllers/provisioners/eks/delete_test.go
@@ -16,6 +16,7 @@ limitations under the License.
 package eks
 
 import (
+	"fmt"
 	"testing"
 
 	"github.com/aws/aws-sdk-go/aws"
@@ -104,14 +105,17 @@ func TestDeleteLaunchConfigurationNegative(t *testing.T) {
 		Publisher: kubeprovider.EventPublisher{
 			Client: k.Kubernetes,
 		},
-		LaunchConfiguration: &autoscaling.LaunchConfiguration{},
+		LaunchConfigurations: []*autoscaling.LaunchConfiguration{
+			{
+				LaunchConfigurationName: aws.String(fmt.Sprintf("%v-123456", ctx.ResourcePrefix)),
+			},
+		},
 	})
 
 	asgMock.DeleteLaunchConfigurationErr = errors.New("some-error")
 	err := ctx.Delete()
 	g.Expect(err).To(gomega.HaveOccurred())
 	g.Expect(ctx.GetState()).To(gomega.Equal(v1alpha1.ReconcileDeleting))
-	asgMock.DeleteLaunchConfigurationErr = nil
 }
 
 func TestDeleteAutoScalingGroupNegative(t *testing.T) {
diff --git a/controllers/provisioners/eks/eks_test.go b/controllers/provisioners/eks/eks_test.go
index c8d5c65..1616566 100644
--- a/controllers/provisioners/eks/eks_test.go
+++ b/controllers/provisioners/eks/eks_test.go
@@ -259,6 +259,7 @@ type MockAutoScalingClient struct {
 	UpdateAutoScalingGroupErr              error
 	DeleteAutoScalingGroupErr              error
 	TerminateInstanceInAutoScalingGroupErr error
+	DeleteLaunchConfigurationCallCount     int
 	LaunchConfiguration                    *autoscaling.LaunchConfiguration
 	LaunchConfigurations                   []*autoscaling.LaunchConfiguration
 	AutoScalingGroup                       *autoscaling.Group
@@ -286,6 +287,7 @@ func (a *MockAutoScalingClient) DescribeLaunchConfigurations(input *autoscaling.
 }
 
 func (a *MockAutoScalingClient) DeleteLaunchConfiguration(input *autoscaling.DeleteLaunchConfigurationInput) (*autoscaling.DeleteLaunchConfigurationOutput, error) {
+	a.DeleteLaunchConfigurationCallCount++
 	return &autoscaling.DeleteLaunchConfigurationOutput{}, a.DeleteLaunchConfigurationErr
 }
 
@@ -310,6 +312,15 @@ func (a *MockAutoScalingClient) DescribeAutoScalingGroupsPages(input *autoscalin
 	return nil
 }
 
+func (a *MockAutoScalingClient) DescribeLaunchConfigurationsPages(input *autoscaling.DescribeLaunchConfigurationsInput, callback func(*autoscaling.DescribeLaunchConfigurationsOutput, bool) bool) error {
+	page, err := a.DescribeLaunchConfigurations(input)
+	if err != nil {
+		return err
+	}
+	callback(page, false)
+	return nil
+}
+
 func (a *MockAutoScalingClient) UpdateAutoScalingGroup(input *autoscaling.UpdateAutoScalingGroupInput) (*autoscaling.UpdateAutoScalingGroupOutput, error) {
 	return &autoscaling.UpdateAutoScalingGroupOutput{}, a.UpdateAutoScalingGroupErr
 }