diff --git a/.changelog/40689.txt b/.changelog/40689.txt new file mode 100644 index 00000000000..e9bc5c41894 --- /dev/null +++ b/.changelog/40689.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_s3_object_copy: Add `override_provider` configuration block, allowing tags inherited from the provider [`default_tags` configuration block](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#default_tags-configuration-block) to be ignored +``` \ No newline at end of file diff --git a/internal/service/s3/object_copy.go b/internal/service/s3/object_copy.go index 2abc9645b8a..be53f5de65a 100644 --- a/internal/service/s3/object_copy.go +++ b/internal/service/s3/object_copy.go @@ -40,6 +40,13 @@ func resourceObjectCopy() *schema.Resource { UpdateWithoutTimeout: resourceObjectCopyUpdate, DeleteWithoutTimeout: resourceObjectCopyDelete, + CustomizeDiff: func(ctx context.Context, d *schema.ResourceDiff, meta interface{}) error { + if ignoreProviderDefaultTags(ctx, d) { + return d.SetNew(names.AttrTagsAll, d.Get(names.AttrTags)) + } + return verify.SetTagsDiff(ctx, d, meta) + }, + Schema: map[string]*schema.Schema{ "acl": { Type: schema.TypeString, @@ -263,6 +270,30 @@ func resourceObjectCopy() *schema.Resource { Computed: true, ValidateFunc: validation.IsRFC3339Time, }, + "override_provider": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "default_tags": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + names.AttrTags: { + Type: schema.TypeMap, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + ValidateDiagFunc: verify.MapSizeBetween(0, 0), + }, + }, + }, + }, + }, + }, + }, "request_charged": { Type: schema.TypeBool, Computed: true, @@ -324,8 +355,6 @@ func resourceObjectCopy() *schema.Resource { Computed: true, }, }, - - CustomizeDiff: verify.SetTagsDiff, } } @@ -649,8 +678,13 @@ func resourceObjectCopyDoCopy(ctx context.Context, d *schema.ResourceData, meta } defaultTagsConfig := meta.(*conns.AWSClient).DefaultTagsConfig(ctx) - tags := tftags.New(ctx, getContextTags(ctx)) - tags = defaultTagsConfig.MergeTags(tags) + tags := tftags.New(ctx, d.Get(names.AttrTags).(map[string]interface{})) + if ignoreProviderDefaultTags(ctx, d) { + tags = tags.RemoveDefaultConfig(defaultTagsConfig) + } else { + tags = defaultTagsConfig.MergeTags(tftags.New(ctx, tags)) + } + if len(tags) > 0 { // The tag-set must be encoded as URL Query parameters. input.Tagging = aws.String(tags.IgnoreAWS().URLEncode()) diff --git a/internal/service/s3/object_copy_test.go b/internal/service/s3/object_copy_test.go index f5f5674e31c..849b9e8478a 100644 --- a/internal/service/s3/object_copy_test.go +++ b/internal/service/s3/object_copy_test.go @@ -218,6 +218,143 @@ func TestAccS3ObjectCopy_BucketKeyEnabled_object(t *testing.T) { }) } +func TestAccS3ObjectCopy_DefaultTags_providerOnly(t *testing.T) { + ctx := acctest.Context(t) + rName1 := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + rName2 := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_s3_object_copy.test" + sourceKey := "dir1/dir2/source" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.S3ServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckObjectCopyDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: acctest.ConfigCompose( + acctest.ConfigDefaultTags_Tags1(acctest.CtProviderKey1, acctest.CtProviderValue1), + testAccObjectCopyConfig_simple(rName1, sourceKey, rName2, names.AttrTarget), + ), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckObjectCopyExists(ctx, resourceName), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsPercent, "0"), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsAllPercent, "1"), + resource.TestCheckResourceAttr(resourceName, "tags_all.providerkey1", acctest.CtProviderValue1), + ), + }, + }, + }) +} + +func TestAccS3ObjectCopy_DefaultTags_providerAndResource(t *testing.T) { + ctx := acctest.Context(t) + rName1 := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + rName2 := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_s3_object_copy.test" + sourceKey := "dir1/dir2/source" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.S3ServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckObjectCopyDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: acctest.ConfigCompose( + acctest.ConfigDefaultTags_Tags1(acctest.CtProviderKey1, acctest.CtProviderValue1), + testAccObjectCopyConfig_tags(rName1, sourceKey, rName2, names.AttrTarget), + ), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckObjectCopyExists(ctx, resourceName), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsPercent, "3"), + resource.TestCheckResourceAttr(resourceName, "tags.Key1", "A@AA"), + resource.TestCheckResourceAttr(resourceName, "tags.Key2", "BBB"), + resource.TestCheckResourceAttr(resourceName, "tags.Key3", "CCC"), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsAllPercent, "4"), + resource.TestCheckResourceAttr(resourceName, "tags_all.providerkey1", acctest.CtProviderValue1), + resource.TestCheckResourceAttr(resourceName, "tags_all.Key1", "A@AA"), + resource.TestCheckResourceAttr(resourceName, "tags_all.Key2", "BBB"), + resource.TestCheckResourceAttr(resourceName, "tags_all.Key3", "CCC"), + ), + }, + { + Config: acctest.ConfigCompose( + acctest.ConfigDefaultTags_Tags1(acctest.CtProviderKey1, acctest.CtProviderValue1), + testAccObjectCopyConfig_updatedTags(rName1, sourceKey, rName2, names.AttrTarget), + ), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckObjectCopyExists(ctx, resourceName), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsPercent, "4"), + resource.TestCheckResourceAttr(resourceName, "tags.Key2", "B@BB"), + resource.TestCheckResourceAttr(resourceName, "tags.Key3", "X X"), + resource.TestCheckResourceAttr(resourceName, "tags.Key4", "DDD"), + resource.TestCheckResourceAttr(resourceName, "tags.Key5", "E:/"), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsAllPercent, "5"), + resource.TestCheckResourceAttr(resourceName, "tags_all.providerkey1", acctest.CtProviderValue1), + resource.TestCheckResourceAttr(resourceName, "tags_all.Key2", "B@BB"), + resource.TestCheckResourceAttr(resourceName, "tags_all.Key3", "X X"), + resource.TestCheckResourceAttr(resourceName, "tags_all.Key4", "DDD"), + resource.TestCheckResourceAttr(resourceName, "tags_all.Key5", "E:/"), + ), + }, + }, + }) +} + +func TestAccS3ObjectCopy_DefaultTags_providerAndResourceWithOverride(t *testing.T) { + ctx := acctest.Context(t) + rName1 := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + rName2 := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_s3_object_copy.test" + sourceKey := "dir1/dir2/source" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.S3ServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckObjectCopyDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: acctest.ConfigCompose( + acctest.ConfigDefaultTags_Tags1(acctest.CtProviderKey1, acctest.CtProviderValue1), + testAccObjectCopyConfig_tagsWithOverride(rName1, sourceKey, rName2, names.AttrTarget), + ), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckObjectCopyExists(ctx, resourceName), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsPercent, "3"), + resource.TestCheckResourceAttr(resourceName, "tags.Key1", "A@AA"), + resource.TestCheckResourceAttr(resourceName, "tags.Key2", "BBB"), + resource.TestCheckResourceAttr(resourceName, "tags.Key3", "CCC"), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsAllPercent, "3"), + resource.TestCheckResourceAttr(resourceName, "tags_all.Key1", "A@AA"), + resource.TestCheckResourceAttr(resourceName, "tags_all.Key2", "BBB"), + resource.TestCheckResourceAttr(resourceName, "tags_all.Key3", "CCC"), + ), + }, + { + Config: acctest.ConfigCompose( + acctest.ConfigDefaultTags_Tags1(acctest.CtProviderKey1, acctest.CtProviderValue1), + testAccObjectCopyConfig_updatedTagsOverride(rName1, sourceKey, rName2, names.AttrTarget), + ), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckObjectCopyExists(ctx, resourceName), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsPercent, "4"), + resource.TestCheckResourceAttr(resourceName, "tags.Key2", "B@BB"), + resource.TestCheckResourceAttr(resourceName, "tags.Key3", "X X"), + resource.TestCheckResourceAttr(resourceName, "tags.Key4", "DDD"), + resource.TestCheckResourceAttr(resourceName, "tags.Key5", "E:/"), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsAllPercent, "4"), + resource.TestCheckResourceAttr(resourceName, "tags.Key2", "B@BB"), + resource.TestCheckResourceAttr(resourceName, "tags.Key3", "X X"), + resource.TestCheckResourceAttr(resourceName, "tags.Key4", "DDD"), + resource.TestCheckResourceAttr(resourceName, "tags.Key5", "E:/"), + ), + }, + }, + }) +} + func TestAccS3ObjectCopy_sourceWithSlashes(t *testing.T) { ctx := acctest.Context(t) rName1 := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) @@ -709,6 +846,102 @@ resource "aws_s3_object_copy" "test" { `, targetKey)) } +func testAccObjectCopyConfig_tags(sourceBucket, sourceKey, targetBucket, targetKey string) string { + return acctest.ConfigCompose(testAccObjectCopyConfig_baseSourceObject(sourceBucket, sourceKey, targetBucket), fmt.Sprintf(` +resource "aws_s3_object_copy" "test" { + bucket = aws_s3_bucket.target.bucket + key = %[1]q + source = "${aws_s3_bucket.source.bucket}/${aws_s3_object.source.key}" + + tags = { + Key1 = "A@AA" + Key2 = "BBB" + Key3 = "CCC" + } + + tagging_directive = "REPLACE" +} +`, targetKey)) +} + +func testAccObjectCopyConfig_tagsWithOverride(sourceBucket, sourceKey, targetBucket, targetKey string) string { + return acctest.ConfigCompose(testAccObjectCopyConfig_baseSourceObject(sourceBucket, sourceKey, targetBucket), fmt.Sprintf(` +resource "aws_s3_object_copy" "test" { + bucket = aws_s3_bucket.target.bucket + key = %[1]q + source = "${aws_s3_bucket.source.bucket}/${aws_s3_object.source.key}" + + tags = { + Key1 = "A@AA" + Key2 = "BBB" + Key3 = "CCC" + } + + tagging_directive = "REPLACE" + + override_provider { + default_tags { + tags = {} + } + } +} +`, targetKey)) +} + +func testAccObjectCopyConfig_updatedTags(sourceBucket, sourceKey, targetBucket, targetKey string) string { + return acctest.ConfigCompose(testAccObjectCopyConfig_baseSourceObject(sourceBucket, sourceKey, targetBucket), fmt.Sprintf(` +resource "aws_s3_object_copy" "test" { + bucket = aws_s3_bucket.target.bucket + key = %[1]q + source = "${aws_s3_bucket.source.bucket}/${aws_s3_object.source.key}" + + tags = { + Key2 = "B@BB" + Key3 = "X X" + Key4 = "DDD" + Key5 = "E:/" + } + + tagging_directive = "REPLACE" +} +`, targetKey)) +} + +func testAccObjectCopyConfig_updatedTagsOverride(sourceBucket, sourceKey, targetBucket, targetKey string) string { + return acctest.ConfigCompose(testAccObjectCopyConfig_baseSourceObject(sourceBucket, sourceKey, targetBucket), fmt.Sprintf(` +resource "aws_s3_object_copy" "test" { + bucket = aws_s3_bucket.target.bucket + key = %[1]q + source = "${aws_s3_bucket.source.bucket}/${aws_s3_object.source.key}" + + tags = { + Key2 = "B@BB" + Key3 = "X X" + Key4 = "DDD" + Key5 = "E:/" + } + + tagging_directive = "REPLACE" + + override_provider { + default_tags { + tags = {} + } + } +} +`, targetKey)) +} + +func testAccObjectCopyConfig_simple(sourceBucket, sourceKey, targetBucket, targetKey string) string { + return acctest.ConfigCompose(testAccObjectCopyConfig_baseSourceObject(sourceBucket, sourceKey, targetBucket), fmt.Sprintf(` +resource "aws_s3_object_copy" "test" { + bucket = aws_s3_bucket.target.bucket + key = %[1]q + source = "${aws_s3_bucket.source.bucket}/${aws_s3_object.source.key}" +} +`, targetKey)) +} + func testAccObjectCopyConfig_externalSourceObject(sourceBucket, sourceKey, targetBucket, targetKey string) string { return acctest.ConfigCompose(testAccObjectCopyConfig_baseSourceAndTargetBuckets(sourceBucket, targetBucket), fmt.Sprintf(` resource "aws_s3_object_copy" "test" { diff --git a/website/docs/r/s3_object_copy.html.markdown b/website/docs/r/s3_object_copy.html.markdown index 15bc54db070..465e38113c8 100644 --- a/website/docs/r/s3_object_copy.html.markdown +++ b/website/docs/r/s3_object_copy.html.markdown @@ -26,6 +26,24 @@ resource "aws_s3_object_copy" "test" { } ``` +### Ignoring Provider `default_tags` + +S3 objects support a [maximum of 10 tags](https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-tagging.html). +If the resource's own `tags` and the provider-level `default_tags` would together lead to more than 10 tags on an S3 object copy, use the `override_provider` configuration block to suppress any provider-level `default_tags`. + +```terraform +resource "aws_s3_object_copy" "test" { + bucket = "destination_bucket" + key = "destination_key" + source = "source_bucket/source_key" + override_provider { + default_tags { + tags = {} + } + } +} +``` + ## Argument Reference The following arguments are required: @@ -89,6 +107,12 @@ This configuration block has the following optional arguments (one of the three -> **Note:** Terraform ignores all leading `/`s in the object's `key` and treats multiple `/`s in the rest of the object's `key` as a single `/`, so values of `/index.html` and `index.html` correspond to the same S3 object as do `first//second///third//` and `first/second/third/`. +### Override Provider + +The `override_provider` block supports the following: + +* `default_tags` - (Optional) Override the provider [`default_tags` configuration block](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#default_tags-configuration-block). + ## Attribute Reference This resource exports the following attributes in addition to the arguments above: