Skip to content

Commit 252f052

Browse files
authored
fix(s3): infer bucketWebsiteUrl and bucketDomainName suffixes from bucket region (#23919)
The current implementations of determining various bucket URLs was incorrectly relying on the stack's region. In practice this did not matter a lot, since the suffix depends on the partition and a cross over from one partition to another is not likely or even impossible. We also required users to determine the correct bucketWebsiteUrl format for a bucket. However this information can reliably be inferred from a bucket's region. Since bucket ARNs do not contain region information, it is best to provide the bucket region whenever bucketWebsiteUrl will be used. As a fallback we assume a bucket is placed in the region of its stack. --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license
1 parent edfca7c commit 252f052

File tree

3 files changed

+105
-26
lines changed

3 files changed

+105
-26
lines changed

packages/@aws-cdk/aws-s3/lib/bucket.ts

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
} from '@aws-cdk/core';
2424
import { CfnReference } from '@aws-cdk/core/lib/private/cfn-reference';
2525
import * as cxapi from '@aws-cdk/cx-api';
26+
import * as regionInformation from '@aws-cdk/region-info';
2627
import { Construct } from 'constructs';
2728
import { BucketPolicy } from './bucket-policy';
2829
import { IBucketNotificationDestination } from './destination';
@@ -408,14 +409,14 @@ export interface BucketAttributes {
408409
/**
409410
* The domain name of the bucket.
410411
*
411-
* @default Inferred from bucket name
412+
* @default - Inferred from bucket name
412413
*/
413414
readonly bucketDomainName?: string;
414415

415416
/**
416417
* The website URL of the bucket (if static web hosting is enabled).
417418
*
418-
* @default Inferred from bucket name
419+
* @default - Inferred from bucket name and region
419420
*/
420421
readonly bucketWebsiteUrl?: string;
421422

@@ -430,13 +431,22 @@ export interface BucketAttributes {
430431
readonly bucketDualStackDomainName?: string;
431432

432433
/**
433-
* The format of the website URL of the bucket. This should be true for
434+
* Force the format of the website URL of the bucket. This should be true for
434435
* regions launched since 2014.
435436
*
436-
* @default false
437+
* @default - inferred from available region information, `false` otherwise
438+
*
439+
* @deprecated The correct website url format can be inferred automatically from the bucket `region`.
440+
* Always provide the bucket region if the `bucketWebsiteUrl` will be used.
441+
* Alternatively provide the full `bucketWebsiteUrl` manually.
437442
*/
438443
readonly bucketWebsiteNewUrlFormat?: boolean;
439444

445+
/**
446+
* KMS encryption key associated with this bucket.
447+
*
448+
* @default - no encryption key
449+
*/
440450
readonly encryptionKey?: kms.IKey;
441451

442452
/**
@@ -455,6 +465,8 @@ export interface BucketAttributes {
455465

456466
/**
457467
* The region this existing bucket is in.
468+
* Features that require the region (e.g. `bucketWebsiteUrl`) won't fully work
469+
* if the region cannot be correctly inferred.
458470
*
459471
* @default - it's assumed the bucket is in the same region as the scope it's being imported into
460472
*/
@@ -1626,21 +1638,27 @@ export class Bucket extends BucketBase {
16261638
public static fromBucketAttributes(scope: Construct, id: string, attrs: BucketAttributes): IBucket {
16271639
const stack = Stack.of(scope);
16281640
const region = attrs.region ?? stack.region;
1629-
const urlSuffix = stack.urlSuffix;
1641+
const regionInfo = regionInformation.RegionInfo.get(region);
1642+
const urlSuffix = regionInfo.domainSuffix ?? stack.urlSuffix;
16301643

16311644
const bucketName = parseBucketName(scope, attrs);
16321645
if (!bucketName) {
16331646
throw new Error('Bucket name is required');
16341647
}
16351648
Bucket.validateBucketName(bucketName);
16361649

1637-
const newUrlFormat = attrs.bucketWebsiteNewUrlFormat === undefined
1638-
? false
1639-
: attrs.bucketWebsiteNewUrlFormat;
1650+
const oldEndpoint = `s3-website-${region}.${urlSuffix}`;
1651+
const newEndpoint = `s3-website.${region}.${urlSuffix}`;
1652+
1653+
let staticDomainEndpoint = regionInfo.s3StaticWebsiteEndpoint
1654+
?? Lazy.string({ produce: () => stack.regionalFact(regionInformation.FactName.S3_STATIC_WEBSITE_ENDPOINT, newEndpoint) });
1655+
1656+
// Deprecated use of bucketWebsiteNewUrlFormat
1657+
if (attrs.bucketWebsiteNewUrlFormat !== undefined) {
1658+
staticDomainEndpoint = attrs.bucketWebsiteNewUrlFormat ? newEndpoint : oldEndpoint;
1659+
}
16401660

1641-
const websiteDomain = newUrlFormat
1642-
? `${bucketName}.s3-website.${region}.${urlSuffix}`
1643-
: `${bucketName}.s3-website-${region}.${urlSuffix}`;
1661+
const websiteDomain = `${bucketName}.${staticDomainEndpoint}`;
16441662

16451663
class Import extends BucketBase {
16461664
public readonly bucketName = bucketName!;
@@ -1650,7 +1668,7 @@ export class Bucket extends BucketBase {
16501668
public readonly bucketWebsiteDomainName = attrs.bucketWebsiteUrl ? Fn.select(2, Fn.split('/', attrs.bucketWebsiteUrl)) : websiteDomain;
16511669
public readonly bucketRegionalDomainName = attrs.bucketRegionalDomainName || `${bucketName}.s3.${region}.${urlSuffix}`;
16521670
public readonly bucketDualStackDomainName = attrs.bucketDualStackDomainName || `${bucketName}.s3.dualstack.${region}.${urlSuffix}`;
1653-
public readonly bucketWebsiteNewUrlFormat = newUrlFormat;
1671+
public readonly bucketWebsiteNewUrlFormat = attrs.bucketWebsiteNewUrlFormat ?? false;
16541672
public readonly encryptionKey = attrs.encryptionKey;
16551673
public readonly isWebsite = attrs.isWebsite ?? false;
16561674
public policy?: BucketPolicy = undefined;

packages/@aws-cdk/aws-s3/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,9 @@
8282
"devDependencies": {
8383
"@aws-cdk/assertions": "0.0.0",
8484
"@aws-cdk/cdk-build-tools": "0.0.0",
85+
"@aws-cdk/cfn2ts": "0.0.0",
8586
"@aws-cdk/integ-runner": "0.0.0",
8687
"@aws-cdk/integ-tests": "0.0.0",
87-
"@aws-cdk/cfn2ts": "0.0.0",
8888
"@aws-cdk/pkglint": "0.0.0",
8989
"@types/aws-lambda": "^8.10.110",
9090
"@types/jest": "^27.5.2",
@@ -96,6 +96,7 @@
9696
"@aws-cdk/aws-kms": "0.0.0",
9797
"@aws-cdk/core": "0.0.0",
9898
"@aws-cdk/cx-api": "0.0.0",
99+
"@aws-cdk/region-info": "0.0.0",
99100
"constructs": "^10.0.0"
100101
},
101102
"homepage": "https://github.com/aws/aws-cdk",
@@ -105,6 +106,7 @@
105106
"@aws-cdk/aws-kms": "0.0.0",
106107
"@aws-cdk/core": "0.0.0",
107108
"@aws-cdk/cx-api": "0.0.0",
109+
"@aws-cdk/region-info": "0.0.0",
108110
"constructs": "^10.0.0"
109111
},
110112
"engines": {

packages/@aws-cdk/aws-s3/test/bucket.test.ts

Lines changed: 72 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { EOL } from 'os';
22
import { Annotations, Match, Template } from '@aws-cdk/assertions';
33
import * as iam from '@aws-cdk/aws-iam';
44
import * as kms from '@aws-cdk/aws-kms';
5+
import { testDeprecated } from '@aws-cdk/cdk-build-tools';
56
import * as cdk from '@aws-cdk/core';
67
import * as s3 from '../lib';
78

@@ -805,18 +806,69 @@ describe('bucket', () => {
805806
});
806807
});
807808

808-
test('import can explicitly set bucket region', () => {
809+
test('import can explicitly set bucket region with different suffix than stack', () => {
809810
const stack = new cdk.Stack(undefined, undefined, {
810-
env: { region: 'us-east-1' },
811+
env: { region: 'cn-north-1' },
811812
});
812813

813814
const bucket = s3.Bucket.fromBucketAttributes(stack, 'ImportedBucket', {
814815
bucketName: 'mybucket',
815816
region: 'eu-west-1',
816817
});
817818

818-
expect(bucket.bucketRegionalDomainName).toEqual(`mybucket.s3.eu-west-1.${stack.urlSuffix}`);
819-
expect(bucket.bucketWebsiteDomainName).toEqual(`mybucket.s3-website-eu-west-1.${stack.urlSuffix}`);
819+
expect(bucket.bucketRegionalDomainName).toEqual('mybucket.s3.eu-west-1.amazonaws.com');
820+
expect(bucket.bucketWebsiteDomainName).toEqual('mybucket.s3-website-eu-west-1.amazonaws.com');
821+
});
822+
823+
test('new bucketWebsiteUrl format for specific region', () => {
824+
const stack = new cdk.Stack(undefined, undefined, {
825+
env: { region: 'us-east-2' },
826+
});
827+
828+
const bucket = s3.Bucket.fromBucketAttributes(stack, 'ImportedBucket', {
829+
bucketName: 'mybucket',
830+
});
831+
832+
expect(bucket.bucketWebsiteUrl).toEqual('http://mybucket.s3-website.us-east-2.amazonaws.com');
833+
});
834+
835+
test('new bucketWebsiteUrl format for specific region with cn suffix', () => {
836+
const stack = new cdk.Stack(undefined, undefined, {
837+
env: { region: 'cn-north-1' },
838+
});
839+
840+
const bucket = s3.Bucket.fromBucketAttributes(stack, 'ImportedBucket', {
841+
bucketName: 'mybucket',
842+
});
843+
844+
expect(bucket.bucketWebsiteUrl).toEqual('http://mybucket.s3-website.cn-north-1.amazonaws.com.cn');
845+
});
846+
847+
848+
testDeprecated('new bucketWebsiteUrl format with explicit bucketWebsiteNewUrlFormat', () => {
849+
const stack = new cdk.Stack(undefined, undefined, {
850+
env: { region: 'us-east-1' },
851+
});
852+
853+
const bucket = s3.Bucket.fromBucketAttributes(stack, 'ImportedBucket', {
854+
bucketName: 'mybucket',
855+
bucketWebsiteNewUrlFormat: true,
856+
});
857+
858+
expect(bucket.bucketWebsiteUrl).toEqual('http://mybucket.s3-website.us-east-1.amazonaws.com');
859+
});
860+
861+
testDeprecated('old bucketWebsiteUrl format with explicit bucketWebsiteNewUrlFormat', () => {
862+
const stack = new cdk.Stack(undefined, undefined, {
863+
env: { region: 'us-east-2' },
864+
});
865+
866+
const bucket = s3.Bucket.fromBucketAttributes(stack, 'ImportedBucket', {
867+
bucketName: 'mybucket',
868+
bucketWebsiteNewUrlFormat: false,
869+
});
870+
871+
expect(bucket.bucketWebsiteUrl).toEqual('http://mybucket.s3-website-us-east-2.amazonaws.com');
820872
});
821873

822874
test('import needs to specify a valid bucket name', () => {
@@ -2026,25 +2078,32 @@ describe('bucket', () => {
20262078
'Fn::Join': [
20272079
'',
20282080
[
2029-
'http://my-test-bucket.s3-website-',
2030-
{ Ref: 'AWS::Region' },
2031-
'.',
2032-
{ Ref: 'AWS::URLSuffix' },
2081+
'http://my-test-bucket.',
2082+
{
2083+
'Fn::FindInMap': [
2084+
'S3staticwebsiteMap',
2085+
{ Ref: 'AWS::Region' },
2086+
'endpoint',
2087+
],
2088+
},
20332089
],
20342090
],
20352091
});
20362092
expect(stack.resolve(bucket.bucketWebsiteDomainName)).toEqual({
20372093
'Fn::Join': [
20382094
'',
20392095
[
2040-
'my-test-bucket.s3-website-',
2041-
{ Ref: 'AWS::Region' },
2042-
'.',
2043-
{ Ref: 'AWS::URLSuffix' },
2096+
'my-test-bucket.',
2097+
{
2098+
'Fn::FindInMap': [
2099+
'S3staticwebsiteMap',
2100+
{ Ref: 'AWS::Region' },
2101+
'endpoint',
2102+
],
2103+
},
20442104
],
20452105
],
20462106
});
2047-
20482107
});
20492108
test('exports the WebsiteURL for imported buckets with url', () => {
20502109
const stack = new cdk.Stack();

0 commit comments

Comments
 (0)