provider/aws: Support aws_instance and volume tagging on creation (#13945)

Fixes: #13173

We now tag at instance creation and introduced `volume_tags` that can be
set so that all devices created on instance creation will receive those
tags

```
% make testacc TEST=./builtin/providers/aws TESTARGS='-run=TestAccAWSInstance_volumeTags'                      2 ↵ ✚ ✭
==> Checking that code complies with gofmt requirements...
go generate $(go list ./... | grep -v /terraform/vendor/)
2017/04/26 06:30:48 Generated command/internal_plugin_list.go
TF_ACC=1 go test ./builtin/providers/aws -v -run=TestAccAWSInstance_volumeTags -timeout 120m
=== RUN   TestAccAWSInstance_volumeTags
--- PASS: TestAccAWSInstance_volumeTags (214.31s)
PASS
ok  	github.com/hashicorp/terraform/builtin/providers/aws	214.332s
```
This commit is contained in:
Paul Stack 2017-04-26 10:12:38 +12:00 committed by GitHub
parent 0e0a5150ff
commit f4015b43c5
5 changed files with 328 additions and 8 deletions

View File

@ -200,6 +200,8 @@ func resourceAwsInstance() *schema.Resource {
"tags": tagsSchema(), "tags": tagsSchema(),
"volume_tags": tagsSchema(),
"block_device": { "block_device": {
Type: schema.TypeMap, Type: schema.TypeMap,
Optional: true, Optional: true,
@ -396,6 +398,34 @@ func resourceAwsInstanceCreate(d *schema.ResourceData, meta interface{}) error {
runOpts.Ipv6Addresses = ipv6Addresses 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 // Create the instance
log.Printf("[DEBUG] Run configuration: %s", runOpts) 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)) d.Set("tags", tagsToMap(instance.Tags))
if err := readVolumeTags(conn, d); err != nil {
return err
}
if err := readSecurityGroups(d, instance); err != nil { if err := readSecurityGroups(d, instance); err != nil {
return err return err
} }
@ -605,16 +639,27 @@ func resourceAwsInstanceUpdate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).ec2conn conn := meta.(*AWSClient).ec2conn
d.Partial(true) d.Partial(true)
if err := setTags(conn, d); err != nil {
return err if d.HasChange("tags") && !d.IsNewResource() {
} else { if err := setTags(conn, d); err != nil {
d.SetPartial("tags") 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() { if d.HasChange("iam_instance_profile") && !d.IsNewResource() {
request := &ec2.DescribeIamInstanceProfileAssociationsInput{ request := &ec2.DescribeIamInstanceProfileAssociationsInput{
Filters: []*ec2.Filter{ Filters: []*ec2.Filter{
&ec2.Filter{ {
Name: aws.String("instance-id"), Name: aws.String("instance-id"),
Values: []*string{aws.String(d.Id())}, Values: []*string{aws.String(d.Id())},
}, },
@ -1125,6 +1170,39 @@ func readBlockDeviceMappingsFromConfig(
return blockDevices, nil 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 // Determine whether we're referring to security groups with
// IDs or names. We use a heuristic to figure this out. By default, // 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 // 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) hash := sha1.Sum(v)
return hex.EncodeToString(hash[:]) 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
}

View File

@ -15,13 +15,13 @@ func resourceAwsInstanceMigrateState(
switch v { switch v {
case 0: case 0:
log.Println("[INFO] Found AWS Instance State v0; migrating to v1") log.Println("[INFO] Found AWS Instance State v0; migrating to v1")
return migrateStateV0toV1(is) return migrateAwsInstanceStateV0toV1(is)
default: default:
return is, fmt.Errorf("Unexpected schema version: %d", v) 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 { if is.Empty() || is.Attributes == nil {
log.Println("[DEBUG] Empty InstanceState; nothing to migrate.") log.Println("[DEBUG] Empty InstanceState; nothing to migrate.")
return is, nil return is, nil

View File

@ -616,7 +616,6 @@ func TestAccAWSInstance_tags(t *testing.T) {
testAccCheckTags(&v.Tags, "#", ""), testAccCheckTags(&v.Tags, "#", ""),
), ),
}, },
{ {
Config: testAccCheckInstanceConfigTagsUpdate, Config: testAccCheckInstanceConfigTagsUpdate,
Check: resource.ComposeTestCheckFunc( 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) { func TestAccAWSInstance_instanceProfileChange(t *testing.T) {
var v ec2.Instance var v ec2.Instance
rName := acctest.RandString(5) 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 = ` const testAccCheckInstanceConfigTagsUpdate = `
resource "aws_instance" "foo" { resource "aws_instance" "foo" {
ami = "ami-4fccb37f" ami = "ami-4fccb37f"

View File

@ -69,6 +69,63 @@ func setElbV2Tags(conn *elbv2.ELBV2, d *schema.ResourceData) error {
return nil 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 // setTags is a helper to set the tags for a resource. It expects the
// tags field to be named "tags" // tags field to be named "tags"
func setTags(conn *ec2.EC2, d *schema.ResourceData) error { func setTags(conn *ec2.EC2, d *schema.ResourceData) error {

View File

@ -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_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 * `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. * `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 * `root_block_device` - (Optional) Customize details about the root block
device of the instance. See [Block Devices](#block-devices) below for details. device of the instance. See [Block Devices](#block-devices) below for details.
* `ebs_block_device` - (Optional) Additional EBS block devices to attach to the * `ebs_block_device` - (Optional) Additional EBS block devices to attach to the