-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
resource: support cloudformation stack type
This allows you to define a cloudformation stack like: - Type: "Cloudformation" Path: "examples/cloudformation/dynamo/table.yml" Name: "painter3" CloudformationParameters: - "HashKeyElementName=Painter" We use the Name and path to ensure that stacks are unique. Cloudformation support depends on change sets that we use to update a deployed stack as the cloudformation changes. The Path for cloudformation should go directly to the template you want to use. Unlike terraform and CDK where there is a directory of contents.
- Loading branch information
Showing
5 changed files
with
325 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
AWSTemplateFormatVersion: "2010-09-09" | ||
|
||
Description: 'AWS CloudFormation Sample Template DynamoDB_Table: This template demonstrates the creation of a DynamoDB table. **WARNING** This template creates an Amazon DynamoDB table. You will be billed for the AWS resources used if you create a stack from this template.' | ||
|
||
Metadata: | ||
License: Apache-2.0 | ||
|
||
Parameters: | ||
HashKeyElementName: | ||
Description: HashType PrimaryKey Name | ||
Type: String | ||
AllowedPattern: '[a-zA-Z0-9]*' | ||
MinLength: "1" | ||
MaxLength: "2048" | ||
ConstraintDescription: must contain only alphanumberic characters | ||
|
||
HashKeyElementType: | ||
Description: HashType PrimaryKey Type | ||
Type: String | ||
Default: S | ||
AllowedPattern: '[S|N]' | ||
MinLength: "1" | ||
MaxLength: "1" | ||
ConstraintDescription: must be either S or N | ||
|
||
ReadCapacityUnits: | ||
Description: Provisioned read throughput | ||
Type: Number | ||
Default: "5" | ||
MinValue: "5" | ||
MaxValue: "10000" | ||
ConstraintDescription: must be between 5 and 10000 | ||
|
||
WriteCapacityUnits: | ||
Description: Provisioned write throughput | ||
Type: Number | ||
Default: "10" | ||
MinValue: "5" | ||
MaxValue: "10000" | ||
ConstraintDescription: must be between 5 and 10000 | ||
|
||
Resources: | ||
myDynamoDBTable: | ||
Type: AWS::DynamoDB::Table | ||
Properties: | ||
AttributeDefinitions: | ||
- AttributeName: !Ref HashKeyElementName | ||
AttributeType: !Ref HashKeyElementType | ||
KeySchema: | ||
- AttributeName: !Ref HashKeyElementName | ||
KeyType: HASH | ||
ProvisionedThroughput: | ||
ReadCapacityUnits: !Ref ReadCapacityUnits | ||
WriteCapacityUnits: !Ref WriteCapacityUnits | ||
|
||
Outputs: | ||
TableName: | ||
Description: Table name of the newly created DynamoDB table | ||
Value: !Ref myDynamoDBTable | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,202 @@ | ||
package resourceoperation | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"io/ioutil" | ||
"strings" | ||
"time" | ||
|
||
"github.com/aws/aws-sdk-go/aws" | ||
"github.com/aws/aws-sdk-go/aws/credentials/stscreds" | ||
"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/samsarahq/go/oops" | ||
"github.com/santiago-labs/telophasecli/cmd/runner" | ||
"github.com/santiago-labs/telophasecli/lib/awssess" | ||
"github.com/santiago-labs/telophasecli/resource" | ||
) | ||
|
||
type cloudformationOp struct { | ||
Account *resource.Account | ||
Operation int | ||
Stack resource.Stack | ||
OutputUI runner.ConsoleUI | ||
DependentOperations []ResourceOperation | ||
CloudformationClient cloudformationiface.CloudFormationAPI | ||
} | ||
|
||
func NewCloudformationOperation(consoleUI runner.ConsoleUI, acct *resource.Account, stack resource.Stack, op int) ResourceOperation { | ||
cfg := aws.NewConfig() | ||
if stack.Region != "" { | ||
cfg.WithRegion(stack.Region) | ||
} | ||
sess := session.Must(awssess.DefaultSession(cfg)) | ||
creds := stscreds.NewCredentials(sess, *stack.RoleARN(*acct)) | ||
cloudformationClient := cloudformation.New(sess, | ||
&aws.Config{ | ||
Credentials: creds, | ||
}) | ||
|
||
return &cloudformationOp{ | ||
Account: acct, | ||
Operation: op, | ||
Stack: stack, | ||
OutputUI: consoleUI, | ||
CloudformationClient: cloudformationClient, | ||
} | ||
} | ||
|
||
func (co *cloudformationOp) AddDependent(op ResourceOperation) { | ||
co.DependentOperations = append(co.DependentOperations, op) | ||
} | ||
|
||
func (co *cloudformationOp) ListDependents() []ResourceOperation { | ||
return co.DependentOperations | ||
} | ||
|
||
func (co *cloudformationOp) Call(ctx context.Context) error { | ||
co.OutputUI.Print(fmt.Sprintf("Executing Cloudformation stack in %s", co.Stack.Path), *co.Account) | ||
|
||
cs, err := co.createChangeSet(ctx) | ||
if err != nil { | ||
return err | ||
} | ||
if aws.StringValue(cs.Status) == cloudformation.ChangeSetStatusFailed { | ||
if strings.Contains(aws.StringValue(cs.StatusReason), "The submitted information didn't contain changes") { | ||
co.OutputUI.Print(fmt.Sprintf("change set (%s) resulted in no diff, skipping", *co.Stack.ChangeSetName()), *co.Account) | ||
return nil | ||
} else { | ||
return oops.Errorf("change set failed, reason (%s)", aws.StringValue(cs.StatusReason)) | ||
} | ||
} else { | ||
co.OutputUI.Print("Created change set with changes:"+cs.String(), *co.Account) | ||
} | ||
|
||
// End call if we aren't deploying | ||
if co.Operation != Deploy { | ||
return nil | ||
} | ||
|
||
_, err = co.executeChangeSet(ctx, cs.ChangeSetId) | ||
if err != nil { | ||
return oops.Wrapf(err, "executing change set") | ||
} | ||
co.OutputUI.Print("Executed change set", *co.Account) | ||
|
||
return nil | ||
} | ||
|
||
func (co *cloudformationOp) createChangeSet(ctx context.Context) (*cloudformation.DescribeChangeSetOutput, error) { | ||
params, err := co.Stack.CloudformationParametersType() | ||
if err != nil { | ||
return nil, oops.Wrapf(err, "CloudformationParameters") | ||
} | ||
|
||
// If we can find the stack then we just update. If not then we continue on | ||
changeSetType := cloudformation.ChangeSetTypeUpdate | ||
|
||
stack, err := co.CloudformationClient.DescribeStacksWithContext(ctx, | ||
&cloudformation.DescribeStacksInput{ | ||
StackName: co.Stack.CloudformationStackName(), | ||
}) | ||
if err != nil { | ||
if strings.Contains(err.Error(), "does not exist") { | ||
changeSetType = cloudformation.ChangeSetTypeCreate | ||
// Reset err in case it is re-referenced somewhere else | ||
err = nil | ||
} else { | ||
return nil, oops.Wrapf(err, "describe stack with name: (%s)", *co.Stack.CloudformationStackName()) | ||
} | ||
} else { | ||
if len(stack.Stacks) == 1 && aws.StringValue(stack.Stacks[0].StackStatus) == cloudformation.StackStatusReviewInProgress { | ||
// If we reset to Create the change set the same change set will be reused. | ||
changeSetType = cloudformation.ChangeSetTypeCreate | ||
} | ||
} | ||
template, err := ioutil.ReadFile(co.Stack.Path) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to read stack template at path: (%s) should be a path to one file", co.Stack.Path) | ||
} | ||
|
||
strTemplate := string(template) | ||
changeSet, err := co.CloudformationClient.CreateChangeSetWithContext(ctx, | ||
&cloudformation.CreateChangeSetInput{ | ||
Parameters: params, | ||
StackName: co.Stack.CloudformationStackName(), | ||
ChangeSetName: co.Stack.ChangeSetName(), | ||
TemplateBody: &strTemplate, | ||
ChangeSetType: &changeSetType, | ||
}, | ||
) | ||
if err != nil { | ||
return nil, oops.Wrapf(err, "createChangeSet for stack: %s", co.Stack.Name) | ||
} | ||
|
||
for { | ||
cs, err := co.CloudformationClient.DescribeChangeSetWithContext(ctx, | ||
&cloudformation.DescribeChangeSetInput{ | ||
StackName: changeSet.StackId, | ||
ChangeSetName: changeSet.Id, | ||
}) | ||
if err != nil { | ||
return nil, oops.Wrapf(err, "DescribeChangeSet") | ||
} | ||
|
||
state := aws.StringValue(cs.Status) | ||
switch state { | ||
case cloudformation.ChangeSetStatusCreateInProgress: | ||
co.OutputUI.Print(fmt.Sprintf("Still creating change set for stack: %s", *co.Stack.CloudformationStackName()), *co.Account) | ||
|
||
case cloudformation.ChangeSetStatusCreateComplete: | ||
co.OutputUI.Print(fmt.Sprintf("Successfully created change set for stack: %s", *co.Stack.CloudformationStackName()), *co.Account) | ||
return cs, nil | ||
|
||
case cloudformation.ChangeSetStatusFailed: | ||
return cs, nil | ||
} | ||
|
||
time.Sleep(5 * time.Second) | ||
} | ||
} | ||
|
||
func (co *cloudformationOp) executeChangeSet(ctx context.Context, changeSetID *string) (*cloudformation.DescribeChangeSetOutput, error) { | ||
_, err := co.CloudformationClient.ExecuteChangeSetWithContext(ctx, | ||
&cloudformation.ExecuteChangeSetInput{ | ||
ChangeSetName: changeSetID, | ||
}) | ||
if err != nil { | ||
return nil, oops.Wrapf(err, "executing change set") | ||
} | ||
|
||
for { | ||
cs, err := co.CloudformationClient.DescribeChangeSetWithContext(ctx, | ||
&cloudformation.DescribeChangeSetInput{ | ||
ChangeSetName: changeSetID, | ||
}) | ||
if err != nil { | ||
return nil, oops.Wrapf(err, "DescribeChangeSet") | ||
} | ||
|
||
state := aws.StringValue(cs.ExecutionStatus) | ||
switch state { | ||
case cloudformation.ExecutionStatusExecuteInProgress: | ||
co.OutputUI.Print(fmt.Sprintf("Still executing change set for stack: (%s) for path: %s", *co.Stack.CloudformationStackName(), co.Stack.Path), *co.Account) | ||
|
||
case cloudformation.ExecutionStatusExecuteComplete: | ||
co.OutputUI.Print(fmt.Sprintf("Successfully executed change set for stack: (%s) for path: %s", *co.Stack.CloudformationStackName(), co.Stack.Path), *co.Account) | ||
return cs, nil | ||
|
||
case cloudformation.ExecutionStatusExecuteFailed: | ||
co.OutputUI.Print(fmt.Sprintf("Failed to execute change set: (%s) for path: %s", *co.Stack.CloudformationStackName(), co.Stack.Path), *co.Account) | ||
return cs, oops.Errorf("ExecuteChangeSet failed") | ||
} | ||
|
||
time.Sleep(5 * time.Second) | ||
} | ||
} | ||
|
||
func (co *cloudformationOp) ToString() string { | ||
return "" | ||
} |