diff --git a/.changelog/33769.txt b/.changelog/33769.txt new file mode 100644 index 000000000000..e4174359c238 --- /dev/null +++ b/.changelog/33769.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_instance: Apply default tags to volumes/block devices managed through an `aws_instance`, add `ebs_block_device.*.tags_all` and `root_block_device.*.tags_all` attributes which include default tags +``` \ No newline at end of file diff --git a/docs/resource-tagging.md b/docs/resource-tagging.md index 1f68ad52e1c6..eebca35f01e5 100644 --- a/docs/resource-tagging.md +++ b/docs/resource-tagging.md @@ -335,7 +335,7 @@ implement the logic to convert the configuration tags into the service tags, e.g === "Terraform Plugin SDK V2" ```go // Typically declared near conn := /*...*/ - defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig + defaultTagsConfig := meta.(*conns.AWSClient).DefaultTagsConfig tags := defaultTagsConfig.MergeTags(tftags.New(ctx, d.Get("tags").(map[string]interface{}))) input := &eks.CreateClusterInput{ @@ -349,7 +349,7 @@ If the service API does not allow passing an empty list, the logic can be adjust === "Terraform Plugin SDK V2" ```go // Typically declared near conn := /*...*/ - defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig + defaultTagsConfig := meta.(*conns.AWSClient).DefaultTagsConfig tags := defaultTagsConfig.MergeTags(tftags.New(ctx, d.Get("tags").(map[string]interface{}))) input := &eks.CreateClusterInput{ @@ -367,7 +367,7 @@ implement the logic to convert the configuration tags into the service API call === "Terraform Plugin SDK V2" ```go // Typically declared near conn := /*...*/ - defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig + defaultTagsConfig := meta.(*conns.AWSClient).DefaultTagsConfig tags := defaultTagsConfig.MergeTags(tftags.New(ctx, d.Get("tags").(map[string]interface{}))) /* ... creation steps ... */ @@ -380,18 +380,18 @@ implement the logic to convert the configuration tags into the service API call ``` Some EC2 resources (e.g., [`aws_ec2_fleet`](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ec2_fleet)) have a `TagSpecifications` field in the `InputStruct` instead of a `Tags` field. -In these cases the `tagSpecificationsFromKeyValueTags()` helper function should be used. +In these cases the `tagSpecificationsFromKeyValue()` helper function should be used. This example shows using `TagSpecifications`: === "Terraform Plugin SDK V2" ```go // Typically declared near conn := /*...*/ - defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig + defaultTagsConfig := meta.(*conns.AWSClient).DefaultTagsConfig tags := defaultTagsConfig.MergeTags(tftags.New(ctx, d.Get("tags").(map[string]interface{}))) input := &ec2.CreateFleetInput{ /* ... other configuration ... */ - TagSpecifications: tagSpecificationsFromKeyValueTags(tags, ec2.ResourceTypeFleet), + TagSpecifications: tagSpecificationsFromKeyValue(tags, ec2.ResourceTypeFleet), } ``` @@ -402,8 +402,8 @@ In the resource `Read` operation, implement the logic to convert the service tag === "Terraform Plugin SDK V2" ```go // Typically declared near conn := /*...*/ - defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig - ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig + defaultTagsConfig := meta.(*conns.AWSClient).DefaultTagsConfig + ignoreTagsConfig := meta.(*conns.AWSClient).IgnoreTagsConfig /* ... other d.Set(...) logic ... */ @@ -424,8 +424,8 @@ use the generated `listTags` function, e.g., with Athena Workgroups: === "Terraform Plugin SDK V2" ```go // Typically declared near conn := /*...*/ - defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig - ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig + defaultTagsConfig := meta.(*conns.AWSClient).DefaultTagsConfig + ignoreTagsConfig := meta.(*conns.AWSClient).IgnoreTagsConfig /* ... other d.Set(...) logic ... */ diff --git a/internal/provider/intercept.go b/internal/provider/intercept.go index 7a5ad365f423..677649f5090c 100644 --- a/internal/provider/intercept.go +++ b/internal/provider/intercept.go @@ -248,7 +248,7 @@ func (r tagsResourceInterceptor) run(ctx context.Context, d schemaResourceData, break } - if d.GetRawPlan().GetAttr("tags_all").IsWhollyKnown() { + if d.GetRawPlan().GetAttr(names.AttrTagsAll).IsWhollyKnown() { if d.HasChange(names.AttrTagsAll) { if identifierAttribute := r.tags.IdentifierAttribute; identifierAttribute != "" { var identifier string diff --git a/internal/service/ec2/ec2_instance.go b/internal/service/ec2/ec2_instance.go index 5429c1cc0e57..cd9db6f5f8a2 100644 --- a/internal/service/ec2/ec2_instance.go +++ b/internal/service/ec2/ec2_instance.go @@ -266,7 +266,8 @@ func ResourceInstance() *schema.Resource { Computed: true, ForceNew: true, }, - "tags": tagsSchemaConflictsWith([]string{"volume_tags"}), + names.AttrTags: tagsSchemaConflictsWith([]string{"volume_tags"}), + names.AttrTagsAll: tftags.TagsSchemaComputed(), "throughput": { Type: schema.TypeInt, Optional: true, @@ -720,7 +721,8 @@ func ResourceInstance() *schema.Resource { Computed: true, ForceNew: true, }, - "tags": tagsSchemaConflictsWith([]string{"volume_tags"}), + names.AttrTags: tagsSchemaConflictsWith([]string{"volume_tags"}), + names.AttrTagsAll: tftags.TagsSchemaComputed(), "throughput": { Type: schema.TypeInt, Optional: true, @@ -937,8 +939,16 @@ func resourceInstanceCreate(ctx context.Context, d *schema.ResourceData, meta in return sdkdiag.AppendErrorf(diags, "collecting instance settings: %s", err) } + // instance itself tagSpecifications := getTagSpecificationsIn(ctx, ec2.ResourceTypeInstance) - tagSpecifications = append(tagSpecifications, tagSpecificationsFromMap(ctx, d.Get("volume_tags").(map[string]interface{}), ec2.ResourceTypeVolume)...) + + // block devices + defaultTagsConfig := meta.(*conns.AWSClient).DefaultTagsConfig + tagSpecifications = append(tagSpecifications, + tagSpecificationsFromKeyValue( + defaultTagsConfig.MergeTags(tftags.New(ctx, d.Get("volume_tags").(map[string]interface{}))), + ec2.ResourceTypeVolume)...) + input := &ec2.RunInstancesInput{ BlockDeviceMappings: instanceOpts.BlockDeviceMappings, CapacityReservationSpecification: instanceOpts.CapacityReservationSpecification, @@ -1032,11 +1042,18 @@ func resourceInstanceCreate(ctx context.Context, d *schema.ResourceData, meta in vL := v.([]interface{}) for _, v := range vL { bd := v.(map[string]interface{}) - if blockDeviceTags, ok := bd["tags"].(map[string]interface{}); ok && len(blockDeviceTags) > 0 { - if rootVolumeId := getRootVolumeId(instance); rootVolumeId != "" { - blockDeviceTagsToCreate[rootVolumeId] = blockDeviceTags - } + + blockDeviceTags, ok := bd["tags"].(map[string]interface{}) + if !ok || len(blockDeviceTags) == 0 { + continue + } + + volID := getRootVolID(instance) + if volID == "" { + continue } + + blockDeviceTagsToCreate[volID] = blockDeviceTags } } @@ -1044,12 +1061,18 @@ func resourceInstanceCreate(ctx context.Context, d *schema.ResourceData, meta in vL := v.(*schema.Set).List() for _, v := range vL { bd := v.(map[string]interface{}) - if blockDeviceTags, ok := bd["tags"].(map[string]interface{}); ok && len(blockDeviceTags) > 0 { - devName := bd["device_name"].(string) - if volumeId := getVolumeIdByDeviceName(instance, devName); volumeId != "" { - blockDeviceTagsToCreate[volumeId] = blockDeviceTags - } + + blockDeviceTags, ok := bd["tags"].(map[string]interface{}) + if !ok || len(blockDeviceTags) == 0 { + continue } + + volID := getVolIDByDeviceName(instance, bd["device_name"].(string)) + if volID == "" { + continue + } + + blockDeviceTagsToCreate[volID] = blockDeviceTags } } @@ -1278,7 +1301,11 @@ func resourceInstanceRead(ctx context.Context, d *schema.ResourceData, meta inte return sdkdiag.AppendErrorf(diags, "reading EC2 Instance (%s): %s", d.Id(), err) } - if err := d.Set("volume_tags", KeyValueTags(ctx, volumeTags).IgnoreAWS().Map()); err != nil { + defaultTagsConfig := meta.(*conns.AWSClient).DefaultTagsConfig + ignoreTagsConfig := meta.(*conns.AWSClient).IgnoreTagsConfig + tags := KeyValueTags(ctx, volumeTags).IgnoreAWS().IgnoreConfig(ignoreTagsConfig) + + if err := d.Set("volume_tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { return sdkdiag.AppendErrorf(diags, "setting volume_tags: %s", err) } } @@ -1292,9 +1319,10 @@ func resourceInstanceRead(ctx context.Context, d *schema.ResourceData, meta inte return sdkdiag.AppendErrorf(diags, "reading EC2 Instance (%s): %s", d.Id(), err) } - if err := readBlockDevices(ctx, d, instance, conn); err != nil { + if err := readBlockDevices(ctx, d, meta, instance); err != nil { return sdkdiag.AppendErrorf(diags, "reading EC2 Instance (%s): %s", d.Id(), err) } + if _, ok := d.GetOk("ephemeral_block_device"); !ok { d.Set("ephemeral_block_device", []interface{}{}) } @@ -1456,16 +1484,16 @@ func resourceInstanceUpdate(ctx context.Context, d *schema.ResourceData, meta in conn := meta.(*conns.AWSClient).EC2Conn(ctx) if d.HasChange("volume_tags") && !d.IsNewResource() { - volumeIds, err := getInstanceVolumeIDs(ctx, conn, d.Id()) + volIDs, err := getInstanceVolIDs(ctx, conn, d.Id()) if err != nil { return sdkdiag.AppendErrorf(diags, "updating EC2 Instance (%s): %s", d.Id(), err) } o, n := d.GetChange("volume_tags") - for _, volumeId := range volumeIds { - if err := updateTags(ctx, conn, volumeId, o, n); err != nil { - return sdkdiag.AppendErrorf(diags, "updating volume_tags (%s): %s", volumeId, err) + for _, volID := range volIDs { + if err := updateTags(ctx, conn, volID, o, n); err != nil { + return sdkdiag.AppendErrorf(diags, "updating volume_tags (%s): %s", volID, err) } } } @@ -1853,10 +1881,10 @@ func resourceInstanceUpdate(ctx context.Context, d *schema.ResourceData, meta in } if d.HasChange("root_block_device.0") && !d.IsNewResource() { - volumeID := d.Get("root_block_device.0.volume_id").(string) + volID := d.Get("root_block_device.0.volume_id").(string) input := &ec2.ModifyVolumeInput{ - VolumeId: aws.String(volumeID), + VolumeId: aws.String(volID), } modifyVolume := false @@ -1902,11 +1930,11 @@ func resourceInstanceUpdate(ctx context.Context, d *schema.ResourceData, meta in _, err := conn.ModifyVolumeWithContext(ctx, input) if err != nil { - return sdkdiag.AppendErrorf(diags, "updating EC2 Instance (%s) volume (%s): %s", d.Id(), volumeID, err) + return sdkdiag.AppendErrorf(diags, "updating EC2 Instance (%s) volume (%s): %s", d.Id(), volID, err) } - if _, err := WaitVolumeModificationComplete(ctx, conn, volumeID, d.Timeout(schema.TimeoutUpdate)); err != nil { - return sdkdiag.AppendErrorf(diags, "waiting for EC2 Instance (%s) volume (%s) update: %s", d.Id(), volumeID, err) + if _, err := WaitVolumeModificationComplete(ctx, conn, volID, d.Timeout(schema.TimeoutUpdate)); err != nil { + return sdkdiag.AppendErrorf(diags, "waiting for EC2 Instance (%s) volume (%s) update: %s", d.Id(), volID, err) } } @@ -1940,8 +1968,16 @@ func resourceInstanceUpdate(ctx context.Context, d *schema.ResourceData, meta in if d.HasChange("root_block_device.0.tags") { o, n := d.GetChange("root_block_device.0.tags") - if err := updateTags(ctx, conn, volumeID, o, n); err != nil { - return sdkdiag.AppendErrorf(diags, "updating tags for volume (%s): %s", volumeID, err) + if err := updateTags(ctx, conn, volID, o, n); err != nil { + return sdkdiag.AppendErrorf(diags, "updating tags for volume (%s): %s", volID, err) + } + } + + if d.HasChange("root_block_device.0.tags_all") && !d.HasChange("root_block_device.0.tags") { + o, n := d.GetChange("root_block_device.0.tags_all") + + if err := updateTags(ctx, conn, volID, o, n); err != nil { + return sdkdiag.AppendErrorf(diags, "updating tags for volume (%s): %s", volID, err) } } } @@ -2125,8 +2161,8 @@ func modifyInstanceAttributeWithStopStart(ctx context.Context, conn *ec2.EC2, in return nil } -func readBlockDevices(ctx context.Context, d *schema.ResourceData, instance *ec2.Instance, conn *ec2.EC2) error { - ibds, err := readBlockDevicesFromInstance(ctx, d, instance, conn) +func readBlockDevices(ctx context.Context, d *schema.ResourceData, meta interface{}, instance *ec2.Instance) error { + ibds, err := readBlockDevicesFromInstance(ctx, d, meta, instance) if err != nil { return fmt.Errorf("reading block devices: %w", err) } @@ -2177,43 +2213,7 @@ func readBlockDevices(ctx context.Context, d *schema.ResourceData, instance *ec2 return nil } -func associateInstanceProfile(ctx context.Context, d *schema.ResourceData, conn *ec2.EC2) error { - input := &ec2.AssociateIamInstanceProfileInput{ - InstanceId: aws.String(d.Id()), - IamInstanceProfile: &ec2.IamInstanceProfileSpecification{ - Name: aws.String(d.Get("iam_instance_profile").(string)), - }, - } - err := retry.RetryContext(ctx, iamPropagationTimeout, func() *retry.RetryError { - _, err := conn.AssociateIamInstanceProfileWithContext(ctx, input) - if err != nil { - if tfawserr.ErrMessageContains(err, "InvalidParameterValue", "Invalid IAM Instance Profile") { - return retry.RetryableError(err) - } - return retry.NonRetryableError(err) - } - return nil - }) - if tfresource.TimedOut(err) { - _, err = conn.AssociateIamInstanceProfileWithContext(ctx, input) - } - if err != nil { - return fmt.Errorf("associating instance profile: %s", err) - } - return nil -} - -func disassociateInstanceProfile(ctx context.Context, associationId *string, conn *ec2.EC2) error { - _, err := conn.DisassociateIamInstanceProfileWithContext(ctx, &ec2.DisassociateIamInstanceProfileInput{ - AssociationId: associationId, - }) - if err != nil { - return fmt.Errorf("disassociating instance profile: %w", err) - } - return nil -} - -func readBlockDevicesFromInstance(ctx context.Context, d *schema.ResourceData, instance *ec2.Instance, conn *ec2.EC2) (map[string]interface{}, error) { +func readBlockDevicesFromInstance(ctx context.Context, d *schema.ResourceData, meta interface{}, instance *ec2.Instance) (map[string]interface{}, error) { blockDevices := make(map[string]interface{}) blockDevices["ebs"] = make([]map[string]interface{}, 0) blockDevices["root"] = nil @@ -2237,6 +2237,7 @@ func readBlockDevicesFromInstance(ctx context.Context, d *schema.ResourceData, i // Need to call DescribeVolumes to get volume_size and volume_type for each // EBS block device + conn := meta.(*conns.AWSClient).EC2Conn(ctx) volResp, err := conn.DescribeVolumesWithContext(ctx, &ec2.DescribeVolumesInput{ VolumeIds: volIDs, }) @@ -2244,6 +2245,9 @@ func readBlockDevicesFromInstance(ctx context.Context, d *schema.ResourceData, i return nil, err } + defaultTagsConfig := meta.(*conns.AWSClient).DefaultTagsConfig + ignoreTagsConfig := meta.(*conns.AWSClient).IgnoreTagsConfig + for _, vol := range volResp.Volumes { instanceBd := instanceBlockDevices[aws.StringValue(vol.VolumeId)] bd := make(map[string]interface{}) @@ -2274,8 +2278,10 @@ func readBlockDevicesFromInstance(ctx context.Context, d *schema.ResourceData, i if instanceBd.DeviceName != nil { bd["device_name"] = aws.StringValue(instanceBd.DeviceName) } - if v, ok := d.GetOk("volume_tags"); (!ok || v == nil || len(v.(map[string]interface{})) == 0) && vol.Tags != nil { - bd["tags"] = KeyValueTags(ctx, vol.Tags).IgnoreAWS().Map() + if v, ok := d.GetOk("volume_tags"); !ok || v == nil || len(v.(map[string]interface{})) == 0 { + tags := KeyValueTags(ctx, vol.Tags).IgnoreAWS().IgnoreConfig(ignoreTagsConfig) + bd[names.AttrTags] = tags.RemoveDefaultConfig(defaultTagsConfig).Map() + bd[names.AttrTagsAll] = tags.Map() } if blockDeviceIsRoot(instanceBd, instance) { @@ -2305,6 +2311,42 @@ func blockDeviceIsRoot(bd *ec2.InstanceBlockDeviceMapping, instance *ec2.Instanc aws.StringValue(bd.DeviceName) == aws.StringValue(instance.RootDeviceName) } +func associateInstanceProfile(ctx context.Context, d *schema.ResourceData, conn *ec2.EC2) error { + input := &ec2.AssociateIamInstanceProfileInput{ + InstanceId: aws.String(d.Id()), + IamInstanceProfile: &ec2.IamInstanceProfileSpecification{ + Name: aws.String(d.Get("iam_instance_profile").(string)), + }, + } + err := retry.RetryContext(ctx, iamPropagationTimeout, func() *retry.RetryError { + _, err := conn.AssociateIamInstanceProfileWithContext(ctx, input) + if err != nil { + if tfawserr.ErrMessageContains(err, "InvalidParameterValue", "Invalid IAM Instance Profile") { + return retry.RetryableError(err) + } + return retry.NonRetryableError(err) + } + return nil + }) + if tfresource.TimedOut(err) { + _, err = conn.AssociateIamInstanceProfileWithContext(ctx, input) + } + if err != nil { + return fmt.Errorf("associating instance profile: %s", err) + } + return nil +} + +func disassociateInstanceProfile(ctx context.Context, associationId *string, conn *ec2.EC2) error { + _, err := conn.DisassociateIamInstanceProfileWithContext(ctx, &ec2.DisassociateIamInstanceProfileInput{ + AssociationId: associationId, + }) + if err != nil { + return fmt.Errorf("disassociating instance profile: %w", err) + } + return nil +} + func FetchRootDeviceName(ctx context.Context, conn *ec2.EC2, amiID string) (*string, error) { if amiID == "" { return nil, errors.New("Cannot fetch root device name for blank AMI ID.") @@ -2593,18 +2635,18 @@ func readBlockDeviceMappingsFromConfig(ctx context.Context, d *schema.ResourceDa } func readVolumeTags(ctx context.Context, conn *ec2.EC2, instanceId string) ([]*ec2.Tag, error) { - volumeIds, err := getInstanceVolumeIDs(ctx, conn, instanceId) + volIDs, err := getInstanceVolIDs(ctx, conn, instanceId) if err != nil { - return nil, fmt.Errorf("getting tags for volumes (%s): %s", volumeIds, err) + return nil, fmt.Errorf("getting tags for volumes (%s): %s", volIDs, err) } resp, err := conn.DescribeTagsWithContext(ctx, &ec2.DescribeTagsInput{ Filters: attributeFiltersFromMultimap(map[string][]string{ - "resource-id": volumeIds, + "resource-id": volIDs, }), }) if err != nil { - return nil, fmt.Errorf("getting tags for volumes (%s): %s", volumeIds, err) + return nil, fmt.Errorf("getting tags for volumes (%s): %s", volIDs, err) } return tagsFromTagDescriptions(resp.Tags), nil @@ -3211,8 +3253,8 @@ func userDataHashSum(user_data string) string { return hex.EncodeToString(hash[:]) } -func getInstanceVolumeIDs(ctx context.Context, conn *ec2.EC2, instanceId string) ([]string, error) { - volumeIds := []string{} +func getInstanceVolIDs(ctx context.Context, conn *ec2.EC2, instanceId string) ([]string, error) { + volIDs := []string{} resp, err := conn.DescribeVolumesWithContext(ctx, &ec2.DescribeVolumesInput{ Filters: BuildAttributeFilterList(map[string]string{ @@ -3224,38 +3266,38 @@ func getInstanceVolumeIDs(ctx context.Context, conn *ec2.EC2, instanceId string) } for _, v := range resp.Volumes { - volumeIds = append(volumeIds, aws.StringValue(v.VolumeId)) + volIDs = append(volIDs, aws.StringValue(v.VolumeId)) } - return volumeIds, nil + return volIDs, nil } -func getRootVolumeId(instance *ec2.Instance) string { - rootVolumeId := "" +func getRootVolID(instance *ec2.Instance) string { + volID := "" for _, bd := range instance.BlockDeviceMappings { if bd.Ebs != nil && blockDeviceIsRoot(bd, instance) { if bd.Ebs.VolumeId != nil { - rootVolumeId = aws.StringValue(bd.Ebs.VolumeId) + volID = aws.StringValue(bd.Ebs.VolumeId) } break } } - return rootVolumeId + return volID } -func getVolumeIdByDeviceName(instance *ec2.Instance, deviceName string) string { - volumeId := "" +func getVolIDByDeviceName(instance *ec2.Instance, deviceName string) string { + volID := "" for _, bd := range instance.BlockDeviceMappings { if aws.StringValue(bd.DeviceName) == deviceName { if bd.Ebs != nil { - volumeId = aws.StringValue(bd.Ebs.VolumeId) + volID = aws.StringValue(bd.Ebs.VolumeId) break } } } - return volumeId + return volID } func blockDeviceTagsDefined(d *schema.ResourceData) bool { @@ -3263,7 +3305,7 @@ func blockDeviceTagsDefined(d *schema.ResourceData) bool { vL := v.([]interface{}) for _, v := range vL { bd := v.(map[string]interface{}) - if blockDeviceTags, ok := bd["tags"].(map[string]interface{}); ok && len(blockDeviceTags) > 0 { + if blockDeviceTags, ok := bd[names.AttrTags].(map[string]interface{}); ok && len(blockDeviceTags) > 0 { return true } } @@ -3273,7 +3315,7 @@ func blockDeviceTagsDefined(d *schema.ResourceData) bool { vL := v.(*schema.Set).List() for _, v := range vL { bd := v.(map[string]interface{}) - if blockDeviceTags, ok := bd["tags"].(map[string]interface{}); ok && len(blockDeviceTags) > 0 { + if blockDeviceTags, ok := bd[names.AttrTags].(map[string]interface{}); ok && len(blockDeviceTags) > 0 { return true } } diff --git a/internal/service/ec2/ec2_instance_data_source.go b/internal/service/ec2/ec2_instance_data_source.go index 3a9b5b44fb0e..ecc11abd0430 100644 --- a/internal/service/ec2/ec2_instance_data_source.go +++ b/internal/service/ec2/ec2_instance_data_source.go @@ -400,7 +400,6 @@ func DataSourceInstance() *schema.Resource { func dataSourceInstanceRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { var diags diag.Diagnostics conn := meta.(*conns.AWSClient).EC2Conn(ctx) - ignoreTagsConfig := meta.(*conns.AWSClient).IgnoreTagsConfig // Build up search parameters input := &ec2.DescribeInstancesInput{} @@ -430,7 +429,7 @@ func dataSourceInstanceRead(ctx context.Context, d *schema.ResourceData, meta in } log.Printf("[DEBUG] aws_instance - Single Instance ID found: %s", aws.StringValue(instance.InstanceId)) - if err := instanceDescriptionAttributes(ctx, d, instance, conn, ignoreTagsConfig); err != nil { + if err := instanceDescriptionAttributes(ctx, d, meta, instance); err != nil { return sdkdiag.AppendErrorf(diags, "reading EC2 Instance (%s): %s", aws.StringValue(instance.InstanceId), err) } @@ -456,8 +455,9 @@ func dataSourceInstanceRead(ctx context.Context, d *schema.ResourceData, meta in } // Populate instance attribute fields with the returned instance -func instanceDescriptionAttributes(ctx context.Context, d *schema.ResourceData, instance *ec2.Instance, conn *ec2.EC2, ignoreTagsConfig *tftags.IgnoreConfig) error { +func instanceDescriptionAttributes(ctx context.Context, d *schema.ResourceData, meta interface{}, instance *ec2.Instance) error { d.SetId(aws.StringValue(instance.InstanceId)) + conn := meta.(*conns.AWSClient).EC2Conn(ctx) instanceType := aws.StringValue(instance.InstanceType) instanceTypeInfo, err := FindInstanceTypeByName(ctx, conn, instanceType) @@ -538,6 +538,7 @@ func instanceDescriptionAttributes(ctx context.Context, d *schema.ResourceData, d.Set("monitoring", monitoringState == "enabled" || monitoringState == "pending") } + ignoreTagsConfig := meta.(*conns.AWSClient).IgnoreTagsConfig if err := d.Set("tags", KeyValueTags(ctx, instance.Tags).IgnoreAWS().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { return fmt.Errorf("setting tags: %w", err) } @@ -548,7 +549,7 @@ func instanceDescriptionAttributes(ctx context.Context, d *schema.ResourceData, } // Block devices - if err := readBlockDevices(ctx, d, instance, conn); err != nil { + if err := readBlockDevices(ctx, d, meta, instance); err != nil { return fmt.Errorf("reading EC2 Instance (%s): %w", aws.StringValue(instance.InstanceId), err) } if _, ok := d.GetOk("ephemeral_block_device"); !ok { diff --git a/internal/service/ec2/ec2_instance_test.go b/internal/service/ec2/ec2_instance_test.go index bc55965d4967..0cc26d69b3b0 100644 --- a/internal/service/ec2/ec2_instance_test.go +++ b/internal/service/ec2/ec2_instance_test.go @@ -1604,6 +1604,158 @@ func TestAccEC2Instance_BlockDeviceTags_ebsAndRoot(t *testing.T) { }) } +func TestAccEC2Instance_BlockDeviceTags_defaultTagsVolumeTags(t *testing.T) { + ctx := acctest.Context(t) + var v ec2.Instance + resourceName := "aws_instance.test" + + emptyMap := map[string]string{} + mapWithOneKey1 := map[string]string{"brodo": "baggins"} + mapWithOneKey2 := map[string]string{"every": "gnomes"} + mapWithTwoKeys := map[string]string{"brodo": "baggins", "jelly": "bean"} + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.EC2ServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckInstanceDestroy(ctx), + Steps: []resource.TestStep{ + { // 1 defaultTags + Config: testAccInstanceConfig_blockDeviceTagsDefaultVolumeRBDEBS(mapWithOneKey2, emptyMap, emptyMap, emptyMap), + Check: resource.ComposeTestCheckFunc( + testAccCheckInstanceExists(ctx, resourceName, &v), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "tags_all.%", "1"), + resource.TestCheckResourceAttr(resourceName, "volume_tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "root_block_device.0.tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "root_block_device.0.tags_all.%", "1"), + resource.TestCheckResourceAttr(resourceName, "root_block_device.0.tags_all.every", "gnomes"), + resource.TestCheckResourceAttr(resourceName, "ebs_block_device.0.tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "ebs_block_device.0.tags_all.%", "1"), + resource.TestCheckResourceAttr(resourceName, "ebs_block_device.0.tags_all.every", "gnomes"), + ), + }, + { // 1 defaultTags + 1 volumeTags + Config: testAccInstanceConfig_blockDeviceTagsDefaultVolumeRBDEBS(mapWithOneKey2, mapWithOneKey1, emptyMap, emptyMap), + Check: resource.ComposeTestCheckFunc( + testAccCheckInstanceExists(ctx, resourceName, &v), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "tags_all.%", "1"), + resource.TestCheckResourceAttr(resourceName, "volume_tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "volume_tags.brodo", "baggins"), + ), + }, + { // 1 defaultTags + 2 volumeTags + Config: testAccInstanceConfig_blockDeviceTagsDefaultVolumeRBDEBS(mapWithOneKey2, mapWithTwoKeys, emptyMap, emptyMap), + Check: resource.ComposeTestCheckFunc( + testAccCheckInstanceExists(ctx, resourceName, &v), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "tags_all.%", "1"), + resource.TestCheckResourceAttr(resourceName, "volume_tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "volume_tags.brodo", "baggins"), + resource.TestCheckResourceAttr(resourceName, "volume_tags.jelly", "bean"), + ), + }, + { // 1 defaultTags + Config: testAccInstanceConfig_blockDeviceTagsDefaultVolumeRBDEBS(mapWithOneKey2, emptyMap, emptyMap, emptyMap), + Check: resource.ComposeTestCheckFunc( + testAccCheckInstanceExists(ctx, resourceName, &v), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "tags_all.%", "1"), + resource.TestCheckResourceAttr(resourceName, "volume_tags.%", "0"), + ), + }, + { // no tags + Config: testAccInstanceConfig_blockDeviceTagsDefaultVolumeRBDEBS(emptyMap, emptyMap, emptyMap, emptyMap), + Check: resource.ComposeTestCheckFunc( + testAccCheckInstanceExists(ctx, resourceName, &v), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "tags_all.%", "0"), + resource.TestCheckResourceAttr(resourceName, "volume_tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "root_block_device.0.tags.%", "0"), + ), + }, + }, + }) +} + +func TestAccEC2Instance_BlockDeviceTags_defaultTagsEBSRoot(t *testing.T) { + ctx := acctest.Context(t) + var v ec2.Instance + resourceName := "aws_instance.test" + + emptyMap := map[string]string{} + mapWithOneKey1 := map[string]string{"gigi": "kitty"} + mapWithOneKey2 := map[string]string{"every": "gnomes"} + mapWithTwoKeys1 := map[string]string{"brodo": "baggins", "jelly": "bean"} + mapWithTwoKeys2 := map[string]string{"brodo": "baggins", "jelly": "andrew"} + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.EC2ServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckInstanceDestroy(ctx), + Steps: []resource.TestStep{ + { // 1 defaultTags + 0 rootTags + 1 ebsTags + Config: testAccInstanceConfig_blockDeviceTagsDefaultVolumeRBDEBS(mapWithOneKey2, emptyMap, emptyMap, mapWithOneKey1), + Check: resource.ComposeTestCheckFunc( + testAccCheckInstanceExists(ctx, resourceName, &v), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "tags_all.%", "1"), + resource.TestCheckResourceAttr(resourceName, "volume_tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "root_block_device.0.tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "root_block_device.0.tags_all.%", "1"), + resource.TestCheckResourceAttr(resourceName, "ebs_block_device.0.tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "ebs_block_device.0.tags_all.%", "2"), + resource.TestCheckResourceAttr(resourceName, "ebs_block_device.0.tags_all.gigi", "kitty"), + resource.TestCheckResourceAttr(resourceName, "ebs_block_device.0.tags_all.every", "gnomes"), + ), + }, + { // 1 defaultTags + 2 rootTags + 1 ebsTags + Config: testAccInstanceConfig_blockDeviceTagsDefaultVolumeRBDEBS(mapWithOneKey2, emptyMap, mapWithTwoKeys1, mapWithOneKey1), + Check: resource.ComposeTestCheckFunc( + testAccCheckInstanceExists(ctx, resourceName, &v), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "tags_all.%", "1"), + resource.TestCheckResourceAttr(resourceName, "volume_tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "root_block_device.0.tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "root_block_device.0.tags_all.%", "3"), + resource.TestCheckResourceAttr(resourceName, "root_block_device.0.tags_all.every", "gnomes"), + resource.TestCheckResourceAttr(resourceName, "root_block_device.0.tags_all.brodo", "baggins"), + resource.TestCheckResourceAttr(resourceName, "root_block_device.0.tags_all.jelly", "bean"), + ), + }, + { // 1 defaultTags + 2 rootTags (1 update) + 1 ebsTags + Config: testAccInstanceConfig_blockDeviceTagsDefaultVolumeRBDEBS(mapWithOneKey2, emptyMap, mapWithTwoKeys2, mapWithOneKey1), + Check: resource.ComposeTestCheckFunc( + testAccCheckInstanceExists(ctx, resourceName, &v), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "tags_all.%", "1"), + resource.TestCheckResourceAttr(resourceName, "volume_tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "root_block_device.0.tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "root_block_device.0.tags_all.%", "3"), + resource.TestCheckResourceAttr(resourceName, "root_block_device.0.tags_all.every", "gnomes"), + resource.TestCheckResourceAttr(resourceName, "root_block_device.0.tags_all.brodo", "baggins"), + resource.TestCheckResourceAttr(resourceName, "root_block_device.0.tags_all.jelly", "andrew"), + ), + }, + { // 0 defaultTags + 2 rootTags + 1 ebsTags + Config: testAccInstanceConfig_blockDeviceTagsDefaultVolumeRBDEBS(emptyMap, emptyMap, mapWithTwoKeys2, mapWithOneKey1), + Check: resource.ComposeTestCheckFunc( + testAccCheckInstanceExists(ctx, resourceName, &v), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "tags_all.%", "0"), + resource.TestCheckResourceAttr(resourceName, "volume_tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "root_block_device.0.tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "root_block_device.0.tags_all.%", "2"), + resource.TestCheckResourceAttr(resourceName, "root_block_device.0.tags_all.brodo", "baggins"), + resource.TestCheckResourceAttr(resourceName, "root_block_device.0.tags_all.jelly", "andrew"), + ), + }, + }, + }) +} + func TestAccEC2Instance_instanceProfileChange(t *testing.T) { ctx := acctest.Context(t) var v ec2.Instance @@ -6851,6 +7003,85 @@ resource "aws_instance" "test" { `, rName)) } +func mapToTagConfig(m map[string]string, indent int) string { + if len(m) == 0 { + return "" + } + + var tags []string + for k, v := range m { + tags = append(tags, fmt.Sprintf("%q = %q", k, v)) + } + + return fmt.Sprintf("%s\n", strings.Join(tags, fmt.Sprintf("\n%s", strings.Repeat(" ", indent)))) +} + +func testAccInstanceConfig_blockDeviceTagsDefaultVolumeRBDEBS(defTg, volTg, rbdTg, ebsTg map[string]string) string { + defTgCfg := "" + if len(defTg) > 0 { + //lintignore:AT004 + defTgCfg = fmt.Sprintf(` +provider "aws" { + default_tags { + tags = { + %[1]s + } + } +}`, mapToTagConfig(defTg, 6)) + } + + volTgCfg := "" + if len(volTg) > 0 { + volTgCfg = fmt.Sprintf(` + volume_tags = { + %[1]s + }`, mapToTagConfig(volTg, 4)) + } + + rbdTgCfg := "" + if len(rbdTg) > 0 { + rbdTgCfg = fmt.Sprintf(` + tags = { + %[1]s + }`, mapToTagConfig(rbdTg, 6)) + } + + ebsTgCfg := "" + if len(ebsTg) > 0 { + ebsTgCfg = fmt.Sprintf(` + tags = { + %[1]s + }`, mapToTagConfig(ebsTg, 6)) + } + + return acctest.ConfigCompose( + acctest.ConfigLatestAmazonLinux2HVMEBSX8664AMI(), + fmt.Sprintf(` +%[1]s + +resource "aws_instance" "test" { + ami = data.aws_ami.amzn2-ami-minimal-hvm-ebs-x86_64.id + + instance_type = "t2.medium" + + %[2]s + + root_block_device { + volume_type = "gp2" + + %[3]s + } + + ebs_block_device { + device_name = "/dev/sdb" + volume_size = 1 + + %[4]s + } +} +`, defTgCfg, volTgCfg, rbdTgCfg, ebsTgCfg)) +} + func testAccInstanceConfig_blockDeviceTagsEBSTags(rName string) string { return acctest.ConfigCompose(acctest.ConfigLatestAmazonLinux2HVMEBSX8664AMI(), fmt.Sprintf(` resource "aws_instance" "test" { @@ -8497,7 +8728,7 @@ resource "aws_subnet" "test" { } } -# must be >= m3 and have an encrypted root volume to eanble hibernation +# must be >= m3 and have an encrypted root volume to enable hibernation resource "aws_instance" "test" { ami = data.aws_ami.amzn2-ami-minimal-hvm-ebs-x86_64.id hibernation = %[2]t diff --git a/internal/service/ec2/ec2_spot_instance_request.go b/internal/service/ec2/ec2_spot_instance_request.go index 4cd8c7519e7b..0cb13a8c1140 100644 --- a/internal/service/ec2/ec2_spot_instance_request.go +++ b/internal/service/ec2/ec2_spot_instance_request.go @@ -317,7 +317,7 @@ func readInstance(ctx context.Context, d *schema.ResourceData, meta interface{}) "host": *instance.PrivateIpAddress, }) } - if err := readBlockDevices(ctx, d, instance, conn); err != nil { + if err := readBlockDevices(ctx, d, meta, instance); err != nil { return sdkdiag.AppendFromErr(diags, err) } diff --git a/internal/service/ec2/tags.go b/internal/service/ec2/tags.go index c04a9297bb9d..b4766af46db0 100644 --- a/internal/service/ec2/tags.go +++ b/internal/service/ec2/tags.go @@ -51,6 +51,20 @@ func tagSpecificationsFromMap(ctx context.Context, m map[string]interface{}, t s } } +// tagSpecificationsFromKeyValue returns the tag specifications for the given tag key/value tags and resource type. +func tagSpecificationsFromKeyValue(tags tftags.KeyValueTags, resourceType string) []*ec2.TagSpecification { + if len(tags) == 0 { + return nil + } + + return []*ec2.TagSpecification{ + { + ResourceType: aws.String(resourceType), + Tags: Tags(tags.IgnoreAWS()), + }, + } +} + // getTagSpecificationsIn returns AWS SDK for Go v1 EC2 service tags from Context. // nil is returned if there are no input tags. func getTagSpecificationsIn(ctx context.Context, resourceType string) []*ec2.TagSpecification { diff --git a/website/docs/r/instance.html.markdown b/website/docs/r/instance.html.markdown index e51894b20c10..eaa0a704050c 100644 --- a/website/docs/r/instance.html.markdown +++ b/website/docs/r/instance.html.markdown @@ -178,6 +178,18 @@ resource "aws_instance" "this" { } ``` +## Tag Guide + +These are the five types of tags you might encounter relative to an `aws_instance`: + +1. **Instance tags**: Applied to instances but not to `ebs_block_device` and `root_block_device` volumes. +2. **Default tags**: Applied to the instance and to `ebs_block_device` and `root_block_device` volumes. +3. **Volume tags**: Applied during creation to `ebs_block_device` and `root_block_device` volumes. +4. **Root block device tags**: Applied only to the `root_block_device` volume. These conflict with `volume_tags`. +5. **EBS block device tags**: Applied only to the specific `ebs_block_device` volume you configure them for and cannot be updated. These conflict with `volume_tags`. + +Do not use `volume_tags` if you plan to manage block device tags outside the `aws_instance` configuration, such as using `tags` in an [`aws_ebs_volume`](/docs/providers/aws/r/ebs_volume.html) resource attached via [`aws_volume_attachment`](/docs/providers/aws/r/volume_attachment.html). Doing so will result in resource cycling and inconsistent behavior. + ## Argument Reference This resource supports the following arguments: @@ -424,11 +436,13 @@ This resource exports the following attributes in addition to the arguments abov For `ebs_block_device`, in addition to the arguments above, the following attribute is exported: * `volume_id` - ID of the volume. For example, the ID can be accessed like this, `aws_instance.web.ebs_block_device.2.volume_id`. +* `tags_all` - Map of tags assigned to the resource, including those inherited from the provider [`default_tags` configuration block](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#default_tags-configuration-block). For `root_block_device`, in addition to the arguments above, the following attributes are exported: * `volume_id` - ID of the volume. For example, the ID can be accessed like this, `aws_instance.web.root_block_device.0.volume_id`. * `device_name` - Device name, e.g., `/dev/sdh` or `xvdh`. +* `tags_all` - Map of tags assigned to the resource, including those inherited from the provider [`default_tags` configuration block](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#default_tags-configuration-block). For `instance_market_options`, in addition to the arguments above, the following attributes are exported: