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:
parent
0e0a5150ff
commit
f4015b43c5
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue