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