provider/aws: Fix root-block-device bug

Previously the `root_block_device` config map was a `schema.TypeSet` with an empty `Set` function, and a hard-limit of 1 on the attribute block.
This prevented a user from making any real changes inside the attribute block, thus leaving the user with a `Apply complete!` message, and nothing changed.

The schema API has since been updated, and we can now specify the `root_block_device` as a `schema.TypeList` with `MaxItems` set to `1`. This fixes the issue, and allows the user to update the `aws_instance`'s `root_block_device` attribute, and see changes actually propagate.
This commit is contained in:
Jake Champlin 2017-02-01 16:25:07 -05:00
parent 2c59c9d44e
commit 3d22adbd5d
No known key found for this signature in database
GPG Key ID: DC31F41958EF4AC2
1 changed files with 53 additions and 59 deletions

View File

@ -5,6 +5,7 @@ import (
"crypto/sha1" "crypto/sha1"
"encoding/base64" "encoding/base64"
"encoding/hex" "encoding/hex"
"errors"
"fmt" "fmt"
"log" "log"
"strings" "strings"
@ -32,67 +33,67 @@ func resourceAwsInstance() *schema.Resource {
MigrateState: resourceAwsInstanceMigrateState, MigrateState: resourceAwsInstanceMigrateState,
Schema: map[string]*schema.Schema{ Schema: map[string]*schema.Schema{
"ami": &schema.Schema{ "ami": {
Type: schema.TypeString, Type: schema.TypeString,
Required: true, Required: true,
ForceNew: true, ForceNew: true,
}, },
"associate_public_ip_address": &schema.Schema{ "associate_public_ip_address": {
Type: schema.TypeBool, Type: schema.TypeBool,
ForceNew: true, ForceNew: true,
Computed: true, Computed: true,
Optional: true, Optional: true,
}, },
"availability_zone": &schema.Schema{ "availability_zone": {
Type: schema.TypeString, Type: schema.TypeString,
Optional: true, Optional: true,
Computed: true, Computed: true,
ForceNew: true, ForceNew: true,
}, },
"placement_group": &schema.Schema{ "placement_group": {
Type: schema.TypeString, Type: schema.TypeString,
Optional: true, Optional: true,
Computed: true, Computed: true,
ForceNew: true, ForceNew: true,
}, },
"instance_type": &schema.Schema{ "instance_type": {
Type: schema.TypeString, Type: schema.TypeString,
Required: true, Required: true,
ForceNew: true, ForceNew: true,
}, },
"key_name": &schema.Schema{ "key_name": {
Type: schema.TypeString, Type: schema.TypeString,
Optional: true, Optional: true,
ForceNew: true, ForceNew: true,
Computed: true, Computed: true,
}, },
"subnet_id": &schema.Schema{ "subnet_id": {
Type: schema.TypeString, Type: schema.TypeString,
Optional: true, Optional: true,
Computed: true, Computed: true,
ForceNew: true, ForceNew: true,
}, },
"private_ip": &schema.Schema{ "private_ip": {
Type: schema.TypeString, Type: schema.TypeString,
Optional: true, Optional: true,
ForceNew: true, ForceNew: true,
Computed: true, Computed: true,
}, },
"source_dest_check": &schema.Schema{ "source_dest_check": {
Type: schema.TypeBool, Type: schema.TypeBool,
Optional: true, Optional: true,
Default: true, Default: true,
}, },
"user_data": &schema.Schema{ "user_data": {
Type: schema.TypeString, Type: schema.TypeString,
Optional: true, Optional: true,
ForceNew: true, ForceNew: true,
@ -106,7 +107,7 @@ func resourceAwsInstance() *schema.Resource {
}, },
}, },
"security_groups": &schema.Schema{ "security_groups": {
Type: schema.TypeSet, Type: schema.TypeSet,
Optional: true, Optional: true,
Computed: true, Computed: true,
@ -115,7 +116,7 @@ func resourceAwsInstance() *schema.Resource {
Set: schema.HashString, Set: schema.HashString,
}, },
"vpc_security_group_ids": &schema.Schema{ "vpc_security_group_ids": {
Type: schema.TypeSet, Type: schema.TypeSet,
Optional: true, Optional: true,
Computed: true, Computed: true,
@ -123,59 +124,59 @@ func resourceAwsInstance() *schema.Resource {
Set: schema.HashString, Set: schema.HashString,
}, },
"public_dns": &schema.Schema{ "public_dns": {
Type: schema.TypeString, Type: schema.TypeString,
Computed: true, Computed: true,
}, },
"network_interface_id": &schema.Schema{ "network_interface_id": {
Type: schema.TypeString, Type: schema.TypeString,
Computed: true, Computed: true,
}, },
"public_ip": &schema.Schema{ "public_ip": {
Type: schema.TypeString, Type: schema.TypeString,
Computed: true, Computed: true,
}, },
"instance_state": &schema.Schema{ "instance_state": {
Type: schema.TypeString, Type: schema.TypeString,
Computed: true, Computed: true,
}, },
"private_dns": &schema.Schema{ "private_dns": {
Type: schema.TypeString, Type: schema.TypeString,
Computed: true, Computed: true,
}, },
"ebs_optimized": &schema.Schema{ "ebs_optimized": {
Type: schema.TypeBool, Type: schema.TypeBool,
Optional: true, Optional: true,
ForceNew: true, ForceNew: true,
}, },
"disable_api_termination": &schema.Schema{ "disable_api_termination": {
Type: schema.TypeBool, Type: schema.TypeBool,
Optional: true, Optional: true,
}, },
"instance_initiated_shutdown_behavior": &schema.Schema{ "instance_initiated_shutdown_behavior": {
Type: schema.TypeString, Type: schema.TypeString,
Optional: true, Optional: true,
}, },
"monitoring": &schema.Schema{ "monitoring": {
Type: schema.TypeBool, Type: schema.TypeBool,
Optional: true, Optional: true,
}, },
"iam_instance_profile": &schema.Schema{ "iam_instance_profile": {
Type: schema.TypeString, Type: schema.TypeString,
ForceNew: true, ForceNew: true,
Optional: true, Optional: true,
}, },
"tenancy": &schema.Schema{ "tenancy": {
Type: schema.TypeString, Type: schema.TypeString,
Optional: true, Optional: true,
Computed: true, Computed: true,
@ -184,60 +185,60 @@ func resourceAwsInstance() *schema.Resource {
"tags": tagsSchema(), "tags": tagsSchema(),
"block_device": &schema.Schema{ "block_device": {
Type: schema.TypeMap, Type: schema.TypeMap,
Optional: true, Optional: true,
Removed: "Split out into three sub-types; see Changelog and Docs", Removed: "Split out into three sub-types; see Changelog and Docs",
}, },
"ebs_block_device": &schema.Schema{ "ebs_block_device": {
Type: schema.TypeSet, Type: schema.TypeSet,
Optional: true, Optional: true,
Computed: true, Computed: true,
Elem: &schema.Resource{ Elem: &schema.Resource{
Schema: map[string]*schema.Schema{ Schema: map[string]*schema.Schema{
"delete_on_termination": &schema.Schema{ "delete_on_termination": {
Type: schema.TypeBool, Type: schema.TypeBool,
Optional: true, Optional: true,
Default: true, Default: true,
ForceNew: true, ForceNew: true,
}, },
"device_name": &schema.Schema{ "device_name": {
Type: schema.TypeString, Type: schema.TypeString,
Required: true, Required: true,
ForceNew: true, ForceNew: true,
}, },
"encrypted": &schema.Schema{ "encrypted": {
Type: schema.TypeBool, Type: schema.TypeBool,
Optional: true, Optional: true,
Computed: true, Computed: true,
ForceNew: true, ForceNew: true,
}, },
"iops": &schema.Schema{ "iops": {
Type: schema.TypeInt, Type: schema.TypeInt,
Optional: true, Optional: true,
Computed: true, Computed: true,
ForceNew: true, ForceNew: true,
}, },
"snapshot_id": &schema.Schema{ "snapshot_id": {
Type: schema.TypeString, Type: schema.TypeString,
Optional: true, Optional: true,
Computed: true, Computed: true,
ForceNew: true, ForceNew: true,
}, },
"volume_size": &schema.Schema{ "volume_size": {
Type: schema.TypeInt, Type: schema.TypeInt,
Optional: true, Optional: true,
Computed: true, Computed: true,
ForceNew: true, ForceNew: true,
}, },
"volume_type": &schema.Schema{ "volume_type": {
Type: schema.TypeString, Type: schema.TypeString,
Optional: true, Optional: true,
Computed: true, Computed: true,
@ -254,24 +255,24 @@ func resourceAwsInstance() *schema.Resource {
}, },
}, },
"ephemeral_block_device": &schema.Schema{ "ephemeral_block_device": {
Type: schema.TypeSet, Type: schema.TypeSet,
Optional: true, Optional: true,
Computed: true, Computed: true,
ForceNew: true, ForceNew: true,
Elem: &schema.Resource{ Elem: &schema.Resource{
Schema: map[string]*schema.Schema{ Schema: map[string]*schema.Schema{
"device_name": &schema.Schema{ "device_name": {
Type: schema.TypeString, Type: schema.TypeString,
Required: true, Required: true,
}, },
"virtual_name": &schema.Schema{ "virtual_name": {
Type: schema.TypeString, Type: schema.TypeString,
Optional: true, Optional: true,
}, },
"no_device": &schema.Schema{ "no_device": {
Type: schema.TypeBool, Type: schema.TypeBool,
Optional: true, Optional: true,
}, },
@ -289,41 +290,38 @@ func resourceAwsInstance() *schema.Resource {
}, },
}, },
"root_block_device": &schema.Schema{ "root_block_device": {
// TODO: This is a set because we don't support singleton Type: schema.TypeList,
// sub-resources today. We'll enforce that the set only ever has
// length zero or one below. When TF gains support for
// sub-resources this can be converted.
Type: schema.TypeSet,
Optional: true, Optional: true,
Computed: true, Computed: true,
MaxItems: 1,
Elem: &schema.Resource{ Elem: &schema.Resource{
// "You can only modify the volume size, volume type, and Delete on // "You can only modify the volume size, volume type, and Delete on
// Termination flag on the block device mapping entry for the root // Termination flag on the block device mapping entry for the root
// device volume." - bit.ly/ec2bdmap // device volume." - bit.ly/ec2bdmap
Schema: map[string]*schema.Schema{ Schema: map[string]*schema.Schema{
"delete_on_termination": &schema.Schema{ "delete_on_termination": {
Type: schema.TypeBool, Type: schema.TypeBool,
Optional: true, Optional: true,
Default: true, Default: true,
ForceNew: true, ForceNew: true,
}, },
"iops": &schema.Schema{ "iops": {
Type: schema.TypeInt, Type: schema.TypeInt,
Optional: true, Optional: true,
Computed: true, Computed: true,
ForceNew: true, ForceNew: true,
}, },
"volume_size": &schema.Schema{ "volume_size": {
Type: schema.TypeInt, Type: schema.TypeInt,
Optional: true, Optional: true,
Computed: true, Computed: true,
ForceNew: true, ForceNew: true,
}, },
"volume_type": &schema.Schema{ "volume_type": {
Type: schema.TypeString, Type: schema.TypeString,
Optional: true, Optional: true,
Computed: true, Computed: true,
@ -331,10 +329,6 @@ func resourceAwsInstance() *schema.Resource {
}, },
}, },
}, },
Set: func(v interface{}) int {
// there can be only one root device; no need to hash anything
return 0
},
}, },
}, },
} }
@ -380,12 +374,12 @@ func resourceAwsInstanceCreate(d *schema.ResourceData, meta interface{}) error {
// IAM instance profiles can take ~10 seconds to propagate in AWS: // IAM instance profiles can take ~10 seconds to propagate in AWS:
// http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html#launch-instance-with-role-console // http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html#launch-instance-with-role-console
if isAWSErr(err, "InvalidParameterValue", "Invalid IAM Instance Profile") { if isAWSErr(err, "InvalidParameterValue", "Invalid IAM Instance Profile") {
log.Printf("[DEBUG] Invalid IAM Instance Profile referenced, retrying...") log.Print("[DEBUG] Invalid IAM Instance Profile referenced, retrying...")
return resource.RetryableError(err) return resource.RetryableError(err)
} }
// IAM roles can also take time to propagate in AWS: // IAM roles can also take time to propagate in AWS:
if isAWSErr(err, "InvalidParameterValue", " has no associated IAM Roles") { if isAWSErr(err, "InvalidParameterValue", " has no associated IAM Roles") {
log.Printf("[DEBUG] IAM Instance Profile appears to have no IAM roles, retrying...") log.Print("[DEBUG] IAM Instance Profile appears to have no IAM roles, retrying...")
return resource.RetryableError(err) return resource.RetryableError(err)
} }
return resource.NonRetryableError(err) return resource.NonRetryableError(err)
@ -400,7 +394,7 @@ func resourceAwsInstanceCreate(d *schema.ResourceData, meta interface{}) error {
return fmt.Errorf("Error launching source instance: %s", err) return fmt.Errorf("Error launching source instance: %s", err)
} }
if runResp == nil || len(runResp.Instances) == 0 { if runResp == nil || len(runResp.Instances) == 0 {
return fmt.Errorf("Error launching source instance: no instances returned in response") return errors.New("Error launching source instance: no instances returned in response")
} }
instance := runResp.Instances[0] instance := runResp.Instances[0]
@ -804,7 +798,7 @@ func blockDeviceIsRoot(bd *ec2.InstanceBlockDeviceMapping, instance *ec2.Instanc
func fetchRootDeviceName(ami string, conn *ec2.EC2) (*string, error) { func fetchRootDeviceName(ami string, conn *ec2.EC2) (*string, error) {
if ami == "" { if ami == "" {
return nil, fmt.Errorf("Cannot fetch root device name for blank AMI ID.") return nil, errors.New("Cannot fetch root device name for blank AMI ID.")
} }
log.Printf("[DEBUG] Describing AMI %q to get root block device name", ami) log.Printf("[DEBUG] Describing AMI %q to get root block device name", ami)
@ -911,7 +905,7 @@ func readBlockDeviceMappingsFromConfig(
} }
if bdm.NoDevice == nil && aws.StringValue(bdm.VirtualName) == "" { if bdm.NoDevice == nil && aws.StringValue(bdm.VirtualName) == "" {
return nil, fmt.Errorf("virtual_name cannot be empty when no_device is false or undefined.") return nil, errors.New("virtual_name cannot be empty when no_device is false or undefined.")
} }
blockDevices = append(blockDevices, bdm) blockDevices = append(blockDevices, bdm)
@ -919,9 +913,9 @@ func readBlockDeviceMappingsFromConfig(
} }
if v, ok := d.GetOk("root_block_device"); ok { if v, ok := d.GetOk("root_block_device"); ok {
vL := v.(*schema.Set).List() vL := v.([]interface{})
if len(vL) > 1 { if len(vL) > 1 {
return nil, fmt.Errorf("Cannot specify more than one root_block_device.") return nil, errors.New("Cannot specify more than one root_block_device.")
} }
for _, v := range vL { for _, v := range vL {
bd := v.(map[string]interface{}) bd := v.(map[string]interface{})
@ -946,7 +940,7 @@ func readBlockDeviceMappingsFromConfig(
ebs.Iops = aws.Int64(int64(v)) ebs.Iops = aws.Int64(int64(v))
} else if v, ok := bd["iops"].(int); ok && v > 0 && *ebs.VolumeType != "io1" { } else if v, ok := bd["iops"].(int); ok && v > 0 && *ebs.VolumeType != "io1" {
// Message user about incompatibility // Message user about incompatibility
log.Printf("[WARN] IOPs is only valid for storate type io1 for EBS Volumes") log.Print("[WARN] IOPs is only valid for storate type io1 for EBS Volumes")
} }
if dn, err := fetchRootDeviceName(d.Get("ami").(string), conn); err == nil { if dn, err := fetchRootDeviceName(d.Get("ami").(string), conn); err == nil {
@ -1096,7 +1090,7 @@ func buildAwsInstanceOpts(
// See http://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_RunInstances.html // See http://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_RunInstances.html
sgs := v.(*schema.Set).List() sgs := v.(*schema.Set).List()
if len(sgs) > 0 && hasSubnet { if len(sgs) > 0 && hasSubnet {
log.Printf("[WARN] Deprecated. Attempting to use 'security_groups' within a VPC instance. Use 'vpc_security_group_ids' instead.") log.Print("[WARN] Deprecated. Attempting to use 'security_groups' within a VPC instance. Use 'vpc_security_group_ids' instead.")
} }
for _, v := range sgs { for _, v := range sgs {
str := v.(string) str := v.(string)