provider/aws: Add implementation for aws_cloudformation_stack

This commit is contained in:
Radek Simko 2015-07-07 08:00:05 +01:00
parent 61948f35d2
commit 4dfbbe3074
4 changed files with 512 additions and 0 deletions

View File

@ -11,6 +11,7 @@ import (
"github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/service/autoscaling" "github.com/aws/aws-sdk-go/service/autoscaling"
"github.com/aws/aws-sdk-go/service/cloudformation"
"github.com/aws/aws-sdk-go/service/cloudwatch" "github.com/aws/aws-sdk-go/service/cloudwatch"
"github.com/aws/aws-sdk-go/service/cloudwatchlogs" "github.com/aws/aws-sdk-go/service/cloudwatchlogs"
"github.com/aws/aws-sdk-go/service/directoryservice" "github.com/aws/aws-sdk-go/service/directoryservice"
@ -47,6 +48,7 @@ type Config struct {
} }
type AWSClient struct { type AWSClient struct {
cfconn *cloudformation.CloudFormation
cloudwatchconn *cloudwatch.CloudWatch cloudwatchconn *cloudwatch.CloudWatch
cloudwatchlogsconn *cloudwatchlogs.CloudWatchLogs cloudwatchlogsconn *cloudwatchlogs.CloudWatchLogs
dsconn *directoryservice.DirectoryService dsconn *directoryservice.DirectoryService
@ -175,6 +177,9 @@ func (c *Config) Client() (interface{}, error) {
log.Println("[INFO] Initializing Lambda Connection") log.Println("[INFO] Initializing Lambda Connection")
client.lambdaconn = lambda.New(awsConfig) client.lambdaconn = lambda.New(awsConfig)
log.Println("[INFO] Initializing Cloudformation Connection")
client.cfconn = cloudformation.New(awsConfig)
log.Println("[INFO] Initializing CloudWatch SDK connection") log.Println("[INFO] Initializing CloudWatch SDK connection")
client.cloudwatchconn = cloudwatch.New(awsConfig) client.cloudwatchconn = cloudwatch.New(awsConfig)

View File

@ -163,6 +163,7 @@ func Provider() terraform.ResourceProvider {
"aws_autoscaling_group": resourceAwsAutoscalingGroup(), "aws_autoscaling_group": resourceAwsAutoscalingGroup(),
"aws_autoscaling_notification": resourceAwsAutoscalingNotification(), "aws_autoscaling_notification": resourceAwsAutoscalingNotification(),
"aws_autoscaling_policy": resourceAwsAutoscalingPolicy(), "aws_autoscaling_policy": resourceAwsAutoscalingPolicy(),
"aws_cloudformation_stack": resourceAwsCloudFormationStack(),
"aws_cloudwatch_log_group": resourceAwsCloudWatchLogGroup(), "aws_cloudwatch_log_group": resourceAwsCloudWatchLogGroup(),
"aws_autoscaling_lifecycle_hook": resourceAwsAutoscalingLifecycleHook(), "aws_autoscaling_lifecycle_hook": resourceAwsAutoscalingLifecycleHook(),
"aws_cloudwatch_metric_alarm": resourceAwsCloudWatchMetricAlarm(), "aws_cloudwatch_metric_alarm": resourceAwsCloudWatchMetricAlarm(),

View File

@ -0,0 +1,451 @@
package aws
import (
"fmt"
"log"
"regexp"
"time"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/helper/schema"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/cloudformation"
)
func resourceAwsCloudFormationStack() *schema.Resource {
return &schema.Resource{
Create: resourceAwsCloudFormationStackCreate,
Read: resourceAwsCloudFormationStackRead,
Update: resourceAwsCloudFormationStackUpdate,
Delete: resourceAwsCloudFormationStackDelete,
Schema: map[string]*schema.Schema{
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"template_body": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
StateFunc: normalizeJson,
},
"template_url": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"capabilities": &schema.Schema{
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
Set: schema.HashString,
},
"disable_rollback": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
ForceNew: true,
},
"notification_arns": &schema.Schema{
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
Set: schema.HashString,
},
"on_failure": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"parameters": &schema.Schema{
Type: schema.TypeMap,
Optional: true,
Computed: true,
},
"outputs": &schema.Schema{
Type: schema.TypeMap,
Computed: true,
},
"policy_body": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
StateFunc: normalizeJson,
},
"policy_url": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"timeout_in_minutes": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
ForceNew: true,
},
"tags": &schema.Schema{
Type: schema.TypeMap,
Optional: true,
ForceNew: true,
},
},
}
}
func resourceAwsCloudFormationStackCreate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).cfconn
input := cloudformation.CreateStackInput{
StackName: aws.String(d.Get("name").(string)),
}
if v, ok := d.GetOk("template_body"); ok {
input.TemplateBody = aws.String(normalizeJson(v.(string)))
}
if v, ok := d.GetOk("template_url"); ok {
input.TemplateURL = aws.String(v.(string))
}
if v, ok := d.GetOk("capabilities"); ok {
input.Capabilities = expandStringList(v.(*schema.Set).List())
}
if v, ok := d.GetOk("disable_rollback"); ok {
input.DisableRollback = aws.Bool(v.(bool))
}
if v, ok := d.GetOk("notification_arns"); ok {
input.NotificationARNs = expandStringList(v.(*schema.Set).List())
}
if v, ok := d.GetOk("on_failure"); ok {
input.OnFailure = aws.String(v.(string))
}
if v, ok := d.GetOk("parameters"); ok {
input.Parameters = expandCloudFormationParameters(v.(map[string]interface{}))
}
if v, ok := d.GetOk("policy_body"); ok {
input.StackPolicyBody = aws.String(normalizeJson(v.(string)))
}
if v, ok := d.GetOk("policy_url"); ok {
input.StackPolicyURL = aws.String(v.(string))
}
if v, ok := d.GetOk("tags"); ok {
input.Tags = expandCloudFormationTags(v.(map[string]interface{}))
}
if v, ok := d.GetOk("timeout_in_minutes"); ok {
input.TimeoutInMinutes = aws.Int64(int64(v.(int)))
}
log.Printf("[DEBUG] Creating CloudFormation Stack: %s", input)
resp, err := conn.CreateStack(&input)
if err != nil {
return fmt.Errorf("Creating CloudFormation stack failed: %s", err.Error())
}
d.SetId(*resp.StackId)
wait := resource.StateChangeConf{
Pending: []string{"CREATE_IN_PROGRESS", "ROLLBACK_IN_PROGRESS", "ROLLBACK_COMPLETE"},
Target: "CREATE_COMPLETE",
Timeout: 30 * time.Minute,
MinTimeout: 5 * time.Second,
Refresh: func() (interface{}, string, error) {
resp, err := conn.DescribeStacks(&cloudformation.DescribeStacksInput{
StackName: aws.String(d.Get("name").(string)),
})
status := *resp.Stacks[0].StackStatus
log.Printf("[DEBUG] Current CloudFormation stack status: %q", status)
if status == "ROLLBACK_COMPLETE" {
stack := resp.Stacks[0]
failures, err := getCloudFormationFailures(stack.StackName, *stack.CreationTime, conn)
if err != nil {
return resp, "", fmt.Errorf(
"Failed getting details about rollback: %q", err.Error())
}
return resp, "", fmt.Errorf("ROLLBACK_COMPLETE:\n%q", failures)
}
return resp, status, err
},
}
_, err = wait.WaitForState()
if err != nil {
return err
}
log.Printf("[INFO] CloudFormation Stack %q created", d.Get("name").(string))
return resourceAwsCloudFormationStackRead(d, meta)
}
func resourceAwsCloudFormationStackRead(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).cfconn
stackName := d.Get("name").(string)
input := &cloudformation.DescribeStacksInput{
StackName: aws.String(stackName),
}
resp, err := conn.DescribeStacks(input)
if err != nil {
return err
}
stacks := resp.Stacks
if len(stacks) < 1 {
return nil
}
tInput := cloudformation.GetTemplateInput{
StackName: aws.String(stackName),
}
out, err := conn.GetTemplate(&tInput)
if err != nil {
return err
}
d.Set("template_body", normalizeJson(*out.TemplateBody))
stack := stacks[0]
log.Printf("[DEBUG] Received CloudFormation stack: %s", stack)
d.Set("name", stack.StackName)
d.Set("arn", stack.StackId)
if stack.TimeoutInMinutes != nil {
d.Set("timeout_in_minutes", int(*stack.TimeoutInMinutes))
}
if stack.Description != nil {
d.Set("description", stack.Description)
}
if stack.DisableRollback != nil {
d.Set("disable_rollback", stack.DisableRollback)
}
if len(stack.NotificationARNs) > 0 {
err = d.Set("notification_arns", schema.NewSet(schema.HashString, flattenStringList(stack.NotificationARNs)))
if err != nil {
return err
}
}
originalParams := d.Get("parameters").(map[string]interface{})
err = d.Set("parameters", flattenCloudFormationParameters(stack.Parameters, originalParams))
if err != nil {
return err
}
err = d.Set("tags", flattenCloudFormationTags(stack.Tags))
if err != nil {
return err
}
err = d.Set("outputs", flattenCloudFormationOutputs(stack.Outputs))
if err != nil {
return err
}
if len(stack.Capabilities) > 0 {
err = d.Set("capabilities", schema.NewSet(schema.HashString, flattenStringList(stack.Capabilities)))
if err != nil {
return err
}
}
return nil
}
func resourceAwsCloudFormationStackUpdate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).cfconn
input := &cloudformation.UpdateStackInput{
StackName: aws.String(d.Get("name").(string)),
}
if d.HasChange("template_body") {
input.TemplateBody = aws.String(normalizeJson(d.Get("template_body").(string)))
}
if d.HasChange("template_url") {
input.TemplateURL = aws.String(d.Get("template_url").(string))
}
if d.HasChange("capabilities") {
input.Capabilities = expandStringList(d.Get("capabilities").(*schema.Set).List())
}
if d.HasChange("notification_arns") {
input.NotificationARNs = expandStringList(d.Get("notification_arns").(*schema.Set).List())
}
if d.HasChange("parameters") {
input.Parameters = expandCloudFormationParameters(d.Get("parameters").(map[string]interface{}))
}
if d.HasChange("policy_body") {
input.StackPolicyBody = aws.String(normalizeJson(d.Get("policy_body").(string)))
}
if d.HasChange("policy_url") {
input.StackPolicyURL = aws.String(d.Get("policy_url").(string))
}
log.Printf("[DEBUG] Updating CloudFormation stack: %s", input)
stack, err := conn.UpdateStack(input)
if err != nil {
return err
}
lastUpdatedTime, err := getLastCfEventTimestamp(d.Get("name").(string), conn)
if err != nil {
return err
}
wait := resource.StateChangeConf{
Pending: []string{
"UPDATE_COMPLETE_CLEANUP_IN_PROGRESS",
"UPDATE_IN_PROGRESS",
"UPDATE_ROLLBACK_IN_PROGRESS",
"UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS",
"UPDATE_ROLLBACK_COMPLETE",
},
Target: "UPDATE_COMPLETE",
Timeout: 15 * time.Minute,
MinTimeout: 5 * time.Second,
Refresh: func() (interface{}, string, error) {
resp, err := conn.DescribeStacks(&cloudformation.DescribeStacksInput{
StackName: aws.String(d.Get("name").(string)),
})
stack := resp.Stacks[0]
status := *stack.StackStatus
log.Printf("[DEBUG] Current CloudFormation stack status: %q", status)
if status == "UPDATE_ROLLBACK_COMPLETE" {
failures, err := getCloudFormationFailures(stack.StackName, *lastUpdatedTime, conn)
if err != nil {
return resp, "", fmt.Errorf(
"Failed getting details about rollback: %q", err.Error())
}
return resp, "", fmt.Errorf(
"UPDATE_ROLLBACK_COMPLETE:\n%q", failures)
}
return resp, status, err
},
}
_, err = wait.WaitForState()
if err != nil {
return err
}
log.Printf("[DEBUG] CloudFormation stack %q has been updated", *stack.StackId)
return resourceAwsCloudFormationStackRead(d, meta)
}
func resourceAwsCloudFormationStackDelete(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).cfconn
input := &cloudformation.DeleteStackInput{
StackName: aws.String(d.Get("name").(string)),
}
log.Printf("[DEBUG] Deleting CloudFormation stack %s", input)
_, err := conn.DeleteStack(input)
if err != nil {
awsErr, ok := err.(awserr.Error)
if !ok {
return err
}
if awsErr.Code() == "ValidationError" {
// Ignore stack which has been already deleted
return nil
}
return err
}
wait := resource.StateChangeConf{
Pending: []string{"DELETE_IN_PROGRESS", "ROLLBACK_IN_PROGRESS"},
Target: "DELETE_COMPLETE",
Timeout: 30 * time.Minute,
MinTimeout: 5 * time.Second,
Refresh: func() (interface{}, string, error) {
resp, err := conn.DescribeStacks(&cloudformation.DescribeStacksInput{
StackName: aws.String(d.Get("name").(string)),
})
if err != nil {
awsErr, ok := err.(awserr.Error)
if !ok {
return resp, "DELETE_FAILED", err
}
log.Printf("[DEBUG] Error when deleting CloudFormation stack: %s: %s",
awsErr.Code(), awsErr.Message())
if awsErr.Code() == "ValidationError" {
return resp, "DELETE_COMPLETE", nil
}
}
if len(resp.Stacks) == 0 {
log.Printf("[DEBUG] CloudFormation stack %q is already gone", d.Get("name"))
return resp, "DELETE_COMPLETE", nil
}
status := *resp.Stacks[0].StackStatus
log.Printf("[DEBUG] Current CloudFormation stack status: %q", status)
return resp, status, err
},
}
_, err = wait.WaitForState()
if err != nil {
return err
}
log.Printf("[DEBUG] CloudFormation stack %q has been deleted", d.Id())
d.SetId("")
return nil
}
// getLastCfEventTimestamp takes the first event in a list
// of events ordered from the newest to the oldest
// and extracts timestamp from it
// LastUpdatedTime only provides last >successful< updated time
func getLastCfEventTimestamp(stackName string, conn *cloudformation.CloudFormation) (
*time.Time, error) {
output, err := conn.DescribeStackEvents(&cloudformation.DescribeStackEventsInput{
StackName: aws.String(stackName),
})
if err != nil {
return nil, err
}
return output.StackEvents[0].Timestamp, nil
}
// getCloudFormationFailures returns ResourceStatusReason(s)
// of events that should be failures based on regexp match of status
func getCloudFormationFailures(stackName *string, afterTime time.Time,
conn *cloudformation.CloudFormation) ([]string, error) {
var failures []string
// Only catching failures from last 100 events
// Some extra iteration logic via NextToken could be added
// but in reality it's nearly impossible to generate >100
// events by a single stack update
events, err := conn.DescribeStackEvents(&cloudformation.DescribeStackEventsInput{
StackName: stackName,
})
if err != nil {
return nil, err
}
failRe := regexp.MustCompile("_FAILED$")
rollbackRe := regexp.MustCompile("^ROLLBACK_")
for _, e := range events.StackEvents {
if (failRe.MatchString(*e.ResourceStatus) || rollbackRe.MatchString(*e.ResourceStatus)) &&
e.Timestamp.After(afterTime) && e.ResourceStatusReason != nil {
failures = append(failures, *e.ResourceStatusReason)
}
}
return failures, nil
}

View File

@ -9,6 +9,7 @@ import (
"strings" "strings"
"github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/cloudformation"
"github.com/aws/aws-sdk-go/service/directoryservice" "github.com/aws/aws-sdk-go/service/directoryservice"
"github.com/aws/aws-sdk-go/service/ec2" "github.com/aws/aws-sdk-go/service/ec2"
"github.com/aws/aws-sdk-go/service/ecs" "github.com/aws/aws-sdk-go/service/ecs"
@ -601,3 +602,57 @@ func flattenDSVpcSettings(
return []map[string]interface{}{settings} return []map[string]interface{}{settings}
} }
func expandCloudFormationParameters(params map[string]interface{}) []*cloudformation.Parameter {
var cfParams []*cloudformation.Parameter
for k, v := range params {
cfParams = append(cfParams, &cloudformation.Parameter{
ParameterKey: aws.String(k),
ParameterValue: aws.String(v.(string)),
})
}
return cfParams
}
// flattenCloudFormationParameters is flattening list of
// *cloudformation.Parameters and only returning existing
// parameters to avoid clash with default values
func flattenCloudFormationParameters(cfParams []*cloudformation.Parameter,
originalParams map[string]interface{}) map[string]interface{} {
params := make(map[string]interface{}, len(cfParams))
for _, p := range cfParams {
_, isConfigured := originalParams[*p.ParameterKey]
if isConfigured {
params[*p.ParameterKey] = *p.ParameterValue
}
}
return params
}
func expandCloudFormationTags(tags map[string]interface{}) []*cloudformation.Tag {
var cfTags []*cloudformation.Tag
for k, v := range tags {
cfTags = append(cfTags, &cloudformation.Tag{
Key: aws.String(k),
Value: aws.String(v.(string)),
})
}
return cfTags
}
func flattenCloudFormationTags(cfTags []*cloudformation.Tag) map[string]string {
tags := make(map[string]string, len(cfTags))
for _, t := range cfTags {
tags[*t.Key] = *t.Value
}
return tags
}
func flattenCloudFormationOutputs(cfOutputs []*cloudformation.Output) map[string]string {
outputs := make(map[string]string, len(cfOutputs))
for _, o := range cfOutputs {
outputs[*o.OutputKey] = *o.OutputValue
}
return outputs
}