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