opentofu/builtin/providers/aws/opsworks_layers.go
Martin Atkins 41c535dc68 Unconditionally set opsworks layer custom_json
Previously in Update we would only set req.CustomJson if a non-empty
value was provided in the config. It seems that the Opsworks API considers
a null CustomJson to mean "do not change" rather than "set to empty",
so we need to explicitly set the empty string in the request body in
order to successfully remove an already-configured custom JSON.
2016-04-16 09:18:47 -07:00

643 lines
18 KiB
Go

package aws
import (
"fmt"
"log"
"strconv"
"github.com/hashicorp/terraform/helper/hashcode"
"github.com/hashicorp/terraform/helper/schema"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/opsworks"
)
// OpsWorks has a single concept of "layer" which represents several different
// layer types. The differences between these are in some extra properties that
// get packed into an "Attributes" map, but in the OpsWorks UI these are presented
// as first-class options, and so Terraform prefers to expose them this way and
// hide the implementation detail that they are all packed into a single type
// in the underlying API.
//
// This file contains utilities that are shared between all of the concrete
// layer resource types, which have names matching aws_opsworks_*_layer .
type opsworksLayerTypeAttribute struct {
AttrName string
Type schema.ValueType
Default interface{}
Required bool
WriteOnly bool
}
type opsworksLayerType struct {
TypeName string
DefaultLayerName string
Attributes map[string]*opsworksLayerTypeAttribute
CustomShortName bool
}
var (
opsworksTrueString = "1"
opsworksFalseString = "0"
)
func (lt *opsworksLayerType) SchemaResource() *schema.Resource {
resourceSchema := map[string]*schema.Schema{
"id": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
"auto_assign_elastic_ips": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: false,
},
"auto_assign_public_ips": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: false,
},
"custom_instance_profile_arn": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"elastic_load_balancer": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"custom_setup_recipes": &schema.Schema{
Type: schema.TypeList,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
"custom_configure_recipes": &schema.Schema{
Type: schema.TypeList,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
"custom_deploy_recipes": &schema.Schema{
Type: schema.TypeList,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
"custom_undeploy_recipes": &schema.Schema{
Type: schema.TypeList,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
"custom_shutdown_recipes": &schema.Schema{
Type: schema.TypeList,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
"custom_security_group_ids": &schema.Schema{
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
Set: schema.HashString,
},
"custom_json": &schema.Schema{
Type: schema.TypeString,
StateFunc: normalizeJson,
Optional: true,
},
"auto_healing": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: true,
},
"install_updates_on_boot": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: true,
},
"instance_shutdown_timeout": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
Default: 120,
},
"drain_elb_on_shutdown": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: true,
},
"system_packages": &schema.Schema{
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
Set: schema.HashString,
},
"stack_id": &schema.Schema{
Type: schema.TypeString,
ForceNew: true,
Required: true,
},
"use_ebs_optimized_instances": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: false,
},
"ebs_volume": &schema.Schema{
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"iops": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
Default: 0,
},
"mount_point": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"number_of_disks": &schema.Schema{
Type: schema.TypeInt,
Required: true,
},
"raid_level": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Default: "",
},
"size": &schema.Schema{
Type: schema.TypeInt,
Required: true,
},
"type": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Default: "standard",
},
},
},
Set: func(v interface{}) int {
m := v.(map[string]interface{})
return hashcode.String(m["mount_point"].(string))
},
},
}
if lt.CustomShortName {
resourceSchema["short_name"] = &schema.Schema{
Type: schema.TypeString,
Required: true,
}
}
if lt.DefaultLayerName != "" {
resourceSchema["name"] = &schema.Schema{
Type: schema.TypeString,
Optional: true,
Default: lt.DefaultLayerName,
}
} else {
resourceSchema["name"] = &schema.Schema{
Type: schema.TypeString,
Required: true,
}
}
for key, def := range lt.Attributes {
resourceSchema[key] = &schema.Schema{
Type: def.Type,
Default: def.Default,
Required: def.Required,
Optional: !def.Required,
}
}
return &schema.Resource{
Read: func(d *schema.ResourceData, meta interface{}) error {
client := meta.(*AWSClient).opsworksconn
return lt.Read(d, client)
},
Create: func(d *schema.ResourceData, meta interface{}) error {
client := meta.(*AWSClient).opsworksconn
return lt.Create(d, client)
},
Update: func(d *schema.ResourceData, meta interface{}) error {
client := meta.(*AWSClient).opsworksconn
return lt.Update(d, client)
},
Delete: func(d *schema.ResourceData, meta interface{}) error {
client := meta.(*AWSClient).opsworksconn
return lt.Delete(d, client)
},
Schema: resourceSchema,
}
}
func (lt *opsworksLayerType) Read(d *schema.ResourceData, client *opsworks.OpsWorks) error {
req := &opsworks.DescribeLayersInput{
LayerIds: []*string{
aws.String(d.Id()),
},
}
log.Printf("[DEBUG] Reading OpsWorks layer: %s", d.Id())
resp, err := client.DescribeLayers(req)
if err != nil {
if awserr, ok := err.(awserr.Error); ok {
if awserr.Code() == "ResourceNotFoundException" {
d.SetId("")
return nil
}
}
return err
}
layer := resp.Layers[0]
d.Set("id", layer.LayerId)
d.Set("auto_assign_elastic_ips", layer.AutoAssignElasticIps)
d.Set("auto_assign_public_ips", layer.AutoAssignPublicIps)
d.Set("custom_instance_profile_arn", layer.CustomInstanceProfileArn)
d.Set("custom_security_group_ids", flattenStringList(layer.CustomSecurityGroupIds))
d.Set("auto_healing", layer.EnableAutoHealing)
d.Set("install_updates_on_boot", layer.InstallUpdatesOnBoot)
d.Set("name", layer.Name)
d.Set("system_packages", flattenStringList(layer.Packages))
d.Set("stack_id", layer.StackId)
d.Set("use_ebs_optimized_instances", layer.UseEbsOptimizedInstances)
if lt.CustomShortName {
d.Set("short_name", layer.Shortname)
}
if v := layer.CustomJson; v == nil {
if err := d.Set("custom_json", ""); err != nil {
return err
}
} else if err := d.Set("custom_json", normalizeJson(*v)); err != nil {
return err
}
lt.SetAttributeMap(d, layer.Attributes)
lt.SetLifecycleEventConfiguration(d, layer.LifecycleEventConfiguration)
lt.SetCustomRecipes(d, layer.CustomRecipes)
lt.SetVolumeConfigurations(d, layer.VolumeConfigurations)
/* get ELB */
ebsRequest := &opsworks.DescribeElasticLoadBalancersInput{
LayerIds: []*string{
aws.String(d.Id()),
},
}
loadBalancers, err := client.DescribeElasticLoadBalancers(ebsRequest)
if err != nil {
return err
}
if loadBalancers.ElasticLoadBalancers == nil || len(loadBalancers.ElasticLoadBalancers) == 0 {
d.Set("elastic_load_balancer", "")
} else {
loadBalancer := loadBalancers.ElasticLoadBalancers[0]
if loadBalancer != nil {
d.Set("elastic_load_balancer", loadBalancer.ElasticLoadBalancerName)
}
}
return nil
}
func (lt *opsworksLayerType) Create(d *schema.ResourceData, client *opsworks.OpsWorks) error {
req := &opsworks.CreateLayerInput{
AutoAssignElasticIps: aws.Bool(d.Get("auto_assign_elastic_ips").(bool)),
AutoAssignPublicIps: aws.Bool(d.Get("auto_assign_public_ips").(bool)),
CustomInstanceProfileArn: aws.String(d.Get("custom_instance_profile_arn").(string)),
CustomRecipes: lt.CustomRecipes(d),
CustomSecurityGroupIds: expandStringSet(d.Get("custom_security_group_ids").(*schema.Set)),
EnableAutoHealing: aws.Bool(d.Get("auto_healing").(bool)),
InstallUpdatesOnBoot: aws.Bool(d.Get("install_updates_on_boot").(bool)),
LifecycleEventConfiguration: lt.LifecycleEventConfiguration(d),
Name: aws.String(d.Get("name").(string)),
Packages: expandStringSet(d.Get("system_packages").(*schema.Set)),
Type: aws.String(lt.TypeName),
StackId: aws.String(d.Get("stack_id").(string)),
UseEbsOptimizedInstances: aws.Bool(d.Get("use_ebs_optimized_instances").(bool)),
Attributes: lt.AttributeMap(d),
VolumeConfigurations: lt.VolumeConfigurations(d),
}
if lt.CustomShortName {
req.Shortname = aws.String(d.Get("short_name").(string))
} else {
req.Shortname = aws.String(lt.TypeName)
}
req.CustomJson = aws.String(d.Get("custom_json").(string))
log.Printf("[DEBUG] Creating OpsWorks layer: %s", d.Id())
resp, err := client.CreateLayer(req)
if err != nil {
return err
}
layerId := *resp.LayerId
d.SetId(layerId)
d.Set("id", layerId)
loadBalancer := aws.String(d.Get("elastic_load_balancer").(string))
if loadBalancer != nil && *loadBalancer != "" {
log.Printf("[DEBUG] Attaching load balancer: %s", *loadBalancer)
_, err := client.AttachElasticLoadBalancer(&opsworks.AttachElasticLoadBalancerInput{
ElasticLoadBalancerName: loadBalancer,
LayerId: &layerId,
})
if err != nil {
return err
}
}
return lt.Read(d, client)
}
func (lt *opsworksLayerType) Update(d *schema.ResourceData, client *opsworks.OpsWorks) error {
req := &opsworks.UpdateLayerInput{
LayerId: aws.String(d.Id()),
AutoAssignElasticIps: aws.Bool(d.Get("auto_assign_elastic_ips").(bool)),
AutoAssignPublicIps: aws.Bool(d.Get("auto_assign_public_ips").(bool)),
CustomInstanceProfileArn: aws.String(d.Get("custom_instance_profile_arn").(string)),
CustomRecipes: lt.CustomRecipes(d),
CustomSecurityGroupIds: expandStringSet(d.Get("custom_security_group_ids").(*schema.Set)),
EnableAutoHealing: aws.Bool(d.Get("auto_healing").(bool)),
InstallUpdatesOnBoot: aws.Bool(d.Get("install_updates_on_boot").(bool)),
LifecycleEventConfiguration: lt.LifecycleEventConfiguration(d),
Name: aws.String(d.Get("name").(string)),
Packages: expandStringSet(d.Get("system_packages").(*schema.Set)),
UseEbsOptimizedInstances: aws.Bool(d.Get("use_ebs_optimized_instances").(bool)),
Attributes: lt.AttributeMap(d),
VolumeConfigurations: lt.VolumeConfigurations(d),
}
if lt.CustomShortName {
req.Shortname = aws.String(d.Get("short_name").(string))
} else {
req.Shortname = aws.String(lt.TypeName)
}
req.CustomJson = aws.String(d.Get("custom_json").(string))
log.Printf("[DEBUG] Updating OpsWorks layer: %s", d.Id())
if d.HasChange("elastic_load_balancer") {
lbo, lbn := d.GetChange("elastic_load_balancer")
loadBalancerOld := aws.String(lbo.(string))
loadBalancerNew := aws.String(lbn.(string))
if loadBalancerOld != nil && *loadBalancerOld != "" {
log.Printf("[DEBUG] Dettaching load balancer: %s", *loadBalancerOld)
_, err := client.DetachElasticLoadBalancer(&opsworks.DetachElasticLoadBalancerInput{
ElasticLoadBalancerName: loadBalancerOld,
LayerId: aws.String(d.Id()),
})
if err != nil {
return err
}
}
if loadBalancerNew != nil && *loadBalancerNew != "" {
log.Printf("[DEBUG] Attaching load balancer: %s", *loadBalancerNew)
_, err := client.AttachElasticLoadBalancer(&opsworks.AttachElasticLoadBalancerInput{
ElasticLoadBalancerName: loadBalancerNew,
LayerId: aws.String(d.Id()),
})
if err != nil {
return err
}
}
}
_, err := client.UpdateLayer(req)
if err != nil {
return err
}
return lt.Read(d, client)
}
func (lt *opsworksLayerType) Delete(d *schema.ResourceData, client *opsworks.OpsWorks) error {
req := &opsworks.DeleteLayerInput{
LayerId: aws.String(d.Id()),
}
log.Printf("[DEBUG] Deleting OpsWorks layer: %s", d.Id())
_, err := client.DeleteLayer(req)
return err
}
func (lt *opsworksLayerType) AttributeMap(d *schema.ResourceData) map[string]*string {
attrs := map[string]*string{}
for key, def := range lt.Attributes {
value := d.Get(key)
switch def.Type {
case schema.TypeString:
strValue := value.(string)
attrs[def.AttrName] = &strValue
case schema.TypeInt:
intValue := value.(int)
strValue := strconv.Itoa(intValue)
attrs[def.AttrName] = &strValue
case schema.TypeBool:
boolValue := value.(bool)
if boolValue {
attrs[def.AttrName] = &opsworksTrueString
} else {
attrs[def.AttrName] = &opsworksFalseString
}
default:
// should never happen
panic(fmt.Errorf("Unsupported OpsWorks layer attribute type"))
}
}
return attrs
}
func (lt *opsworksLayerType) SetAttributeMap(d *schema.ResourceData, attrs map[string]*string) {
for key, def := range lt.Attributes {
// Ignore write-only attributes; we'll just keep what we already have stored.
// (The AWS API returns garbage placeholder values for these.)
if def.WriteOnly {
continue
}
if strPtr, ok := attrs[def.AttrName]; ok && strPtr != nil {
strValue := *strPtr
switch def.Type {
case schema.TypeString:
d.Set(key, strValue)
case schema.TypeInt:
intValue, err := strconv.Atoi(strValue)
if err == nil {
d.Set(key, intValue)
} else {
// Got garbage from the AWS API
d.Set(key, nil)
}
case schema.TypeBool:
boolValue := true
if strValue == opsworksFalseString {
boolValue = false
}
d.Set(key, boolValue)
default:
// should never happen
panic(fmt.Errorf("Unsupported OpsWorks layer attribute type"))
}
return
} else {
d.Set(key, nil)
}
}
}
func (lt *opsworksLayerType) LifecycleEventConfiguration(d *schema.ResourceData) *opsworks.LifecycleEventConfiguration {
return &opsworks.LifecycleEventConfiguration{
Shutdown: &opsworks.ShutdownEventConfiguration{
DelayUntilElbConnectionsDrained: aws.Bool(d.Get("drain_elb_on_shutdown").(bool)),
ExecutionTimeout: aws.Int64(int64(d.Get("instance_shutdown_timeout").(int))),
},
}
}
func (lt *opsworksLayerType) SetLifecycleEventConfiguration(d *schema.ResourceData, v *opsworks.LifecycleEventConfiguration) {
if v == nil || v.Shutdown == nil {
d.Set("drain_elb_on_shutdown", nil)
d.Set("instance_shutdown_timeout", nil)
} else {
d.Set("drain_elb_on_shutdown", v.Shutdown.DelayUntilElbConnectionsDrained)
d.Set("instance_shutdown_timeout", v.Shutdown.ExecutionTimeout)
}
}
func (lt *opsworksLayerType) CustomRecipes(d *schema.ResourceData) *opsworks.Recipes {
return &opsworks.Recipes{
Configure: expandStringList(d.Get("custom_configure_recipes").([]interface{})),
Deploy: expandStringList(d.Get("custom_deploy_recipes").([]interface{})),
Setup: expandStringList(d.Get("custom_setup_recipes").([]interface{})),
Shutdown: expandStringList(d.Get("custom_shutdown_recipes").([]interface{})),
Undeploy: expandStringList(d.Get("custom_undeploy_recipes").([]interface{})),
}
}
func (lt *opsworksLayerType) SetCustomRecipes(d *schema.ResourceData, v *opsworks.Recipes) {
// Null out everything first, and then we'll consider what to put back.
d.Set("custom_configure_recipes", nil)
d.Set("custom_deploy_recipes", nil)
d.Set("custom_setup_recipes", nil)
d.Set("custom_shutdown_recipes", nil)
d.Set("custom_undeploy_recipes", nil)
if v == nil {
return
}
d.Set("custom_configure_recipes", flattenStringList(v.Configure))
d.Set("custom_deploy_recipes", flattenStringList(v.Deploy))
d.Set("custom_setup_recipes", flattenStringList(v.Setup))
d.Set("custom_shutdown_recipes", flattenStringList(v.Shutdown))
d.Set("custom_undeploy_recipes", flattenStringList(v.Undeploy))
}
func (lt *opsworksLayerType) VolumeConfigurations(d *schema.ResourceData) []*opsworks.VolumeConfiguration {
configuredVolumes := d.Get("ebs_volume").(*schema.Set).List()
result := make([]*opsworks.VolumeConfiguration, len(configuredVolumes))
for i := 0; i < len(configuredVolumes); i++ {
volumeData := configuredVolumes[i].(map[string]interface{})
result[i] = &opsworks.VolumeConfiguration{
MountPoint: aws.String(volumeData["mount_point"].(string)),
NumberOfDisks: aws.Int64(int64(volumeData["number_of_disks"].(int))),
Size: aws.Int64(int64(volumeData["size"].(int))),
VolumeType: aws.String(volumeData["type"].(string)),
}
iops := int64(volumeData["iops"].(int))
if iops != 0 {
result[i].Iops = aws.Int64(iops)
}
raidLevelStr := volumeData["raid_level"].(string)
if raidLevelStr != "" {
raidLevel, err := strconv.Atoi(raidLevelStr)
if err == nil {
result[i].RaidLevel = aws.Int64(int64(raidLevel))
}
}
}
return result
}
func (lt *opsworksLayerType) SetVolumeConfigurations(d *schema.ResourceData, v []*opsworks.VolumeConfiguration) {
newValue := make([]*map[string]interface{}, len(v))
for i := 0; i < len(v); i++ {
config := v[i]
data := make(map[string]interface{})
newValue[i] = &data
if config.Iops != nil {
data["iops"] = int(*config.Iops)
} else {
data["iops"] = 0
}
if config.MountPoint != nil {
data["mount_point"] = *config.MountPoint
}
if config.NumberOfDisks != nil {
data["number_of_disks"] = int(*config.NumberOfDisks)
}
if config.RaidLevel != nil {
data["raid_level"] = strconv.Itoa(int(*config.RaidLevel))
}
if config.Size != nil {
data["size"] = int(*config.Size)
}
if config.VolumeType != nil {
data["type"] = *config.VolumeType
}
}
d.Set("ebs_volume", newValue)
}