Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

r/aws_s3_bucket_lifecycle_configuration: Add transition_default_minimum_object_size argument #39578

7 changes: 7 additions & 0 deletions .changelog/39578.txt
Original file line number Diff line number Diff line change
@@ -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
```
8 changes: 7 additions & 1 deletion internal/service/s3/bucket.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
43 changes: 27 additions & 16 deletions internal/service/s3/bucket_lifecycle_configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -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](),
},
},
}
}
Expand All @@ -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)
Expand All @@ -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)
}

Expand All @@ -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)
Expand All @@ -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()))
}
Expand All @@ -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) {
Expand All @@ -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
}
Expand All @@ -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)
Expand All @@ -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)
}

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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),
}
Expand All @@ -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 {
Expand All @@ -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
Expand All @@ -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
}
}

Expand Down
81 changes: 79 additions & 2 deletions internal/service/s3/bucket_lifecycle_configuration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
),
},
{
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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)
}
2 changes: 1 addition & 1 deletion internal/service/s3/exports_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ var (
FindBucket = findBucket
FindBucketACL = findBucketACL
FindBucketAccelerateConfiguration = findBucketAccelerateConfiguration
FindBucketLifecycleConfiguration = findBucketLifecycleConfiguration
FindBucketNotificationConfiguration = findBucketNotificationConfiguration
FindBucketPolicy = findBucketPolicy
FindBucketRequestPayment = findBucketRequestPayment
Expand All @@ -45,7 +46,6 @@ var (
FindCORSRules = findCORSRules
FindIntelligentTieringConfiguration = findIntelligentTieringConfiguration
FindInventoryConfiguration = findInventoryConfiguration
FindLifecycleRules = findLifecycleRules
FindLoggingEnabled = findLoggingEnabled
FindMetricsConfiguration = findMetricsConfiguration
FindObjectByBucketAndKey = findObjectByBucketAndKey
Expand Down
16 changes: 10 additions & 6 deletions website/docs/r/s3_bucket_lifecycle_configuration.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
}
```
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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.
Expand Down
Loading