Skip to content

Commit

Permalink
Implement support for SNS Topics (#354)
Browse files Browse the repository at this point in the history
  • Loading branch information
zackproser authored Aug 23, 2022
1 parent d1e276b commit 04b0bb1
Show file tree
Hide file tree
Showing 7 changed files with 302 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ The currently supported functionality includes:
- Inspecting and deleting all Kinesis Streams in an AWS account
- Inspecting and deleting all API Gateways (v1 and v2) in an AWS account
- Inspecting and deleting all Elastic FileSystems (efs) in an AWS account
- Inspecting and deleting all SNS Topics in an AWS account

### BEWARE!

Expand Down
15 changes: 15 additions & 0 deletions aws/aws.go
Original file line number Diff line number Diff line change
Expand Up @@ -802,6 +802,20 @@ func GetAllResources(targetRegions []string, excludeAfter time.Time, resourceTyp
}
// End Elastic FileSystems (efs)

// SNS Topics
snsTopics := SNSTopic{}
if IsNukeable(snsTopics.ResourceName(), resourceTypes) {
snsTopicArns, err := getAllSNSTopics(cloudNukeSession, excludeAfter, configObj)
if err != nil {
return nil, errors.WithStackTrace(err)
}
if len(snsTopicArns) > 0 {
snsTopics.Arns = awsgo.StringValueSlice(snsTopicArns)
resourcesInRegion.Resources = append(resourcesInRegion.Resources, snsTopics)
}
}
// End SNS Topics

if len(resourcesInRegion.Resources) > 0 {
account.Resources[region] = resourcesInRegion
}
Expand Down Expand Up @@ -917,6 +931,7 @@ func ListResourceTypes() []string {
ApiGateway{}.ResourceName(),
ApiGatewayV2{}.ResourceName(),
ElasticFileSystem{}.ResourceName(),
SNSTopic{}.ResourceName(),
}
sort.Strings(resourceTypes)
return resourceTypes
Expand Down
102 changes: 102 additions & 0 deletions aws/sns.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package aws

import (
"context"
"sync"
"time"

awsconfig "github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/sns"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/gruntwork-io/cloud-nuke/config"
"github.com/gruntwork-io/cloud-nuke/logging"
"github.com/gruntwork-io/go-commons/errors"
"github.com/hashicorp/go-multierror"
)

func getAllSNSTopics(session *session.Session, excludeAfter time.Time, configObj config.Config) ([]*string, error) {
cfg, err := awsconfig.LoadDefaultConfig(context.TODO(), awsconfig.WithRegion(aws.StringValue(session.Config.Region)))
if err != nil {
return []*string{}, errors.WithStackTrace(err)
}
svc := sns.NewFromConfig(cfg)

allSNSTopics := []*string{}

paginator := sns.NewListTopicsPaginator(svc, nil)

for paginator.HasMorePages() {
resp, err := paginator.NextPage(context.TODO())
if err != nil {
return []*string{}, errors.WithStackTrace(err)
}
for _, topic := range resp.Topics {
allSNSTopics = append(allSNSTopics, topic.TopicArn)
}
}
return allSNSTopics, nil
}

func nukeAllSNSTopics(session *session.Session, identifiers []*string) error {
region := aws.StringValue(session.Config.Region)

cfg, err := awsconfig.LoadDefaultConfig(context.TODO(), awsconfig.WithRegion(aws.StringValue(session.Config.Region)))
if err != nil {
return errors.WithStackTrace(err)
}
svc := sns.NewFromConfig(cfg)

if len(identifiers) == 0 {
logging.Logger.Infof("No SNS Topics to nuke in region %s", region)
}

if len(identifiers) > 100 {
logging.Logger.Errorf("Nuking too many SNS Topics (100): halting to avoid hitting AWS API rate limiting")
return TooManySNSTopicsErr{}
}

// There is no bulk delete SNS API, so we delete the batch of SNS Topics concurrently using goroutines
logging.Logger.Infof("Deleting SNS Topics in region %s", region)
wg := new(sync.WaitGroup)
wg.Add(len(identifiers))
errChans := make([]chan error, len(identifiers))
for i, topicArn := range identifiers {
errChans[i] = make(chan error, 1)
go deleteSNSTopicAsync(wg, errChans[i], svc, topicArn, region)
}
wg.Wait()

var allErrs *multierror.Error
for _, errChan := range errChans {
if err := <-errChan; err != nil {
allErrs = multierror.Append(allErrs, err)
logging.Logger.Errorf("[Failed] %s", err)
}
}
finalErr := allErrs.ErrorOrNil()
if finalErr != nil {
return errors.WithStackTrace(finalErr)
}
return nil
}

func deleteSNSTopicAsync(wg *sync.WaitGroup, errChan chan error, svc *sns.Client, topicArn *string, region string) {
defer wg.Done()

deleteParam := &sns.DeleteTopicInput{
TopicArn: topicArn,
}

logging.Logger.Infof("Deleting SNS Topic (arn=%s) in region: %s", aws.StringValue(topicArn), region)

_, err := svc.DeleteTopic(context.TODO(), deleteParam)

errChan <- err

if err == nil {
logging.Logger.Infof("[OK] Deleted SNS Topic (arn=%s) in region: %s", aws.StringValue(topicArn), region)
} else {
logging.Logger.Errorf("[Failed] Error deleting SNS Topic (arn=%s) in %s", aws.StringValue(topicArn), region)
}
}
143 changes: 143 additions & 0 deletions aws/sns_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package aws

import (
"context"
"fmt"
"math/rand"
"testing"
"time"

awsconfig "github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/sns"
"github.com/aws/aws-sdk-go/aws"
awsgo "github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/gruntwork-io/cloud-nuke/config"
"github.com/gruntwork-io/cloud-nuke/util"
"github.com/gruntwork-io/go-commons/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

type TestSNSTopic struct {
Name *string
Arn *string
}

func createTestSNSTopic(t *testing.T, session *session.Session, name string) (*TestSNSTopic, error) {
cfg, err := awsconfig.LoadDefaultConfig(context.TODO(), awsconfig.WithRegion(aws.StringValue(session.Config.Region)))
require.NoError(t, err)

svc := sns.NewFromConfig(cfg)

testSNSTopic := &TestSNSTopic{
Name: aws.String(name),
}

param := &sns.CreateTopicInput{
Name: testSNSTopic.Name,
}

// Do a coin-flip to choose either a FIFO or Standard SNS Topic
coin := []string{
"true",
"false",
}
rand.Seed(time.Now().UnixNano())
coinflip := coin[rand.Intn(len(coin))]
param.Attributes = make(map[string]string)
param.Attributes["FifoTopic"] = coinflip

// If we did choose to create a fifo queue, the name must end in ".fifo"
if coinflip == "true" {
param.Name = aws.String(fmt.Sprintf("%s.fifo", aws.StringValue(param.Name)))
}

output, err := svc.CreateTopic(context.TODO(), param)
if err != nil {
assert.Failf(t, "Could not create test SNS Topic: %s", errors.WithStackTrace(err).Error())
}

testSNSTopic.Arn = output.TopicArn

return testSNSTopic, nil
}

func TestListSNSTopics(t *testing.T) {
t.Parallel()

region, err := getRandomRegion()
require.NoError(t, err)
session, err := session.NewSession(&awsgo.Config{
Region: awsgo.String(region),
},
)
if err != nil {
assert.Fail(t, errors.WithStackTrace(err).Error())
}

snsTopicName := "aws-nuke-test-" + util.UniqueID()
testSNSTopic, createTestSNSTopicErr := createTestSNSTopic(t, session, snsTopicName)
require.NoError(t, createTestSNSTopicErr)
// clean up after this test
defer nukeAllSNSTopics(session, []*string{testSNSTopic.Arn})

snsTopicArns, err := getAllSNSTopics(session, time.Now(), config.Config{})
if err != nil {
assert.Fail(t, "Unable to fetch list of SNS Topics")
}

assert.Contains(t, awsgo.StringValueSlice(snsTopicArns), aws.StringValue(testSNSTopic.Arn))
}

func TestNukeSNSTopicOne(t *testing.T) {
t.Parallel()

region, err := getRandomRegion()
require.NoError(t, err)

session, err := session.NewSession(&aws.Config{Region: aws.String(region)})
require.NoError(t, err)

snsTopicName := "aws-nuke-test-" + util.UniqueID()

testSNSTopic, createTestSNSTopicErr := createTestSNSTopic(t, session, snsTopicName)
require.NoError(t, createTestSNSTopicErr)

nukeErr := nukeAllSNSTopics(session, []*string{testSNSTopic.Arn})
require.NoError(t, nukeErr)

// Make sure the SNS Topic was deleted
snsTopicArns, err := getAllSNSTopics(session, time.Now(), config.Config{})
require.NoError(t, err)

assert.NotContains(t, aws.StringValueSlice(snsTopicArns), aws.StringValue(testSNSTopic.Arn))
}

func TestNukeSNSTopicMoreThanOne(t *testing.T) {
t.Parallel()

region, err := getRandomRegion()
require.NoError(t, err)

session, err := session.NewSession(&aws.Config{Region: aws.String(region)})
require.NoError(t, err)

testSNSTopicName := "aws-nuke-test-" + util.UniqueID()
testSNSTopicName2 := "aws-nuke-test-" + util.UniqueID()

testSNSTopic, createTestErr := createTestSNSTopic(t, session, testSNSTopicName)
require.NoError(t, createTestErr)
testSNSTopic2, createTestErr2 := createTestSNSTopic(t, session, testSNSTopicName2)
require.NoError(t, createTestErr2)

nukeErr := nukeAllSNSTopics(session, []*string{testSNSTopic.Arn, testSNSTopic2.Arn})
require.NoError(t, nukeErr)

// Make sure the SNS topics were deleted
snsTopicArns, err := getAllSNSTopics(session, time.Now(), config.Config{})
require.NoError(t, err)

assert.NotContains(t, aws.StringValueSlice(snsTopicArns), aws.StringValue(testSNSTopic.Arn))
assert.NotContains(t, aws.StringValueSlice(snsTopicArns), aws.StringValue(testSNSTopic2.Arn))
}
38 changes: 38 additions & 0 deletions aws/sns_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package aws

import (
awsgo "github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/gruntwork-io/go-commons/errors"
)

type SNSTopic struct {
Arns []string
}

func (s SNSTopic) ResourceName() string {
return "snstopic"
}

func (s SNSTopic) ResourceIdentifiers() []string {
return s.Arns
}

func (s SNSTopic) MaxBatchSize() int {
return 50
}

func (s SNSTopic) Nuke(session *session.Session, identifiers []string) error {
if err := nukeAllSNSTopics(session, awsgo.StringSlice(identifiers)); err != nil {
return errors.WithStackTrace(err)
}
return nil
}

// custom errors

type TooManySNSTopicsErr struct{}

func (err TooManySNSTopicsErr) Error() string {
return "Too many SNS Topics requested at once."
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ require (
github.com/aws/aws-sdk-go v1.44.74
github.com/aws/aws-sdk-go-v2/config v1.17.1
github.com/aws/aws-sdk-go-v2/service/efs v1.17.10
github.com/aws/aws-sdk-go-v2/service/sns v1.17.13
github.com/fatih/color v1.9.0
github.com/golang/mock v1.6.0
github.com/gruntwork-io/go-commons v0.8.2
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ github.com/aws/aws-sdk-go-v2/service/efs v1.17.10 h1:3dvqOTkKIdO61/yeXD9m6diukbe
github.com/aws/aws-sdk-go-v2/service/efs v1.17.10/go.mod h1:f1PVX6O2FYtO5ujTK6ewawqGUDYIH7keEWnbYStZVtM=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.12 h1:7iPTTX4SAI2U2VOogD7/gmHlsgnYSgoNHt7MSQXtG2M=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.12/go.mod h1:1TODGhheLWjpQWSuhYuAUWYTCKwEjx2iblIFKDHjeTc=
github.com/aws/aws-sdk-go-v2/service/sns v1.17.13 h1:sa8NDFztt68pihEfE31LhX+nJ1wDBJHcFh3T6crluDo=
github.com/aws/aws-sdk-go-v2/service/sns v1.17.13/go.mod h1:yE3hE9v3YRRI9Rsl38kYJ4fyZ6vKSljaZ+28W5xzqgM=
github.com/aws/aws-sdk-go-v2/service/sso v1.11.17 h1:pXxu9u2z1UqSbjO9YA8kmFJBhFc1EVTDaf7A+S+Ivq8=
github.com/aws/aws-sdk-go-v2/service/sso v1.11.17/go.mod h1:mS5xqLZc/6kc06IpXn5vRxdLaED+jEuaSRv5BxtnsiY=
github.com/aws/aws-sdk-go-v2/service/sts v1.16.13 h1:dl8T0PJlN92rvEGOEUiD0+YPYdPEaCZK0TqHukvSfII=
Expand Down

0 comments on commit 04b0bb1

Please sign in to comment.