diff --git a/.changelog/23232.txt b/.changelog/23232.txt new file mode 100644 index 00000000000..0670b9afed9 --- /dev/null +++ b/.changelog/23232.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_s3_bucket_lifecycle_configuration: Prevent non-empty plans when `filter` is an empty configuration block +``` \ No newline at end of file diff --git a/internal/service/s3/bucket_lifecycle_configuration_test.go b/internal/service/s3/bucket_lifecycle_configuration_test.go index 063b3db93f7..2edbe1d441a 100644 --- a/internal/service/s3/bucket_lifecycle_configuration_test.go +++ b/internal/service/s3/bucket_lifecycle_configuration_test.go @@ -603,6 +603,32 @@ func TestAccS3BucketLifecycleConfiguration_TransitionUpdateBetweenDaysAndDate_in }) } +// Reference: https://github.com/hashicorp/terraform-provider-aws/issues/23228 +func TestAccS3BucketLifecycleConfiguration_EmptyFilter_NonCurrentVersions(t *testing.T) { + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_s3_bucket_lifecycle_configuration.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, s3.EndpointsID), + Providers: acctest.Providers, + CheckDestroy: testAccCheckBucketLifecycleConfigurationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccBucketLifecycleConfiguration_EmptyFilter_NonCurrentVersionsConfig(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckBucketLifecycleConfigurationExists(resourceName), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + func testAccCheckBucketLifecycleConfigurationDestroy(s *terraform.State) error { conn := acctest.Provider.Meta().(*conns.AWSClient).S3Conn @@ -1111,3 +1137,38 @@ resource "aws_s3_bucket_lifecycle_configuration" "test" { } `, rName, transitionDate, storageClass) } + +func testAccBucketLifecycleConfiguration_EmptyFilter_NonCurrentVersionsConfig(rName string) string { + return fmt.Sprintf(` +resource "aws_s3_bucket" "test" { + bucket = %[1]q +} + +resource "aws_s3_bucket_acl" "test" { + bucket = aws_s3_bucket.test.id + acl = "private" +} + +resource "aws_s3_bucket_lifecycle_configuration" "test" { + bucket = aws_s3_bucket.test.bucket + + rule { + id = %[1]q + + filter {} + + noncurrent_version_expiration { + newer_noncurrent_versions = 2 + noncurrent_days = 30 + } + + noncurrent_version_transition { + noncurrent_days = 30 + storage_class = "STANDARD_IA" + } + + status = "Enabled" + } +} +`, rName) +} diff --git a/internal/service/s3/flex.go b/internal/service/s3/flex.go index cc870abd82e..101749b4c6e 100644 --- a/internal/service/s3/flex.go +++ b/internal/service/s3/flex.go @@ -886,14 +886,6 @@ func FlattenLifecycleRuleFilter(filter *s3.LifecycleRuleFilter) []interface{} { return nil } - if filter.And == nil && - filter.ObjectSizeGreaterThan == nil && - filter.ObjectSizeLessThan == nil && - (filter.Prefix == nil || aws.StringValue(filter.Prefix) == "") && - filter.Tag == nil { - return nil - } - m := make(map[string]interface{}) if filter.And != nil { @@ -908,7 +900,7 @@ func FlattenLifecycleRuleFilter(filter *s3.LifecycleRuleFilter) []interface{} { m["object_size_less_than"] = int(aws.Int64Value(filter.ObjectSizeLessThan)) } - if filter.Prefix != nil && aws.StringValue(filter.Prefix) != "" { + if filter.Prefix != nil { m["prefix"] = aws.StringValue(filter.Prefix) } diff --git a/website/docs/guides/version-4-upgrade.html.md b/website/docs/guides/version-4-upgrade.html.md index e2bd6ec44c7..cde5f017ea6 100644 --- a/website/docs/guides/version-4-upgrade.html.md +++ b/website/docs/guides/version-4-upgrade.html.md @@ -467,6 +467,199 @@ your Terraform state and will henceforth be managed by Terraform. Switch your Terraform configuration to the [`aws_s3_bucket_lifecycle_configuration` resource](/docs/providers/aws/r/s3_bucket_lifecycle_configuration.html) instead. +#### For Lifecycle Rules with no `prefix` previously configured + +For example, given this previous configuration: + +```terraform +resource "aws_s3_bucket" "example" { + bucket = "my-example-bucket" + + lifecycle_rule { + id = "Keep previous version 30 days, then in Glacier another 60" + enabled = true + + noncurrent_version_transition { + days = 30 + storage_class = "GLACIER" + } + + noncurrent_version_expiration { + days = 90 + } + } + + lifecycle_rule { + id = "Delete old incomplete multi-part uploads" + enabled = true + abort_incomplete_multipart_upload_days = 7 + } +} +``` + +It will receive the following error after upgrading: + +``` +│ Error: Value for unconfigurable attribute +│ +│ with aws_s3_bucket.example, +│ on main.tf line 1, in resource "aws_s3_bucket" "example": +│ 1: resource "aws_s3_bucket" "example" { +│ +│ Can't configure a value for "lifecycle_rule": its value will be decided automatically based on the result of applying this configuration. +``` + +Since the `lifecycle_rule` argument changed to read-only, the recommendation is to update the configuration to use the `aws_s3_bucket_lifecycle_configuration` +resource and remove any references to `lifecycle_rule` and its nested arguments in the `aws_s3_bucket` resource. + +~> **Note:** When configuring the `rule.filter` configuration block in the new `aws_s3_bucket_lifecycle_configuration` resource, it is recommended to use the AWS CLI s3api [get-bucket-lifecycle-configuration](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/s3api/get-bucket-lifecycle-configuration.html) +to fetch the source bucket's lifecycle configuration and determine if the `Filter` is configured as `"Filter" : {}` or `"Filter" : { "Prefix": "" }`. +If the former is returned, `rule.filter` should be configured as `filter {}`. If the latter is returned, `rule.filter` should be configured as follows. + +```terraform +resource "aws_s3_bucket" "example" { + # ... other configuration ... +} + +resource "aws_s3_bucket_lifecycle_configuration" "example" { + bucket = aws_s3_bucket.example.id + + rule { + id = "Keep previous version 30 days, then in Glacier another 60" + status = "Enabled" + + filter { + prefix = "" + } + + noncurrent_version_transition { + noncurrent_days = 30 + storage_class = "GLACIER" + } + + noncurrent_version_expiration { + noncurrent_days = 90 + } + } + + rule { + id = "Delete old incomplete multi-part uploads" + status = "Enabled" + + filter { + prefix = "" + } + + abort_incomplete_multipart_upload { + days_after_initiation = 7 + } + } +} +``` + +It is then recommended running `terraform import` on each new resource to prevent data loss, e.g. + +```shell +$ terraform import aws_s3_bucket_lifecycle_configuration.example example +aws_s3_bucket_lifecycle_configuration.example: Importing from ID "example"... +aws_s3_bucket_lifecycle_configuration.example: Import prepared! + Prepared aws_s3_bucket_lifecycle_configuration for import +aws_s3_bucket_lifecycle_configuration.example: Refreshing state... [id=example] + +Import successful! + +The resources that were imported are shown above. These resources are now in +your Terraform state and will henceforth be managed by Terraform. +``` + +#### For Lifecycle Rules with `prefix` previously configured as an empty string + +For example, given this configuration: + +```terraform +resource "aws_s3_bucket" "example" { + bucket = "my-example-bucket" + + lifecycle_rule { + id = "log-expiration" + enabled = true + prefix = "" + + transition { + days = 30 + storage_class = "STANDARD_IA" + } + + transition { + days = 180 + storage_class = "GLACIER" + } + } +} +``` + +It will receive the following error after upgrading: + +``` +│ Error: Value for unconfigurable attribute +│ +│ with aws_s3_bucket.example, +│ on main.tf line 1, in resource "aws_s3_bucket" "example": +│ 1: resource "aws_s3_bucket" "example" { +│ +│ Can't configure a value for "lifecycle_rule": its value will be decided automatically based on the result of applying this configuration. +``` + +Since the `lifecycle_rule` argument changed to read-only, the recommendation is to update the configuration to use the `aws_s3_bucket_lifecycle_configuration` +resource and remove any references to `lifecycle_rule` and its nested arguments in the `aws_s3_bucket` resource: + +```terraform +resource "aws_s3_bucket" "example" { + # ... other configuration ... +} + +resource "aws_s3_bucket_lifecycle_configuration" "example" { + bucket = aws_s3_bucket.example.id + + rule { + id = "log-expiration" + status = "Enabled" + + filter { + prefix = "" + } + + transition { + days = 30 + storage_class = "STANDARD_IA" + } + + transition { + days = 180 + storage_class = "GLACIER" + } + } +} +``` + +It is then recommended running `terraform import` on each new resource to prevent data loss, e.g. + +```shell +$ terraform import aws_s3_bucket_lifecycle_configuration.example example +aws_s3_bucket_lifecycle_configuration.example: Importing from ID "example"... +aws_s3_bucket_lifecycle_configuration.example: Import prepared! + Prepared aws_s3_bucket_lifecycle_configuration for import +aws_s3_bucket_lifecycle_configuration.example: Refreshing state... [id=example] + +Import successful! + +The resources that were imported are shown above. These resources are now in +your Terraform state and will henceforth be managed by Terraform. +``` + +#### For Lifecycle Rules with a `prefix` + + For example, given this previous configuration: ```terraform @@ -476,18 +669,22 @@ resource "aws_s3_bucket" "example" { id = "log" enabled = true prefix = "log/" + tags = { rule = "log" autoclean = "true" } + transition { days = 30 storage_class = "STANDARD_IA" } + transition { days = 60 storage_class = "GLACIER" } + expiration { days = 90 } @@ -497,6 +694,7 @@ resource "aws_s3_bucket" "example" { id = "tmp" prefix = "tmp/" enabled = true + expiration { date = "2022-12-31" } @@ -534,6 +732,7 @@ resource "aws_s3_bucket_lifecycle_configuration" "example" { filter { and { prefix = "log/" + tags = { rule = "log" autoclean = "true" diff --git a/website/docs/r/s3_bucket_lifecycle_configuration.html.markdown b/website/docs/r/s3_bucket_lifecycle_configuration.html.markdown index 5e3ef2e4ccf..55d5788b306 100644 --- a/website/docs/r/s3_bucket_lifecycle_configuration.html.markdown +++ b/website/docs/r/s3_bucket_lifecycle_configuration.html.markdown @@ -151,11 +151,11 @@ The `rule` configuration block supports the following arguments: * `abort_incomplete_multipart_upload` - (Optional) Configuration block that specifies the days since the initiation of an incomplete multipart upload that Amazon S3 will wait before permanently removing all parts of the upload [documented below](#abort_incomplete_multipart_upload). * `expiration` - (Optional) Configuration block that specifies the expiration for the lifecycle of the object in the form of date, days and, whether the object has a delete marker [documented below](#expiration). -* `filter` - (Optional) Configuration block used to identify objects that a Lifecycle Rule applies to [documented below](#filter). +* `filter` - (Optional) Configuration block used to identify objects that a Lifecycle Rule applies to [documented below](#filter). If not specified, the `rule` will default to using `prefix`. * `id` - (Required) Unique identifier for the rule. The value cannot be longer than 255 characters. * `noncurrent_version_expiration` - (Optional) Configuration block that specifies when noncurrent object versions expire [documented below](#noncurrent_version_expiration). * `noncurrent_version_transition` - (Optional) Set of configuration blocks that specify the transition rule for the lifecycle rule that describes when noncurrent objects transition to a specific storage class [documented below](#noncurrent_version_transition). -* `prefix` - (Optional) **DEPRECATED** Use `filter` instead. This has been deprecated by Amazon S3. Prefix identifying one or more objects to which the rule applies. +* `prefix` - (Optional) **DEPRECATED** Use `filter` instead. This has been deprecated by Amazon S3. Prefix identifying one or more objects to which the rule applies. Defaults to an empty string (`""`) if `filter` is not specified. * `status` - (Required) Whether the rule is currently being applied. Valid values: `Enabled` or `Disabled`. * `transition` - (Optional) Set of configuration blocks that specify when an Amazon S3 object transitions to a specified storage class [documented below](#transition).