github.com/daveadams/terraform@v0.6.4-0.20160830094355-13ce74975936/builtin/providers/aws/resource_aws_cloudformation_stack.go (about)

     1  package aws
     2  
     3  import (
     4  	"fmt"
     5  	"log"
     6  	"regexp"
     7  	"time"
     8  
     9  	"github.com/hashicorp/terraform/helper/resource"
    10  	"github.com/hashicorp/terraform/helper/schema"
    11  
    12  	"github.com/aws/aws-sdk-go/aws"
    13  	"github.com/aws/aws-sdk-go/aws/awserr"
    14  	"github.com/aws/aws-sdk-go/service/cloudformation"
    15  )
    16  
    17  func resourceAwsCloudFormationStack() *schema.Resource {
    18  	return &schema.Resource{
    19  		Create: resourceAwsCloudFormationStackCreate,
    20  		Read:   resourceAwsCloudFormationStackRead,
    21  		Update: resourceAwsCloudFormationStackUpdate,
    22  		Delete: resourceAwsCloudFormationStackDelete,
    23  
    24  		Schema: map[string]*schema.Schema{
    25  			"name": &schema.Schema{
    26  				Type:     schema.TypeString,
    27  				Required: true,
    28  				ForceNew: true,
    29  			},
    30  			"template_body": &schema.Schema{
    31  				Type:      schema.TypeString,
    32  				Optional:  true,
    33  				Computed:  true,
    34  				StateFunc: normalizeJson,
    35  			},
    36  			"template_url": &schema.Schema{
    37  				Type:     schema.TypeString,
    38  				Optional: true,
    39  			},
    40  			"capabilities": &schema.Schema{
    41  				Type:     schema.TypeSet,
    42  				Optional: true,
    43  				Elem:     &schema.Schema{Type: schema.TypeString},
    44  				Set:      schema.HashString,
    45  			},
    46  			"disable_rollback": &schema.Schema{
    47  				Type:     schema.TypeBool,
    48  				Optional: true,
    49  				ForceNew: true,
    50  			},
    51  			"notification_arns": &schema.Schema{
    52  				Type:     schema.TypeSet,
    53  				Optional: true,
    54  				Elem:     &schema.Schema{Type: schema.TypeString},
    55  				Set:      schema.HashString,
    56  			},
    57  			"on_failure": &schema.Schema{
    58  				Type:     schema.TypeString,
    59  				Optional: true,
    60  				ForceNew: true,
    61  			},
    62  			"parameters": &schema.Schema{
    63  				Type:     schema.TypeMap,
    64  				Optional: true,
    65  				Computed: true,
    66  			},
    67  			"outputs": &schema.Schema{
    68  				Type:     schema.TypeMap,
    69  				Computed: true,
    70  			},
    71  			"policy_body": &schema.Schema{
    72  				Type:      schema.TypeString,
    73  				Optional:  true,
    74  				Computed:  true,
    75  				StateFunc: normalizeJson,
    76  			},
    77  			"policy_url": &schema.Schema{
    78  				Type:     schema.TypeString,
    79  				Optional: true,
    80  			},
    81  			"timeout_in_minutes": &schema.Schema{
    82  				Type:     schema.TypeInt,
    83  				Optional: true,
    84  				ForceNew: true,
    85  			},
    86  			"tags": &schema.Schema{
    87  				Type:     schema.TypeMap,
    88  				Optional: true,
    89  				ForceNew: true,
    90  			},
    91  		},
    92  	}
    93  }
    94  
    95  func resourceAwsCloudFormationStackCreate(d *schema.ResourceData, meta interface{}) error {
    96  	retryTimeout := int64(30)
    97  	conn := meta.(*AWSClient).cfconn
    98  
    99  	input := cloudformation.CreateStackInput{
   100  		StackName: aws.String(d.Get("name").(string)),
   101  	}
   102  	if v, ok := d.GetOk("template_body"); ok {
   103  		input.TemplateBody = aws.String(normalizeJson(v.(string)))
   104  	}
   105  	if v, ok := d.GetOk("template_url"); ok {
   106  		input.TemplateURL = aws.String(v.(string))
   107  	}
   108  	if v, ok := d.GetOk("capabilities"); ok {
   109  		input.Capabilities = expandStringList(v.(*schema.Set).List())
   110  	}
   111  	if v, ok := d.GetOk("disable_rollback"); ok {
   112  		input.DisableRollback = aws.Bool(v.(bool))
   113  	}
   114  	if v, ok := d.GetOk("notification_arns"); ok {
   115  		input.NotificationARNs = expandStringList(v.(*schema.Set).List())
   116  	}
   117  	if v, ok := d.GetOk("on_failure"); ok {
   118  		input.OnFailure = aws.String(v.(string))
   119  	}
   120  	if v, ok := d.GetOk("parameters"); ok {
   121  		input.Parameters = expandCloudFormationParameters(v.(map[string]interface{}))
   122  	}
   123  	if v, ok := d.GetOk("policy_body"); ok {
   124  		input.StackPolicyBody = aws.String(normalizeJson(v.(string)))
   125  	}
   126  	if v, ok := d.GetOk("policy_url"); ok {
   127  		input.StackPolicyURL = aws.String(v.(string))
   128  	}
   129  	if v, ok := d.GetOk("tags"); ok {
   130  		input.Tags = expandCloudFormationTags(v.(map[string]interface{}))
   131  	}
   132  	if v, ok := d.GetOk("timeout_in_minutes"); ok {
   133  		m := int64(v.(int))
   134  		input.TimeoutInMinutes = aws.Int64(m)
   135  		if m > retryTimeout {
   136  			retryTimeout = m + 5
   137  			log.Printf("[DEBUG] CloudFormation timeout: %d", retryTimeout)
   138  		}
   139  	}
   140  
   141  	log.Printf("[DEBUG] Creating CloudFormation Stack: %s", input)
   142  	resp, err := conn.CreateStack(&input)
   143  	if err != nil {
   144  		return fmt.Errorf("Creating CloudFormation stack failed: %s", err.Error())
   145  	}
   146  
   147  	d.SetId(*resp.StackId)
   148  
   149  	wait := resource.StateChangeConf{
   150  		Pending:    []string{"CREATE_IN_PROGRESS", "ROLLBACK_IN_PROGRESS", "ROLLBACK_COMPLETE"},
   151  		Target:     []string{"CREATE_COMPLETE"},
   152  		Timeout:    time.Duration(retryTimeout) * time.Minute,
   153  		MinTimeout: 5 * time.Second,
   154  		Refresh: func() (interface{}, string, error) {
   155  			resp, err := conn.DescribeStacks(&cloudformation.DescribeStacksInput{
   156  				StackName: aws.String(d.Get("name").(string)),
   157  			})
   158  			status := *resp.Stacks[0].StackStatus
   159  			log.Printf("[DEBUG] Current CloudFormation stack status: %q", status)
   160  
   161  			if status == "ROLLBACK_COMPLETE" {
   162  				stack := resp.Stacks[0]
   163  				failures, err := getCloudFormationFailures(stack.StackName, *stack.CreationTime, conn)
   164  				if err != nil {
   165  					return resp, "", fmt.Errorf(
   166  						"Failed getting details about rollback: %q", err.Error())
   167  				}
   168  
   169  				return resp, "", fmt.Errorf("ROLLBACK_COMPLETE:\n%q", failures)
   170  			}
   171  			return resp, status, err
   172  		},
   173  	}
   174  
   175  	_, err = wait.WaitForState()
   176  	if err != nil {
   177  		return err
   178  	}
   179  
   180  	log.Printf("[INFO] CloudFormation Stack %q created", d.Get("name").(string))
   181  
   182  	return resourceAwsCloudFormationStackRead(d, meta)
   183  }
   184  
   185  func resourceAwsCloudFormationStackRead(d *schema.ResourceData, meta interface{}) error {
   186  	conn := meta.(*AWSClient).cfconn
   187  	stackName := d.Get("name").(string)
   188  
   189  	input := &cloudformation.DescribeStacksInput{
   190  		StackName: aws.String(stackName),
   191  	}
   192  	resp, err := conn.DescribeStacks(input)
   193  	if err != nil {
   194  		return err
   195  	}
   196  
   197  	stacks := resp.Stacks
   198  	if len(stacks) < 1 {
   199  		log.Printf("[DEBUG] Removing CloudFormation stack %s as it's already gone", d.Id())
   200  		d.SetId("")
   201  		return nil
   202  	}
   203  	for _, s := range stacks {
   204  		if *s.StackId == d.Id() && *s.StackStatus == "DELETE_COMPLETE" {
   205  			log.Printf("[DEBUG] Removing CloudFormation stack %s"+
   206  				" as it has been already deleted", d.Id())
   207  			d.SetId("")
   208  			return nil
   209  		}
   210  	}
   211  
   212  	tInput := cloudformation.GetTemplateInput{
   213  		StackName: aws.String(stackName),
   214  	}
   215  	out, err := conn.GetTemplate(&tInput)
   216  	if err != nil {
   217  		return err
   218  	}
   219  
   220  	d.Set("template_body", normalizeJson(*out.TemplateBody))
   221  
   222  	stack := stacks[0]
   223  	log.Printf("[DEBUG] Received CloudFormation stack: %s", stack)
   224  
   225  	d.Set("name", stack.StackName)
   226  	d.Set("arn", stack.StackId)
   227  
   228  	if stack.TimeoutInMinutes != nil {
   229  		d.Set("timeout_in_minutes", int(*stack.TimeoutInMinutes))
   230  	}
   231  	if stack.Description != nil {
   232  		d.Set("description", stack.Description)
   233  	}
   234  	if stack.DisableRollback != nil {
   235  		d.Set("disable_rollback", stack.DisableRollback)
   236  	}
   237  	if len(stack.NotificationARNs) > 0 {
   238  		err = d.Set("notification_arns", schema.NewSet(schema.HashString, flattenStringList(stack.NotificationARNs)))
   239  		if err != nil {
   240  			return err
   241  		}
   242  	}
   243  
   244  	originalParams := d.Get("parameters").(map[string]interface{})
   245  	err = d.Set("parameters", flattenCloudFormationParameters(stack.Parameters, originalParams))
   246  	if err != nil {
   247  		return err
   248  	}
   249  
   250  	err = d.Set("tags", flattenCloudFormationTags(stack.Tags))
   251  	if err != nil {
   252  		return err
   253  	}
   254  
   255  	err = d.Set("outputs", flattenCloudFormationOutputs(stack.Outputs))
   256  	if err != nil {
   257  		return err
   258  	}
   259  
   260  	if len(stack.Capabilities) > 0 {
   261  		err = d.Set("capabilities", schema.NewSet(schema.HashString, flattenStringList(stack.Capabilities)))
   262  		if err != nil {
   263  			return err
   264  		}
   265  	}
   266  
   267  	return nil
   268  }
   269  
   270  func resourceAwsCloudFormationStackUpdate(d *schema.ResourceData, meta interface{}) error {
   271  	retryTimeout := int64(30)
   272  	conn := meta.(*AWSClient).cfconn
   273  
   274  	input := &cloudformation.UpdateStackInput{
   275  		StackName: aws.String(d.Get("name").(string)),
   276  	}
   277  
   278  	// Either TemplateBody, TemplateURL or UsePreviousTemplate are required
   279  	if v, ok := d.GetOk("template_url"); ok {
   280  		input.TemplateURL = aws.String(v.(string))
   281  	}
   282  	if v, ok := d.GetOk("template_body"); ok && input.TemplateURL == nil {
   283  		input.TemplateBody = aws.String(normalizeJson(v.(string)))
   284  	}
   285  
   286  	// Capabilities must be present whether they are changed or not
   287  	if v, ok := d.GetOk("capabilities"); ok {
   288  		input.Capabilities = expandStringList(v.(*schema.Set).List())
   289  	}
   290  
   291  	if d.HasChange("notification_arns") {
   292  		input.NotificationARNs = expandStringList(d.Get("notification_arns").(*schema.Set).List())
   293  	}
   294  
   295  	// Parameters must be present whether they are changed or not
   296  	if v, ok := d.GetOk("parameters"); ok {
   297  		input.Parameters = expandCloudFormationParameters(v.(map[string]interface{}))
   298  	}
   299  
   300  	if d.HasChange("policy_body") {
   301  		input.StackPolicyBody = aws.String(normalizeJson(d.Get("policy_body").(string)))
   302  	}
   303  	if d.HasChange("policy_url") {
   304  		input.StackPolicyURL = aws.String(d.Get("policy_url").(string))
   305  	}
   306  
   307  	log.Printf("[DEBUG] Updating CloudFormation stack: %s", input)
   308  	stack, err := conn.UpdateStack(input)
   309  	if err != nil {
   310  		return err
   311  	}
   312  
   313  	lastUpdatedTime, err := getLastCfEventTimestamp(d.Get("name").(string), conn)
   314  	if err != nil {
   315  		return err
   316  	}
   317  
   318  	if v, ok := d.GetOk("timeout_in_minutes"); ok {
   319  		m := int64(v.(int))
   320  		if m > retryTimeout {
   321  			retryTimeout = m + 5
   322  			log.Printf("[DEBUG] CloudFormation timeout: %d", retryTimeout)
   323  		}
   324  	}
   325  	wait := resource.StateChangeConf{
   326  		Pending: []string{
   327  			"UPDATE_COMPLETE_CLEANUP_IN_PROGRESS",
   328  			"UPDATE_IN_PROGRESS",
   329  			"UPDATE_ROLLBACK_IN_PROGRESS",
   330  			"UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS",
   331  			"UPDATE_ROLLBACK_COMPLETE",
   332  		},
   333  		Target:     []string{"UPDATE_COMPLETE"},
   334  		Timeout:    time.Duration(retryTimeout) * time.Minute,
   335  		MinTimeout: 5 * time.Second,
   336  		Refresh: func() (interface{}, string, error) {
   337  			resp, err := conn.DescribeStacks(&cloudformation.DescribeStacksInput{
   338  				StackName: aws.String(d.Get("name").(string)),
   339  			})
   340  			stack := resp.Stacks[0]
   341  			status := *stack.StackStatus
   342  			log.Printf("[DEBUG] Current CloudFormation stack status: %q", status)
   343  
   344  			if status == "UPDATE_ROLLBACK_COMPLETE" {
   345  				failures, err := getCloudFormationFailures(stack.StackName, *lastUpdatedTime, conn)
   346  				if err != nil {
   347  					return resp, "", fmt.Errorf(
   348  						"Failed getting details about rollback: %q", err.Error())
   349  				}
   350  
   351  				return resp, "", fmt.Errorf(
   352  					"UPDATE_ROLLBACK_COMPLETE:\n%q", failures)
   353  			}
   354  
   355  			return resp, status, err
   356  		},
   357  	}
   358  
   359  	_, err = wait.WaitForState()
   360  	if err != nil {
   361  		return err
   362  	}
   363  
   364  	log.Printf("[DEBUG] CloudFormation stack %q has been updated", *stack.StackId)
   365  
   366  	return resourceAwsCloudFormationStackRead(d, meta)
   367  }
   368  
   369  func resourceAwsCloudFormationStackDelete(d *schema.ResourceData, meta interface{}) error {
   370  	conn := meta.(*AWSClient).cfconn
   371  
   372  	input := &cloudformation.DeleteStackInput{
   373  		StackName: aws.String(d.Get("name").(string)),
   374  	}
   375  	log.Printf("[DEBUG] Deleting CloudFormation stack %s", input)
   376  	_, err := conn.DeleteStack(input)
   377  	if err != nil {
   378  		awsErr, ok := err.(awserr.Error)
   379  		if !ok {
   380  			return err
   381  		}
   382  
   383  		if awsErr.Code() == "ValidationError" {
   384  			// Ignore stack which has been already deleted
   385  			return nil
   386  		}
   387  		return err
   388  	}
   389  
   390  	wait := resource.StateChangeConf{
   391  		Pending:    []string{"DELETE_IN_PROGRESS", "ROLLBACK_IN_PROGRESS"},
   392  		Target:     []string{"DELETE_COMPLETE"},
   393  		Timeout:    30 * time.Minute,
   394  		MinTimeout: 5 * time.Second,
   395  		Refresh: func() (interface{}, string, error) {
   396  			resp, err := conn.DescribeStacks(&cloudformation.DescribeStacksInput{
   397  				StackName: aws.String(d.Get("name").(string)),
   398  			})
   399  
   400  			if err != nil {
   401  				awsErr, ok := err.(awserr.Error)
   402  				if !ok {
   403  					return resp, "DELETE_FAILED", err
   404  				}
   405  
   406  				log.Printf("[DEBUG] Error when deleting CloudFormation stack: %s: %s",
   407  					awsErr.Code(), awsErr.Message())
   408  
   409  				if awsErr.Code() == "ValidationError" {
   410  					return resp, "DELETE_COMPLETE", nil
   411  				}
   412  			}
   413  
   414  			if len(resp.Stacks) == 0 {
   415  				log.Printf("[DEBUG] CloudFormation stack %q is already gone", d.Get("name"))
   416  				return resp, "DELETE_COMPLETE", nil
   417  			}
   418  
   419  			status := *resp.Stacks[0].StackStatus
   420  			log.Printf("[DEBUG] Current CloudFormation stack status: %q", status)
   421  
   422  			return resp, status, err
   423  		},
   424  	}
   425  
   426  	_, err = wait.WaitForState()
   427  	if err != nil {
   428  		return err
   429  	}
   430  
   431  	log.Printf("[DEBUG] CloudFormation stack %q has been deleted", d.Id())
   432  
   433  	d.SetId("")
   434  
   435  	return nil
   436  }
   437  
   438  // getLastCfEventTimestamp takes the first event in a list
   439  // of events ordered from the newest to the oldest
   440  // and extracts timestamp from it
   441  // LastUpdatedTime only provides last >successful< updated time
   442  func getLastCfEventTimestamp(stackName string, conn *cloudformation.CloudFormation) (
   443  	*time.Time, error) {
   444  	output, err := conn.DescribeStackEvents(&cloudformation.DescribeStackEventsInput{
   445  		StackName: aws.String(stackName),
   446  	})
   447  	if err != nil {
   448  		return nil, err
   449  	}
   450  
   451  	return output.StackEvents[0].Timestamp, nil
   452  }
   453  
   454  // getCloudFormationFailures returns ResourceStatusReason(s)
   455  // of events that should be failures based on regexp match of status
   456  func getCloudFormationFailures(stackName *string, afterTime time.Time,
   457  	conn *cloudformation.CloudFormation) ([]string, error) {
   458  	var failures []string
   459  	// Only catching failures from last 100 events
   460  	// Some extra iteration logic via NextToken could be added
   461  	// but in reality it's nearly impossible to generate >100
   462  	// events by a single stack update
   463  	events, err := conn.DescribeStackEvents(&cloudformation.DescribeStackEventsInput{
   464  		StackName: stackName,
   465  	})
   466  
   467  	if err != nil {
   468  		return nil, err
   469  	}
   470  
   471  	failRe := regexp.MustCompile("_FAILED$")
   472  	rollbackRe := regexp.MustCompile("^ROLLBACK_")
   473  
   474  	for _, e := range events.StackEvents {
   475  		if (failRe.MatchString(*e.ResourceStatus) || rollbackRe.MatchString(*e.ResourceStatus)) &&
   476  			e.Timestamp.After(afterTime) && e.ResourceStatusReason != nil {
   477  			failures = append(failures, *e.ResourceStatusReason)
   478  		}
   479  	}
   480  
   481  	return failures, nil
   482  }