Skip to content

Commit

Permalink
Allow deploy without prior stage when FORCE_DEPLOY environment …
Browse files Browse the repository at this point in the history
…is `true` (#72)

* Allow `deploy` without prior `stage`

A pattern that is used with stratus may be to `stage` in branch builds,
and `deploy` in default branch builds.

Upon disasters, like accidental stack deletions, retrying the default branch
to re-create the infrastructure, without worrying about making a temporary branch and staging,
would be a fast path to recovery.

* Require env var for this behaviour

* Be smarter with when we run through the FORCE flow
  • Loading branch information
AaronMoat authored Jul 2, 2024
1 parent dfc44ae commit eb4c699
Show file tree
Hide file tree
Showing 7 changed files with 259 additions and 30 deletions.
2 changes: 1 addition & 1 deletion internal/cli/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,6 @@ func stageAdapter(
client *stratus.Client,
stack *config.Stack,
) (err error) {
_, err = command.Stage(ctx, client, stack)
_, _, err = command.Stage(ctx, client, stack)
return
}
21 changes: 20 additions & 1 deletion internal/command/deploy.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package command

import (
"fmt"
"os"

"github.com/72636c/stratus/internal/config"
"github.com/72636c/stratus/internal/context"
"github.com/72636c/stratus/internal/stratus"
Expand All @@ -20,7 +23,23 @@ func Deploy(
return err
}

if changeSet != nil {
if changeSet == nil && os.Getenv("FORCE_DEPLOY") == "true" {
logger.Title("Could not find existing change set. FORCE_DEPLOY is true, so creating a new change set.")

_, changeSet, err = Stage(ctx, client, stack)
if err != nil {
return err
}
}

if changeSet == nil {
logger.Title("Could not find existing change set, exiting. To force a deployment with a new change set, set FORCE_DEPLOY=true and retry.")
return fmt.Errorf("could not find existing change set")
}

if stratus.IsNoopChangeSet(changeSet) {
logger.Title("No changes to execute.")
} else {
logger.Title("Execute change set")

err = client.ExecuteChangeSet(ctx, stack, *changeSet.ChangeSetName)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package command_test

import (
"os"
"testing"

"github.com/aws/aws-sdk-go/aws"
Expand Down Expand Up @@ -360,9 +361,12 @@ func Test_Deploy_Happy_NoopChangeSet(t *testing.T) {
).
Return(
&cloudformation.DescribeChangeSetOutput{
ChangeSetName: aws.String(mockChangeSetUpdateName),
Capabilities: make([]*string, 0),
Parameters: make([]*cloudformation.Parameter, 0),
ChangeSetName: aws.String(mockChangeSetUpdateName),
Capabilities: make([]*string, 0),
Parameters: make([]*cloudformation.Parameter, 0),
ExecutionStatus: aws.String(cloudformation.ExecutionStatusUnavailable),
Status: aws.String(cloudformation.ChangeSetStatusFailed),
StatusReason: aws.String("The submitted information didn't contain changes. Submit different information to create a change set."),
},
nil,
).
Expand Down Expand Up @@ -469,9 +473,12 @@ func Test_Deploy_Happy_NoopChangeSet_UploadArtefacts(t *testing.T) {
).
Return(
&cloudformation.DescribeChangeSetOutput{
ChangeSetName: aws.String(mockChangeSetUpdateName),
Capabilities: make([]*string, 0),
Parameters: make([]*cloudformation.Parameter, 0),
ChangeSetName: aws.String(mockChangeSetUpdateName),
Capabilities: make([]*string, 0),
Parameters: make([]*cloudformation.Parameter, 0),
ExecutionStatus: aws.String(cloudformation.ExecutionStatusUnavailable),
Status: aws.String(cloudformation.ChangeSetStatusFailed),
StatusReason: aws.String("The submitted information didn't contain changes. Submit different information to create a change set."),
},
nil,
).
Expand Down Expand Up @@ -513,3 +520,205 @@ func Test_Deploy_Happy_NoopChangeSet_UploadArtefacts(t *testing.T) {
err := command.Deploy(context.Background(), client, stack)
assert.NoError(err)
}

func Test_Deploy_Happy_ImplicitStage(t *testing.T) {
assert := assert.New(t)

stack := &config.Stack{
Name: mockStackName,

Capabilities: make([]string, 0),
Parameters: make(config.StackParameters, 0),
TerminationProtection: true,

Policy: []byte(mockStackPolicy),
Template: []byte(mockStackTemplate),

Checksum: mockChecksum,
}

os.Setenv("FORCE_DEPLOY", "true")

cfn := stratus.NewCloudFormationMock()
defer cfn.AssertExpectations(t)
cfn.
On(
"ListChangeSetsWithContext",
&cloudformation.ListChangeSetsInput{
StackName: aws.String(stack.Name),
},
).
Return(
&cloudformation.ListChangeSetsOutput{
Summaries: []*cloudformation.ChangeSetSummary{},
},
nil,
).Once().
On(
"ValidateTemplateWithContext",
&cloudformation.ValidateTemplateInput{
TemplateBody: aws.String(string(stack.Template)),
},
).
Return(nil, nil).
On(
"CreateChangeSetWithContext",
&cloudformation.CreateChangeSetInput{
Capabilities: make([]*string, 0),
ChangeSetName: aws.String(mockChangeSetUpdateName),
ChangeSetType: aws.String(cloudformation.ChangeSetTypeUpdate),
StackName: aws.String(stack.Name),
Parameters: make([]*cloudformation.Parameter, 0),
Tags: make([]*cloudformation.Tag, 0),
TemplateBody: aws.String(string(stack.Template)),
UsePreviousTemplate: aws.Bool(false),
},
).
Return(nil, nil).
On(
"WaitUntilChangeSetCreateCompleteWithContext",
&cloudformation.DescribeChangeSetInput{
ChangeSetName: aws.String(mockChangeSetUpdateName),
StackName: aws.String(stack.Name),
},
).
Return(nil).
On(
"DescribeChangeSetWithContext",
&cloudformation.DescribeChangeSetInput{
ChangeSetName: aws.String(mockChangeSetUpdateName),
StackName: aws.String(stack.Name),
},
).
Return(
&cloudformation.DescribeChangeSetOutput{
ChangeSetName: aws.String(mockChangeSetUpdateName),
Capabilities: make([]*string, 0),
Parameters: make([]*cloudformation.Parameter, 0),
},
nil,
).
On(
"DescribeStacksWithContext",
&cloudformation.DescribeStacksInput{
StackName: aws.String(stack.Name),
},
).
Return(
&cloudformation.DescribeStacksOutput{
Stacks: []*cloudformation.Stack{
{
EnableTerminationProtection: aws.Bool(false),
},
},
},
nil,
).
On(
"GetStackPolicyWithContext",
&cloudformation.GetStackPolicyInput{
StackName: aws.String(stack.Name),
},
).
Return(nil, nil).
On(
"DescribeStackEventsWithContext",
&cloudformation.DescribeStackEventsInput{
StackName: aws.String(stack.Name),
},
).
Return(
&cloudformation.DescribeStackEventsOutput{
StackEvents: make([]*cloudformation.StackEvent, 0),
},
nil,
).
On(
"ExecuteChangeSetWithContext",
&cloudformation.ExecuteChangeSetInput{
ChangeSetName: aws.String(mockChangeSetUpdateName),
StackName: aws.String(mockStackName),
},
).
Return(nil, nil).
On(
"DescribeStackEventsWithContext",
&cloudformation.DescribeStackEventsInput{
StackName: aws.String(stack.Name),
},
).
Return(
&cloudformation.DescribeStackEventsOutput{
StackEvents: make([]*cloudformation.StackEvent, 0),
},
nil,
).
On(
"WaitUntilStackUpdateCompleteWithContext",
&cloudformation.DescribeStacksInput{
StackName: aws.String(mockStackName),
},
).
Return(nil).
On(
"SetStackPolicyWithContext",
&cloudformation.SetStackPolicyInput{
StackName: aws.String(mockStackName),
StackPolicyBody: aws.String(mockStackPolicy),
},
).
Return(nil, nil).
On(
"UpdateTerminationProtectionWithContext",
&cloudformation.UpdateTerminationProtectionInput{
EnableTerminationProtection: aws.Bool(true),
StackName: aws.String(mockStackName),
},
).
Return(nil, nil)

client := stratus.NewClient(cfn, nil)

err := command.Deploy(context.Background(), client, stack)
assert.NoError(err)

os.Unsetenv("FORCE_DEPLOY")
}

func Test_Deploy_NoImplicitStage_Fails(t *testing.T) {
assert := assert.New(t)

stack := &config.Stack{
Name: mockStackName,

Capabilities: make([]string, 0),
Parameters: make(config.StackParameters, 0),
TerminationProtection: true,

Policy: []byte(mockStackPolicy),
Template: []byte(mockStackTemplate),

Checksum: mockChecksum,
}

cfn := stratus.NewCloudFormationMock()
defer cfn.AssertExpectations(t)
cfn.
On(
"ListChangeSetsWithContext",
&cloudformation.ListChangeSetsInput{
StackName: aws.String(stack.Name),
},
).
Return(
&cloudformation.ListChangeSetsOutput{
Summaries: []*cloudformation.ChangeSetSummary{},
},
nil,
)

client := stratus.NewClient(cfn, nil)

err := command.Deploy(context.Background(), client, stack)
assert.Error(err)
}
13 changes: 7 additions & 6 deletions internal/command/stage.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,21 @@ import (
"github.com/72636c/stratus/internal/config"
"github.com/72636c/stratus/internal/context"
"github.com/72636c/stratus/internal/stratus"
"github.com/aws/aws-sdk-go/service/cloudformation"
)

func Stage(
ctx context.Context,
client *stratus.Client,
stack *config.Stack,
) (*stratus.Diff, error) {
) (*stratus.Diff, *cloudformation.DescribeChangeSetOutput, error) {
logger := context.Logger(ctx)

logger.Title("Validate template")

validateOutput, err := client.ValidateTemplate(ctx, stack)
if err != nil {
return nil, err
return nil, nil, err
}

logger.Data(validateOutput)
Expand All @@ -27,25 +28,25 @@ func Stage(

err = client.UploadArtefacts(ctx, stack)
if err != nil {
return nil, err
return nil, nil, err
}
}

logger.Title("Create change set")

describeOutput, err := client.CreateChangeSet(ctx, stack)
if err != nil {
return nil, err
return nil, nil, err
}

logger.Title("Diff stack")

diffOutput, err := client.Diff(ctx, stack, describeOutput)
if err != nil {
return nil, err
return nil, nil, err
}

logger.Data(diffOutput)

return diffOutput, nil
return diffOutput, describeOutput, nil
}
Loading

0 comments on commit eb4c699

Please sign in to comment.