kops/upup/pkg/fi/cloudup/awstasks/launchtemplate_target_api.go

417 lines
12 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"
"sort"
"strings"
"time"
"k8s.io/kops/upup/pkg/fi"
"k8s.io/kops/upup/pkg/fi/cloudup/awsup"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ec2"
"k8s.io/klog"
)
// RenderAWS is responsible for performing creating / updating the launch template
func (t *LaunchTemplate) RenderAWS(c *awsup.AWSAPITarget, a, ep, changes *LaunchTemplate) error {
name := t.LaunchTemplateName()
// @step: resolve the image id to an AMI for us
image, err := c.Cloud.ResolveImage(fi.StringValue(t.ImageID))
if err != nil {
return err
}
// @step: lets build the launch template input
input := &ec2.CreateLaunchTemplateInput{
LaunchTemplateData: &ec2.RequestLaunchTemplateData{
DisableApiTermination: fi.Bool(false),
EbsOptimized: t.RootVolumeOptimization,
ImageId: image.ImageId,
InstanceType: t.InstanceType,
},
LaunchTemplateName: aws.String(name),
}
lc := input.LaunchTemplateData
// @step: add the actual block device mappings
rootDevices, err := t.buildRootDevice(c.Cloud)
if err != nil {
return err
}
ephemeralDevices, err := buildEphemeralDevices(c.Cloud, fi.StringValue(t.InstanceType))
if err != nil {
return 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 {
input.LaunchTemplateData.BlockDeviceMappings = append(input.LaunchTemplateData.BlockDeviceMappings, device.ToLaunchTemplateBootDeviceRequest(name))
}
}
// @step: add the ssh key
if t.SSHKey != nil {
lc.KeyName = t.SSHKey.Name
}
var securityGroups []*string
// @step: add the security groups
for _, sg := range t.SecurityGroups {
securityGroups = append(securityGroups, sg.ID)
}
// @step: add any tenacy details
if t.Tenancy != nil {
lc.Placement = &ec2.LaunchTemplatePlacementRequest{Tenancy: t.Tenancy}
}
// @step: set the instance monitoring
lc.Monitoring = &ec2.LaunchTemplatesMonitoringRequest{Enabled: fi.Bool(false)}
if t.InstanceMonitoring != nil {
lc.Monitoring = &ec2.LaunchTemplatesMonitoringRequest{Enabled: t.InstanceMonitoring}
}
// @step: add the iam instance profile
if t.IAMInstanceProfile != nil {
lc.IamInstanceProfile = &ec2.LaunchTemplateIamInstanceProfileSpecificationRequest{
Name: t.IAMInstanceProfile.Name,
}
}
// @step: are the node publicly facing
if fi.BoolValue(t.AssociatePublicIP) {
lc.NetworkInterfaces = append(lc.NetworkInterfaces,
&ec2.LaunchTemplateInstanceNetworkInterfaceSpecificationRequest{
AssociatePublicIpAddress: t.AssociatePublicIP,
DeleteOnTermination: aws.Bool(true),
DeviceIndex: fi.Int64(0),
Groups: securityGroups,
})
} else {
lc.SecurityGroupIds = securityGroups
}
// @step: add the userdata
if t.UserData != nil {
d, err := t.UserData.AsBytes()
if err != nil {
return fmt.Errorf("error rendering LaunchTemplate UserData: %v", err)
}
lc.UserData = aws.String(base64.StdEncoding.EncodeToString(d))
}
// @step: attempt to create the launch template
err = func() error {
for attempt := 0; attempt < 10; attempt++ {
if _, err = c.Cloud.EC2().CreateLaunchTemplate(input); err == nil {
return nil
}
if awsup.AWSErrorCode(err) == "ValidationError" {
message := awsup.AWSErrorMessage(err)
if strings.Contains(message, "not authorized") || strings.Contains(message, "Invalid IamInstance") {
if attempt > 10 {
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(ep.IAMInstanceProfile.Name), message)
time.Sleep(5 * time.Second)
continue
}
klog.V(4).Infof("ErrorCode=%q, Message=%q", awsup.AWSErrorCode(err), awsup.AWSErrorMessage(err))
}
}
return err
}()
if err != nil {
return fmt.Errorf("failed to create aws launch template: %s", err)
}
ep.ID = fi.String(name)
return nil
}
// Find is responsible for finding the launch template for us
func (t *LaunchTemplate) Find(c *fi.Context) (*LaunchTemplate, error) {
cloud, ok := c.Cloud.(awsup.AWSCloud)
if !ok {
return nil, fmt.Errorf("invalid cloud provider: %v, expected: %s", c.Cloud, "awsup.AWSCloud")
}
// @step: get the latest launch template version
lt, err := t.findLatestLaunchTemplate(c)
if err != nil {
return nil, err
}
if lt == nil {
return nil, nil
}
klog.V(3).Infof("found existing LaunchTemplate: %s", fi.StringValue(lt.LaunchTemplateName))
actual := &LaunchTemplate{
AssociatePublicIP: fi.Bool(false),
ID: lt.LaunchTemplateName,
ImageID: lt.LaunchTemplateData.ImageId,
InstanceMonitoring: fi.Bool(false),
InstanceType: lt.LaunchTemplateData.InstanceType,
Lifecycle: t.Lifecycle,
Name: t.Name,
RootVolumeOptimization: lt.LaunchTemplateData.EbsOptimized,
}
// @step: check if any of the interfaces are public facing
for _, x := range lt.LaunchTemplateData.NetworkInterfaces {
if aws.BoolValue(x.AssociatePublicIpAddress) {
actual.AssociatePublicIP = fi.Bool(true)
// @note: not sure i like this https://github.com/hashicorp/terraform/issues/2998
for _, id := range x.Groups {
actual.SecurityGroups = append(actual.SecurityGroups, &SecurityGroup{ID: id})
}
}
}
// @step: add at the security groups
for _, id := range lt.LaunchTemplateData.SecurityGroupIds {
actual.SecurityGroups = append(actual.SecurityGroups, &SecurityGroup{ID: id})
}
sort.Sort(OrderSecurityGroupsById(actual.SecurityGroups))
// @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 {
actual.Tenancy = 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: get the image is order to find out the root device name as using the index
// is not vaiable, under conditions they move
image, err := cloud.ResolveImage(fi.StringValue(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.StringValue(b.DeviceName) == fi.StringValue(image.RootDeviceName) {
actual.RootVolumeSize = b.Ebs.VolumeSize
actual.RootVolumeType = b.Ebs.VolumeType
actual.RootVolumeIops = b.Ebs.Iops
} else {
_, d := BlockDeviceMappingFromLaunchTemplateBootDeviceRequest(b)
actual.BlockDeviceMappings = append(actual.BlockDeviceMappings, d)
}
}
if lt.LaunchTemplateData.UserData != nil {
ud, err := base64.StdEncoding.DecodeString(aws.StringValue(lt.LaunchTemplateData.UserData))
if err != nil {
return nil, fmt.Errorf("error decoding userdata: %s", err)
}
actual.UserData = fi.WrapResource(fi.NewStringResource(string(ud)))
}
// @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.StringValue(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.Context) ([]*ec2.LaunchTemplate, error) {
var list []*ec2.LaunchTemplate
cloud := c.Cloud.(awsup.AWSCloud)
var next *string
for {
resp, err := cloud.EC2().DescribeLaunchTemplates(&ec2.DescribeLaunchTemplatesInput{
NextToken: next,
})
if err != nil {
return nil, err
}
for _, x := range resp.LaunchTemplates {
list = append(list, x)
}
if resp.NextToken == nil {
return list, nil
}
next = resp.NextToken
}
}
// findAllLaunchTemplateVersions returns all the launch templates versions for us
func (t *LaunchTemplate) findAllLaunchTemplatesVersions(c *fi.Context) ([]*ec2.LaunchTemplateVersion, error) {
var list []*ec2.LaunchTemplateVersion
cloud, ok := c.Cloud.(awsup.AWSCloud)
if !ok {
return []*ec2.LaunchTemplateVersion{}, fmt.Errorf("invalid cloud provider: %v, expected: awsup.AWSCloud", c.Cloud)
}
templates, err := t.findAllLaunchTemplates(c)
if err != nil {
return nil, err
}
var next *string
for _, x := range templates {
err := func() error {
for {
resp, err := cloud.EC2().DescribeLaunchTemplateVersions(&ec2.DescribeLaunchTemplateVersionsInput{
LaunchTemplateName: x.LaunchTemplateName,
NextToken: next,
})
if err != nil {
return err
}
for _, x := range resp.LaunchTemplateVersions {
list = append(list, x)
}
if resp.NextToken == nil {
return nil
}
next = resp.NextToken
}
}()
if err != nil {
return nil, err
}
}
return list, nil
}
// findLaunchTemplates returns a list of launch templates
func (t *LaunchTemplate) findLaunchTemplates(c *fi.Context) ([]*ec2.LaunchTemplateVersion, error) {
// @step: get a list of the launch templates
list, err := t.findAllLaunchTemplatesVersions(c)
if err != nil {
return nil, err
}
prefix := fmt.Sprintf("%s-", fi.StringValue(t.Name))
// @step: filter out the templates we are interested in
var filtered []*ec2.LaunchTemplateVersion
for _, x := range list {
if strings.HasPrefix(aws.StringValue(x.LaunchTemplateName), prefix) {
filtered = append(filtered, x)
}
}
// @step: we can sort the configurations in chronological order
sort.Slice(filtered, func(i, j int) bool {
ti := filtered[i].CreateTime
tj := filtered[j].CreateTime
if tj == nil {
return true
}
if ti == nil {
return false
}
return ti.UnixNano() < tj.UnixNano()
})
return filtered, nil
}
// findLatestLaunchTemplate returns the latest template
func (t *LaunchTemplate) findLatestLaunchTemplate(c *fi.Context) (*ec2.LaunchTemplateVersion, error) {
// @step: get a list of configuration
configurations, err := t.findLaunchTemplates(c)
if err != nil {
return nil, err
}
if len(configurations) == 0 {
return nil, nil
}
return configurations[len(configurations)-1], nil
}
// deleteLaunchTemplate tracks a LaunchConfiguration that we're going to delete
// It implements fi.Deletion
type deleteLaunchTemplate struct {
lc *ec2.LaunchTemplateVersion
}
var _ fi.Deletion = &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.StringValue(d.lc.LaunchTemplateName)
}
func (d *deleteLaunchTemplate) Delete(t fi.Target) error {
awsTarget, ok := t.(*awsup.AWSAPITarget)
if !ok {
return fmt.Errorf("unexpected target type for deletion: %T", t)
}
if _, err := awsTarget.Cloud.EC2().DeleteLaunchTemplate(&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()
}