diff --git a/.changelog/37441.txt b/.changelog/37441.txt new file mode 100644 index 00000000000..39788401d76 --- /dev/null +++ b/.changelog/37441.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_instance: Fixed issues with `volume_tags`, `root_block_device.*.tags`, and `ebs_block_device.*.tags` where tags overlapped with default tags. These are now handled consistently with top-level tags throughout the provider. Specifically, tags defined in both locations are no longer removed, preventing erroneous differences. +``` \ No newline at end of file diff --git a/internal/provider/intercept.go b/internal/provider/intercept.go index aa066eb28f6..7553b0f54d0 100644 --- a/internal/provider/intercept.go +++ b/internal/provider/intercept.go @@ -362,7 +362,7 @@ func (r tagsResourceInterceptor) run(ctx context.Context, d schemaResourceData, tags := tagsInContext.TagsOut.UnwrapOrDefault().IgnoreSystem(inContext.ServicePackageName).IgnoreConfig(tagsInContext.IgnoreConfig) // The resource's configured tags can now include duplicate tags that have been configured on the provider. - if err := d.Set(names.AttrTags, tags.ResolveDuplicates(ctx, tagsInContext.DefaultConfig, tagsInContext.IgnoreConfig, d).Map()); err != nil { + if err := d.Set(names.AttrTags, tags.ResolveDuplicates(ctx, tagsInContext.DefaultConfig, tagsInContext.IgnoreConfig, d, names.AttrTags, nil).Map()); err != nil { return ctx, sdkdiag.AppendErrorf(diags, "setting %s: %s", names.AttrTags, err) } diff --git a/internal/provider/tags_interceptor.go b/internal/provider/tags_interceptor.go index 9c786903af7..8d2523f9d6c 100644 --- a/internal/provider/tags_interceptor.go +++ b/internal/provider/tags_interceptor.go @@ -161,7 +161,7 @@ func tagsReadFunc(ctx context.Context, d schemaResourceData, sp conns.ServicePac toAdd := tagsInContext.TagsOut.UnwrapOrDefault().IgnoreSystem(inContext.ServicePackageName).IgnoreConfig(tagsInContext.IgnoreConfig) // The resource's configured tags can now include duplicate tags that have been configured on the provider. - if err := d.Set(names.AttrTags, toAdd.ResolveDuplicates(ctx, tagsInContext.DefaultConfig, tagsInContext.IgnoreConfig, d).Map()); err != nil { + if err := d.Set(names.AttrTags, toAdd.ResolveDuplicates(ctx, tagsInContext.DefaultConfig, tagsInContext.IgnoreConfig, d, names.AttrTags, nil).Map()); err != nil { return ctx, sdkdiag.AppendErrorf(diags, "setting %s: %s", names.AttrTags, err) } diff --git a/internal/service/ec2/ebs_volume.go b/internal/service/ec2/ebs_volume.go index 4751830e03a..dc0c78c0e95 100644 --- a/internal/service/ec2/ebs_volume.go +++ b/internal/service/ec2/ebs_volume.go @@ -44,7 +44,7 @@ func resourceEBSVolume() *schema.Resource { Timeouts: &schema.ResourceTimeout{ Create: schema.DefaultTimeout(5 * time.Minute), Update: schema.DefaultTimeout(5 * time.Minute), - Delete: schema.DefaultTimeout(5 * time.Minute), + Delete: schema.DefaultTimeout(10 * time.Minute), }, CustomizeDiff: customdiff.Sequence( diff --git a/internal/service/ec2/ec2_instance.go b/internal/service/ec2/ec2_instance.go index c722cb3d0f7..e19e74df001 100644 --- a/internal/service/ec2/ec2_instance.go +++ b/internal/service/ec2/ec2_instance.go @@ -21,6 +21,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" awstypes "github.com/aws/aws-sdk-go-v2/service/ec2/types" "github.com/hashicorp/aws-sdk-go-base/v2/tfawserr" + "github.com/hashicorp/go-cty/cty" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" @@ -1335,7 +1336,7 @@ func resourceInstanceRead(ctx context.Context, d *schema.ResourceData, meta inte ignoreTagsConfig := meta.(*conns.AWSClient).IgnoreTagsConfig tags := keyValueTags(ctx, volumeTags).IgnoreAWS().IgnoreConfig(ignoreTagsConfig) - if err := d.Set("volume_tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { + if err := d.Set("volume_tags", tags.ResolveDuplicates(ctx, defaultTagsConfig, ignoreTagsConfig, d, "volume_tags", nil).Map()); err != nil { return sdkdiag.AppendErrorf(diags, "setting volume_tags: %s", err) } } @@ -2352,13 +2353,36 @@ func readBlockDevicesFromInstance(ctx context.Context, d *schema.ResourceData, m if instanceBd.DeviceName != nil { bd[names.AttrDeviceName] = aws.ToString(instanceBd.DeviceName) } - if v, ok := d.GetOk("volume_tags"); !ok || v == nil || len(v.(map[string]interface{})) == 0 { - if ds { - bd[names.AttrTags] = keyValueTags(ctx, vol.Tags).IgnoreAWS().IgnoreConfig(ignoreTagsConfig).Map() - } else { - tags := keyValueTags(ctx, vol.Tags).IgnoreAWS().IgnoreConfig(ignoreTagsConfig) - bd[names.AttrTags] = tags.RemoveDefaultConfig(defaultTagsConfig).Map() - bd[names.AttrTagsAll] = tags.Map() + if v, ok := d.GetOk("volume_tags"); (!ok || v == nil || len(v.(map[string]interface{})) == 0) && ds { + bd[names.AttrTags] = keyValueTags(ctx, vol.Tags).IgnoreAWS().IgnoreConfig(ignoreTagsConfig).Map() + } + + if v, ok := d.GetOk("volume_tags"); (!ok || v == nil || len(v.(map[string]interface{})) == 0) && !ds { + tags := keyValueTags(ctx, vol.Tags).IgnoreAWS().IgnoreConfig(ignoreTagsConfig) + + // default setup, in case we don't find config for the block device (don't resolve duplicates) + bd[names.AttrTags] = tags.Map() + bd[names.AttrTagsAll] = tags.Map() + + if v, ok := d.GetOk("ebs_block_device"); ok && v.(*schema.Set).Len() > 0 { + ebdList := v.(*schema.Set).List() + for _, ebd := range ebdList { + ebd, ok := ebd.(map[string]interface{}) + if !ok { + continue + } + + if ebd[names.AttrDeviceName] == aws.ToString(instanceBd.DeviceName) { + bd[names.AttrTags] = tags.ResolveDuplicates(ctx, defaultTagsConfig, ignoreTagsConfig, d, fmt.Sprintf("ebs_block_device[%s].tags", aws.ToString(instanceBd.DeviceName)), func(attr string, val cty.Value) bool { + return val.GetAttr(names.AttrDeviceName).AsString() == attr + }).Map() + break + } + } + } + + if v, ok := d.GetOk("root_block_device"); ok && len(v.([]interface{})) > 0 && blockDeviceIsRoot(instanceBd, instance) { + bd[names.AttrTags] = tags.ResolveDuplicates(ctx, defaultTagsConfig, ignoreTagsConfig, d, "root_block_device[0].tags", nil).Map() } } diff --git a/internal/service/ec2/ec2_instance_test.go b/internal/service/ec2/ec2_instance_test.go index b56b2ae1712..dc26ce2136c 100644 --- a/internal/service/ec2/ec2_instance_test.go +++ b/internal/service/ec2/ec2_instance_test.go @@ -1587,7 +1587,7 @@ func TestAccEC2Instance_BlockDeviceTags_defaultTagsVolumeTags(t *testing.T) { emptyMap := map[string]string{} mapWithOneKey1 := map[string]string{"brodo": "baggins"} - mapWithOneKey2 := map[string]string{"every": "gnomes"} + mapWithOneKey2 := map[string]string{"every": "gnomesie"} mapWithTwoKeys := map[string]string{"brodo": "baggins", "jelly": "bean"} resource.ParallelTest(t, resource.TestCase{ @@ -1605,13 +1605,13 @@ func TestAccEC2Instance_BlockDeviceTags_defaultTagsVolumeTags(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "volume_tags.%", acctest.Ct0), resource.TestCheckResourceAttr(resourceName, "root_block_device.0.tags.%", acctest.Ct0), resource.TestCheckResourceAttr(resourceName, "root_block_device.0.tags_all.%", acctest.Ct1), - resource.TestCheckResourceAttr(resourceName, "root_block_device.0.tags_all.every", "gnomes"), + resource.TestCheckResourceAttr(resourceName, "root_block_device.0.tags_all.every", "gnomesie"), resource.TestCheckResourceAttr(resourceName, "ebs_block_device.0.tags.%", acctest.Ct0), resource.TestCheckResourceAttr(resourceName, "ebs_block_device.0.tags_all.%", acctest.Ct1), - resource.TestCheckResourceAttr(resourceName, "ebs_block_device.0.tags_all.every", "gnomes"), + resource.TestCheckResourceAttr(resourceName, "ebs_block_device.0.tags_all.every", "gnomesie"), ), }, - { // 1 defaultTags + 1 volumeTags + { // 1 defaultTags + 1 volumeTags (no overlap) Config: testAccInstanceConfig_blockDeviceTagsDefaultVolumeRBDEBS(mapWithOneKey2, mapWithOneKey1, emptyMap, emptyMap), Check: resource.ComposeTestCheckFunc( testAccCheckInstanceExists(ctx, resourceName, &v), @@ -1621,7 +1621,7 @@ func TestAccEC2Instance_BlockDeviceTags_defaultTagsVolumeTags(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "volume_tags.brodo", "baggins"), ), }, - { // 1 defaultTags + 2 volumeTags + { // 1 defaultTags + 2 volumeTags (no overlap) Config: testAccInstanceConfig_blockDeviceTagsDefaultVolumeRBDEBS(mapWithOneKey2, mapWithTwoKeys, emptyMap, emptyMap), Check: resource.ComposeTestCheckFunc( testAccCheckInstanceExists(ctx, resourceName, &v), @@ -1632,7 +1632,7 @@ func TestAccEC2Instance_BlockDeviceTags_defaultTagsVolumeTags(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "volume_tags.jelly", "bean"), ), }, - { // 1 defaultTags + { // 1 defaultTags (no overlap) Config: testAccInstanceConfig_blockDeviceTagsDefaultVolumeRBDEBS(mapWithOneKey2, emptyMap, emptyMap, emptyMap), Check: resource.ComposeTestCheckFunc( testAccCheckInstanceExists(ctx, resourceName, &v), @@ -1641,7 +1641,7 @@ func TestAccEC2Instance_BlockDeviceTags_defaultTagsVolumeTags(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "volume_tags.%", acctest.Ct0), ), }, - { // no tags + { // no tags (no overlap) Config: testAccInstanceConfig_blockDeviceTagsDefaultVolumeRBDEBS(emptyMap, emptyMap, emptyMap, emptyMap), Check: resource.ComposeTestCheckFunc( testAccCheckInstanceExists(ctx, resourceName, &v), @@ -1662,7 +1662,7 @@ func TestAccEC2Instance_BlockDeviceTags_defaultTagsEBSRoot(t *testing.T) { emptyMap := map[string]string{} mapWithOneKey1 := map[string]string{"gigi": "kitty"} - mapWithOneKey2 := map[string]string{"every": "gnomes"} + mapWithOneKey2 := map[string]string{"every": "gnomesie"} mapWithTwoKeys1 := map[string]string{"brodo": "baggins", "jelly": "bean"} mapWithTwoKeys2 := map[string]string{"brodo": "baggins", "jelly": "andrew"} @@ -1684,7 +1684,7 @@ func TestAccEC2Instance_BlockDeviceTags_defaultTagsEBSRoot(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "ebs_block_device.0.tags.%", acctest.Ct1), resource.TestCheckResourceAttr(resourceName, "ebs_block_device.0.tags_all.%", acctest.Ct2), resource.TestCheckResourceAttr(resourceName, "ebs_block_device.0.tags_all.gigi", "kitty"), - resource.TestCheckResourceAttr(resourceName, "ebs_block_device.0.tags_all.every", "gnomes"), + resource.TestCheckResourceAttr(resourceName, "ebs_block_device.0.tags_all.every", "gnomesie"), ), }, { // 1 defaultTags + 2 rootTags + 1 ebsTags @@ -1696,7 +1696,7 @@ func TestAccEC2Instance_BlockDeviceTags_defaultTagsEBSRoot(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "volume_tags.%", acctest.Ct0), resource.TestCheckResourceAttr(resourceName, "root_block_device.0.tags.%", acctest.Ct2), resource.TestCheckResourceAttr(resourceName, "root_block_device.0.tags_all.%", acctest.Ct3), - resource.TestCheckResourceAttr(resourceName, "root_block_device.0.tags_all.every", "gnomes"), + resource.TestCheckResourceAttr(resourceName, "root_block_device.0.tags_all.every", "gnomesie"), resource.TestCheckResourceAttr(resourceName, "root_block_device.0.tags_all.brodo", "baggins"), resource.TestCheckResourceAttr(resourceName, "root_block_device.0.tags_all.jelly", "bean"), ), @@ -1710,7 +1710,7 @@ func TestAccEC2Instance_BlockDeviceTags_defaultTagsEBSRoot(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "volume_tags.%", acctest.Ct0), resource.TestCheckResourceAttr(resourceName, "root_block_device.0.tags.%", acctest.Ct2), resource.TestCheckResourceAttr(resourceName, "root_block_device.0.tags_all.%", acctest.Ct3), - resource.TestCheckResourceAttr(resourceName, "root_block_device.0.tags_all.every", "gnomes"), + resource.TestCheckResourceAttr(resourceName, "root_block_device.0.tags_all.every", "gnomesie"), resource.TestCheckResourceAttr(resourceName, "root_block_device.0.tags_all.brodo", "baggins"), resource.TestCheckResourceAttr(resourceName, "root_block_device.0.tags_all.jelly", "andrew"), ), @@ -1732,6 +1732,140 @@ func TestAccEC2Instance_BlockDeviceTags_defaultTagsEBSRoot(t *testing.T) { }) } +func TestAccEC2Instance_BlockDeviceTags_defaultTagsRBDOverlap(t *testing.T) { + ctx := acctest.Context(t) + var v awstypes.Instance + resourceName := "aws_instance.test" + + emptyMap := map[string]string{} + + // default tags and root tags overlapping is causing perpetual diffs + defTags := map[string]string{"every": "gnomesie", "iz": "paws", "gigi": "kitty", "brodo": "baggins"} + rbdTags := map[string]string{"gigi": "kitty", "brodo": "baggins", "tristy": "boo", "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{ + { // 4 defaultTags + 4 rbdTags with 2 in common + Config: testAccInstanceConfig_blockDeviceTagsDefaultVolumeRBDEBS(defTags, emptyMap, rbdTags, emptyMap), + Check: resource.ComposeTestCheckFunc( + testAccCheckInstanceExists(ctx, resourceName, &v), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsPercent, acctest.Ct0), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsAllPercent, acctest.Ct4), + resource.TestCheckResourceAttr(resourceName, "volume_tags.%", acctest.Ct0), + resource.TestCheckResourceAttr(resourceName, "root_block_device.0.tags.%", acctest.Ct4), + resource.TestCheckResourceAttr(resourceName, "root_block_device.0.tags_all.%", "6"), + resource.TestCheckResourceAttr(resourceName, "ebs_block_device.0.tags.%", acctest.Ct0), + resource.TestCheckResourceAttr(resourceName, "ebs_block_device.0.tags_all.%", acctest.Ct4), + ), + }, + { // 4 defaultTags + 4 rbdTags with 2 in common + Config: testAccInstanceConfig_blockDeviceTagsDefaultVolumeRBDEBS(defTags, emptyMap, rbdTags, emptyMap), + PlanOnly: true, + }, + }, + }) +} + +func TestAccEC2Instance_BlockDeviceTags_defaultTagsEBDOverlaps(t *testing.T) { + ctx := acctest.Context(t) + var v awstypes.Instance + resourceName := "aws_instance.test" + + emptyMap := map[string]string{} + + // default tags and root tags overlapping is causing perpetual diffs + defTags := map[string]string{"every": "gnomesie", "iz": "paws", "gigi": "kitty", "brodo": "baggins"} + ebdTags2 := map[string]string{"gigi": "kitty", "brodo": "baggins", "tristy": "boo", "jelly": "bean"} + ebdTags3 := map[string]string{"gigi": "kitty", "brodo": "baggins"} + + 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{ + { // 4 defaultTags + 4 rbdTags with 2 in common + Config: testAccInstanceConfig_blockDeviceTagsDefault3EBS(defTags, emptyMap, ebdTags2, ebdTags3), + Check: resource.ComposeTestCheckFunc( + testAccCheckInstanceExists(ctx, resourceName, &v), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsPercent, acctest.Ct0), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsAllPercent, acctest.Ct4), + resource.TestCheckResourceAttr(resourceName, "volume_tags.%", acctest.Ct0), + resource.TestCheckResourceAttr(resourceName, "ebs_block_device.0.tags.%", acctest.Ct0), + resource.TestCheckResourceAttr(resourceName, "ebs_block_device.0.tags_all.%", acctest.Ct4), + resource.TestCheckResourceAttr(resourceName, "ebs_block_device.1.tags.%", acctest.Ct4), + resource.TestCheckResourceAttr(resourceName, "ebs_block_device.1.tags_all.%", "6"), + resource.TestCheckResourceAttr(resourceName, "ebs_block_device.2.tags.%", acctest.Ct2), + resource.TestCheckResourceAttr(resourceName, "ebs_block_device.2.tags_all.%", acctest.Ct4), + ), + }, + }, + }) +} + +func TestAccEC2Instance_BlockDeviceTags_defaultTagsVolumeTagsOverlap(t *testing.T) { + ctx := acctest.Context(t) + var v awstypes.Instance + resourceName := "aws_instance.test" + + emptyMap := map[string]string{} + defTags := map[string]string{"brodo": "baggins", "jelly": "bean"} + volTags1 := map[string]string{"every": "gnomesie"} + volTags2 := map[string]string{"brodo": "baggins"} + volTags3 := map[string]string{"every": "gnomesie", "iz": "paws", "gigi": "kitty", "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{ + { // 2 defaultTags + 1 volumeTags (no overlap) + Config: testAccInstanceConfig_blockDeviceTagsDefaultVolumeRBDEBS(defTags, volTags1, emptyMap, emptyMap), + Check: resource.ComposeTestCheckFunc( + testAccCheckInstanceExists(ctx, resourceName, &v), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsPercent, acctest.Ct0), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsAllPercent, acctest.Ct2), + resource.TestCheckResourceAttr(resourceName, "volume_tags.%", acctest.Ct1), + resource.TestCheckResourceAttr(resourceName, "volume_tags.every", "gnomesie"), + ), + }, + { // 2 defaultTags + 1 volumeTags (overlap) + Config: testAccInstanceConfig_blockDeviceTagsDefaultVolumeRBDEBS(defTags, volTags2, emptyMap, emptyMap), + Check: resource.ComposeTestCheckFunc( + testAccCheckInstanceExists(ctx, resourceName, &v), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsPercent, acctest.Ct0), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsAllPercent, acctest.Ct2), + resource.TestCheckResourceAttr(resourceName, "volume_tags.%", acctest.Ct1), + resource.TestCheckResourceAttr(resourceName, "volume_tags.brodo", "baggins"), + ), + }, + { // 2 defaultTags + 5 volumeTags (overlap) + Config: testAccInstanceConfig_blockDeviceTagsDefaultVolumeRBDEBS(defTags, volTags3, emptyMap, emptyMap), + Check: resource.ComposeTestCheckFunc( + testAccCheckInstanceExists(ctx, resourceName, &v), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsPercent, acctest.Ct0), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsAllPercent, acctest.Ct2), + resource.TestCheckResourceAttr(resourceName, "volume_tags.%", "5"), + resource.TestCheckResourceAttr(resourceName, "volume_tags.brodo", "baggins"), // overlap + resource.TestCheckResourceAttr(resourceName, "volume_tags.jelly", "bean"), // overlap + resource.TestCheckResourceAttr(resourceName, "volume_tags.every", "gnomesie"), // non-overlap + resource.TestCheckResourceAttr(resourceName, "volume_tags.iz", "paws"), // non-overlap + resource.TestCheckResourceAttr(resourceName, "volume_tags.gigi", "kitty"), // non-overlap + ), + }, + { // 2 defaultTags + 5 volumeTags (overlap) + Config: testAccInstanceConfig_blockDeviceTagsDefaultVolumeRBDEBS(defTags, volTags3, emptyMap, emptyMap), + PlanOnly: true, + }, + }, + }) +} + func TestAccEC2Instance_instanceProfileChange(t *testing.T) { ctx := acctest.Context(t) var v awstypes.Instance @@ -7030,6 +7164,53 @@ resource "aws_instance" "test" { `, defTgCfg, volTgCfg, rbdTgCfg, ebsTgCfg)) } +func testAccInstanceConfig_blockDeviceTagsDefault3EBS(defTg, ebsTg1, ebsTg2, ebsTg3 map[string]string) string { + //lintignore:AT004 + return acctest.ConfigCompose( + acctest.ConfigLatestAmazonLinux2HVMEBSX8664AMI(), + fmt.Sprintf(` +provider "aws" { + default_tags { + tags = { + %[1]s + } + } +} + +resource "aws_instance" "test" { + ami = data.aws_ami.amzn2-ami-minimal-hvm-ebs-x86_64.id + instance_type = "t2.medium" + + ebs_block_device { + device_name = "/dev/sdb" + volume_size = 1 + + tags = { + %[2]s + } + } + + ebs_block_device { + device_name = "/dev/sdc" + volume_size = 1 + + tags = { + %[3]s + } + } + + ebs_block_device { + device_name = "/dev/sdd" + volume_size = 1 + + tags = { + %[4]s + } + } +} +`, mapToTagConfig(defTg, 6), mapToTagConfig(ebsTg1, 6), mapToTagConfig(ebsTg2, 6), mapToTagConfig(ebsTg3, 6))) +} + func testAccInstanceConfig_blockDeviceTagsEBSTags(rName string) string { return acctest.ConfigCompose(acctest.ConfigLatestAmazonLinux2HVMEBSX8664AMI(), fmt.Sprintf(` resource "aws_instance" "test" { diff --git a/internal/tags/key_value_tags.go b/internal/tags/key_value_tags.go index 2499ae820b2..c763561a286 100644 --- a/internal/tags/key_value_tags.go +++ b/internal/tags/key_value_tags.go @@ -744,8 +744,80 @@ type configTag struct { source tagSource } +// GetAnyAttr traverses the cty.Value based on the attr string and returns the attribute. +// It takes an additional function parameter to determine whether to return a set element. +func GetAnyAttr(value cty.Value, attr string, shouldReturnSetElement func(string, cty.Value) bool) (cty.Value, error) { + // Base case: if attr is empty, return the current value + if attr == "" { + return value, nil + } + + // Split the attr string into the first part and the rest + var part, rest string + if dotIndex := strings.Index(attr, "."); dotIndex != -1 { + part = attr[:dotIndex] + rest = attr[dotIndex+1:] + } else { + part = attr + rest = "" + } + + // Handle indexed attribute + if strings.Contains(part, "[") && strings.Contains(part, "]") { + attrNameEnd := strings.Index(part, "[") + indexStart := attrNameEnd + 1 + indexEnd := strings.Index(part, "]") + + if attrNameEnd == -1 || indexEnd == -1 { + return cty.NilVal, fmt.Errorf("invalid indexed attribute format: %s", part) + } + + attrName := part[:attrNameEnd] + indexStr := part[indexStart:indexEnd] + + if !value.Type().HasAttribute(attrName) { + return cty.NilVal, fmt.Errorf("attribute %s not found", attrName) + } + + value = value.GetAttr(attrName) + + if value.Type().IsSetType() { + it := value.ElementIterator() + for it.Next() { + _, v := it.Element() + if shouldReturnSetElement(indexStr, v) { + return GetAnyAttr(v, rest, shouldReturnSetElement) + } + } + return cty.NilVal, fmt.Errorf("set element not found for attribute %s", attrName) + } + + if !value.Type().IsListType() && !value.Type().IsTupleType() { + return cty.NilVal, fmt.Errorf("attribute %s is not a list, tuple, or set", attrName) + } + + index, err := strconv.Atoi(indexStr) + if err != nil { + return cty.NilVal, fmt.Errorf("invalid index: %s", indexStr) + } + + if index >= value.LengthInt() { + return cty.NilVal, fmt.Errorf("index %d out of range for attribute %s", index, attrName) + } + + return GetAnyAttr(value.Index(cty.NumberIntVal(int64(index))), rest, shouldReturnSetElement) + } + + // Handle regular attribute + if !value.Type().HasAttribute(part) { + return cty.NilVal, fmt.Errorf("attribute %s not found", part) + } + + return GetAnyAttr(value.GetAttr(part), rest, shouldReturnSetElement) +} + // ResolveDuplicates resolves differences between incoming tags, defaultTags, and ignoreConfig -func (tags KeyValueTags) ResolveDuplicates(ctx context.Context, defaultConfig *DefaultConfig, ignoreConfig *IgnoreConfig, d schemaResourceData) KeyValueTags { +func (tags KeyValueTags) ResolveDuplicates(ctx context.Context, defaultConfig *DefaultConfig, ignoreConfig *IgnoreConfig, d schemaResourceData, tagsAttr string, setFunc func(string, cty.Value) bool) KeyValueTags { // remove default config. t := tags.RemoveDefaultConfig(defaultConfig) @@ -759,7 +831,12 @@ func (tags KeyValueTags) ResolveDuplicates(ctx context.Context, defaultConfig *D configTags := make(map[string]configTag) if configExists { - c := cf.GetAttr(names.AttrTags) + c, err := GetAnyAttr(cf, tagsAttr, setFunc) + if err != nil { + // in situations with imports and computed attributes where there's no + // matching config, return the tags unchanged + return tags + } // if the config is null just return the incoming tags // no duplicates to calculate @@ -773,14 +850,20 @@ func (tags KeyValueTags) ResolveDuplicates(ctx context.Context, defaultConfig *D } if pl := d.GetRawPlan(); !pl.IsNull() && pl.IsKnown() { - c := pl.GetAttr(names.AttrTags) + c, err := GetAnyAttr(pl, tagsAttr, setFunc) + if err != nil { + panic(fmt.Sprintf("failed to get attribute %s: %v", tagsAttr, err)) + } if !c.IsNull() && c.IsKnown() { normalizeTagsFromRaw(c.AsValueMap(), configTags, plan) } } if st := d.GetRawState(); !st.IsNull() && st.IsKnown() { - c := st.GetAttr(names.AttrTags) + c, err := GetAnyAttr(st, tagsAttr, setFunc) + if err != nil { + panic(fmt.Sprintf("failed to get attribute %s: %v", tagsAttr, err)) + } if !c.IsNull() { normalizeTagsFromRaw(c.AsValueMap(), configTags, state) } diff --git a/website/docs/r/ebs_volume.html.markdown b/website/docs/r/ebs_volume.html.markdown index 361d2d17dff..5021c7daf35 100644 --- a/website/docs/r/ebs_volume.html.markdown +++ b/website/docs/r/ebs_volume.html.markdown @@ -58,7 +58,7 @@ This resource exports the following attributes in addition to the arguments abov - `create` - (Default `5m`) - `update` - (Default `5m`) -- `delete` - (Default `5m`) +- `delete` - (Default `10m`) ## Import