559 lines
16 KiB
Go
559 lines
16 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,
|
|
},
|
|
|
|
"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,
|
|
},
|
|
|
|
"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)
|
|
}
|
|
|
|
lt.SetAttributeMap(d, layer.Attributes)
|
|
lt.SetLifecycleEventConfiguration(d, layer.LifecycleEventConfiguration)
|
|
lt.SetCustomRecipes(d, layer.CustomRecipes)
|
|
lt.SetVolumeConfigurations(d, layer.VolumeConfigurations)
|
|
|
|
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)
|
|
}
|
|
|
|
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)
|
|
|
|
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)
|
|
}
|
|
|
|
log.Printf("[DEBUG] Updating OpsWorks layer: %s", d.Id())
|
|
|
|
_, 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)
|
|
}
|