mirror of https://github.com/kubernetes/kops.git
460 lines
15 KiB
Go
460 lines
15 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 (
|
|
"context"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"sort"
|
|
"strconv"
|
|
|
|
"github.com/aws/aws-sdk-go-v2/aws"
|
|
"github.com/aws/aws-sdk-go-v2/service/ec2"
|
|
ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
|
|
"k8s.io/klog/v2"
|
|
|
|
"k8s.io/kops/upup/pkg/fi"
|
|
"k8s.io/kops/upup/pkg/fi/cloudup/awsup"
|
|
)
|
|
|
|
// RenderAWS is responsible for performing creating / updating the launch template
|
|
func (t *LaunchTemplate) RenderAWS(c *awsup.AWSAPITarget, a, e, changes *LaunchTemplate) error {
|
|
ctx := context.TODO()
|
|
// @step: resolve the image id to an AMI for us
|
|
image, err := c.Cloud.ResolveImage(fi.ValueOf(t.ImageID))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// @step: lets build the launch template data
|
|
data := &ec2types.RequestLaunchTemplateData{
|
|
DisableApiTermination: fi.PtrTo(false),
|
|
EbsOptimized: t.RootVolumeOptimization,
|
|
ImageId: image.ImageId,
|
|
InstanceType: fi.ValueOf(t.InstanceType),
|
|
MetadataOptions: &ec2types.LaunchTemplateInstanceMetadataOptionsRequest{
|
|
HttpPutResponseHopLimit: t.HTTPPutResponseHopLimit,
|
|
HttpTokens: fi.ValueOf(t.HTTPTokens),
|
|
HttpProtocolIpv6: fi.ValueOf(t.HTTPProtocolIPv6),
|
|
},
|
|
NetworkInterfaces: []ec2types.LaunchTemplateInstanceNetworkInterfaceSpecificationRequest{
|
|
{
|
|
AssociatePublicIpAddress: t.AssociatePublicIP,
|
|
DeleteOnTermination: aws.Bool(true),
|
|
DeviceIndex: fi.PtrTo(int32(0)),
|
|
Ipv6AddressCount: t.IPv6AddressCount,
|
|
},
|
|
},
|
|
}
|
|
|
|
// @step: add the actual block device mappings
|
|
rootDevices, err := t.buildRootDevice(c.Cloud)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to build root device: %w", err)
|
|
}
|
|
ephemeralDevices, err := buildEphemeralDevices(c.Cloud, fi.ValueOf(t.InstanceType))
|
|
if err != nil {
|
|
return fmt.Errorf("failed to build ephemeral devices: %w", err)
|
|
}
|
|
additionalDevices, err := buildAdditionalDevices(t.BlockDeviceMappings)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, x := range []map[string]*BlockDeviceMapping{rootDevices, ephemeralDevices, additionalDevices} {
|
|
for name, device := range x {
|
|
data.BlockDeviceMappings = append(data.BlockDeviceMappings, device.ToLaunchTemplateBootDeviceRequest(name))
|
|
}
|
|
}
|
|
|
|
// @step: add the ssh key
|
|
if t.SSHKey != nil {
|
|
data.KeyName = t.SSHKey.Name
|
|
}
|
|
// @step: add the security groups
|
|
for _, sg := range t.SecurityGroups {
|
|
data.NetworkInterfaces[0].Groups = append(data.NetworkInterfaces[0].Groups, fi.ValueOf(sg.ID))
|
|
}
|
|
// @step: add any tenancy details
|
|
if t.Tenancy != nil {
|
|
data.Placement = &ec2types.LaunchTemplatePlacementRequest{Tenancy: fi.ValueOf(t.Tenancy)}
|
|
}
|
|
// @step: set the instance monitoring
|
|
data.Monitoring = &ec2types.LaunchTemplatesMonitoringRequest{Enabled: fi.PtrTo(false)}
|
|
if t.InstanceMonitoring != nil {
|
|
data.Monitoring = &ec2types.LaunchTemplatesMonitoringRequest{Enabled: t.InstanceMonitoring}
|
|
}
|
|
// @step: add the iam instance profile
|
|
if t.IAMInstanceProfile != nil {
|
|
data.IamInstanceProfile = &ec2types.LaunchTemplateIamInstanceProfileSpecificationRequest{
|
|
Name: t.IAMInstanceProfile.Name,
|
|
}
|
|
}
|
|
// @step: add the tags
|
|
var tags []ec2types.Tag
|
|
if len(t.Tags) > 0 {
|
|
for k, v := range t.Tags {
|
|
tags = append(tags, ec2types.Tag{
|
|
Key: aws.String(k),
|
|
Value: aws.String(v),
|
|
})
|
|
}
|
|
data.TagSpecifications = append(data.TagSpecifications, ec2types.LaunchTemplateTagSpecificationRequest{
|
|
ResourceType: ec2types.ResourceTypeInstance,
|
|
Tags: tags,
|
|
})
|
|
data.TagSpecifications = append(data.TagSpecifications, ec2types.LaunchTemplateTagSpecificationRequest{
|
|
ResourceType: ec2types.ResourceTypeVolume,
|
|
Tags: tags,
|
|
})
|
|
}
|
|
// @step: add the userdata
|
|
if t.UserData != nil {
|
|
d, err := fi.ResourceAsBytes(t.UserData)
|
|
if err != nil {
|
|
return fmt.Errorf("error rendering LaunchTemplate UserData: %v", err)
|
|
}
|
|
data.UserData = aws.String(base64.StdEncoding.EncodeToString(d))
|
|
}
|
|
// @step: add market options
|
|
if fi.ValueOf(t.SpotPrice) != "" {
|
|
s := &ec2types.LaunchTemplateSpotMarketOptionsRequest{
|
|
BlockDurationMinutes: t.SpotDurationInMinutes,
|
|
MaxPrice: t.SpotPrice,
|
|
}
|
|
if t.InstanceInterruptionBehavior != nil {
|
|
s.InstanceInterruptionBehavior = fi.ValueOf(t.InstanceInterruptionBehavior)
|
|
}
|
|
data.InstanceMarketOptions = &ec2types.LaunchTemplateInstanceMarketOptionsRequest{
|
|
MarketType: ec2types.MarketTypeSpot,
|
|
SpotOptions: s,
|
|
}
|
|
}
|
|
if fi.ValueOf(t.CPUCredits) != "" {
|
|
data.CreditSpecification = &ec2types.CreditSpecificationRequest{
|
|
CpuCredits: t.CPUCredits,
|
|
}
|
|
}
|
|
// @step: attempt to create the launch template
|
|
if a == nil {
|
|
input := &ec2.CreateLaunchTemplateInput{
|
|
LaunchTemplateName: t.Name,
|
|
LaunchTemplateData: data,
|
|
TagSpecifications: []ec2types.TagSpecification{
|
|
{
|
|
ResourceType: ec2types.ResourceTypeLaunchTemplate,
|
|
Tags: tags,
|
|
},
|
|
},
|
|
}
|
|
output, err := c.Cloud.EC2().CreateLaunchTemplate(ctx, input)
|
|
if err != nil || output.LaunchTemplate == nil {
|
|
return fmt.Errorf("error creating LaunchTemplate %q: %v", fi.ValueOf(t.Name), err)
|
|
}
|
|
e.ID = output.LaunchTemplate.LaunchTemplateId
|
|
} else {
|
|
input := &ec2.CreateLaunchTemplateVersionInput{
|
|
LaunchTemplateName: t.Name,
|
|
LaunchTemplateData: data,
|
|
}
|
|
if version, err := c.Cloud.EC2().CreateLaunchTemplateVersion(ctx, input); err != nil {
|
|
return fmt.Errorf("error creating LaunchTemplateVersion: %v", err)
|
|
} else {
|
|
newDefault := strconv.FormatInt(*version.LaunchTemplateVersion.VersionNumber, 10)
|
|
input := &ec2.ModifyLaunchTemplateInput{
|
|
DefaultVersion: &newDefault,
|
|
LaunchTemplateId: version.LaunchTemplateVersion.LaunchTemplateId,
|
|
}
|
|
if _, err := c.Cloud.EC2().ModifyLaunchTemplate(ctx, input); err != nil {
|
|
return fmt.Errorf("error updating launch template version: %w", err)
|
|
}
|
|
}
|
|
if changes.Tags != nil {
|
|
err = c.UpdateTags(fi.ValueOf(a.ID), e.Tags)
|
|
if err != nil {
|
|
return fmt.Errorf("error updating LaunchTemplate tags: %v", err)
|
|
}
|
|
}
|
|
e.ID = a.ID
|
|
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Find is responsible for finding the launch template for us
|
|
func (t *LaunchTemplate) Find(c *fi.CloudupContext) (*LaunchTemplate, error) {
|
|
cloud, ok := c.T.Cloud.(awsup.AWSCloud)
|
|
if !ok {
|
|
return nil, fmt.Errorf("invalid cloud provider: %v, expected: %s", c.T.Cloud, "awsup.AWSCloud")
|
|
}
|
|
|
|
// @step: get the latest launch template version
|
|
lt, err := t.findLatestLaunchTemplateVersion(c)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if lt == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
klog.V(3).Infof("found existing LaunchTemplate: %s", fi.ValueOf(lt.LaunchTemplateName))
|
|
|
|
actual := &LaunchTemplate{
|
|
AssociatePublicIP: fi.PtrTo(false),
|
|
ID: lt.LaunchTemplateId,
|
|
ImageID: lt.LaunchTemplateData.ImageId,
|
|
InstanceMonitoring: fi.PtrTo(false),
|
|
Lifecycle: t.Lifecycle,
|
|
Name: t.Name,
|
|
RootVolumeOptimization: lt.LaunchTemplateData.EbsOptimized,
|
|
}
|
|
if len(lt.LaunchTemplateData.InstanceType) > 0 {
|
|
actual.InstanceType = fi.PtrTo(lt.LaunchTemplateData.InstanceType)
|
|
}
|
|
|
|
// @step: check if any of the interfaces are public facing
|
|
for _, x := range lt.LaunchTemplateData.NetworkInterfaces {
|
|
if aws.ToBool(x.AssociatePublicIpAddress) {
|
|
actual.AssociatePublicIP = fi.PtrTo(true)
|
|
}
|
|
for _, id := range x.Groups {
|
|
actual.SecurityGroups = append(actual.SecurityGroups, &SecurityGroup{ID: fi.PtrTo(id)})
|
|
}
|
|
actual.IPv6AddressCount = x.Ipv6AddressCount
|
|
}
|
|
// In older Kops versions, security groups were added to LaunchTemplateData.SecurityGroupIds
|
|
for _, id := range lt.LaunchTemplateData.SecurityGroupIds {
|
|
actual.SecurityGroups = append(actual.SecurityGroups, &SecurityGroup{ID: fi.PtrTo("legacy-" + id)})
|
|
}
|
|
sort.Sort(OrderSecurityGroupsById(actual.SecurityGroups))
|
|
|
|
if lt.LaunchTemplateData.CreditSpecification != nil && lt.LaunchTemplateData.CreditSpecification.CpuCredits != nil {
|
|
actual.CPUCredits = lt.LaunchTemplateData.CreditSpecification.CpuCredits
|
|
} else {
|
|
actual.CPUCredits = aws.String("")
|
|
}
|
|
// @step: check if monitoring it enabled
|
|
if lt.LaunchTemplateData.Monitoring != nil {
|
|
actual.InstanceMonitoring = lt.LaunchTemplateData.Monitoring.Enabled
|
|
}
|
|
// @step: add the tenancy
|
|
if lt.LaunchTemplateData.Placement != nil && len(lt.LaunchTemplateData.Placement.Tenancy) > 0 {
|
|
actual.Tenancy = fi.PtrTo(lt.LaunchTemplateData.Placement.Tenancy)
|
|
}
|
|
// @step: add the ssh if there is one
|
|
if lt.LaunchTemplateData.KeyName != nil {
|
|
actual.SSHKey = &SSHKey{Name: lt.LaunchTemplateData.KeyName}
|
|
}
|
|
// @step: add a instance if there is one
|
|
if lt.LaunchTemplateData.IamInstanceProfile != nil {
|
|
actual.IAMInstanceProfile = &IAMInstanceProfile{Name: lt.LaunchTemplateData.IamInstanceProfile.Name}
|
|
}
|
|
// @step: add InstanceMarketOptions if there are any
|
|
imo := lt.LaunchTemplateData.InstanceMarketOptions
|
|
if imo != nil && imo.SpotOptions != nil && aws.ToString(imo.SpotOptions.MaxPrice) != "" {
|
|
actual.SpotPrice = imo.SpotOptions.MaxPrice
|
|
actual.SpotDurationInMinutes = imo.SpotOptions.BlockDurationMinutes
|
|
if len(imo.SpotOptions.InstanceInterruptionBehavior) > 0 {
|
|
actual.InstanceInterruptionBehavior = fi.PtrTo(imo.SpotOptions.InstanceInterruptionBehavior)
|
|
}
|
|
} else {
|
|
actual.SpotPrice = aws.String("")
|
|
}
|
|
|
|
// @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.ValueOf(t.ImageID))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// @step: find the root volume
|
|
for _, b := range lt.LaunchTemplateData.BlockDeviceMappings {
|
|
if b.Ebs == nil {
|
|
continue
|
|
}
|
|
if b.DeviceName != nil && fi.ValueOf(b.DeviceName) == fi.ValueOf(image.RootDeviceName) {
|
|
actual.RootVolumeSize = b.Ebs.VolumeSize
|
|
actual.RootVolumeType = b.Ebs.VolumeType
|
|
actual.RootVolumeIops = b.Ebs.Iops
|
|
actual.RootVolumeThroughput = b.Ebs.Throughput
|
|
actual.RootVolumeEncryption = b.Ebs.Encrypted
|
|
if b.Ebs.KmsKeyId != nil {
|
|
actual.RootVolumeKmsKey = b.Ebs.KmsKeyId
|
|
} else {
|
|
actual.RootVolumeKmsKey = fi.PtrTo("")
|
|
}
|
|
} else {
|
|
_, d := BlockDeviceMappingFromLaunchTemplateBootDeviceRequest(b)
|
|
actual.BlockDeviceMappings = append(actual.BlockDeviceMappings, d)
|
|
}
|
|
}
|
|
|
|
if lt.LaunchTemplateData.UserData != nil {
|
|
ud, err := base64.StdEncoding.DecodeString(aws.ToString(lt.LaunchTemplateData.UserData))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error decoding userdata: %s", err)
|
|
}
|
|
actual.UserData = fi.NewStringResource(string(ud))
|
|
}
|
|
|
|
// @step: add tags
|
|
if len(lt.LaunchTemplateData.TagSpecifications) > 0 {
|
|
ts := lt.LaunchTemplateData.TagSpecifications[0]
|
|
if ts.Tags != nil {
|
|
tags := mapEC2TagsToMap(ts.Tags)
|
|
actual.Tags = tags
|
|
}
|
|
}
|
|
|
|
// @step: add instance metadata options
|
|
if options := lt.LaunchTemplateData.MetadataOptions; options != nil {
|
|
actual.HTTPPutResponseHopLimit = options.HttpPutResponseHopLimit
|
|
if len(options.HttpTokens) > 0 {
|
|
actual.HTTPTokens = fi.PtrTo(options.HttpTokens)
|
|
}
|
|
if len(options.HttpProtocolIpv6) > 0 {
|
|
actual.HTTPProtocolIPv6 = fi.PtrTo(options.HttpProtocolIpv6)
|
|
}
|
|
}
|
|
|
|
// @step: to avoid spurious changes on ImageId
|
|
if t.ImageID != nil && actual.ImageID != nil && *actual.ImageID != *t.ImageID {
|
|
image, err := cloud.ResolveImage(*t.ImageID)
|
|
if err != nil {
|
|
klog.Warningf("unable to resolve image: %q: %v", *t.ImageID, err)
|
|
} else if image == nil {
|
|
klog.Warningf("unable to resolve image: %q: not found", *t.ImageID)
|
|
} else if aws.ToString(image.ImageId) == *actual.ImageID {
|
|
klog.V(4).Infof("Returning matching ImageId as expected name: %q -> %q", *actual.ImageID, *t.ImageID)
|
|
actual.ImageID = t.ImageID
|
|
}
|
|
}
|
|
|
|
if t.ID == nil {
|
|
t.ID = actual.ID
|
|
}
|
|
|
|
return actual, nil
|
|
}
|
|
|
|
// findAllLaunchTemplates returns all the launch templates for us
|
|
func (t *LaunchTemplate) findAllLaunchTemplates(c *fi.CloudupContext) ([]ec2types.LaunchTemplate, error) {
|
|
ctx := c.Context()
|
|
|
|
cloud, ok := c.T.Cloud.(awsup.AWSCloud)
|
|
if !ok {
|
|
return nil, fmt.Errorf("invalid cloud provider: %v, expected: %s", c.T.Cloud, "awsup.AWSCloud")
|
|
}
|
|
|
|
input := &ec2.DescribeLaunchTemplatesInput{
|
|
Filters: []ec2types.Filter{
|
|
{
|
|
Name: aws.String("tag:Name"),
|
|
Values: []string{fi.ValueOf(t.Name)},
|
|
},
|
|
},
|
|
}
|
|
|
|
var list []ec2types.LaunchTemplate
|
|
paginator := ec2.NewDescribeLaunchTemplatesPaginator(cloud.EC2(), input)
|
|
for paginator.HasMorePages() {
|
|
page, err := paginator.NextPage(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error listing AutoScaling LaunchTemplates: %v", err)
|
|
}
|
|
list = append(list, page.LaunchTemplates...)
|
|
}
|
|
|
|
return list, nil
|
|
}
|
|
|
|
// findLatestLaunchTemplateVersion returns the latest template version
|
|
func (t *LaunchTemplate) findLatestLaunchTemplateVersion(c *fi.CloudupContext) (*ec2types.LaunchTemplateVersion, error) {
|
|
ctx := c.Context()
|
|
|
|
cloud, ok := c.T.Cloud.(awsup.AWSCloud)
|
|
if !ok {
|
|
return nil, fmt.Errorf("invalid cloud provider: %v, expected: awsup.AWSCloud", c.T.Cloud)
|
|
}
|
|
|
|
input := &ec2.DescribeLaunchTemplateVersionsInput{
|
|
LaunchTemplateName: t.Name,
|
|
Versions: []string{("$Latest")},
|
|
}
|
|
|
|
output, err := cloud.EC2().DescribeLaunchTemplateVersions(ctx, input)
|
|
if err != nil {
|
|
if awsup.AWSErrorCode(err) == "InvalidLaunchTemplateName.NotFoundException" {
|
|
klog.V(4).Infof("Got InvalidLaunchTemplateName.NotFoundException error describing latest launch template version: %q", aws.ToString(t.Name))
|
|
return nil, nil
|
|
} else {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
if len(output.LaunchTemplateVersions) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
return &output.LaunchTemplateVersions[0], nil
|
|
}
|
|
|
|
// deleteLaunchTemplate tracks a LaunchConfiguration that we're going to delete
|
|
// It implements fi.CloudupDeletion
|
|
type deleteLaunchTemplate struct {
|
|
lc *ec2types.LaunchTemplate
|
|
}
|
|
|
|
var _ fi.CloudupDeletion = &deleteLaunchTemplate{}
|
|
|
|
// TaskName returns the task name
|
|
func (d *deleteLaunchTemplate) TaskName() string {
|
|
return "LaunchTemplate"
|
|
}
|
|
|
|
// Item returns the launch template name
|
|
func (d *deleteLaunchTemplate) Item() string {
|
|
return fi.ValueOf(d.lc.LaunchTemplateName)
|
|
}
|
|
|
|
func (d *deleteLaunchTemplate) Delete(t fi.CloudupTarget) error {
|
|
ctx := context.TODO()
|
|
awsTarget, ok := t.(*awsup.AWSAPITarget)
|
|
if !ok {
|
|
return fmt.Errorf("unexpected target type for deletion: %T", t)
|
|
}
|
|
|
|
if _, err := awsTarget.Cloud.EC2().DeleteLaunchTemplate(ctx, &ec2.DeleteLaunchTemplateInput{
|
|
LaunchTemplateName: d.lc.LaunchTemplateName,
|
|
}); err != nil {
|
|
return fmt.Errorf("error deleting LaunchTemplate %s: error: %s", d.Item(), err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// String returns a string representation of the task
|
|
func (d *deleteLaunchTemplate) String() string {
|
|
return d.TaskName() + "-" + d.Item()
|
|
}
|
|
|
|
func (d *deleteLaunchTemplate) DeferDeletion() bool {
|
|
return false // TODO: Should we defer deletion?
|
|
}
|