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 }