github.com/gabrielperezs/terraform@v0.7.0-rc2.0.20160715084931-f7da2612946f/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  	conn := meta.(*AWSClient).cfconn
   272  
   273  	input := &cloudformation.UpdateStackInput{
   274  		StackName: aws.String(d.Get("name").(string)),
   275  	}
   276  
   277  	// Either TemplateBody, TemplateURL or UsePreviousTemplate are required
   278  	if v, ok := d.GetOk("template_url"); ok {
   279  		input.TemplateURL = aws.String(v.(string))
   280  	}
   281  	if v, ok := d.GetOk("template_body"); ok && input.TemplateURL == nil {
   282  		input.TemplateBody = aws.String(normalizeJson(v.(string)))
   283  	}
   284  
   285  	// Capabilities must be present whether they are changed or not
   286  	if v, ok := d.GetOk("capabilities"); ok {
   287  		input.Capabilities = expandStringList(v.(*schema.Set).List())
   288  	}
   289  
   290  	if d.HasChange("notification_arns") {
   291  		input.NotificationARNs = expandStringList(d.Get("notification_arns").(*schema.Set).List())
   292  	}
   293  
   294  	// Parameters must be present whether they are changed or not
   295  	if v, ok := d.GetOk("parameters"); ok {
   296  		input.Parameters = expandCloudFormationParameters(v.(map[string]interface{}))
   297  	}
   298  
   299  	if d.HasChange("policy_body") {
   300  		input.StackPolicyBody = aws.String(normalizeJson(d.Get("policy_body").(string)))
   301  	}
   302  	if d.HasChange("policy_url") {
   303  		input.StackPolicyURL = aws.String(d.Get("policy_url").(string))
   304  	}
   305  
   306  	log.Printf("[DEBUG] Updating CloudFormation stack: %s", input)
   307  	stack, err := conn.UpdateStack(input)
   308  	if err != nil {
   309  		return err
   310  	}
   311  
   312  	lastUpdatedTime, err := getLastCfEventTimestamp(d.Get("name").(string), conn)
   313  	if err != nil {
   314  		return err
   315  	}
   316  
   317  	wait := resource.StateChangeConf{
   318  		Pending: []string{
   319  			"UPDATE_COMPLETE_CLEANUP_IN_PROGRESS",
   320  			"UPDATE_IN_PROGRESS",
   321  			"UPDATE_ROLLBACK_IN_PROGRESS",
   322  			"UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS",
   323  			"UPDATE_ROLLBACK_COMPLETE",
   324  		},
   325  		Target:     []string{"UPDATE_COMPLETE"},
   326  		Timeout:    15 * time.Minute,
   327  		MinTimeout: 5 * time.Second,
   328  		Refresh: func() (interface{}, string, error) {
   329  			resp, err := conn.DescribeStacks(&cloudformation.DescribeStacksInput{
   330  				StackName: aws.String(d.Get("name").(string)),
   331  			})
   332  			stack := resp.Stacks[0]
   333  			status := *stack.StackStatus
   334  			log.Printf("[DEBUG] Current CloudFormation stack status: %q", status)
   335  
   336  			if status == "UPDATE_ROLLBACK_COMPLETE" {
   337  				failures, err := getCloudFormationFailures(stack.StackName, *lastUpdatedTime, conn)
   338  				if err != nil {
   339  					return resp, "", fmt.Errorf(
   340  						"Failed getting details about rollback: %q", err.Error())
   341  				}
   342  
   343  				return resp, "", fmt.Errorf(
   344  					"UPDATE_ROLLBACK_COMPLETE:\n%q", failures)
   345  			}
   346  
   347  			return resp, status, err
   348  		},
   349  	}
   350  
   351  	_, err = wait.WaitForState()
   352  	if err != nil {
   353  		return err
   354  	}
   355  
   356  	log.Printf("[DEBUG] CloudFormation stack %q has been updated", *stack.StackId)
   357  
   358  	return resourceAwsCloudFormationStackRead(d, meta)
   359  }
   360  
   361  func resourceAwsCloudFormationStackDelete(d *schema.ResourceData, meta interface{}) error {
   362  	conn := meta.(*AWSClient).cfconn
   363  
   364  	input := &cloudformation.DeleteStackInput{
   365  		StackName: aws.String(d.Get("name").(string)),
   366  	}
   367  	log.Printf("[DEBUG] Deleting CloudFormation stack %s", input)
   368  	_, err := conn.DeleteStack(input)
   369  	if err != nil {
   370  		awsErr, ok := err.(awserr.Error)
   371  		if !ok {
   372  			return err
   373  		}
   374  
   375  		if awsErr.Code() == "ValidationError" {
   376  			// Ignore stack which has been already deleted
   377  			return nil
   378  		}
   379  		return err
   380  	}
   381  
   382  	wait := resource.StateChangeConf{
   383  		Pending:    []string{"DELETE_IN_PROGRESS", "ROLLBACK_IN_PROGRESS"},
   384  		Target:     []string{"DELETE_COMPLETE"},
   385  		Timeout:    30 * time.Minute,
   386  		MinTimeout: 5 * time.Second,
   387  		Refresh: func() (interface{}, string, error) {
   388  			resp, err := conn.DescribeStacks(&cloudformation.DescribeStacksInput{
   389  				StackName: aws.String(d.Get("name").(string)),
   390  			})
   391  
   392  			if err != nil {
   393  				awsErr, ok := err.(awserr.Error)
   394  				if !ok {
   395  					return resp, "DELETE_FAILED", err
   396  				}
   397  
   398  				log.Printf("[DEBUG] Error when deleting CloudFormation stack: %s: %s",
   399  					awsErr.Code(), awsErr.Message())
   400  
   401  				if awsErr.Code() == "ValidationError" {
   402  					return resp, "DELETE_COMPLETE", nil
   403  				}
   404  			}
   405  
   406  			if len(resp.Stacks) == 0 {
   407  				log.Printf("[DEBUG] CloudFormation stack %q is already gone", d.Get("name"))
   408  				return resp, "DELETE_COMPLETE", nil
   409  			}
   410  
   411  			status := *resp.Stacks[0].StackStatus
   412  			log.Printf("[DEBUG] Current CloudFormation stack status: %q", status)
   413  
   414  			return resp, status, err
   415  		},
   416  	}
   417  
   418  	_, err = wait.WaitForState()
   419  	if err != nil {
   420  		return err
   421  	}
   422  
   423  	log.Printf("[DEBUG] CloudFormation stack %q has been deleted", d.Id())
   424  
   425  	d.SetId("")
   426  
   427  	return nil
   428  }
   429  
   430  // getLastCfEventTimestamp takes the first event in a list
   431  // of events ordered from the newest to the oldest
   432  // and extracts timestamp from it
   433  // LastUpdatedTime only provides last >successful< updated time
   434  func getLastCfEventTimestamp(stackName string, conn *cloudformation.CloudFormation) (
   435  	*time.Time, error) {
   436  	output, err := conn.DescribeStackEvents(&cloudformation.DescribeStackEventsInput{
   437  		StackName: aws.String(stackName),
   438  	})
   439  	if err != nil {
   440  		return nil, err
   441  	}
   442  
   443  	return output.StackEvents[0].Timestamp, nil
   444  }
   445  
   446  // getCloudFormationFailures returns ResourceStatusReason(s)
   447  // of events that should be failures based on regexp match of status
   448  func getCloudFormationFailures(stackName *string, afterTime time.Time,
   449  	conn *cloudformation.CloudFormation) ([]string, error) {
   450  	var failures []string
   451  	// Only catching failures from last 100 events
   452  	// Some extra iteration logic via NextToken could be added
   453  	// but in reality it's nearly impossible to generate >100
   454  	// events by a single stack update
   455  	events, err := conn.DescribeStackEvents(&cloudformation.DescribeStackEventsInput{
   456  		StackName: stackName,
   457  	})
   458  
   459  	if err != nil {
   460  		return nil, err
   461  	}
   462  
   463  	failRe := regexp.MustCompile("_FAILED$")
   464  	rollbackRe := regexp.MustCompile("^ROLLBACK_")
   465  
   466  	for _, e := range events.StackEvents {
   467  		if (failRe.MatchString(*e.ResourceStatus) || rollbackRe.MatchString(*e.ResourceStatus)) &&
   468  			e.Timestamp.After(afterTime) && e.ResourceStatusReason != nil {
   469  			failures = append(failures, *e.ResourceStatusReason)
   470  		}
   471  	}
   472  
   473  	return failures, nil
   474  }