github.com/danp/terraform@v0.9.5-0.20170426144147-39d740081351/builtin/providers/aws/opsworks_layers.go (about) 1 package aws 2 3 import ( 4 "fmt" 5 "log" 6 "strconv" 7 8 "github.com/hashicorp/terraform/helper/hashcode" 9 "github.com/hashicorp/terraform/helper/schema" 10 11 "github.com/aws/aws-sdk-go/aws" 12 "github.com/aws/aws-sdk-go/aws/awserr" 13 "github.com/aws/aws-sdk-go/service/opsworks" 14 ) 15 16 // OpsWorks has a single concept of "layer" which represents several different 17 // layer types. The differences between these are in some extra properties that 18 // get packed into an "Attributes" map, but in the OpsWorks UI these are presented 19 // as first-class options, and so Terraform prefers to expose them this way and 20 // hide the implementation detail that they are all packed into a single type 21 // in the underlying API. 22 // 23 // This file contains utilities that are shared between all of the concrete 24 // layer resource types, which have names matching aws_opsworks_*_layer . 25 26 type opsworksLayerTypeAttribute struct { 27 AttrName string 28 Type schema.ValueType 29 Default interface{} 30 Required bool 31 WriteOnly bool 32 } 33 34 type opsworksLayerType struct { 35 TypeName string 36 DefaultLayerName string 37 Attributes map[string]*opsworksLayerTypeAttribute 38 CustomShortName bool 39 } 40 41 var ( 42 opsworksTrueString = "true" 43 opsworksFalseString = "false" 44 ) 45 46 func (lt *opsworksLayerType) SchemaResource() *schema.Resource { 47 resourceSchema := map[string]*schema.Schema{ 48 "id": &schema.Schema{ 49 Type: schema.TypeString, 50 Computed: true, 51 }, 52 53 "auto_assign_elastic_ips": &schema.Schema{ 54 Type: schema.TypeBool, 55 Optional: true, 56 Default: false, 57 }, 58 59 "auto_assign_public_ips": &schema.Schema{ 60 Type: schema.TypeBool, 61 Optional: true, 62 Default: false, 63 }, 64 65 "custom_instance_profile_arn": &schema.Schema{ 66 Type: schema.TypeString, 67 Optional: true, 68 }, 69 70 "elastic_load_balancer": &schema.Schema{ 71 Type: schema.TypeString, 72 Optional: true, 73 }, 74 75 "custom_setup_recipes": &schema.Schema{ 76 Type: schema.TypeList, 77 Optional: true, 78 Elem: &schema.Schema{Type: schema.TypeString}, 79 }, 80 81 "custom_configure_recipes": &schema.Schema{ 82 Type: schema.TypeList, 83 Optional: true, 84 Elem: &schema.Schema{Type: schema.TypeString}, 85 }, 86 87 "custom_deploy_recipes": &schema.Schema{ 88 Type: schema.TypeList, 89 Optional: true, 90 Elem: &schema.Schema{Type: schema.TypeString}, 91 }, 92 93 "custom_undeploy_recipes": &schema.Schema{ 94 Type: schema.TypeList, 95 Optional: true, 96 Elem: &schema.Schema{Type: schema.TypeString}, 97 }, 98 99 "custom_shutdown_recipes": &schema.Schema{ 100 Type: schema.TypeList, 101 Optional: true, 102 Elem: &schema.Schema{Type: schema.TypeString}, 103 }, 104 105 "custom_security_group_ids": &schema.Schema{ 106 Type: schema.TypeSet, 107 Optional: true, 108 Elem: &schema.Schema{Type: schema.TypeString}, 109 Set: schema.HashString, 110 }, 111 112 "custom_json": &schema.Schema{ 113 Type: schema.TypeString, 114 StateFunc: normalizeJson, 115 Optional: true, 116 }, 117 118 "auto_healing": &schema.Schema{ 119 Type: schema.TypeBool, 120 Optional: true, 121 Default: true, 122 }, 123 124 "install_updates_on_boot": &schema.Schema{ 125 Type: schema.TypeBool, 126 Optional: true, 127 Default: true, 128 }, 129 130 "instance_shutdown_timeout": &schema.Schema{ 131 Type: schema.TypeInt, 132 Optional: true, 133 Default: 120, 134 }, 135 136 "drain_elb_on_shutdown": &schema.Schema{ 137 Type: schema.TypeBool, 138 Optional: true, 139 Default: true, 140 }, 141 142 "system_packages": &schema.Schema{ 143 Type: schema.TypeSet, 144 Optional: true, 145 Elem: &schema.Schema{Type: schema.TypeString}, 146 Set: schema.HashString, 147 }, 148 149 "stack_id": &schema.Schema{ 150 Type: schema.TypeString, 151 ForceNew: true, 152 Required: true, 153 }, 154 155 "use_ebs_optimized_instances": &schema.Schema{ 156 Type: schema.TypeBool, 157 Optional: true, 158 Default: false, 159 }, 160 161 "ebs_volume": &schema.Schema{ 162 Type: schema.TypeSet, 163 Optional: true, 164 Elem: &schema.Resource{ 165 Schema: map[string]*schema.Schema{ 166 167 "iops": &schema.Schema{ 168 Type: schema.TypeInt, 169 Optional: true, 170 Default: 0, 171 }, 172 173 "mount_point": &schema.Schema{ 174 Type: schema.TypeString, 175 Required: true, 176 }, 177 178 "number_of_disks": &schema.Schema{ 179 Type: schema.TypeInt, 180 Required: true, 181 }, 182 183 "raid_level": &schema.Schema{ 184 Type: schema.TypeString, 185 Optional: true, 186 Default: "", 187 }, 188 189 "size": &schema.Schema{ 190 Type: schema.TypeInt, 191 Required: true, 192 }, 193 194 "type": &schema.Schema{ 195 Type: schema.TypeString, 196 Optional: true, 197 Default: "standard", 198 }, 199 }, 200 }, 201 Set: func(v interface{}) int { 202 m := v.(map[string]interface{}) 203 return hashcode.String(m["mount_point"].(string)) 204 }, 205 }, 206 } 207 208 if lt.CustomShortName { 209 resourceSchema["short_name"] = &schema.Schema{ 210 Type: schema.TypeString, 211 Required: true, 212 } 213 } 214 215 if lt.DefaultLayerName != "" { 216 resourceSchema["name"] = &schema.Schema{ 217 Type: schema.TypeString, 218 Optional: true, 219 Default: lt.DefaultLayerName, 220 } 221 } else { 222 resourceSchema["name"] = &schema.Schema{ 223 Type: schema.TypeString, 224 Required: true, 225 } 226 } 227 228 for key, def := range lt.Attributes { 229 resourceSchema[key] = &schema.Schema{ 230 Type: def.Type, 231 Default: def.Default, 232 Required: def.Required, 233 Optional: !def.Required, 234 } 235 } 236 237 return &schema.Resource{ 238 Read: func(d *schema.ResourceData, meta interface{}) error { 239 client := meta.(*AWSClient).opsworksconn 240 return lt.Read(d, client) 241 }, 242 Create: func(d *schema.ResourceData, meta interface{}) error { 243 client := meta.(*AWSClient).opsworksconn 244 return lt.Create(d, client) 245 }, 246 Update: func(d *schema.ResourceData, meta interface{}) error { 247 client := meta.(*AWSClient).opsworksconn 248 return lt.Update(d, client) 249 }, 250 Delete: func(d *schema.ResourceData, meta interface{}) error { 251 client := meta.(*AWSClient).opsworksconn 252 return lt.Delete(d, client) 253 }, 254 Importer: &schema.ResourceImporter{ 255 State: schema.ImportStatePassthrough, 256 }, 257 258 Schema: resourceSchema, 259 } 260 } 261 262 func (lt *opsworksLayerType) Read(d *schema.ResourceData, client *opsworks.OpsWorks) error { 263 264 req := &opsworks.DescribeLayersInput{ 265 LayerIds: []*string{ 266 aws.String(d.Id()), 267 }, 268 } 269 270 log.Printf("[DEBUG] Reading OpsWorks layer: %s", d.Id()) 271 272 resp, err := client.DescribeLayers(req) 273 if err != nil { 274 if awserr, ok := err.(awserr.Error); ok { 275 if awserr.Code() == "ResourceNotFoundException" { 276 d.SetId("") 277 return nil 278 } 279 } 280 return err 281 } 282 283 layer := resp.Layers[0] 284 d.Set("id", layer.LayerId) 285 d.Set("auto_assign_elastic_ips", layer.AutoAssignElasticIps) 286 d.Set("auto_assign_public_ips", layer.AutoAssignPublicIps) 287 d.Set("custom_instance_profile_arn", layer.CustomInstanceProfileArn) 288 d.Set("custom_security_group_ids", flattenStringList(layer.CustomSecurityGroupIds)) 289 d.Set("auto_healing", layer.EnableAutoHealing) 290 d.Set("install_updates_on_boot", layer.InstallUpdatesOnBoot) 291 d.Set("name", layer.Name) 292 d.Set("system_packages", flattenStringList(layer.Packages)) 293 d.Set("stack_id", layer.StackId) 294 d.Set("use_ebs_optimized_instances", layer.UseEbsOptimizedInstances) 295 296 if lt.CustomShortName { 297 d.Set("short_name", layer.Shortname) 298 } 299 300 if v := layer.CustomJson; v == nil { 301 if err := d.Set("custom_json", ""); err != nil { 302 return err 303 } 304 } else if err := d.Set("custom_json", normalizeJson(*v)); err != nil { 305 return err 306 } 307 308 lt.SetAttributeMap(d, layer.Attributes) 309 lt.SetLifecycleEventConfiguration(d, layer.LifecycleEventConfiguration) 310 lt.SetCustomRecipes(d, layer.CustomRecipes) 311 lt.SetVolumeConfigurations(d, layer.VolumeConfigurations) 312 313 /* get ELB */ 314 ebsRequest := &opsworks.DescribeElasticLoadBalancersInput{ 315 LayerIds: []*string{ 316 aws.String(d.Id()), 317 }, 318 } 319 loadBalancers, err := client.DescribeElasticLoadBalancers(ebsRequest) 320 if err != nil { 321 return err 322 } 323 324 if loadBalancers.ElasticLoadBalancers == nil || len(loadBalancers.ElasticLoadBalancers) == 0 { 325 d.Set("elastic_load_balancer", "") 326 } else { 327 loadBalancer := loadBalancers.ElasticLoadBalancers[0] 328 if loadBalancer != nil { 329 d.Set("elastic_load_balancer", loadBalancer.ElasticLoadBalancerName) 330 } 331 } 332 333 return nil 334 } 335 336 func (lt *opsworksLayerType) Create(d *schema.ResourceData, client *opsworks.OpsWorks) error { 337 338 req := &opsworks.CreateLayerInput{ 339 AutoAssignElasticIps: aws.Bool(d.Get("auto_assign_elastic_ips").(bool)), 340 AutoAssignPublicIps: aws.Bool(d.Get("auto_assign_public_ips").(bool)), 341 CustomInstanceProfileArn: aws.String(d.Get("custom_instance_profile_arn").(string)), 342 CustomRecipes: lt.CustomRecipes(d), 343 CustomSecurityGroupIds: expandStringSet(d.Get("custom_security_group_ids").(*schema.Set)), 344 EnableAutoHealing: aws.Bool(d.Get("auto_healing").(bool)), 345 InstallUpdatesOnBoot: aws.Bool(d.Get("install_updates_on_boot").(bool)), 346 LifecycleEventConfiguration: lt.LifecycleEventConfiguration(d), 347 Name: aws.String(d.Get("name").(string)), 348 Packages: expandStringSet(d.Get("system_packages").(*schema.Set)), 349 Type: aws.String(lt.TypeName), 350 StackId: aws.String(d.Get("stack_id").(string)), 351 UseEbsOptimizedInstances: aws.Bool(d.Get("use_ebs_optimized_instances").(bool)), 352 Attributes: lt.AttributeMap(d), 353 VolumeConfigurations: lt.VolumeConfigurations(d), 354 } 355 356 if lt.CustomShortName { 357 req.Shortname = aws.String(d.Get("short_name").(string)) 358 } else { 359 req.Shortname = aws.String(lt.TypeName) 360 } 361 362 req.CustomJson = aws.String(d.Get("custom_json").(string)) 363 364 log.Printf("[DEBUG] Creating OpsWorks layer: %s", d.Id()) 365 366 resp, err := client.CreateLayer(req) 367 if err != nil { 368 return err 369 } 370 371 layerId := *resp.LayerId 372 d.SetId(layerId) 373 d.Set("id", layerId) 374 375 loadBalancer := aws.String(d.Get("elastic_load_balancer").(string)) 376 if loadBalancer != nil && *loadBalancer != "" { 377 log.Printf("[DEBUG] Attaching load balancer: %s", *loadBalancer) 378 _, err := client.AttachElasticLoadBalancer(&opsworks.AttachElasticLoadBalancerInput{ 379 ElasticLoadBalancerName: loadBalancer, 380 LayerId: &layerId, 381 }) 382 if err != nil { 383 return err 384 } 385 } 386 387 return lt.Read(d, client) 388 } 389 390 func (lt *opsworksLayerType) Update(d *schema.ResourceData, client *opsworks.OpsWorks) error { 391 392 req := &opsworks.UpdateLayerInput{ 393 LayerId: aws.String(d.Id()), 394 AutoAssignElasticIps: aws.Bool(d.Get("auto_assign_elastic_ips").(bool)), 395 AutoAssignPublicIps: aws.Bool(d.Get("auto_assign_public_ips").(bool)), 396 CustomInstanceProfileArn: aws.String(d.Get("custom_instance_profile_arn").(string)), 397 CustomRecipes: lt.CustomRecipes(d), 398 CustomSecurityGroupIds: expandStringSet(d.Get("custom_security_group_ids").(*schema.Set)), 399 EnableAutoHealing: aws.Bool(d.Get("auto_healing").(bool)), 400 InstallUpdatesOnBoot: aws.Bool(d.Get("install_updates_on_boot").(bool)), 401 LifecycleEventConfiguration: lt.LifecycleEventConfiguration(d), 402 Name: aws.String(d.Get("name").(string)), 403 Packages: expandStringSet(d.Get("system_packages").(*schema.Set)), 404 UseEbsOptimizedInstances: aws.Bool(d.Get("use_ebs_optimized_instances").(bool)), 405 Attributes: lt.AttributeMap(d), 406 VolumeConfigurations: lt.VolumeConfigurations(d), 407 } 408 409 if lt.CustomShortName { 410 req.Shortname = aws.String(d.Get("short_name").(string)) 411 } else { 412 req.Shortname = aws.String(lt.TypeName) 413 } 414 415 req.CustomJson = aws.String(d.Get("custom_json").(string)) 416 417 log.Printf("[DEBUG] Updating OpsWorks layer: %s", d.Id()) 418 419 if d.HasChange("elastic_load_balancer") { 420 lbo, lbn := d.GetChange("elastic_load_balancer") 421 422 loadBalancerOld := aws.String(lbo.(string)) 423 loadBalancerNew := aws.String(lbn.(string)) 424 425 if loadBalancerOld != nil && *loadBalancerOld != "" { 426 log.Printf("[DEBUG] Dettaching load balancer: %s", *loadBalancerOld) 427 _, err := client.DetachElasticLoadBalancer(&opsworks.DetachElasticLoadBalancerInput{ 428 ElasticLoadBalancerName: loadBalancerOld, 429 LayerId: aws.String(d.Id()), 430 }) 431 if err != nil { 432 return err 433 } 434 } 435 436 if loadBalancerNew != nil && *loadBalancerNew != "" { 437 log.Printf("[DEBUG] Attaching load balancer: %s", *loadBalancerNew) 438 _, err := client.AttachElasticLoadBalancer(&opsworks.AttachElasticLoadBalancerInput{ 439 ElasticLoadBalancerName: loadBalancerNew, 440 LayerId: aws.String(d.Id()), 441 }) 442 if err != nil { 443 return err 444 } 445 } 446 } 447 448 _, err := client.UpdateLayer(req) 449 if err != nil { 450 return err 451 } 452 453 return lt.Read(d, client) 454 } 455 456 func (lt *opsworksLayerType) Delete(d *schema.ResourceData, client *opsworks.OpsWorks) error { 457 req := &opsworks.DeleteLayerInput{ 458 LayerId: aws.String(d.Id()), 459 } 460 461 log.Printf("[DEBUG] Deleting OpsWorks layer: %s", d.Id()) 462 463 _, err := client.DeleteLayer(req) 464 return err 465 } 466 467 func (lt *opsworksLayerType) AttributeMap(d *schema.ResourceData) map[string]*string { 468 attrs := map[string]*string{} 469 470 for key, def := range lt.Attributes { 471 value := d.Get(key) 472 switch def.Type { 473 case schema.TypeString: 474 strValue := value.(string) 475 attrs[def.AttrName] = &strValue 476 case schema.TypeInt: 477 intValue := value.(int) 478 strValue := strconv.Itoa(intValue) 479 attrs[def.AttrName] = &strValue 480 case schema.TypeBool: 481 boolValue := value.(bool) 482 if boolValue { 483 attrs[def.AttrName] = &opsworksTrueString 484 } else { 485 attrs[def.AttrName] = &opsworksFalseString 486 } 487 default: 488 // should never happen 489 panic(fmt.Errorf("Unsupported OpsWorks layer attribute type")) 490 } 491 } 492 493 return attrs 494 } 495 496 func (lt *opsworksLayerType) SetAttributeMap(d *schema.ResourceData, attrs map[string]*string) { 497 for key, def := range lt.Attributes { 498 // Ignore write-only attributes; we'll just keep what we already have stored. 499 // (The AWS API returns garbage placeholder values for these.) 500 if def.WriteOnly { 501 continue 502 } 503 504 if strPtr, ok := attrs[def.AttrName]; ok && strPtr != nil { 505 strValue := *strPtr 506 507 switch def.Type { 508 case schema.TypeString: 509 d.Set(key, strValue) 510 case schema.TypeInt: 511 intValue, err := strconv.Atoi(strValue) 512 if err == nil { 513 d.Set(key, intValue) 514 } else { 515 // Got garbage from the AWS API 516 d.Set(key, nil) 517 } 518 case schema.TypeBool: 519 boolValue := true 520 if strValue == opsworksFalseString { 521 boolValue = false 522 } 523 d.Set(key, boolValue) 524 default: 525 // should never happen 526 panic(fmt.Errorf("Unsupported OpsWorks layer attribute type")) 527 } 528 return 529 530 } else { 531 d.Set(key, nil) 532 } 533 } 534 } 535 536 func (lt *opsworksLayerType) LifecycleEventConfiguration(d *schema.ResourceData) *opsworks.LifecycleEventConfiguration { 537 return &opsworks.LifecycleEventConfiguration{ 538 Shutdown: &opsworks.ShutdownEventConfiguration{ 539 DelayUntilElbConnectionsDrained: aws.Bool(d.Get("drain_elb_on_shutdown").(bool)), 540 ExecutionTimeout: aws.Int64(int64(d.Get("instance_shutdown_timeout").(int))), 541 }, 542 } 543 } 544 545 func (lt *opsworksLayerType) SetLifecycleEventConfiguration(d *schema.ResourceData, v *opsworks.LifecycleEventConfiguration) { 546 if v == nil || v.Shutdown == nil { 547 d.Set("drain_elb_on_shutdown", nil) 548 d.Set("instance_shutdown_timeout", nil) 549 } else { 550 d.Set("drain_elb_on_shutdown", v.Shutdown.DelayUntilElbConnectionsDrained) 551 d.Set("instance_shutdown_timeout", v.Shutdown.ExecutionTimeout) 552 } 553 } 554 555 func (lt *opsworksLayerType) CustomRecipes(d *schema.ResourceData) *opsworks.Recipes { 556 return &opsworks.Recipes{ 557 Configure: expandStringList(d.Get("custom_configure_recipes").([]interface{})), 558 Deploy: expandStringList(d.Get("custom_deploy_recipes").([]interface{})), 559 Setup: expandStringList(d.Get("custom_setup_recipes").([]interface{})), 560 Shutdown: expandStringList(d.Get("custom_shutdown_recipes").([]interface{})), 561 Undeploy: expandStringList(d.Get("custom_undeploy_recipes").([]interface{})), 562 } 563 } 564 565 func (lt *opsworksLayerType) SetCustomRecipes(d *schema.ResourceData, v *opsworks.Recipes) { 566 // Null out everything first, and then we'll consider what to put back. 567 d.Set("custom_configure_recipes", nil) 568 d.Set("custom_deploy_recipes", nil) 569 d.Set("custom_setup_recipes", nil) 570 d.Set("custom_shutdown_recipes", nil) 571 d.Set("custom_undeploy_recipes", nil) 572 573 if v == nil { 574 return 575 } 576 577 d.Set("custom_configure_recipes", flattenStringList(v.Configure)) 578 d.Set("custom_deploy_recipes", flattenStringList(v.Deploy)) 579 d.Set("custom_setup_recipes", flattenStringList(v.Setup)) 580 d.Set("custom_shutdown_recipes", flattenStringList(v.Shutdown)) 581 d.Set("custom_undeploy_recipes", flattenStringList(v.Undeploy)) 582 } 583 584 func (lt *opsworksLayerType) VolumeConfigurations(d *schema.ResourceData) []*opsworks.VolumeConfiguration { 585 configuredVolumes := d.Get("ebs_volume").(*schema.Set).List() 586 result := make([]*opsworks.VolumeConfiguration, len(configuredVolumes)) 587 588 for i := 0; i < len(configuredVolumes); i++ { 589 volumeData := configuredVolumes[i].(map[string]interface{}) 590 591 result[i] = &opsworks.VolumeConfiguration{ 592 MountPoint: aws.String(volumeData["mount_point"].(string)), 593 NumberOfDisks: aws.Int64(int64(volumeData["number_of_disks"].(int))), 594 Size: aws.Int64(int64(volumeData["size"].(int))), 595 VolumeType: aws.String(volumeData["type"].(string)), 596 } 597 iops := int64(volumeData["iops"].(int)) 598 if iops != 0 { 599 result[i].Iops = aws.Int64(iops) 600 } 601 602 raidLevelStr := volumeData["raid_level"].(string) 603 if raidLevelStr != "" { 604 raidLevel, err := strconv.Atoi(raidLevelStr) 605 if err == nil { 606 result[i].RaidLevel = aws.Int64(int64(raidLevel)) 607 } 608 } 609 } 610 611 return result 612 } 613 614 func (lt *opsworksLayerType) SetVolumeConfigurations(d *schema.ResourceData, v []*opsworks.VolumeConfiguration) { 615 newValue := make([]*map[string]interface{}, len(v)) 616 617 for i := 0; i < len(v); i++ { 618 config := v[i] 619 data := make(map[string]interface{}) 620 newValue[i] = &data 621 622 if config.Iops != nil { 623 data["iops"] = int(*config.Iops) 624 } else { 625 data["iops"] = 0 626 } 627 if config.MountPoint != nil { 628 data["mount_point"] = *config.MountPoint 629 } 630 if config.NumberOfDisks != nil { 631 data["number_of_disks"] = int(*config.NumberOfDisks) 632 } 633 if config.RaidLevel != nil { 634 data["raid_level"] = strconv.Itoa(int(*config.RaidLevel)) 635 } 636 if config.Size != nil { 637 data["size"] = int(*config.Size) 638 } 639 if config.VolumeType != nil { 640 data["type"] = *config.VolumeType 641 } 642 } 643 644 d.Set("ebs_volume", newValue) 645 }