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