diff --git a/README.md b/README.md index 0046c320..ab7343ee 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ The currently supported functionality includes: * Deleting all EBS Volumes in an AWS account * Deleting all unprotected EC2 instances in an AWS account * Deleting all AMIs in an AWS account +* Deleting all Snapshots in an AWS account ### WARNING: THIS TOOL IS HIGHLY DESTRUCTIVE, ALL SUPPORTED RESOURCES WILL BE DELETED. ITS EFFECTS ARE IRREVERSIBLE AND SHOULD NEVER BE USED IN A PRODUCTION ENVIRONMENT diff --git a/aws/aws.go b/aws/aws.go index c4e06b4a..e4db9d89 100644 --- a/aws/aws.go +++ b/aws/aws.go @@ -145,6 +145,19 @@ func GetAllResources(regions []string, excludedRegions []string, excludeAfter ti resourcesInRegion.Resources = append(resourcesInRegion.Resources, amis) // End AMIs + // Snapshots + snapshotIds, err := getAllSnapshots(session, region, excludeAfter) + if err != nil { + return nil, errors.WithStackTrace(err) + } + + snapshots := Snapshots{ + SnapshotIds: awsgo.StringValueSlice(snapshotIds), + } + + resourcesInRegion.Resources = append(resourcesInRegion.Resources, snapshots) + // End Snapshots + account.Resources[region] = resourcesInRegion } diff --git a/aws/snapshot.go b/aws/snapshot.go new file mode 100644 index 00000000..50fd3319 --- /dev/null +++ b/aws/snapshot.go @@ -0,0 +1,63 @@ +package aws + +import ( + "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/aws-nuke/logging" + "github.com/gruntwork-io/gruntwork-cli/errors" +) + +// Returns a formatted string of Snapshot snapshot ids +func getAllSnapshots(session *session.Session, region string, excludeAfter time.Time) ([]*string, error) { + svc := ec2.New(session) + + params := &ec2.DescribeSnapshotsInput{ + OwnerIds: []*string{awsgo.String("self")}, + } + + output, err := svc.DescribeSnapshots(params) + if err != nil { + return nil, errors.WithStackTrace(err) + } + + var snapshotIds []*string + for _, snapshot := range output.Snapshots { + if excludeAfter.After(*snapshot.StartTime) { + snapshotIds = append(snapshotIds, snapshot.SnapshotId) + } + } + + return snapshotIds, nil +} + +// Deletes all Snapshots +func nukeAllSnapshots(session *session.Session, snapshotIds []*string) error { + svc := ec2.New(session) + + if len(snapshotIds) == 0 { + logging.Logger.Infof("No Snapshots to nuke in region %s", *session.Config.Region) + return nil + } + + logging.Logger.Infof("Terminating all Snapshots in region %s", *session.Config.Region) + + for _, snapshotID := range snapshotIds { + params := &ec2.DeleteSnapshotInput{ + SnapshotId: snapshotID, + } + + _, err := svc.DeleteSnapshot(params) + if err != nil { + logging.Logger.Errorf("[Failed] %s", err) + return errors.WithStackTrace(err) + } + + logging.Logger.Infof("Deleted Snapshot: %s", *snapshotID) + } + + logging.Logger.Infof("[OK] %d Snapshot(s) terminated in %s", len(snapshotIds), *session.Config.Region) + return nil +} diff --git a/aws/snapshot_test.go b/aws/snapshot_test.go new file mode 100644 index 00000000..ba2a6943 --- /dev/null +++ b/aws/snapshot_test.go @@ -0,0 +1,111 @@ +package aws + +import ( + "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/aws-nuke/util" + "github.com/gruntwork-io/gruntwork-cli/errors" + "github.com/stretchr/testify/assert" +) + +func createTestSnapshot(t *testing.T, session *session.Session, name string) ec2.Snapshot { + svc := ec2.New(session) + + az := awsgo.StringValue(session.Config.Region) + "a" + volume := createTestEBSVolume(t, session, name, az) + snapshot, err := svc.CreateSnapshot(&ec2.CreateSnapshotInput{ + VolumeId: volume.VolumeId, + }) + + if err != nil { + assert.Failf(t, "Could not create test Snapshot", errors.WithStackTrace(err).Error()) + } + + err = svc.WaitUntilSnapshotCompleted(&ec2.DescribeSnapshotsInput{ + OwnerIds: []*string{awsgo.String("self")}, + SnapshotIds: []*string{snapshot.SnapshotId}, + }) + + if err != nil { + assert.Fail(t, errors.WithStackTrace(err).Error()) + } + + return *snapshot +} + +func TestListSnapshots(t *testing.T) { + t.Parallel() + + region := getRandomRegion() + session, err := session.NewSession(&awsgo.Config{ + Region: awsgo.String(region)}, + ) + + if err != nil { + assert.Fail(t, errors.WithStackTrace(err).Error()) + } + + uniqueTestID := "aws-nuke-test-" + util.UniqueID() + snapshot := createTestSnapshot(t, session, uniqueTestID) + + // clean up after this test + defer nukeAllSnapshots(session, []*string{snapshot.SnapshotId}) + defer nukeAllEbsVolumes(session, findEBSVolumesByNameTag(t, session, uniqueTestID)) + + snapshots, err := getAllSnapshots(session, region, time.Now().Add(1*time.Hour*-1)) + if err != nil { + assert.Fail(t, "Unable to fetch list of Snapshots") + } + + assert.NotContains(t, awsgo.StringValueSlice(snapshots), *snapshot.SnapshotId) + + snapshots, err = getAllSnapshots(session, region, time.Now().Add(1*time.Hour)) + if err != nil { + assert.Fail(t, "Unable to fetch list of Snapshots") + } + + assert.Contains(t, awsgo.StringValueSlice(snapshots), *snapshot.SnapshotId) +} + +func TestNukeSnapshots(t *testing.T) { + t.Parallel() + + region := getRandomRegion() + 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 := "aws-nuke-test-" + util.UniqueID() + snapshot := createTestSnapshot(t, session, uniqueTestID) + + // clean up ec2 instance created by the above call + defer nukeAllEbsVolumes(session, findEBSVolumesByNameTag(t, session, uniqueTestID)) + + _, err = svc.DescribeSnapshots(&ec2.DescribeSnapshotsInput{ + SnapshotIds: []*string{snapshot.SnapshotId}, + }) + + if err != nil { + assert.Fail(t, errors.WithStackTrace(err).Error()) + } + + if err := nukeAllSnapshots(session, []*string{snapshot.SnapshotId}); err != nil { + assert.Fail(t, errors.WithStackTrace(err).Error()) + } + + snapshots, err := getAllSnapshots(session, region, time.Now().Add(1*time.Hour)) + if err != nil { + assert.Fail(t, "Unable to fetch list of Snapshots") + } + + assert.NotContains(t, awsgo.StringValueSlice(snapshots), *snapshot.SnapshotId) +} diff --git a/aws/snapshot_types.go b/aws/snapshot_types.go new file mode 100644 index 00000000..384cbfd9 --- /dev/null +++ b/aws/snapshot_types.go @@ -0,0 +1,30 @@ +package aws + +import ( + awsgo "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/gruntwork-io/gruntwork-cli/errors" +) + +// Snapshots - represents all user owned Snapshots +type Snapshots struct { + SnapshotIds []string +} + +// ResourceName - the simple name of the aws resource +func (snapshot Snapshots) ResourceName() string { + return "snap" +} + +// ResourceIdentifiers - The Snapshot snapshot ids +func (snapshot Snapshots) ResourceIdentifiers() []string { + return snapshot.SnapshotIds +} + +// Nuke - nuke 'em all!!! +func (snapshot Snapshots) Nuke(session *session.Session) error { + if err := nukeAllSnapshots(session, awsgo.StringSlice(snapshot.SnapshotIds)); err != nil { + return errors.WithStackTrace(err) + } + return nil +} diff --git a/commands/cli.go b/commands/cli.go index e88d9a9a..8ee47c39 100644 --- a/commands/cli.go +++ b/commands/cli.go @@ -23,7 +23,7 @@ func CreateCli(version string) *cli.App { app.HelpName = app.Name app.Author = "Gruntwork " app.Version = version - app.Usage = "A CLI tool to cleanup AWS resources (ASG, ELB, ELBv2, EBS, EC2, AMI). THIS TOOL WILL COMPLETELY REMOVE ALL RESOURCES AND ITS EFFECTS ARE IRREVERSIBLE!!!" + app.Usage = "A CLI tool to cleanup AWS resources (ASG, ELB, ELBv2, EBS, EC2, AMI, Snapshots). THIS TOOL WILL COMPLETELY REMOVE ALL RESOURCES AND ITS EFFECTS ARE IRREVERSIBLE!!!" app.Flags = []cli.Flag{ cli.StringSliceFlag{ Name: "exclude-region",