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