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