From 27c055d895f65361b4023d0d88e2668c7cda277f Mon Sep 17 00:00:00 2001 From: Andrew Rudoi Date: Mon, 13 Jul 2020 16:41:09 -0700 Subject: [PATCH] feat: look up official EKS AMI when appropriate --- .../bootstrap/cluster_api_controller.go | 11 +++++ .../bootstrap/fixtures/with_eks_enable.yaml | 5 +++ go.mod | 1 + pkg/cloud/scope/clients.go | 12 ++++++ pkg/cloud/services/ec2/ami.go | 41 ++++++++++++++++++ pkg/cloud/services/ec2/instances.go | 43 ++++++++++++------- pkg/cloud/services/ec2/service.go | 5 +++ 7 files changed, 103 insertions(+), 15 deletions(-) diff --git a/cmd/clusterawsadm/cloudformation/bootstrap/cluster_api_controller.go b/cmd/clusterawsadm/cloudformation/bootstrap/cluster_api_controller.go index ccdd4e6bf7..2b6e740e38 100644 --- a/cmd/clusterawsadm/cloudformation/bootstrap/cluster_api_controller.go +++ b/cmd/clusterawsadm/cloudformation/bootstrap/cluster_api_controller.go @@ -151,6 +151,16 @@ func (t Template) controllersPolicy() *iamv1.PolicyDocument { "iam:GetRole", "iam:ListAttachedRolePolicies", } + statement = append(statement, iamv1.StatementEntry{ + Effect: iamv1.EffectAllow, + Resource: iamv1.Resources{ + "arn:aws:ssm:*:*:parameter/aws/service/eks/optimized-ami/*", + }, + Action: iamv1.Actions{ + "ssm:GetParameter", + }, + }) + if t.Spec.ClusterAPIControllers.EKS.IAMRoleCreation { allowedIAMActions = append(allowedIAMActions, iamv1.Actions{ "iam:DetachRolePolicy", @@ -206,6 +216,7 @@ func (t Template) controllersPolicy() *iamv1.PolicyDocument { }, }...) } + return &iamv1.PolicyDocument{ Version: iamv1.CurrentVersion, Statement: statement, diff --git a/cmd/clusterawsadm/cloudformation/bootstrap/fixtures/with_eks_enable.yaml b/cmd/clusterawsadm/cloudformation/bootstrap/fixtures/with_eks_enable.yaml index 6f4aee5c29..7e5ed184be 100644 --- a/cmd/clusterawsadm/cloudformation/bootstrap/fixtures/with_eks_enable.yaml +++ b/cmd/clusterawsadm/cloudformation/bootstrap/fixtures/with_eks_enable.yaml @@ -214,6 +214,11 @@ Resources: Effect: Allow Resource: - arn:*:secretsmanager:*:*:secret:aws.cluster.x-k8s.io/* + - Action: + - ssm:GetParameter + Effect: Allow + Resource: + - arn:aws:ssm:*:*:parameter/aws/service/eks/optimized-ami/* - Action: - iam:GetRole - iam:ListAttachedRolePolicies diff --git a/go.mod b/go.mod index 1eba363931..a2f2fb9287 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.13 require ( github.com/aws/aws-sdk-go v1.34.10 github.com/awslabs/goformation/v4 v4.11.0 + github.com/blang/semver v3.5.1+incompatible github.com/go-logr/logr v0.1.0 github.com/golang/mock v1.4.3 github.com/google/goexpect v0.0.0-20200703111054-623d5ca06f56 diff --git a/pkg/cloud/scope/clients.go b/pkg/cloud/scope/clients.go index 15175ff853..16b5c3fb1c 100644 --- a/pkg/cloud/scope/clients.go +++ b/pkg/cloud/scope/clients.go @@ -31,6 +31,8 @@ import ( "github.com/aws/aws-sdk-go/service/resourcegroupstaggingapi/resourcegroupstaggingapiiface" "github.com/aws/aws-sdk-go/service/secretsmanager" "github.com/aws/aws-sdk-go/service/secretsmanager/secretsmanageriface" + "github.com/aws/aws-sdk-go/service/ssm" + "github.com/aws/aws-sdk-go/service/ssm/ssmiface" "github.com/aws/aws-sdk-go/service/sts" "github.com/aws/aws-sdk-go/service/sts/stsiface" @@ -112,6 +114,16 @@ func NewSTSClient(scopeUser cloud.ScopeUsage, session cloud.Session, target runt return stsClient } +// NewSSMClient creates a new Secrets API client for a given session +func NewSSMClient(scopeUser cloud.ScopeUsage, session cloud.Session, target runtime.Object) ssmiface.SSMAPI { + ssmClient := ssm.New(session.Session()) + ssmClient.Handlers.Build.PushFrontNamed(getUserAgentHandler()) + ssmClient.Handlers.CompleteAttempt.PushFront(awsmetrics.CaptureRequestMetrics(scopeUser.ControllerName())) + ssmClient.Handlers.Complete.PushBack(recordAWSPermissionsIssue(target)) + + return ssmClient +} + func recordAWSPermissionsIssue(target runtime.Object) func(r *request.Request) { return func(r *request.Request) { if awsErr, ok := r.Error.(awserr.Error); ok { diff --git a/pkg/cloud/services/ec2/ami.go b/pkg/cloud/services/ec2/ami.go index 7453c6f9a2..3191356665 100644 --- a/pkg/cloud/services/ec2/ami.go +++ b/pkg/cloud/services/ec2/ami.go @@ -18,6 +18,7 @@ package ec2 import ( "bytes" + "fmt" "sort" "strings" "text/template" @@ -25,6 +26,8 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go/service/ssm" + "github.com/blang/semver" "github.com/pkg/errors" "sigs.k8s.io/cluster-api-provider-aws/pkg/record" ) @@ -48,6 +51,9 @@ const ( // Amazon's AMI timestamp format createDateTimestampFormat = "2006-01-02T15:04:05.000Z" + + // EKS AMI ID SSM Parameter name + eksAmiSSMParameterFormat = "/aws/service/eks/optimized-ami/%s/amazon-linux-2/recommended/image_id" ) // AMILookup contains the parameters used to template AMI names used for lookup. @@ -202,3 +208,38 @@ func (s *Service) defaultBastionAMILookup(region string) string { return "unknown region" } } + +func (s *Service) eksAMILookup(kubernetesVersion string) (string, error) { + // format ssm parameter path properly + formattedVersion, err := formatVersionForEKS(kubernetesVersion) + if err != nil { + return "", err + } + + paramName := fmt.Sprintf(eksAmiSSMParameterFormat, formattedVersion) + + input := &ssm.GetParameterInput{ + Name: aws.String(paramName), + } + + out, err := s.SSMClient.GetParameter(input) + if err != nil { + record.Eventf(s.scope.InfraCluster(), "FailedGetParameter", "Failed to get ami SSM parameter %q: %v", paramName, err) + return "", errors.Wrapf(err, "failed to get ami SSM parameter: %q", paramName) + } + + if out.Parameter.Value == nil { + return "", errors.Errorf("SSM parameter returned with nil value: %q", paramName) + } + + return aws.StringValue(out.Parameter.Value), nil +} + +func formatVersionForEKS(version string) (string, error) { + parsed, err := semver.ParseTolerant(version) + if err != nil { + return "", err + } + + return fmt.Sprintf("%d.%d", parsed.Major, parsed.Minor), nil +} diff --git a/pkg/cloud/services/ec2/instances.go b/pkg/cloud/services/ec2/instances.go index cd470a0f51..20db64d685 100644 --- a/pkg/cloud/services/ec2/instances.go +++ b/pkg/cloud/services/ec2/instances.go @@ -140,24 +140,31 @@ func (s *Service) CreateInstance(scope *scope.MachineScope, userData []byte) (*i return nil, err } - imageLookupFormat := scope.AWSMachine.Spec.ImageLookupFormat - if imageLookupFormat == "" { - imageLookupFormat = scope.InfraCluster.ImageLookupFormat() - } + if s.shouldUseEksAMI() { + input.ImageID, err = s.eksAMILookup(*scope.Machine.Spec.Version) + if err != nil { + return nil, err + } + } else { + imageLookupFormat := scope.AWSMachine.Spec.ImageLookupFormat + if imageLookupFormat == "" { + imageLookupFormat = scope.InfraCluster.ImageLookupFormat() + } - imageLookupOrg := scope.AWSMachine.Spec.ImageLookupOrg - if imageLookupOrg == "" { - imageLookupOrg = scope.InfraCluster.ImageLookupOrg() - } + imageLookupOrg := scope.AWSMachine.Spec.ImageLookupOrg + if imageLookupOrg == "" { + imageLookupOrg = scope.InfraCluster.ImageLookupOrg() + } - imageLookupBaseOS := scope.AWSMachine.Spec.ImageLookupBaseOS - if imageLookupBaseOS == "" { - imageLookupBaseOS = scope.InfraCluster.ImageLookupBaseOS() - } + imageLookupBaseOS := scope.AWSMachine.Spec.ImageLookupBaseOS + if imageLookupBaseOS == "" { + imageLookupBaseOS = scope.InfraCluster.ImageLookupBaseOS() + } - input.ImageID, err = s.defaultAMILookup(imageLookupFormat, imageLookupOrg, imageLookupBaseOS, *scope.Machine.Spec.Version) - if err != nil { - return nil, err + input.ImageID, err = s.defaultAMILookup(imageLookupFormat, imageLookupOrg, imageLookupBaseOS, *scope.Machine.Spec.Version) + if err != nil { + return nil, err + } } } @@ -812,6 +819,12 @@ func (s *Service) DetachSecurityGroupsFromNetworkInterface(groups []string, inte return nil } +func (s *Service) shouldUseEksAMI() bool { + gvk := s.scope.InfraCluster().GetObjectKind().GroupVersionKind() + + return gvk.Kind == "AWSManagedControlPlane" +} + // filterGroups filters a list for a string. func filterGroups(list []string, strToFilter string) (newList []string) { for _, item := range list { diff --git a/pkg/cloud/services/ec2/service.go b/pkg/cloud/services/ec2/service.go index 5dd7c9e0a1..e8beac1005 100644 --- a/pkg/cloud/services/ec2/service.go +++ b/pkg/cloud/services/ec2/service.go @@ -18,6 +18,7 @@ package ec2 import ( "github.com/aws/aws-sdk-go/service/ec2/ec2iface" + "github.com/aws/aws-sdk-go/service/ssm/ssmiface" "sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/scope" ) @@ -28,6 +29,9 @@ import ( type Service struct { scope scope.EC2Scope EC2Client ec2iface.EC2API + + // SSMClient is used to look up the official EKS AMI ID + SSMClient ssmiface.SSMAPI } // NewService returns a new service given the ec2 api client. @@ -35,5 +39,6 @@ func NewService(clusterScope scope.EC2Scope) *Service { return &Service{ scope: clusterScope, EC2Client: scope.NewEC2Client(clusterScope, clusterScope, clusterScope.InfraCluster()), + SSMClient: scope.NewSSMClient(clusterScope, clusterScope, clusterScope.InfraCluster()), } }