Skip to content

Commit b34d0b7

Browse files
authored
feat(s3-deployment): add additional sources with addSource (#23321)
PR #22857 is introducing a use case where we need to be able to add additional sources after the `BucketDeployment` resource is created. This PR adds an `addSource` method and changes all the sources evaluation within the construct to be lazy. ---- ### All Submissions: * [ ] Have you followed the guidelines in our [Contributing guide?](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) ### Adding new Construct Runtime Dependencies: * [ ] This PR adds new construct runtime dependencies following the process described [here](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md/#adding-construct-runtime-dependencies) ### New Features * [ ] Have you added the new feature to an [integration test](https://github.com/aws/aws-cdk/blob/main/INTEGRATION_TESTS.md)? * [ ] Did you use `yarn integ` to deploy the infrastructure and generate the snapshot (i.e. `yarn integ` without `--dry-run`)? *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 6dc15d4 commit b34d0b7

File tree

4 files changed

+88
-19
lines changed

4 files changed

+88
-19
lines changed

packages/@aws-cdk/aws-s3-deployment/README.md

+14
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,20 @@ new ConstructThatReadsFromTheBucket(this, 'Consumer', {
6161
});
6262
```
6363

64+
It is also possible to add additional sources using the `addSource` method.
65+
66+
```ts
67+
declare const websiteBucket: s3.IBucket;
68+
69+
const deployment = new s3deploy.BucketDeployment(this, 'DeployWebsite', {
70+
sources: [s3deploy.Source.asset('./website-dist')],
71+
destinationBucket: websiteBucket,
72+
destinationKeyPrefix: 'web/static', // optional prefix in destination bucket
73+
});
74+
75+
deployment.addSource(s3deploy.Source.asset('./another-asset'));
76+
```
77+
6478
## Supported sources
6579

6680
The following source types are supported for bucket deployments:

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

+41-11
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,8 @@ export class BucketDeployment extends Construct {
253253
private readonly cr: cdk.CustomResource;
254254
private _deployedBucket?: s3.IBucket;
255255
private requestDestinationArn: boolean = false;
256+
private readonly sources: SourceConfig[];
257+
private readonly handlerRole: iam.IRole;
256258

257259
constructor(scope: Construct, id: string, props: BucketDeploymentProps) {
258260
super(scope, id);
@@ -327,8 +329,9 @@ export class BucketDeployment extends Construct {
327329

328330
const handlerRole = handler.role;
329331
if (!handlerRole) { throw new Error('lambda.SingletonFunction should have created a Role'); }
332+
this.handlerRole = handlerRole;
330333

331-
const sources: SourceConfig[] = props.sources.map((source: ISource) => source.bind(this, { handlerRole }));
334+
this.sources = props.sources.map((source: ISource) => source.bind(this, { handlerRole: this.handlerRole }));
332335

333336
props.destinationBucket.grantReadWrite(handler);
334337
if (props.accessControl) {
@@ -342,24 +345,35 @@ export class BucketDeployment extends Construct {
342345
}));
343346
}
344347

345-
// to avoid redundant stack updates, only include "SourceMarkers" if one of
346-
// the sources actually has markers.
347-
const hasMarkers = sources.some(source => source.markers);
348-
349348
// Markers are not replaced if zip sources are not extracted, so throw an error
350349
// if extraction is not wanted and sources have markers.
351-
if (hasMarkers && props.extract == false) {
352-
throw new Error('Some sources are incompatible with extract=false; sources with deploy-time values (such as \'snsTopic.topicArn\') must be extracted.');
353-
}
350+
const _this = this;
351+
this.node.addValidation({
352+
validate(): string[] {
353+
if (_this.sources.some(source => source.markers) && props.extract == false) {
354+
return ['Some sources are incompatible with extract=false; sources with deploy-time values (such as \'snsTopic.topicArn\') must be extracted.'];
355+
}
356+
return [];
357+
},
358+
});
354359

355360
const crUniqueId = `CustomResource${this.renderUniqueId(props.memoryLimit, props.ephemeralStorageSize, props.vpc)}`;
356361
this.cr = new cdk.CustomResource(this, crUniqueId, {
357362
serviceToken: handler.functionArn,
358363
resourceType: 'Custom::CDKBucketDeployment',
359364
properties: {
360-
SourceBucketNames: sources.map(source => source.bucket.bucketName),
361-
SourceObjectKeys: sources.map(source => source.zipObjectKey),
362-
SourceMarkers: hasMarkers ? sources.map(source => source.markers ?? {}) : undefined,
365+
SourceBucketNames: cdk.Lazy.list({ produce: () => this.sources.map(source => source.bucket.bucketName) }),
366+
SourceObjectKeys: cdk.Lazy.list({ produce: () => this.sources.map(source => source.zipObjectKey) }),
367+
SourceMarkers: cdk.Lazy.any({
368+
produce: () => {
369+
return this.sources.reduce((acc, source) => {
370+
if (source.markers) {
371+
acc.push(source.markers);
372+
}
373+
return acc;
374+
}, [] as Array<Record<string, any>>);
375+
},
376+
}, { omitEmptyArray: true }),
363377
DestinationBucketName: props.destinationBucket.bucketName,
364378
DestinationBucketKeyPrefix: props.destinationKeyPrefix,
365379
RetainOnDelete: props.retainOnDelete,
@@ -465,6 +479,22 @@ export class BucketDeployment extends Construct {
465479
return objectKeys;
466480
}
467481

482+
/**
483+
* Add an additional source to the bucket deployment
484+
*
485+
* @example
486+
* declare const websiteBucket: s3.IBucket;
487+
* const deployment = new s3deploy.BucketDeployment(this, 'Deployment', {
488+
* sources: [s3deploy.Source.asset('./website-dist')],
489+
* destinationBucket: websiteBucket,
490+
* });
491+
*
492+
* deployment.addSource(s3deploy.Source.asset('./another-asset'));
493+
*/
494+
public addSource(source: ISource): void {
495+
this.sources.push(source.bind(this, { handlerRole: this.handlerRole }));
496+
}
497+
468498
private renderUniqueId(memoryLimit?: number, ephemeralStorageSize?: cdk.Size, vpc?: ec2.IVpc) {
469499
let uuid = '';
470500

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

+29-5
Original file line numberDiff line numberDiff line change
@@ -997,14 +997,15 @@ test('given a source with markers and extract is false, BucketDeployment throws
997997
},
998998
},
999999
});
1000+
new s3deploy.BucketDeployment(stack, 'Deploy', {
1001+
sources: [file],
1002+
destinationBucket: bucket,
1003+
extract: false,
1004+
});
10001005

10011006
// THEN
10021007
expect(() => {
1003-
new s3deploy.BucketDeployment(stack, 'Deploy', {
1004-
sources: [file],
1005-
destinationBucket: bucket,
1006-
extract: false,
1007-
});
1008+
Template.fromStack(stack);
10081009
}).toThrow('Some sources are incompatible with extract=false; sources with deploy-time values (such as \'snsTopic.topicArn\') must be extracted.');
10091010
});
10101011

@@ -1360,6 +1361,29 @@ test('Source.jsonData() can be used to create a file with a JSON object', () =>
13601361
});
13611362
});
13621363

1364+
test('can add sources with addSource', () => {
1365+
const app = new cdk.App();
1366+
const stack = new cdk.Stack(app, 'Test');
1367+
const bucket = new s3.Bucket(stack, 'Bucket');
1368+
const deployment = new s3deploy.BucketDeployment(stack, 'Deploy', {
1369+
sources: [s3deploy.Source.data('my/path.txt', 'helloWorld')],
1370+
destinationBucket: bucket,
1371+
});
1372+
deployment.addSource(s3deploy.Source.data('my/other/path.txt', 'hello world'));
1373+
1374+
const result = app.synth();
1375+
const content = readDataFile(result, 'my/path.txt');
1376+
const content2 = readDataFile(result, 'my/other/path.txt');
1377+
expect(content).toStrictEqual('helloWorld');
1378+
expect(content2).toStrictEqual('hello world');
1379+
Template.fromStack(stack).hasResourceProperties('Custom::CDKBucketDeployment', {
1380+
SourceMarkers: [
1381+
{},
1382+
{},
1383+
],
1384+
});
1385+
});
1386+
13631387

13641388
function readDataFile(casm: cxapi.CloudAssembly, relativePath: string): string {
13651389
const assetDirs = readdirSync(casm.directory).filter(f => f.startsWith('asset.'));

packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment-data.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,14 @@ const file1 = Source.data('file1.txt', 'boom');
1010
const file2 = Source.data('path/to/file2.txt', `bam! ${bucket.bucketName}`);
1111
const file3 = Source.jsonData('my/config.json', { website_url: bucket.bucketWebsiteUrl });
1212

13-
new BucketDeployment(stack, 'DeployMeHere', {
13+
const deployment = new BucketDeployment(stack, 'DeployMeHere', {
1414
destinationBucket: bucket,
15-
sources: [file1, file2, file3],
15+
sources: [file1, file2],
1616
destinationKeyPrefix: 'deploy/here/',
1717
retainOnDelete: false, // default is true, which will block the integration test cleanup
1818
});
19+
deployment.addSource(file3);
1920

2021
new CfnOutput(stack, 'BucketName', { value: bucket.bucketName });
2122

22-
app.synth();
23+
app.synth();

0 commit comments

Comments
 (0)