mirror of https://github.com/kubernetes/kops.git
417 lines
12 KiB
Go
417 lines
12 KiB
Go
/*
|
|
Copyright 2018 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 publically 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()
|
|
}
|