github.com/vtorhonen/terraform@v0.9.0-beta2.0.20170307220345-5d894e4ffda7/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/aws/aws-sdk-go/aws"
    10  	"github.com/aws/aws-sdk-go/aws/awserr"
    11  	"github.com/aws/aws-sdk-go/service/cloudformation"
    12  	"github.com/hashicorp/errwrap"
    13  	"github.com/hashicorp/terraform/helper/resource"
    14  	"github.com/hashicorp/terraform/helper/schema"
    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  				ValidateFunc: validateCloudFormationTemplate,
    35  				StateFunc: func(v interface{}) string {
    36  					template, _ := normalizeCloudFormationTemplate(v)
    37  					return template
    38  				},
    39  			},
    40  			"template_url": &schema.Schema{
    41  				Type:     schema.TypeString,
    42  				Optional: true,
    43  			},
    44  			"capabilities": &schema.Schema{
    45  				Type:     schema.TypeSet,
    46  				Optional: true,
    47  				Elem:     &schema.Schema{Type: schema.TypeString},
    48  				Set:      schema.HashString,
    49  			},
    50  			"disable_rollback": &schema.Schema{
    51  				Type:     schema.TypeBool,
    52  				Optional: true,
    53  				ForceNew: true,
    54  			},
    55  			"notification_arns": &schema.Schema{
    56  				Type:     schema.TypeSet,
    57  				Optional: true,
    58  				Elem:     &schema.Schema{Type: schema.TypeString},
    59  				Set:      schema.HashString,
    60  			},
    61  			"on_failure": &schema.Schema{
    62  				Type:     schema.TypeString,
    63  				Optional: true,
    64  				ForceNew: true,
    65  			},
    66  			"parameters": &schema.Schema{
    67  				Type:     schema.TypeMap,
    68  				Optional: true,
    69  				Computed: true,
    70  			},
    71  			"outputs": &schema.Schema{
    72  				Type:     schema.TypeMap,
    73  				Computed: true,
    74  			},
    75  			"policy_body": &schema.Schema{
    76  				Type:         schema.TypeString,
    77  				Optional:     true,
    78  				Computed:     true,
    79  				ValidateFunc: validateJsonString,
    80  				StateFunc: func(v interface{}) string {
    81  					json, _ := normalizeJsonString(v)
    82  					return json
    83  				},
    84  			},
    85  			"policy_url": &schema.Schema{
    86  				Type:     schema.TypeString,
    87  				Optional: true,
    88  			},
    89  			"timeout_in_minutes": &schema.Schema{
    90  				Type:     schema.TypeInt,
    91  				Optional: true,
    92  				ForceNew: true,
    93  			},
    94  			"tags": &schema.Schema{
    95  				Type:     schema.TypeMap,
    96  				Optional: true,
    97  				ForceNew: true,
    98  			},
    99  		},
   100  	}
   101  }
   102  
   103  func resourceAwsCloudFormationStackCreate(d *schema.ResourceData, meta interface{}) error {
   104  	retryTimeout := int64(30)
   105  	conn := meta.(*AWSClient).cfconn
   106  
   107  	input := cloudformation.CreateStackInput{
   108  		StackName: aws.String(d.Get("name").(string)),
   109  	}
   110  	if v, ok := d.GetOk("template_body"); ok {
   111  		template, err := normalizeCloudFormationTemplate(v)
   112  		if err != nil {
   113  			return errwrap.Wrapf("template body contains an invalid JSON or YAML: {{err}}", err)
   114  		}
   115  		input.TemplateBody = aws.String(template)
   116  	}
   117  	if v, ok := d.GetOk("template_url"); ok {
   118  		input.TemplateURL = aws.String(v.(string))
   119  	}
   120  	if v, ok := d.GetOk("capabilities"); ok {
   121  		input.Capabilities = expandStringList(v.(*schema.Set).List())
   122  	}
   123  	if v, ok := d.GetOk("disable_rollback"); ok {
   124  		input.DisableRollback = aws.Bool(v.(bool))
   125  	}
   126  	if v, ok := d.GetOk("notification_arns"); ok {
   127  		input.NotificationARNs = expandStringList(v.(*schema.Set).List())
   128  	}
   129  	if v, ok := d.GetOk("on_failure"); ok {
   130  		input.OnFailure = aws.String(v.(string))
   131  	}
   132  	if v, ok := d.GetOk("parameters"); ok {
   133  		input.Parameters = expandCloudFormationParameters(v.(map[string]interface{}))
   134  	}
   135  	if v, ok := d.GetOk("policy_body"); ok {
   136  		policy, err := normalizeJsonString(v)
   137  		if err != nil {
   138  			return errwrap.Wrapf("policy body contains an invalid JSON: {{err}}", err)
   139  		}
   140  		input.StackPolicyBody = aws.String(policy)
   141  	}
   142  	if v, ok := d.GetOk("policy_url"); ok {
   143  		input.StackPolicyURL = aws.String(v.(string))
   144  	}
   145  	if v, ok := d.GetOk("tags"); ok {
   146  		input.Tags = expandCloudFormationTags(v.(map[string]interface{}))
   147  	}
   148  	if v, ok := d.GetOk("timeout_in_minutes"); ok {
   149  		m := int64(v.(int))
   150  		input.TimeoutInMinutes = aws.Int64(m)
   151  		if m > retryTimeout {
   152  			retryTimeout = m + 5
   153  			log.Printf("[DEBUG] CloudFormation timeout: %d", retryTimeout)
   154  		}
   155  	}
   156  
   157  	log.Printf("[DEBUG] Creating CloudFormation Stack: %s", input)
   158  	resp, err := conn.CreateStack(&input)
   159  	if err != nil {
   160  		return fmt.Errorf("Creating CloudFormation stack failed: %s", err.Error())
   161  	}
   162  
   163  	d.SetId(*resp.StackId)
   164  	var lastStatus string
   165  
   166  	wait := resource.StateChangeConf{
   167  		Pending: []string{
   168  			"CREATE_IN_PROGRESS",
   169  			"DELETE_IN_PROGRESS",
   170  			"ROLLBACK_IN_PROGRESS",
   171  		},
   172  		Target: []string{
   173  			"CREATE_COMPLETE",
   174  			"CREATE_FAILED",
   175  			"DELETE_COMPLETE",
   176  			"DELETE_FAILED",
   177  			"ROLLBACK_COMPLETE",
   178  			"ROLLBACK_FAILED",
   179  		},
   180  		Timeout:    time.Duration(retryTimeout) * time.Minute,
   181  		MinTimeout: 1 * time.Second,
   182  		Refresh: func() (interface{}, string, error) {
   183  			resp, err := conn.DescribeStacks(&cloudformation.DescribeStacksInput{
   184  				StackName: aws.String(d.Id()),
   185  			})
   186  			if err != nil {
   187  				log.Printf("[ERROR] Failed to describe stacks: %s", err)
   188  				return nil, "", err
   189  			}
   190  			if len(resp.Stacks) == 0 {
   191  				// This shouldn't happen unless CloudFormation is inconsistent
   192  				// See https://github.com/hashicorp/terraform/issues/5487
   193  				log.Printf("[WARN] CloudFormation stack %q not found.\nresponse: %q",
   194  					d.Id(), resp)
   195  				return resp, "", fmt.Errorf(
   196  					"CloudFormation stack %q vanished unexpectedly during creation.\n"+
   197  						"Unless you knowingly manually deleted the stack "+
   198  						"please report this as bug at https://github.com/hashicorp/terraform/issues\n"+
   199  						"along with the config & Terraform version & the details below:\n"+
   200  						"Full API response: %s\n",
   201  					d.Id(), resp)
   202  			}
   203  
   204  			status := *resp.Stacks[0].StackStatus
   205  			lastStatus = status
   206  			log.Printf("[DEBUG] Current CloudFormation stack status: %q", status)
   207  
   208  			return resp, status, err
   209  		},
   210  	}
   211  
   212  	_, err = wait.WaitForState()
   213  	if err != nil {
   214  		return err
   215  	}
   216  
   217  	if lastStatus == "ROLLBACK_COMPLETE" || lastStatus == "ROLLBACK_FAILED" {
   218  		reasons, err := getCloudFormationRollbackReasons(d.Id(), nil, conn)
   219  		if err != nil {
   220  			return fmt.Errorf("Failed getting rollback reasons: %q", err.Error())
   221  		}
   222  
   223  		return fmt.Errorf("%s: %q", lastStatus, reasons)
   224  	}
   225  	if lastStatus == "DELETE_COMPLETE" || lastStatus == "DELETE_FAILED" {
   226  		reasons, err := getCloudFormationDeletionReasons(d.Id(), conn)
   227  		if err != nil {
   228  			return fmt.Errorf("Failed getting deletion reasons: %q", err.Error())
   229  		}
   230  
   231  		d.SetId("")
   232  		return fmt.Errorf("%s: %q", lastStatus, reasons)
   233  	}
   234  	if lastStatus == "CREATE_FAILED" {
   235  		reasons, err := getCloudFormationFailures(d.Id(), conn)
   236  		if err != nil {
   237  			return fmt.Errorf("Failed getting failure reasons: %q", err.Error())
   238  		}
   239  		return fmt.Errorf("%s: %q", lastStatus, reasons)
   240  	}
   241  
   242  	log.Printf("[INFO] CloudFormation Stack %q created", d.Id())
   243  
   244  	return resourceAwsCloudFormationStackRead(d, meta)
   245  }
   246  
   247  func resourceAwsCloudFormationStackRead(d *schema.ResourceData, meta interface{}) error {
   248  	conn := meta.(*AWSClient).cfconn
   249  
   250  	input := &cloudformation.DescribeStacksInput{
   251  		StackName: aws.String(d.Id()),
   252  	}
   253  	resp, err := conn.DescribeStacks(input)
   254  	if err != nil {
   255  		awsErr, ok := err.(awserr.Error)
   256  		// ValidationError: Stack with id % does not exist
   257  		if ok && awsErr.Code() == "ValidationError" {
   258  			log.Printf("[WARN] Removing CloudFormation stack %s as it's already gone", d.Id())
   259  			d.SetId("")
   260  			return nil
   261  		}
   262  
   263  		return err
   264  	}
   265  
   266  	stacks := resp.Stacks
   267  	if len(stacks) < 1 {
   268  		log.Printf("[WARN] Removing CloudFormation stack %s as it's already gone", d.Id())
   269  		d.SetId("")
   270  		return nil
   271  	}
   272  	for _, s := range stacks {
   273  		if *s.StackId == d.Id() && *s.StackStatus == "DELETE_COMPLETE" {
   274  			log.Printf("[DEBUG] Removing CloudFormation stack %s"+
   275  				" as it has been already deleted", d.Id())
   276  			d.SetId("")
   277  			return nil
   278  		}
   279  	}
   280  
   281  	tInput := cloudformation.GetTemplateInput{
   282  		StackName: aws.String(d.Id()),
   283  	}
   284  	out, err := conn.GetTemplate(&tInput)
   285  	if err != nil {
   286  		return err
   287  	}
   288  
   289  	template, err := normalizeCloudFormationTemplate(*out.TemplateBody)
   290  	if err != nil {
   291  		return errwrap.Wrapf("template body contains an invalid JSON or YAML: {{err}}", err)
   292  	}
   293  	d.Set("template_body", template)
   294  
   295  	stack := stacks[0]
   296  	log.Printf("[DEBUG] Received CloudFormation stack: %s", stack)
   297  
   298  	d.Set("name", stack.StackName)
   299  	d.Set("arn", stack.StackId)
   300  
   301  	if stack.TimeoutInMinutes != nil {
   302  		d.Set("timeout_in_minutes", int(*stack.TimeoutInMinutes))
   303  	}
   304  	if stack.Description != nil {
   305  		d.Set("description", stack.Description)
   306  	}
   307  	if stack.DisableRollback != nil {
   308  		d.Set("disable_rollback", stack.DisableRollback)
   309  	}
   310  	if len(stack.NotificationARNs) > 0 {
   311  		err = d.Set("notification_arns", schema.NewSet(schema.HashString, flattenStringList(stack.NotificationARNs)))
   312  		if err != nil {
   313  			return err
   314  		}
   315  	}
   316  
   317  	originalParams := d.Get("parameters").(map[string]interface{})
   318  	err = d.Set("parameters", flattenCloudFormationParameters(stack.Parameters, originalParams))
   319  	if err != nil {
   320  		return err
   321  	}
   322  
   323  	err = d.Set("tags", flattenCloudFormationTags(stack.Tags))
   324  	if err != nil {
   325  		return err
   326  	}
   327  
   328  	err = d.Set("outputs", flattenCloudFormationOutputs(stack.Outputs))
   329  	if err != nil {
   330  		return err
   331  	}
   332  
   333  	if len(stack.Capabilities) > 0 {
   334  		err = d.Set("capabilities", schema.NewSet(schema.HashString, flattenStringList(stack.Capabilities)))
   335  		if err != nil {
   336  			return err
   337  		}
   338  	}
   339  
   340  	return nil
   341  }
   342  
   343  func resourceAwsCloudFormationStackUpdate(d *schema.ResourceData, meta interface{}) error {
   344  	retryTimeout := int64(30)
   345  	conn := meta.(*AWSClient).cfconn
   346  
   347  	input := &cloudformation.UpdateStackInput{
   348  		StackName: aws.String(d.Id()),
   349  	}
   350  
   351  	// Either TemplateBody, TemplateURL or UsePreviousTemplate are required
   352  	if v, ok := d.GetOk("template_url"); ok {
   353  		input.TemplateURL = aws.String(v.(string))
   354  	}
   355  	if v, ok := d.GetOk("template_body"); ok && input.TemplateURL == nil {
   356  		template, err := normalizeCloudFormationTemplate(v)
   357  		if err != nil {
   358  			return errwrap.Wrapf("template body contains an invalid JSON or YAML: {{err}}", err)
   359  		}
   360  		input.TemplateBody = aws.String(template)
   361  	}
   362  
   363  	// Capabilities must be present whether they are changed or not
   364  	if v, ok := d.GetOk("capabilities"); ok {
   365  		input.Capabilities = expandStringList(v.(*schema.Set).List())
   366  	}
   367  
   368  	if d.HasChange("notification_arns") {
   369  		input.NotificationARNs = expandStringList(d.Get("notification_arns").(*schema.Set).List())
   370  	}
   371  
   372  	// Parameters must be present whether they are changed or not
   373  	if v, ok := d.GetOk("parameters"); ok {
   374  		input.Parameters = expandCloudFormationParameters(v.(map[string]interface{}))
   375  	}
   376  
   377  	if d.HasChange("policy_body") {
   378  		policy, err := normalizeJsonString(d.Get("policy_body"))
   379  		if err != nil {
   380  			return errwrap.Wrapf("policy body contains an invalid JSON: {{err}}", err)
   381  		}
   382  		input.StackPolicyBody = aws.String(policy)
   383  	}
   384  	if d.HasChange("policy_url") {
   385  		input.StackPolicyURL = aws.String(d.Get("policy_url").(string))
   386  	}
   387  
   388  	log.Printf("[DEBUG] Updating CloudFormation stack: %s", input)
   389  	stack, err := conn.UpdateStack(input)
   390  	if err != nil {
   391  		return err
   392  	}
   393  
   394  	lastUpdatedTime, err := getLastCfEventTimestamp(d.Id(), conn)
   395  	if err != nil {
   396  		return err
   397  	}
   398  
   399  	if v, ok := d.GetOk("timeout_in_minutes"); ok {
   400  		m := int64(v.(int))
   401  		if m > retryTimeout {
   402  			retryTimeout = m + 5
   403  			log.Printf("[DEBUG] CloudFormation timeout: %d", retryTimeout)
   404  		}
   405  	}
   406  	var lastStatus string
   407  	wait := resource.StateChangeConf{
   408  		Pending: []string{
   409  			"UPDATE_COMPLETE_CLEANUP_IN_PROGRESS",
   410  			"UPDATE_IN_PROGRESS",
   411  			"UPDATE_ROLLBACK_IN_PROGRESS",
   412  			"UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS",
   413  		},
   414  		Target: []string{
   415  			"UPDATE_COMPLETE",
   416  			"UPDATE_ROLLBACK_COMPLETE",
   417  			"UPDATE_ROLLBACK_FAILED",
   418  		},
   419  		Timeout:    time.Duration(retryTimeout) * time.Minute,
   420  		MinTimeout: 5 * time.Second,
   421  		Refresh: func() (interface{}, string, error) {
   422  			resp, err := conn.DescribeStacks(&cloudformation.DescribeStacksInput{
   423  				StackName: aws.String(d.Id()),
   424  			})
   425  			if err != nil {
   426  				log.Printf("[ERROR] Failed to describe stacks: %s", err)
   427  				return nil, "", err
   428  			}
   429  
   430  			status := *resp.Stacks[0].StackStatus
   431  			lastStatus = status
   432  			log.Printf("[DEBUG] Current CloudFormation stack status: %q", status)
   433  
   434  			return resp, status, err
   435  		},
   436  	}
   437  
   438  	_, err = wait.WaitForState()
   439  	if err != nil {
   440  		return err
   441  	}
   442  
   443  	if lastStatus == "UPDATE_ROLLBACK_COMPLETE" || lastStatus == "UPDATE_ROLLBACK_FAILED" {
   444  		reasons, err := getCloudFormationRollbackReasons(*stack.StackId, lastUpdatedTime, conn)
   445  		if err != nil {
   446  			return fmt.Errorf("Failed getting details about rollback: %q", err.Error())
   447  		}
   448  
   449  		return fmt.Errorf("%s: %q", lastStatus, reasons)
   450  	}
   451  
   452  	log.Printf("[DEBUG] CloudFormation stack %q has been updated", *stack.StackId)
   453  
   454  	return resourceAwsCloudFormationStackRead(d, meta)
   455  }
   456  
   457  func resourceAwsCloudFormationStackDelete(d *schema.ResourceData, meta interface{}) error {
   458  	conn := meta.(*AWSClient).cfconn
   459  
   460  	input := &cloudformation.DeleteStackInput{
   461  		StackName: aws.String(d.Id()),
   462  	}
   463  	log.Printf("[DEBUG] Deleting CloudFormation stack %s", input)
   464  	_, err := conn.DeleteStack(input)
   465  	if err != nil {
   466  		awsErr, ok := err.(awserr.Error)
   467  		if !ok {
   468  			return err
   469  		}
   470  
   471  		if awsErr.Code() == "ValidationError" {
   472  			// Ignore stack which has been already deleted
   473  			return nil
   474  		}
   475  		return err
   476  	}
   477  	var lastStatus string
   478  	wait := resource.StateChangeConf{
   479  		Pending: []string{
   480  			"DELETE_IN_PROGRESS",
   481  			"ROLLBACK_IN_PROGRESS",
   482  		},
   483  		Target: []string{
   484  			"DELETE_COMPLETE",
   485  			"DELETE_FAILED",
   486  		},
   487  		Timeout:    30 * time.Minute,
   488  		MinTimeout: 5 * time.Second,
   489  		Refresh: func() (interface{}, string, error) {
   490  			resp, err := conn.DescribeStacks(&cloudformation.DescribeStacksInput{
   491  				StackName: aws.String(d.Id()),
   492  			})
   493  			if err != nil {
   494  				awsErr, ok := err.(awserr.Error)
   495  				if !ok {
   496  					return nil, "", err
   497  				}
   498  
   499  				log.Printf("[DEBUG] Error when deleting CloudFormation stack: %s: %s",
   500  					awsErr.Code(), awsErr.Message())
   501  
   502  				// ValidationError: Stack with id % does not exist
   503  				if awsErr.Code() == "ValidationError" {
   504  					return resp, "DELETE_COMPLETE", nil
   505  				}
   506  				return nil, "", err
   507  			}
   508  
   509  			if len(resp.Stacks) == 0 {
   510  				log.Printf("[DEBUG] CloudFormation stack %q is already gone", d.Id())
   511  				return resp, "DELETE_COMPLETE", nil
   512  			}
   513  
   514  			status := *resp.Stacks[0].StackStatus
   515  			lastStatus = status
   516  			log.Printf("[DEBUG] Current CloudFormation stack status: %q", status)
   517  
   518  			return resp, status, err
   519  		},
   520  	}
   521  
   522  	_, err = wait.WaitForState()
   523  	if err != nil {
   524  		return err
   525  	}
   526  
   527  	if lastStatus == "DELETE_FAILED" {
   528  		reasons, err := getCloudFormationFailures(d.Id(), conn)
   529  		if err != nil {
   530  			return fmt.Errorf("Failed getting reasons of failure: %q", err.Error())
   531  		}
   532  
   533  		return fmt.Errorf("%s: %q", lastStatus, reasons)
   534  	}
   535  
   536  	log.Printf("[DEBUG] CloudFormation stack %q has been deleted", d.Id())
   537  
   538  	d.SetId("")
   539  
   540  	return nil
   541  }
   542  
   543  // getLastCfEventTimestamp takes the first event in a list
   544  // of events ordered from the newest to the oldest
   545  // and extracts timestamp from it
   546  // LastUpdatedTime only provides last >successful< updated time
   547  func getLastCfEventTimestamp(stackName string, conn *cloudformation.CloudFormation) (
   548  	*time.Time, error) {
   549  	output, err := conn.DescribeStackEvents(&cloudformation.DescribeStackEventsInput{
   550  		StackName: aws.String(stackName),
   551  	})
   552  	if err != nil {
   553  		return nil, err
   554  	}
   555  
   556  	return output.StackEvents[0].Timestamp, nil
   557  }
   558  
   559  func getCloudFormationRollbackReasons(stackId string, afterTime *time.Time, conn *cloudformation.CloudFormation) ([]string, error) {
   560  	var failures []string
   561  
   562  	err := conn.DescribeStackEventsPages(&cloudformation.DescribeStackEventsInput{
   563  		StackName: aws.String(stackId),
   564  	}, func(page *cloudformation.DescribeStackEventsOutput, lastPage bool) bool {
   565  		for _, e := range page.StackEvents {
   566  			if afterTime != nil && !e.Timestamp.After(*afterTime) {
   567  				continue
   568  			}
   569  
   570  			if cfStackEventIsFailure(e) || cfStackEventIsRollback(e) {
   571  				failures = append(failures, *e.ResourceStatusReason)
   572  			}
   573  		}
   574  		return !lastPage
   575  	})
   576  
   577  	return failures, err
   578  }
   579  
   580  func getCloudFormationDeletionReasons(stackId string, conn *cloudformation.CloudFormation) ([]string, error) {
   581  	var failures []string
   582  
   583  	err := conn.DescribeStackEventsPages(&cloudformation.DescribeStackEventsInput{
   584  		StackName: aws.String(stackId),
   585  	}, func(page *cloudformation.DescribeStackEventsOutput, lastPage bool) bool {
   586  		for _, e := range page.StackEvents {
   587  			if cfStackEventIsFailure(e) || cfStackEventIsStackDeletion(e) {
   588  				failures = append(failures, *e.ResourceStatusReason)
   589  			}
   590  		}
   591  		return !lastPage
   592  	})
   593  
   594  	return failures, err
   595  }
   596  
   597  func getCloudFormationFailures(stackId string, conn *cloudformation.CloudFormation) ([]string, error) {
   598  	var failures []string
   599  
   600  	err := conn.DescribeStackEventsPages(&cloudformation.DescribeStackEventsInput{
   601  		StackName: aws.String(stackId),
   602  	}, func(page *cloudformation.DescribeStackEventsOutput, lastPage bool) bool {
   603  		for _, e := range page.StackEvents {
   604  			if cfStackEventIsFailure(e) {
   605  				failures = append(failures, *e.ResourceStatusReason)
   606  			}
   607  		}
   608  		return !lastPage
   609  	})
   610  
   611  	return failures, err
   612  }
   613  
   614  func cfStackEventIsFailure(event *cloudformation.StackEvent) bool {
   615  	failRe := regexp.MustCompile("_FAILED$")
   616  	return failRe.MatchString(*event.ResourceStatus) && event.ResourceStatusReason != nil
   617  }
   618  
   619  func cfStackEventIsRollback(event *cloudformation.StackEvent) bool {
   620  	rollbackRe := regexp.MustCompile("^ROLLBACK_")
   621  	return rollbackRe.MatchString(*event.ResourceStatus) && event.ResourceStatusReason != nil
   622  }
   623  
   624  func cfStackEventIsStackDeletion(event *cloudformation.StackEvent) bool {
   625  	return *event.ResourceStatus == "DELETE_IN_PROGRESS" &&
   626  		*event.ResourceType == "AWS::CloudFormation::Stack" &&
   627  		event.ResourceStatusReason != nil
   628  }