mirror of https://github.com/kubernetes/kops.git
799 lines
26 KiB
Go
799 lines
26 KiB
Go
/*
|
|
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"
|
|
"time"
|
|
|
|
"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"
|
|
)
|
|
|
|
// 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
|
|
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
|
|
// 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.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)}
|
|
}
|
|
|
|
attempt := 0
|
|
maxAttempts := 10
|
|
for {
|
|
attempt++
|
|
|
|
klog.V(8).Infof("AWS CreateLaunchConfiguration %s", aws.StringValue(request.LaunchConfigurationName))
|
|
_, err = t.Cloud.Autoscaling().CreateLaunchConfiguration(request)
|
|
if err == nil {
|
|
break
|
|
}
|
|
|
|
if awsup.AWSErrorCode(err) == "ValidationError" {
|
|
message := awsup.AWSErrorMessage(err)
|
|
if strings.Contains(message, "not authorized") || strings.Contains(message, "Invalid IamInstance") {
|
|
if attempt > maxAttempts {
|
|
return fmt.Errorf("IAM instance profile not yet created/propagated (original error: %v)", message)
|
|
}
|
|
klog.V(4).Infof("got an error indicating that the IAM instance profile %q is not ready: %q", fi.StringValue(e.IAMInstanceProfile.Name), message)
|
|
klog.Infof("waiting for IAM instance profile %q to be ready", fi.StringValue(e.IAMInstanceProfile.Name))
|
|
time.Sleep(10 * time.Second)
|
|
continue
|
|
}
|
|
klog.V(4).Infof("ErrorCode=%q, Message=%q", awsup.AWSErrorCode(err), awsup.AWSErrorMessage(err))
|
|
}
|
|
|
|
return fmt.Errorf("error creating AutoscalingLaunchConfiguration: %v", err)
|
|
}
|
|
|
|
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,
|
|
}
|
|
|
|
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
|
|
}
|