Skip to content

Commit

Permalink
Added termination protection support
Browse files Browse the repository at this point in the history
  • Loading branch information
Nathan Dines committed Apr 23, 2018
1 parent f9c96ae commit a3eb8c2
Show file tree
Hide file tree
Showing 6 changed files with 190 additions and 34 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ state of the stack deployment.
- YAML and JSON formatted stack policies
- Deploy using an assumed IAM role (often used to deploy stacks to other
accounts)
- Enable Termination Protection at deployment time

More features are currently on the roadmap, which can be [found on
Trello](https://trello.com/b/ECuGN86A)
Expand Down
8 changes: 8 additions & 0 deletions commands/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,5 +152,13 @@ func init() {
)
deployCmd.MarkFlagFilename("stack-policy-file")

deployCmd.PersistentFlags().BoolVar(
&stack.TerminationProtection,
"termination-protection",
false,
"Set termination protection for this stack",
)
deployCmd.MarkFlagFilename("stack-policy-file")

rootCmd.AddCommand(deployCmd)
}
9 changes: 9 additions & 0 deletions forgelib/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,5 +126,14 @@ func (s *Stack) Deploy() (output DeployOut, err error) {
return output, err
}
}
// Only SET termination protection, do not remove
if s.TerminationProtection != false {
cfnClient.UpdateTerminationProtection(
&cloudformation.UpdateTerminationProtectionInput{
EnableTerminationProtection: aws.Bool(s.TerminationProtection),
StackName: aws.String(s.StackID),
},
)
}
return
}
160 changes: 135 additions & 25 deletions forgelib/deploy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ type fakeStack struct {
RoleARN, StackName, StackID, StackStatus string
Tags []fakeTag
Parameters []fakeParameter
EnableTerminationProtection bool
}

type fakeTag struct {
Expand Down Expand Up @@ -60,30 +61,35 @@ func genFakeStackData(realStack cloudformation.Stack) fakeStack {
output.RoleARN = *r
}

if t := realStack.EnableTerminationProtection; t != nil {
output.EnableTerminationProtection = *t
}

return output
}

func TestDeploy(t *testing.T) {
cases := []struct {
accountID string
capabilityIam bool
cfnRoleName string
expectFailure bool
expectOutput DeployOut
expectStacks []cloudformation.Stack
expectStackPolicy string
failCreate bool
failDescribe bool
failValidate bool
newStackID string
noUpdates bool
parameterInput string
requiredParameters []string
stacks []cloudformation.Stack
stackPolicies map[string]string
stackPolicyInput string
tagInput string
thisStack Stack
accountID string
capabilityIam bool
cfnRoleName string
expectFailure bool
expectOutput DeployOut
expectStacks []cloudformation.Stack
expectStackPolicy string
failCreate bool
failDescribe bool
failValidate bool
newStackID string
noUpdates bool
parameterInput string
requiredParameters []string
stacks []cloudformation.Stack
stackPolicies map[string]string
stackPolicyInput string
tagInput string
terminationProtection bool
thisStack Stack
}{
// Create new stack with previously used name
{
Expand Down Expand Up @@ -659,6 +665,109 @@ func TestDeploy(t *testing.T) {
},
expectStackPolicy: `{"Statement":[{"Action":"Update:*","Effect":"Allow","NotResource":"LogicalResourceId/ProductionDatabase","Principal":"*"}]}`,
},
// Create new stack with termination protection
{
terminationProtection: true,
newStackID: "test-stack/id0",
stacks: []cloudformation.Stack{},
expectStacks: []cloudformation.Stack{
{
StackName: aws.String("test-stack"),
StackId: aws.String("test-stack/id0"),
StackStatus: aws.String(cloudformation.StackStatusCreateComplete),
EnableTerminationProtection: aws.Bool(true),
},
},
},
// Update stack and turn on termination protection
{
terminationProtection: true,
stacks: []cloudformation.Stack{
{
StackName: aws.String("test-stack"),
StackId: aws.String("test-stack/id0"),
StackStatus: aws.String(cloudformation.StackStatusDeleteComplete),
},
{
StackName: aws.String("test-stack"),
StackId: aws.String("test-stack/id1"),
StackStatus: aws.String(cloudformation.StackStatusCreateComplete),
EnableTerminationProtection: aws.Bool(false),
},
},
expectStacks: []cloudformation.Stack{
{
StackName: aws.String("test-stack"),
StackId: aws.String("test-stack/id0"),
StackStatus: aws.String(cloudformation.StackStatusDeleteComplete),
},
{
StackName: aws.String("test-stack"),
StackId: aws.String("test-stack/id1"),
StackStatus: aws.String(cloudformation.StackStatusUpdateComplete),
EnableTerminationProtection: aws.Bool(true),
},
},
},
// Update stack and leave termination protection alone
{
stacks: []cloudformation.Stack{
{
StackName: aws.String("test-stack"),
StackId: aws.String("test-stack/id0"),
StackStatus: aws.String(cloudformation.StackStatusDeleteComplete),
},
{
StackName: aws.String("test-stack"),
StackId: aws.String("test-stack/id1"),
StackStatus: aws.String(cloudformation.StackStatusCreateComplete),
EnableTerminationProtection: aws.Bool(true),
},
},
expectStacks: []cloudformation.Stack{
{
StackName: aws.String("test-stack"),
StackId: aws.String("test-stack/id0"),
StackStatus: aws.String(cloudformation.StackStatusDeleteComplete),
},
{
StackName: aws.String("test-stack"),
StackId: aws.String("test-stack/id1"),
StackStatus: aws.String(cloudformation.StackStatusUpdateComplete),
EnableTerminationProtection: aws.Bool(true),
},
},
},
// Update stack and leave termination protection alone (v. 2)
{
terminationProtection: false,
stacks: []cloudformation.Stack{
{
StackName: aws.String("test-stack"),
StackId: aws.String("test-stack/id0"),
StackStatus: aws.String(cloudformation.StackStatusDeleteComplete),
},
{
StackName: aws.String("test-stack"),
StackId: aws.String("test-stack/id1"),
StackStatus: aws.String(cloudformation.StackStatusCreateComplete),
EnableTerminationProtection: aws.Bool(true),
},
},
expectStacks: []cloudformation.Stack{
{
StackName: aws.String("test-stack"),
StackId: aws.String("test-stack/id0"),
StackStatus: aws.String(cloudformation.StackStatusDeleteComplete),
},
{
StackName: aws.String("test-stack"),
StackId: aws.String("test-stack/id1"),
StackStatus: aws.String(cloudformation.StackStatusUpdateComplete),
EnableTerminationProtection: aws.Bool(true),
},
},
},
}

oldCFNClient := cfnClient
Expand Down Expand Up @@ -687,12 +796,13 @@ func TestDeploy(t *testing.T) {
thisStack := c.thisStack
if thisStack == (Stack{}) {
thisStack = Stack{
ParametersBody: c.parameterInput,
StackName: "test-stack",
TagsBody: c.tagInput,
TemplateBody: `{"Resources":{"SNS":{"Type":"AWS::SNS::Topic"}}}`,
CfnRoleName: c.cfnRoleName,
StackPolicyBody: c.stackPolicyInput,
ParametersBody: c.parameterInput,
StackName: "test-stack",
TagsBody: c.tagInput,
TemplateBody: `{"Resources":{"SNS":{"Type":"AWS::SNS::Topic"}}}`,
CfnRoleName: c.cfnRoleName,
StackPolicyBody: c.stackPolicyInput,
TerminationProtection: c.terminationProtection,
}
}

Expand Down
19 changes: 10 additions & 9 deletions forgelib/forge.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,16 @@ import "github.com/aws/aws-sdk-go/service/cloudformation"
// Stack represents the attributes of a stack deployment, including the AWS
// parameters, and local resources which represent what needs to be deployed
type Stack struct {
ParametersBody string
ProjectManifest string
CfnRoleName string
StackID string
StackInfo *cloudformation.Stack
StackName string
StackPolicyBody string
TagsBody string
TemplateBody string
ParametersBody string
ProjectManifest string
CfnRoleName string
StackID string
StackInfo *cloudformation.Stack
StackName string
StackPolicyBody string
TagsBody string
TemplateBody string
TerminationProtection bool
}

// GetStackInfo populates the StackInfo for this object from the existing stack
Expand Down
27 changes: 27 additions & 0 deletions forgelib/mock_cfn_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -250,3 +250,30 @@ func (m mockCfn) DescribeStackEventsPages(input *cloudformation.DescribeStackEve
}
return nil
}

func (m mockCfn) UpdateTerminationProtection(input *cloudformation.UpdateTerminationProtectionInput) (*cloudformation.UpdateTerminationProtectionOutput, error) {
output := cloudformation.UpdateTerminationProtectionOutput{}
// For each existing stack, match against the stack ID first, then the stack
// name. If found and the stack is in a good state, set values against it
// and return
for i := 0; i < len(*m.stacks); i++ {
if s := *input.StackName; s == *(*m.stacks)[i].StackId ||
s == *(*m.stacks)[i].StackName {
switch *(*m.stacks)[i].StackStatus {
case cloudformation.StackStatusCreateComplete,
cloudformation.StackStatusUpdateComplete,
cloudformation.StackStatusUpdateRollbackComplete:

(*m.stacks)[i].EnableTerminationProtection = input.EnableTerminationProtection

output.StackId = (*m.stacks)[i].StackId
return &output, nil
}
}
}
return &output, awserr.New(
"ValidationError",
fmt.Sprintf("Stack with id %s does not exist", *input.StackName),
nil,
)
}

0 comments on commit a3eb8c2

Please sign in to comment.