Skip to content

Commit 1ec0f16

Browse files
author
Andrew Ellison
authored
add support for redshift (#456)
1 parent 608710b commit 1ec0f16

File tree

8 files changed

+233
-4
lines changed

8 files changed

+233
-4
lines changed

README.md

+4
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,9 @@ The following resources support the Config file:
456456
- CloudWatch Alarms
457457
- Resource type: `cloudwatch-alarm`
458458
- Config key: `CloudWatchAlarm`
459+
- Redshift
460+
- Resource type: `redshift`
461+
- Config key: `Redshift`
459462

460463

461464

@@ -591,6 +594,7 @@ To find out what we options are supported in the config file today, consult this
591594
| config-recorders | none | ✅ | none | none |
592595
| config-rules | none | ✅ | none | none |
593596
| cloudwatch-alarm | none | ✅ | none | none |
597+
| redshift | none | ✅ | none | none |
594598
| ... (more to come) | none | none | none | none |
595599

596600

aws/aws.go

+28
Original file line numberDiff line numberDiff line change
@@ -1376,6 +1376,33 @@ func GetAllResources(targetRegions []string, excludeAfter time.Time, resourceTyp
13761376
}
13771377
// End Kinesis Streams
13781378

1379+
// Redshift Clusters
1380+
redshiftClusters := RedshiftClusters{}
1381+
if IsNukeable(redshiftClusters.ResourceName(), resourceTypes) {
1382+
start := time.Now()
1383+
clusters, err := getAllRedshiftClusters(cloudNukeSession, region, excludeAfter, configObj)
1384+
if err != nil {
1385+
ge := report.GeneralError{
1386+
Error: err,
1387+
Description: "Unable to retrieve redshift clusters",
1388+
ResourceType: redshiftClusters.ResourceName(),
1389+
}
1390+
report.RecordError(ge)
1391+
}
1392+
telemetry.TrackEvent(commonTelemetry.EventContext{
1393+
EventName: "Done Listing Redshift Clusters",
1394+
}, map[string]interface{}{
1395+
"region": region,
1396+
"recordCount": len(clusters),
1397+
"actionTime": time.Since(start).Seconds(),
1398+
})
1399+
if len(clusters) > 0 {
1400+
redshiftClusters.ClusterIdentifiers = awsgo.StringValueSlice(clusters)
1401+
resourcesInRegion.Resources = append(resourcesInRegion.Resources, redshiftClusters)
1402+
}
1403+
}
1404+
// End Redshift Clusters
1405+
13791406
// API Gateways (v1)
13801407
apiGateways := ApiGateway{}
13811408
if IsNukeable(apiGateways.ResourceName(), resourceTypes) {
@@ -1847,6 +1874,7 @@ func ListResourceTypes() []string {
18471874
MacieMember{}.ResourceName(),
18481875
SageMakerNotebookInstances{}.ResourceName(),
18491876
KinesisStreams{}.ResourceName(),
1877+
RedshiftClusters{}.ResourceName(),
18501878
ApiGateway{}.ResourceName(),
18511879
ApiGatewayV2{}.ResourceName(),
18521880
ElasticFileSystem{}.ResourceName(),

aws/elasticache.go

+4-4
Original file line numberDiff line numberDiff line change
@@ -272,8 +272,8 @@ func shouldIncludeElasticacheParameterGroup(paramGroup *elasticache.CacheParamet
272272

273273
return config.ShouldInclude(
274274
aws.StringValue(paramGroup.CacheParameterGroupName),
275-
configObj.ElasticacheParameterGroup.IncludeRule.NamesRegExp,
276-
configObj.ElasticacheParameterGroup.ExcludeRule.NamesRegExp,
275+
configObj.ElasticacheParameterGroups.IncludeRule.NamesRegExp,
276+
configObj.ElasticacheParameterGroups.ExcludeRule.NamesRegExp,
277277
)
278278
}
279279

@@ -338,8 +338,8 @@ func shouldIncludeElasticacheSubnetGroup(subnetGroup *elasticache.CacheSubnetGro
338338

339339
return config.ShouldInclude(
340340
aws.StringValue(subnetGroup.CacheSubnetGroupName),
341-
configObj.ElasticacheSubnetGroup.IncludeRule.NamesRegExp,
342-
configObj.ElasticacheSubnetGroup.ExcludeRule.NamesRegExp,
341+
configObj.ElasticacheSubnetGroups.IncludeRule.NamesRegExp,
342+
configObj.ElasticacheSubnetGroups.ExcludeRule.NamesRegExp,
343343
)
344344
}
345345

aws/redshift.go

+93
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package aws
2+
3+
import (
4+
"github.com/aws/aws-sdk-go/aws"
5+
"github.com/aws/aws-sdk-go/aws/session"
6+
"github.com/aws/aws-sdk-go/service/redshift"
7+
"github.com/gruntwork-io/cloud-nuke/config"
8+
"github.com/gruntwork-io/cloud-nuke/logging"
9+
"github.com/gruntwork-io/cloud-nuke/report"
10+
"github.com/gruntwork-io/cloud-nuke/telemetry"
11+
"github.com/gruntwork-io/go-commons/errors"
12+
commonTelemetry "github.com/gruntwork-io/go-commons/telemetry"
13+
"time"
14+
)
15+
16+
func getAllRedshiftClusters(session *session.Session, region string, excludeAfter time.Time, configObj config.Config) ([]*string, error) {
17+
svc := redshift.New(session)
18+
var clusterIds []*string
19+
err := svc.DescribeClustersPages(
20+
&redshift.DescribeClustersInput{},
21+
func(page *redshift.DescribeClustersOutput, lastPage bool) bool {
22+
for _, cluster := range page.Clusters {
23+
if shouldIncludeRedshiftCluster(cluster, excludeAfter, configObj) {
24+
clusterIds = append(clusterIds, cluster.ClusterIdentifier)
25+
}
26+
}
27+
return !lastPage
28+
},
29+
)
30+
return clusterIds, errors.WithStackTrace(err)
31+
}
32+
33+
func shouldIncludeRedshiftCluster(cluster *redshift.Cluster, excludeAfter time.Time, configObj config.Config) bool {
34+
if cluster == nil {
35+
return false
36+
}
37+
if excludeAfter.Before(*cluster.ClusterCreateTime) {
38+
return false
39+
}
40+
return config.ShouldInclude(
41+
aws.StringValue(cluster.ClusterIdentifier),
42+
configObj.Redshift.IncludeRule.NamesRegExp,
43+
configObj.Redshift.ExcludeRule.NamesRegExp,
44+
)
45+
}
46+
47+
func nukeAllRedshiftClusters(session *session.Session, identifiers []*string) error {
48+
svc := redshift.New(session)
49+
if len(identifiers) == 0 {
50+
logging.Logger.Debugf("No Redshift Clusters to nuke in region %s", *session.Config.Region)
51+
return nil
52+
}
53+
logging.Logger.Debugf("Deleting all Redshift Clusters in region %s", *session.Config.Region)
54+
deletedIds := []*string{}
55+
for _, id := range identifiers {
56+
_, err := svc.DeleteCluster(&redshift.DeleteClusterInput{ClusterIdentifier: id, SkipFinalClusterSnapshot: aws.Bool(true)})
57+
if err != nil {
58+
telemetry.TrackEvent(commonTelemetry.EventContext{
59+
EventName: "Error Nuking RedshiftCluster",
60+
}, map[string]interface{}{
61+
"region": *session.Config.Region,
62+
})
63+
logging.Logger.Errorf("[Failed] %s: %s", *id, err)
64+
} else {
65+
deletedIds = append(deletedIds, id)
66+
logging.Logger.Debugf("Deleted Redshift Cluster: %s", aws.StringValue(id))
67+
}
68+
}
69+
70+
if len(deletedIds) > 0 {
71+
for _, id := range deletedIds {
72+
err := svc.WaitUntilClusterDeleted(&redshift.DescribeClustersInput{ClusterIdentifier: id})
73+
// Record status of this resource
74+
e := report.Entry{
75+
Identifier: aws.StringValue(id),
76+
ResourceType: "Redshift Cluster",
77+
Error: err,
78+
}
79+
report.Record(e)
80+
if err != nil {
81+
telemetry.TrackEvent(commonTelemetry.EventContext{
82+
EventName: "Error Nuking Redshift Cluster",
83+
}, map[string]interface{}{
84+
"region": *session.Config.Region,
85+
})
86+
logging.Logger.Errorf("[Failed] %s", err)
87+
return errors.WithStackTrace(err)
88+
}
89+
}
90+
}
91+
logging.Logger.Debugf("[OK] %d Redshift Cluster(s) deleted in %s", len(deletedIds), *session.Config.Region)
92+
return nil
93+
}

aws/redshift_test.go

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package aws
2+
3+
import (
4+
"github.com/aws/aws-sdk-go/aws"
5+
awsSession "github.com/aws/aws-sdk-go/aws/session"
6+
"github.com/aws/aws-sdk-go/service/redshift"
7+
"github.com/gruntwork-io/cloud-nuke/config"
8+
"github.com/gruntwork-io/cloud-nuke/telemetry"
9+
"github.com/gruntwork-io/cloud-nuke/util"
10+
"github.com/stretchr/testify/assert"
11+
"github.com/stretchr/testify/require"
12+
"strings"
13+
"testing"
14+
"time"
15+
)
16+
17+
func TestNukeRedshiftClusters(t *testing.T) {
18+
telemetry.InitTelemetry("cloud-nuke", "", "")
19+
t.Parallel()
20+
region, err := getRandomRegion()
21+
require.NoError(t, err)
22+
23+
session, err := awsSession.NewSession(&aws.Config{
24+
Region: aws.String(region),
25+
})
26+
require.NoError(t, err)
27+
28+
svc := redshift.New(session)
29+
30+
clusterName := "test-" + strings.ToLower(util.UniqueID())
31+
32+
//create cluster
33+
_, err = svc.CreateCluster(
34+
&redshift.CreateClusterInput{
35+
ClusterIdentifier: aws.String(clusterName),
36+
MasterUsername: aws.String("grunty"),
37+
MasterUserPassword: aws.String("Gruntysecurepassword1"),
38+
NodeType: aws.String("dc2.large"),
39+
NumberOfNodes: aws.Int64(2),
40+
},
41+
)
42+
require.NoError(t, err)
43+
err = svc.WaitUntilClusterAvailable(&redshift.DescribeClustersInput{
44+
ClusterIdentifier: aws.String(clusterName),
45+
})
46+
require.NoError(t, err)
47+
defer svc.DeleteCluster(&redshift.DeleteClusterInput{ClusterIdentifier: aws.String(clusterName)})
48+
49+
//Sleep for a minute for consistency in aws
50+
sleepTime, err := time.ParseDuration("1m")
51+
time.Sleep(sleepTime)
52+
53+
//test list clusters
54+
clusters, err := getAllRedshiftClusters(session, region, time.Now().Add(1*time.Hour), config.Config{})
55+
require.NoError(t, err)
56+
57+
//Ensure our cluster exists
58+
assert.Contains(t, aws.StringValueSlice(clusters), clusterName)
59+
60+
//nuke cluster
61+
err = nukeAllRedshiftClusters(session, aws.StringSlice([]string{clusterName}))
62+
require.NoError(t, err)
63+
64+
//check that the cluster no longer exists
65+
clusters, err = getAllRedshiftClusters(session, region, time.Now().Add(1*time.Hour), config.Config{})
66+
require.NoError(t, err)
67+
assert.NotContains(t, aws.StringValueSlice(clusters), aws.StringSlice([]string{clusterName}))
68+
}

aws/redshift_types.go

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package aws
2+
3+
import (
4+
awsgo "github.com/aws/aws-sdk-go/aws"
5+
"github.com/aws/aws-sdk-go/aws/session"
6+
"github.com/gruntwork-io/go-commons/errors"
7+
)
8+
9+
type RedshiftClusters struct {
10+
ClusterIdentifiers []string
11+
}
12+
13+
func (cluster RedshiftClusters) ResourceName() string {
14+
return "redshift"
15+
}
16+
17+
// ResourceIdentifiers - The instance names of the rds db instances
18+
func (cluster RedshiftClusters) ResourceIdentifiers() []string {
19+
return cluster.ClusterIdentifiers
20+
}
21+
22+
func (cluster RedshiftClusters) MaxBatchSize() int {
23+
// Tentative batch size to ensure AWS doesn't throttle
24+
return 49
25+
}
26+
27+
// Nuke - nuke 'em all!!!
28+
func (cluster RedshiftClusters) Nuke(session *session.Session, identifiers []string) error {
29+
if err := nukeAllRedshiftClusters(session, awsgo.StringSlice(identifiers)); err != nil {
30+
return errors.WithStackTrace(err)
31+
}
32+
33+
return nil
34+
}

config/config.go

+1
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ type Config struct {
5353
ConfigServiceRule ResourceType `yaml:"ConfigServiceRule"`
5454
ConfigServiceRecorder ResourceType `yaml:"ConfigServiceRecorder"`
5555
CloudWatchAlarm ResourceType `yaml:"CloudWatchAlarm"`
56+
Redshift ResourceType `yaml:"Redshift"`
5657
}
5758

5859
type ResourceType struct {

config/config_test.go

+1
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ func emptyConfig() *Config {
5454
ResourceType{FilterRule{}, FilterRule{}},
5555
ResourceType{FilterRule{}, FilterRule{}},
5656
ResourceType{FilterRule{}, FilterRule{}},
57+
ResourceType{FilterRule{}, FilterRule{}},
5758
}
5859
}
5960

0 commit comments

Comments
 (0)