Skip to content

Commit df1ad86

Browse files
committed
Add support for nuking Redshift Snapshot Copy Grants
Closes #965 These resources can accumulate from failed test runs and hit the AWS quota limit of 10 grants per region, causing Terraform deployments to fail with SnapshotCopyGrantQuotaExceededFault.
1 parent 4782605 commit df1ad86

File tree

5 files changed

+230
-0
lines changed

5 files changed

+230
-0
lines changed

aws/resource_registry.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ func getRegisteredRegionalResources() []AwsResource {
126126
&resources.RdsSnapshot{},
127127
&resources.RdsParameterGroup{},
128128
&resources.RedshiftClusters{},
129+
&resources.RedshiftSnapshotCopyGrants{},
129130
&resources.S3Buckets{},
130131
&resources.S3AccessPoint{},
131132
&resources.S3ObjectLambdaAccessPoint{},
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package resources
2+
3+
import (
4+
"context"
5+
6+
"github.com/aws/aws-sdk-go-v2/aws"
7+
"github.com/aws/aws-sdk-go-v2/service/redshift"
8+
"github.com/gruntwork-io/cloud-nuke/config"
9+
"github.com/gruntwork-io/cloud-nuke/logging"
10+
"github.com/gruntwork-io/cloud-nuke/report"
11+
"github.com/gruntwork-io/go-commons/errors"
12+
)
13+
14+
func (g *RedshiftSnapshotCopyGrants) getAll(ctx context.Context, configObj config.Config) ([]*string, error) {
15+
var grantNames []*string
16+
var marker *string
17+
18+
for {
19+
output, err := g.Client.DescribeSnapshotCopyGrants(ctx, &redshift.DescribeSnapshotCopyGrantsInput{
20+
Marker: marker,
21+
})
22+
if err != nil {
23+
logging.Debugf("[Redshift Snapshot Copy Grant] Failed to list grants: %s", err)
24+
return nil, errors.WithStackTrace(err)
25+
}
26+
27+
for _, grant := range output.SnapshotCopyGrants {
28+
if configObj.RedshiftSnapshotCopyGrant.ShouldInclude(config.ResourceValue{
29+
Name: grant.SnapshotCopyGrantName,
30+
}) {
31+
grantNames = append(grantNames, grant.SnapshotCopyGrantName)
32+
}
33+
}
34+
35+
if output.Marker == nil {
36+
break
37+
}
38+
marker = output.Marker
39+
}
40+
41+
return grantNames, nil
42+
}
43+
44+
func (g *RedshiftSnapshotCopyGrants) nukeAll(identifiers []*string) error {
45+
if len(identifiers) == 0 {
46+
logging.Debugf("No Redshift Snapshot Copy Grants to nuke in region %s", g.Region)
47+
return nil
48+
}
49+
50+
logging.Debugf("Deleting all Redshift Snapshot Copy Grants in region %s", g.Region)
51+
52+
deletedCount := 0
53+
for _, name := range identifiers {
54+
_, err := g.Client.DeleteSnapshotCopyGrant(g.Context, &redshift.DeleteSnapshotCopyGrantInput{
55+
SnapshotCopyGrantName: name,
56+
})
57+
58+
e := report.Entry{
59+
Identifier: aws.ToString(name),
60+
ResourceType: "Redshift Snapshot Copy Grant",
61+
Error: err,
62+
}
63+
report.Record(e)
64+
65+
if err != nil {
66+
logging.Errorf("[Failed] %s: %s", aws.ToString(name), err)
67+
} else {
68+
deletedCount++
69+
logging.Debugf("Deleted Redshift Snapshot Copy Grant: %s", aws.ToString(name))
70+
}
71+
}
72+
73+
logging.Debugf("[OK] %d Redshift Snapshot Copy Grant(s) deleted in %s", deletedCount, g.Region)
74+
return nil
75+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package resources
2+
3+
import (
4+
"context"
5+
"regexp"
6+
"testing"
7+
8+
"github.com/aws/aws-sdk-go-v2/aws"
9+
"github.com/aws/aws-sdk-go-v2/service/redshift"
10+
"github.com/aws/aws-sdk-go-v2/service/redshift/types"
11+
"github.com/gruntwork-io/cloud-nuke/config"
12+
"github.com/stretchr/testify/require"
13+
)
14+
15+
type mockedRedshiftSnapshotCopyGrants struct {
16+
RedshiftSnapshotCopyGrantsAPI
17+
18+
DescribeSnapshotCopyGrantsOutput redshift.DescribeSnapshotCopyGrantsOutput
19+
DeleteSnapshotCopyGrantOutput redshift.DeleteSnapshotCopyGrantOutput
20+
}
21+
22+
func (m mockedRedshiftSnapshotCopyGrants) DescribeSnapshotCopyGrants(ctx context.Context, input *redshift.DescribeSnapshotCopyGrantsInput, opts ...func(*redshift.Options)) (*redshift.DescribeSnapshotCopyGrantsOutput, error) {
23+
return &m.DescribeSnapshotCopyGrantsOutput, nil
24+
}
25+
26+
func (m mockedRedshiftSnapshotCopyGrants) DeleteSnapshotCopyGrant(ctx context.Context, input *redshift.DeleteSnapshotCopyGrantInput, opts ...func(*redshift.Options)) (*redshift.DeleteSnapshotCopyGrantOutput, error) {
27+
return &m.DeleteSnapshotCopyGrantOutput, nil
28+
}
29+
30+
func TestRedshiftSnapshotCopyGrant_GetAll(t *testing.T) {
31+
t.Parallel()
32+
33+
testName1 := "test-grant1"
34+
testName2 := "test-grant2"
35+
g := RedshiftSnapshotCopyGrants{
36+
Client: mockedRedshiftSnapshotCopyGrants{
37+
DescribeSnapshotCopyGrantsOutput: redshift.DescribeSnapshotCopyGrantsOutput{
38+
SnapshotCopyGrants: []types.SnapshotCopyGrant{
39+
{
40+
SnapshotCopyGrantName: aws.String(testName1),
41+
},
42+
{
43+
SnapshotCopyGrantName: aws.String(testName2),
44+
},
45+
},
46+
},
47+
},
48+
}
49+
50+
tests := map[string]struct {
51+
configObj config.ResourceType
52+
expected []string
53+
}{
54+
"emptyFilter": {
55+
configObj: config.ResourceType{},
56+
expected: []string{testName1, testName2},
57+
},
58+
"nameExclusionFilter": {
59+
configObj: config.ResourceType{
60+
ExcludeRule: config.FilterRule{
61+
NamesRegExp: []config.Expression{{
62+
RE: *regexp.MustCompile(testName1),
63+
}},
64+
},
65+
},
66+
expected: []string{testName2},
67+
},
68+
}
69+
70+
for name, tc := range tests {
71+
t.Run(name, func(t *testing.T) {
72+
names, err := g.getAll(context.Background(), config.Config{
73+
RedshiftSnapshotCopyGrant: tc.configObj,
74+
})
75+
require.NoError(t, err)
76+
require.Equal(t, tc.expected, aws.ToStringSlice(names))
77+
})
78+
}
79+
}
80+
81+
func TestRedshiftSnapshotCopyGrant_NukeAll(t *testing.T) {
82+
t.Parallel()
83+
84+
g := RedshiftSnapshotCopyGrants{
85+
Client: mockedRedshiftSnapshotCopyGrants{
86+
DeleteSnapshotCopyGrantOutput: redshift.DeleteSnapshotCopyGrantOutput{},
87+
},
88+
}
89+
g.Context = context.Background()
90+
91+
err := g.nukeAll([]*string{aws.String("test-grant")})
92+
require.NoError(t, err)
93+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package resources
2+
3+
import (
4+
"context"
5+
6+
"github.com/aws/aws-sdk-go-v2/aws"
7+
"github.com/aws/aws-sdk-go-v2/service/redshift"
8+
"github.com/gruntwork-io/cloud-nuke/config"
9+
"github.com/gruntwork-io/go-commons/errors"
10+
)
11+
12+
type RedshiftSnapshotCopyGrantsAPI interface {
13+
DescribeSnapshotCopyGrants(ctx context.Context, params *redshift.DescribeSnapshotCopyGrantsInput, optFns ...func(*redshift.Options)) (*redshift.DescribeSnapshotCopyGrantsOutput, error)
14+
DeleteSnapshotCopyGrant(ctx context.Context, params *redshift.DeleteSnapshotCopyGrantInput, optFns ...func(*redshift.Options)) (*redshift.DeleteSnapshotCopyGrantOutput, error)
15+
}
16+
17+
type RedshiftSnapshotCopyGrants struct {
18+
BaseAwsResource
19+
Client RedshiftSnapshotCopyGrantsAPI
20+
Region string
21+
GrantNames []string
22+
}
23+
24+
func (g *RedshiftSnapshotCopyGrants) Init(cfg aws.Config) {
25+
g.Client = redshift.NewFromConfig(cfg)
26+
}
27+
28+
func (g *RedshiftSnapshotCopyGrants) ResourceName() string {
29+
return "redshift-snapshot-copy-grant"
30+
}
31+
32+
func (g *RedshiftSnapshotCopyGrants) ResourceIdentifiers() []string {
33+
return g.GrantNames
34+
}
35+
36+
func (g *RedshiftSnapshotCopyGrants) MaxBatchSize() int {
37+
return 49
38+
}
39+
40+
func (g *RedshiftSnapshotCopyGrants) GetAndSetResourceConfig(configObj config.Config) config.ResourceType {
41+
return configObj.RedshiftSnapshotCopyGrant
42+
}
43+
44+
func (g *RedshiftSnapshotCopyGrants) GetAndSetIdentifiers(c context.Context, configObj config.Config) ([]string, error) {
45+
identifiers, err := g.getAll(c, configObj)
46+
if err != nil {
47+
return nil, err
48+
}
49+
50+
g.GrantNames = aws.ToStringSlice(identifiers)
51+
return g.GrantNames, nil
52+
}
53+
54+
func (g *RedshiftSnapshotCopyGrants) Nuke(identifiers []string) error {
55+
if err := g.nukeAll(aws.StringSlice(identifiers)); err != nil {
56+
return errors.WithStackTrace(err)
57+
}
58+
59+
return nil
60+
}

config/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ type Config struct {
103103
OIDCProvider ResourceType `yaml:"OIDCProvider"`
104104
OpenSearchDomain ResourceType `yaml:"OpenSearchDomain"`
105105
Redshift ResourceType `yaml:"Redshift"`
106+
RedshiftSnapshotCopyGrant ResourceType `yaml:"RedshiftSnapshotCopyGrant"`
106107
RdsSnapshot ResourceType `yaml:"RdsSnapshot"`
107108
RdsParameterGroup ResourceType `yaml:"RdsParameterGroup"`
108109
RdsProxy ResourceType `yaml:"RdsProxy"`

0 commit comments

Comments
 (0)