Skip to content
This repository was archived by the owner on Jun 2, 2025. It is now read-only.

Commit d1d4c8b

Browse files
authored
Use public parameters in parameter store for ami id (#114)
* Use public parameters in parameter store for ami id
1 parent 232ca66 commit d1d4c8b

File tree

3 files changed

+133
-42
lines changed

3 files changed

+133
-42
lines changed

pkg/ec2helper/ec2helper.go

Lines changed: 103 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import (
3535
"github.com/aws/aws-sdk-go/aws/awserr"
3636
"github.com/aws/aws-sdk-go/aws/session"
3737
"github.com/aws/aws-sdk-go/service/ec2"
38+
"github.com/aws/aws-sdk-go/service/ssm"
3839
"github.com/google/uuid"
3940
)
4041

@@ -352,76 +353,137 @@ func (h *EC2Helper) getInstanceTypes(input *ec2.DescribeInstanceTypesInput) ([]*
352353
// Define all OS and corresponding AMI name formats
353354
var osDescs = map[string]map[string]string{
354355
"Amazon Linux": {
355-
"ebs": "amzn-ami-hvm-????.??.?.????????.?-*-gp2",
356-
"instance-store": "amzn-ami-hvm-????.??.?.????????.?-*-s3",
356+
"ebs": "amzn-ami-hvm-*",
357+
"instance-store": "amzn-ami-hvm-*",
357358
},
358359
"Amazon Linux 2": {
359-
"ebs": "amzn2-ami-hvm-2.?.????????.?-*-gp2",
360+
"ebs": "amzn2-ami-hvm-*",
360361
},
361362
"Red Hat": {
362-
"ebs": "RHEL-?.?.?_HVM-????????-*-?-Hourly2-GP2",
363+
"ebs": "RHEL-9*",
363364
},
364365
"SUSE Linux": {
365-
"ebs": "suse-sles-??-sp?-v????????-hvm-ssd-*",
366+
"ebs": "suse-sles-*",
366367
},
367-
// Ubuntu 18.04 LTS
368368
"Ubuntu": {
369-
"ebs": "ubuntu/images/hvm-ssd/ubuntu-bionic-18.04-*-server-????????",
370-
"instance-store": "ubuntu/images/hvm-instance/ubuntu-bionic-18.04-*-server-????????",
369+
"ebs": "ubuntu/images/*",
370+
"instance-store": "ubuntu/images/*",
371371
},
372-
// 64 bit Microsoft Windows Server with Desktop Experience Locale English AMI
373372
"Windows": {
374-
"ebs": "Windows_Server-????-English-Full-Base-????.??.??",
373+
"ebs": "Windows_Server-*-English-Full-Base*",
374+
},
375+
}
376+
377+
// Define all OS and corresponding AMI public parameters path in Parameter Store
378+
var osSsmPath = map[string]map[string]string{
379+
"Amazon Linux": {
380+
"ebs": "/aws/service/ami-amazon-linux-latest",
381+
"instance-store": "/aws/service/ami-amazon-linux-latest",
382+
},
383+
"Amazon Linux 2": {
384+
"ebs": "/aws/service/ami-amazon-linux-latest",
385+
},
386+
"Red Hat": {
387+
"ebs": "",
388+
},
389+
"SUSE Linux": {
390+
"ebs": "/aws/service/suse/sles",
391+
},
392+
"Ubuntu": {
393+
"ebs": "/aws/service/canonical/ubuntu/server/24.04/stable/current",
394+
"instance-store": "/aws/service/canonical/ubuntu/server/24.04/stable/current",
395+
},
396+
"Windows": {
397+
"ebs": "/aws/service/ami-windows-latest",
375398
},
376399
}
377400

378401
// Get the appropriate input for describing images
379-
func getDescribeImagesInputs(rootDeviceType string, architectures []*string) *map[string]ec2.DescribeImagesInput {
402+
func (h *EC2Helper) GetDescribeImagesInputs(rootDeviceType string, architectures []*string) *map[string]ec2.DescribeImagesInput {
403+
ssmClient := ssm.New(h.Sess)
404+
380405
// Construct all the inputs
381406
imageInputs := map[string]ec2.DescribeImagesInput{}
382407
for osName, rootDeviceTypes := range osDescs {
383408

384409
// Only add inputs if the corresponding root device type is applicable for the specified os
385410
desc, found := rootDeviceTypes[rootDeviceType]
386-
if found {
387-
imageInputs[osName] = ec2.DescribeImagesInput{
388-
Filters: []*ec2.Filter{
389-
{
390-
Name: aws.String("name"),
391-
Values: []*string{
392-
aws.String(desc),
393-
},
394-
},
395-
{
396-
Name: aws.String("state"),
397-
Values: []*string{
398-
aws.String("available"),
399-
},
411+
if !found {
412+
continue
413+
}
414+
imageInputs[osName] = ec2.DescribeImagesInput{
415+
Filters: []*ec2.Filter{
416+
{
417+
Name: aws.String("name"),
418+
Values: []*string{
419+
aws.String(desc),
400420
},
401-
{
402-
Name: aws.String("root-device-type"),
403-
Values: []*string{
404-
aws.String(rootDeviceType),
405-
},
421+
},
422+
{
423+
Name: aws.String("state"),
424+
Values: []*string{
425+
aws.String("available"),
406426
},
407-
{
408-
Name: aws.String("architecture"),
409-
Values: architectures,
427+
},
428+
{
429+
Name: aws.String("root-device-type"),
430+
Values: []*string{
431+
aws.String(rootDeviceType),
410432
},
411-
{
412-
Name: aws.String("owner-alias"),
413-
Values: []*string{
414-
aws.String("amazon"),
415-
},
433+
},
434+
{
435+
Name: aws.String("architecture"),
436+
Values: architectures,
437+
},
438+
{
439+
Name: aws.String("owner-alias"),
440+
Values: []*string{
441+
aws.String("amazon"),
416442
},
417443
},
418-
}
444+
},
445+
}
446+
ssmPath, found := osSsmPath[osName][rootDeviceType]
447+
if !found || ssmPath == "" {
448+
continue
449+
}
450+
if imageIds, err := h.GetImageIdsFromSSM(ssmClient, ssmPath); err == nil {
451+
input := imageInputs[osName]
452+
input.ImageIds = imageIds
453+
imageInputs[osName] = input
419454
}
420455
}
421456

422457
return &imageInputs
423458
}
424459

460+
func (h *EC2Helper) GetImageIdsFromSSM(ssmClient *ssm.SSM, ssmPath string) ([]*string, error) {
461+
var imageIds []*string
462+
463+
input := &ssm.GetParametersByPathInput{
464+
Path: aws.String(ssmPath),
465+
Recursive: aws.Bool(true),
466+
WithDecryption: aws.Bool(false),
467+
}
468+
469+
err := ssmClient.GetParametersByPathPages(input,
470+
func(page *ssm.GetParametersByPathOutput, lastPage bool) bool {
471+
for _, parameter := range page.Parameters {
472+
if !strings.HasPrefix(*parameter.Value, "ami") {
473+
continue
474+
}
475+
imageIds = append(imageIds, parameter.Value)
476+
}
477+
return !lastPage
478+
})
479+
480+
if err != nil {
481+
return nil, fmt.Errorf("error getting parameters from SSM: %v", err)
482+
}
483+
484+
return imageIds, nil
485+
}
486+
425487
// Sort interface for images
426488
type byCreationDate []*ec2.Image
427489

@@ -436,9 +498,9 @@ Empty result is allowed.
436498
func (h *EC2Helper) GetLatestImages(rootDeviceType *string, architectures []*string) (*map[string]*ec2.Image, error) {
437499
var inputs *map[string]ec2.DescribeImagesInput
438500
if rootDeviceType == nil {
439-
inputs = getDescribeImagesInputs("ebs", architectures)
501+
inputs = h.GetDescribeImagesInputs("ebs", architectures)
440502
} else {
441-
inputs = getDescribeImagesInputs(*rootDeviceType, architectures)
503+
inputs = h.GetDescribeImagesInputs(*rootDeviceType, architectures)
442504
}
443505

444506
images := map[string]*ec2.Image{}

pkg/question/question_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,7 @@ func TestAskImage_Success(t *testing.T) {
343343
const expectedImage = "ami-12345"
344344
const testInstanceType = ec2.InstanceTypeT2Micro
345345

346+
testEC2 = ec2helper.New(session.Must(session.NewSession()))
346347
testEC2.Svc = &th.MockedEC2Svc{
347348
InstanceTypes: []*ec2.InstanceTypeInfo{
348349
{

test/e2e/e2e-ec2helper-test/e2e_ec2helper_test.go

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
"github.com/aws/aws-sdk-go/aws/session"
3030
"github.com/aws/aws-sdk-go/service/cloudformation"
3131
"github.com/aws/aws-sdk-go/service/ec2"
32+
"github.com/aws/aws-sdk-go/service/ssm"
3233
)
3334

3435
const testStackName = "simple-ec2-e2e-ec2helper-test"
@@ -155,8 +156,21 @@ func TestGetInstanceTypesFromInstanceSelector(t *testing.T) {
155156
func TestGetLatestImages(t *testing.T) {
156157
th.Assert(t, h != nil, "EC2Helper was not initialized successfully")
157158

158-
_, err := h.GetLatestImages(nil, aws.StringSlice([]string{"x86_64"}))
159+
osNames := []string{
160+
"Amazon Linux",
161+
"Amazon Linux 2",
162+
"Red Hat",
163+
"SUSE Linux",
164+
"Ubuntu",
165+
"Windows",
166+
}
167+
168+
imagesMap, err := h.GetLatestImages(nil, aws.StringSlice([]string{"x86_64"}))
159169
th.Ok(t, err)
170+
171+
for _, os := range osNames {
172+
th.Assert(t, (*imagesMap)[os] != nil, fmt.Sprintf("GetLatestImages should fetch image for %s", os))
173+
}
160174
}
161175

162176
func TestGetDefaultImageForAmd(t *testing.T) {
@@ -403,6 +417,20 @@ func ValidateInstanceTags(t *testing.T, actualInstanceTags []*ec2.Tag, launchReq
403417
th.Assert(t, countOfExpectedTags == countOfActualTagsMatched, "Didn't find all of the expected tags on the actual instance")
404418
}
405419

420+
func TestGetImageIdsFromSSM(t *testing.T) {
421+
ssmClient := ssm.New(h.Sess)
422+
validSsmPath := "/aws/service/ami-amazon-linux-latest"
423+
imageIds, err := h.GetImageIdsFromSSM(ssmClient, validSsmPath)
424+
th.Ok(t, err)
425+
th.Assert(t, imageIds != nil, "imageIds should not be nil")
426+
th.Assert(t, len(imageIds) > 0, "imageIds should not be empty")
427+
428+
invalidSsmPath := "/aws/service/ami-amazon-linux"
429+
_, err = h.GetImageIdsFromSSM(ssmClient, invalidSsmPath)
430+
expectedErrorMsg := fmt.Sprintf("%s is not a valid namespace", invalidSsmPath[1:])
431+
th.Assert(t, strings.Contains(err.Error(), expectedErrorMsg), "Error message should contain invalid SSM path (namespace) information")
432+
}
433+
406434
func TestTerminateInstances(t *testing.T) {
407435
th.Assert(t, h != nil, "EC2Helper was not initialized successfully")
408436
th.Assert(t, instanceId != nil, "No test instance ID found")

0 commit comments

Comments
 (0)