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