Repository: gruntwork-io/cloud-nuke
Pull request #613 (2023-10-24 01:20:15)

Implement a new resource type for RDS snapshot
<!-- Prepend '[WIP]' to the title if this PR is still a work-in-progress. Remove it when it is ready for review! -->

## Description

Fixes https://github.com/gruntwork-io/cloud-nuke/issues/120. 

<!-- Description of the changes introduced by this PR. -->

## TODOs

Read the [Gruntwork contribution guidelines](https://gruntwork.notion.site/Gruntwork-Coding-Methodology-02fdcd6e4b004e818553684760bf691e).

- [ ] Update the docs.
- [ ] Run the relevant tests successfully, including pre-commit checks.
- [ ] Ensure any 3rd party code adheres with our [license policy](https://www.notion.so/gruntwork/Gruntwork-licenses-and-open-source-usage-policy-f7dece1f780341c7b69c1763f22b1378) or delete this line if its not applicable.
- [ ] Include release notes. If this PR is backward incompatible, include a migration guide.
- [ ] Attention Grunts - if this PR adds support for a new resource, ensure the `nuke_sandbox` and `nuke_phxdevops` jobs in `.circleci/config.yml` have been updated with appropriate exclusions (either directly in the job or via the `.circleci/nuke_config.yml` file) to prevent nuking IAM roles, groups, resources, etc that are important for the test accounts.


## Release Notes (draft)

<!-- One-line description of the PR that can be included in the final release notes. -->
Added / Removed / Updated [X].

### Migration Guide

<!-- Important: If you made any backward incompatible changes, then you must write a migration guide! -->




Patch:
diff --git a/aws/resource_registry.go b/aws/resource_registry.go
index 43ebb47..d6301a6 100644
--- a/aws/resource_registry.go
+++ b/aws/resource_registry.go
@@ -92,6 +92,7 @@ func getRegisteredRegionalResources() []AwsResources {
 		&resources.DBInstances{},
 		&resources.DBSubnetGroups{},
 		&resources.DBClusters{},
+		&resources.RdsSnapshot{},
 		&resources.RedshiftClusters{},
 		&resources.S3Buckets{},
 		&resources.SageMakerNotebookInstances{},
diff --git a/aws/resources/ec2_dhcp_option.go b/aws/resources/ec2_dhcp_option.go
index c09ea0a..12e22e4 100644
--- a/aws/resources/ec2_dhcp_option.go
+++ b/aws/resources/ec2_dhcp_option.go
@@ -2,9 +2,11 @@ package resources
 
 import (
 	"context"
-	"fmt"
+	"github.com/aws/aws-sdk-go/aws"
 	"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/go-commons/errors"
 	"github.com/pterm/pterm"
 )
@@ -35,9 +37,18 @@ func (v *EC2DhcpOption) nukeAll(identifiers []*string) error {
 			DhcpOptionsId: identifier,
 		})
 		if err != nil {
-			pterm.Debug.Println(fmt.Sprintf("Failed to delete DHCP option w/ err: %s.", err))
-			return errors.WithStackTrace(err)
+			logging.Debugf("Failed to delete DHCP option w/ err: %s.", err)
+		} else {
+			logging.Infof("Successfully deleted DHCP option %s.", pterm.Green(*identifier))
 		}
+
+		// Record status of this resource
+		e := report.Entry{
+			Identifier:   aws.StringValue(identifier),
+			ResourceType: v.ResourceName(),
+			Error:        err,
+		}
+		report.Record(e)
 	}
 
 	return nil
diff --git a/aws/resources/rds_snapshot.go b/aws/resources/rds_snapshot.go
new file mode 100644
index 0000000..1cf64bd
--- /dev/null
+++ b/aws/resources/rds_snapshot.go
@@ -0,0 +1,58 @@
+package resources
+
+import (
+	"context"
+	"github.com/aws/aws-sdk-go/aws"
+	"github.com/aws/aws-sdk-go/service/rds"
+	"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/util"
+)
+
+func (snapshot *RdsSnapshot) getAll(c context.Context, configObj config.Config) ([]*string, error) {
+	var identifiers []*string
+	err := snapshot.Client.DescribeDBSnapshotsPages(&rds.DescribeDBSnapshotsInput{}, func(page *rds.DescribeDBSnapshotsOutput, lastPage bool) bool {
+		for _, s := range page.DBSnapshots {
+			if configObj.RdsSnapshot.ShouldInclude(config.ResourceValue{
+				Name: s.DBSnapshotIdentifier,
+				Time: s.SnapshotCreateTime,
+				Tags: util.ConvertRDSTagsToMap(s.TagList),
+			}) {
+				identifiers = append(identifiers, s.DBSnapshotIdentifier)
+			}
+		}
+
+		return !lastPage
+	})
+	if err != nil {
+		logging.Debugf("[RDS Snapshot] Failed to list snapshots: %s", err)
+		return nil, err
+	}
+
+	return identifiers, nil
+}
+
+func (snapshot *RdsSnapshot) nukeAll(identifiers []*string) error {
+	for _, identifier := range identifiers {
+		logging.Debugf("[RDS Snapshot] Deleting %s in region %s", *identifier, snapshot.Region)
+		_, err := snapshot.Client.DeleteDBSnapshot(&rds.DeleteDBSnapshotInput{
+			DBSnapshotIdentifier: identifier,
+		})
+		if err != nil {
+			logging.Errorf("[RDS Snapshot] Error deleting RDS Snapshot %s: %s", *identifier, err)
+		} else {
+			logging.Debugf("[RDS Snapshot] Deleted RDS Snapshot %s", *identifier)
+		}
+
+		// Record status of this resource
+		e := report.Entry{
+			Identifier:   aws.StringValue(identifier),
+			ResourceType: snapshot.ResourceName(),
+			Error:        err,
+		}
+		report.Record(e)
+	}
+
+	return nil
+}
diff --git a/aws/resources/rds_snapshot_types.go b/aws/resources/rds_snapshot_types.go
new file mode 100644
index 0000000..6ba9134
--- /dev/null
+++ b/aws/resources/rds_snapshot_types.go
@@ -0,0 +1,52 @@
+package resources
+
+import (
+	"context"
+	awsgo "github.com/aws/aws-sdk-go/aws"
+	"github.com/aws/aws-sdk-go/aws/session"
+	"github.com/aws/aws-sdk-go/service/rds"
+	"github.com/aws/aws-sdk-go/service/rds/rdsiface"
+	"github.com/gruntwork-io/cloud-nuke/config"
+	"github.com/gruntwork-io/go-commons/errors"
+)
+
+type RdsSnapshot struct {
+	Client      rdsiface.RDSAPI
+	Region      string
+	Identifiers []string
+}
+
+func (snapshot *RdsSnapshot) Init(session *session.Session) {
+	snapshot.Client = rds.New(session)
+}
+
+func (snapshot *RdsSnapshot) ResourceName() string {
+	return "rds-snapshot"
+}
+
+func (snapshot *RdsSnapshot) ResourceIdentifiers() []string {
+	return snapshot.Identifiers
+}
+
+func (snapshot *RdsSnapshot) MaxBatchSize() int {
+	return 49
+}
+
+func (snapshot *RdsSnapshot) GetAndSetIdentifiers(c context.Context, configObj config.Config) ([]string, error) {
+	identifiers, err := snapshot.getAll(c, configObj)
+	if err != nil {
+		return nil, err
+	}
+
+	snapshot.Identifiers = awsgo.StringValueSlice(identifiers)
+	return snapshot.Identifiers, nil
+}
+
+// Nuke - nuke 'em all!!!
+func (snapshot *RdsSnapshot) Nuke(identifiers []string) error {
+	if err := snapshot.nukeAll(awsgo.StringSlice(identifiers)); err != nil {
+		return errors.WithStackTrace(err)
+	}
+
+	return nil
+}
diff --git a/config/config.go b/config/config.go
index b530605..a1d5f18 100644
--- a/config/config.go
+++ b/config/config.go
@@ -67,6 +67,7 @@ type Config struct {
 	OIDCProvider                 ResourceType               `yaml:"OIDCProvider"`
 	OpenSearchDomain             ResourceType               `yaml:"OpenSearchDomain"`
 	Redshift                     ResourceType               `yaml:"Redshift"`
+	RdsSnapshot                  ResourceType               `yaml:"RdsSnapshot"`
 	S3                           ResourceType               `yaml:"s3"`
 	SNS                          ResourceType               `yaml:"SNS"`
 	SQS                          ResourceType               `yaml:"SQS"`
diff --git a/go.mod b/go.mod
index 2b4d334..b4e319d 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,6 @@
 module github.com/gruntwork-io/cloud-nuke
 
-go 1.18
+go 1.21
 
 require (
 	github.com/aws/aws-sdk-go v1.44.304


Test patch:
diff --git a/aws/resources/rds_snapshot_test.go b/aws/resources/rds_snapshot_test.go
new file mode 100644
index 0000000..144b18c
--- /dev/null
+++ b/aws/resources/rds_snapshot_test.go
@@ -0,0 +1,105 @@
+package resources
+
+import (
+	"context"
+	"github.com/aws/aws-sdk-go/aws"
+	"github.com/aws/aws-sdk-go/service/rds"
+	"github.com/aws/aws-sdk-go/service/rds/rdsiface"
+	"github.com/gruntwork-io/cloud-nuke/config"
+	"github.com/gruntwork-io/cloud-nuke/telemetry"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	"regexp"
+	"testing"
+	"time"
+)
+
+type mockedRdsSnapshot struct {
+	rdsiface.RDSAPI
+	DescribeDBSnapshotsPagesOutput rds.DescribeDBSnapshotsOutput
+	DeleteDBSnapshotOutput         rds.DeleteDBSnapshotOutput
+}
+
+func (m mockedRdsSnapshot) DescribeDBSnapshotsPages(input *rds.DescribeDBSnapshotsInput, callback func(*rds.DescribeDBSnapshotsOutput, bool) bool) error {
+	callback(&m.DescribeDBSnapshotsPagesOutput, true)
+	return nil
+}
+
+func (m mockedRdsSnapshot) DeleteDBSnapshot(input *rds.DeleteDBSnapshotInput) (*rds.DeleteDBSnapshotOutput, error) {
+	return &m.DeleteDBSnapshotOutput, nil
+}
+
+func TestRdsSnapshot_GetAll(t *testing.T) {
+	telemetry.InitTelemetry("cloud-nuke", "")
+	t.Parallel()
+
+	testName1 := "test-name1"
+	testName2 := "test-name2"
+	now := time.Now()
+	snapshot := RdsSnapshot{
+		Client: mockedRdsSnapshot{
+			DescribeDBSnapshotsPagesOutput: rds.DescribeDBSnapshotsOutput{
+				DBSnapshots: []*rds.DBSnapshot{
+					{
+						DBSnapshotIdentifier: &testName1,
+						SnapshotCreateTime:   &now,
+					},
+					{
+						DBSnapshotIdentifier: &testName2,
+						SnapshotCreateTime:   aws.Time(now.Add(1)),
+					},
+				},
+			},
+		},
+	}
+
+	tests := map[string]struct {
+		configObj config.ResourceType
+		expected  []string
+	}{
+		"emptyFilter": {
+			configObj: config.ResourceType{},
+			expected:  []string{testName1, testName2},
+		},
+		"nameExclusionFilter": {
+			configObj: config.ResourceType{
+				ExcludeRule: config.FilterRule{
+					NamesRegExp: []config.Expression{{
+						RE: *regexp.MustCompile(testName1),
+					}}},
+			},
+			expected: []string{testName2},
+		},
+		"timeAfterExclusionFilter": {
+			configObj: config.ResourceType{
+				ExcludeRule: config.FilterRule{
+					TimeAfter: aws.Time(now.Add(-1 * time.Hour)),
+				}},
+			expected: []string{},
+		},
+	}
+	for name, tc := range tests {
+		t.Run(name, func(t *testing.T) {
+			names, err := snapshot.getAll(context.Background(), config.Config{
+				RdsSnapshot: tc.configObj,
+			})
+			require.NoError(t, err)
+			require.Equal(t, tc.expected, aws.StringValueSlice(names))
+		})
+	}
+}
+
+func TestRdsSnapshot_NukeAll(t *testing.T) {
+	telemetry.InitTelemetry("cloud-nuke", "")
+	t.Parallel()
+
+	testName := "test-db-cluster"
+	snapshot := RdsSnapshot{
+		Client: mockedRdsSnapshot{
+			DeleteDBSnapshotOutput: rds.DeleteDBSnapshotOutput{},
+		},
+	}
+
+	err := snapshot.nukeAll([]*string{&testName})
+	assert.NoError(t, err)
+}
diff --git a/config/config_test.go b/config/config_test.go
index 59b7374..bec4e31 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -76,6 +76,7 @@ func emptyConfig() *Config {
 		ResourceType{FilterRule{}, FilterRule{}},
 		ResourceType{FilterRule{}, FilterRule{}},
 		ResourceType{FilterRule{}, FilterRule{}},
+		ResourceType{FilterRule{}, FilterRule{}},
 	}
 }