Repository: gruntwork-io/cloud-nuke
Pull request #647 (2024-03-06 17:03:50)

Deleting DB Parameter Groups
<!-- 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/645

<!-- 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/README.md b/README.md
index 7d695fb..c07d41f 100644
--- a/README.md
+++ b/README.md
@@ -50,11 +50,11 @@ Cloud-nuke suppports 🔎 inspecting and 🔥💀 deleting the following AWS res
 | EC2                     | Elastic IPs                                              |
 | EC2                     | Launch Configurations                                    |
 | EC2                     | IPAM (Amazon VPC IP Address Manager)                     |
-| EC2                     | IPAM Pool															                                 |
-| EC2                     | IPAM Scope														                                 |
-| EC2                     | IPAM Custom Allocation 								                          |
-| EC2                     | IPAM BYOASN	          								                           |
-| EC2                     | IPAM Resource Discovery 							                          |
+| EC2                     | IPAM Pool															                   |
+| EC2                     | IPAM Scope														                   |
+| EC2                     | IPAM Custom Allocation 								                   |
+| EC2                     | IPAM BYOASN	          								                   |
+| EC2                     | IPAM Resource Discovery 							                   |
 | Certificate Manager     | ACM Private CA                                           |
 | Direct Connect          | Transit Gateways                                         |
 | Elasticache             | Clusters                                                 |
@@ -64,9 +64,6 @@ Cloud-nuke suppports 🔎 inspecting and 🔥💀 deleting the following AWS res
 | ECS                     | Services                                                 |
 | ECS                     | Clusters                                                 |
 | EKS                     | Clusters                                                 |
-| RDS                     | RDS databases                                            |
-| RDS                     | Neptune                                                  |
-| RDS                     | Document DB instances                                    |
 | DynamoDB                | Tables                                                   |
 | Lambda                  | Functions                                                |
 | SQS                     | Queues                                                   |
@@ -99,6 +96,10 @@ Cloud-nuke suppports 🔎 inspecting and 🔥💀 deleting the following AWS res
 | ECR                     | Repositories                                             |
 | Config                  | Service recorders                                        |
 | Config                  | Service rules                                            |
+| RDS                     | RDS databases                                            |
+| RDS                     | Neptune                                                  |
+| RDS                     | Document DB instances                                    |
+| RDS                     | RDS parameter group                                      |
 | Security Hub            | Hubs                                                     |
 | Security Hub            | Members                                                  |
 | Security Hub            | Administrators                                           |
@@ -491,8 +492,8 @@ _Note: it doesn't support including resources by tags._
 To find out what we options are supported in the config file today, consult this table. Resource types at the top level
 of the file that are supported are listed here.
 
-| resource type               | config key                   | names_regex                           | time                                | tags | 
-|-----------------------------|------------------------------|---------------------------------------|-------------------------------------|------|
+| resource type               | config key                   | names_regex                            | time                                 | tags  | 
+|-----------------------------|------------------------------|----------------------------------------|--------------------------------------|-------|
 | acm                         | ACM                          | ✅ (Domain Name)                       | ✅ (Created Time)                    | ❌    |
 | acmpca                      | ACMPCA                       | ❌                                     | ✅ (LastStateChange or Created Time) | ❌    |
 | ami                         | AMI                          | ✅ (Image Name)                        | ✅ (Creation Time)                   | ❌    |
@@ -508,9 +509,6 @@ of the file that are supported are listed here.
 | codedeploy-application      | CodeDeployApplications       | ✅ (Application Name)                  | ✅ (Creation Time)                   | ❌    |
 | config-recorders            | ConfigServiceRecorder        | ✅ (Recorder Name)                     | ❌                                   | ❌    |
 | config-rules                | ConfigServiceRule            | ✅ (Rule Name)                         | ❌                                   | ❌    |
-| rds-cluster                 | DBClusters                   | ✅ (DB Cluster Identifier )            | ✅ (Creation Time)                   | ✅    |
-| rds                         | DBInstances                  | ✅ (DB Name)                           | ✅ (Creation Time)                   | ✅    |
-| rds-subnet-group            | DBSubnetGroups               | ✅ (DB Subnet Group Name)              | ❌                                   | ❌    |
 | dynamodb                    | DynamoDB                     | ✅ (Table Name)                        | ✅ (Creation Time)                   | ❌    |
 | ebs                         | EBSVolume                    | ✅ (Volume Name)                       | ✅ (Creation Time)                   | ✅    |
 | elastic-beanstalk           | ElasticBeanstalk             | ✅ (Application Name)                  | ✅ (Creation Time)                   | ❌    |
@@ -518,7 +516,7 @@ of the file that are supported are listed here.
 | ec2-dedicated-hosts         | EC2DedicatedHosts            | ✅ (EC2 Name Tag)                      | ✅ (Allocation Time)                 | ❌    |
 | ec2-dhcp-option             | EC2DhcpOption                | ❌                                     | ❌                                   | ❌    |
 | ec2-keypairs                | EC2KeyPairs                  | ✅ (Key Pair Name)                     | ✅ (Creation Time)                   | ✅    |
-| ec2-ipam                	   | EC2IPAM			               | ✅ (IPAM name)                    	   | ✅ (Creation Time)                   | ✅    |
+| ec2-ipam                	  | EC2IPAM			               | ✅ (IPAM name)                    	   | ✅ (Creation Time)                     | ✅    |
 | ec2-ipam-pool               | EC2IPAMPool			             | ✅ (IPAM Pool name)                    | ✅ (Creation Time)                   | ✅    |
 | ec2-ipam-resource-discovery | EC2IPAMResourceDiscovery		 | ✅ (IPAM Discovery Name)               | ✅ (Creation Time)                   | ✅    |
 | ec2-ipam-scope              | EC2IPAMScope		 						 | ✅ (IPAM Scope Name)               		 | ✅ (Creation Time)                   | ✅    |
@@ -550,6 +548,10 @@ of the file that are supported are listed here.
 | oidcprovider                | OIDCProvider                 | ✅ (Provider URL)                      | ✅ (Creation Time)                   | ❌    |
 | opensearchdomain            | OpenSearchDomain             | ✅ (Domain Name)                       | ✅ (First Seen Tag Time)             | ❌    |
 | redshift                    | Redshift                     | ✅ (Cluster Identifier)                | ✅ (Creation Time)                   | ❌    |
+| rds-cluster                 | DBClusters                   | ✅ (DB Cluster Identifier )            | ✅ (Creation Time)                   | ✅    |
+| rds                         | DBInstances                  | ✅ (DB Name)                           | ✅ (Creation Time)                   | ✅    |
+| rds-parameter-group         | RdsParameterGroup            | ✅ (Group Name)                        | ❌                                   | ❌    |
+| rds-subnet-group            | DBSubnetGroups               | ✅ (DB Subnet Group Name)              | ❌                                   | ❌    |
 | s3                          | s3                           | ✅ (Bucket Name)                       | ✅ (Creation Time)                   | ✅    |
 | ses-configuration-set       | SesConfigurationset          | ✅ (Configuration set name)            | ❌                                   | ❌    |
 | ses-email-template          | SesEmailTemplates            | ✅ (Template Name)                     | ✅ (Creation Time)                   | ❌    |
diff --git a/aws/resource_registry.go b/aws/resource_registry.go
index 63a8a9c..6434183 100644
--- a/aws/resource_registry.go
+++ b/aws/resource_registry.go
@@ -100,6 +100,7 @@ func getRegisteredRegionalResources() []AwsResource {
 		&resources.DBSubnetGroups{},
 		&resources.DBClusters{},
 		&resources.RdsSnapshot{},
+		&resources.RdsParameterGroup{},
 		&resources.RedshiftClusters{},
 		&resources.S3Buckets{},
 		&resources.SageMakerNotebookInstances{},
diff --git a/aws/resources/rds_parameter_group.go b/aws/resources/rds_parameter_group.go
new file mode 100644
index 0000000..bc51061
--- /dev/null
+++ b/aws/resources/rds_parameter_group.go
@@ -0,0 +1,69 @@
+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/go-commons/errors"
+)
+
+func (pg *RdsParameterGroup) getAll(c context.Context, configObj config.Config) ([]*string, error) {
+	var names []*string
+	err := pg.Client.DescribeDBParameterGroupsPages(
+		&rds.DescribeDBParameterGroupsInput{},
+		func(page *rds.DescribeDBParameterGroupsOutput, lastPage bool) bool {
+			for _, parameterGroup := range page.DBParameterGroups {
+				if configObj.RdsParameterGroup.ShouldInclude(config.ResourceValue{
+					Name: parameterGroup.DBParameterGroupName,
+				}) {
+					names = append(names, parameterGroup.DBParameterGroupName)
+				}
+			}
+
+			return !lastPage
+		})
+	if err != nil {
+		return nil, errors.WithStackTrace(err)
+	}
+
+	return names, nil
+}
+
+func (pg *RdsParameterGroup) nukeAll(identifiers []*string) error {
+	if len(identifiers) == 0 {
+		logging.Debugf("No DB parameter groups in region %s", pg.Region)
+		return nil
+	}
+
+	logging.Debugf("Deleting all DB parameter groups in region %s", pg.Region)
+	var deleted []*string
+
+	for _, identifier := range identifiers {
+		logging.Debugf("[RDS Parameter Group] Deleting %s in region %s", *identifier, pg.Region)
+
+		_, err := pg.Client.DeleteDBParameterGroup(&rds.DeleteDBParameterGroupInput{
+			DBParameterGroupName: identifier,
+		})
+		if err != nil {
+			logging.Errorf("[RDS Parameter Group] Error deleting RDS Parameter Group %s: %s", *identifier, err)
+		} else {
+			deleted = append(deleted, identifier)
+			logging.Debugf("[RDS Parameter Group] Deleted RDS Parameter Group %s", *identifier)
+		}
+
+		// Record status of this resource
+		e := report.Entry{
+			Identifier:   aws.StringValue(identifier),
+			ResourceType: pg.ResourceName(),
+			Error:        err,
+		}
+		report.Record(e)
+	}
+
+	logging.Debugf("[OK] %d RDS DB parameter group(s) nuked in %s", len(deleted), pg.Region)
+	return nil
+}
diff --git a/aws/resources/rds_parameter_group_types.go b/aws/resources/rds_parameter_group_types.go
new file mode 100644
index 0000000..2ce7737
--- /dev/null
+++ b/aws/resources/rds_parameter_group_types.go
@@ -0,0 +1,56 @@
+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 RdsParameterGroup struct {
+	BaseAwsResource
+	Client     rdsiface.RDSAPI
+	Region     string
+	GroupNames []string
+}
+
+func (pg *RdsParameterGroup) Init(session *session.Session) {
+	pg.Client = rds.New(session)
+}
+
+func (pg *RdsParameterGroup) ResourceName() string {
+	return "rds-parameter-group"
+}
+
+// ResourceIdentifiers - The names of the rds parameter group
+func (pg *RdsParameterGroup) ResourceIdentifiers() []string {
+	return pg.GroupNames
+}
+
+func (pg *RdsParameterGroup) MaxBatchSize() int {
+	// Tentative batch size to ensure AWS doesn't throttle
+	return 49
+}
+
+func (pg *RdsParameterGroup) GetAndSetIdentifiers(c context.Context, configObj config.Config) ([]string, error) {
+	identifiers, err := pg.getAll(c, configObj)
+	if err != nil {
+		return nil, err
+	}
+
+	pg.GroupNames = awsgo.StringValueSlice(identifiers)
+	return pg.GroupNames, nil
+}
+
+// Nuke - nuke 'em all!!!
+func (pg *RdsParameterGroup) Nuke(identifiers []string) error {
+	if err := pg.nukeAll(awsgo.StringSlice(identifiers)); err != nil {
+		return errors.WithStackTrace(err)
+	}
+
+	return nil
+}
diff --git a/config/config.go b/config/config.go
index 5f7abc4..de5398d 100644
--- a/config/config.go
+++ b/config/config.go
@@ -74,6 +74,7 @@ type Config struct {
 	OpenSearchDomain                ResourceType               `yaml:"OpenSearchDomain"`
 	Redshift                        ResourceType               `yaml:"Redshift"`
 	RdsSnapshot                     ResourceType               `yaml:"RdsSnapshot"`
+	RdsParameterGroup               ResourceType               `yaml:"RdsParameterGroup"`
 	S3                              ResourceType               `yaml:"s3"`
 	SESIdentity                     ResourceType               `yaml:"SesIdentity"`
 	SESConfigurationSet             ResourceType               `yaml:"SesConfigurationset"`


Test patch:
diff --git a/aws/resources/rds_parameter_group_test.go b/aws/resources/rds_parameter_group_test.go
new file mode 100644
index 0000000..abe79b5
--- /dev/null
+++ b/aws/resources/rds_parameter_group_test.go
@@ -0,0 +1,96 @@
+package resources
+
+import (
+	"context"
+	"regexp"
+	"testing"
+
+	awsgo "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/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+type mockedRdsDBParameterGroup struct {
+	rdsiface.RDSAPI
+	DescribeDBParameterGroupsOutput rds.DescribeDBParameterGroupsOutput
+	DeleteDBParameterGroupOutput    rds.DeleteDBParameterGroupOutput
+}
+
+func (m mockedRdsDBParameterGroup) DescribeDBParameterGroupsPages(_ *rds.DescribeDBParameterGroupsInput, callback func(*rds.DescribeDBParameterGroupsOutput, bool) bool) error {
+	callback(&m.DescribeDBParameterGroupsOutput, true)
+	return nil
+}
+
+func (m mockedRdsDBParameterGroup) DeleteDBParameterGroup(*rds.DeleteDBParameterGroupInput) (*rds.DeleteDBParameterGroupOutput, error) {
+	return &m.DeleteDBParameterGroupOutput, nil
+}
+
+func TestRDSparameterGroupGetAll(t *testing.T) {
+	t.Parallel()
+
+	testName01 := "test-db-paramater-group-01"
+	testName02 := "test-db-paramater-group-02"
+
+	pg := RdsParameterGroup{
+		Client: mockedRdsDBParameterGroup{
+			DescribeDBParameterGroupsOutput: rds.DescribeDBParameterGroupsOutput{
+				DBParameterGroups: []*rds.DBParameterGroup{
+					{
+						DBParameterGroupName: awsgo.String(testName01),
+					},
+					{
+						DBParameterGroupName: awsgo.String(testName02),
+					},
+				},
+			},
+		},
+	}
+
+	tests := map[string]struct {
+		configObj config.ResourceType
+		expected  []string
+	}{
+		"emptyFilter": {
+			configObj: config.ResourceType{},
+			expected:  []string{testName01, testName02},
+		},
+		"nameExclusionFilter": {
+			configObj: config.ResourceType{
+				ExcludeRule: config.FilterRule{
+					NamesRegExp: []config.Expression{{
+						RE: *regexp.MustCompile(testName01),
+					}}},
+			},
+			expected: []string{testName02},
+		},
+	}
+	for name, tc := range tests {
+		t.Run(name, func(t *testing.T) {
+			names, err := pg.getAll(context.Background(), config.Config{
+				RdsParameterGroup: tc.configObj,
+			})
+			require.NoError(t, err)
+
+			require.Equal(t, len(tc.expected), len(names))
+			for _, name := range names {
+				require.Contains(t, tc.expected, *name)
+			}
+		})
+	}
+}
+
+func TestRDSParameterGroupNukeAll(t *testing.T) {
+	t.Parallel()
+
+	testName := "test-db-parameter-group"
+	dbCluster := RdsParameterGroup{
+		Client: mockedRdsDBParameterGroup{
+			DeleteDBParameterGroupOutput: rds.DeleteDBParameterGroupOutput{},
+		},
+	}
+	err := dbCluster.nukeAll([]*string{&testName})
+	assert.NoError(t, err)
+}
diff --git a/config/config_test.go b/config/config_test.go
index b16abd8..a35955f 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -92,6 +92,7 @@ func emptyConfig() *Config {
 		ResourceType{FilterRule{}, FilterRule{}},
 		ResourceType{FilterRule{}, FilterRule{}},
 		ResourceType{FilterRule{}, FilterRule{}},
+		ResourceType{FilterRule{}, FilterRule{}},
 	}
 }