github.com/koding/terraform@v0.6.4-0.20170608090606-5d7e0339779d/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 _, err := conn.UpdateStack(input) 402 if err != nil { 403 awsErr, ok := err.(awserr.Error) 404 // ValidationError: No updates are to be performed. 405 if !ok || 406 awsErr.Code() != "ValidationError" || 407 awsErr.Message() != "No updates are to be performed." { 408 return err 409 } 410 411 log.Printf("[DEBUG] Current CloudFormation stack has no updates") 412 } 413 414 lastUpdatedTime, err := getLastCfEventTimestamp(d.Id(), conn) 415 if err != nil { 416 return err 417 } 418 419 if v, ok := d.GetOk("timeout_in_minutes"); ok { 420 m := int64(v.(int)) 421 if m > retryTimeout { 422 retryTimeout = m + 5 423 log.Printf("[DEBUG] CloudFormation timeout: %d", retryTimeout) 424 } 425 } 426 var lastStatus string 427 var stackId string 428 wait := resource.StateChangeConf{ 429 Pending: []string{ 430 "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", 431 "UPDATE_IN_PROGRESS", 432 "UPDATE_ROLLBACK_IN_PROGRESS", 433 "UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS", 434 }, 435 Target: []string{ 436 "CREATE_COMPLETE", // If no stack update was performed 437 "UPDATE_COMPLETE", 438 "UPDATE_ROLLBACK_COMPLETE", 439 "UPDATE_ROLLBACK_FAILED", 440 }, 441 Timeout: time.Duration(retryTimeout) * time.Minute, 442 MinTimeout: 5 * time.Second, 443 Refresh: func() (interface{}, string, error) { 444 resp, err := conn.DescribeStacks(&cloudformation.DescribeStacksInput{ 445 StackName: aws.String(d.Id()), 446 }) 447 if err != nil { 448 log.Printf("[ERROR] Failed to describe stacks: %s", err) 449 return nil, "", err 450 } 451 452 stackId = aws.StringValue(resp.Stacks[0].StackId) 453 454 status := *resp.Stacks[0].StackStatus 455 lastStatus = status 456 log.Printf("[DEBUG] Current CloudFormation stack status: %q", status) 457 458 return resp, status, err 459 }, 460 } 461 462 _, err = wait.WaitForState() 463 if err != nil { 464 return err 465 } 466 467 if lastStatus == "UPDATE_ROLLBACK_COMPLETE" || lastStatus == "UPDATE_ROLLBACK_FAILED" { 468 reasons, err := getCloudFormationRollbackReasons(stackId, lastUpdatedTime, conn) 469 if err != nil { 470 return fmt.Errorf("Failed getting details about rollback: %q", err.Error()) 471 } 472 473 return fmt.Errorf("%s: %q", lastStatus, reasons) 474 } 475 476 log.Printf("[DEBUG] CloudFormation stack %q has been updated", stackId) 477 478 return resourceAwsCloudFormationStackRead(d, meta) 479 } 480 481 func resourceAwsCloudFormationStackDelete(d *schema.ResourceData, meta interface{}) error { 482 conn := meta.(*AWSClient).cfconn 483 484 input := &cloudformation.DeleteStackInput{ 485 StackName: aws.String(d.Id()), 486 } 487 log.Printf("[DEBUG] Deleting CloudFormation stack %s", input) 488 _, err := conn.DeleteStack(input) 489 if err != nil { 490 awsErr, ok := err.(awserr.Error) 491 if !ok { 492 return err 493 } 494 495 if awsErr.Code() == "ValidationError" { 496 // Ignore stack which has been already deleted 497 return nil 498 } 499 return err 500 } 501 var lastStatus string 502 wait := resource.StateChangeConf{ 503 Pending: []string{ 504 "DELETE_IN_PROGRESS", 505 "ROLLBACK_IN_PROGRESS", 506 }, 507 Target: []string{ 508 "DELETE_COMPLETE", 509 "DELETE_FAILED", 510 }, 511 Timeout: 30 * time.Minute, 512 MinTimeout: 5 * time.Second, 513 Refresh: func() (interface{}, string, error) { 514 resp, err := conn.DescribeStacks(&cloudformation.DescribeStacksInput{ 515 StackName: aws.String(d.Id()), 516 }) 517 if err != nil { 518 awsErr, ok := err.(awserr.Error) 519 if !ok { 520 return nil, "", err 521 } 522 523 log.Printf("[DEBUG] Error when deleting CloudFormation stack: %s: %s", 524 awsErr.Code(), awsErr.Message()) 525 526 // ValidationError: Stack with id % does not exist 527 if awsErr.Code() == "ValidationError" { 528 return resp, "DELETE_COMPLETE", nil 529 } 530 return nil, "", err 531 } 532 533 if len(resp.Stacks) == 0 { 534 log.Printf("[DEBUG] CloudFormation stack %q is already gone", d.Id()) 535 return resp, "DELETE_COMPLETE", nil 536 } 537 538 status := *resp.Stacks[0].StackStatus 539 lastStatus = status 540 log.Printf("[DEBUG] Current CloudFormation stack status: %q", status) 541 542 return resp, status, err 543 }, 544 } 545 546 _, err = wait.WaitForState() 547 if err != nil { 548 return err 549 } 550 551 if lastStatus == "DELETE_FAILED" { 552 reasons, err := getCloudFormationFailures(d.Id(), conn) 553 if err != nil { 554 return fmt.Errorf("Failed getting reasons of failure: %q", err.Error()) 555 } 556 557 return fmt.Errorf("%s: %q", lastStatus, reasons) 558 } 559 560 log.Printf("[DEBUG] CloudFormation stack %q has been deleted", d.Id()) 561 562 d.SetId("") 563 564 return nil 565 } 566 567 // getLastCfEventTimestamp takes the first event in a list 568 // of events ordered from the newest to the oldest 569 // and extracts timestamp from it 570 // LastUpdatedTime only provides last >successful< updated time 571 func getLastCfEventTimestamp(stackName string, conn *cloudformation.CloudFormation) ( 572 *time.Time, error) { 573 output, err := conn.DescribeStackEvents(&cloudformation.DescribeStackEventsInput{ 574 StackName: aws.String(stackName), 575 }) 576 if err != nil { 577 return nil, err 578 } 579 580 return output.StackEvents[0].Timestamp, nil 581 } 582 583 func getCloudFormationRollbackReasons(stackId string, afterTime *time.Time, conn *cloudformation.CloudFormation) ([]string, error) { 584 var failures []string 585 586 err := conn.DescribeStackEventsPages(&cloudformation.DescribeStackEventsInput{ 587 StackName: aws.String(stackId), 588 }, func(page *cloudformation.DescribeStackEventsOutput, lastPage bool) bool { 589 for _, e := range page.StackEvents { 590 if afterTime != nil && !e.Timestamp.After(*afterTime) { 591 continue 592 } 593 594 if cfStackEventIsFailure(e) || cfStackEventIsRollback(e) { 595 failures = append(failures, *e.ResourceStatusReason) 596 } 597 } 598 return !lastPage 599 }) 600 601 return failures, err 602 } 603 604 func getCloudFormationDeletionReasons(stackId string, conn *cloudformation.CloudFormation) ([]string, error) { 605 var failures []string 606 607 err := conn.DescribeStackEventsPages(&cloudformation.DescribeStackEventsInput{ 608 StackName: aws.String(stackId), 609 }, func(page *cloudformation.DescribeStackEventsOutput, lastPage bool) bool { 610 for _, e := range page.StackEvents { 611 if cfStackEventIsFailure(e) || cfStackEventIsStackDeletion(e) { 612 failures = append(failures, *e.ResourceStatusReason) 613 } 614 } 615 return !lastPage 616 }) 617 618 return failures, err 619 } 620 621 func getCloudFormationFailures(stackId string, conn *cloudformation.CloudFormation) ([]string, error) { 622 var failures []string 623 624 err := conn.DescribeStackEventsPages(&cloudformation.DescribeStackEventsInput{ 625 StackName: aws.String(stackId), 626 }, func(page *cloudformation.DescribeStackEventsOutput, lastPage bool) bool { 627 for _, e := range page.StackEvents { 628 if cfStackEventIsFailure(e) { 629 failures = append(failures, *e.ResourceStatusReason) 630 } 631 } 632 return !lastPage 633 }) 634 635 return failures, err 636 } 637 638 func cfStackEventIsFailure(event *cloudformation.StackEvent) bool { 639 failRe := regexp.MustCompile("_FAILED$") 640 return failRe.MatchString(*event.ResourceStatus) && event.ResourceStatusReason != nil 641 } 642 643 func cfStackEventIsRollback(event *cloudformation.StackEvent) bool { 644 rollbackRe := regexp.MustCompile("^ROLLBACK_") 645 return rollbackRe.MatchString(*event.ResourceStatus) && event.ResourceStatusReason != nil 646 } 647 648 func cfStackEventIsStackDeletion(event *cloudformation.StackEvent) bool { 649 return *event.ResourceStatus == "DELETE_IN_PROGRESS" && 650 *event.ResourceType == "AWS::CloudFormation::Stack" && 651 event.ResourceStatusReason != nil 652 }