github.com/daveadams/terraform@v0.6.4-0.20160830094355-13ce74975936/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 149 wait := resource.StateChangeConf{ 150 Pending: []string{"CREATE_IN_PROGRESS", "ROLLBACK_IN_PROGRESS", "ROLLBACK_COMPLETE"}, 151 Target: []string{"CREATE_COMPLETE"}, 152 Timeout: time.Duration(retryTimeout) * time.Minute, 153 MinTimeout: 5 * time.Second, 154 Refresh: func() (interface{}, string, error) { 155 resp, err := conn.DescribeStacks(&cloudformation.DescribeStacksInput{ 156 StackName: aws.String(d.Get("name").(string)), 157 }) 158 status := *resp.Stacks[0].StackStatus 159 log.Printf("[DEBUG] Current CloudFormation stack status: %q", status) 160 161 if status == "ROLLBACK_COMPLETE" { 162 stack := resp.Stacks[0] 163 failures, err := getCloudFormationFailures(stack.StackName, *stack.CreationTime, conn) 164 if err != nil { 165 return resp, "", fmt.Errorf( 166 "Failed getting details about rollback: %q", err.Error()) 167 } 168 169 return resp, "", fmt.Errorf("ROLLBACK_COMPLETE:\n%q", failures) 170 } 171 return resp, status, err 172 }, 173 } 174 175 _, err = wait.WaitForState() 176 if err != nil { 177 return err 178 } 179 180 log.Printf("[INFO] CloudFormation Stack %q created", d.Get("name").(string)) 181 182 return resourceAwsCloudFormationStackRead(d, meta) 183 } 184 185 func resourceAwsCloudFormationStackRead(d *schema.ResourceData, meta interface{}) error { 186 conn := meta.(*AWSClient).cfconn 187 stackName := d.Get("name").(string) 188 189 input := &cloudformation.DescribeStacksInput{ 190 StackName: aws.String(stackName), 191 } 192 resp, err := conn.DescribeStacks(input) 193 if err != nil { 194 return err 195 } 196 197 stacks := resp.Stacks 198 if len(stacks) < 1 { 199 log.Printf("[DEBUG] Removing CloudFormation stack %s as it's already gone", d.Id()) 200 d.SetId("") 201 return nil 202 } 203 for _, s := range stacks { 204 if *s.StackId == d.Id() && *s.StackStatus == "DELETE_COMPLETE" { 205 log.Printf("[DEBUG] Removing CloudFormation stack %s"+ 206 " as it has been already deleted", d.Id()) 207 d.SetId("") 208 return nil 209 } 210 } 211 212 tInput := cloudformation.GetTemplateInput{ 213 StackName: aws.String(stackName), 214 } 215 out, err := conn.GetTemplate(&tInput) 216 if err != nil { 217 return err 218 } 219 220 d.Set("template_body", normalizeJson(*out.TemplateBody)) 221 222 stack := stacks[0] 223 log.Printf("[DEBUG] Received CloudFormation stack: %s", stack) 224 225 d.Set("name", stack.StackName) 226 d.Set("arn", stack.StackId) 227 228 if stack.TimeoutInMinutes != nil { 229 d.Set("timeout_in_minutes", int(*stack.TimeoutInMinutes)) 230 } 231 if stack.Description != nil { 232 d.Set("description", stack.Description) 233 } 234 if stack.DisableRollback != nil { 235 d.Set("disable_rollback", stack.DisableRollback) 236 } 237 if len(stack.NotificationARNs) > 0 { 238 err = d.Set("notification_arns", schema.NewSet(schema.HashString, flattenStringList(stack.NotificationARNs))) 239 if err != nil { 240 return err 241 } 242 } 243 244 originalParams := d.Get("parameters").(map[string]interface{}) 245 err = d.Set("parameters", flattenCloudFormationParameters(stack.Parameters, originalParams)) 246 if err != nil { 247 return err 248 } 249 250 err = d.Set("tags", flattenCloudFormationTags(stack.Tags)) 251 if err != nil { 252 return err 253 } 254 255 err = d.Set("outputs", flattenCloudFormationOutputs(stack.Outputs)) 256 if err != nil { 257 return err 258 } 259 260 if len(stack.Capabilities) > 0 { 261 err = d.Set("capabilities", schema.NewSet(schema.HashString, flattenStringList(stack.Capabilities))) 262 if err != nil { 263 return err 264 } 265 } 266 267 return nil 268 } 269 270 func resourceAwsCloudFormationStackUpdate(d *schema.ResourceData, meta interface{}) error { 271 retryTimeout := int64(30) 272 conn := meta.(*AWSClient).cfconn 273 274 input := &cloudformation.UpdateStackInput{ 275 StackName: aws.String(d.Get("name").(string)), 276 } 277 278 // Either TemplateBody, TemplateURL or UsePreviousTemplate are required 279 if v, ok := d.GetOk("template_url"); ok { 280 input.TemplateURL = aws.String(v.(string)) 281 } 282 if v, ok := d.GetOk("template_body"); ok && input.TemplateURL == nil { 283 input.TemplateBody = aws.String(normalizeJson(v.(string))) 284 } 285 286 // Capabilities must be present whether they are changed or not 287 if v, ok := d.GetOk("capabilities"); ok { 288 input.Capabilities = expandStringList(v.(*schema.Set).List()) 289 } 290 291 if d.HasChange("notification_arns") { 292 input.NotificationARNs = expandStringList(d.Get("notification_arns").(*schema.Set).List()) 293 } 294 295 // Parameters must be present whether they are changed or not 296 if v, ok := d.GetOk("parameters"); ok { 297 input.Parameters = expandCloudFormationParameters(v.(map[string]interface{})) 298 } 299 300 if d.HasChange("policy_body") { 301 input.StackPolicyBody = aws.String(normalizeJson(d.Get("policy_body").(string))) 302 } 303 if d.HasChange("policy_url") { 304 input.StackPolicyURL = aws.String(d.Get("policy_url").(string)) 305 } 306 307 log.Printf("[DEBUG] Updating CloudFormation stack: %s", input) 308 stack, err := conn.UpdateStack(input) 309 if err != nil { 310 return err 311 } 312 313 lastUpdatedTime, err := getLastCfEventTimestamp(d.Get("name").(string), conn) 314 if err != nil { 315 return err 316 } 317 318 if v, ok := d.GetOk("timeout_in_minutes"); ok { 319 m := int64(v.(int)) 320 if m > retryTimeout { 321 retryTimeout = m + 5 322 log.Printf("[DEBUG] CloudFormation timeout: %d", retryTimeout) 323 } 324 } 325 wait := resource.StateChangeConf{ 326 Pending: []string{ 327 "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", 328 "UPDATE_IN_PROGRESS", 329 "UPDATE_ROLLBACK_IN_PROGRESS", 330 "UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS", 331 "UPDATE_ROLLBACK_COMPLETE", 332 }, 333 Target: []string{"UPDATE_COMPLETE"}, 334 Timeout: time.Duration(retryTimeout) * time.Minute, 335 MinTimeout: 5 * time.Second, 336 Refresh: func() (interface{}, string, error) { 337 resp, err := conn.DescribeStacks(&cloudformation.DescribeStacksInput{ 338 StackName: aws.String(d.Get("name").(string)), 339 }) 340 stack := resp.Stacks[0] 341 status := *stack.StackStatus 342 log.Printf("[DEBUG] Current CloudFormation stack status: %q", status) 343 344 if status == "UPDATE_ROLLBACK_COMPLETE" { 345 failures, err := getCloudFormationFailures(stack.StackName, *lastUpdatedTime, conn) 346 if err != nil { 347 return resp, "", fmt.Errorf( 348 "Failed getting details about rollback: %q", err.Error()) 349 } 350 351 return resp, "", fmt.Errorf( 352 "UPDATE_ROLLBACK_COMPLETE:\n%q", failures) 353 } 354 355 return resp, status, err 356 }, 357 } 358 359 _, err = wait.WaitForState() 360 if err != nil { 361 return err 362 } 363 364 log.Printf("[DEBUG] CloudFormation stack %q has been updated", *stack.StackId) 365 366 return resourceAwsCloudFormationStackRead(d, meta) 367 } 368 369 func resourceAwsCloudFormationStackDelete(d *schema.ResourceData, meta interface{}) error { 370 conn := meta.(*AWSClient).cfconn 371 372 input := &cloudformation.DeleteStackInput{ 373 StackName: aws.String(d.Get("name").(string)), 374 } 375 log.Printf("[DEBUG] Deleting CloudFormation stack %s", input) 376 _, err := conn.DeleteStack(input) 377 if err != nil { 378 awsErr, ok := err.(awserr.Error) 379 if !ok { 380 return err 381 } 382 383 if awsErr.Code() == "ValidationError" { 384 // Ignore stack which has been already deleted 385 return nil 386 } 387 return err 388 } 389 390 wait := resource.StateChangeConf{ 391 Pending: []string{"DELETE_IN_PROGRESS", "ROLLBACK_IN_PROGRESS"}, 392 Target: []string{"DELETE_COMPLETE"}, 393 Timeout: 30 * time.Minute, 394 MinTimeout: 5 * time.Second, 395 Refresh: func() (interface{}, string, error) { 396 resp, err := conn.DescribeStacks(&cloudformation.DescribeStacksInput{ 397 StackName: aws.String(d.Get("name").(string)), 398 }) 399 400 if err != nil { 401 awsErr, ok := err.(awserr.Error) 402 if !ok { 403 return resp, "DELETE_FAILED", err 404 } 405 406 log.Printf("[DEBUG] Error when deleting CloudFormation stack: %s: %s", 407 awsErr.Code(), awsErr.Message()) 408 409 if awsErr.Code() == "ValidationError" { 410 return resp, "DELETE_COMPLETE", nil 411 } 412 } 413 414 if len(resp.Stacks) == 0 { 415 log.Printf("[DEBUG] CloudFormation stack %q is already gone", d.Get("name")) 416 return resp, "DELETE_COMPLETE", nil 417 } 418 419 status := *resp.Stacks[0].StackStatus 420 log.Printf("[DEBUG] Current CloudFormation stack status: %q", status) 421 422 return resp, status, err 423 }, 424 } 425 426 _, err = wait.WaitForState() 427 if err != nil { 428 return err 429 } 430 431 log.Printf("[DEBUG] CloudFormation stack %q has been deleted", d.Id()) 432 433 d.SetId("") 434 435 return nil 436 } 437 438 // getLastCfEventTimestamp takes the first event in a list 439 // of events ordered from the newest to the oldest 440 // and extracts timestamp from it 441 // LastUpdatedTime only provides last >successful< updated time 442 func getLastCfEventTimestamp(stackName string, conn *cloudformation.CloudFormation) ( 443 *time.Time, error) { 444 output, err := conn.DescribeStackEvents(&cloudformation.DescribeStackEventsInput{ 445 StackName: aws.String(stackName), 446 }) 447 if err != nil { 448 return nil, err 449 } 450 451 return output.StackEvents[0].Timestamp, nil 452 } 453 454 // getCloudFormationFailures returns ResourceStatusReason(s) 455 // of events that should be failures based on regexp match of status 456 func getCloudFormationFailures(stackName *string, afterTime time.Time, 457 conn *cloudformation.CloudFormation) ([]string, error) { 458 var failures []string 459 // Only catching failures from last 100 events 460 // Some extra iteration logic via NextToken could be added 461 // but in reality it's nearly impossible to generate >100 462 // events by a single stack update 463 events, err := conn.DescribeStackEvents(&cloudformation.DescribeStackEventsInput{ 464 StackName: stackName, 465 }) 466 467 if err != nil { 468 return nil, err 469 } 470 471 failRe := regexp.MustCompile("_FAILED$") 472 rollbackRe := regexp.MustCompile("^ROLLBACK_") 473 474 for _, e := range events.StackEvents { 475 if (failRe.MatchString(*e.ResourceStatus) || rollbackRe.MatchString(*e.ResourceStatus)) && 476 e.Timestamp.After(afterTime) && e.ResourceStatusReason != nil { 477 failures = append(failures, *e.ResourceStatusReason) 478 } 479 } 480 481 return failures, nil 482 }