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 }