From d32989481dc0bd53b15efd1f9c8d1eb5f5c44b8c Mon Sep 17 00:00:00 2001 From: Alberto Contreras Date: Tue, 25 Jun 2024 17:00:08 +0200 Subject: [PATCH] Add auto-ssm ami resolution for ubuntu Issue #3224 --- pkg/ami/ssm_resolver.go | 72 ++++++++++++- pkg/ami/ssm_resolver_test.go | 200 ++++++++++++++++++++++++++++++++++- pkg/eks/api_test.go | 31 ++++-- 3 files changed, 289 insertions(+), 14 deletions(-) diff --git a/pkg/ami/ssm_resolver.go b/pkg/ami/ssm_resolver.go index bbc147b88b..de0edbbefc 100644 --- a/pkg/ami/ssm_resolver.go +++ b/pkg/ami/ssm_resolver.go @@ -75,9 +75,19 @@ func MakeSSMParameterName(version, instanceType, imageFamily string) (string, er return fmt.Sprintf("/aws/service/ami-windows-latest/Windows_Server-2022-English-%s-EKS_Optimized-%s/%s", windowsAmiType(imageFamily), version, fieldName), nil case api.NodeImageFamilyBottlerocket: return fmt.Sprintf("/aws/service/bottlerocket/aws-k8s-%s/%s/latest/%s", imageType(imageFamily, instanceType, version), instanceEC2ArchName(instanceType), fieldName), nil - case api.NodeImageFamilyUbuntuPro2204, api.NodeImageFamilyUbuntu2204, api.NodeImageFamilyUbuntu2004, api.NodeImageFamilyUbuntu1804: - // FIXME: SSM lookup for Ubuntu EKS images is supported nowadays - return "", &UnsupportedQueryError{msg: fmt.Sprintf("SSM Parameter lookups for %s AMIs is not supported yet", imageFamily)} + case api.NodeImageFamilyUbuntu1804: + return "", &UnsupportedQueryError{msg: fmt.Sprintf("SSM Parameter lookups for %s AMIs is not supported", imageFamily)} + case api.NodeImageFamilyUbuntu2004, + api.NodeImageFamilyUbuntu2204, + api.NodeImageFamilyUbuntuPro2204: + if err := validateVersionForUbuntu(version, imageFamily); err != nil { + return "", err + } + eksProduct := "eks" + if imageFamily == api.NodeImageFamilyUbuntuPro2204 { + eksProduct = "eks-pro" + } + return fmt.Sprint("/aws/service/canonical/ubuntu/", eksProduct, "/", ubuntuReleaseName(imageFamily), "/", version, "/stable/current/", ubuntuArchName(instanceType), "/hvm/ebs-gp2/ami-id"), nil default: return "", fmt.Errorf("unknown image family %s", imageFamily) } @@ -118,6 +128,13 @@ func instanceEC2ArchName(instanceType string) string { return "x86_64" } +func ubuntuArchName(instanceType string) string { + if instanceutils.IsARMInstanceType(instanceType) { + return "arm64" + } + return "amd64" +} + func imageType(imageFamily, instanceType, version string) string { family := utils.ToKebabCase(imageFamily) switch imageFamily { @@ -143,3 +160,52 @@ func windowsAmiType(imageFamily string) string { } return "Full" } + +func ubuntuReleaseName(imageFamily string) string { + switch imageFamily { + case api.NodeImageFamilyUbuntu2004: + return "20.04" + case api.NodeImageFamilyUbuntu2204, api.NodeImageFamilyUbuntuPro2204: + return "22.04" + default: + return "18.04" + } +} + +func validateVersionForUbuntu(version, imageFamily string) error { + switch imageFamily { + case api.NodeImageFamilyUbuntu2004: + var err error + supportsUbuntu := false + const minVersion = api.Version1_21 + const maxVersion = api.Version1_29 + supportsUbuntu, err = utils.IsMinVersion(minVersion, version) + if err != nil { + return err + } + if !supportsUbuntu { + return &UnsupportedQueryError{msg: fmt.Sprintf("%s requires EKS version greater or equal than %s and lower than %s", imageFamily, minVersion, maxVersion)} + } + supportsUbuntu, err = utils.IsMinVersion(version, maxVersion) + if err != nil { + return err + } + if !supportsUbuntu { + return &UnsupportedQueryError{msg: fmt.Sprintf("%s requires EKS version greater or equal than %s and lower than %s", imageFamily, minVersion, maxVersion)} + } + case api.NodeImageFamilyUbuntu2204, api.NodeImageFamilyUbuntuPro2204: + var err error + supportsUbuntu := false + const minVersion = api.Version1_29 + supportsUbuntu, err = utils.IsMinVersion(minVersion, version) + if err != nil { + return err + } + if !supportsUbuntu { + return &UnsupportedQueryError{msg: fmt.Sprintf("%s requires EKS version greater or equal than %s", imageFamily, minVersion)} + } + default: + return &UnsupportedQueryError{msg: fmt.Sprintf("SSM Parameter lookups for %s AMIs is not supported", imageFamily)} + } + return nil +} diff --git a/pkg/ami/ssm_resolver_test.go b/pkg/ami/ssm_resolver_test.go index fec0b84193..7ae0084b42 100644 --- a/pkg/ami/ssm_resolver_test.go +++ b/pkg/ami/ssm_resolver_test.go @@ -2,6 +2,7 @@ package ami_test import ( "context" + "fmt" "strings" "github.com/aws/aws-sdk-go-v2/aws" @@ -233,10 +234,11 @@ var _ = Describe("AMI Auto Resolution", func() { }) - Context("and Ubuntu family", func() { + Context("and Ubuntu1804 family", func() { BeforeEach(func() { p = mockprovider.NewMockProvider() - imageFamily = "Ubuntu2004" + instanceType = "t2.medium" + imageFamily = "Ubuntu1804" }) It("should return an error", func() { @@ -244,6 +246,200 @@ var _ = Describe("AMI Auto Resolution", func() { resolvedAmi, err = resolver.Resolve(context.Background(), region, version, instanceType, imageFamily) Expect(err).To(HaveOccurred()) + Expect(err).To(MatchError("SSM Parameter lookups for Ubuntu1804 AMIs is not supported")) + }) + + }) + + Context("and Ubuntu2004 family", func() { + BeforeEach(func() { + p = mockprovider.NewMockProvider() + instanceType = "t2.medium" + imageFamily = "Ubuntu2004" + }) + + DescribeTable("should return an error", + func(version string) { + resolver := NewSSMResolver(p.MockSSM()) + resolvedAmi, err = resolver.Resolve(context.Background(), region, version, instanceType, imageFamily) + + Expect(err).To(HaveOccurred()) + Expect(err).To(MatchError("Ubuntu2004 requires EKS version greater or equal than 1.21 and lower than 1.29")) + }, + EntryDescription("When EKS version is %s"), + Entry(nil, "1.20"), + Entry(nil, "1.30"), + ) + + DescribeTable("should return a valid AMI", + func(version string) { + addMockGetParameter(p, fmt.Sprintf("/aws/service/canonical/ubuntu/eks/20.04/%s/stable/current/amd64/hvm/ebs-gp2/ami-id", version), expectedAmi) + + resolver := NewSSMResolver(p.MockSSM()) + resolvedAmi, err = resolver.Resolve(context.Background(), region, version, instanceType, imageFamily) + + Expect(err).NotTo(HaveOccurred()) + Expect(p.MockSSM().AssertNumberOfCalls(GinkgoT(), "GetParameter", 1)).To(BeTrue()) + Expect(resolvedAmi).To(BeEquivalentTo(expectedAmi)) + }, + EntryDescription("When EKS version is %s"), + Entry(nil, "1.21"), + Entry(nil, "1.22"), + Entry(nil, "1.23"), + Entry(nil, "1.24"), + Entry(nil, "1.25"), + Entry(nil, "1.26"), + Entry(nil, "1.27"), + Entry(nil, "1.28"), + Entry(nil, "1.29"), + ) + + Context("for arm instance type", func() { + BeforeEach(func() { + instanceType = "a1.large" + }) + DescribeTable("should return a valid AMI for arm64", + func(version string) { + addMockGetParameter(p, fmt.Sprintf("/aws/service/canonical/ubuntu/eks/20.04/%s/stable/current/arm64/hvm/ebs-gp2/ami-id", version), expectedAmi) + + resolver := NewSSMResolver(p.MockSSM()) + resolvedAmi, err = resolver.Resolve(context.Background(), region, version, instanceType, imageFamily) + + Expect(err).NotTo(HaveOccurred()) + Expect(p.MockSSM().AssertNumberOfCalls(GinkgoT(), "GetParameter", 1)).To(BeTrue()) + Expect(resolvedAmi).To(BeEquivalentTo(expectedAmi)) + }, + EntryDescription("When EKS version is %s"), + Entry(nil, "1.21"), + Entry(nil, "1.22"), + Entry(nil, "1.23"), + Entry(nil, "1.24"), + Entry(nil, "1.25"), + Entry(nil, "1.26"), + Entry(nil, "1.27"), + Entry(nil, "1.28"), + Entry(nil, "1.29"), + ) + }) + }) + + Context("and Ubuntu2204 family", func() { + BeforeEach(func() { + p = mockprovider.NewMockProvider() + instanceType = "t2.medium" + imageFamily = "Ubuntu2204" + }) + + DescribeTable("should return an error", + func(version string) { + resolver := NewSSMResolver(p.MockSSM()) + resolvedAmi, err = resolver.Resolve(context.Background(), region, version, instanceType, imageFamily) + + Expect(err).To(HaveOccurred()) + Expect(err).To(MatchError("Ubuntu2204 requires EKS version greater or equal than 1.29")) + }, + EntryDescription("When EKS version is %s"), + Entry(nil, "1.21"), + Entry(nil, "1.28"), + ) + + DescribeTable("should return a valid AMI", + func(version string) { + addMockGetParameter(p, fmt.Sprintf("/aws/service/canonical/ubuntu/eks/22.04/%s/stable/current/amd64/hvm/ebs-gp2/ami-id", version), expectedAmi) + + resolver := NewSSMResolver(p.MockSSM()) + resolvedAmi, err = resolver.Resolve(context.Background(), region, version, instanceType, imageFamily) + + Expect(err).NotTo(HaveOccurred()) + Expect(p.MockSSM().AssertNumberOfCalls(GinkgoT(), "GetParameter", 1)).To(BeTrue()) + Expect(resolvedAmi).To(BeEquivalentTo(expectedAmi)) + }, + EntryDescription("When EKS version is %s"), + Entry(nil, "1.29"), + Entry(nil, "1.30"), + Entry(nil, "1.31"), + ) + + Context("for arm instance type", func() { + BeforeEach(func() { + instanceType = "a1.large" + }) + DescribeTable("should return a valid AMI for arm64", + func(version string) { + addMockGetParameter(p, fmt.Sprintf("/aws/service/canonical/ubuntu/eks/22.04/%s/stable/current/arm64/hvm/ebs-gp2/ami-id", version), expectedAmi) + + resolver := NewSSMResolver(p.MockSSM()) + resolvedAmi, err = resolver.Resolve(context.Background(), region, version, instanceType, imageFamily) + + Expect(err).NotTo(HaveOccurred()) + Expect(p.MockSSM().AssertNumberOfCalls(GinkgoT(), "GetParameter", 1)).To(BeTrue()) + Expect(resolvedAmi).To(BeEquivalentTo(expectedAmi)) + }, + EntryDescription("When EKS version is %s"), + Entry(nil, "1.29"), + Entry(nil, "1.30"), + Entry(nil, "1.31"), + ) + }) + }) + + Context("and UbuntuPro2204 family", func() { + BeforeEach(func() { + p = mockprovider.NewMockProvider() + instanceType = "t2.medium" + imageFamily = "UbuntuPro2204" + }) + + DescribeTable("should return an error", + func(version string) { + resolver := NewSSMResolver(p.MockSSM()) + resolvedAmi, err = resolver.Resolve(context.Background(), region, version, instanceType, imageFamily) + + Expect(err).To(HaveOccurred()) + Expect(err).To(MatchError("UbuntuPro2204 requires EKS version greater or equal than 1.29")) + }, + EntryDescription("When EKS version is %s"), + Entry(nil, "1.21"), + Entry(nil, "1.28"), + ) + + DescribeTable("should return a valid AMI", + func(version string) { + addMockGetParameter(p, fmt.Sprintf("/aws/service/canonical/ubuntu/eks-pro/22.04/%s/stable/current/amd64/hvm/ebs-gp2/ami-id", version), expectedAmi) + + resolver := NewSSMResolver(p.MockSSM()) + resolvedAmi, err = resolver.Resolve(context.Background(), region, version, instanceType, imageFamily) + + Expect(err).NotTo(HaveOccurred()) + Expect(p.MockSSM().AssertNumberOfCalls(GinkgoT(), "GetParameter", 1)).To(BeTrue()) + Expect(resolvedAmi).To(BeEquivalentTo(expectedAmi)) + }, + EntryDescription("When EKS version is %s"), + Entry(nil, "1.29"), + Entry(nil, "1.30"), + Entry(nil, "1.31"), + ) + + Context("for arm instance type", func() { + BeforeEach(func() { + instanceType = "a1.large" + }) + DescribeTable("should return a valid AMI for arm64", + func(version string) { + addMockGetParameter(p, fmt.Sprintf("/aws/service/canonical/ubuntu/eks-pro/22.04/%s/stable/current/arm64/hvm/ebs-gp2/ami-id", version), expectedAmi) + + resolver := NewSSMResolver(p.MockSSM()) + resolvedAmi, err = resolver.Resolve(context.Background(), region, version, instanceType, imageFamily) + + Expect(err).NotTo(HaveOccurred()) + Expect(p.MockSSM().AssertNumberOfCalls(GinkgoT(), "GetParameter", 1)).To(BeTrue()) + Expect(resolvedAmi).To(BeEquivalentTo(expectedAmi)) + }, + EntryDescription("When EKS version is %s"), + Entry(nil, "1.29"), + Entry(nil, "1.30"), + Entry(nil, "1.31"), + ) }) }) diff --git a/pkg/eks/api_test.go b/pkg/eks/api_test.go index 5b9bbce31a..707f76ccf2 100644 --- a/pkg/eks/api_test.go +++ b/pkg/eks/api_test.go @@ -261,8 +261,8 @@ var _ = Describe("eksctl API", func() { }) - testEnsureAMI := func(matcher gomegatypes.GomegaMatcher) { - err := ResolveAMI(context.Background(), provider, "1.14", ng) + testEnsureAMI := func(matcher gomegatypes.GomegaMatcher, version string) { + err := ResolveAMI(context.Background(), provider, version, ng) ExpectWithOffset(1, err).NotTo(HaveOccurred()) ExpectWithOffset(1, ng.AMI).To(matcher) } @@ -276,7 +276,7 @@ var _ = Describe("eksctl API", func() { }, }, nil) - testEnsureAMI(Equal("ami-ssm")) + testEnsureAMI(Equal("ami-ssm"), "1.14") }) It("should fall back to auto resolution for Ubuntu1804", func() { @@ -284,15 +284,28 @@ var _ = Describe("eksctl API", func() { mockDescribeImages(provider, "ami-ubuntu", func(input *ec2.DescribeImagesInput) bool { return input.Owners[0] == "099720109477" }) - testEnsureAMI(Equal("ami-ubuntu")) + testEnsureAMI(Equal("ami-ubuntu"), "1.14") }) - It("should fall back to auto resolution for Ubuntu2004", func() { + It("should fall back to auto resolution for Ubuntu2004 on 1.14", func() { ng.AMIFamily = api.NodeImageFamilyUbuntu2004 mockDescribeImages(provider, "ami-ubuntu", func(input *ec2.DescribeImagesInput) bool { return input.Owners[0] == "099720109477" }) - testEnsureAMI(Equal("ami-ubuntu")) + testEnsureAMI(Equal("ami-ubuntu"), "1.14") + }) + + It("should resolve AMI using SSM Parameter Store for Ubuntu2004 on 1.29", func() { + provider.MockSSM().On("GetParameter", mock.Anything, &ssm.GetParameterInput{ + Name: aws.String("/aws/service/canonical/ubuntu/eks/20.04/1.29/stable/current/amd64/hvm/ebs-gp2/ami-id"), + }).Return(&ssm.GetParameterOutput{ + Parameter: &ssmtypes.Parameter{ + Value: aws.String("ami-ubuntu"), + }, + }, nil) + ng.AMIFamily = api.NodeImageFamilyUbuntu2004 + + testEnsureAMI(Equal("ami-ubuntu"), "1.29") }) It("should fall back to auto resolution for Ubuntu2204", func() { @@ -300,7 +313,7 @@ var _ = Describe("eksctl API", func() { mockDescribeImages(provider, "ami-ubuntu", func(input *ec2.DescribeImagesInput) bool { return input.Owners[0] == "099720109477" }) - testEnsureAMI(Equal("ami-ubuntu")) + testEnsureAMI(Equal("ami-ubuntu"), "1.14") }) It("should fall back to auto resolution for UbuntuPro2204", func() { @@ -308,7 +321,7 @@ var _ = Describe("eksctl API", func() { mockDescribeImages(provider, "ami-ubuntu", func(input *ec2.DescribeImagesInput) bool { return input.Owners[0] == "099720109477" }) - testEnsureAMI(Equal("ami-ubuntu")) + testEnsureAMI(Equal("ami-ubuntu"), "1.14") }) It("should retrieve the AMI from EC2 when AMI is auto", func() { @@ -318,7 +331,7 @@ var _ = Describe("eksctl API", func() { return len(input.ImageIds) == 0 }) - testEnsureAMI(Equal("ami-auto")) + testEnsureAMI(Equal("ami-auto"), "1.14") }) })