Skip to content
Merged
Show file tree
Hide file tree
Changes from 37 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
200e5e3
10/2/2025 AWS SDK for Go v2 with S3 Starman.
ewbankkit Feb 16, 2026
928e6dc
r/aws_s3_bucket: Add 'bucket_namespace' argument.
ewbankkit Feb 16, 2026
88713ab
r/aws_s3_bucket: Document 'bucket_namespace' argument.
ewbankkit Feb 16, 2026
72e4a10
Add 'TestAccS3Bucket_Namespace_global'.
ewbankkit Feb 16, 2026
7fa4c6b
Add 'TestAccS3Bucket_Namespace_accountRegional'.
ewbankkit Feb 16, 2026
02f956e
Add 'inttypes.CanonicalAccountIDPattern'.
ewbankkit Feb 16, 2026
1e36adf
Copy 'inttypes.CanonicalRegionPattern' from 'main'.
ewbankkit Feb 16, 2026
7720cf9
Add and test 'accountRegionalBucketNameRegex'.
ewbankkit Feb 16, 2026
ff68414
r/aws_s3_bucket: Set 'bucket_namespace' in Read.
ewbankkit Feb 16, 2026
2a92bcb
Acceptance test output:
ewbankkit Feb 16, 2026
8f491b1
r/aws_s3_bucket: Validate bucket/bucket_namespace combo.
ewbankkit Feb 16, 2026
dcc63cd
Update CHANGELOG entry.
ewbankkit Feb 16, 2026
f27b1d4
Add documentation example.
ewbankkit Feb 16, 2026
85a782a
r/aws_s3_bucket: Handle prefix and generated name for 'account-region…
ewbankkit Feb 16, 2026
0177aac
Acceptance test output:
ewbankkit Feb 16, 2026
7d88f45
Simplify Bucket name validation.
ewbankkit Feb 17, 2026
39124e7
Bump the aws-sdk-go-v2 group across 1 directory with 11 updates
dependabot[bot] Mar 12, 2026
023adf2
Merge pull request #2886 from hashicorp/dependabot/go_modules/aws-sdk…
ewbankkit Mar 12, 2026
686ffa9
Revert "10/2/2025 AWS SDK for Go v2 with S3 Starman."
ewbankkit Mar 12, 2026
7b5ff32
Merge remote-tracking branch 'private/main' into HEAD
ewbankkit Mar 12, 2026
a229b57
Bump the aws-sdk-go-v2 group across 1 directory with 11 updates
dependabot[bot] Mar 12, 2026
0c63783
Bump the aws-sdk-go-v2 group across 1 directory with 14 updates
dependabot[bot] Mar 12, 2026
e5509a9
Merge remote-tracking branch 'private/main' into HEAD
ewbankkit Mar 12, 2026
ccec8a1
Merge commit '0e904764dec53a76e49925776d6c0682b8d2479c' into HEAD
ewbankkit Mar 13, 2026
d632418
Correct CHANGELOG entry file name.
ewbankkit Mar 13, 2026
3c7e830
Tweak CHANGELOG entry.
ewbankkit Mar 13, 2026
34c9fe1
Correct copyright headers.
ewbankkit Mar 13, 2026
c6379d2
Fix golangci-lint 'importas'.
ewbankkit Mar 13, 2026
62494ef
Fix providerlint 'AWSAT003'.
ewbankkit Mar 13, 2026
1a5601c
GA naming for account regional namespaces.
ewbankkit Mar 13, 2026
fb63b24
Fix semgrep 'ci.semgrep.acctest.vcr.use-acctest-paralleltest'.
ewbankkit Mar 13, 2026
f2935d3
r/aws_s3_bucket: Set bucket namespace in 'resourceBucketFlatten'.
ewbankkit Mar 13, 2026
8685923
Merge branch 'main' into f-aws_s3_bucket.bucket_namespace
ewbankkit Mar 13, 2026
bfb979c
Fix 'TestAccS3BucketInventory_directoryBucket'.
ewbankkit Mar 13, 2026
70b083c
Fix 'TestAccS3BucketReplicationConfiguration_directoryBucket'.
ewbankkit Mar 13, 2026
50874b1
Fix 'TestAccS3Bucket_List_includeResource'.
ewbankkit Mar 13, 2026
ed9c128
Fix golangci-lint 'unparam'.
ewbankkit Mar 13, 2026
67bff3f
Update internal/service/s3/testdata/Bucket/namespace_account-regional…
ewbankkit Mar 16, 2026
fec5a23
Update website/docs/r/s3_bucket.html.markdown
ewbankkit Mar 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changelog/46917.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:enhancement
resource/aws_s3_bucket: Add `bucket_namespace` argument in support of [account regional namespaces for general purpose buckets](https://docs.aws.amazon.com/AmazonS3/latest/userguide/gpbucketnamespaces.html#account-regional-gp-buckets)
```
96 changes: 73 additions & 23 deletions internal/service/s3/bucket.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import (
"github.com/hashicorp/terraform-provider-aws/internal/retry"
tftags "github.com/hashicorp/terraform-provider-aws/internal/tags"
"github.com/hashicorp/terraform-provider-aws/internal/tfresource"
inttypes "github.com/hashicorp/terraform-provider-aws/internal/types"
"github.com/hashicorp/terraform-provider-aws/internal/verify"
"github.com/hashicorp/terraform-provider-aws/names"
)
Expand All @@ -50,6 +51,13 @@ const (
bucketPropagationTimeout = 2 * time.Minute
)

var (
// See https://docs.aws.amazon.com/AmazonS3/latest/userguide/gpbucketnamespaces.html#account-regional-naming.
// See https://docs.aws.amazon.com/AmazonS3/latest/userguide/gpbucketnamespaces.html#region-code-format.
// e.g. example-123456789012-us-west-2-an.
accountRegionalBucketNameRegex = regexache.MustCompile(`^[a-z0-9-.]+-` + inttypes.CanonicalAccountIDPatternNoAnchors + `-(?:` + inttypes.CanonicalRegionPatternNoAnchors + `)-an$`)
)

func isGeneralPurposeBucket(bucket string) bool {
return bucketNameTypeFor(bucket) == bucketNameTypeGeneralPurposeBucket
}
Expand Down Expand Up @@ -121,6 +129,13 @@ func resourceBucket() *schema.Resource {
Type: schema.TypeString,
Computed: true,
},
"bucket_namespace": {
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
ValidateDiagFunc: enum.Validate[types.BucketNamespace](),
},
names.AttrBucketPrefix: {
Type: schema.TypeString,
Optional: true,
Expand Down Expand Up @@ -727,9 +742,15 @@ func resourceBucketCreate(ctx context.Context, d *schema.ResourceData, meta any)
c := meta.(*conns.AWSClient)
conn := c.S3Client(ctx)

bucket := create.Name(ctx, d.Get(names.AttrBucket).(string), d.Get(names.AttrBucketPrefix).(string))
ns := types.BucketNamespace(d.Get("bucket_namespace").(string))
optFns := []create.NameGeneratorOptionsFunc{create.WithConfiguredName(d.Get(names.AttrBucket).(string)), create.WithConfiguredPrefix(d.Get(names.AttrBucketPrefix).(string))}
switch ns {
case types.BucketNamespaceAccountRegional:
optFns = append(optFns, create.WithSuffix(fmt.Sprintf("-%s-%s-an", c.AccountID(ctx), c.Region(ctx))))
}
bucket := create.NewNameGenerator(optFns...).Generate(ctx)
region := c.Region(ctx)
if err := validBucketName(bucket, region); err != nil {
if err := validBucketName(bucket, region, ns); err != nil {
return sdkdiag.AppendErrorf(diags, "validating S3 Bucket (%s) name: %s", bucket, err)
}

Expand All @@ -755,6 +776,10 @@ func resourceBucketCreate(ctx context.Context, d *schema.ResourceData, meta any)
input.ACL = types.BucketCannedACLPrivate
}

if v, ok := d.GetOk("bucket_namespace"); ok {
input.BucketNamespace = types.BucketNamespace(v.(string))
}

// See https://docs.aws.amazon.com/AmazonS3/latest/API/API_CreateBucket.html#AmazonS3-CreateBucket-request-LocationConstraint.
if region != endpoints.UsEast1RegionID {
input.CreateBucketConfiguration = &types.CreateBucketConfiguration{
Expand Down Expand Up @@ -837,10 +862,7 @@ func resourceBucketRead(ctx context.Context, d *schema.ResourceData, meta any) d
return sdkdiag.AppendErrorf(diags, "reading S3 Bucket (%s): %s", d.Id(), err)
}

d.Set(names.AttrBucket, d.Id())
d.Set(names.AttrBucketPrefix, create.NamePrefixFromName(d.Id()))

resourceBucketFlatten(ctx, meta.(*conns.AWSClient), d.Id(), d)
resourceBucketFlatten(ctx, c, d.Id(), d)

//
// Bucket Policy.
Expand Down Expand Up @@ -1635,8 +1657,17 @@ func resourceBucketDelete(ctx context.Context, d *schema.ResourceData, meta any)

func resourceBucketFlatten(ctx context.Context, awsClient *conns.AWSClient, bucketName string, d *schema.ResourceData) {
d.Set(names.AttrARN, bucketARN(ctx, awsClient, bucketName))
d.Set(names.AttrBucket, bucketName)
d.Set("bucket_domain_name", awsClient.PartitionHostname(ctx, bucketName+".s3"))

bucketNamespace := bucketNamespace(bucketName)
d.Set("bucket_namespace", bucketNamespace)
if bucketNamespace == types.BucketNamespaceAccountRegional {
d.Set(names.AttrBucketPrefix, create.NamePrefixFromNameWithSuffix(bucketName, fmt.Sprintf("-%s-%s-an", awsClient.AccountID(ctx), awsClient.Region(ctx))))
} else {
d.Set(names.AttrBucketPrefix, create.NamePrefixFromName(bucketName))
}

region := awsClient.Region(ctx)
d.Set("bucket_regional_domain_name", bucketRegionalDomainName(bucketName, region))
if hostedZoneID, err := hostedZoneIDForRegion(region); err == nil {
Expand Down Expand Up @@ -1747,6 +1778,13 @@ func bucketWebsiteEndpointAndDomain(bucket, region string) (string, string) {
return fmt.Sprintf("%s.%s", bucket, domain), domain
}

func bucketNamespace(bucket string) types.BucketNamespace {
if accountRegionalBucketNameRegex.MatchString(bucket) {
return types.BucketNamespaceAccountRegional
}
return types.BucketNamespaceGlobal
}

//
// Bucket CORS Configuration.
//
Expand Down Expand Up @@ -3005,34 +3043,46 @@ func flattenObjectLockConfiguration(apiObject *types.ObjectLockConfiguration) []
// validBucketName validates any S3 bucket name that is not inside the us-east-1 region.
// Buckets outside of this region have to be DNS-compliant. After the same restrictions are
// applied to buckets in the us-east-1 region, this function can be refactored as a SchemaValidateFunc
func validBucketName(value string, region string) error {
func validBucketName(bucket, region string, ns types.BucketNamespace) error {
if region != endpoints.UsEast1RegionID {
if (len(value) < 3) || (len(value) > 63) {
return fmt.Errorf("%q must contain from 3 to 63 characters", value)
if (len(bucket) < 3) || (len(bucket) > 63) {
return fmt.Errorf("%q must contain from 3 to 63 characters", bucket)
}
if !regexache.MustCompile(`^[0-9a-z-.]+$`).MatchString(value) {
return fmt.Errorf("only lowercase alphanumeric characters and hyphens allowed in %q", value)
if !regexache.MustCompile(`^[0-9a-z-.]+$`).MatchString(bucket) {
return fmt.Errorf("only lowercase alphanumeric characters and hyphens allowed in %q", bucket)
}
if regexache.MustCompile(`^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$`).MatchString(value) {
return fmt.Errorf("%q must not be formatted as an IP address", value)
if regexache.MustCompile(`^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$`).MatchString(bucket) {
return fmt.Errorf("%q must not be formatted as an IP address", bucket)
}
if strings.HasPrefix(value, `.`) {
return fmt.Errorf("%q cannot start with a period", value)
if strings.HasPrefix(bucket, `.`) {
return fmt.Errorf("%q cannot start with a period", bucket)
}
if strings.HasSuffix(value, `.`) {
return fmt.Errorf("%q cannot end with a period", value)
if strings.HasSuffix(bucket, `.`) {
return fmt.Errorf("%q cannot end with a period", bucket)
}
if strings.Contains(value, `..`) {
return fmt.Errorf("%q can be only one period between labels", value)
if strings.Contains(bucket, `..`) {
return fmt.Errorf("%q can be only one period between labels", bucket)
}
} else {
if len(value) > 255 {
return fmt.Errorf("%q must contain less than 256 characters", value)
if len(bucket) > 255 {
return fmt.Errorf("%q must contain less than 256 characters", bucket)
}
if !regexache.MustCompile(`^[0-9A-Za-z_.-]+$`).MatchString(value) {
return fmt.Errorf("only alphanumeric characters, hyphens, periods, and underscores allowed in %q", value)
if !regexache.MustCompile(`^[0-9A-Za-z_.-]+$`).MatchString(bucket) {
return fmt.Errorf("only alphanumeric characters, hyphens, periods, and underscores allowed in %q", bucket)
}
}

switch ns {
case types.BucketNamespaceAccountRegional:
if !accountRegionalBucketNameRegex.MatchString(bucket) {
return fmt.Errorf("%s is not an account-regional namespace bucket", bucket)
}
default:
if accountRegionalBucketNameRegex.MatchString(bucket) {
return fmt.Errorf("%s is an account-regional namespace bucket", bucket)
}
}

return nil
}

Expand Down
2 changes: 1 addition & 1 deletion internal/service/s3/bucket_data_source_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ func TestAccS3BucketDataSource_basic(t *testing.T) {
Config: testAccBucketDataSourceConfig_basic(rName),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttrPair(dataSourceName, names.AttrARN, resourceName, names.AttrARN),
testAccCheckBucketDomainName(ctx, dataSourceName, "bucket_domain_name", rName),
testAccCheckBucketDomainName(ctx, t, dataSourceName, "bucket_domain_name", rName),
resource.TestCheckResourceAttr(dataSourceName, "bucket_region", region),
resource.TestCheckResourceAttr(dataSourceName, "bucket_regional_domain_name", testAccBucketRegionalDomainName(rName, region)),
resource.TestCheckResourceAttr(dataSourceName, names.AttrHostedZoneID, hostedZoneID),
Expand Down
3 changes: 2 additions & 1 deletion internal/service/s3/bucket_inventory.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"context"
"fmt"
"log"
"net/http"
"strings"

"github.com/aws/aws-sdk-go-v2/aws"
Expand Down Expand Up @@ -225,7 +226,7 @@ func resourceBucketInventoryPut(ctx context.Context, d *schema.ResourceData, met
return conn.PutBucketInventoryConfiguration(ctx, input)
}, errCodeNoSuchBucket)

if tfawserr.ErrMessageContains(err, errCodeInvalidArgument, "InventoryConfiguration is not valid, expected CreateBucketConfiguration") {
if tfawserr.ErrMessageContains(err, errCodeInvalidArgument, "InventoryConfiguration is not valid, expected CreateBucketConfiguration") || tfawserr.ErrHTTPStatusCodeEquals(err, http.StatusMethodNotAllowed) {
err = errDirectoryBucket(err)
}

Expand Down
2 changes: 1 addition & 1 deletion internal/service/s3/bucket_list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ func TestAccS3Bucket_List_includeResource(t *testing.T) {
tfquerycheck.KnownValueCheck(tfjsonpath.New(names.AttrARN), tfknownvalue.GlobalARNNoAccountIDExact("s3", rName+"-0")),
tfquerycheck.KnownValueCheck(tfjsonpath.New(names.AttrBucket), knownvalue.StringExact(rName+"-0")),
tfquerycheck.KnownValueCheck(tfjsonpath.New("bucket_domain_name"), knownvalue.NotNull()),
tfquerycheck.KnownValueCheck(tfjsonpath.New(names.AttrBucketPrefix), knownvalue.Null()),
tfquerycheck.KnownValueCheck(tfjsonpath.New(names.AttrBucketPrefix), knownvalue.NotNull()),
tfquerycheck.KnownValueCheck(tfjsonpath.New("bucket_region"), knownvalue.Null()), // `bucket_region` is redundant
tfquerycheck.KnownValueCheck(tfjsonpath.New("bucket_regional_domain_name"), knownvalue.StringExact(testAccBucketRegionalDomainName(rName+"-0", acctest.Region()))),
tfquerycheck.KnownValueCheck(tfjsonpath.New(names.AttrForceDestroy), knownvalue.Null()),
Expand Down
4 changes: 0 additions & 4 deletions internal/service/s3/bucket_replication_configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -348,10 +348,6 @@ func resourceBucketReplicationConfigurationCreate(ctx context.Context, d *schema
return nil
})

if err != nil {
return sdkdiag.AppendErrorf(diags, "creating S3 Bucket (%s) Replication Configuration: %s", bucket, err)
}

if tfawserr.ErrMessageContains(err, errCodeInvalidArgument, "ReplicationConfiguration is not valid, expected CreateBucketConfiguration") {
err = errDirectoryBucket(err)
}
Expand Down
Loading
Loading