github.com/andresvia/terraform@v0.6.15-0.20160412045437-d51c75946785/builtin/providers/aws/resource_aws_elastic_beanstalk_environment.go (about) 1 package aws 2 3 import ( 4 "fmt" 5 "log" 6 "regexp" 7 "sort" 8 "strings" 9 "time" 10 11 "github.com/hashicorp/terraform/helper/hashcode" 12 "github.com/hashicorp/terraform/helper/resource" 13 "github.com/hashicorp/terraform/helper/schema" 14 15 "github.com/aws/aws-sdk-go/aws" 16 "github.com/aws/aws-sdk-go/service/ec2" 17 "github.com/aws/aws-sdk-go/service/elasticbeanstalk" 18 ) 19 20 func resourceAwsElasticBeanstalkOptionSetting() *schema.Resource { 21 return &schema.Resource{ 22 Schema: map[string]*schema.Schema{ 23 "namespace": &schema.Schema{ 24 Type: schema.TypeString, 25 Required: true, 26 }, 27 "name": &schema.Schema{ 28 Type: schema.TypeString, 29 Required: true, 30 }, 31 "value": &schema.Schema{ 32 Type: schema.TypeString, 33 Required: true, 34 }, 35 }, 36 } 37 } 38 39 func resourceAwsElasticBeanstalkEnvironment() *schema.Resource { 40 return &schema.Resource{ 41 Create: resourceAwsElasticBeanstalkEnvironmentCreate, 42 Read: resourceAwsElasticBeanstalkEnvironmentRead, 43 Update: resourceAwsElasticBeanstalkEnvironmentUpdate, 44 Delete: resourceAwsElasticBeanstalkEnvironmentDelete, 45 46 Schema: map[string]*schema.Schema{ 47 "name": &schema.Schema{ 48 Type: schema.TypeString, 49 Required: true, 50 ForceNew: true, 51 }, 52 "application": &schema.Schema{ 53 Type: schema.TypeString, 54 Required: true, 55 }, 56 "description": &schema.Schema{ 57 Type: schema.TypeString, 58 Optional: true, 59 }, 60 "cname": &schema.Schema{ 61 Type: schema.TypeString, 62 Computed: true, 63 }, 64 "cname_prefix": &schema.Schema{ 65 Type: schema.TypeString, 66 Computed: true, 67 Optional: true, 68 ForceNew: true, 69 }, 70 "tier": &schema.Schema{ 71 Type: schema.TypeString, 72 Optional: true, 73 Default: "WebServer", 74 ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) { 75 value := v.(string) 76 switch value { 77 case 78 "Worker", 79 "WebServer": 80 return 81 } 82 errors = append(errors, fmt.Errorf("%s is not a valid tier. Valid options are WebServer or Worker", value)) 83 return 84 }, 85 ForceNew: true, 86 }, 87 "setting": &schema.Schema{ 88 Type: schema.TypeSet, 89 Optional: true, 90 Computed: true, 91 Elem: resourceAwsElasticBeanstalkOptionSetting(), 92 Set: optionSettingValueHash, 93 }, 94 "all_settings": &schema.Schema{ 95 Type: schema.TypeSet, 96 Computed: true, 97 Elem: resourceAwsElasticBeanstalkOptionSetting(), 98 Set: optionSettingValueHash, 99 }, 100 "solution_stack_name": &schema.Schema{ 101 Type: schema.TypeString, 102 Optional: true, 103 }, 104 "template_name": &schema.Schema{ 105 Type: schema.TypeString, 106 Optional: true, 107 ConflictsWith: []string{"solution_stack_name"}, 108 }, 109 "wait_for_ready_timeout": &schema.Schema{ 110 Type: schema.TypeString, 111 Optional: true, 112 Default: "10m", 113 ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) { 114 value := v.(string) 115 duration, err := time.ParseDuration(value) 116 if err != nil { 117 errors = append(errors, fmt.Errorf( 118 "%q cannot be parsed as a duration: %s", k, err)) 119 } 120 if duration < 0 { 121 errors = append(errors, fmt.Errorf( 122 "%q must be greater than zero", k)) 123 } 124 return 125 }, 126 }, 127 "autoscaling_groups": &schema.Schema{ 128 Type: schema.TypeList, 129 Computed: true, 130 Elem: &schema.Schema{Type: schema.TypeString}, 131 }, 132 "instances": &schema.Schema{ 133 Type: schema.TypeList, 134 Computed: true, 135 Elem: &schema.Schema{Type: schema.TypeString}, 136 }, 137 "launch_configurations": &schema.Schema{ 138 Type: schema.TypeList, 139 Computed: true, 140 Elem: &schema.Schema{Type: schema.TypeString}, 141 }, 142 "load_balancers": &schema.Schema{ 143 Type: schema.TypeList, 144 Computed: true, 145 Elem: &schema.Schema{Type: schema.TypeString}, 146 }, 147 "queues": &schema.Schema{ 148 Type: schema.TypeList, 149 Computed: true, 150 Elem: &schema.Schema{Type: schema.TypeString}, 151 }, 152 "triggers": &schema.Schema{ 153 Type: schema.TypeList, 154 Computed: true, 155 Elem: &schema.Schema{Type: schema.TypeString}, 156 }, 157 158 "tags": tagsSchema(), 159 }, 160 } 161 } 162 163 func resourceAwsElasticBeanstalkEnvironmentCreate(d *schema.ResourceData, meta interface{}) error { 164 conn := meta.(*AWSClient).elasticbeanstalkconn 165 166 // Get values from config 167 name := d.Get("name").(string) 168 cnamePrefix := d.Get("cname_prefix").(string) 169 tier := d.Get("tier").(string) 170 app := d.Get("application").(string) 171 desc := d.Get("description").(string) 172 settings := d.Get("setting").(*schema.Set) 173 solutionStack := d.Get("solution_stack_name").(string) 174 templateName := d.Get("template_name").(string) 175 waitForReadyTimeOut, err := time.ParseDuration(d.Get("wait_for_ready_timeout").(string)) 176 if err != nil { 177 return err 178 } 179 180 // TODO set tags 181 // Note: at time of writing, you cannot view or edit Tags after creation 182 // d.Set("tags", tagsToMap(instance.Tags)) 183 createOpts := elasticbeanstalk.CreateEnvironmentInput{ 184 EnvironmentName: aws.String(name), 185 ApplicationName: aws.String(app), 186 OptionSettings: extractOptionSettings(settings), 187 Tags: tagsFromMapBeanstalk(d.Get("tags").(map[string]interface{})), 188 } 189 190 if desc != "" { 191 createOpts.Description = aws.String(desc) 192 } 193 194 if cnamePrefix != "" { 195 if tier != "WebServer" { 196 return fmt.Errorf("Cannont set cname_prefix for tier: %s.", tier) 197 } 198 createOpts.CNAMEPrefix = aws.String(cnamePrefix) 199 } 200 201 if tier != "" { 202 var tierType string 203 204 switch tier { 205 case "WebServer": 206 tierType = "Standard" 207 case "Worker": 208 tierType = "SQS/HTTP" 209 } 210 environmentTier := elasticbeanstalk.EnvironmentTier{ 211 Name: aws.String(tier), 212 Type: aws.String(tierType), 213 } 214 createOpts.Tier = &environmentTier 215 } 216 217 if solutionStack != "" { 218 createOpts.SolutionStackName = aws.String(solutionStack) 219 } 220 221 if templateName != "" { 222 createOpts.TemplateName = aws.String(templateName) 223 } 224 225 log.Printf("[DEBUG] Elastic Beanstalk Environment create opts: %s", createOpts) 226 resp, err := conn.CreateEnvironment(&createOpts) 227 if err != nil { 228 return err 229 } 230 231 // Assign the application name as the resource ID 232 d.SetId(*resp.EnvironmentId) 233 234 stateConf := &resource.StateChangeConf{ 235 Pending: []string{"Launching", "Updating"}, 236 Target: []string{"Ready"}, 237 Refresh: environmentStateRefreshFunc(conn, d.Id()), 238 Timeout: waitForReadyTimeOut, 239 Delay: 10 * time.Second, 240 MinTimeout: 3 * time.Second, 241 } 242 243 _, err = stateConf.WaitForState() 244 if err != nil { 245 return fmt.Errorf( 246 "Error waiting for Elastic Beanstalk Environment (%s) to become ready: %s", 247 d.Id(), err) 248 } 249 250 return resourceAwsElasticBeanstalkEnvironmentRead(d, meta) 251 } 252 253 func resourceAwsElasticBeanstalkEnvironmentUpdate(d *schema.ResourceData, meta interface{}) error { 254 conn := meta.(*AWSClient).elasticbeanstalkconn 255 256 if d.HasChange("description") { 257 if err := resourceAwsElasticBeanstalkEnvironmentDescriptionUpdate(conn, d); err != nil { 258 return err 259 } 260 } 261 262 if d.HasChange("solution_stack_name") { 263 if err := resourceAwsElasticBeanstalkEnvironmentSolutionStackUpdate(conn, d); err != nil { 264 return err 265 } 266 } 267 268 if d.HasChange("setting") { 269 if err := resourceAwsElasticBeanstalkEnvironmentOptionSettingsUpdate(conn, d); err != nil { 270 return err 271 } 272 } 273 274 return resourceAwsElasticBeanstalkEnvironmentRead(d, meta) 275 } 276 277 func resourceAwsElasticBeanstalkEnvironmentDescriptionUpdate(conn *elasticbeanstalk.ElasticBeanstalk, d *schema.ResourceData) error { 278 name := d.Get("name").(string) 279 desc := d.Get("description").(string) 280 envId := d.Id() 281 282 log.Printf("[DEBUG] Elastic Beanstalk application: %s, update description: %s", name, desc) 283 284 _, err := conn.UpdateEnvironment(&elasticbeanstalk.UpdateEnvironmentInput{ 285 EnvironmentId: aws.String(envId), 286 Description: aws.String(desc), 287 }) 288 289 return err 290 } 291 292 func resourceAwsElasticBeanstalkEnvironmentOptionSettingsUpdate(conn *elasticbeanstalk.ElasticBeanstalk, d *schema.ResourceData) error { 293 name := d.Get("name").(string) 294 envId := d.Id() 295 296 log.Printf("[DEBUG] Elastic Beanstalk application: %s, update options", name) 297 298 req := &elasticbeanstalk.UpdateEnvironmentInput{ 299 EnvironmentId: aws.String(envId), 300 } 301 302 if d.HasChange("setting") { 303 o, n := d.GetChange("setting") 304 if o == nil { 305 o = &schema.Set{F: optionSettingValueHash} 306 } 307 if n == nil { 308 n = &schema.Set{F: optionSettingValueHash} 309 } 310 311 os := o.(*schema.Set) 312 ns := n.(*schema.Set) 313 314 req.OptionSettings = extractOptionSettings(ns.Difference(os)) 315 } 316 317 if _, err := conn.UpdateEnvironment(req); err != nil { 318 return err 319 } 320 321 return nil 322 } 323 324 func resourceAwsElasticBeanstalkEnvironmentSolutionStackUpdate(conn *elasticbeanstalk.ElasticBeanstalk, d *schema.ResourceData) error { 325 name := d.Get("name").(string) 326 solutionStack := d.Get("solution_stack_name").(string) 327 envId := d.Id() 328 329 log.Printf("[DEBUG] Elastic Beanstalk application: %s, update solution_stack_name: %s", name, solutionStack) 330 331 _, err := conn.UpdateEnvironment(&elasticbeanstalk.UpdateEnvironmentInput{ 332 EnvironmentId: aws.String(envId), 333 SolutionStackName: aws.String(solutionStack), 334 }) 335 336 return err 337 } 338 339 func resourceAwsElasticBeanstalkEnvironmentRead(d *schema.ResourceData, meta interface{}) error { 340 conn := meta.(*AWSClient).elasticbeanstalkconn 341 342 app := d.Get("application").(string) 343 envId := d.Id() 344 tier := d.Get("tier").(string) 345 346 log.Printf("[DEBUG] Elastic Beanstalk environment read %s: id %s", d.Get("name").(string), d.Id()) 347 348 resp, err := conn.DescribeEnvironments(&elasticbeanstalk.DescribeEnvironmentsInput{ 349 ApplicationName: aws.String(app), 350 EnvironmentIds: []*string{aws.String(envId)}, 351 }) 352 353 if err != nil { 354 return err 355 } 356 357 if len(resp.Environments) == 0 { 358 log.Printf("[DEBUG] Elastic Beanstalk environment properties: could not find environment %s", d.Id()) 359 360 d.SetId("") 361 return nil 362 } else if len(resp.Environments) != 1 { 363 return fmt.Errorf("Error reading application properties: found %d environments, expected 1", len(resp.Environments)) 364 } 365 366 env := resp.Environments[0] 367 368 if *env.Status == "Terminated" { 369 log.Printf("[DEBUG] Elastic Beanstalk environment %s was terminated", d.Id()) 370 371 d.SetId("") 372 return nil 373 } 374 375 resources, err := conn.DescribeEnvironmentResources(&elasticbeanstalk.DescribeEnvironmentResourcesInput{ 376 EnvironmentId: aws.String(envId), 377 }) 378 379 if err != nil { 380 return err 381 } 382 383 if err := d.Set("description", env.Description); err != nil { 384 return err 385 } 386 387 if err := d.Set("cname", env.CNAME); err != nil { 388 return err 389 } 390 391 if tier == "WebServer" { 392 beanstalkCnamePrefixRegexp := regexp.MustCompile(`(^[^.]+).\w{2}-\w{4}-\d.elasticbeanstalk.com$`) 393 var cnamePrefix string 394 cnamePrefixMatch := beanstalkCnamePrefixRegexp.FindStringSubmatch(*env.CNAME) 395 396 if cnamePrefixMatch == nil { 397 cnamePrefix = "" 398 } else { 399 cnamePrefix = cnamePrefixMatch[1] 400 } 401 402 if err := d.Set("cname_prefix", cnamePrefix); err != nil { 403 return err 404 } 405 } 406 407 if err := d.Set("autoscaling_groups", flattenBeanstalkAsg(resources.EnvironmentResources.AutoScalingGroups)); err != nil { 408 return err 409 } 410 411 if err := d.Set("instances", flattenBeanstalkInstances(resources.EnvironmentResources.Instances)); err != nil { 412 return err 413 } 414 if err := d.Set("launch_configurations", flattenBeanstalkLc(resources.EnvironmentResources.LaunchConfigurations)); err != nil { 415 return err 416 } 417 if err := d.Set("load_balancers", flattenBeanstalkElb(resources.EnvironmentResources.LoadBalancers)); err != nil { 418 return err 419 } 420 if err := d.Set("queues", flattenBeanstalkSqs(resources.EnvironmentResources.Queues)); err != nil { 421 return err 422 } 423 if err := d.Set("triggers", flattenBeanstalkTrigger(resources.EnvironmentResources.Triggers)); err != nil { 424 return err 425 } 426 427 return resourceAwsElasticBeanstalkEnvironmentSettingsRead(d, meta) 428 } 429 430 func fetchAwsElasticBeanstalkEnvironmentSettings(d *schema.ResourceData, meta interface{}) (*schema.Set, error) { 431 conn := meta.(*AWSClient).elasticbeanstalkconn 432 433 app := d.Get("application").(string) 434 name := d.Get("name").(string) 435 436 resp, err := conn.DescribeConfigurationSettings(&elasticbeanstalk.DescribeConfigurationSettingsInput{ 437 ApplicationName: aws.String(app), 438 EnvironmentName: aws.String(name), 439 }) 440 441 if err != nil { 442 return nil, err 443 } 444 445 if len(resp.ConfigurationSettings) != 1 { 446 return nil, fmt.Errorf("Error reading environment settings: received %d settings groups, expected 1", len(resp.ConfigurationSettings)) 447 } 448 449 settings := &schema.Set{F: optionSettingValueHash} 450 for _, optionSetting := range resp.ConfigurationSettings[0].OptionSettings { 451 m := map[string]interface{}{} 452 453 if optionSetting.Namespace != nil { 454 m["namespace"] = *optionSetting.Namespace 455 } else { 456 return nil, fmt.Errorf("Error reading environment settings: option setting with no namespace: %v", optionSetting) 457 } 458 459 if optionSetting.OptionName != nil { 460 m["name"] = *optionSetting.OptionName 461 } else { 462 return nil, fmt.Errorf("Error reading environment settings: option setting with no name: %v", optionSetting) 463 } 464 465 if optionSetting.Value != nil { 466 switch *optionSetting.OptionName { 467 case "SecurityGroups": 468 m["value"] = dropGeneratedSecurityGroup(*optionSetting.Value, meta) 469 case "Subnets", "ELBSubnets": 470 m["value"] = sortValues(*optionSetting.Value) 471 default: 472 m["value"] = *optionSetting.Value 473 } 474 } 475 476 settings.Add(m) 477 } 478 479 return settings, nil 480 } 481 482 func resourceAwsElasticBeanstalkEnvironmentSettingsRead(d *schema.ResourceData, meta interface{}) error { 483 log.Printf("[DEBUG] Elastic Beanstalk environment settings read %s: id %s", d.Get("name").(string), d.Id()) 484 485 allSettings, err := fetchAwsElasticBeanstalkEnvironmentSettings(d, meta) 486 if err != nil { 487 return err 488 } 489 490 settings := d.Get("setting").(*schema.Set) 491 492 log.Printf("[DEBUG] Elastic Beanstalk allSettings: %s", allSettings.GoString()) 493 log.Printf("[DEBUG] Elastic Beanstalk settings: %s", settings.GoString()) 494 495 // perform the set operation with only name/namespace as keys, excluding value 496 // this is so we override things in the settings resource data key with updated values 497 // from the api. we skip values we didn't know about before because there are so many 498 // defaults set by the eb api that we would delete many useful defaults. 499 // 500 // there is likely a better way to do this 501 allSettingsKeySet := schema.NewSet(optionSettingKeyHash, allSettings.List()) 502 settingsKeySet := schema.NewSet(optionSettingKeyHash, settings.List()) 503 updatedSettingsKeySet := allSettingsKeySet.Intersection(settingsKeySet) 504 505 log.Printf("[DEBUG] Elastic Beanstalk updatedSettingsKeySet: %s", updatedSettingsKeySet.GoString()) 506 507 updatedSettings := schema.NewSet(optionSettingValueHash, updatedSettingsKeySet.List()) 508 509 log.Printf("[DEBUG] Elastic Beanstalk updatedSettings: %s", updatedSettings.GoString()) 510 511 if err := d.Set("all_settings", allSettings.List()); err != nil { 512 return err 513 } 514 515 if err := d.Set("setting", updatedSettings.List()); err != nil { 516 return err 517 } 518 519 return nil 520 } 521 522 func resourceAwsElasticBeanstalkEnvironmentDelete(d *schema.ResourceData, meta interface{}) error { 523 conn := meta.(*AWSClient).elasticbeanstalkconn 524 525 waitForReadyTimeOut, err := time.ParseDuration(d.Get("wait_for_ready_timeout").(string)) 526 if err != nil { 527 return err 528 } 529 530 opts := elasticbeanstalk.TerminateEnvironmentInput{ 531 EnvironmentId: aws.String(d.Id()), 532 TerminateResources: aws.Bool(true), 533 } 534 535 log.Printf("[DEBUG] Elastic Beanstalk Environment terminate opts: %s", opts) 536 _, err = conn.TerminateEnvironment(&opts) 537 538 if err != nil { 539 return err 540 } 541 542 stateConf := &resource.StateChangeConf{ 543 Pending: []string{"Terminating"}, 544 Target: []string{"Terminated"}, 545 Refresh: environmentStateRefreshFunc(conn, d.Id()), 546 Timeout: waitForReadyTimeOut, 547 Delay: 10 * time.Second, 548 MinTimeout: 3 * time.Second, 549 } 550 551 _, err = stateConf.WaitForState() 552 if err != nil { 553 return fmt.Errorf( 554 "Error waiting for Elastic Beanstalk Environment (%s) to become terminated: %s", 555 d.Id(), err) 556 } 557 558 return nil 559 } 560 561 // environmentStateRefreshFunc returns a resource.StateRefreshFunc that is used to watch 562 // the creation of the Beanstalk Environment 563 func environmentStateRefreshFunc(conn *elasticbeanstalk.ElasticBeanstalk, environmentId string) resource.StateRefreshFunc { 564 return func() (interface{}, string, error) { 565 resp, err := conn.DescribeEnvironments(&elasticbeanstalk.DescribeEnvironmentsInput{ 566 EnvironmentIds: []*string{aws.String(environmentId)}, 567 }) 568 if err != nil { 569 log.Printf("[Err] Error waiting for Elastic Beanstalk Environment state: %s", err) 570 return -1, "failed", fmt.Errorf("[Err] Error waiting for Elastic Beanstalk Environment state: %s", err) 571 } 572 573 if resp == nil || len(resp.Environments) == 0 { 574 // Sometimes AWS just has consistency issues and doesn't see 575 // our instance yet. Return an empty state. 576 return nil, "", nil 577 } 578 579 var env *elasticbeanstalk.EnvironmentDescription 580 for _, e := range resp.Environments { 581 if environmentId == *e.EnvironmentId { 582 env = e 583 } 584 } 585 586 if env == nil { 587 return -1, "failed", fmt.Errorf("[Err] Error finding Elastic Beanstalk Environment, environment not found") 588 } 589 590 return env, *env.Status, nil 591 } 592 } 593 594 // we use the following two functions to allow us to split out defaults 595 // as they become overridden from within the template 596 func optionSettingValueHash(v interface{}) int { 597 rd := v.(map[string]interface{}) 598 namespace := rd["namespace"].(string) 599 optionName := rd["name"].(string) 600 value, _ := rd["value"].(string) 601 hk := fmt.Sprintf("%s:%s=%s", namespace, optionName, sortValues(value)) 602 log.Printf("[DEBUG] Elastic Beanstalk optionSettingValueHash(%#v): %s: hk=%s,hc=%d", v, optionName, hk, hashcode.String(hk)) 603 return hashcode.String(hk) 604 } 605 606 func optionSettingKeyHash(v interface{}) int { 607 rd := v.(map[string]interface{}) 608 namespace := rd["namespace"].(string) 609 optionName := rd["name"].(string) 610 hk := fmt.Sprintf("%s:%s", namespace, optionName) 611 log.Printf("[DEBUG] Elastic Beanstalk optionSettingKeyHash(%#v): %s: hk=%s,hc=%d", v, optionName, hk, hashcode.String(hk)) 612 return hashcode.String(hk) 613 } 614 615 func sortValues(v string) string { 616 values := strings.Split(v, ",") 617 sort.Strings(values) 618 return strings.Join(values, ",") 619 } 620 621 func extractOptionSettings(s *schema.Set) []*elasticbeanstalk.ConfigurationOptionSetting { 622 settings := []*elasticbeanstalk.ConfigurationOptionSetting{} 623 624 if s != nil { 625 for _, setting := range s.List() { 626 settings = append(settings, &elasticbeanstalk.ConfigurationOptionSetting{ 627 Namespace: aws.String(setting.(map[string]interface{})["namespace"].(string)), 628 OptionName: aws.String(setting.(map[string]interface{})["name"].(string)), 629 Value: aws.String(setting.(map[string]interface{})["value"].(string)), 630 }) 631 } 632 } 633 634 return settings 635 } 636 637 func dropGeneratedSecurityGroup(settingValue string, meta interface{}) string { 638 conn := meta.(*AWSClient).ec2conn 639 640 groups := strings.Split(settingValue, ",") 641 642 resp, err := conn.DescribeSecurityGroups(&ec2.DescribeSecurityGroupsInput{ 643 GroupIds: aws.StringSlice(groups), 644 }) 645 646 if err != nil { 647 log.Printf("[DEBUG] Elastic Beanstalk error describing SecurityGroups: %v", err) 648 return settingValue 649 } 650 651 var legitGroups []string 652 for _, group := range resp.SecurityGroups { 653 log.Printf("[DEBUG] Elastic Beanstalk SecurityGroup: %v", *group.GroupName) 654 if !strings.HasPrefix(*group.GroupName, "awseb") { 655 legitGroups = append(legitGroups, *group.GroupId) 656 } 657 } 658 659 sort.Strings(legitGroups) 660 661 return strings.Join(legitGroups, ",") 662 }