2015-07-07 09:00:05 +02:00
|
|
|
package aws
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"log"
|
|
|
|
"regexp"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/aws/aws-sdk-go/aws"
|
|
|
|
"github.com/aws/aws-sdk-go/aws/awserr"
|
|
|
|
"github.com/aws/aws-sdk-go/service/cloudformation"
|
2016-09-22 18:21:27 +02:00
|
|
|
"github.com/hashicorp/errwrap"
|
|
|
|
"github.com/hashicorp/terraform/helper/resource"
|
|
|
|
"github.com/hashicorp/terraform/helper/schema"
|
2015-07-07 09:00:05 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
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{
|
2016-09-18 14:37:21 +02:00
|
|
|
Type: schema.TypeString,
|
|
|
|
Optional: true,
|
|
|
|
Computed: true,
|
|
|
|
ValidateFunc: validateJsonString,
|
|
|
|
StateFunc: func(v interface{}) string {
|
|
|
|
json, _ := normalizeJsonString(v)
|
|
|
|
return json
|
|
|
|
},
|
2015-07-07 09:00:05 +02:00
|
|
|
},
|
|
|
|
"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{
|
2016-09-18 14:37:21 +02:00
|
|
|
Type: schema.TypeString,
|
|
|
|
Optional: true,
|
|
|
|
Computed: true,
|
|
|
|
ValidateFunc: validateJsonString,
|
|
|
|
StateFunc: func(v interface{}) string {
|
|
|
|
json, _ := normalizeJsonString(v)
|
|
|
|
return json
|
|
|
|
},
|
2015-07-07 09:00:05 +02:00
|
|
|
},
|
|
|
|
"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 {
|
2016-03-18 14:16:16 +01:00
|
|
|
retryTimeout := int64(30)
|
2015-07-07 09:00:05 +02:00
|
|
|
conn := meta.(*AWSClient).cfconn
|
|
|
|
|
|
|
|
input := cloudformation.CreateStackInput{
|
|
|
|
StackName: aws.String(d.Get("name").(string)),
|
|
|
|
}
|
|
|
|
if v, ok := d.GetOk("template_body"); ok {
|
2016-09-22 18:21:27 +02:00
|
|
|
template, err := normalizeJsonString(v)
|
|
|
|
if err != nil {
|
|
|
|
return errwrap.Wrapf("template body contains an invalid JSON: {{err}}", err)
|
|
|
|
}
|
2016-09-18 14:37:21 +02:00
|
|
|
input.TemplateBody = aws.String(template)
|
2015-07-07 09:00:05 +02:00
|
|
|
}
|
|
|
|
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 {
|
2016-09-22 18:21:27 +02:00
|
|
|
policy, err := normalizeJsonString(v)
|
|
|
|
if err != nil {
|
|
|
|
return errwrap.Wrapf("policy body contains an invalid JSON: {{err}}", err)
|
|
|
|
}
|
2016-09-18 14:37:21 +02:00
|
|
|
input.StackPolicyBody = aws.String(policy)
|
2015-07-07 09:00:05 +02:00
|
|
|
}
|
|
|
|
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 {
|
2016-03-18 14:16:16 +01:00
|
|
|
m := int64(v.(int))
|
|
|
|
input.TimeoutInMinutes = aws.Int64(m)
|
|
|
|
if m > retryTimeout {
|
|
|
|
retryTimeout = m + 5
|
|
|
|
log.Printf("[DEBUG] CloudFormation timeout: %d", retryTimeout)
|
|
|
|
}
|
2015-07-07 09:00:05 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
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)
|
2016-03-13 13:15:41 +01:00
|
|
|
var lastStatus string
|
2015-07-07 09:00:05 +02:00
|
|
|
|
|
|
|
wait := resource.StateChangeConf{
|
2016-03-13 13:15:41 +01:00
|
|
|
Pending: []string{
|
|
|
|
"CREATE_IN_PROGRESS",
|
|
|
|
"DELETE_IN_PROGRESS",
|
|
|
|
"ROLLBACK_IN_PROGRESS",
|
|
|
|
},
|
|
|
|
Target: []string{
|
|
|
|
"CREATE_COMPLETE",
|
|
|
|
"CREATE_FAILED",
|
|
|
|
"DELETE_COMPLETE",
|
|
|
|
"DELETE_FAILED",
|
|
|
|
"ROLLBACK_COMPLETE",
|
|
|
|
"ROLLBACK_FAILED",
|
|
|
|
},
|
2016-03-18 14:16:16 +01:00
|
|
|
Timeout: time.Duration(retryTimeout) * time.Minute,
|
2016-03-13 13:15:41 +01:00
|
|
|
MinTimeout: 1 * time.Second,
|
2015-07-07 09:00:05 +02:00
|
|
|
Refresh: func() (interface{}, string, error) {
|
|
|
|
resp, err := conn.DescribeStacks(&cloudformation.DescribeStacksInput{
|
2016-03-13 13:15:41 +01:00
|
|
|
StackName: aws.String(d.Id()),
|
2015-07-07 09:00:05 +02:00
|
|
|
})
|
2016-03-13 13:15:41 +01:00
|
|
|
if err != nil {
|
|
|
|
log.Printf("[ERROR] Failed to describe stacks: %s", err)
|
|
|
|
return nil, "", err
|
|
|
|
}
|
|
|
|
if len(resp.Stacks) == 0 {
|
|
|
|
// This shouldn't happen unless CloudFormation is inconsistent
|
|
|
|
// See https://github.com/hashicorp/terraform/issues/5487
|
|
|
|
log.Printf("[WARN] CloudFormation stack %q not found.\nresponse: %q",
|
|
|
|
d.Id(), resp)
|
|
|
|
return resp, "", fmt.Errorf(
|
|
|
|
"CloudFormation stack %q vanished unexpectedly during creation.\n"+
|
|
|
|
"Unless you knowingly manually deleted the stack "+
|
|
|
|
"please report this as bug at https://github.com/hashicorp/terraform/issues\n"+
|
|
|
|
"along with the config & Terraform version & the details below:\n"+
|
|
|
|
"Full API response: %s\n",
|
|
|
|
d.Id(), resp)
|
|
|
|
}
|
|
|
|
|
2015-07-07 09:00:05 +02:00
|
|
|
status := *resp.Stacks[0].StackStatus
|
2016-03-13 13:15:41 +01:00
|
|
|
lastStatus = status
|
2015-07-07 09:00:05 +02:00
|
|
|
log.Printf("[DEBUG] Current CloudFormation stack status: %q", status)
|
|
|
|
|
|
|
|
return resp, status, err
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
_, err = wait.WaitForState()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2016-03-13 13:15:41 +01:00
|
|
|
if lastStatus == "ROLLBACK_COMPLETE" || lastStatus == "ROLLBACK_FAILED" {
|
|
|
|
reasons, err := getCloudFormationRollbackReasons(d.Id(), nil, conn)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("Failed getting rollback reasons: %q", err.Error())
|
|
|
|
}
|
|
|
|
|
|
|
|
return fmt.Errorf("%s: %q", lastStatus, reasons)
|
|
|
|
}
|
|
|
|
if lastStatus == "DELETE_COMPLETE" || lastStatus == "DELETE_FAILED" {
|
|
|
|
reasons, err := getCloudFormationDeletionReasons(d.Id(), conn)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("Failed getting deletion reasons: %q", err.Error())
|
|
|
|
}
|
|
|
|
|
|
|
|
d.SetId("")
|
|
|
|
return fmt.Errorf("%s: %q", lastStatus, reasons)
|
|
|
|
}
|
|
|
|
if lastStatus == "CREATE_FAILED" {
|
|
|
|
reasons, err := getCloudFormationFailures(d.Id(), conn)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("Failed getting failure reasons: %q", err.Error())
|
|
|
|
}
|
|
|
|
return fmt.Errorf("%s: %q", lastStatus, reasons)
|
|
|
|
}
|
|
|
|
|
|
|
|
log.Printf("[INFO] CloudFormation Stack %q created", d.Id())
|
2015-07-07 09:00:05 +02:00
|
|
|
|
|
|
|
return resourceAwsCloudFormationStackRead(d, meta)
|
|
|
|
}
|
|
|
|
|
|
|
|
func resourceAwsCloudFormationStackRead(d *schema.ResourceData, meta interface{}) error {
|
|
|
|
conn := meta.(*AWSClient).cfconn
|
|
|
|
|
|
|
|
input := &cloudformation.DescribeStacksInput{
|
2016-03-13 13:15:41 +01:00
|
|
|
StackName: aws.String(d.Id()),
|
2015-07-07 09:00:05 +02:00
|
|
|
}
|
|
|
|
resp, err := conn.DescribeStacks(input)
|
|
|
|
if err != nil {
|
2016-03-13 11:28:14 +01:00
|
|
|
awsErr, ok := err.(awserr.Error)
|
|
|
|
// ValidationError: Stack with id % does not exist
|
|
|
|
if ok && awsErr.Code() == "ValidationError" {
|
|
|
|
log.Printf("[WARN] Removing CloudFormation stack %s as it's already gone", d.Id())
|
|
|
|
d.SetId("")
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2015-07-07 09:00:05 +02:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
stacks := resp.Stacks
|
|
|
|
if len(stacks) < 1 {
|
2016-03-13 11:28:14 +01:00
|
|
|
log.Printf("[WARN] Removing CloudFormation stack %s as it's already gone", d.Id())
|
2015-12-17 17:58:11 +01:00
|
|
|
d.SetId("")
|
2015-07-07 09:00:05 +02:00
|
|
|
return nil
|
|
|
|
}
|
2015-12-17 17:58:11 +01:00
|
|
|
for _, s := range stacks {
|
|
|
|
if *s.StackId == d.Id() && *s.StackStatus == "DELETE_COMPLETE" {
|
|
|
|
log.Printf("[DEBUG] Removing CloudFormation stack %s"+
|
|
|
|
" as it has been already deleted", d.Id())
|
|
|
|
d.SetId("")
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
2015-07-07 09:00:05 +02:00
|
|
|
|
|
|
|
tInput := cloudformation.GetTemplateInput{
|
2016-03-13 13:15:41 +01:00
|
|
|
StackName: aws.String(d.Id()),
|
2015-07-07 09:00:05 +02:00
|
|
|
}
|
|
|
|
out, err := conn.GetTemplate(&tInput)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2016-09-22 18:21:27 +02:00
|
|
|
template, err := normalizeJsonString(*out.TemplateBody)
|
|
|
|
if err != nil {
|
|
|
|
return errwrap.Wrapf("template body contains an invalid JSON: {{err}}", err)
|
|
|
|
}
|
2016-09-18 14:37:21 +02:00
|
|
|
d.Set("template_body", template)
|
2015-07-07 09:00:05 +02:00
|
|
|
|
|
|
|
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 {
|
2016-08-05 10:44:10 +02:00
|
|
|
retryTimeout := int64(30)
|
2015-07-07 09:00:05 +02:00
|
|
|
conn := meta.(*AWSClient).cfconn
|
|
|
|
|
|
|
|
input := &cloudformation.UpdateStackInput{
|
2016-03-13 13:15:41 +01:00
|
|
|
StackName: aws.String(d.Id()),
|
2015-07-07 09:00:05 +02:00
|
|
|
}
|
|
|
|
|
2016-03-12 23:22:28 +01:00
|
|
|
// Either TemplateBody, TemplateURL or UsePreviousTemplate are required
|
2015-12-17 17:01:31 +01:00
|
|
|
if v, ok := d.GetOk("template_url"); ok {
|
|
|
|
input.TemplateURL = aws.String(v.(string))
|
2015-07-07 09:00:05 +02:00
|
|
|
}
|
2016-01-07 12:44:49 +01:00
|
|
|
if v, ok := d.GetOk("template_body"); ok && input.TemplateURL == nil {
|
2016-09-22 18:21:27 +02:00
|
|
|
template, err := normalizeJsonString(v)
|
|
|
|
if err != nil {
|
|
|
|
return errwrap.Wrapf("template body contains an invalid JSON: {{err}}", err)
|
|
|
|
}
|
2016-09-18 14:37:21 +02:00
|
|
|
input.TemplateBody = aws.String(template)
|
2016-01-07 12:44:49 +01:00
|
|
|
}
|
2015-12-17 17:01:31 +01:00
|
|
|
|
2016-03-12 23:22:28 +01:00
|
|
|
// Capabilities must be present whether they are changed or not
|
|
|
|
if v, ok := d.GetOk("capabilities"); ok {
|
|
|
|
input.Capabilities = expandStringList(v.(*schema.Set).List())
|
2015-07-07 09:00:05 +02:00
|
|
|
}
|
2016-03-12 23:22:28 +01:00
|
|
|
|
2015-07-07 09:00:05 +02:00
|
|
|
if d.HasChange("notification_arns") {
|
|
|
|
input.NotificationARNs = expandStringList(d.Get("notification_arns").(*schema.Set).List())
|
|
|
|
}
|
2016-03-12 23:22:28 +01:00
|
|
|
|
|
|
|
// Parameters must be present whether they are changed or not
|
|
|
|
if v, ok := d.GetOk("parameters"); ok {
|
|
|
|
input.Parameters = expandCloudFormationParameters(v.(map[string]interface{}))
|
2015-07-07 09:00:05 +02:00
|
|
|
}
|
2016-03-12 23:22:28 +01:00
|
|
|
|
2015-07-07 09:00:05 +02:00
|
|
|
if d.HasChange("policy_body") {
|
2016-09-22 18:21:27 +02:00
|
|
|
policy, err := normalizeJsonString(d.Get("policy_body"))
|
|
|
|
if err != nil {
|
|
|
|
return errwrap.Wrapf("policy body contains an invalid JSON: {{err}}", err)
|
|
|
|
}
|
2016-09-18 14:37:21 +02:00
|
|
|
input.StackPolicyBody = aws.String(policy)
|
2015-07-07 09:00:05 +02:00
|
|
|
}
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2016-03-13 13:15:41 +01:00
|
|
|
lastUpdatedTime, err := getLastCfEventTimestamp(d.Id(), conn)
|
2015-07-07 09:00:05 +02:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2016-08-05 10:44:10 +02:00
|
|
|
if v, ok := d.GetOk("timeout_in_minutes"); ok {
|
|
|
|
m := int64(v.(int))
|
|
|
|
if m > retryTimeout {
|
|
|
|
retryTimeout = m + 5
|
|
|
|
log.Printf("[DEBUG] CloudFormation timeout: %d", retryTimeout)
|
|
|
|
}
|
|
|
|
}
|
2016-03-13 13:15:41 +01:00
|
|
|
var lastStatus string
|
2015-07-07 09:00:05 +02:00
|
|
|
wait := resource.StateChangeConf{
|
|
|
|
Pending: []string{
|
|
|
|
"UPDATE_COMPLETE_CLEANUP_IN_PROGRESS",
|
|
|
|
"UPDATE_IN_PROGRESS",
|
|
|
|
"UPDATE_ROLLBACK_IN_PROGRESS",
|
|
|
|
"UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS",
|
2016-03-13 13:15:41 +01:00
|
|
|
},
|
|
|
|
Target: []string{
|
|
|
|
"UPDATE_COMPLETE",
|
2015-07-07 09:00:05 +02:00
|
|
|
"UPDATE_ROLLBACK_COMPLETE",
|
2016-03-13 13:15:41 +01:00
|
|
|
"UPDATE_ROLLBACK_FAILED",
|
2015-07-07 09:00:05 +02:00
|
|
|
},
|
2016-08-05 10:44:10 +02:00
|
|
|
Timeout: time.Duration(retryTimeout) * time.Minute,
|
2015-07-07 09:00:05 +02:00
|
|
|
MinTimeout: 5 * time.Second,
|
|
|
|
Refresh: func() (interface{}, string, error) {
|
|
|
|
resp, err := conn.DescribeStacks(&cloudformation.DescribeStacksInput{
|
2016-03-13 13:15:41 +01:00
|
|
|
StackName: aws.String(d.Id()),
|
2015-07-07 09:00:05 +02:00
|
|
|
})
|
2016-03-13 13:15:41 +01:00
|
|
|
if err != nil {
|
|
|
|
log.Printf("[ERROR] Failed to describe stacks: %s", err)
|
|
|
|
return nil, "", err
|
2015-07-07 09:00:05 +02:00
|
|
|
}
|
|
|
|
|
2016-03-13 13:15:41 +01:00
|
|
|
status := *resp.Stacks[0].StackStatus
|
|
|
|
lastStatus = status
|
|
|
|
log.Printf("[DEBUG] Current CloudFormation stack status: %q", status)
|
|
|
|
|
2015-07-07 09:00:05 +02:00
|
|
|
return resp, status, err
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
_, err = wait.WaitForState()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2016-03-13 13:15:41 +01:00
|
|
|
if lastStatus == "UPDATE_ROLLBACK_COMPLETE" || lastStatus == "UPDATE_ROLLBACK_FAILED" {
|
|
|
|
reasons, err := getCloudFormationRollbackReasons(*stack.StackId, lastUpdatedTime, conn)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("Failed getting details about rollback: %q", err.Error())
|
|
|
|
}
|
|
|
|
|
|
|
|
return fmt.Errorf("%s: %q", lastStatus, reasons)
|
|
|
|
}
|
|
|
|
|
2015-07-07 09:00:05 +02:00
|
|
|
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{
|
2016-03-13 13:15:41 +01:00
|
|
|
StackName: aws.String(d.Id()),
|
2015-07-07 09:00:05 +02:00
|
|
|
}
|
|
|
|
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
|
|
|
|
}
|
2016-03-13 13:15:41 +01:00
|
|
|
var lastStatus string
|
2015-07-07 09:00:05 +02:00
|
|
|
wait := resource.StateChangeConf{
|
2016-03-13 13:15:41 +01:00
|
|
|
Pending: []string{
|
|
|
|
"DELETE_IN_PROGRESS",
|
|
|
|
"ROLLBACK_IN_PROGRESS",
|
|
|
|
},
|
|
|
|
Target: []string{
|
|
|
|
"DELETE_COMPLETE",
|
|
|
|
"DELETE_FAILED",
|
|
|
|
},
|
2015-07-07 09:00:05 +02:00
|
|
|
Timeout: 30 * time.Minute,
|
|
|
|
MinTimeout: 5 * time.Second,
|
|
|
|
Refresh: func() (interface{}, string, error) {
|
|
|
|
resp, err := conn.DescribeStacks(&cloudformation.DescribeStacksInput{
|
2016-03-13 13:15:41 +01:00
|
|
|
StackName: aws.String(d.Id()),
|
2015-07-07 09:00:05 +02:00
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
awsErr, ok := err.(awserr.Error)
|
|
|
|
if !ok {
|
2016-03-13 13:15:41 +01:00
|
|
|
return nil, "", err
|
2015-07-07 09:00:05 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
log.Printf("[DEBUG] Error when deleting CloudFormation stack: %s: %s",
|
|
|
|
awsErr.Code(), awsErr.Message())
|
|
|
|
|
2016-03-13 11:28:14 +01:00
|
|
|
// ValidationError: Stack with id % does not exist
|
2015-07-07 09:00:05 +02:00
|
|
|
if awsErr.Code() == "ValidationError" {
|
|
|
|
return resp, "DELETE_COMPLETE", nil
|
|
|
|
}
|
2016-03-13 13:15:41 +01:00
|
|
|
return nil, "", err
|
2015-07-07 09:00:05 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if len(resp.Stacks) == 0 {
|
2016-03-13 13:15:41 +01:00
|
|
|
log.Printf("[DEBUG] CloudFormation stack %q is already gone", d.Id())
|
2015-07-07 09:00:05 +02:00
|
|
|
return resp, "DELETE_COMPLETE", nil
|
|
|
|
}
|
|
|
|
|
|
|
|
status := *resp.Stacks[0].StackStatus
|
2016-03-13 13:15:41 +01:00
|
|
|
lastStatus = status
|
2015-07-07 09:00:05 +02:00
|
|
|
log.Printf("[DEBUG] Current CloudFormation stack status: %q", status)
|
|
|
|
|
|
|
|
return resp, status, err
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
_, err = wait.WaitForState()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2016-03-13 13:15:41 +01:00
|
|
|
if lastStatus == "DELETE_FAILED" {
|
|
|
|
reasons, err := getCloudFormationFailures(d.Id(), conn)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("Failed getting reasons of failure: %q", err.Error())
|
|
|
|
}
|
|
|
|
|
|
|
|
return fmt.Errorf("%s: %q", lastStatus, reasons)
|
|
|
|
}
|
|
|
|
|
2015-07-07 09:00:05 +02:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2016-03-13 13:15:41 +01:00
|
|
|
func getCloudFormationRollbackReasons(stackId string, afterTime *time.Time, conn *cloudformation.CloudFormation) ([]string, error) {
|
2015-07-07 09:00:05 +02:00
|
|
|
var failures []string
|
2016-03-13 13:15:41 +01:00
|
|
|
|
|
|
|
err := conn.DescribeStackEventsPages(&cloudformation.DescribeStackEventsInput{
|
|
|
|
StackName: aws.String(stackId),
|
|
|
|
}, func(page *cloudformation.DescribeStackEventsOutput, lastPage bool) bool {
|
|
|
|
for _, e := range page.StackEvents {
|
|
|
|
if afterTime != nil && !e.Timestamp.After(*afterTime) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
if cfStackEventIsFailure(e) || cfStackEventIsRollback(e) {
|
|
|
|
failures = append(failures, *e.ResourceStatusReason)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return !lastPage
|
2015-07-07 09:00:05 +02:00
|
|
|
})
|
|
|
|
|
2016-03-13 13:15:41 +01:00
|
|
|
return failures, err
|
|
|
|
}
|
2015-07-07 09:00:05 +02:00
|
|
|
|
2016-03-13 13:15:41 +01:00
|
|
|
func getCloudFormationDeletionReasons(stackId string, conn *cloudformation.CloudFormation) ([]string, error) {
|
|
|
|
var failures []string
|
2015-07-07 09:00:05 +02:00
|
|
|
|
2016-03-13 13:15:41 +01:00
|
|
|
err := conn.DescribeStackEventsPages(&cloudformation.DescribeStackEventsInput{
|
|
|
|
StackName: aws.String(stackId),
|
|
|
|
}, func(page *cloudformation.DescribeStackEventsOutput, lastPage bool) bool {
|
|
|
|
for _, e := range page.StackEvents {
|
|
|
|
if cfStackEventIsFailure(e) || cfStackEventIsStackDeletion(e) {
|
|
|
|
failures = append(failures, *e.ResourceStatusReason)
|
|
|
|
}
|
2015-07-07 09:00:05 +02:00
|
|
|
}
|
2016-03-13 13:15:41 +01:00
|
|
|
return !lastPage
|
|
|
|
})
|
|
|
|
|
|
|
|
return failures, err
|
|
|
|
}
|
|
|
|
|
|
|
|
func getCloudFormationFailures(stackId string, conn *cloudformation.CloudFormation) ([]string, error) {
|
|
|
|
var failures []string
|
|
|
|
|
|
|
|
err := conn.DescribeStackEventsPages(&cloudformation.DescribeStackEventsInput{
|
|
|
|
StackName: aws.String(stackId),
|
|
|
|
}, func(page *cloudformation.DescribeStackEventsOutput, lastPage bool) bool {
|
|
|
|
for _, e := range page.StackEvents {
|
|
|
|
if cfStackEventIsFailure(e) {
|
|
|
|
failures = append(failures, *e.ResourceStatusReason)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return !lastPage
|
|
|
|
})
|
|
|
|
|
|
|
|
return failures, err
|
|
|
|
}
|
|
|
|
|
|
|
|
func cfStackEventIsFailure(event *cloudformation.StackEvent) bool {
|
|
|
|
failRe := regexp.MustCompile("_FAILED$")
|
|
|
|
return failRe.MatchString(*event.ResourceStatus) && event.ResourceStatusReason != nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func cfStackEventIsRollback(event *cloudformation.StackEvent) bool {
|
|
|
|
rollbackRe := regexp.MustCompile("^ROLLBACK_")
|
|
|
|
return rollbackRe.MatchString(*event.ResourceStatus) && event.ResourceStatusReason != nil
|
|
|
|
}
|
2015-07-07 09:00:05 +02:00
|
|
|
|
2016-03-13 13:15:41 +01:00
|
|
|
func cfStackEventIsStackDeletion(event *cloudformation.StackEvent) bool {
|
|
|
|
return *event.ResourceStatus == "DELETE_IN_PROGRESS" &&
|
|
|
|
*event.ResourceType == "AWS::CloudFormation::Stack" &&
|
|
|
|
event.ResourceStatusReason != nil
|
2015-07-07 09:00:05 +02:00
|
|
|
}
|