diff --git a/.changelog/39578.txt b/.changelog/39578.txt new file mode 100644 index 00000000000..a74d21355b3 --- /dev/null +++ b/.changelog/39578.txt @@ -0,0 +1,7 @@ +```release-note:enhancement +resource/aws_s3_bucket_lifecycle_configuration: Add `transition_default_minimum_object_size` argument +``` + +```release-note:note +resource/aws_s3_bucket_lifecycle_configuration: Amazon S3 now applies a default minimum object size of 128 KB for S3 Lifecycle transition rules to any S3 storage class. This new default behavior will be applied to any new or modified S3 Lifecycle configuration. You can override this new default and customize the minimum object size for S3 Lifecycle transition rules to any value +``` diff --git a/internal/service/s3/bucket.go b/internal/service/s3/bucket.go index 9bd8d859b33..81a94fbadda 100644 --- a/internal/service/s3/bucket.go +++ b/internal/service/s3/bucket.go @@ -1010,7 +1010,13 @@ func resourceBucketRead(ctx context.Context, d *schema.ResourceData, meta interf // Bucket Lifecycle Configuration. // lifecycleRules, err := retryWhenNoSuchBucketError(ctx, d.Timeout(schema.TimeoutRead), func() ([]types.LifecycleRule, error) { - return findLifecycleRules(ctx, conn, d.Id(), "") + output, err := findBucketLifecycleConfiguration(ctx, conn, d.Id(), "") + + if err != nil { + return nil, err + } + + return output.Rules, nil }) if !d.IsNewResource() && tfawserr.ErrCodeEquals(err, errCodeNoSuchBucket) { diff --git a/internal/service/s3/bucket_lifecycle_configuration.go b/internal/service/s3/bucket_lifecycle_configuration.go index d31352a8341..6db1b3b349c 100644 --- a/internal/service/s3/bucket_lifecycle_configuration.go +++ b/internal/service/s3/bucket_lifecycle_configuration.go @@ -252,6 +252,12 @@ func resourceBucketLifecycleConfiguration() *schema.Resource { }, }, }, + "transition_default_minimum_object_size": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ValidateDiagFunc: enum.Validate[types.TransitionDefaultMinimumObjectSize](), + }, }, } } @@ -273,6 +279,10 @@ func resourceBucketLifecycleConfigurationCreate(ctx context.Context, d *schema.R input.ExpectedBucketOwner = aws.String(expectedBucketOwner) } + if v, ok := d.GetOk("transition_default_minimum_object_size"); ok { + input.TransitionDefaultMinimumObjectSize = types.TransitionDefaultMinimumObjectSize(v.(string)) + } + _, err := tfresource.RetryWhenAWSErrCodeEquals(ctx, bucketPropagationTimeout, func() (interface{}, error) { return conn.PutBucketLifecycleConfiguration(ctx, input) }, errCodeNoSuchBucket) @@ -287,9 +297,7 @@ func resourceBucketLifecycleConfigurationCreate(ctx context.Context, d *schema.R d.SetId(CreateResourceID(bucket, expectedBucketOwner)) - _, err = waitLifecycleRulesEquals(ctx, conn, bucket, expectedBucketOwner, rules, d.Timeout(schema.TimeoutCreate)) - - if err != nil { + if _, err := waitLifecycleRulesEquals(ctx, conn, bucket, expectedBucketOwner, rules, d.Timeout(schema.TimeoutCreate)); err != nil { return sdkdiag.AppendErrorf(diags, "waiting for S3 Bucket Lifecycle Configuration (%s) create: %s", d.Id(), err) } @@ -309,14 +317,14 @@ func resourceBucketLifecycleConfigurationRead(ctx context.Context, d *schema.Res lifecycleConfigurationExtraRetryDelay = 5 * time.Second lifecycleConfigurationRulesSteadyTimeout = 2 * time.Minute ) - var lastOutput, output []types.LifecycleRule + var lastOutput, output *s3.GetBucketLifecycleConfigurationOutput err = retry.RetryContext(ctx, lifecycleConfigurationRulesSteadyTimeout, func() *retry.RetryError { var err error time.Sleep(lifecycleConfigurationExtraRetryDelay) - output, err = findLifecycleRules(ctx, conn, bucket, expectedBucketOwner) + output, err = findBucketLifecycleConfiguration(ctx, conn, bucket, expectedBucketOwner) if d.IsNewResource() && tfresource.NotFound(err) { return retry.RetryableError(err) @@ -326,7 +334,7 @@ func resourceBucketLifecycleConfigurationRead(ctx context.Context, d *schema.Res return retry.NonRetryableError(err) } - if lastOutput == nil || !lifecycleRulesEqual(lastOutput, output) { + if lastOutput == nil || !lifecycleRulesEqual(lastOutput.Rules, output.Rules) { lastOutput = output return retry.RetryableError(fmt.Errorf("S3 Bucket Lifecycle Configuration (%s) has not stablized; retrying", d.Id())) } @@ -335,7 +343,7 @@ func resourceBucketLifecycleConfigurationRead(ctx context.Context, d *schema.Res }) if tfresource.TimedOut(err) { - output, err = findLifecycleRules(ctx, conn, bucket, expectedBucketOwner) + output, err = findBucketLifecycleConfiguration(ctx, conn, bucket, expectedBucketOwner) } if !d.IsNewResource() && tfresource.NotFound(err) { @@ -350,9 +358,10 @@ func resourceBucketLifecycleConfigurationRead(ctx context.Context, d *schema.Res d.Set(names.AttrBucket, bucket) d.Set(names.AttrExpectedBucketOwner, expectedBucketOwner) - if err := d.Set(names.AttrRule, flattenLifecycleRules(ctx, output)); err != nil { + if err := d.Set(names.AttrRule, flattenLifecycleRules(ctx, output.Rules)); err != nil { return sdkdiag.AppendErrorf(diags, "setting rule: %s", err) } + d.Set("transition_default_minimum_object_size", output.TransitionDefaultMinimumObjectSize) return diags } @@ -377,6 +386,10 @@ func resourceBucketLifecycleConfigurationUpdate(ctx context.Context, d *schema.R input.ExpectedBucketOwner = aws.String(expectedBucketOwner) } + if v, ok := d.GetOk("transition_default_minimum_object_size"); ok { + input.TransitionDefaultMinimumObjectSize = types.TransitionDefaultMinimumObjectSize(v.(string)) + } + _, err = tfresource.RetryWhenAWSErrCodeEquals(ctx, bucketPropagationTimeout, func() (interface{}, error) { return conn.PutBucketLifecycleConfiguration(ctx, input) }, errCodeNoSuchLifecycleConfiguration) @@ -385,9 +398,7 @@ func resourceBucketLifecycleConfigurationUpdate(ctx context.Context, d *schema.R return sdkdiag.AppendErrorf(diags, "updating S3 Bucket Lifecycle Configuration (%s): %s", d.Id(), err) } - _, err = waitLifecycleRulesEquals(ctx, conn, bucket, expectedBucketOwner, rules, d.Timeout(schema.TimeoutUpdate)) - - if err != nil { + if _, err := waitLifecycleRulesEquals(ctx, conn, bucket, expectedBucketOwner, rules, d.Timeout(schema.TimeoutUpdate)); err != nil { return sdkdiag.AppendErrorf(diags, "waiting for S3 Bucket Lifecycle Configuration (%s) update: %s", d.Id(), err) } @@ -421,7 +432,7 @@ func resourceBucketLifecycleConfigurationDelete(ctx context.Context, d *schema.R } _, err = tfresource.RetryUntilNotFound(ctx, bucketPropagationTimeout, func() (interface{}, error) { - return findLifecycleRules(ctx, conn, bucket, expectedBucketOwner) + return findBucketLifecycleConfiguration(ctx, conn, bucket, expectedBucketOwner) }) if err != nil { @@ -454,7 +465,7 @@ func suppressMissingFilterConfigurationBlock(k, old, new string, d *schema.Resou return false } -func findLifecycleRules(ctx context.Context, conn *s3.Client, bucket, expectedBucketOwner string) ([]types.LifecycleRule, error) { +func findBucketLifecycleConfiguration(ctx context.Context, conn *s3.Client, bucket, expectedBucketOwner string) (*s3.GetBucketLifecycleConfigurationOutput, error) { input := &s3.GetBucketLifecycleConfigurationInput{ Bucket: aws.String(bucket), } @@ -479,7 +490,7 @@ func findLifecycleRules(ctx context.Context, conn *s3.Client, bucket, expectedBu return nil, tfresource.NewEmptyResultError(input) } - return output.Rules, nil + return output, nil } func lifecycleRulesEqual(rules1, rules2 []types.LifecycleRule) bool { @@ -501,7 +512,7 @@ func lifecycleRulesEqual(rules1, rules2 []types.LifecycleRule) bool { func statusLifecycleRulesEquals(ctx context.Context, conn *s3.Client, bucket, expectedBucketOwner string, rules []types.LifecycleRule) retry.StateRefreshFunc { return func() (interface{}, string, error) { - output, err := findLifecycleRules(ctx, conn, bucket, expectedBucketOwner) + output, err := findBucketLifecycleConfiguration(ctx, conn, bucket, expectedBucketOwner) if tfresource.NotFound(err) { return nil, "", nil @@ -511,7 +522,7 @@ func statusLifecycleRulesEquals(ctx context.Context, conn *s3.Client, bucket, ex return nil, "", err } - return output, strconv.FormatBool(lifecycleRulesEqual(output, rules)), nil + return output, strconv.FormatBool(lifecycleRulesEqual(output.Rules, rules)), nil } } diff --git a/internal/service/s3/bucket_lifecycle_configuration_test.go b/internal/service/s3/bucket_lifecycle_configuration_test.go index d22a2f31523..504db65006e 100644 --- a/internal/service/s3/bucket_lifecycle_configuration_test.go +++ b/internal/service/s3/bucket_lifecycle_configuration_test.go @@ -46,6 +46,7 @@ func TestAccS3BucketLifecycleConfiguration_basic(t *testing.T) { names.AttrID: rName, names.AttrStatus: tfs3.LifecycleRuleStatusEnabled, }), + resource.TestCheckResourceAttr(resourceName, "transition_default_minimum_object_size", "all_storage_classes_128K"), ), }, { @@ -1067,6 +1068,60 @@ func TestAccS3BucketLifecycleConfiguration_directoryBucket(t *testing.T) { }) } +func TestAccS3BucketLifecycleConfiguration_basicTransitionDefaultMinimumObjectSize(t *testing.T) { + ctx := acctest.Context(t) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_s3_bucket_lifecycle_configuration.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.S3ServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckBucketLifecycleConfigurationDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccBucketLifecycleConfigurationConfig_basicTransitionDefaultMinimumObjectSize(rName, "varies_by_storage_class"), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckBucketLifecycleConfigurationExists(ctx, resourceName), + resource.TestCheckResourceAttrPair(resourceName, names.AttrBucket, "aws_s3_bucket.test", names.AttrBucket), + resource.TestCheckResourceAttr(resourceName, acctest.CtRulePound, acctest.Ct1), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rule.*", map[string]string{ + "expiration.#": acctest.Ct1, + "expiration.0.days": "365", + "filter.#": acctest.Ct1, + "filter.0.prefix": "", + names.AttrID: rName, + names.AttrStatus: tfs3.LifecycleRuleStatusEnabled, + }), + resource.TestCheckResourceAttr(resourceName, "transition_default_minimum_object_size", "varies_by_storage_class"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccBucketLifecycleConfigurationConfig_basicTransitionDefaultMinimumObjectSize(rName, "all_storage_classes_128K"), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckBucketLifecycleConfigurationExists(ctx, resourceName), + resource.TestCheckResourceAttrPair(resourceName, names.AttrBucket, "aws_s3_bucket.test", names.AttrBucket), + resource.TestCheckResourceAttr(resourceName, acctest.CtRulePound, acctest.Ct1), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "rule.*", map[string]string{ + "expiration.#": acctest.Ct1, + "expiration.0.days": "365", + "filter.#": acctest.Ct1, + "filter.0.prefix": "", + names.AttrID: rName, + names.AttrStatus: tfs3.LifecycleRuleStatusEnabled, + }), + resource.TestCheckResourceAttr(resourceName, "transition_default_minimum_object_size", "all_storage_classes_128K"), + ), + }, + }, + }) +} + func testAccCheckBucketLifecycleConfigurationDestroy(ctx context.Context) resource.TestCheckFunc { return func(s *terraform.State) error { conn := acctest.Provider.Meta().(*conns.AWSClient).S3Client(ctx) @@ -1081,7 +1136,7 @@ func testAccCheckBucketLifecycleConfigurationDestroy(ctx context.Context) resour return err } - _, err = tfs3.FindLifecycleRules(ctx, conn, bucket, expectedBucketOwner) + _, err = tfs3.FindBucketLifecycleConfiguration(ctx, conn, bucket, expectedBucketOwner) if tfresource.NotFound(err) { continue @@ -1112,7 +1167,7 @@ func testAccCheckBucketLifecycleConfigurationExists(ctx context.Context, n strin return err } - _, err = tfs3.FindLifecycleRules(ctx, conn, bucket, expectedBucketOwner) + _, err = tfs3.FindBucketLifecycleConfiguration(ctx, conn, bucket, expectedBucketOwner) return err } @@ -1797,3 +1852,25 @@ resource "aws_s3_bucket_lifecycle_configuration" "test" { } `, rName)) } + +func testAccBucketLifecycleConfigurationConfig_basicTransitionDefaultMinimumObjectSize(rName, transitionDefaultMinimumObjectSize string) string { + return fmt.Sprintf(` +resource "aws_s3_bucket" "test" { + bucket = %[1]q +} + +resource "aws_s3_bucket_lifecycle_configuration" "test" { + bucket = aws_s3_bucket.test.bucket + rule { + id = %[1]q + status = "Enabled" + + expiration { + days = 365 + } + } + + transition_default_minimum_object_size = %[2]q +} +`, rName, transitionDefaultMinimumObjectSize) +} diff --git a/internal/service/s3/exports_test.go b/internal/service/s3/exports_test.go index 7b62aa5ef34..d35399e43df 100644 --- a/internal/service/s3/exports_test.go +++ b/internal/service/s3/exports_test.go @@ -37,6 +37,7 @@ var ( FindBucket = findBucket FindBucketACL = findBucketACL FindBucketAccelerateConfiguration = findBucketAccelerateConfiguration + FindBucketLifecycleConfiguration = findBucketLifecycleConfiguration FindBucketNotificationConfiguration = findBucketNotificationConfiguration FindBucketPolicy = findBucketPolicy FindBucketRequestPayment = findBucketRequestPayment @@ -45,7 +46,6 @@ var ( FindCORSRules = findCORSRules FindIntelligentTieringConfiguration = findIntelligentTieringConfiguration FindInventoryConfiguration = findInventoryConfiguration - FindLifecycleRules = findLifecycleRules FindLoggingEnabled = findLoggingEnabled FindMetricsConfiguration = findMetricsConfiguration FindObjectByBucketAndKey = findObjectByBucketAndKey diff --git a/website/docs/r/s3_bucket_lifecycle_configuration.html.markdown b/website/docs/r/s3_bucket_lifecycle_configuration.html.markdown index 5474765ce68..16c0ad7de9f 100644 --- a/website/docs/r/s3_bucket_lifecycle_configuration.html.markdown +++ b/website/docs/r/s3_bucket_lifecycle_configuration.html.markdown @@ -205,22 +205,25 @@ resource "aws_s3_bucket_lifecycle_configuration" "example" { ### Specifying a filter based on object size -Object size values are in bytes. Maximum filter size is 5TB. Some storage classes have minimum object size limitations, for more information, see [Comparing the Amazon S3 storage classes](https://docs.aws.amazon.com/AmazonS3/latest/userguide/storage-class-intro.html#sc-compare). +Object size values are in bytes. Maximum filter size is 5TB. Amazon S3 applies a default behavior to your Lifecycle configuration that prevents objects smaller than 128 KB from being transitioned to any storage class. You can allow smaller objects to transition by adding a minimum size (`object_size_greater_than`) or a maximum size (`object_size_less_than`) filter that specifies a smaller size to the configuration. This example allows any object smaller than 128 KB to transition to the S3 Glacier Instant Retrieval storage class: ```terraform resource "aws_s3_bucket_lifecycle_configuration" "example" { bucket = aws_s3_bucket.bucket.id rule { - id = "rule-1" + id = "Allow small object transitions" filter { - object_size_greater_than = 500 + object_size_greater_than = 1 } - # ... other transition/expiration actions ... - status = "Enabled" + + transition { + days = 365 + storage_class = "GLACIER_IR" + } } } ``` @@ -367,6 +370,7 @@ This resource supports the following arguments: * `bucket` - (Required) Name of the source S3 bucket you want Amazon S3 to monitor. * `expected_bucket_owner` - (Optional) Account ID of the expected bucket owner. If the bucket is owned by a different account, the request will fail with an HTTP 403 (Access Denied) error. * `rule` - (Required) List of configuration blocks describing the rules managing the replication. [See below](#rule). +* `transition_default_minimum_object_size` - (Optional) The default minimum object size behavior applied to the lifecycle configuration. Valid values: `all_storage_classes_128K` (default), `varies_by_storage_class`. To customize the minimum object size for any transition you can add a `filter` that specifies a custom `object_size_greater_than` or `object_size_less_than` value. Custom filters always take precedence over the default transition behavior. ### rule @@ -444,7 +448,7 @@ The `transition` configuration block supports the following arguments: The `and` configuration block supports the following arguments: -* `object_size_greater_than` - (Optional) Minimum object size to which the rule applies. Value must be at least `0` if specified. +* `object_size_greater_than` - (Optional) Minimum object size to which the rule applies. Value must be at least `0` if specified. Defaults to 128000 (128 KB) for all `storage_class` values unless `transition_default_minimum_object_size` specifies otherwise. * `object_size_less_than` - (Optional) Maximum object size to which the rule applies. Value must be at least `1` if specified. * `prefix` - (Optional) Prefix identifying one or more objects to which the rule applies. * `tags` - (Optional) Key-value map of resource tags. All of these tags must exist in the object's tag set in order for the rule to apply.