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