Skip to content
This repository has been archived by the owner on Oct 15, 2024. It is now read-only.

Commit

Permalink
add cloudformation stackset support (#475)
Browse files Browse the repository at this point in the history
  • Loading branch information
tylersouthwick authored Feb 13, 2020
1 parent 4760e7d commit 5d16eb6
Show file tree
Hide file tree
Showing 2 changed files with 355 additions and 0 deletions.
158 changes: 158 additions & 0 deletions resources/cloudformation-stackset.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package resources

import (
"fmt"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/cloudformation"
"github.com/aws/aws-sdk-go/service/cloudformation/cloudformationiface"
"github.com/rebuy-de/aws-nuke/pkg/types"
"github.com/sirupsen/logrus"
"strings"
"time"
)

func init() {
register("CloudFormationStackSet", ListCloudFormationStackSets)
}

func ListCloudFormationStackSets(sess *session.Session) ([]Resource, error) {
svc := cloudformation.New(sess)

params := &cloudformation.ListStackSetsInput{}
resources := make([]Resource, 0)

for {
resp, err := svc.ListStackSets(params)
if err != nil {
return nil, err
}
for _, stackSetSummary := range resp.Summaries {
resources = append(resources, &CloudFormationStackSet{
svc: svc,
stackSetSummary: stackSetSummary,
sleepDuration: 10 * time.Second,
})
}

if resp.NextToken == nil {
break
}

params.NextToken = resp.NextToken
}

return resources, nil
}

type CloudFormationStackSet struct {
svc cloudformationiface.CloudFormationAPI
stackSetSummary *cloudformation.StackSetSummary
sleepDuration time.Duration
}

func (cfs *CloudFormationStackSet) findStackInstances() (map[string][]string, error) {
accounts := make(map[string][]string)

input := &cloudformation.ListStackInstancesInput{
StackSetName: cfs.stackSetSummary.StackSetName,
}

for {
resp, err := cfs.svc.ListStackInstances(input)
if err != nil {
return nil, err
}
for _, stackInstanceSummary := range resp.Summaries {
if regions, ok := accounts[*stackInstanceSummary.Account]; !ok {
accounts[*stackInstanceSummary.Account] = []string{*stackInstanceSummary.Region}
} else {
accounts[*stackInstanceSummary.Account] = append(regions, *stackInstanceSummary.Region)
}
}

if resp.NextToken == nil {
break
}

input.NextToken = resp.NextToken
}

return accounts, nil
}

func (cfs *CloudFormationStackSet) waitForStackSetOperation(operationId string) error {
for {
result, err := cfs.svc.DescribeStackSetOperation(&cloudformation.DescribeStackSetOperationInput{
StackSetName: cfs.stackSetSummary.StackSetName,
OperationId: &operationId,
})
if err != nil {
return err
}
logrus.Infof("Got stackInstance operation status on stackSet=%s operationId=%s status=%s", *cfs.stackSetSummary.StackSetName, operationId, *result.StackSetOperation.Status)
if *result.StackSetOperation.Status == cloudformation.StackSetOperationResultStatusSucceeded {
return nil
} else if *result.StackSetOperation.Status == cloudformation.StackSetOperationResultStatusFailed || *result.StackSetOperation.Status == cloudformation.StackSetOperationResultStatusCancelled {
return fmt.Errorf("unable to delete stackSet=%s operationId=%s status=%s", *cfs.stackSetSummary.StackSetName, operationId, *result.StackSetOperation.Status)
} else {
logrus.Infof("Waiting on stackSet=%s operationId=%s status=%s", *cfs.stackSetSummary.StackSetName, operationId, *result.StackSetOperation.Status)
time.Sleep(cfs.sleepDuration)
}
}
}

func (cfs *CloudFormationStackSet) deleteStackInstances(accountId string, regions []string) error {
logrus.Infof("Deleting stack instance accountId=%s regions=%s", accountId, strings.Join(regions, ","))
regionsInput := make([]*string, len(regions))
for i, region := range regions {
regionsInput[i] = aws.String(region)
fmt.Printf("region=%s i=%d\n", region, i)
}
result, err := cfs.svc.DeleteStackInstances(&cloudformation.DeleteStackInstancesInput{
StackSetName: cfs.stackSetSummary.StackSetName,
Accounts: []*string{&accountId},
Regions: regionsInput,
RetainStacks: aws.Bool(true), //this will remove the stack set instance from the stackset, but will leave the stack in the account/region it was deployed to
})

fmt.Printf("got result=%v err=%v\n", result, err)

if result == nil {
return fmt.Errorf("got null result")
}
if err != nil {
return err
}

return cfs.waitForStackSetOperation(*result.OperationId)
}

func (cfs *CloudFormationStackSet) Remove() error {
accounts, err := cfs.findStackInstances()
if err != nil {
return err
}
for accountId, regions := range accounts {
err := cfs.deleteStackInstances(accountId, regions)
if err != nil {
return err
}
}
_, err = cfs.svc.DeleteStackSet(&cloudformation.DeleteStackSetInput{
StackSetName: cfs.stackSetSummary.StackSetName,
})
return err
}

func (cfs *CloudFormationStackSet) Properties() types.Properties {
properties := types.NewProperties()
properties.Set("Name", cfs.stackSetSummary.StackSetName)
properties.Set("StackSetId", cfs.stackSetSummary.StackSetId)

return properties
}

func (cfs *CloudFormationStackSet) String() string {
return *cfs.stackSetSummary.StackSetName
}
197 changes: 197 additions & 0 deletions resources/cloudformation-stackset_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
package resources

import (
"testing"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/cloudformation"
"github.com/golang/mock/gomock"
"github.com/rebuy-de/aws-nuke/mocks/mock_cloudformationiface"
"github.com/stretchr/testify/assert"
)

func TestCloudformationStackSet_Remove(t *testing.T) {
a := assert.New(t)
ctrl := gomock.NewController(t)
defer ctrl.Finish()

mockCloudformation := mock_cloudformationiface.NewMockCloudFormationAPI(ctrl)

stackSet := CloudFormationStackSet{
svc: mockCloudformation,
stackSetSummary: &cloudformation.StackSetSummary{
StackSetName: aws.String("foobar"),
},
}

mockCloudformation.EXPECT().ListStackInstances(gomock.Eq(&cloudformation.ListStackInstancesInput{
StackSetName: aws.String("foobar"),
})).Return(&cloudformation.ListStackInstancesOutput{
Summaries: []*cloudformation.StackInstanceSummary{
{
Account: aws.String("a1"),
Region: aws.String("r1"),
},
{
Account: aws.String("a1"),
Region: aws.String("r2"),
},
},
}, nil)

mockCloudformation.EXPECT().DeleteStackInstances(gomock.Eq(&cloudformation.DeleteStackInstancesInput{
StackSetName: aws.String("foobar"),
Accounts: []*string{aws.String("a1")},
Regions: []*string{aws.String("r1"), aws.String("r2")},
RetainStacks: aws.Bool(true),
})).Return(&cloudformation.DeleteStackInstancesOutput{
OperationId: aws.String("o1"),
}, nil)

describeStackSetStatuses := []string{
cloudformation.StackSetOperationResultStatusPending,
cloudformation.StackSetOperationResultStatusRunning,
cloudformation.StackSetOperationResultStatusSucceeded,
}
describeStackSetOperationCalls := make([]*gomock.Call, len(describeStackSetStatuses))
for i, status := range describeStackSetStatuses {
describeStackSetOperationCalls[i] = mockCloudformation.EXPECT().DescribeStackSetOperation(gomock.Eq(&cloudformation.DescribeStackSetOperationInput{
OperationId: aws.String("o1"),
StackSetName: aws.String("foobar"),
})).Return(&cloudformation.DescribeStackSetOperationOutput{
StackSetOperation: &cloudformation.StackSetOperation{
Status: aws.String(status),
},
}, nil)
}
gomock.InOrder(describeStackSetOperationCalls...)

mockCloudformation.EXPECT().DeleteStackSet(gomock.Eq(&cloudformation.DeleteStackSetInput{
StackSetName: aws.String("foobar"),
})).Return(&cloudformation.DeleteStackSetOutput{}, nil)

err := stackSet.Remove()
a.Nil(err)
}

func TestCloudformationStackSet_Remove_MultipleAccounts(t *testing.T) {
a := assert.New(t)
ctrl := gomock.NewController(t)
defer ctrl.Finish()

mockCloudformation := mock_cloudformationiface.NewMockCloudFormationAPI(ctrl)

stackSet := CloudFormationStackSet{
svc: mockCloudformation,
stackSetSummary: &cloudformation.StackSetSummary{
StackSetName: aws.String("foobar"),
},
}

mockCloudformation.EXPECT().ListStackInstances(gomock.Eq(&cloudformation.ListStackInstancesInput{
StackSetName: aws.String("foobar"),
})).Return(&cloudformation.ListStackInstancesOutput{
Summaries: []*cloudformation.StackInstanceSummary{
{
Account: aws.String("a1"),
Region: aws.String("r1"),
},
{
Account: aws.String("a1"),
Region: aws.String("r2"),
},
{
Account: aws.String("a2"),
Region: aws.String("r2"),
},
},
}, nil)

mockCloudformation.EXPECT().DeleteStackInstances(gomock.Eq(&cloudformation.DeleteStackInstancesInput{
StackSetName: aws.String("foobar"),
Accounts: []*string{aws.String("a1")},
Regions: []*string{aws.String("r1"), aws.String("r2")},
RetainStacks: aws.Bool(true),
})).Return(&cloudformation.DeleteStackInstancesOutput{
OperationId: aws.String("a1-oId"),
}, nil)
mockCloudformation.EXPECT().DeleteStackInstances(gomock.Eq(&cloudformation.DeleteStackInstancesInput{
StackSetName: aws.String("foobar"),
Accounts: []*string{aws.String("a2")},
Regions: []*string{aws.String("r2")},
RetainStacks: aws.Bool(true),
})).Return(&cloudformation.DeleteStackInstancesOutput{
OperationId: aws.String("a2-oId"),
}, nil)

mockCloudformation.EXPECT().DescribeStackSetOperation(gomock.Eq(&cloudformation.DescribeStackSetOperationInput{
OperationId: aws.String("a1-oId"),
StackSetName: aws.String("foobar"),
})).Return(&cloudformation.DescribeStackSetOperationOutput{
StackSetOperation: &cloudformation.StackSetOperation{
Status: aws.String(cloudformation.StackSetOperationResultStatusSucceeded),
},
}, nil)
mockCloudformation.EXPECT().DescribeStackSetOperation(gomock.Eq(&cloudformation.DescribeStackSetOperationInput{
OperationId: aws.String("a2-oId"),
StackSetName: aws.String("foobar"),
})).Return(&cloudformation.DescribeStackSetOperationOutput{
StackSetOperation: &cloudformation.StackSetOperation{
Status: aws.String(cloudformation.StackSetOperationResultStatusSucceeded),
},
}, nil)

mockCloudformation.EXPECT().DeleteStackSet(gomock.Eq(&cloudformation.DeleteStackSetInput{
StackSetName: aws.String("foobar"),
})).Return(&cloudformation.DeleteStackSetOutput{}, nil)

err := stackSet.Remove()
a.Nil(err)
}

func TestCloudformationStackSet_Remove_DeleteStackInstanceFailed(t *testing.T) {
a := assert.New(t)
ctrl := gomock.NewController(t)
defer ctrl.Finish()

mockCloudformation := mock_cloudformationiface.NewMockCloudFormationAPI(ctrl)

stackSet := CloudFormationStackSet{
svc: mockCloudformation,
stackSetSummary: &cloudformation.StackSetSummary{
StackSetName: aws.String("foobar"),
},
}

mockCloudformation.EXPECT().ListStackInstances(gomock.Eq(&cloudformation.ListStackInstancesInput{
StackSetName: aws.String("foobar"),
})).Return(&cloudformation.ListStackInstancesOutput{
Summaries: []*cloudformation.StackInstanceSummary{
{
Account: aws.String("a1"),
Region: aws.String("r1"),
},
},
}, nil)

mockCloudformation.EXPECT().DeleteStackInstances(gomock.Eq(&cloudformation.DeleteStackInstancesInput{
StackSetName: aws.String("foobar"),
Accounts: []*string{aws.String("a1")},
Regions: []*string{aws.String("r1")},
RetainStacks: aws.Bool(true),
})).Return(&cloudformation.DeleteStackInstancesOutput{
OperationId: aws.String("o1"),
}, nil)

mockCloudformation.EXPECT().DescribeStackSetOperation(gomock.Eq(&cloudformation.DescribeStackSetOperationInput{
OperationId: aws.String("o1"),
StackSetName: aws.String("foobar"),
})).Return(&cloudformation.DescribeStackSetOperationOutput{
StackSetOperation: &cloudformation.StackSetOperation{
Status: aws.String(cloudformation.StackSetOperationResultStatusFailed),
},
}, nil)

err := stackSet.Remove()
a.EqualError(err, "unable to delete stackSet=foobar operationId=o1 status=FAILED")
}

0 comments on commit 5d16eb6

Please sign in to comment.