sigs.k8s.io/cluster-api-provider-aws@v1.5.5/pkg/cloud/services/ec2/launchtemplate.go (about)

     1  /*
     2  Copyright 2018 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8  	http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package ec2
    18  
    19  import (
    20  	"encoding/base64"
    21  	"fmt"
    22  	"sort"
    23  	"strconv"
    24  	"strings"
    25  
    26  	"github.com/aws/aws-sdk-go/aws"
    27  	"github.com/aws/aws-sdk-go/service/ec2"
    28  	"github.com/google/go-cmp/cmp"
    29  	"github.com/pkg/errors"
    30  	"k8s.io/utils/pointer"
    31  
    32  	infrav1 "sigs.k8s.io/cluster-api-provider-aws/api/v1beta1"
    33  	expinfrav1 "sigs.k8s.io/cluster-api-provider-aws/exp/api/v1beta1"
    34  	"sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/awserrors"
    35  	"sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/scope"
    36  	"sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/services/userdata"
    37  )
    38  
    39  // GetLaunchTemplate returns the existing LaunchTemplate or nothing if it doesn't exist.
    40  // For now by name until we need the input to be something different.
    41  func (s *Service) GetLaunchTemplate(launchTemplateName string) (*expinfrav1.AWSLaunchTemplate, string, error) {
    42  	if launchTemplateName == "" {
    43  		return nil, "", nil
    44  	}
    45  
    46  	s.scope.V(2).Info("Looking for existing LaunchTemplates")
    47  
    48  	input := &ec2.DescribeLaunchTemplateVersionsInput{
    49  		LaunchTemplateName: aws.String(launchTemplateName),
    50  		Versions:           aws.StringSlice([]string{expinfrav1.LaunchTemplateLatestVersion}),
    51  	}
    52  
    53  	out, err := s.EC2Client.DescribeLaunchTemplateVersions(input)
    54  	switch {
    55  	case awserrors.IsNotFound(err):
    56  		return nil, "", nil
    57  	case err != nil:
    58  		return nil, "", err
    59  	}
    60  
    61  	if out == nil || out.LaunchTemplateVersions == nil || len(out.LaunchTemplateVersions) == 0 {
    62  		return nil, "", nil
    63  	}
    64  
    65  	return s.SDKToLaunchTemplate(out.LaunchTemplateVersions[0])
    66  }
    67  
    68  // GetLaunchTemplateID returns the existing LaunchTemplateId or empty string if it doesn't exist.
    69  func (s *Service) GetLaunchTemplateID(launchTemplateName string) (string, error) {
    70  	if launchTemplateName == "" {
    71  		return "", nil
    72  	}
    73  
    74  	input := &ec2.DescribeLaunchTemplateVersionsInput{
    75  		LaunchTemplateName: aws.String(launchTemplateName),
    76  		Versions:           aws.StringSlice([]string{expinfrav1.LaunchTemplateLatestVersion}),
    77  	}
    78  
    79  	out, err := s.EC2Client.DescribeLaunchTemplateVersions(input)
    80  	switch {
    81  	case awserrors.IsNotFound(err):
    82  		return "", nil
    83  	case err != nil:
    84  		s.scope.Info("", "aerr", err.Error())
    85  		return "", err
    86  	}
    87  
    88  	if out == nil || out.LaunchTemplateVersions == nil || len(out.LaunchTemplateVersions) == 0 {
    89  		return "", nil
    90  	}
    91  
    92  	return aws.StringValue(out.LaunchTemplateVersions[0].LaunchTemplateId), nil
    93  }
    94  
    95  // CreateLaunchTemplate generates a launch template to be used with the autoscaling group.
    96  func (s *Service) CreateLaunchTemplate(scope *scope.MachinePoolScope, imageID *string, userData []byte) (string, error) {
    97  	s.scope.Info("Create a new launch template")
    98  
    99  	launchTemplateData, err := s.createLaunchTemplateData(scope, imageID, userData)
   100  	if err != nil {
   101  		return "", errors.Wrapf(err, "unable to form launch template data")
   102  	}
   103  
   104  	input := &ec2.CreateLaunchTemplateInput{
   105  		LaunchTemplateData: launchTemplateData,
   106  		LaunchTemplateName: aws.String(scope.Name()),
   107  	}
   108  
   109  	additionalTags := scope.AdditionalTags()
   110  	// Set the cloud provider tag
   111  	additionalTags[infrav1.ClusterAWSCloudProviderTagKey(s.scope.Name())] = string(infrav1.ResourceLifecycleOwned)
   112  
   113  	tags := infrav1.Build(infrav1.BuildParams{
   114  		ClusterName: s.scope.Name(),
   115  		Lifecycle:   infrav1.ResourceLifecycleOwned,
   116  		Name:        aws.String(scope.Name()),
   117  		Role:        aws.String("node"),
   118  		Additional:  additionalTags,
   119  	})
   120  
   121  	if len(tags) > 0 {
   122  		spec := &ec2.TagSpecification{ResourceType: aws.String(ec2.ResourceTypeLaunchTemplate)}
   123  		for key, value := range tags {
   124  			spec.Tags = append(spec.Tags, &ec2.Tag{
   125  				Key:   aws.String(key),
   126  				Value: aws.String(value),
   127  			})
   128  		}
   129  		input.TagSpecifications = append(input.TagSpecifications, spec)
   130  	}
   131  
   132  	result, err := s.EC2Client.CreateLaunchTemplate(input)
   133  	if err != nil {
   134  		return "", err
   135  	}
   136  	return aws.StringValue(result.LaunchTemplate.LaunchTemplateId), nil
   137  }
   138  
   139  // CreateLaunchTemplateVersion will create a launch template.
   140  func (s *Service) CreateLaunchTemplateVersion(scope *scope.MachinePoolScope, imageID *string, userData []byte) error {
   141  	s.scope.V(2).Info("creating new launch template version", "machine-pool", scope.Name())
   142  
   143  	launchTemplateData, err := s.createLaunchTemplateData(scope, imageID, userData)
   144  	if err != nil {
   145  		return errors.Wrapf(err, "unable to form launch template data")
   146  	}
   147  
   148  	input := &ec2.CreateLaunchTemplateVersionInput{
   149  		LaunchTemplateData: launchTemplateData,
   150  		LaunchTemplateId:   aws.String(scope.AWSMachinePool.Status.LaunchTemplateID),
   151  	}
   152  
   153  	_, err = s.EC2Client.CreateLaunchTemplateVersion(input)
   154  	if err != nil {
   155  		return errors.Wrapf(err, "unable to create launch template version")
   156  	}
   157  
   158  	return nil
   159  }
   160  
   161  func (s *Service) createLaunchTemplateData(scope *scope.MachinePoolScope, imageID *string, userData []byte) (*ec2.RequestLaunchTemplateData, error) {
   162  	lt := scope.AWSMachinePool.Spec.AWSLaunchTemplate
   163  
   164  	// An explicit empty string for SSHKeyName means do not specify a key in the ASG launch
   165  	var sshKeyNamePtr *string
   166  	if lt.SSHKeyName != nil && *lt.SSHKeyName != "" {
   167  		sshKeyNamePtr = lt.SSHKeyName
   168  	}
   169  
   170  	data := &ec2.RequestLaunchTemplateData{
   171  		InstanceType: aws.String(lt.InstanceType),
   172  		IamInstanceProfile: &ec2.LaunchTemplateIamInstanceProfileSpecificationRequest{
   173  			Name: aws.String(lt.IamInstanceProfile),
   174  		},
   175  		KeyName:  sshKeyNamePtr,
   176  		UserData: pointer.StringPtr(base64.StdEncoding.EncodeToString(userData)),
   177  	}
   178  
   179  	ids, err := s.GetCoreNodeSecurityGroups(scope)
   180  	if err != nil {
   181  		return nil, err
   182  	}
   183  
   184  	for _, id := range ids {
   185  		data.SecurityGroupIds = append(data.SecurityGroupIds, aws.String(id))
   186  	}
   187  
   188  	// add additional security groups as well
   189  	securityGroupIDs, err := s.GetAdditionalSecurityGroupsIDs(scope.AWSMachinePool.Spec.AWSLaunchTemplate.AdditionalSecurityGroups)
   190  	if err != nil {
   191  		return nil, err
   192  	}
   193  	data.SecurityGroupIds = append(data.SecurityGroupIds, aws.StringSlice(securityGroupIDs)...)
   194  
   195  	// set the AMI ID
   196  	data.ImageId = imageID
   197  
   198  	// Set up root volume
   199  	if lt.RootVolume != nil {
   200  		rootDeviceName, err := s.checkRootVolume(lt.RootVolume, *data.ImageId)
   201  		if err != nil {
   202  			return nil, err
   203  		}
   204  
   205  		lt.RootVolume.DeviceName = aws.StringValue(rootDeviceName)
   206  
   207  		req := volumeToLaunchTemplateBlockDeviceMappingRequest(lt.RootVolume)
   208  		data.BlockDeviceMappings = []*ec2.LaunchTemplateBlockDeviceMappingRequest{
   209  			req,
   210  		}
   211  	}
   212  
   213  	data.TagSpecifications = s.buildLaunchTemplateTagSpecificationRequest(scope)
   214  
   215  	return data, nil
   216  }
   217  
   218  func volumeToLaunchTemplateBlockDeviceMappingRequest(v *infrav1.Volume) *ec2.LaunchTemplateBlockDeviceMappingRequest {
   219  	ltEbsDevice := &ec2.LaunchTemplateEbsBlockDeviceRequest{
   220  		DeleteOnTermination: aws.Bool(true),
   221  		VolumeSize:          aws.Int64(v.Size),
   222  		Encrypted:           v.Encrypted,
   223  	}
   224  
   225  	if v.Throughput != nil {
   226  		ltEbsDevice.Throughput = v.Throughput
   227  	}
   228  
   229  	if v.IOPS != 0 {
   230  		ltEbsDevice.Iops = aws.Int64(v.IOPS)
   231  	}
   232  
   233  	if v.EncryptionKey != "" {
   234  		ltEbsDevice.Encrypted = aws.Bool(true)
   235  		ltEbsDevice.KmsKeyId = aws.String(v.EncryptionKey)
   236  	}
   237  
   238  	if v.Type != "" {
   239  		ltEbsDevice.VolumeType = aws.String(string(v.Type))
   240  	}
   241  
   242  	return &ec2.LaunchTemplateBlockDeviceMappingRequest{
   243  		DeviceName: &v.DeviceName,
   244  		Ebs:        ltEbsDevice,
   245  	}
   246  }
   247  
   248  // DeleteLaunchTemplate delete a launch template.
   249  func (s *Service) DeleteLaunchTemplate(id string) error {
   250  	s.scope.V(2).Info("Deleting launch template", "id", id)
   251  
   252  	input := &ec2.DeleteLaunchTemplateInput{
   253  		LaunchTemplateId: aws.String(id),
   254  	}
   255  
   256  	if _, err := s.EC2Client.DeleteLaunchTemplate(input); err != nil {
   257  		return errors.Wrapf(err, "failed to delete launch template %q", id)
   258  	}
   259  
   260  	s.scope.V(2).Info("Deleted launch template", "id", id)
   261  	return nil
   262  }
   263  
   264  // PruneLaunchTemplateVersions deletes one old launch template version.
   265  // It does not delete the "latest" version, because that version may still be in use.
   266  // It does not delete the "default" version, because that version cannot be deleted.
   267  // It does not assume that versions are sequential. Versions may be deleted out of band.
   268  func (s *Service) PruneLaunchTemplateVersions(id string) error {
   269  	// When there is one version available, it is the default and the latest.
   270  	// When there are two versions available, one the is the default, the other is the latest.
   271  	// Therefore we only prune when there are at least 3 versions available.
   272  	const minCountToAllowPrune = 3
   273  
   274  	input := &ec2.DescribeLaunchTemplateVersionsInput{
   275  		LaunchTemplateId: aws.String(id),
   276  		MinVersion:       aws.String("0"),
   277  		MaxVersion:       aws.String(expinfrav1.LaunchTemplateLatestVersion),
   278  		MaxResults:       aws.Int64(minCountToAllowPrune),
   279  	}
   280  
   281  	out, err := s.EC2Client.DescribeLaunchTemplateVersions(input)
   282  	if err != nil {
   283  		s.scope.Info("", "aerr", err.Error())
   284  		return err
   285  	}
   286  
   287  	// len(out.LaunchTemplateVersions)	|	items
   288  	// -------------------------------- + -----------------------
   289  	// 								1	|	[default/latest]
   290  	// 								2	|	[default, latest]
   291  	// 								3	| 	[default, versionToPrune, latest]
   292  	if len(out.LaunchTemplateVersions) < minCountToAllowPrune {
   293  		return nil
   294  	}
   295  	versionToPrune := out.LaunchTemplateVersions[1].VersionNumber
   296  	return s.deleteLaunchTemplateVersion(id, versionToPrune)
   297  }
   298  
   299  func (s *Service) deleteLaunchTemplateVersion(id string, version *int64) error {
   300  	s.scope.V(2).Info("Deleting launch template version", "id", id)
   301  
   302  	if version == nil {
   303  		return errors.New("version is a nil pointer")
   304  	}
   305  	versions := []string{strconv.FormatInt(*version, 10)}
   306  
   307  	input := &ec2.DeleteLaunchTemplateVersionsInput{
   308  		LaunchTemplateId: aws.String(id),
   309  		Versions:         aws.StringSlice(versions),
   310  	}
   311  
   312  	_, err := s.EC2Client.DeleteLaunchTemplateVersions(input)
   313  	if err != nil {
   314  		return err
   315  	}
   316  
   317  	s.scope.V(2).Info("Deleted launch template", "id", id, "version", *version)
   318  	return nil
   319  }
   320  
   321  // SDKToLaunchTemplate converts an AWS EC2 SDK instance to the CAPA instance type.
   322  func (s *Service) SDKToLaunchTemplate(d *ec2.LaunchTemplateVersion) (*expinfrav1.AWSLaunchTemplate, string, error) {
   323  	v := d.LaunchTemplateData
   324  	i := &expinfrav1.AWSLaunchTemplate{
   325  		Name: aws.StringValue(d.LaunchTemplateName),
   326  		AMI: infrav1.AMIReference{
   327  			ID: v.ImageId,
   328  		},
   329  		IamInstanceProfile: aws.StringValue(v.IamInstanceProfile.Name),
   330  		InstanceType:       aws.StringValue(v.InstanceType),
   331  		SSHKeyName:         v.KeyName,
   332  		VersionNumber:      d.VersionNumber,
   333  	}
   334  
   335  	// Extract IAM Instance Profile name from ARN
   336  	if v.IamInstanceProfile != nil && v.IamInstanceProfile.Arn != nil {
   337  		split := strings.Split(aws.StringValue(v.IamInstanceProfile.Arn), "instance-profile/")
   338  		if len(split) > 1 && split[1] != "" {
   339  			i.IamInstanceProfile = split[1]
   340  		}
   341  	}
   342  
   343  	for _, id := range v.SecurityGroupIds {
   344  		// FIXME(dlipovetsky): This will include the core security groups as well, making the
   345  		// "Additional" a bit dishonest. However, including the core groups drastically simplifies
   346  		// comparison with the incoming security groups.
   347  		i.AdditionalSecurityGroups = append(i.AdditionalSecurityGroups, infrav1.AWSResourceReference{ID: id})
   348  	}
   349  
   350  	if v.UserData == nil {
   351  		return i, userdata.ComputeHash(nil), nil
   352  	}
   353  	decodedUserData, err := base64.StdEncoding.DecodeString(*v.UserData)
   354  	if err != nil {
   355  		return nil, "", errors.Wrap(err, "unable to decode UserData")
   356  	}
   357  
   358  	return i, userdata.ComputeHash(decodedUserData), nil
   359  }
   360  
   361  // LaunchTemplateNeedsUpdate checks if a new launch template version is needed.
   362  //
   363  // FIXME(dlipovetsky): This check should account for changed userdata, but does not yet do so.
   364  // Although userdata is stored in an EC2 Launch Template, it is not a field of AWSLaunchTemplate.
   365  func (s *Service) LaunchTemplateNeedsUpdate(scope *scope.MachinePoolScope, incoming *expinfrav1.AWSLaunchTemplate, existing *expinfrav1.AWSLaunchTemplate) (bool, error) {
   366  	if incoming.IamInstanceProfile != existing.IamInstanceProfile {
   367  		return true, nil
   368  	}
   369  
   370  	if incoming.InstanceType != existing.InstanceType {
   371  		return true, nil
   372  	}
   373  
   374  	incomingIDs, err := s.GetAdditionalSecurityGroupsIDs(incoming.AdditionalSecurityGroups)
   375  	if err != nil {
   376  		return false, err
   377  	}
   378  
   379  	coreIDs, err := s.GetCoreNodeSecurityGroups(scope)
   380  	if err != nil {
   381  		return false, err
   382  	}
   383  
   384  	incomingIDs = append(incomingIDs, coreIDs...)
   385  	existingIDs, err := s.GetAdditionalSecurityGroupsIDs(existing.AdditionalSecurityGroups)
   386  	if err != nil {
   387  		return false, err
   388  	}
   389  	sort.Strings(incomingIDs)
   390  	sort.Strings(existingIDs)
   391  
   392  	if !cmp.Equal(incomingIDs, existingIDs) {
   393  		return true, nil
   394  	}
   395  
   396  	return false, nil
   397  }
   398  
   399  // DiscoverLaunchTemplateAMI will discover the AMI launch template.
   400  func (s *Service) DiscoverLaunchTemplateAMI(scope *scope.MachinePoolScope) (*string, error) {
   401  	lt := scope.AWSMachinePool.Spec.AWSLaunchTemplate
   402  
   403  	if lt.AMI.ID != nil {
   404  		return lt.AMI.ID, nil
   405  	}
   406  
   407  	if scope.MachinePool.Spec.Template.Spec.Version == nil {
   408  		err := errors.New("Either AWSMachinePool's spec.awslaunchtemplate.ami.id or MachinePool's spec.template.spec.version must be defined")
   409  		s.scope.Error(err, "")
   410  		return nil, err
   411  	}
   412  
   413  	var lookupAMI string
   414  	var err error
   415  
   416  	imageLookupFormat := lt.ImageLookupFormat
   417  	if imageLookupFormat == "" {
   418  		imageLookupFormat = scope.InfraCluster.ImageLookupFormat()
   419  	}
   420  
   421  	imageLookupOrg := lt.ImageLookupOrg
   422  	if imageLookupOrg == "" {
   423  		imageLookupOrg = scope.InfraCluster.ImageLookupOrg()
   424  	}
   425  
   426  	imageLookupBaseOS := lt.ImageLookupBaseOS
   427  	if imageLookupBaseOS == "" {
   428  		imageLookupBaseOS = scope.InfraCluster.ImageLookupBaseOS()
   429  	}
   430  
   431  	if scope.IsEKSManaged() && imageLookupFormat == "" && imageLookupOrg == "" && imageLookupBaseOS == "" {
   432  		lookupAMI, err = s.eksAMILookup(*scope.MachinePool.Spec.Template.Spec.Version, scope.AWSMachinePool.Spec.AWSLaunchTemplate.AMI.EKSOptimizedLookupType)
   433  		if err != nil {
   434  			return nil, err
   435  		}
   436  	} else {
   437  		lookupAMI, err = s.defaultAMIIDLookup(imageLookupFormat, imageLookupOrg, imageLookupBaseOS, *scope.MachinePool.Spec.Template.Spec.Version)
   438  		if err != nil {
   439  			return nil, err
   440  		}
   441  	}
   442  
   443  	return aws.String(lookupAMI), nil
   444  }
   445  
   446  func (s *Service) GetAdditionalSecurityGroupsIDs(securityGroups []infrav1.AWSResourceReference) ([]string, error) {
   447  	var additionalSecurityGroupsIDs []string
   448  
   449  	for _, sg := range securityGroups {
   450  		if sg.ID != nil {
   451  			additionalSecurityGroupsIDs = append(additionalSecurityGroupsIDs, *sg.ID)
   452  		} else if sg.Filters != nil {
   453  			id, err := s.getFilteredSecurityGroupID(sg)
   454  			if err != nil {
   455  				return nil, err
   456  			}
   457  
   458  			additionalSecurityGroupsIDs = append(additionalSecurityGroupsIDs, id)
   459  		}
   460  	}
   461  
   462  	return additionalSecurityGroupsIDs, nil
   463  }
   464  
   465  func (s *Service) buildLaunchTemplateTagSpecificationRequest(scope *scope.MachinePoolScope) []*ec2.LaunchTemplateTagSpecificationRequest {
   466  	tagSpecifications := make([]*ec2.LaunchTemplateTagSpecificationRequest, 0)
   467  	additionalTags := scope.AdditionalTags()
   468  	// Set the cloud provider tag
   469  	additionalTags[infrav1.ClusterAWSCloudProviderTagKey(s.scope.Name())] = string(infrav1.ResourceLifecycleOwned)
   470  
   471  	tags := infrav1.Build(infrav1.BuildParams{
   472  		ClusterName: s.scope.Name(),
   473  		Lifecycle:   infrav1.ResourceLifecycleOwned,
   474  		Name:        aws.String(scope.Name()),
   475  		Role:        aws.String("node"),
   476  		Additional:  additionalTags,
   477  	})
   478  
   479  	if len(tags) > 0 {
   480  		// tag instances
   481  		spec := &ec2.LaunchTemplateTagSpecificationRequest{ResourceType: aws.String(ec2.ResourceTypeInstance)}
   482  		for key, value := range tags {
   483  			spec.Tags = append(spec.Tags, &ec2.Tag{
   484  				Key:   aws.String(key),
   485  				Value: aws.String(value),
   486  			})
   487  		}
   488  		tagSpecifications = append(tagSpecifications, spec)
   489  
   490  		// tag EBS volumes
   491  		spec = &ec2.LaunchTemplateTagSpecificationRequest{ResourceType: aws.String(ec2.ResourceTypeVolume)}
   492  		for key, value := range tags {
   493  			spec.Tags = append(spec.Tags, &ec2.Tag{
   494  				Key:   aws.String(key),
   495  				Value: aws.String(value),
   496  			})
   497  		}
   498  		tagSpecifications = append(tagSpecifications, spec)
   499  	}
   500  	return tagSpecifications
   501  }
   502  
   503  // getFilteredSecurityGroupID get security group ID using filters.
   504  func (s *Service) getFilteredSecurityGroupID(securityGroup infrav1.AWSResourceReference) (string, error) {
   505  	if securityGroup.Filters == nil {
   506  		return "", nil
   507  	}
   508  
   509  	filters := []*ec2.Filter{}
   510  	for _, f := range securityGroup.Filters {
   511  		filters = append(filters, &ec2.Filter{Name: aws.String(f.Name), Values: aws.StringSlice(f.Values)})
   512  	}
   513  
   514  	sgs, err := s.EC2Client.DescribeSecurityGroups(&ec2.DescribeSecurityGroupsInput{Filters: filters})
   515  	if err != nil {
   516  		return "", err
   517  	}
   518  
   519  	if len(sgs.SecurityGroups) == 0 {
   520  		return "", fmt.Errorf("failed to find security group matching filters: %q, reason: %w", filters, err)
   521  	}
   522  
   523  	return *sgs.SecurityGroups[0].GroupId, nil
   524  }