Skip to content

Commit 527c106

Browse files
authored
Merge pull request #47962 from hashicorp/b-s3-source-selection-empty
`aws_s3_bucket`: Keep deprecated attributes from fighting with standalone resources
2 parents 29021fc + 28cc698 commit 527c106

3 files changed

Lines changed: 298 additions & 14 deletions

File tree

.changelog/47962.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
```release-note:bug
2+
resource/aws_s3_bucket: Fix `InvalidRequest: SourceSelectionCriteria cannot be empty` errors on unrelated updates (e.g. `tags`) when replication is managed by the dedicated `aws_s3_bucket_replication_configuration` resource using `replica_modifications`
3+
```
4+
5+
```release-note:bug
6+
resource/aws_s3_bucket: Defer to the corresponding dedicated standalone resource for each deprecated nested attribute (`acceleration_status`, `acl`, `cors_rule`, `grant`, `lifecycle_rule`, `logging`, `object_lock_configuration`, `policy`, `replication_configuration`, `request_payer`, `server_side_encryption_configuration`, `versioning`, `website`) when the attribute is not set in configuration, preventing similar fights between the bucket resource and its standalone counterparts
7+
```

internal/service/s3/bucket.go

Lines changed: 74 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1229,7 +1229,9 @@ func resourceBucketUpdate(ctx context.Context, d *schema.ResourceData, meta any)
12291229
//
12301230
// Bucket Policy.
12311231
//
1232-
if d.HasChange(names.AttrPolicy) {
1232+
// Only act if the practitioner has explicitly configured `policy` in HCL.
1233+
// When unset, defer to the dedicated `aws_s3_bucket_policy` resource.
1234+
if d.HasChange(names.AttrPolicy) && deprecatedAttributeInRawConfig(d, names.AttrPolicy) {
12331235
policy, err := structure.NormalizeJsonString(d.Get(names.AttrPolicy).(string))
12341236
if err != nil {
12351237
return sdkdiag.AppendFromErr(diags, err)
@@ -1264,7 +1266,9 @@ func resourceBucketUpdate(ctx context.Context, d *schema.ResourceData, meta any)
12641266
//
12651267
// Bucket CORS Configuration.
12661268
//
1267-
if d.HasChange("cors_rule") {
1269+
// Only act if the practitioner has explicitly configured `cors_rule` in HCL.
1270+
// When unset, defer to the dedicated `aws_s3_bucket_cors_configuration` resource.
1271+
if d.HasChange("cors_rule") && deprecatedAttributeInRawConfig(d, "cors_rule") {
12681272
if v, ok := d.GetOk("cors_rule"); !ok || len(v.([]any)) == 0 {
12691273
_, err := tfresource.RetryWhenAWSErrCodeEquals(ctx, d.Timeout(schema.TimeoutUpdate), func(ctx context.Context) (any, error) {
12701274
return conn.DeleteBucketCors(ctx, &s3.DeleteBucketCorsInput{
@@ -1296,7 +1300,9 @@ func resourceBucketUpdate(ctx context.Context, d *schema.ResourceData, meta any)
12961300
//
12971301
// Bucket Website Configuration.
12981302
//
1299-
if d.HasChange("website") {
1303+
// Only act if the practitioner has explicitly configured `website` in HCL.
1304+
// When unset, defer to the dedicated `aws_s3_bucket_website_configuration` resource.
1305+
if d.HasChange("website") && deprecatedAttributeInRawConfig(d, "website") {
13001306
if v, ok := d.GetOk("website"); !ok || len(v.([]any)) == 0 || v.([]any)[0] == nil {
13011307
_, err := tfresource.RetryWhenAWSErrCodeEquals(ctx, d.Timeout(schema.TimeoutUpdate), func(ctx context.Context) (any, error) {
13021308
return conn.DeleteBucketWebsite(ctx, &s3.DeleteBucketWebsiteInput{
@@ -1331,7 +1337,9 @@ func resourceBucketUpdate(ctx context.Context, d *schema.ResourceData, meta any)
13311337
//
13321338
// Bucket Versioning.
13331339
//
1334-
if d.HasChange("versioning") {
1340+
// Only act if the practitioner has explicitly configured `versioning` in HCL.
1341+
// When unset, defer to the dedicated `aws_s3_bucket_versioning` resource.
1342+
if d.HasChange("versioning") && deprecatedAttributeInRawConfig(d, "versioning") {
13351343
v := d.Get("versioning").([]any)
13361344
var versioningConfig *types.VersioningConfiguration
13371345

@@ -1360,7 +1368,11 @@ func resourceBucketUpdate(ctx context.Context, d *schema.ResourceData, meta any)
13601368
//
13611369
// Bucket ACL.
13621370
//
1363-
if (d.HasChange("acl") && !d.IsNewResource()) || (d.HasChange("grant") && d.Get("grant").(*schema.Set).Len() == 0) {
1371+
// Only act if the practitioner has explicitly configured `acl` or `grant` in HCL.
1372+
// When unset, defer to the dedicated `aws_s3_bucket_acl` resource.
1373+
aclChanged := d.HasChange("acl") && !d.IsNewResource() && deprecatedAttributeInRawConfig(d, "acl")
1374+
grantEmptied := d.HasChange("grant") && d.Get("grant").(*schema.Set).Len() == 0 && deprecatedAttributeInRawConfig(d, "grant")
1375+
if aclChanged || grantEmptied {
13641376
acl := types.BucketCannedACL(d.Get("acl").(string))
13651377
if acl == "" {
13661378
// Use default value previously available in v3.x of the provider.
@@ -1380,7 +1392,7 @@ func resourceBucketUpdate(ctx context.Context, d *schema.ResourceData, meta any)
13801392
}
13811393
}
13821394

1383-
if d.HasChange("grant") && d.Get("grant").(*schema.Set).Len() > 0 {
1395+
if d.HasChange("grant") && d.Get("grant").(*schema.Set).Len() > 0 && deprecatedAttributeInRawConfig(d, "grant") {
13841396
bucketACL, err := retryWhenNoSuchBucketError(ctx, d.Timeout(schema.TimeoutUpdate), func(ctx context.Context) (*s3.GetBucketAclOutput, error) {
13851397
return findBucketACL(ctx, conn, d.Id(), "")
13861398
})
@@ -1409,7 +1421,9 @@ func resourceBucketUpdate(ctx context.Context, d *schema.ResourceData, meta any)
14091421
//
14101422
// Bucket Logging.
14111423
//
1412-
if d.HasChange("logging") {
1424+
// Only act if the practitioner has explicitly configured `logging` in HCL.
1425+
// When unset, defer to the dedicated `aws_s3_bucket_logging` resource.
1426+
if d.HasChange("logging") && deprecatedAttributeInRawConfig(d, "logging") {
14131427
input := &s3.PutBucketLoggingInput{
14141428
Bucket: aws.String(d.Id()),
14151429
BucketLoggingStatus: &types.BucketLoggingStatus{},
@@ -1441,7 +1455,9 @@ func resourceBucketUpdate(ctx context.Context, d *schema.ResourceData, meta any)
14411455
//
14421456
// Bucket Lifecycle Configuration.
14431457
//
1444-
if d.HasChange("lifecycle_rule") {
1458+
// Only act if the practitioner has explicitly configured `lifecycle_rule` in HCL.
1459+
// When unset, defer to the dedicated `aws_s3_bucket_lifecycle_configuration` resource.
1460+
if d.HasChange("lifecycle_rule") && deprecatedAttributeInRawConfig(d, "lifecycle_rule") {
14451461
if v, ok := d.GetOk("lifecycle_rule"); !ok || len(v.([]any)) == 0 {
14461462
_, err := tfresource.RetryWhenAWSErrCodeEquals(ctx, d.Timeout(schema.TimeoutUpdate), func(ctx context.Context) (any, error) {
14471463
return conn.DeleteBucketLifecycle(ctx, &s3.DeleteBucketLifecycleInput{
@@ -1473,7 +1489,9 @@ func resourceBucketUpdate(ctx context.Context, d *schema.ResourceData, meta any)
14731489
//
14741490
// Bucket Accelerate Configuration.
14751491
//
1476-
if d.HasChange("acceleration_status") {
1492+
// Only act if the practitioner has explicitly configured `acceleration_status` in HCL.
1493+
// When unset, defer to the dedicated `aws_s3_bucket_accelerate_configuration` resource.
1494+
if d.HasChange("acceleration_status") && deprecatedAttributeInRawConfig(d, "acceleration_status") {
14771495
input := &s3.PutBucketAccelerateConfigurationInput{
14781496
AccelerateConfiguration: &types.AccelerateConfiguration{
14791497
Status: types.BucketAccelerateStatus(d.Get("acceleration_status").(string)),
@@ -1493,7 +1511,9 @@ func resourceBucketUpdate(ctx context.Context, d *schema.ResourceData, meta any)
14931511
//
14941512
// Bucket Request Payment Configuration.
14951513
//
1496-
if d.HasChange("request_payer") {
1514+
// Only act if the practitioner has explicitly configured `request_payer` in HCL.
1515+
// When unset, defer to the dedicated `aws_s3_bucket_request_payment_configuration` resource.
1516+
if d.HasChange("request_payer") && deprecatedAttributeInRawConfig(d, "request_payer") {
14971517
input := &s3.PutBucketRequestPaymentInput{
14981518
Bucket: aws.String(d.Id()),
14991519
RequestPaymentConfiguration: &types.RequestPaymentConfiguration{
@@ -1513,7 +1533,13 @@ func resourceBucketUpdate(ctx context.Context, d *schema.ResourceData, meta any)
15131533
//
15141534
// Bucket Replication Configuration.
15151535
//
1516-
if d.HasChange("replication_configuration") {
1536+
// Only act if the practitioner has explicitly configured `replication_configuration` in HCL.
1537+
// When unset, defer to the dedicated `aws_s3_bucket_replication_configuration` resource. This
1538+
// avoids round-tripping fields not represented in this deprecated schema (e.g.
1539+
// `replica_modifications`) and writing back a corrupted configuration on unrelated bucket
1540+
// updates such as tags.
1541+
// Reference: https://github.com/hashicorp/terraform-provider-aws/issues/44192
1542+
if d.HasChange("replication_configuration") && deprecatedAttributeInRawConfig(d, "replication_configuration") {
15171543
if v, ok := d.GetOk("replication_configuration"); !ok || len(v.([]any)) == 0 || v.([]any)[0] == nil {
15181544
_, err := tfresource.RetryWhenAWSErrCodeEquals(ctx, d.Timeout(schema.TimeoutUpdate), func(ctx context.Context) (any, error) {
15191545
return conn.DeleteBucketReplication(ctx, &s3.DeleteBucketReplicationInput{
@@ -1567,7 +1593,9 @@ func resourceBucketUpdate(ctx context.Context, d *schema.ResourceData, meta any)
15671593
//
15681594
// Bucket Server-side Encryption Configuration.
15691595
//
1570-
if d.HasChange("server_side_encryption_configuration") {
1596+
// Only act if the practitioner has explicitly configured `server_side_encryption_configuration` in HCL.
1597+
// When unset, defer to the dedicated `aws_s3_bucket_server_side_encryption_configuration` resource.
1598+
if d.HasChange("server_side_encryption_configuration") && deprecatedAttributeInRawConfig(d, "server_side_encryption_configuration") {
15711599
if v, ok := d.GetOk("server_side_encryption_configuration"); !ok || len(v.([]any)) == 0 {
15721600
_, err := tfresource.RetryWhenAWSErrCodeEquals(ctx, d.Timeout(schema.TimeoutUpdate), func(ctx context.Context) (any, error) {
15731601
return conn.DeleteBucketEncryption(ctx, &s3.DeleteBucketEncryptionInput{
@@ -1599,7 +1627,9 @@ func resourceBucketUpdate(ctx context.Context, d *schema.ResourceData, meta any)
15991627
//
16001628
// Bucket Object Lock Configuration.
16011629
//
1602-
if d.HasChange("object_lock_configuration") {
1630+
// Only act if the practitioner has explicitly configured `object_lock_configuration` in HCL.
1631+
// When unset, defer to the dedicated `aws_s3_bucket_object_lock_configuration` resource.
1632+
if d.HasChange("object_lock_configuration") && deprecatedAttributeInRawConfig(d, "object_lock_configuration") {
16031633
// S3 Object Lock configuration cannot be deleted, only updated.
16041634
input := &s3.PutObjectLockConfigurationInput{
16051635
Bucket: aws.String(d.Id()),
@@ -1618,6 +1648,37 @@ func resourceBucketUpdate(ctx context.Context, d *schema.ResourceData, meta any)
16181648
return append(diags, resourceBucketRead(ctx, d, meta)...)
16191649
}
16201650

1651+
// deprecatedAttributeInRawConfig reports whether the practitioner has the
1652+
// named top-level attribute (a deprecated bucket nested attribute) in their
1653+
// HCL configuration. When the value in state was populated solely by Read
1654+
// (i.e. the attribute is Computed-only at runtime), this returns false, and
1655+
// callers should skip the corresponding API update. This avoids the
1656+
// deprecated attribute fighting with its dedicated standalone resource over
1657+
// fields not represented in the deprecated schema, which can write back
1658+
// corrupted configuration on unrelated bucket updates such as tags.
1659+
//
1660+
// Reference: https://github.com/hashicorp/terraform-provider-aws/issues/44192
1661+
func deprecatedAttributeInRawConfig(d *schema.ResourceData, name string) bool {
1662+
rawConfig := d.GetRawConfig()
1663+
if !rawConfig.IsKnown() || rawConfig.IsNull() {
1664+
return false
1665+
}
1666+
1667+
v := rawConfig.GetAttr(name)
1668+
if !v.IsKnown() || v.IsNull() {
1669+
return false
1670+
}
1671+
1672+
// For collection types (most commonly nested block lists/sets), an
1673+
// unconfigured attribute reports as a non-null empty collection rather
1674+
// than null, so check the length explicitly.
1675+
if t := v.Type(); t.IsListType() || t.IsSetType() || t.IsMapType() || t.IsTupleType() || t.IsObjectType() {
1676+
return v.LengthInt() > 0
1677+
}
1678+
1679+
return true
1680+
}
1681+
16211682
func resourceBucketDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics {
16221683
var diags diag.Diagnostics
16231684
conn := meta.(*conns.AWSClient).S3Client(ctx)

0 commit comments

Comments
 (0)