github.com/andresvia/terraform@v0.6.15-0.20160412045437-d51c75946785/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 conn := meta.(*AWSClient).cfconn 272 273 input := &cloudformation.UpdateStackInput{ 274 StackName: aws.String(d.Get("name").(string)), 275 } 276 277 // Either TemplateBody, TemplateURL or UsePreviousTemplate are required 278 if v, ok := d.GetOk("template_url"); ok { 279 input.TemplateURL = aws.String(v.(string)) 280 } 281 if v, ok := d.GetOk("template_body"); ok && input.TemplateURL == nil { 282 input.TemplateBody = aws.String(normalizeJson(v.(string))) 283 } 284 285 // Capabilities must be present whether they are changed or not 286 if v, ok := d.GetOk("capabilities"); ok { 287 input.Capabilities = expandStringList(v.(*schema.Set).List()) 288 } 289 290 if d.HasChange("notification_arns") { 291 input.NotificationARNs = expandStringList(d.Get("notification_arns").(*schema.Set).List()) 292 } 293 294 // Parameters must be present whether they are changed or not 295 if v, ok := d.GetOk("parameters"); ok { 296 input.Parameters = expandCloudFormationParameters(v.(map[string]interface{})) 297 } 298 299 if d.HasChange("policy_body") { 300 input.StackPolicyBody = aws.String(normalizeJson(d.Get("policy_body").(string))) 301 } 302 if d.HasChange("policy_url") { 303 input.StackPolicyURL = aws.String(d.Get("policy_url").(string)) 304 } 305 306 log.Printf("[DEBUG] Updating CloudFormation stack: %s", input) 307 stack, err := conn.UpdateStack(input) 308 if err != nil { 309 return err 310 } 311 312 lastUpdatedTime, err := getLastCfEventTimestamp(d.Get("name").(string), conn) 313 if err != nil { 314 return err 315 } 316 317 wait := resource.StateChangeConf{ 318 Pending: []string{ 319 "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", 320 "UPDATE_IN_PROGRESS", 321 "UPDATE_ROLLBACK_IN_PROGRESS", 322 "UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS", 323 "UPDATE_ROLLBACK_COMPLETE", 324 }, 325 Target: []string{"UPDATE_COMPLETE"}, 326 Timeout: 15 * time.Minute, 327 MinTimeout: 5 * time.Second, 328 Refresh: func() (interface{}, string, error) { 329 resp, err := conn.DescribeStacks(&cloudformation.DescribeStacksInput{ 330 StackName: aws.String(d.Get("name").(string)), 331 }) 332 stack := resp.Stacks[0] 333 status := *stack.StackStatus 334 log.Printf("[DEBUG] Current CloudFormation stack status: %q", status) 335 336 if status == "UPDATE_ROLLBACK_COMPLETE" { 337 failures, err := getCloudFormationFailures(stack.StackName, *lastUpdatedTime, conn) 338 if err != nil { 339 return resp, "", fmt.Errorf( 340 "Failed getting details about rollback: %q", err.Error()) 341 } 342 343 return resp, "", fmt.Errorf( 344 "UPDATE_ROLLBACK_COMPLETE:\n%q", failures) 345 } 346 347 return resp, status, err 348 }, 349 } 350 351 _, err = wait.WaitForState() 352 if err != nil { 353 return err 354 } 355 356 log.Printf("[DEBUG] CloudFormation stack %q has been updated", *stack.StackId) 357 358 return resourceAwsCloudFormationStackRead(d, meta) 359 } 360 361 func resourceAwsCloudFormationStackDelete(d *schema.ResourceData, meta interface{}) error { 362 conn := meta.(*AWSClient).cfconn 363 364 input := &cloudformation.DeleteStackInput{ 365 StackName: aws.String(d.Get("name").(string)), 366 } 367 log.Printf("[DEBUG] Deleting CloudFormation stack %s", input) 368 _, err := conn.DeleteStack(input) 369 if err != nil { 370 awsErr, ok := err.(awserr.Error) 371 if !ok { 372 return err 373 } 374 375 if awsErr.Code() == "ValidationError" { 376 // Ignore stack which has been already deleted 377 return nil 378 } 379 return err 380 } 381 382 wait := resource.StateChangeConf{ 383 Pending: []string{"DELETE_IN_PROGRESS", "ROLLBACK_IN_PROGRESS"}, 384 Target: []string{"DELETE_COMPLETE"}, 385 Timeout: 30 * time.Minute, 386 MinTimeout: 5 * time.Second, 387 Refresh: func() (interface{}, string, error) { 388 resp, err := conn.DescribeStacks(&cloudformation.DescribeStacksInput{ 389 StackName: aws.String(d.Get("name").(string)), 390 }) 391 392 if err != nil { 393 awsErr, ok := err.(awserr.Error) 394 if !ok { 395 return resp, "DELETE_FAILED", err 396 } 397 398 log.Printf("[DEBUG] Error when deleting CloudFormation stack: %s: %s", 399 awsErr.Code(), awsErr.Message()) 400 401 if awsErr.Code() == "ValidationError" { 402 return resp, "DELETE_COMPLETE", nil 403 } 404 } 405 406 if len(resp.Stacks) == 0 { 407 log.Printf("[DEBUG] CloudFormation stack %q is already gone", d.Get("name")) 408 return resp, "DELETE_COMPLETE", nil 409 } 410 411 status := *resp.Stacks[0].StackStatus 412 log.Printf("[DEBUG] Current CloudFormation stack status: %q", status) 413 414 return resp, status, err 415 }, 416 } 417 418 _, err = wait.WaitForState() 419 if err != nil { 420 return err 421 } 422 423 log.Printf("[DEBUG] CloudFormation stack %q has been deleted", d.Id()) 424 425 d.SetId("") 426 427 return nil 428 } 429 430 // getLastCfEventTimestamp takes the first event in a list 431 // of events ordered from the newest to the oldest 432 // and extracts timestamp from it 433 // LastUpdatedTime only provides last >successful< updated time 434 func getLastCfEventTimestamp(stackName string, conn *cloudformation.CloudFormation) ( 435 *time.Time, error) { 436 output, err := conn.DescribeStackEvents(&cloudformation.DescribeStackEventsInput{ 437 StackName: aws.String(stackName), 438 }) 439 if err != nil { 440 return nil, err 441 } 442 443 return output.StackEvents[0].Timestamp, nil 444 } 445 446 // getCloudFormationFailures returns ResourceStatusReason(s) 447 // of events that should be failures based on regexp match of status 448 func getCloudFormationFailures(stackName *string, afterTime time.Time, 449 conn *cloudformation.CloudFormation) ([]string, error) { 450 var failures []string 451 // Only catching failures from last 100 events 452 // Some extra iteration logic via NextToken could be added 453 // but in reality it's nearly impossible to generate >100 454 // events by a single stack update 455 events, err := conn.DescribeStackEvents(&cloudformation.DescribeStackEventsInput{ 456 StackName: stackName, 457 }) 458 459 if err != nil { 460 return nil, err 461 } 462 463 failRe := regexp.MustCompile("_FAILED$") 464 rollbackRe := regexp.MustCompile("^ROLLBACK_") 465 466 for _, e := range events.StackEvents { 467 if (failRe.MatchString(*e.ResourceStatus) || rollbackRe.MatchString(*e.ResourceStatus)) && 468 e.Timestamp.After(afterTime) && e.ResourceStatusReason != nil { 469 failures = append(failures, *e.ResourceStatusReason) 470 } 471 } 472 473 return failures, nil 474 }