Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor AMI Resource Type #505

Merged
merged 3 commits into from
Jul 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 16 additions & 19 deletions aws/ami.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,25 @@ package aws
import (
"time"

"github.com/gruntwork-io/cloud-nuke/telemetry"
"github.com/gruntwork-io/cloud-nuke/util"
awsgo "github.com/aws/aws-sdk-go/aws"
commonTelemetry "github.com/gruntwork-io/go-commons/telemetry"

"github.com/aws/aws-sdk-go/aws"
awsgo "github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/gruntwork-io/cloud-nuke/config"
"github.com/gruntwork-io/cloud-nuke/logging"
"github.com/gruntwork-io/cloud-nuke/report"
"github.com/gruntwork-io/cloud-nuke/telemetry"
"github.com/gruntwork-io/go-commons/errors"
)

// Returns a formatted string of AMI Image ids
func getAllAMIs(session *session.Session, region string, excludeAfter time.Time) ([]*string, error) {
svc := ec2.New(session)

func (ami AMIs) getAll(configObj config.Config) ([]*string, error) {
params := &ec2.DescribeImagesInput{
Owners: []*string{awsgo.String("self")},
}

output, err := svc.DescribeImages(params)
output, err := ami.Client.DescribeImages(params)
if err != nil {
return nil, errors.WithStackTrace(err)
}
Expand All @@ -37,33 +34,33 @@ func getAllAMIs(session *session.Session, region string, excludeAfter time.Time)
return nil, err
}

// Test for time exclusion and check if resource is managed by AWS Backup (see note in README)
if excludeAfter.After(createdTime) && !util.HasAWSBackupTag(image.Tags) {
if configObj.AMI.ShouldInclude(config.ResourceValue{
Name: image.Name,
Time: &createdTime,
}) {
imageIds = append(imageIds, image.ImageId)
}
}

return imageIds, nil
}

// Deletes all AMIs
func nukeAllAMIs(session *session.Session, imageIds []*string) error {
svc := ec2.New(session)

// Deletes all AMI
func (ami AMIs) nukeAll(imageIds []*string) error {
if len(imageIds) == 0 {
logging.Logger.Debugf("No AMIs to nuke in region %s", *session.Config.Region)
logging.Logger.Debugf("No AMI to nuke in region %s", ami.Region)
return nil
}

logging.Logger.Debugf("Deleting all AMIs in region %s", *session.Config.Region)
logging.Logger.Debugf("Deleting all AMI in region %s", ami.Region)

deletedCount := 0
for _, imageID := range imageIds {
params := &ec2.DeregisterImageInput{
ImageId: imageID,
}

_, err := svc.DeregisterImage(params)
_, err := ami.Client.DeregisterImage(params)

// Record status of this resource
e := report.Entry{
Expand All @@ -78,14 +75,14 @@ func nukeAllAMIs(session *session.Session, imageIds []*string) error {
telemetry.TrackEvent(commonTelemetry.EventContext{
EventName: "Error Nuking AMI",
}, map[string]interface{}{
"region": *session.Config.Region,
"region": ami.Region,
})
} else {
deletedCount++
logging.Logger.Debugf("Deleted AMI: %s", *imageID)
}
}

logging.Logger.Debugf("[OK] %d AMI(s) terminated in %s", deletedCount, *session.Config.Region)
logging.Logger.Debugf("[OK] %d AMI(s) terminated in %s", deletedCount, ami.Region)
return nil
}
210 changes: 59 additions & 151 deletions aws/ami_test.go
Original file line number Diff line number Diff line change
@@ -1,178 +1,86 @@
package aws

import (
"github.com/aws/aws-sdk-go/service/ec2/ec2iface"
"github.com/gruntwork-io/cloud-nuke/config"
"github.com/gruntwork-io/cloud-nuke/telemetry"
"github.com/stretchr/testify/assert"
"regexp"
"testing"
"time"

awsgo "github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/gruntwork-io/cloud-nuke/logging"
"github.com/gruntwork-io/cloud-nuke/util"
"github.com/gruntwork-io/go-commons/errors"
"github.com/stretchr/testify/assert"
)

func waitUntilImageAvailable(svc *ec2.EC2, input *ec2.DescribeImagesInput) error {
for i := 0; i < 70; i++ {
output, err := svc.DescribeImages(input)
if err != nil {
return err
}

if *output.Images[0].State == "available" {
return nil
}

logging.Logger.Debug("Waiting for ELB to be available")
time.Sleep(10 * time.Second)
}

return ImageAvailableError{}
type mockedAMI struct {
ec2iface.EC2API
DescribeImagesOutput ec2.DescribeImagesOutput
DeregisterImageOutput ec2.DeregisterImageOutput
}

func createTestAMI(t *testing.T, session *session.Session, name string) (*ec2.Image, error) {
svc := ec2.New(session)
instance := createTestEC2Instance(t, session, name, false)
output, err := svc.CreateImage(&ec2.CreateImageInput{
InstanceId: instance.InstanceId,
Name: awsgo.String(name),
})

if err != nil {
assert.Failf(t, "Could not create test AMI", errors.WithStackTrace(err).Error())
}

params := &ec2.DescribeImagesInput{
Owners: []*string{awsgo.String("self")},
ImageIds: []*string{output.ImageId},
}

err = svc.WaitUntilImageExists(params)

if err != nil {
assert.Fail(t, errors.WithStackTrace(err).Error())
}

err = svc.WaitUntilImageAvailable(params)

if err != nil {
// clean this up since we won't use it again
defer nukeAllAMIs(session, []*string{output.ImageId})
return nil, errors.WithStackTrace(err)
}

images, err := svc.DescribeImages(&ec2.DescribeImagesInput{
ImageIds: []*string{output.ImageId},
})

if err != nil {
assert.Fail(t, errors.WithStackTrace(err).Error())
}
func (m mockedAMI) DescribeImages(input *ec2.DescribeImagesInput) (*ec2.DescribeImagesOutput, error) {
return &m.DescribeImagesOutput, nil
}

return images.Images[0], nil
func (m mockedAMI) DeregisterImage(input *ec2.DeregisterImageInput) (*ec2.DeregisterImageOutput, error) {
return &m.DeregisterImageOutput, nil
}

func TestListAMIs(t *testing.T) {
func TestAMIGetAll(t *testing.T) {
telemetry.InitTelemetry("cloud-nuke", "")
t.Parallel()

region, err := getRandomRegion()
if err != nil {
assert.Fail(t, errors.WithStackTrace(err).Error())
}
session, err := session.NewSession(&awsgo.Config{
Region: awsgo.String(region)},
)

if err != nil {
assert.Fail(t, errors.WithStackTrace(err).Error())
}

uniqueTestID := "cloud-nuke-test-" + util.UniqueID()
image, err := createTestAMI(t, session, uniqueTestID)
attempts := 0

for err != nil && attempts <= 10 {
// Image didn't become availabe in time, try again
image, err = createTestAMI(t, session, uniqueTestID)
attempts++
}

if err != nil {
assert.Fail(t, errors.WithStackTrace(err).Error())
}

// clean up after this test
defer nukeAllAMIs(session, []*string{image.ImageId})
defer nukeAllEc2Instances(session, findEC2InstancesByNameTag(t, session, uniqueTestID))

amis, err := getAllAMIs(session, region, time.Now().Add(1*time.Hour*-1))
if err != nil {
assert.Fail(t, "Unable to fetch list of AMIs")
}

assert.NotContains(t, awsgo.StringValueSlice(amis), *image.ImageId)

amis, err = getAllAMIs(session, region, time.Now().Add(1*time.Hour))
if err != nil {
assert.Fail(t, "Unable to fetch list of AMIs")
}

assert.Contains(t, awsgo.StringValueSlice(amis), *image.ImageId)
testName := "test-ami"
testImageId := "test-image-id"
now := time.Now()
acm := AMIs{
Client: mockedAMI{
DescribeImagesOutput: ec2.DescribeImagesOutput{
Images: []*ec2.Image{{
ImageId: &testImageId,
Name: &testName,
CreationDate: awsgo.String(now.Format("2006-01-02T15:04:05.000Z")),
}},
},
},
}

// without filters
amis, err := acm.getAll(config.Config{})
assert.NoError(t, err)
assert.Contains(t, awsgo.StringValueSlice(amis), testImageId)

// with name filter
amis, err = acm.getAll(config.Config{
AMI: config.ResourceType{
ExcludeRule: config.FilterRule{
NamesRegExp: []config.Expression{{
RE: *regexp.MustCompile("test-ami"),
}}}}})
assert.NoError(t, err)
assert.NotContains(t, awsgo.StringValueSlice(amis), testImageId)

// with time filter
amis, err = acm.getAll(config.Config{
AMI: config.ResourceType{
ExcludeRule: config.FilterRule{
TimeAfter: awsgo.Time(now.Add(-12 * time.Hour))}}})
assert.NoError(t, err)
assert.NotContains(t, awsgo.StringValueSlice(amis), testImageId)
}

func TestNukeAMIs(t *testing.T) {
func TestAMINukeAll(t *testing.T) {
telemetry.InitTelemetry("cloud-nuke", "")
t.Parallel()

region, err := getRandomRegion()
if err != nil {
assert.Fail(t, errors.WithStackTrace(err).Error())
}
session, err := session.NewSession(&awsgo.Config{
Region: awsgo.String(region)},
)
svc := ec2.New(session)

if err != nil {
assert.Fail(t, errors.WithStackTrace(err).Error())
}

uniqueTestID := "cloud-nuke-test-" + util.UniqueID()
image, err := createTestAMI(t, session, uniqueTestID)
attempts := 0

for err != nil && attempts <= 10 {
// Image didn't become availabe in time, try again
image, err = createTestAMI(t, session, uniqueTestID)
attempts++
}

if err != nil {
assert.Fail(t, errors.WithStackTrace(err).Error())
}

// clean up ec2 instance created by the above call
defer nukeAllEc2Instances(session, findEC2InstancesByNameTag(t, session, uniqueTestID))

_, err = svc.DescribeImages(&ec2.DescribeImagesInput{
ImageIds: []*string{image.ImageId},
})

if err != nil {
assert.Fail(t, errors.WithStackTrace(err).Error())
}

if err := nukeAllAMIs(session, []*string{image.ImageId}); err != nil {
assert.Fail(t, errors.WithStackTrace(err).Error())
}

amis, err := getAllAMIs(session, region, time.Now().Add(1*time.Hour))
if err != nil {
assert.Fail(t, "Unable to fetch list of AMIs")
testName := "test-ami"
acm := AMIs{
Client: mockedAMI{
DeregisterImageOutput: ec2.DeregisterImageOutput{},
},
}

assert.NotContains(t, awsgo.StringValueSlice(amis), *image.ImageId)
err := acm.nukeAll([]*string{&testName})
assert.NoError(t, err)
}
12 changes: 6 additions & 6 deletions aws/ami_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,23 @@ type AMIs struct {
}

// ResourceName - the simple name of the aws resource
func (image AMIs) ResourceName() string {
func (ami AMIs) ResourceName() string {
return "ami"
}

// ResourceIdentifiers - The AMI image ids
func (image AMIs) ResourceIdentifiers() []string {
return image.ImageIds
func (ami AMIs) ResourceIdentifiers() []string {
return ami.ImageIds
}

func (image AMIs) MaxBatchSize() int {
func (ami AMIs) MaxBatchSize() int {
// Tentative batch size to ensure AWS doesn't throttle
return 49
}

// Nuke - nuke 'em all!!!
func (image AMIs) Nuke(session *session.Session, identifiers []string) error {
if err := nukeAllAMIs(session, awsgo.StringSlice(identifiers)); err != nil {
func (ami AMIs) Nuke(session *session.Session, identifiers []string) error {
if err := ami.nukeAll(awsgo.StringSlice(identifiers)); err != nil {
return errors.WithStackTrace(err)
}

Expand Down
2 changes: 1 addition & 1 deletion aws/aws.go
Original file line number Diff line number Diff line change
Expand Up @@ -764,7 +764,7 @@ func GetAllResources(targetRegions []string, excludeAfter time.Time, resourceTyp
}
if IsNukeable(amis.ResourceName(), resourceTypes) {
start := time.Now()
imageIds, err := getAllAMIs(cloudNukeSession, region, excludeAfter)
imageIds, err := amis.getAll(configObj)
if err != nil {
ge := report.GeneralError{
Error: err,
Expand Down