github.com/turtlemonvh/terraform@v0.6.9-0.20151204001754-8e40b6b855e8/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:     "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  		return nil
   194  	}
   195  
   196  	tInput := cloudformation.GetTemplateInput{
   197  		StackName: aws.String(stackName),
   198  	}
   199  	out, err := conn.GetTemplate(&tInput)
   200  	if err != nil {
   201  		return err
   202  	}
   203  
   204  	d.Set("template_body", normalizeJson(*out.TemplateBody))
   205  
   206  	stack := stacks[0]
   207  	log.Printf("[DEBUG] Received CloudFormation stack: %s", stack)
   208  
   209  	d.Set("name", stack.StackName)
   210  	d.Set("arn", stack.StackId)
   211  
   212  	if stack.TimeoutInMinutes != nil {
   213  		d.Set("timeout_in_minutes", int(*stack.TimeoutInMinutes))
   214  	}
   215  	if stack.Description != nil {
   216  		d.Set("description", stack.Description)
   217  	}
   218  	if stack.DisableRollback != nil {
   219  		d.Set("disable_rollback", stack.DisableRollback)
   220  	}
   221  	if len(stack.NotificationARNs) > 0 {
   222  		err = d.Set("notification_arns", schema.NewSet(schema.HashString, flattenStringList(stack.NotificationARNs)))
   223  		if err != nil {
   224  			return err
   225  		}
   226  	}
   227  
   228  	originalParams := d.Get("parameters").(map[string]interface{})
   229  	err = d.Set("parameters", flattenCloudFormationParameters(stack.Parameters, originalParams))
   230  	if err != nil {
   231  		return err
   232  	}
   233  
   234  	err = d.Set("tags", flattenCloudFormationTags(stack.Tags))
   235  	if err != nil {
   236  		return err
   237  	}
   238  
   239  	err = d.Set("outputs", flattenCloudFormationOutputs(stack.Outputs))
   240  	if err != nil {
   241  		return err
   242  	}
   243  
   244  	if len(stack.Capabilities) > 0 {
   245  		err = d.Set("capabilities", schema.NewSet(schema.HashString, flattenStringList(stack.Capabilities)))
   246  		if err != nil {
   247  			return err
   248  		}
   249  	}
   250  
   251  	return nil
   252  }
   253  
   254  func resourceAwsCloudFormationStackUpdate(d *schema.ResourceData, meta interface{}) error {
   255  	conn := meta.(*AWSClient).cfconn
   256  
   257  	input := &cloudformation.UpdateStackInput{
   258  		StackName: aws.String(d.Get("name").(string)),
   259  	}
   260  
   261  	if d.HasChange("template_body") {
   262  		input.TemplateBody = aws.String(normalizeJson(d.Get("template_body").(string)))
   263  	}
   264  	if d.HasChange("template_url") {
   265  		input.TemplateURL = aws.String(d.Get("template_url").(string))
   266  	}
   267  	if d.HasChange("capabilities") {
   268  		input.Capabilities = expandStringList(d.Get("capabilities").(*schema.Set).List())
   269  	}
   270  	if d.HasChange("notification_arns") {
   271  		input.NotificationARNs = expandStringList(d.Get("notification_arns").(*schema.Set).List())
   272  	}
   273  	if d.HasChange("parameters") {
   274  		input.Parameters = expandCloudFormationParameters(d.Get("parameters").(map[string]interface{}))
   275  	}
   276  	if d.HasChange("policy_body") {
   277  		input.StackPolicyBody = aws.String(normalizeJson(d.Get("policy_body").(string)))
   278  	}
   279  	if d.HasChange("policy_url") {
   280  		input.StackPolicyURL = aws.String(d.Get("policy_url").(string))
   281  	}
   282  
   283  	log.Printf("[DEBUG] Updating CloudFormation stack: %s", input)
   284  	stack, err := conn.UpdateStack(input)
   285  	if err != nil {
   286  		return err
   287  	}
   288  
   289  	lastUpdatedTime, err := getLastCfEventTimestamp(d.Get("name").(string), conn)
   290  	if err != nil {
   291  		return err
   292  	}
   293  
   294  	wait := resource.StateChangeConf{
   295  		Pending: []string{
   296  			"UPDATE_COMPLETE_CLEANUP_IN_PROGRESS",
   297  			"UPDATE_IN_PROGRESS",
   298  			"UPDATE_ROLLBACK_IN_PROGRESS",
   299  			"UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS",
   300  			"UPDATE_ROLLBACK_COMPLETE",
   301  		},
   302  		Target:     "UPDATE_COMPLETE",
   303  		Timeout:    15 * time.Minute,
   304  		MinTimeout: 5 * time.Second,
   305  		Refresh: func() (interface{}, string, error) {
   306  			resp, err := conn.DescribeStacks(&cloudformation.DescribeStacksInput{
   307  				StackName: aws.String(d.Get("name").(string)),
   308  			})
   309  			stack := resp.Stacks[0]
   310  			status := *stack.StackStatus
   311  			log.Printf("[DEBUG] Current CloudFormation stack status: %q", status)
   312  
   313  			if status == "UPDATE_ROLLBACK_COMPLETE" {
   314  				failures, err := getCloudFormationFailures(stack.StackName, *lastUpdatedTime, conn)
   315  				if err != nil {
   316  					return resp, "", fmt.Errorf(
   317  						"Failed getting details about rollback: %q", err.Error())
   318  				}
   319  
   320  				return resp, "", fmt.Errorf(
   321  					"UPDATE_ROLLBACK_COMPLETE:\n%q", failures)
   322  			}
   323  
   324  			return resp, status, err
   325  		},
   326  	}
   327  
   328  	_, err = wait.WaitForState()
   329  	if err != nil {
   330  		return err
   331  	}
   332  
   333  	log.Printf("[DEBUG] CloudFormation stack %q has been updated", *stack.StackId)
   334  
   335  	return resourceAwsCloudFormationStackRead(d, meta)
   336  }
   337  
   338  func resourceAwsCloudFormationStackDelete(d *schema.ResourceData, meta interface{}) error {
   339  	conn := meta.(*AWSClient).cfconn
   340  
   341  	input := &cloudformation.DeleteStackInput{
   342  		StackName: aws.String(d.Get("name").(string)),
   343  	}
   344  	log.Printf("[DEBUG] Deleting CloudFormation stack %s", input)
   345  	_, err := conn.DeleteStack(input)
   346  	if err != nil {
   347  		awsErr, ok := err.(awserr.Error)
   348  		if !ok {
   349  			return err
   350  		}
   351  
   352  		if awsErr.Code() == "ValidationError" {
   353  			// Ignore stack which has been already deleted
   354  			return nil
   355  		}
   356  		return err
   357  	}
   358  
   359  	wait := resource.StateChangeConf{
   360  		Pending:    []string{"DELETE_IN_PROGRESS", "ROLLBACK_IN_PROGRESS"},
   361  		Target:     "DELETE_COMPLETE",
   362  		Timeout:    30 * time.Minute,
   363  		MinTimeout: 5 * time.Second,
   364  		Refresh: func() (interface{}, string, error) {
   365  			resp, err := conn.DescribeStacks(&cloudformation.DescribeStacksInput{
   366  				StackName: aws.String(d.Get("name").(string)),
   367  			})
   368  
   369  			if err != nil {
   370  				awsErr, ok := err.(awserr.Error)
   371  				if !ok {
   372  					return resp, "DELETE_FAILED", err
   373  				}
   374  
   375  				log.Printf("[DEBUG] Error when deleting CloudFormation stack: %s: %s",
   376  					awsErr.Code(), awsErr.Message())
   377  
   378  				if awsErr.Code() == "ValidationError" {
   379  					return resp, "DELETE_COMPLETE", nil
   380  				}
   381  			}
   382  
   383  			if len(resp.Stacks) == 0 {
   384  				log.Printf("[DEBUG] CloudFormation stack %q is already gone", d.Get("name"))
   385  				return resp, "DELETE_COMPLETE", nil
   386  			}
   387  
   388  			status := *resp.Stacks[0].StackStatus
   389  			log.Printf("[DEBUG] Current CloudFormation stack status: %q", status)
   390  
   391  			return resp, status, err
   392  		},
   393  	}
   394  
   395  	_, err = wait.WaitForState()
   396  	if err != nil {
   397  		return err
   398  	}
   399  
   400  	log.Printf("[DEBUG] CloudFormation stack %q has been deleted", d.Id())
   401  
   402  	d.SetId("")
   403  
   404  	return nil
   405  }
   406  
   407  // getLastCfEventTimestamp takes the first event in a list
   408  // of events ordered from the newest to the oldest
   409  // and extracts timestamp from it
   410  // LastUpdatedTime only provides last >successful< updated time
   411  func getLastCfEventTimestamp(stackName string, conn *cloudformation.CloudFormation) (
   412  	*time.Time, error) {
   413  	output, err := conn.DescribeStackEvents(&cloudformation.DescribeStackEventsInput{
   414  		StackName: aws.String(stackName),
   415  	})
   416  	if err != nil {
   417  		return nil, err
   418  	}
   419  
   420  	return output.StackEvents[0].Timestamp, nil
   421  }
   422  
   423  // getCloudFormationFailures returns ResourceStatusReason(s)
   424  // of events that should be failures based on regexp match of status
   425  func getCloudFormationFailures(stackName *string, afterTime time.Time,
   426  	conn *cloudformation.CloudFormation) ([]string, error) {
   427  	var failures []string
   428  	// Only catching failures from last 100 events
   429  	// Some extra iteration logic via NextToken could be added
   430  	// but in reality it's nearly impossible to generate >100
   431  	// events by a single stack update
   432  	events, err := conn.DescribeStackEvents(&cloudformation.DescribeStackEventsInput{
   433  		StackName: stackName,
   434  	})
   435  
   436  	if err != nil {
   437  		return nil, err
   438  	}
   439  
   440  	failRe := regexp.MustCompile("_FAILED$")
   441  	rollbackRe := regexp.MustCompile("^ROLLBACK_")
   442  
   443  	for _, e := range events.StackEvents {
   444  		if (failRe.MatchString(*e.ResourceStatus) || rollbackRe.MatchString(*e.ResourceStatus)) &&
   445  			e.Timestamp.After(afterTime) && e.ResourceStatusReason != nil {
   446  			failures = append(failures, *e.ResourceStatusReason)
   447  		}
   448  	}
   449  
   450  	return failures, nil
   451  }