Skip to content

Commit

Permalink
r/s3_bucket_replication_configuration: correctly populate API request…
Browse files Browse the repository at this point in the history
…s with empty rule.filter and empty rule.filter.prefix
  • Loading branch information
anGie44 committed Mar 9, 2022
1 parent 69a6ac8 commit 35cb45e
Show file tree
Hide file tree
Showing 3 changed files with 168 additions and 13 deletions.
143 changes: 143 additions & 0 deletions internal/service/s3/bucket_replication_configuration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -758,6 +758,91 @@ func TestAccS3BucketReplicationConfiguration_existingObjectReplication(t *testin
})
}

// Reference: https://github.com/hashicorp/terraform-provider-aws/issues/23487
func TestAccS3BucketReplicationConfiguration_filter_emptyConfigurationBlock(t *testing.T) {
resourceName := "aws_s3_bucket_replication_configuration.test"
rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix)
dstBucketResourceName := "aws_s3_bucket.destination"
iamRoleResourceName := "aws_iam_role.test"

// record the initialized providers so that we can use them to check for the instances in each region
var providers []*schema.Provider

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(t) },
ErrorCheck: acctest.ErrorCheck(t, s3.EndpointsID),
ProviderFactories: acctest.FactoriesAlternate(&providers),
CheckDestroy: acctest.CheckWithProviders(testAccCheckBucketReplicationConfigurationDestroy, &providers),
Steps: []resource.TestStep{
{
Config: testAccBucketReplicationConfiguration_filter_emptyConfigurationBlock(rName),
Check: resource.ComposeTestCheckFunc(
testAccCheckBucketReplicationConfigurationExists(resourceName),
resource.TestCheckResourceAttrPair(resourceName, "role", iamRoleResourceName, "arn"),
resource.TestCheckResourceAttr(resourceName, "rule.#", "1"),
resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rule.*", map[string]string{
"id": "foobar",
"delete_marker_replication.#": "1",
"delete_marker_replication.0.status": s3.DeleteMarkerReplicationStatusDisabled,
"filter.#": "1",
"status": s3.ReplicationRuleStatusEnabled,
"destination.#": "1",
}),
resource.TestCheckTypeSetElemAttrPair(resourceName, "rule.*.destination.0.bucket", dstBucketResourceName, "arn"),
),
},
{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
},
},
})
}

// Reference: https://github.com/hashicorp/terraform-provider-aws/issues/23487
func TestAccS3BucketReplicationConfiguration_filter_emptyPrefix(t *testing.T) {
resourceName := "aws_s3_bucket_replication_configuration.test"
rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix)
dstBucketResourceName := "aws_s3_bucket.destination"
iamRoleResourceName := "aws_iam_role.test"

// record the initialized providers so that we can use them to check for the instances in each region
var providers []*schema.Provider

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(t) },
ErrorCheck: acctest.ErrorCheck(t, s3.EndpointsID),
ProviderFactories: acctest.FactoriesAlternate(&providers),
CheckDestroy: acctest.CheckWithProviders(testAccCheckBucketReplicationConfigurationDestroy, &providers),
Steps: []resource.TestStep{
{
Config: testAccBucketReplicationConfiguration_filter_emptyPrefix(rName),
Check: resource.ComposeTestCheckFunc(
testAccCheckBucketReplicationConfigurationExists(resourceName),
resource.TestCheckResourceAttrPair(resourceName, "role", iamRoleResourceName, "arn"),
resource.TestCheckResourceAttr(resourceName, "rule.#", "1"),
resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rule.*", map[string]string{
"id": "foobar",
"delete_marker_replication.#": "1",
"delete_marker_replication.0.status": s3.DeleteMarkerReplicationStatusDisabled,
"filter.#": "1",
"filter.0.prefix": "",
"status": s3.ReplicationRuleStatusEnabled,
"destination.#": "1",
}),
resource.TestCheckTypeSetElemAttrPair(resourceName, "rule.*.destination.0.bucket", dstBucketResourceName, "arn"),
),
},
{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
},
},
})
}

func TestAccS3BucketReplicationConfiguration_filter_tagFilter(t *testing.T) {
resourceName := "aws_s3_bucket_replication_configuration.test"
rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix)
Expand Down Expand Up @@ -1707,6 +1792,64 @@ resource "aws_s3_bucket_replication_configuration" "test" {
`, rName, rNameDestination)
}

func testAccBucketReplicationConfiguration_filter_emptyConfigurationBlock(rName string) string {
return acctest.ConfigCompose(
testAccBucketReplicationConfigurationBase(rName),
`
resource "aws_s3_bucket_replication_configuration" "test" {
depends_on = [aws_s3_bucket_versioning.source]
bucket = aws_s3_bucket.source.id
role = aws_iam_role.test.arn
rule {
id = "foobar"
delete_marker_replication {
status = "Disabled"
}
filter {}
status = "Enabled"
destination {
bucket = aws_s3_bucket.destination.arn
}
}
}`)
}

func testAccBucketReplicationConfiguration_filter_emptyPrefix(rName string) string {
return acctest.ConfigCompose(
testAccBucketReplicationConfigurationBase(rName), `
resource "aws_s3_bucket_replication_configuration" "test" {
depends_on = [aws_s3_bucket_versioning.source]
bucket = aws_s3_bucket.source.id
role = aws_iam_role.test.arn
rule {
id = "foobar"
delete_marker_replication {
status = "Disabled"
}
filter {
prefix = ""
}
status = "Enabled"
destination {
bucket = aws_s3_bucket.destination.arn
}
}
}`,
)
}

func testAccBucketReplicationConfiguration_filter_tag(rName, key, value string) string {
return acctest.ConfigCompose(
testAccBucketReplicationConfigurationBase(rName),
Expand Down
31 changes: 21 additions & 10 deletions internal/service/s3/flex.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,33 +135,41 @@ func ExpandExistingObjectReplication(l []interface{}) *s3.ExistingObjectReplicat
}

func ExpandFilter(l []interface{}) *s3.ReplicationRuleFilter {
if len(l) == 0 || l[0] == nil {
if len(l) == 0 {
return nil
}

tfMap, ok := l[0].(map[string]interface{})
result := &s3.ReplicationRuleFilter{}

if !ok {
return nil
// Support the empty filter block in terraform i.e. 'filter {}',
// which is also supported by the API even though the docs note that
// one of Prefix, Tag, or And is required.
if l[0] == nil {
return result
}

result := &s3.ReplicationRuleFilter{}
tfMap := l[0].(map[string]interface{})

if v, ok := tfMap["and"].([]interface{}); ok && len(v) > 0 && v[0] != nil {
result.And = ExpandReplicationRuleAndOperator(v)
}

if v, ok := tfMap["prefix"].(string); ok && v != "" {
result.Prefix = aws.String(v)
}

if v, ok := tfMap["tag"].([]interface{}); ok && len(v) > 0 && v[0] != nil {
tags := Tags(tftags.New(v[0]).IgnoreAWS())
if len(tags) > 0 {
result.Tag = tags[0]
}
}

// Per AWS S3 API, "A Filter must have exactly one of Prefix, Tag, or And specified";
// Specifying more than one of the listed parameters results in a MalformedXML error.
// If a filter is specified as filter { prefix = "" } in Terraform, we should send the prefix value
// in the API request even if it is an empty value, else Terraform will report non-empty plans.
// Reference: https://github.com/hashicorp/terraform-provider-aws/issues/23487
if v, ok := tfMap["prefix"].(string); ok && result.And == nil && result.Tag == nil {
result.Prefix = aws.String(v)
}

return result
}

Expand Down Expand Up @@ -606,7 +614,10 @@ func ExpandRules(l []interface{}) []*s3.ReplicationRule {
rule.Status = aws.String(v)
}

if v, ok := tfMap["filter"].([]interface{}); ok && len(v) > 0 && v[0] != nil {
// Support the empty filter block in terraform i.e. 'filter {}',
// which implies the replication rule does not require a specific filter,
// by expanding the "filter" array even if the first element is nil.
if v, ok := tfMap["filter"].([]interface{}); ok && len(v) > 0 {
// XML schema V2
rule.Filter = ExpandFilter(v)
rule.Priority = aws.Int64(int64(tfMap["priority"].(int)))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -229,9 +229,9 @@ The `rule` configuration block supports the following arguments:
* `delete_marker_replication` - (Optional) Whether delete markers are replicated. This argument is only valid with V2 replication configurations (i.e., when `filter` is used)[documented below](#delete_marker_replication).
* `destination` - (Required) Specifies the destination for the rule [documented below](#destination).
* `existing_object_replication` - (Optional) Replicate existing objects in the source bucket according to the rule configurations [documented below](#existing_object_replication).
* `filter` - (Optional, Conflicts with `prefix`) Filter that identifies subset of objects to which the replication rule applies [documented below](#filter).
* `filter` - (Optional, Conflicts with `prefix`) Filter that identifies subset of objects to which the replication rule applies [documented below](#filter). If not specified, the `rule` will default to using `prefix`.
* `id` - (Optional) Unique identifier for the rule. Must be less than or equal to 255 characters in length.
* `prefix` - (Optional, Conflicts with `filter`) Object key name prefix identifying one or more objects to which the rule applies. Must be less than or equal to 1024 characters in length.
* `prefix` - (Optional, Conflicts with `filter`) Object key name prefix identifying one or more objects to which the rule applies. Must be less than or equal to 1024 characters in length. Defaults to an empty string (`""`) if `filter` is not specified.
* `priority` - (Optional) The priority associated with the rule. Priority should only be set if `filter` is configured. If not provided, defaults to `0`. Priority must be unique between multiple rules.
* `source_selection_criteria` - (Optional) Specifies special object selection criteria [documented below](#source_selection_criteria).
* `status` - (Required) The status of the rule. Either `"Enabled"` or `"Disabled"`. The rule is ignored if status is not "Enabled".
Expand Down Expand Up @@ -346,7 +346,8 @@ The `existing_object_replication` configuration block supports the following arg

### filter

~> **NOTE:** With the `filter` argument, you must specify exactly one of `prefix`, `tag`, or `and`. Replication configuration V1 supports filtering based on only the `prefix` attribute. For backwards compatibility, Amazon S3 continues to support the V1 configuration.
~> **NOTE:** The `filter` argument must be specified as either an empty configuration block (`filter {}`) to imply the rule requires no filter or with exactly one of `prefix`, `tag`, or `and`.
Replication configuration V1 supports filtering based on only the `prefix` attribute. For backwards compatibility, Amazon S3 continues to support the V1 configuration.

The `filter` configuration block supports the following arguments:

Expand Down

0 comments on commit 35cb45e

Please sign in to comment.