diff --git a/builtin/providers/aws/resource_aws_instance.go b/builtin/providers/aws/resource_aws_instance.go index 65e348d34..4e51e6869 100644 --- a/builtin/providers/aws/resource_aws_instance.go +++ b/builtin/providers/aws/resource_aws_instance.go @@ -200,6 +200,8 @@ func resourceAwsInstance() *schema.Resource { "tags": tagsSchema(), + "volume_tags": tagsSchema(), + "block_device": { Type: schema.TypeMap, Optional: true, @@ -396,6 +398,34 @@ func resourceAwsInstanceCreate(d *schema.ResourceData, meta interface{}) error { runOpts.Ipv6Addresses = ipv6Addresses } + tagsSpec := make([]*ec2.TagSpecification, 0) + + if v, ok := d.GetOk("tags"); ok { + tags := tagsFromMap(v.(map[string]interface{})) + + spec := &ec2.TagSpecification{ + ResourceType: aws.String("instance"), + Tags: tags, + } + + tagsSpec = append(tagsSpec, spec) + } + + if v, ok := d.GetOk("volume_tags"); ok { + tags := tagsFromMap(v.(map[string]interface{})) + + spec := &ec2.TagSpecification{ + ResourceType: aws.String("volume"), + Tags: tags, + } + + tagsSpec = append(tagsSpec, spec) + } + + if len(tagsSpec) > 0 { + runOpts.TagSpecifications = tagsSpec + } + // Create the instance log.Printf("[DEBUG] Run configuration: %s", runOpts) @@ -563,6 +593,10 @@ func resourceAwsInstanceRead(d *schema.ResourceData, meta interface{}) error { d.Set("tags", tagsToMap(instance.Tags)) + if err := readVolumeTags(conn, d); err != nil { + return err + } + if err := readSecurityGroups(d, instance); err != nil { return err } @@ -605,16 +639,27 @@ func resourceAwsInstanceUpdate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).ec2conn d.Partial(true) - if err := setTags(conn, d); err != nil { - return err - } else { - d.SetPartial("tags") + + if d.HasChange("tags") && !d.IsNewResource() { + if err := setTags(conn, d); err != nil { + return err + } else { + d.SetPartial("tags") + } + } + + if d.HasChange("volume_tags") && !d.IsNewResource() { + if err := setVolumeTags(conn, d); err != nil { + return err + } else { + d.SetPartial("volume_tags") + } } if d.HasChange("iam_instance_profile") && !d.IsNewResource() { request := &ec2.DescribeIamInstanceProfileAssociationsInput{ Filters: []*ec2.Filter{ - &ec2.Filter{ + { Name: aws.String("instance-id"), Values: []*string{aws.String(d.Id())}, }, @@ -1125,6 +1170,39 @@ func readBlockDeviceMappingsFromConfig( return blockDevices, nil } +func readVolumeTags(conn *ec2.EC2, d *schema.ResourceData) error { + volumeIds, err := getAwsInstanceVolumeIds(conn, d) + if err != nil { + return err + } + + tagsResp, err := conn.DescribeTags(&ec2.DescribeTagsInput{ + Filters: []*ec2.Filter{ + { + Name: aws.String("resource-id"), + Values: volumeIds, + }, + }, + }) + if err != nil { + return err + } + + var tags []*ec2.Tag + + for _, t := range tagsResp.Tags { + tag := &ec2.Tag{ + Key: t.Key, + Value: t.Value, + } + tags = append(tags, tag) + } + + d.Set("volume_tags", tagsToMap(tags)) + + return nil +} + // Determine whether we're referring to security groups with // IDs or names. We use a heuristic to figure this out. By default, // we use IDs if we're in a VPC. However, if we previously had an @@ -1372,3 +1450,27 @@ func userDataHashSum(user_data string) string { hash := sha1.Sum(v) return hex.EncodeToString(hash[:]) } + +func getAwsInstanceVolumeIds(conn *ec2.EC2, d *schema.ResourceData) ([]*string, error) { + volumeIds := make([]*string, 0) + + opts := &ec2.DescribeVolumesInput{ + Filters: []*ec2.Filter{ + { + Name: aws.String("attachment.instance-id"), + Values: []*string{aws.String(d.Id())}, + }, + }, + } + + resp, err := conn.DescribeVolumes(opts) + if err != nil { + return nil, err + } + + for _, v := range resp.Volumes { + volumeIds = append(volumeIds, v.VolumeId) + } + + return volumeIds, nil +} diff --git a/builtin/providers/aws/resource_aws_instance_migrate.go b/builtin/providers/aws/resource_aws_instance_migrate.go index 28a256b7b..31f28b39f 100644 --- a/builtin/providers/aws/resource_aws_instance_migrate.go +++ b/builtin/providers/aws/resource_aws_instance_migrate.go @@ -15,13 +15,13 @@ func resourceAwsInstanceMigrateState( switch v { case 0: log.Println("[INFO] Found AWS Instance State v0; migrating to v1") - return migrateStateV0toV1(is) + return migrateAwsInstanceStateV0toV1(is) default: return is, fmt.Errorf("Unexpected schema version: %d", v) } } -func migrateStateV0toV1(is *terraform.InstanceState) (*terraform.InstanceState, error) { +func migrateAwsInstanceStateV0toV1(is *terraform.InstanceState) (*terraform.InstanceState, error) { if is.Empty() || is.Attributes == nil { log.Println("[DEBUG] Empty InstanceState; nothing to migrate.") return is, nil diff --git a/builtin/providers/aws/resource_aws_instance_test.go b/builtin/providers/aws/resource_aws_instance_test.go index 2b835f6d7..3426c35d9 100644 --- a/builtin/providers/aws/resource_aws_instance_test.go +++ b/builtin/providers/aws/resource_aws_instance_test.go @@ -616,7 +616,6 @@ func TestAccAWSInstance_tags(t *testing.T) { testAccCheckTags(&v.Tags, "#", ""), ), }, - { Config: testAccCheckInstanceConfigTagsUpdate, Check: resource.ComposeTestCheckFunc( @@ -629,6 +628,56 @@ func TestAccAWSInstance_tags(t *testing.T) { }) } +func TestAccAWSInstance_volumeTags(t *testing.T) { + var v ec2.Instance + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckInstanceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCheckInstanceConfigNoVolumeTags, + Check: resource.ComposeTestCheckFunc( + testAccCheckInstanceExists("aws_instance.foo", &v), + resource.TestCheckNoResourceAttr( + "aws_instance.foo", "volume_tags"), + ), + }, + { + Config: testAccCheckInstanceConfigWithVolumeTags, + Check: resource.ComposeTestCheckFunc( + testAccCheckInstanceExists("aws_instance.foo", &v), + resource.TestCheckResourceAttr( + "aws_instance.foo", "volume_tags.%", "1"), + resource.TestCheckResourceAttr( + "aws_instance.foo", "volume_tags.Name", "acceptance-test-volume-tag"), + ), + }, + { + Config: testAccCheckInstanceConfigWithVolumeTagsUpdate, + Check: resource.ComposeTestCheckFunc( + testAccCheckInstanceExists("aws_instance.foo", &v), + resource.TestCheckResourceAttr( + "aws_instance.foo", "volume_tags.%", "2"), + resource.TestCheckResourceAttr( + "aws_instance.foo", "volume_tags.Name", "acceptance-test-volume-tag"), + resource.TestCheckResourceAttr( + "aws_instance.foo", "volume_tags.Environment", "dev"), + ), + }, + { + Config: testAccCheckInstanceConfigNoVolumeTags, + Check: resource.ComposeTestCheckFunc( + testAccCheckInstanceExists("aws_instance.foo", &v), + resource.TestCheckNoResourceAttr( + "aws_instance.foo", "volume_tags"), + ), + }, + }, + }) +} + func TestAccAWSInstance_instanceProfileChange(t *testing.T) { var v ec2.Instance rName := acctest.RandString(5) @@ -1281,6 +1330,117 @@ resource "aws_instance" "foo" { } ` +const testAccCheckInstanceConfigNoVolumeTags = ` +resource "aws_instance" "foo" { + ami = "ami-55a7ea65" + + instance_type = "m3.medium" + + root_block_device { + volume_type = "gp2" + volume_size = 11 + } + ebs_block_device { + device_name = "/dev/sdb" + volume_size = 9 + } + ebs_block_device { + device_name = "/dev/sdc" + volume_size = 10 + volume_type = "io1" + iops = 100 + } + + ebs_block_device { + device_name = "/dev/sdd" + volume_size = 12 + encrypted = true + } + + ephemeral_block_device { + device_name = "/dev/sde" + virtual_name = "ephemeral0" + } +} +` + +const testAccCheckInstanceConfigWithVolumeTags = ` +resource "aws_instance" "foo" { + ami = "ami-55a7ea65" + + instance_type = "m3.medium" + + root_block_device { + volume_type = "gp2" + volume_size = 11 + } + ebs_block_device { + device_name = "/dev/sdb" + volume_size = 9 + } + ebs_block_device { + device_name = "/dev/sdc" + volume_size = 10 + volume_type = "io1" + iops = 100 + } + + ebs_block_device { + device_name = "/dev/sdd" + volume_size = 12 + encrypted = true + } + + ephemeral_block_device { + device_name = "/dev/sde" + virtual_name = "ephemeral0" + } + + volume_tags { + Name = "acceptance-test-volume-tag" + } +} +` + +const testAccCheckInstanceConfigWithVolumeTagsUpdate = ` +resource "aws_instance" "foo" { + ami = "ami-55a7ea65" + + instance_type = "m3.medium" + + root_block_device { + volume_type = "gp2" + volume_size = 11 + } + ebs_block_device { + device_name = "/dev/sdb" + volume_size = 9 + } + ebs_block_device { + device_name = "/dev/sdc" + volume_size = 10 + volume_type = "io1" + iops = 100 + } + + ebs_block_device { + device_name = "/dev/sdd" + volume_size = 12 + encrypted = true + } + + ephemeral_block_device { + device_name = "/dev/sde" + virtual_name = "ephemeral0" + } + + volume_tags { + Name = "acceptance-test-volume-tag" + Environment = "dev" + } +} +` + const testAccCheckInstanceConfigTagsUpdate = ` resource "aws_instance" "foo" { ami = "ami-4fccb37f" diff --git a/builtin/providers/aws/tags.go b/builtin/providers/aws/tags.go index 90fda0146..57a8f6ab0 100644 --- a/builtin/providers/aws/tags.go +++ b/builtin/providers/aws/tags.go @@ -69,6 +69,63 @@ func setElbV2Tags(conn *elbv2.ELBV2, d *schema.ResourceData) error { return nil } +func setVolumeTags(conn *ec2.EC2, d *schema.ResourceData) error { + if d.HasChange("volume_tags") { + oraw, nraw := d.GetChange("volume_tags") + o := oraw.(map[string]interface{}) + n := nraw.(map[string]interface{}) + create, remove := diffTags(tagsFromMap(o), tagsFromMap(n)) + + volumeIds, err := getAwsInstanceVolumeIds(conn, d) + if err != nil { + return err + } + + if len(remove) > 0 { + err := resource.Retry(2*time.Minute, func() *resource.RetryError { + log.Printf("[DEBUG] Removing volume tags: %#v from %s", remove, d.Id()) + _, err := conn.DeleteTags(&ec2.DeleteTagsInput{ + Resources: volumeIds, + Tags: remove, + }) + if err != nil { + ec2err, ok := err.(awserr.Error) + if ok && strings.Contains(ec2err.Code(), ".NotFound") { + return resource.RetryableError(err) // retry + } + return resource.NonRetryableError(err) + } + return nil + }) + if err != nil { + return err + } + } + if len(create) > 0 { + err := resource.Retry(2*time.Minute, func() *resource.RetryError { + log.Printf("[DEBUG] Creating vol tags: %s for %s", create, d.Id()) + _, err := conn.CreateTags(&ec2.CreateTagsInput{ + Resources: volumeIds, + Tags: create, + }) + if err != nil { + ec2err, ok := err.(awserr.Error) + if ok && strings.Contains(ec2err.Code(), ".NotFound") { + return resource.RetryableError(err) // retry + } + return resource.NonRetryableError(err) + } + return nil + }) + if err != nil { + return err + } + } + } + + return nil +} + // setTags is a helper to set the tags for a resource. It expects the // tags field to be named "tags" func setTags(conn *ec2.EC2, d *schema.ResourceData) error { diff --git a/website/source/docs/providers/aws/r/instance.html.markdown b/website/source/docs/providers/aws/r/instance.html.markdown index 4d8725de2..cfcedb07b 100644 --- a/website/source/docs/providers/aws/r/instance.html.markdown +++ b/website/source/docs/providers/aws/r/instance.html.markdown @@ -80,6 +80,7 @@ instances. See [Shutdown Behavior](https://docs.aws.amazon.com/AWSEC2/latest/Use * `ipv6_address_count`- (Optional) A number of IPv6 addresses to associate with the primary network interface. Amazon EC2 chooses the IPv6 addresses from the range of your subnet. * `ipv6_addresses` - (Optional) Specify one or more IPv6 addresses from the range of the subnet to associate with the primary network interface * `tags` - (Optional) A mapping of tags to assign to the resource. +* `volume_tags` - (Optional) A mapping of tags to assign to the devices created by the instance at launch time. * `root_block_device` - (Optional) Customize details about the root block device of the instance. See [Block Devices](#block-devices) below for details. * `ebs_block_device` - (Optional) Additional EBS block devices to attach to the