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