/* Copyright 2019 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package awstasks import ( "encoding/base64" "fmt" "math" "sort" "strings" "k8s.io/kops/pkg/apis/kops" "k8s.io/kops/pkg/featureflag" "k8s.io/kops/upup/pkg/fi" "k8s.io/kops/upup/pkg/fi/cloudup/awsup" "k8s.io/kops/upup/pkg/fi/cloudup/cloudformation" "k8s.io/kops/upup/pkg/fi/cloudup/terraform" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/autoscaling" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/klog/v2" ) // defaultRetainLaunchConfigurationCount is the number of launch configurations (matching the name prefix) that we should // keep, we delete older ones var defaultRetainLaunchConfigurationCount = 3 // RetainLaunchConfigurationCount returns the number of launch configurations to keep func RetainLaunchConfigurationCount() int { if featureflag.KeepLaunchConfigurations.Enabled() { return math.MaxInt32 } return defaultRetainLaunchConfigurationCount } // LaunchConfiguration is the specification for a launch configuration // +kops:fitask type LaunchConfiguration struct { // Name is the name of the configuration Name *string // Lifecycle is the resource lifecycle Lifecycle *fi.Lifecycle // AssociatePublicIP indicates if a public ip address is assigned to instabces AssociatePublicIP *bool // BlockDeviceMappings is a block device mappings BlockDeviceMappings []*BlockDeviceMapping // IAMInstanceProfile is the IAM profile to assign to the nodes IAMInstanceProfile *IAMInstanceProfile // ID is the launch configuration name ID *string // ImageID is the AMI to use for the instances ImageID *string // InstanceMonitoring indicates if monitoring is enabled InstanceMonitoring *bool // InstanceType is the machine type to use InstanceType *string // RootVolumeDeleteOnTermination states if the root volume will be deleted after instance termination RootVolumeDeleteOnTermination *bool // If volume type is io1, then we need to specify the number of Iops. RootVolumeIops *int64 // RootVolumeOptimization enables EBS optimization for an instance RootVolumeOptimization *bool // RootVolumeSize is the size of the EBS root volume to use, in GB RootVolumeSize *int64 // RootVolumeType is the type of the EBS root volume to use (e.g. gp2) RootVolumeType *string // RootVolumeEncryption enables EBS root volume encryption for an instance RootVolumeEncryption *bool // SSHKey is the ssh key for the instances SSHKey *SSHKey // SecurityGroups is a list of security group associated SecurityGroups []*SecurityGroup // SpotPrice is set to the spot-price bid if this is a spot pricing request SpotPrice string // Tenancy. Can be either default or dedicated. Tenancy *string // UserData is the user data configuration UserData *fi.ResourceHolder } var _ fi.CompareWithID = &LaunchConfiguration{} var _ fi.ProducesDeletions = &LaunchConfiguration{} func (e *LaunchConfiguration) CompareWithID() *string { return e.ID } // findLaunchConfigurations returns matching LaunchConfigurations, sorted by CreatedTime (ascending) func (e *LaunchConfiguration) findLaunchConfigurations(c *fi.Context) ([]*autoscaling.LaunchConfiguration, error) { cloud := c.Cloud.(awsup.AWSCloud) request := &autoscaling.DescribeLaunchConfigurationsInput{} prefix := *e.Name + "-" var configurations []*autoscaling.LaunchConfiguration err := cloud.Autoscaling().DescribeLaunchConfigurationsPages(request, func(page *autoscaling.DescribeLaunchConfigurationsOutput, lastPage bool) bool { for _, l := range page.LaunchConfigurations { name := aws.StringValue(l.LaunchConfigurationName) if strings.HasPrefix(name, prefix) { configurations = append(configurations, l) } } return true }) if err != nil { return nil, fmt.Errorf("error listing AutoscalingLaunchConfigurations: %v", err) } 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, nil } // Find is responsible for finding the launch configuration func (e *LaunchConfiguration) Find(c *fi.Context) (*LaunchConfiguration, error) { cloud := c.Cloud.(awsup.AWSCloud) configurations, err := e.findLaunchConfigurations(c) if err != nil { return nil, err } if len(configurations) == 0 { return nil, nil } // We pick up the latest launch configuration // (TODO: this might not actually be attached to the AutoScalingGroup, if something went wrong previously) lc := configurations[len(configurations)-1] klog.V(2).Infof("found existing AutoscalingLaunchConfiguration: %q", *lc.LaunchConfigurationName) actual := &LaunchConfiguration{ Name: e.Name, AssociatePublicIP: lc.AssociatePublicIpAddress, ID: lc.LaunchConfigurationName, ImageID: lc.ImageId, InstanceMonitoring: lc.InstanceMonitoring.Enabled, InstanceType: lc.InstanceType, RootVolumeOptimization: lc.EbsOptimized, SpotPrice: aws.StringValue(lc.SpotPrice), Tenancy: lc.PlacementTenancy, } // Only assign keyName if the existing launch config has one // lc.KeyName comes back as an empty string when there is no key assigned if lc.KeyName != nil && *lc.KeyName != "" { actual.SSHKey = &SSHKey{Name: lc.KeyName} } if lc.IamInstanceProfile != nil { actual.IAMInstanceProfile = &IAMInstanceProfile{Name: lc.IamInstanceProfile} } securityGroups := []*SecurityGroup{} for _, sgID := range lc.SecurityGroups { securityGroups = append(securityGroups, &SecurityGroup{ID: sgID}) } sort.Sort(OrderSecurityGroupsById(securityGroups)) actual.SecurityGroups = securityGroups // @step: get the image is order to find out the root device name as using the index // is not variable, under conditions they move image, err := cloud.ResolveImage(fi.StringValue(e.ImageID)) if err != nil { return nil, err } // Find the root volume for _, b := range lc.BlockDeviceMappings { if b.Ebs == nil { continue } if b.DeviceName != nil && fi.StringValue(b.DeviceName) == fi.StringValue(image.RootDeviceName) { actual.RootVolumeSize = b.Ebs.VolumeSize actual.RootVolumeType = b.Ebs.VolumeType actual.RootVolumeIops = b.Ebs.Iops actual.RootVolumeEncryption = b.Ebs.Encrypted actual.RootVolumeDeleteOnTermination = b.Ebs.DeleteOnTermination } else { _, d := BlockDeviceMappingFromAutoscaling(b) actual.BlockDeviceMappings = append(actual.BlockDeviceMappings, d) } } if lc.UserData != nil { userData, err := base64.StdEncoding.DecodeString(aws.StringValue(lc.UserData)) if err != nil { return nil, fmt.Errorf("error decoding UserData: %v", err) } actual.UserData = fi.WrapResource(fi.NewStringResource(string(userData))) } // Avoid spurious changes on ImageId if e.ImageID != nil && actual.ImageID != nil && *actual.ImageID != *e.ImageID { image, err := cloud.ResolveImage(*e.ImageID) if err != nil { klog.Warningf("unable to resolve image: %q: %v", *e.ImageID, err) } else if image == nil { klog.Warningf("unable to resolve image: %q: not found", *e.ImageID) } else if aws.StringValue(image.ImageId) == *actual.ImageID { klog.V(4).Infof("Returning matching ImageId as expected name: %q -> %q", *actual.ImageID, *e.ImageID) actual.ImageID = e.ImageID } } // Avoid spurious changes actual.Lifecycle = e.Lifecycle if e.ID == nil { e.ID = actual.ID } return actual, nil } func (e *LaunchConfiguration) Run(c *fi.Context) error { // TODO: Make Normalize a standard method e.Normalize() if e.SSHKey == nil && !useSSHKey(c.Cluster) { e.SSHKey = &SSHKey{} } return fi.DefaultDeltaRunMethod(e, c) } func (e *LaunchConfiguration) Normalize() { // We need to sort our arrays consistently, so we don't get spurious changes sort.Stable(OrderSecurityGroupsById(e.SecurityGroups)) } func (s *LaunchConfiguration) CheckChanges(a, e, changes *LaunchConfiguration) error { if e.ImageID == nil { return fi.RequiredField("ImageID") } if e.InstanceType == nil { return fi.RequiredField("InstanceType") } if a != nil { if e.Name == nil { return fi.RequiredField("Name") } } return nil } // RenderAWS is responsible for creating the launchconfiguration via api func (_ *LaunchConfiguration) RenderAWS(t *awsup.AWSAPITarget, a, e, changes *LaunchConfiguration) error { launchConfigurationName := *e.Name + "-" + fi.BuildTimestampString() klog.V(2).Infof("Creating AutoscalingLaunchConfiguration with Name:%q", launchConfigurationName) if e.ImageID == nil { return fi.RequiredField("ImageID") } image, err := t.Cloud.ResolveImage(*e.ImageID) if err != nil { return err } request := &autoscaling.CreateLaunchConfigurationInput{ AssociatePublicIpAddress: e.AssociatePublicIP, EbsOptimized: e.RootVolumeOptimization, ImageId: image.ImageId, InstanceType: e.InstanceType, LaunchConfigurationName: &launchConfigurationName, } if e.SSHKey != nil { request.KeyName = e.SSHKey.Name } if e.Tenancy != nil { request.PlacementTenancy = e.Tenancy } securityGroupIDs := []*string{} for _, sg := range e.SecurityGroups { securityGroupIDs = append(securityGroupIDs, sg.ID) } request.SecurityGroups = securityGroupIDs request.AssociatePublicIpAddress = e.AssociatePublicIP if e.SpotPrice != "" { request.SpotPrice = aws.String(e.SpotPrice) } // Build up the actual block device mappings { rootDevices, err := e.buildRootDevice(t.Cloud) if err != nil { return err } ephemeralDevices, err := buildEphemeralDevices(t.Cloud, fi.StringValue(e.InstanceType)) if err != nil { return err } additionalDevices, err := buildAdditionalDevices(e.BlockDeviceMappings) if err != nil { return err } // @step: add all the devices to the block device mappings for _, x := range []map[string]*BlockDeviceMapping{rootDevices, ephemeralDevices, additionalDevices} { for name, device := range x { request.BlockDeviceMappings = append(request.BlockDeviceMappings, device.ToAutoscaling(name)) } } } if e.UserData != nil { d, err := e.UserData.AsBytes() if err != nil { return fmt.Errorf("error rendering AutoScalingLaunchConfiguration UserData: %v", err) } request.UserData = aws.String(base64.StdEncoding.EncodeToString(d)) } if e.IAMInstanceProfile != nil { request.IamInstanceProfile = e.IAMInstanceProfile.Name } if e.InstanceMonitoring != nil { request.InstanceMonitoring = &autoscaling.InstanceMonitoring{Enabled: e.InstanceMonitoring} } else { request.InstanceMonitoring = &autoscaling.InstanceMonitoring{Enabled: fi.Bool(false)} } if _, err = t.Cloud.Autoscaling().CreateLaunchConfiguration(request); err != nil { code := awsup.AWSErrorCode(err) message := awsup.AWSErrorMessage(err) if code == "ValidationError" && strings.Contains(message, "Invalid IamInstanceProfile") { klog.V(4).Infof("error creating LaunchConfiguration: %s", message) return fi.NewTryAgainLaterError("waiting for the IAM Instance Profile to be propagated") } return fmt.Errorf("error creating LaunchConfiguration: %s", message) } e.ID = fi.String(launchConfigurationName) return nil // No tags on a launch configuration } // buildRootDevice is responsible for retrieving a boot device mapping from the image name func (t *LaunchConfiguration) buildRootDevice(cloud awsup.AWSCloud) (map[string]*BlockDeviceMapping, error) { image := fi.StringValue(t.ImageID) // @step: resolve the image ami img, err := cloud.ResolveImage(image) if err != nil { return nil, fmt.Errorf("unable to resolve image: %q: %v", image, err) } else if img == nil { return nil, fmt.Errorf("unable to resolve image: %q: not found", image) } bm := make(map[string]*BlockDeviceMapping) bm[aws.StringValue(img.RootDeviceName)] = &BlockDeviceMapping{ EbsDeleteOnTermination: t.RootVolumeDeleteOnTermination, EbsVolumeSize: t.RootVolumeSize, EbsVolumeType: t.RootVolumeType, EbsVolumeIops: t.RootVolumeIops, EbsEncrypted: t.RootVolumeEncryption, } return bm, nil } type terraformLaunchConfiguration struct { NamePrefix *string `json:"name_prefix,omitempty" cty:"name_prefix"` ImageID *string `json:"image_id,omitempty" cty:"image_id"` InstanceType *string `json:"instance_type,omitempty" cty:"instance_type"` KeyName *terraform.Literal `json:"key_name,omitempty" cty:"key_name"` IAMInstanceProfile *terraform.Literal `json:"iam_instance_profile,omitempty" cty:"iam_instance_profile"` SecurityGroups []*terraform.Literal `json:"security_groups,omitempty" cty:"security_groups"` AssociatePublicIpAddress *bool `json:"associate_public_ip_address,omitempty" cty:"associate_public_ip_address"` UserData *terraform.Literal `json:"user_data,omitempty" cty:"user_data"` RootBlockDevice *terraformBlockDevice `json:"root_block_device,omitempty" cty:"root_block_device"` EBSOptimized *bool `json:"ebs_optimized,omitempty" cty:"ebs_optimized"` EBSBlockDevice []*terraformBlockDevice `json:"ebs_block_device,omitempty" cty:"ebs_block_device"` EphemeralBlockDevice []*terraformBlockDevice `json:"ephemeral_block_device,omitempty" cty:"ephemeral_block_device"` Lifecycle *terraform.Lifecycle `json:"lifecycle,omitempty" cty:"lifecycle"` SpotPrice *string `json:"spot_price,omitempty" cty:"spot_price"` PlacementTenancy *string `json:"placement_tenancy,omitempty" cty:"placement_tenancy"` InstanceMonitoring *bool `json:"enable_monitoring,omitempty" cty:"enable_monitoring"` } type terraformBlockDevice struct { // For ephemeral devices DeviceName *string `json:"device_name,omitempty" cty:"device_name"` VirtualName *string `json:"virtual_name,omitempty" cty:"virtual_name"` // For root VolumeType *string `json:"volume_type,omitempty" cty:"volume_type"` VolumeSize *int64 `json:"volume_size,omitempty" cty:"volume_size"` Iops *int64 `json:"iops,omitempty" cty:"iops"` // Encryption Encrypted *bool `json:"encrypted,omitempty" cty:"encrypted"` // Termination DeleteOnTermination *bool `json:"delete_on_termination,omitempty" cty:"delete_on_termination"` } // RenderTerraform is responsible for rendering the terraform json func (_ *LaunchConfiguration) RenderTerraform(t *terraform.TerraformTarget, a, e, changes *LaunchConfiguration) error { cloud := t.Cloud.(awsup.AWSCloud) if e.ImageID == nil { return fi.RequiredField("ImageID") } image, err := cloud.ResolveImage(*e.ImageID) if err != nil { return err } tf := &terraformLaunchConfiguration{ NamePrefix: fi.String(*e.Name + "-"), ImageID: image.ImageId, InstanceType: e.InstanceType, } if e.SpotPrice != "" { tf.SpotPrice = aws.String(e.SpotPrice) } if e.SSHKey != nil { tf.KeyName = e.SSHKey.TerraformLink() } if e.Tenancy != nil { tf.PlacementTenancy = e.Tenancy } for _, sg := range e.SecurityGroups { tf.SecurityGroups = append(tf.SecurityGroups, sg.TerraformLink()) } tf.AssociatePublicIpAddress = e.AssociatePublicIP tf.EBSOptimized = e.RootVolumeOptimization { rootDevices, err := e.buildRootDevice(cloud) if err != nil { return err } ephemeralDevices, err := buildEphemeralDevices(cloud, fi.StringValue(e.InstanceType)) if err != nil { return err } additionalDevices, err := buildAdditionalDevices(e.BlockDeviceMappings) if err != nil { return err } if len(rootDevices) != 0 { if len(rootDevices) != 1 { return fmt.Errorf("unexpectedly found multiple root devices") } for _, bdm := range rootDevices { tf.RootBlockDevice = &terraformBlockDevice{ VolumeType: bdm.EbsVolumeType, VolumeSize: bdm.EbsVolumeSize, Iops: bdm.EbsVolumeIops, DeleteOnTermination: bdm.EbsDeleteOnTermination, } } } if len(ephemeralDevices) != 0 { tf.EphemeralBlockDevice = []*terraformBlockDevice{} for _, deviceName := range sets.StringKeySet(ephemeralDevices).List() { bdm := ephemeralDevices[deviceName] tf.EphemeralBlockDevice = append(tf.EphemeralBlockDevice, &terraformBlockDevice{ VirtualName: bdm.VirtualName, DeviceName: fi.String(deviceName), }) } } if len(additionalDevices) != 0 { tf.EBSBlockDevice = []*terraformBlockDevice{} for _, deviceName := range sets.StringKeySet(additionalDevices).List() { bdm := additionalDevices[deviceName] tf.EBSBlockDevice = append(tf.EBSBlockDevice, &terraformBlockDevice{ DeleteOnTermination: bdm.EbsDeleteOnTermination, DeviceName: fi.String(deviceName), Encrypted: bdm.EbsEncrypted, VolumeSize: bdm.EbsVolumeSize, VolumeType: bdm.EbsVolumeType, }) } } } if e.UserData != nil { userData, err := fi.ResourceAsString(e.UserData) if err != nil { return err } if userData != "" { tf.UserData, err = t.AddFile("aws_launch_configuration", *e.Name, "user_data", e.UserData, false) if err != nil { return err } } } if e.IAMInstanceProfile != nil { tf.IAMInstanceProfile = e.IAMInstanceProfile.TerraformLink() } if e.InstanceMonitoring != nil { tf.InstanceMonitoring = e.InstanceMonitoring } else { tf.InstanceMonitoring = fi.Bool(false) } // So that we can update configurations tf.Lifecycle = &terraform.Lifecycle{CreateBeforeDestroy: fi.Bool(true)} return t.RenderResource("aws_launch_configuration", fi.StringValue(e.Name), tf) } // TerraformLink returns the terraform reference func (e *LaunchConfiguration) TerraformLink() *terraform.Literal { return terraform.LiteralProperty("aws_launch_configuration", fi.StringValue(e.Name), "id") } type cloudformationLaunchConfiguration struct { AssociatePublicIpAddress *bool `json:"AssociatePublicIpAddress,omitempty"` BlockDeviceMappings []*cloudformationBlockDevice `json:"BlockDeviceMappings,omitempty"` EBSOptimized *bool `json:"EbsOptimized,omitempty"` IAMInstanceProfile *cloudformation.Literal `json:"IamInstanceProfile,omitempty"` ImageID *string `json:"ImageId,omitempty"` InstanceType *string `json:"InstanceType,omitempty"` KeyName *string `json:"KeyName,omitempty"` SecurityGroups []*cloudformation.Literal `json:"SecurityGroups,omitempty"` SpotPrice *string `json:"SpotPrice,omitempty"` UserData *string `json:"UserData,omitempty"` PlacementTenancy *string `json:"PlacementTenancy,omitempty"` InstanceMonitoring *bool `json:"InstanceMonitoring,omitempty"` } type cloudformationBlockDevice struct { // For ephemeral devices DeviceName *string `json:"DeviceName,omitempty"` VirtualName *string `json:"VirtualName,omitempty"` // For root Ebs *cloudformationBlockDeviceEBS `json:"Ebs,omitempty"` } type cloudformationBlockDeviceEBS struct { VolumeType *string `json:"VolumeType,omitempty"` VolumeSize *int64 `json:"VolumeSize,omitempty"` Iops *int64 `json:"Iops,omitempty"` DeleteOnTermination *bool `json:"DeleteOnTermination,omitempty"` Encrypted *bool `json:"Encrypted,omitempty"` } // RenderCloudformation is responsible for rendering the cloudformation template func (_ *LaunchConfiguration) RenderCloudformation(t *cloudformation.CloudformationTarget, a, e, changes *LaunchConfiguration) error { cloud := t.Cloud.(awsup.AWSCloud) if e.ImageID == nil { return fi.RequiredField("ImageID") } image, err := cloud.ResolveImage(*e.ImageID) if err != nil { return err } cf := &cloudformationLaunchConfiguration{ //NamePrefix: fi.String(*e.Name + "-"), ImageID: image.ImageId, InstanceType: e.InstanceType, } if e.SpotPrice != "" { cf.SpotPrice = aws.String(e.SpotPrice) } if e.SSHKey != nil && !e.SSHKey.NoSSHKey() { if e.SSHKey.Name == nil { return fmt.Errorf("SSHKey Name not set") } cf.KeyName = e.SSHKey.Name } if e.Tenancy != nil { cf.PlacementTenancy = e.Tenancy } for _, sg := range e.SecurityGroups { cf.SecurityGroups = append(cf.SecurityGroups, sg.CloudformationLink()) } cf.AssociatePublicIpAddress = e.AssociatePublicIP cf.EBSOptimized = e.RootVolumeOptimization { rootDevices, err := e.buildRootDevice(cloud) if err != nil { return err } ephemeralDevices, err := buildEphemeralDevices(cloud, fi.StringValue(e.InstanceType)) if err != nil { return err } additionalDevices, err := buildAdditionalDevices(e.BlockDeviceMappings) if err != nil { return err } if len(rootDevices) != 0 { if len(rootDevices) != 1 { return fmt.Errorf("unexpectedly found multiple root devices") } for deviceName, bdm := range rootDevices { d := &cloudformationBlockDevice{ DeviceName: fi.String(deviceName), Ebs: &cloudformationBlockDeviceEBS{ VolumeType: bdm.EbsVolumeType, VolumeSize: bdm.EbsVolumeSize, Iops: bdm.EbsVolumeIops, DeleteOnTermination: bdm.EbsDeleteOnTermination, }, } cf.BlockDeviceMappings = append(cf.BlockDeviceMappings, d) } } if len(ephemeralDevices) != 0 { for deviceName, bdm := range ephemeralDevices { cf.BlockDeviceMappings = append(cf.BlockDeviceMappings, &cloudformationBlockDevice{ VirtualName: bdm.VirtualName, DeviceName: fi.String(deviceName), }) } } if len(additionalDevices) != 0 { for deviceName, bdm := range additionalDevices { d := &cloudformationBlockDevice{ DeviceName: fi.String(deviceName), Ebs: &cloudformationBlockDeviceEBS{ VolumeType: bdm.EbsVolumeType, VolumeSize: bdm.EbsVolumeSize, DeleteOnTermination: bdm.EbsDeleteOnTermination, Encrypted: bdm.EbsEncrypted, }, } cf.BlockDeviceMappings = append(cf.BlockDeviceMappings, d) } } } if e.UserData != nil { d, err := e.UserData.AsBytes() if err != nil { return fmt.Errorf("error rendering AutoScalingLaunchConfiguration UserData: %v", err) } cf.UserData = aws.String(base64.StdEncoding.EncodeToString(d)) } if e.IAMInstanceProfile != nil { cf.IAMInstanceProfile = e.IAMInstanceProfile.CloudformationLink() } if e.InstanceMonitoring != nil { cf.InstanceMonitoring = e.InstanceMonitoring } else { cf.InstanceMonitoring = fi.Bool(false) } // So that we can update configurations //tf.Lifecycle = &cloudformation.Lifecycle{CreateBeforeDestroy: fi.Bool(true)} return t.RenderResource("AWS::AutoScaling::LaunchConfiguration", *e.Name, cf) } func (e *LaunchConfiguration) CloudformationLink() *cloudformation.Literal { return cloudformation.Ref("AWS::AutoScaling::LaunchConfiguration", *e.Name) } // deleteLaunchConfiguration tracks a LaunchConfiguration that we're going to delete // It implements fi.Deletion type deleteLaunchConfiguration struct { lc *autoscaling.LaunchConfiguration } var _ fi.Deletion = &deleteLaunchConfiguration{} func (d *deleteLaunchConfiguration) TaskName() string { return "LaunchConfiguration" } func (d *deleteLaunchConfiguration) Item() string { return aws.StringValue(d.lc.LaunchConfigurationName) } func (d *deleteLaunchConfiguration) Delete(t fi.Target) error { klog.V(2).Infof("deleting launch configuration %v", d) awsTarget, ok := t.(*awsup.AWSAPITarget) if !ok { return fmt.Errorf("unexpected target type for deletion: %T", t) } request := &autoscaling.DeleteLaunchConfigurationInput{ LaunchConfigurationName: d.lc.LaunchConfigurationName, } name := aws.StringValue(request.LaunchConfigurationName) klog.V(2).Infof("Calling autoscaling DeleteLaunchConfiguration for %s", name) _, err := awsTarget.Cloud.Autoscaling().DeleteLaunchConfiguration(request) if err != nil { return fmt.Errorf("error deleting autoscaling LaunchConfiguration %s: %v", name, err) } return nil } func (d *deleteLaunchConfiguration) String() string { return d.TaskName() + "-" + d.Item() } func (e *LaunchConfiguration) FindDeletions(c *fi.Context) ([]fi.Deletion, error) { var removals []fi.Deletion configurations, err := e.findLaunchConfigurations(c) if err != nil { return nil, err } if len(configurations) <= RetainLaunchConfigurationCount() { return nil, nil } configurations = configurations[:len(configurations)-RetainLaunchConfigurationCount()] for _, configuration := range configurations { removals = append(removals, &deleteLaunchConfiguration{lc: configuration}) } klog.V(2).Infof("will delete launch configurations: %v", removals) return removals, nil } func useSSHKey(c *kops.Cluster) bool { if c != nil { sshKeyName := c.Spec.SSHKeyName return sshKeyName != nil && *sshKeyName != "" } return true }