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