diff --git a/builtin/providers/aws/resource_aws_instance.go b/builtin/providers/aws/resource_aws_instance.go index 4e966e3b6..b19c90304 100644 --- a/builtin/providers/aws/resource_aws_instance.go +++ b/builtin/providers/aws/resource_aws_instance.go @@ -24,6 +24,9 @@ func resourceAwsInstance() *schema.Resource { Update: resourceAwsInstanceUpdate, Delete: resourceAwsInstanceDelete, + SchemaVersion: 1, + MigrateState: resourceAwsInstanceMigrateState, + Schema: map[string]*schema.Schema{ "ami": &schema.Schema{ Type: schema.TypeString, @@ -127,53 +130,28 @@ func resourceAwsInstance() *schema.Resource { ForceNew: true, Optional: true, }, + "tenancy": &schema.Schema{ Type: schema.TypeString, Optional: true, Computed: true, ForceNew: true, }, + "tags": tagsSchema(), "block_device": &schema.Schema{ + Type: schema.TypeMap, + Optional: true, + Removed: "Split out into three sub-types; see Changelog and Docs", + }, + + "ebs_block_device": &schema.Schema{ Type: schema.TypeSet, Optional: true, Computed: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ - "device_name": &schema.Schema{ - Type: schema.TypeString, - Required: true, - ForceNew: true, - }, - - "virtual_name": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - ForceNew: true, - }, - - "snapshot_id": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - Computed: true, - ForceNew: true, - }, - - "volume_type": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - Computed: true, - ForceNew: true, - }, - - "volume_size": &schema.Schema{ - Type: schema.TypeInt, - Optional: true, - Computed: true, - ForceNew: true, - }, - "delete_on_termination": &schema.Schema{ Type: schema.TypeBool, Optional: true, @@ -181,6 +159,12 @@ func resourceAwsInstance() *schema.Resource { ForceNew: true, }, + "device_name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "encrypted": &schema.Schema{ Type: schema.TypeBool, Optional: true, @@ -194,17 +178,79 @@ func resourceAwsInstance() *schema.Resource { Computed: true, ForceNew: true, }, + + "snapshot_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + + "volume_size": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + Computed: true, + ForceNew: true, + }, + + "volume_type": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, }, }, - Set: resourceAwsInstanceBlockDevicesHash, + Set: func(v interface{}) int { + var buf bytes.Buffer + m := v.(map[string]interface{}) + buf.WriteString(fmt.Sprintf("%t-", m["delete_on_termination"].(bool))) + buf.WriteString(fmt.Sprintf("%s-", m["device_name"].(string))) + buf.WriteString(fmt.Sprintf("%t-", m["encrypted"].(bool))) + // NOTE: Not considering IOPS in hash; when using gp2, IOPS can come + // back set to something like "33", which throws off the set + // calculation and generates an unresolvable diff. + // buf.WriteString(fmt.Sprintf("%d-", m["iops"].(int))) + buf.WriteString(fmt.Sprintf("%s-", m["snapshot_id"].(string))) + buf.WriteString(fmt.Sprintf("%d-", m["volume_size"].(int))) + buf.WriteString(fmt.Sprintf("%s-", m["volume_type"].(string))) + return hashcode.String(buf.String()) + }, + }, + + "ephemeral_block_device": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + Computed: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "device_name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "virtual_name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + }, + }, + Set: func(v interface{}) int { + var buf bytes.Buffer + m := v.(map[string]interface{}) + buf.WriteString(fmt.Sprintf("%s-", m["device_name"].(string))) + buf.WriteString(fmt.Sprintf("%s-", m["virtual_name"].(string))) + return hashcode.String(buf.String()) + }, }, "root_block_device": &schema.Schema{ - // TODO: This is a list because we don't support singleton - // sub-resources today. We'll enforce that the list only ever has + // TODO: This is a set because we don't support singleton + // 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.TypeList, + Type: schema.TypeSet, Optional: true, Computed: true, Elem: &schema.Resource{ @@ -226,6 +272,13 @@ func resourceAwsInstance() *schema.Resource { Default: "/dev/sda1", }, + "iops": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + Computed: true, + ForceNew: true, + }, + "volume_size": &schema.Schema{ Type: schema.TypeInt, Optional: true, @@ -239,15 +292,19 @@ func resourceAwsInstance() *schema.Resource { Computed: true, ForceNew: true, }, - - "iops": &schema.Schema{ - Type: schema.TypeInt, - Optional: true, - Computed: true, - ForceNew: true, - }, }, }, + Set: func(v interface{}) int { + var buf bytes.Buffer + m := v.(map[string]interface{}) + buf.WriteString(fmt.Sprintf("%t-", m["delete_on_termination"].(bool))) + buf.WriteString(fmt.Sprintf("%s-", m["device_name"].(string))) + // See the NOTE in "ebs_block_device" for why we skip iops here. + // buf.WriteString(fmt.Sprintf("%d-", m["iops"].(int))) + buf.WriteString(fmt.Sprintf("%d-", m["volume_size"].(int))) + buf.WriteString(fmt.Sprintf("%s-", m["volume_type"].(string))) + return hashcode.String(buf.String()) + }, }, }, } @@ -349,46 +406,87 @@ func resourceAwsInstanceCreate(d *schema.ResourceData, meta interface{}) error { runOpts.KeyName = aws.String(v.(string)) } - blockDevices := make([]interface{}, 0) + blockDevices := make([]ec2.BlockDeviceMapping, 0) - if v := d.Get("block_device"); v != nil { - blockDevices = append(blockDevices, v.(*schema.Set).List()...) - } - - if v := d.Get("root_block_device"); v != nil { - rootBlockDevices := v.([]interface{}) - if len(rootBlockDevices) > 1 { - return fmt.Errorf("Cannot specify more than one root_block_device.") - } - blockDevices = append(blockDevices, rootBlockDevices...) - } - - if len(blockDevices) > 0 { - runOpts.BlockDeviceMappings = make([]ec2.BlockDeviceMapping, len(blockDevices)) - for i, v := range blockDevices { + if v, ok := d.GetOk("ebs_block_device"); ok { + vL := v.(*schema.Set).List() + for _, v := range vL { bd := v.(map[string]interface{}) - runOpts.BlockDeviceMappings[i].DeviceName = aws.String(bd["device_name"].(string)) - runOpts.BlockDeviceMappings[i].EBS = &ec2.EBSBlockDevice{ - VolumeType: aws.String(bd["volume_type"].(string)), - VolumeSize: aws.Integer(bd["volume_size"].(int)), + ebs := &ec2.EBSBlockDevice{ DeleteOnTermination: aws.Boolean(bd["delete_on_termination"].(bool)), } - if v, ok := bd["virtual_name"].(string); ok { - runOpts.BlockDeviceMappings[i].VirtualName = aws.String(v) - } if v, ok := bd["snapshot_id"].(string); ok && v != "" { - runOpts.BlockDeviceMappings[i].EBS.SnapshotID = aws.String(v) + ebs.SnapshotID = aws.String(v) } - if v, ok := bd["encrypted"].(bool); ok { - runOpts.BlockDeviceMappings[i].EBS.Encrypted = aws.Boolean(v) + + if v, ok := bd["volume_size"].(int); ok && v != 0 { + ebs.VolumeSize = aws.Integer(v) } + + if v, ok := bd["volume_type"].(string); ok && v != "" { + ebs.VolumeType = aws.String(v) + } + if v, ok := bd["iops"].(int); ok && v > 0 { - runOpts.BlockDeviceMappings[i].EBS.IOPS = aws.Integer(v) + ebs.IOPS = aws.Integer(v) } + + blockDevices = append(blockDevices, ec2.BlockDeviceMapping{ + DeviceName: aws.String(bd["device_name"].(string)), + EBS: ebs, + }) } } + if v, ok := d.GetOk("ephemeral_block_device"); ok { + vL := v.(*schema.Set).List() + for _, v := range vL { + bd := v.(map[string]interface{}) + blockDevices = append(blockDevices, ec2.BlockDeviceMapping{ + DeviceName: aws.String(bd["device_name"].(string)), + VirtualName: aws.String(bd["virtual_name"].(string)), + }) + } + // if err := d.Set("ephemeral_block_device", vL); err != nil { + // return err + // } + } + + if v, ok := d.GetOk("root_block_device"); ok { + vL := v.(*schema.Set).List() + if len(vL) > 1 { + return fmt.Errorf("Cannot specify more than one root_block_device.") + } + for _, v := range vL { + bd := v.(map[string]interface{}) + ebs := &ec2.EBSBlockDevice{ + DeleteOnTermination: aws.Boolean(bd["delete_on_termination"].(bool)), + } + + if v, ok := bd["volume_size"].(int); ok && v != 0 { + ebs.VolumeSize = aws.Integer(v) + } + + if v, ok := bd["volume_type"].(string); ok && v != "" { + ebs.VolumeType = aws.String(v) + } + + if v, ok := bd["iops"].(int); ok && v > 0 { + ebs.IOPS = aws.Integer(v) + } + + blockDevices = append(blockDevices, ec2.BlockDeviceMapping{ + DeviceName: aws.String(bd["device_name"].(string)), + EBS: ebs, + }) + } + } + + if len(blockDevices) > 0 { + runOpts.BlockDeviceMappings = blockDevices + } + // Create the instance log.Printf("[DEBUG] Run configuration: %#v", runOpts) runResp, err := ec2conn.RunInstances(runOpts) @@ -520,50 +618,10 @@ func resourceAwsInstanceRead(d *schema.ResourceData, meta interface{}) error { } d.Set("security_groups", sgs) - blockDevices := make(map[string]ec2.InstanceBlockDeviceMapping) - for _, bd := range instance.BlockDeviceMappings { - blockDevices[*bd.EBS.VolumeID] = bd - } - - volIDs := make([]string, 0, len(blockDevices)) - for _, vol := range blockDevices { - volIDs = append(volIDs, *vol.EBS.VolumeID) - } - - volResp, err := ec2conn.DescribeVolumes(&ec2.DescribeVolumesRequest{ - VolumeIDs: volIDs, - }) - if err != nil { + if err := readBlockDevices(d, instance, ec2conn); err != nil { return err } - nonRootBlockDevices := make([]map[string]interface{}, 0) - rootBlockDevice := make([]interface{}, 0, 1) - for _, vol := range volResp.Volumes { - blockDevice := make(map[string]interface{}) - blockDevice["device_name"] = *blockDevices[*vol.VolumeID].DeviceName - blockDevice["volume_type"] = *vol.VolumeType - blockDevice["volume_size"] = *vol.Size - if vol.IOPS != nil { - blockDevice["iops"] = *vol.IOPS - } - blockDevice["delete_on_termination"] = - *blockDevices[*vol.VolumeID].EBS.DeleteOnTermination - - // If this is the root device, save it. We stop here since we - // can't put invalid keys into this map. - if blockDevice["device_name"] == *instance.RootDeviceName { - rootBlockDevice = []interface{}{blockDevice} - continue - } - - blockDevice["snapshot_id"] = *vol.SnapshotID - blockDevice["encrypted"] = *vol.Encrypted - nonRootBlockDevices = append(nonRootBlockDevices, blockDevice) - } - d.Set("block_device", nonRootBlockDevices) - d.Set("root_block_device", rootBlockDevice) - return nil } @@ -659,11 +717,89 @@ func InstanceStateRefreshFunc(conn *ec2.EC2, instanceID string) resource.StateRe } } -func resourceAwsInstanceBlockDevicesHash(v interface{}) int { - var buf bytes.Buffer - m := v.(map[string]interface{}) - buf.WriteString(fmt.Sprintf("%s-", m["device_name"].(string))) - buf.WriteString(fmt.Sprintf("%s-", m["virtual_name"].(string))) - buf.WriteString(fmt.Sprintf("%t-", m["delete_on_termination"].(bool))) - return hashcode.String(buf.String()) +func readBlockDevices(d *schema.ResourceData, instance *ec2.Instance, ec2conn *ec2.EC2) error { + ibds, err := readBlockDevicesFromInstance(instance, ec2conn) + if err != nil { + return err + } + + if err := d.Set("ebs_block_device", ibds["ebs"]); err != nil { + return err + } + if ibds["root"] != nil { + if err := d.Set("root_block_device", []interface{}{ibds["root"]}); err != nil { + return err + } + } + + return nil +} + +func readBlockDevicesFromInstance(instance *ec2.Instance, ec2conn *ec2.EC2) (map[string]interface{}, error) { + blockDevices := make(map[string]interface{}) + blockDevices["ebs"] = make([]map[string]interface{}, 0) + blockDevices["root"] = nil + + instanceBlockDevices := make(map[string]ec2.InstanceBlockDeviceMapping) + for _, bd := range instance.BlockDeviceMappings { + if bd.EBS != nil { + instanceBlockDevices[*(bd.EBS.VolumeID)] = bd + } + } + + volIDs := make([]string, 0, len(instanceBlockDevices)) + for volID := range instanceBlockDevices { + volIDs = append(volIDs, volID) + } + + // Need to call DescribeVolumes to get volume_size and volume_type for each + // EBS block device + volResp, err := ec2conn.DescribeVolumes(&ec2.DescribeVolumesRequest{ + VolumeIDs: volIDs, + }) + if err != nil { + return nil, err + } + + for _, vol := range volResp.Volumes { + instanceBd := instanceBlockDevices[*vol.VolumeID] + bd := make(map[string]interface{}) + + if instanceBd.EBS != nil && instanceBd.EBS.DeleteOnTermination != nil { + bd["delete_on_termination"] = *instanceBd.EBS.DeleteOnTermination + } + if instanceBd.DeviceName != nil { + bd["device_name"] = *instanceBd.DeviceName + } + if vol.Size != nil { + bd["volume_size"] = *vol.Size + } + if vol.VolumeType != nil { + bd["volume_type"] = *vol.VolumeType + } + if vol.IOPS != nil { + bd["iops"] = *vol.IOPS + } + + if blockDeviceIsRoot(instanceBd, instance) { + blockDevices["root"] = bd + } else { + if vol.Encrypted != nil { + bd["encrypted"] = *vol.Encrypted + } + if vol.SnapshotID != nil { + bd["snapshot_id"] = *vol.SnapshotID + } + + blockDevices["ebs"] = append(blockDevices["ebs"].([]map[string]interface{}), bd) + } + } + + return blockDevices, nil +} + +func blockDeviceIsRoot(bd ec2.InstanceBlockDeviceMapping, instance *ec2.Instance) bool { + return (bd.DeviceName != nil && + instance.RootDeviceName != nil && + *bd.DeviceName == *instance.RootDeviceName) } diff --git a/builtin/providers/aws/resource_aws_instance_migrate.go b/builtin/providers/aws/resource_aws_instance_migrate.go new file mode 100644 index 000000000..adb7a01ec --- /dev/null +++ b/builtin/providers/aws/resource_aws_instance_migrate.go @@ -0,0 +1,107 @@ +package aws + +import ( + "fmt" + "log" + "strconv" + "strings" + + "github.com/hashicorp/terraform/helper/hashcode" + "github.com/hashicorp/terraform/terraform" +) + +func resourceAwsInstanceMigrateState( + v int, is *terraform.InstanceState, meta interface{}) (*terraform.InstanceState, error) { + switch v { + case 0: + log.Println("[INFO] Found AWS Instance State v0; migrating to v1") + return migrateStateV0toV1(is) + default: + return is, fmt.Errorf("Unexpected schema version: %d", v) + } + + return is, nil +} + +func migrateStateV0toV1(is *terraform.InstanceState) (*terraform.InstanceState, error) { + log.Printf("[DEBUG] Attributes before migration: %#v", is.Attributes) + // Delete old count + delete(is.Attributes, "block_device.#") + + oldBds, err := readV0BlockDevices(is) + if err != nil { + return is, err + } + // seed count fields for new types + is.Attributes["ebs_block_device.#"] = "0" + is.Attributes["ephemeral_block_device.#"] = "0" + // depending on if state was v0.3.7 or an earlier version, it might have + // root_block_device defined already + if _, ok := is.Attributes["root_block_device.#"]; !ok { + is.Attributes["root_block_device.#"] = "0" + } + for _, oldBd := range oldBds { + if err := writeV1BlockDevice(is, oldBd); err != nil { + return is, err + } + } + log.Printf("[DEBUG] Attributes after migration: %#v", is.Attributes) + return is, nil +} + +func readV0BlockDevices(is *terraform.InstanceState) (map[string]map[string]string, error) { + oldBds := make(map[string]map[string]string) + for k, v := range is.Attributes { + if !strings.HasPrefix(k, "block_device.") { + continue + } + path := strings.Split(k, ".") + if len(path) != 3 { + return oldBds, fmt.Errorf("Found unexpected block_device field: %#v", k) + } + hashcode, attribute := path[1], path[2] + oldBd, ok := oldBds[hashcode] + if !ok { + oldBd = make(map[string]string) + oldBds[hashcode] = oldBd + } + oldBd[attribute] = v + delete(is.Attributes, k) + } + return oldBds, nil +} + +func writeV1BlockDevice( + is *terraform.InstanceState, oldBd map[string]string) error { + code := hashcode.String(oldBd["device_name"]) + bdType := "ebs_block_device" + if vn, ok := oldBd["virtual_name"]; ok && strings.HasPrefix(vn, "ephemeral") { + bdType = "ephemeral_block_device" + } else if dn, ok := oldBd["device_name"]; ok && dn == "/dev/sda1" { + bdType = "root_block_device" + } + + switch bdType { + case "ebs_block_device": + delete(oldBd, "virtual_name") + case "root_block_device": + delete(oldBd, "virtual_name") + delete(oldBd, "encrypted") + delete(oldBd, "snapshot_id") + case "ephemeral_block_device": + delete(oldBd, "delete_on_termination") + delete(oldBd, "encrypted") + delete(oldBd, "iops") + delete(oldBd, "volume_size") + delete(oldBd, "volume_type") + } + for attr, val := range oldBd { + attrKey := fmt.Sprintf("%s.%d.%s", bdType, code, attr) + is.Attributes[attrKey] = val + } + + countAttr := fmt.Sprintf("%s.#", bdType) + count, _ := strconv.Atoi(is.Attributes[countAttr]) + is.Attributes[countAttr] = strconv.Itoa(count + 1) + return nil +} diff --git a/builtin/providers/aws/resource_aws_instance_migrate_test.go b/builtin/providers/aws/resource_aws_instance_migrate_test.go new file mode 100644 index 000000000..5738f2508 --- /dev/null +++ b/builtin/providers/aws/resource_aws_instance_migrate_test.go @@ -0,0 +1,135 @@ +package aws + +import ( + "testing" + + "github.com/hashicorp/terraform/terraform" +) + +func TestAWSInstanceMigrateState(t *testing.T) { + cases := map[string]struct { + StateVersion int + Attributes map[string]string + Expected map[string]string + Meta interface{} + }{ + "v0.3.6 and earlier": { + StateVersion: 0, + Attributes: map[string]string{ + // EBS + "block_device.#": "2", + "block_device.3851383343.delete_on_termination": "true", + "block_device.3851383343.device_name": "/dev/sdx", + "block_device.3851383343.encrypted": "false", + "block_device.3851383343.snapshot_id": "", + "block_device.3851383343.virtual_name": "", + "block_device.3851383343.volume_size": "5", + "block_device.3851383343.volume_type": "standard", + // Ephemeral + "block_device.3101711606.delete_on_termination": "false", + "block_device.3101711606.device_name": "/dev/sdy", + "block_device.3101711606.encrypted": "false", + "block_device.3101711606.snapshot_id": "", + "block_device.3101711606.virtual_name": "ephemeral0", + "block_device.3101711606.volume_size": "", + "block_device.3101711606.volume_type": "", + // Root + "block_device.56575650.delete_on_termination": "true", + "block_device.56575650.device_name": "/dev/sda1", + "block_device.56575650.encrypted": "false", + "block_device.56575650.snapshot_id": "", + "block_device.56575650.volume_size": "10", + "block_device.56575650.volume_type": "standard", + }, + Expected: map[string]string{ + "ebs_block_device.#": "1", + "ebs_block_device.3851383343.delete_on_termination": "true", + "ebs_block_device.3851383343.device_name": "/dev/sdx", + "ebs_block_device.3851383343.encrypted": "false", + "ebs_block_device.3851383343.snapshot_id": "", + "ebs_block_device.3851383343.volume_size": "5", + "ebs_block_device.3851383343.volume_type": "standard", + "ephemeral_block_device.#": "1", + "ephemeral_block_device.2458403513.device_name": "/dev/sdy", + "ephemeral_block_device.2458403513.virtual_name": "ephemeral0", + "root_block_device.#": "1", + "root_block_device.3018388612.delete_on_termination": "true", + "root_block_device.3018388612.device_name": "/dev/sda1", + "root_block_device.3018388612.snapshot_id": "", + "root_block_device.3018388612.volume_size": "10", + "root_block_device.3018388612.volume_type": "standard", + }, + }, + "v0.3.7": { + StateVersion: 0, + Attributes: map[string]string{ + // EBS + "block_device.#": "2", + "block_device.3851383343.delete_on_termination": "true", + "block_device.3851383343.device_name": "/dev/sdx", + "block_device.3851383343.encrypted": "false", + "block_device.3851383343.snapshot_id": "", + "block_device.3851383343.virtual_name": "", + "block_device.3851383343.volume_size": "5", + "block_device.3851383343.volume_type": "standard", + "block_device.3851383343.iops": "", + // Ephemeral + "block_device.3101711606.delete_on_termination": "false", + "block_device.3101711606.device_name": "/dev/sdy", + "block_device.3101711606.encrypted": "false", + "block_device.3101711606.snapshot_id": "", + "block_device.3101711606.virtual_name": "ephemeral0", + "block_device.3101711606.volume_size": "", + "block_device.3101711606.volume_type": "", + "block_device.3101711606.iops": "", + // Root + "root_block_device.#": "1", + "root_block_device.3018388612.delete_on_termination": "true", + "root_block_device.3018388612.device_name": "/dev/sda1", + "root_block_device.3018388612.snapshot_id": "", + "root_block_device.3018388612.volume_size": "10", + "root_block_device.3018388612.volume_type": "io1", + "root_block_device.3018388612.iops": "1000", + }, + Expected: map[string]string{ + "ebs_block_device.#": "1", + "ebs_block_device.3851383343.delete_on_termination": "true", + "ebs_block_device.3851383343.device_name": "/dev/sdx", + "ebs_block_device.3851383343.encrypted": "false", + "ebs_block_device.3851383343.snapshot_id": "", + "ebs_block_device.3851383343.volume_size": "5", + "ebs_block_device.3851383343.volume_type": "standard", + "ephemeral_block_device.#": "1", + "ephemeral_block_device.2458403513.device_name": "/dev/sdy", + "ephemeral_block_device.2458403513.virtual_name": "ephemeral0", + "root_block_device.#": "1", + "root_block_device.3018388612.delete_on_termination": "true", + "root_block_device.3018388612.device_name": "/dev/sda1", + "root_block_device.3018388612.snapshot_id": "", + "root_block_device.3018388612.volume_size": "10", + "root_block_device.3018388612.volume_type": "io1", + "root_block_device.3018388612.iops": "1000", + }, + }, + } + + for tn, tc := range cases { + is := &terraform.InstanceState{ + Attributes: tc.Attributes, + } + is, err := resourceAwsInstanceMigrateState( + tc.StateVersion, is, tc.Meta) + + if err != nil { + t.Fatalf("bad: %s, err: %#v", tn, err) + } + + for k, v := range tc.Expected { + if is.Attributes[k] != v { + t.Fatalf( + "bad: %s\n\n expected: %#v -> %#v\n got: %#v -> %#v\n in: %#v", + tn, k, v, k, is.Attributes[k], is.Attributes) + } + } + } +} diff --git a/builtin/providers/aws/resource_aws_instance_test.go b/builtin/providers/aws/resource_aws_instance_test.go index 941dd5dd4..9e847c43b 100644 --- a/builtin/providers/aws/resource_aws_instance_test.go +++ b/builtin/providers/aws/resource_aws_instance_test.go @@ -111,31 +111,33 @@ func TestAccAWSInstance_blockDevices(t *testing.T) { resource.TestCheckResourceAttr( "aws_instance.foo", "root_block_device.#", "1"), resource.TestCheckResourceAttr( - "aws_instance.foo", "root_block_device.0.device_name", "/dev/sda1"), + "aws_instance.foo", "root_block_device.3018388612.device_name", "/dev/sda1"), resource.TestCheckResourceAttr( - "aws_instance.foo", "root_block_device.0.volume_size", "11"), - // this one is important because it's the only root_block_device - // attribute that comes back from the API. so checking it verifies - // that we set state properly + "aws_instance.foo", "root_block_device.3018388612.volume_size", "11"), resource.TestCheckResourceAttr( - "aws_instance.foo", "root_block_device.0.volume_type", "gp2"), + "aws_instance.foo", "root_block_device.3018388612.volume_type", "gp2"), resource.TestCheckResourceAttr( - "aws_instance.foo", "block_device.#", "2"), + "aws_instance.foo", "ebs_block_device.#", "2"), resource.TestCheckResourceAttr( - "aws_instance.foo", "block_device.172787947.device_name", "/dev/sdb"), + "aws_instance.foo", "ebs_block_device.418220885.device_name", "/dev/sdb"), resource.TestCheckResourceAttr( - "aws_instance.foo", "block_device.172787947.volume_size", "9"), + "aws_instance.foo", "ebs_block_device.418220885.volume_size", "9"), resource.TestCheckResourceAttr( - "aws_instance.foo", "block_device.172787947.iops", "0"), - // Check provisioned SSD device + "aws_instance.foo", "ebs_block_device.418220885.volume_type", "standard"), resource.TestCheckResourceAttr( - "aws_instance.foo", "block_device.3336996981.volume_type", "io1"), + "aws_instance.foo", "ebs_block_device.1877654467.device_name", "/dev/sdc"), resource.TestCheckResourceAttr( - "aws_instance.foo", "block_device.3336996981.device_name", "/dev/sdc"), + "aws_instance.foo", "ebs_block_device.1877654467.volume_size", "10"), resource.TestCheckResourceAttr( - "aws_instance.foo", "block_device.3336996981.volume_size", "10"), + "aws_instance.foo", "ebs_block_device.1877654467.volume_type", "io1"), resource.TestCheckResourceAttr( - "aws_instance.foo", "block_device.3336996981.iops", "100"), + "aws_instance.foo", "ebs_block_device.1877654467.iops", "100"), + resource.TestCheckResourceAttr( + "aws_instance.foo", "ephemeral_block_device.#", "1"), + resource.TestCheckResourceAttr( + "aws_instance.foo", "ephemeral_block_device.2087552357.device_name", "/dev/sde"), + resource.TestCheckResourceAttr( + "aws_instance.foo", "ephemeral_block_device.2087552357.virtual_name", "ephemeral0"), testCheck(), ), }, @@ -420,21 +422,26 @@ resource "aws_instance" "foo" { # us-west-2 ami = "ami-55a7ea65" instance_type = "m1.small" + root_block_device { device_name = "/dev/sda1" volume_type = "gp2" volume_size = 11 } - block_device { + ebs_block_device { device_name = "/dev/sdb" volume_size = 9 } - block_device { + ebs_block_device { device_name = "/dev/sdc" volume_size = 10 volume_type = "io1" iops = 100 } + ephemeral_block_device { + device_name = "/dev/sde" + virtual_name = "ephemeral0" + } } ` diff --git a/helper/schema/resource.go b/helper/schema/resource.go index a19912eed..797d021ab 100644 --- a/helper/schema/resource.go +++ b/helper/schema/resource.go @@ -151,7 +151,7 @@ func (r *Resource) Apply( err = r.Update(data, meta) } - return data.State(), err + return r.recordCurrentSchemaVersion(data.State()), err } // Diff returns a diff of this resource and is API compatible with the @@ -207,14 +207,7 @@ func (r *Resource) Refresh( state = nil } - if state != nil && r.SchemaVersion > 0 { - if state.Meta == nil { - state.Meta = make(map[string]string) - } - state.Meta["schema_version"] = strconv.Itoa(r.SchemaVersion) - } - - return state, err + return r.recordCurrentSchemaVersion(state), err } // InternalValidate should be called to validate the structure @@ -241,3 +234,14 @@ func (r *Resource) checkSchemaVersion(is *terraform.InstanceState) (bool, int) { stateSchemaVersion, _ := strconv.Atoi(is.Meta["schema_version"]) return stateSchemaVersion < r.SchemaVersion, stateSchemaVersion } + +func (r *Resource) recordCurrentSchemaVersion( + state *terraform.InstanceState) *terraform.InstanceState { + if state != nil && r.SchemaVersion > 0 { + if state.Meta == nil { + state.Meta = make(map[string]string) + } + state.Meta["schema_version"] = strconv.Itoa(r.SchemaVersion) + } + return state +} diff --git a/helper/schema/resource_test.go b/helper/schema/resource_test.go index b1c42721f..e406e55b9 100644 --- a/helper/schema/resource_test.go +++ b/helper/schema/resource_test.go @@ -11,6 +11,7 @@ import ( func TestResourceApply_create(t *testing.T) { r := &Resource{ + SchemaVersion: 2, Schema: map[string]*Schema{ "foo": &Schema{ Type: TypeInt, @@ -51,6 +52,9 @@ func TestResourceApply_create(t *testing.T) { "id": "foo", "foo": "42", }, + Meta: map[string]string{ + "schema_version": "2", + }, } if !reflect.DeepEqual(actual, expected) { @@ -339,6 +343,7 @@ func TestResourceInternalValidate(t *testing.T) { func TestResourceRefresh(t *testing.T) { r := &Resource{ + SchemaVersion: 2, Schema: map[string]*Schema{ "foo": &Schema{ Type: TypeInt, @@ -368,6 +373,9 @@ func TestResourceRefresh(t *testing.T) { "id": "bar", "foo": "13", }, + Meta: map[string]string{ + "schema_version": "2", + }, } actual, err := r.Refresh(s, 42) diff --git a/terraform/state.go b/terraform/state.go index 3dbad7ae6..42e9023ba 100644 --- a/terraform/state.go +++ b/terraform/state.go @@ -843,6 +843,9 @@ func (i *InstanceState) init() { if i.Attributes == nil { i.Attributes = make(map[string]string) } + if i.Meta == nil { + i.Meta = make(map[string]string) + } i.Ephemeral.init() } @@ -860,6 +863,12 @@ func (i *InstanceState) deepcopy() *InstanceState { n.Attributes[k] = v } } + if i.Meta != nil { + n.Meta = make(map[string]string, len(i.Meta)) + for k, v := range i.Meta { + n.Meta[k] = v + } + } return n } diff --git a/website/source/docs/providers/aws/r/instance.html.markdown b/website/source/docs/providers/aws/r/instance.html.markdown index 94f042af3..545dff44b 100644 --- a/website/source/docs/providers/aws/r/instance.html.markdown +++ b/website/source/docs/providers/aws/r/instance.html.markdown @@ -14,7 +14,8 @@ and deleted. Instances also support [provisioning](/docs/provisioners/index.html ## Example Usage ``` -# Create a new instance of the ami-1234 on an m1.small node with an AWS Tag naming it "HelloWorld" +# Create a new instance of the ami-1234 on an m1.small node +# with an AWS Tag naming it "HelloWorld" resource "aws_instance" "web" { ami = "ami-1234" instance_type = "m1.small" @@ -47,32 +48,79 @@ The following arguments are supported: * `iam_instance_profile` - (Optional) The IAM Instance Profile to launch the instance with. * `tags` - (Optional) A mapping of tags to assign to the resource. -* `block_device` - (Optional) A list of block devices to add. Their keys are documented below. * `root_block_device` - (Optional) Customize details about the root block - device of the instance. Available keys are documented below. + device of the instance. See [Block Devices](#block-devices) below for details. +* `ebs_block_device` - (Optional) Additional EBS block devices to attach to the + instance. See [Block Devices](#block-devices) below for details. +* `ephemeral_block_device` - (Optional) Customize Ephemeral (also known as + "Instance Store") volumes on the instance. See [Block Devices](#block-devices) below for details. -Each `block_device` supports the following: -* `device_name` - The name of the device to mount. -* `virtual_name` - (Optional) The virtual device name. -* `snapshot_id` - (Optional) The Snapshot ID to mount. -* `volume_type` - (Optional) The type of volume. Can be standard, gp2, or io1. Defaults to standard. -* `volume_size` - (Optional) The size of the volume in gigabytes. -* `iops` - (Optional) The amount of provisioned IOPS. Setting this implies a - volume_type of "io1". -* `delete_on_termination` - (Optional) Should the volume be destroyed on instance termination (defaults true). -* `encrypted` - (Optional) Should encryption be enabled (defaults false). + +## Block devices + +Each of the `*_block_device` attributes controls a portion of the AWS +Instance's "Block Device Mapping". It's a good idea to familiarize yourself with [AWS's Block Device +Mapping docs](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/block-device-mapping-concepts.html) +to understand the implications of using these attributes. The `root_block_device` mapping supports the following: * `device_name` - The name of the root device on the target instance. Must - match the root device as defined in the AMI. Defaults to "/dev/sda1", which + match the root device as defined in the AMI. Defaults to `"/dev/sda1"`, which is the typical root volume for Linux instances. -* `volume_type` - (Optional) The type of volume. Can be standard, gp2, or io1. Defaults to standard. +* `volume_type` - (Optional) The type of volume. Can be `"standard"`, `"gp2"`, + or `"io1"`. (Default: `"standard"`). * `volume_size` - (Optional) The size of the volume in gigabytes. -* `iops` - (Optional) The amount of provisioned IOPS. Setting this implies a - volume_type of "io1". -* `delete_on_termination` - (Optional) Should the volume be destroyed on instance termination (defaults true). +* `iops` - (Optional) The amount of provisioned + [IOPS](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-io-characteristics.html). + This must be set with a `volume_type` of `"io1"`. +* `delete_on_termination` - (Optional) Whether the volume should be destroyed + on instance termination (Default: `true`). + +Modifying any of the `root_block_device` settings requires resource +replacement. + +Each `ebs_block_device` supports the following: + +* `device_name` - The name of the device to mount. +* `snapshot_id` - (Optional) The Snapshot ID to mount. +* `volume_type` - (Optional) The type of volume. Can be `"standard"`, `"gp2"`, + or `"io1"`. (Default: `"standard"`). +* `volume_size` - (Optional) The size of the volume in gigabytes. +* `iops` - (Optional) The amount of provisioned + [IOPS](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-io-characteristics.html). + This must be set with a `volume_type` of `"io1"`. +* `delete_on_termination` - (Optional) Whether the volume should be destroyed + on instance termination (Default: `true`). +* `encrypted` - (Optional) Enables [EBS + encryption](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSEncryption.html) + on the volume (Default: `false`). + +Modifying any `ebs_block_device` currently requires resource replacement. + +Each `ephemeral_block_device` supports the following: + +* `device_name` - The name of the block device to mount on the instance. +* `virtual_name` - The [Instance Store Device + Name](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/InstanceStorage.html#InstanceStoreDeviceNames) + (e.g. `"ephemeral0"`) + +Each AWS Instance type has a different set of Instance Store block devices +available for attachment. AWS [publishes a +list](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/InstanceStorage.html#StorageOnInstanceTypes) +of which ephemeral devices are available on each type. The devices are always +identified by the `virtual_name` in the format `"ephemeral{0..N}"`. + + +~> **NOTE:** Because AWS [does not expose Instance Store mapping +details](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/block-device-mapping-concepts.html#bdm-instance-metadata) +via an externally accessible API, `ephemeral_block_device` configuration may +only be applied at instance creation time, and changes to configuration of +existing resources cannot be detected by Terraform. Updates to Instance Store +block device configuration can be manually triggered by using the [`taint` +command](/docs/commands/taint.html). + ## Attributes Reference