Merge pull request #2636 from TimeIncOSS/f-aws-cloudformation
provider/aws: Add aws_cloudformation_stack
This commit is contained in:
commit
0d8d6fde79
|
@ -12,6 +12,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/codedeploy"
|
"github.com/aws/aws-sdk-go/service/codedeploy"
|
||||||
|
@ -49,6 +50,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
|
||||||
|
@ -180,6 +182,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)
|
||||||
|
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -0,0 +1,228 @@
|
||||||
|
package aws
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/aws/aws-sdk-go/aws"
|
||||||
|
"github.com/aws/aws-sdk-go/service/cloudformation"
|
||||||
|
"github.com/hashicorp/terraform/helper/resource"
|
||||||
|
"github.com/hashicorp/terraform/terraform"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAccAWSCloudFormation_basic(t *testing.T) {
|
||||||
|
var stack cloudformation.Stack
|
||||||
|
|
||||||
|
resource.Test(t, resource.TestCase{
|
||||||
|
PreCheck: func() { testAccPreCheck(t) },
|
||||||
|
Providers: testAccProviders,
|
||||||
|
CheckDestroy: testAccCheckAWSCloudFormationDestroy,
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
resource.TestStep{
|
||||||
|
Config: testAccAWSCloudFormationConfig,
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
testAccCheckCloudFormationStackExists("aws_cloudformation_stack.network", &stack),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccAWSCloudFormation_defaultParams(t *testing.T) {
|
||||||
|
var stack cloudformation.Stack
|
||||||
|
|
||||||
|
resource.Test(t, resource.TestCase{
|
||||||
|
PreCheck: func() { testAccPreCheck(t) },
|
||||||
|
Providers: testAccProviders,
|
||||||
|
CheckDestroy: testAccCheckAWSCloudFormationDestroy,
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
resource.TestStep{
|
||||||
|
Config: testAccAWSCloudFormationConfig_defaultParams,
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
testAccCheckCloudFormationStackExists("aws_cloudformation_stack.asg-demo", &stack),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccAWSCloudFormation_allAttributes(t *testing.T) {
|
||||||
|
var stack cloudformation.Stack
|
||||||
|
|
||||||
|
resource.Test(t, resource.TestCase{
|
||||||
|
PreCheck: func() { testAccPreCheck(t) },
|
||||||
|
Providers: testAccProviders,
|
||||||
|
CheckDestroy: testAccCheckAWSCloudFormationDestroy,
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
resource.TestStep{
|
||||||
|
Config: testAccAWSCloudFormationConfig_allAttributes,
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
testAccCheckCloudFormationStackExists("aws_cloudformation_stack.full", &stack),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccCheckCloudFormationStackExists(n string, stack *cloudformation.Stack) resource.TestCheckFunc {
|
||||||
|
return func(s *terraform.State) error {
|
||||||
|
rs, ok := s.RootModule().Resources[n]
|
||||||
|
if !ok {
|
||||||
|
rs = rs
|
||||||
|
return fmt.Errorf("Not found: %s", n)
|
||||||
|
}
|
||||||
|
|
||||||
|
conn := testAccProvider.Meta().(*AWSClient).cfconn
|
||||||
|
params := &cloudformation.DescribeStacksInput{
|
||||||
|
StackName: aws.String(rs.Primary.ID),
|
||||||
|
}
|
||||||
|
resp, err := conn.DescribeStacks(params)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(resp.Stacks) == 0 {
|
||||||
|
return fmt.Errorf("CloudFormation stack not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccCheckAWSCloudFormationDestroy(s *terraform.State) error {
|
||||||
|
conn := testAccProvider.Meta().(*AWSClient).cfconn
|
||||||
|
|
||||||
|
for _, rs := range s.RootModule().Resources {
|
||||||
|
if rs.Type != "aws_cloudformation_stack" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
params := cloudformation.DescribeStacksInput{
|
||||||
|
StackName: aws.String(rs.Primary.ID),
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := conn.DescribeStacks(¶ms)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
if len(resp.Stacks) != 0 &&
|
||||||
|
*resp.Stacks[0].StackId == rs.Primary.ID {
|
||||||
|
return fmt.Errorf("CloudFormation stack still exists: %q", rs.Primary.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var testAccAWSCloudFormationConfig = `
|
||||||
|
resource "aws_cloudformation_stack" "network" {
|
||||||
|
name = "tf-networking-stack"
|
||||||
|
template_body = <<STACK
|
||||||
|
{
|
||||||
|
"Resources" : {
|
||||||
|
"MyVPC": {
|
||||||
|
"Type" : "AWS::EC2::VPC",
|
||||||
|
"Properties" : {
|
||||||
|
"CidrBlock" : "10.0.0.0/16",
|
||||||
|
"Tags" : [
|
||||||
|
{"Key": "Name", "Value": "Primary_CF_VPC"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Outputs" : {
|
||||||
|
"DefaultSgId" : {
|
||||||
|
"Description": "The ID of default security group",
|
||||||
|
"Value" : { "Fn::GetAtt" : [ "MyVPC", "DefaultSecurityGroup" ]}
|
||||||
|
},
|
||||||
|
"VpcID" : {
|
||||||
|
"Description": "The VPC ID",
|
||||||
|
"Value" : { "Ref" : "MyVPC" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
STACK
|
||||||
|
}`
|
||||||
|
|
||||||
|
var testAccAWSCloudFormationConfig_defaultParams = `
|
||||||
|
resource "aws_cloudformation_stack" "asg-demo" {
|
||||||
|
name = "tf-asg-demo-stack"
|
||||||
|
template_body = <<BODY
|
||||||
|
{
|
||||||
|
"Parameters": {
|
||||||
|
"TopicName": {
|
||||||
|
"Type": "String"
|
||||||
|
},
|
||||||
|
"VPCCIDR": {
|
||||||
|
"Type": "String",
|
||||||
|
"Default": "10.10.0.0/16"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Resources": {
|
||||||
|
"NotificationTopic": {
|
||||||
|
"Type": "AWS::SNS::Topic",
|
||||||
|
"Properties": {
|
||||||
|
"TopicName": {
|
||||||
|
"Ref": "TopicName"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"MyVPC": {
|
||||||
|
"Type": "AWS::EC2::VPC",
|
||||||
|
"Properties": {
|
||||||
|
"CidrBlock": {
|
||||||
|
"Ref": "VPCCIDR"
|
||||||
|
},
|
||||||
|
"Tags": [
|
||||||
|
{
|
||||||
|
"Key": "Name",
|
||||||
|
"Value": "Primary_CF_VPC"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Outputs": {
|
||||||
|
"VPCCIDR": {
|
||||||
|
"Value": {
|
||||||
|
"Ref": "VPCCIDR"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BODY
|
||||||
|
|
||||||
|
parameters {
|
||||||
|
TopicName = "ExampleTopic"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
var testAccAWSCloudFormationConfig_allAttributes = `
|
||||||
|
resource "aws_cloudformation_stack" "full" {
|
||||||
|
name = "tf-full-stack"
|
||||||
|
template_body = <<STACK
|
||||||
|
{
|
||||||
|
"Resources" : {
|
||||||
|
"MyVPC": {
|
||||||
|
"Type" : "AWS::EC2::VPC",
|
||||||
|
"Properties" : {
|
||||||
|
"CidrBlock" : "10.0.0.0/16",
|
||||||
|
"Tags" : [
|
||||||
|
{"Key": "Name", "Value": "Primary_CF_VPC"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
STACK
|
||||||
|
|
||||||
|
capabilities = ["CAPABILITY_IAM"]
|
||||||
|
notification_arns = ["${aws_sns_topic.cf-updates.arn}"]
|
||||||
|
on_failure = "DELETE"
|
||||||
|
timeout_in_minutes = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_sns_topic" "cf-updates" {
|
||||||
|
name = "tf-cf-notifications"
|
||||||
|
}
|
||||||
|
`
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
---
|
||||||
|
layout: "aws"
|
||||||
|
page_title: "AWS: aws_cloudformation_stack"
|
||||||
|
sidebar_current: "docs-aws-resource-cloudformation-stack"
|
||||||
|
description: |-
|
||||||
|
Provides a CloudFormation Stack resource.
|
||||||
|
---
|
||||||
|
|
||||||
|
# aws\_cloudformation\_stack
|
||||||
|
|
||||||
|
Provides a CloudFormation Stack resource.
|
||||||
|
|
||||||
|
## Example Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
resource "aws_cloudformation_stack" "network" {
|
||||||
|
name = "networking-stack"
|
||||||
|
template_body = <<STACK
|
||||||
|
{
|
||||||
|
"Resources" : {
|
||||||
|
"my-vpc": {
|
||||||
|
"Type" : "AWS::EC2::VPC",
|
||||||
|
"Properties" : {
|
||||||
|
"CidrBlock" : "10.0.0.0/16",
|
||||||
|
"Tags" : [
|
||||||
|
{"Key": "Name", "Value": "Primary_CF_VPC"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
STACK
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Argument Reference
|
||||||
|
|
||||||
|
The following arguments are supported:
|
||||||
|
|
||||||
|
* `name` - (Required) Stack name.
|
||||||
|
* `template_body` - (Optional) Structure containing the template body (max size: 51,200 bytes).
|
||||||
|
* `template_url` - (Optional) Location of a file containing the template body (max size: 460,800 bytes).
|
||||||
|
* `capabilities` - (Optional) A list of capabilities.
|
||||||
|
Currently, the only valid value is `CAPABILITY_IAM`
|
||||||
|
* `disable_rollback` - (Optional) Set to true to disable rollback of the stack if stack creation failed.
|
||||||
|
Conflicts with `on_failure`.
|
||||||
|
* `notification_arns` - (Optional) A list of SNS topic ARNs to publish stack related events.
|
||||||
|
* `on_failure` - (Optional) Action to be taken if stack creation fails. This must be
|
||||||
|
one of: `DO_NOTHING`, `ROLLBACK`, or `DELETE`. Conflicts with `disable_rollback`.
|
||||||
|
* `parameters` - (Optional) A list of Parameter structures that specify input parameters for the stack.
|
||||||
|
* `policy_body` - (Optional) Structure containing the stack policy body.
|
||||||
|
Conflicts w/ `policy_url`.
|
||||||
|
* `policy_url` - (Optional) Location of a file containing the stack policy.
|
||||||
|
Conflicts w/ `policy_body`.
|
||||||
|
* `tags` - (Optional) A list of tags to associate with this stack.
|
||||||
|
* `timeout_in_minutes` - (Optional) The amount of time that can pass before the stack status becomes `CREATE_FAILED`.
|
||||||
|
|
||||||
|
## Attributes Reference
|
||||||
|
|
||||||
|
The following attributes are exported:
|
||||||
|
|
||||||
|
* `arn` - A unique identifier of the stack.
|
||||||
|
* `outputs` - A list of output structures.
|
|
@ -10,6 +10,14 @@
|
||||||
<a href="/docs/providers/aws/index.html">AWS Provider</a>
|
<a href="/docs/providers/aws/index.html">AWS Provider</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<li<%= sidebar_current(/^docs-aws-resource-cloudformation/) %>>
|
||||||
|
<a href="#">CloudFormation Resources</a>
|
||||||
|
<ul class="nav nav-visible">
|
||||||
|
<li<%= sidebar_current("docs-aws-resource-cloudformation-stack") %>>
|
||||||
|
<a href="/docs/providers/aws/r/cloudformation_stack.html">aws_cloudformation_stack</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
|
||||||
<li<%= sidebar_current(/^docs-aws-resource-cloudwatch/) %>>
|
<li<%= sidebar_current(/^docs-aws-resource-cloudwatch/) %>>
|
||||||
<a href="#">CloudWatch Resources</a>
|
<a href="#">CloudWatch Resources</a>
|
||||||
|
|
Loading…
Reference in New Issue