diff --git a/README.md b/README.md index b4fe3040..02b8e5bd 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,7 @@ Cloud-nuke suppports 🔎 inspecting and 🔥💀 deleting the following AWS res | RDS | Neptune | | RDS | Document DB instances | | RDS | RDS parameter group | +| RDS | RDS Proxy | | Security Hub | Hubs | | Security Hub | Members | | Security Hub | Administrators | @@ -602,6 +603,7 @@ of the file that are supported are listed here. | rds | DBInstances | ✅ (DB Name) | ✅ (Creation Time) | ✅ | ✅ | | rds-parameter-group | RdsParameterGroup | ✅ (Group Name) | ❌ | ❌ | ✅ | | rds-subnet-group | DBSubnetGroups | ✅ (DB Subnet Group Name) | ❌ | ❌ | ✅ | +| rds-proxy | RDSProxy | ✅ (proxy Name) | ✅ (Creation Time) | ❌ | ✅ | | s3 | s3 | ✅ (Bucket Name) | ✅ (Creation Time) | ✅ | ✅ | | s3-ap | s3AccessPoint | ✅ (Access point Name) | ❌ | ❌ | ✅ | | s3-olap | S3ObjectLambdaAccessPoint | ✅ (Object Lambda Access point Name) | ❌ | ❌ | ✅ | diff --git a/aws/resource_registry.go b/aws/resource_registry.go index d6e85517..e75a52fe 100644 --- a/aws/resource_registry.go +++ b/aws/resource_registry.go @@ -107,6 +107,7 @@ func getRegisteredRegionalResources() []AwsResource { &resources.DBInstances{}, &resources.DBSubnetGroups{}, &resources.DBClusters{}, + &resources.RdsProxy{}, &resources.RdsSnapshot{}, &resources.RdsParameterGroup{}, &resources.RedshiftClusters{}, diff --git a/aws/resources/rds_proxy.go b/aws/resources/rds_proxy.go new file mode 100644 index 00000000..2d02c3e9 --- /dev/null +++ b/aws/resources/rds_proxy.go @@ -0,0 +1,73 @@ +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 (rdp *RdsProxy) getAll(_ context.Context, configObj config.Config) ([]*string, error) { + var names []*string + err := rdp.Client.DescribeDBProxiesPagesWithContext( + rdp.Context, + &rds.DescribeDBProxiesInput{}, + func(page *rds.DescribeDBProxiesOutput, lastPage bool) bool { + for _, proxy := range page.DBProxies { + if configObj.RdsProxy.ShouldInclude(config.ResourceValue{ + Name: proxy.DBProxyName, + Time: proxy.CreatedDate, + }) { + names = append(names, proxy.DBProxyName) + } + } + + return !lastPage + }) + if err != nil { + return nil, errors.WithStackTrace(err) + } + + return names, nil +} + +func (rdp *RdsProxy) nukeAll(identifiers []*string) error { + if len(identifiers) == 0 { + logging.Debugf("No RDS proxy in region %s", rdp.Region) + return nil + } + + logging.Debugf("Deleting all DB Proxies in region %s", rdp.Region) + var deleted []*string + + for _, identifier := range identifiers { + logging.Debugf("[RDS Proxy] Deleting %s in region %s", *identifier, rdp.Region) + + _, err := rdp.Client.DeleteDBProxyWithContext( + rdp.Context, + &rds.DeleteDBProxyInput{ + DBProxyName: identifier, + }) + if err != nil { + logging.Errorf("[RDS Proxy] Error deleting RDS Proxy %s: %s", *identifier, err) + } else { + deleted = append(deleted, identifier) + logging.Debugf("[RDS Proxy] Deleted RDS Proxy %s", *identifier) + } + + // Record status of this resource + e := report.Entry{ + Identifier: aws.StringValue(identifier), + ResourceType: rdp.ResourceName(), + Error: err, + } + report.Record(e) + } + + logging.Debugf("[OK] %d RDS DB proxi(s) nuked in %s", len(deleted), rdp.Region) + return nil +} diff --git a/aws/resources/rds_proxy_test.go b/aws/resources/rds_proxy_test.go new file mode 100644 index 00000000..433013e5 --- /dev/null +++ b/aws/resources/rds_proxy_test.go @@ -0,0 +1,106 @@ +package resources + +import ( + "context" + "regexp" + "testing" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/request" + "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 mockedRdsProxy struct { + rdsiface.RDSAPI + DescribeDBProxiesOutput rds.DescribeDBProxiesOutput + DeleteDBProxyOutput rds.DeleteDBProxyOutput +} + +func (m mockedRdsProxy) DescribeDBProxiesPagesWithContext(_ aws.Context, _ *rds.DescribeDBProxiesInput, callback func(*rds.DescribeDBProxiesOutput, bool) bool, _ ...request.Option) error { + callback(&m.DescribeDBProxiesOutput, true) + return nil +} + +func (m mockedRdsProxy) DeleteDBProxyWithContext(aws.Context, *rds.DeleteDBProxyInput, ...request.Option) (*rds.DeleteDBProxyOutput, error) { + return &m.DeleteDBProxyOutput, nil +} + +func TestRdsProxy_GetAll(t *testing.T) { + + t.Parallel() + + testName1 := "test-name1" + testName2 := "test-name2" + now := time.Now() + snapshot := RdsProxy{ + Client: mockedRdsProxy{ + DescribeDBProxiesOutput: rds.DescribeDBProxiesOutput{ + DBProxies: []*rds.DBProxy{ + { + DBProxyName: &testName1, + CreatedDate: &now, + }, + { + DBProxyName: &testName2, + CreatedDate: 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{ + RdsProxy: tc.configObj, + }) + require.NoError(t, err) + require.Equal(t, tc.expected, aws.StringValueSlice(names)) + }) + } +} + +func TestRdsProxy_NukeAll(t *testing.T) { + + t.Parallel() + + testName := "test-db-proxy" + snapshot := RdsProxy{ + Client: mockedRdsProxy{ + DeleteDBProxyOutput: rds.DeleteDBProxyOutput{}, + }, + } + + err := snapshot.nukeAll([]*string{&testName}) + assert.NoError(t, err) +} diff --git a/aws/resources/rds_proxy_types.go b/aws/resources/rds_proxy_types.go new file mode 100644 index 00000000..1a7ee031 --- /dev/null +++ b/aws/resources/rds_proxy_types.go @@ -0,0 +1,60 @@ +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 RdsProxy struct { + BaseAwsResource + Client rdsiface.RDSAPI + Region string + GroupNames []string +} + +func (pg *RdsProxy) Init(session *session.Session) { + pg.Client = rds.New(session) +} + +func (pg *RdsProxy) ResourceName() string { + return "rds-proxy" +} + +// ResourceIdentifiers - The names of the rds parameter group +func (pg *RdsProxy) ResourceIdentifiers() []string { + return pg.GroupNames +} + +func (pg *RdsProxy) MaxBatchSize() int { + // Tentative batch size to ensure AWS doesn't throttle + return 49 +} + +func (pg *RdsProxy) GetAndSetResourceConfig(configObj config.Config) config.ResourceType { + return configObj.RdsProxy +} + +func (pg *RdsProxy) 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 *RdsProxy) 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 4018b337..184242cc 100644 --- a/config/config.go +++ b/config/config.go @@ -80,6 +80,7 @@ type Config struct { Redshift ResourceType `yaml:"Redshift"` RdsSnapshot ResourceType `yaml:"RdsSnapshot"` RdsParameterGroup ResourceType `yaml:"RdsParameterGroup"` + RdsProxy ResourceType `yaml:"RdsProxy"` S3 ResourceType `yaml:"s3"` S3AccessPoint ResourceType `yaml:"S3AccessPoint"` S3ObjectLambdaAccessPoint ResourceType `yaml:"S3ObjectLambdaAccessPoint"` diff --git a/config/config_test.go b/config/config_test.go index 5b508fa7..9ffe1dbe 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -97,6 +97,7 @@ func emptyConfig() *Config { ResourceType{FilterRule{}, FilterRule{}, ""}, ResourceType{FilterRule{}, FilterRule{}, ""}, ResourceType{FilterRule{}, FilterRule{}, ""}, + ResourceType{FilterRule{}, FilterRule{}, ""}, EC2ResourceType{false, ResourceType{FilterRule{}, FilterRule{}, ""}}, ResourceType{FilterRule{}, FilterRule{}, ""}, ResourceType{FilterRule{}, FilterRule{}, ""},