Skip to content

Commit

Permalink
feat(s3-deployment): add additional sources with addSource (aws#23321)
Browse files Browse the repository at this point in the history
PR aws#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*
  • Loading branch information
corymhall authored and Brennan Ho committed Feb 22, 2023
1 parent 1aef786 commit fb189bd
Show file tree
Hide file tree
Showing 4 changed files with 88 additions and 19 deletions.
14 changes: 14 additions & 0 deletions packages/@aws-cdk/aws-s3-deployment/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,20 @@ new ConstructThatReadsFromTheBucket(this, 'Consumer', {
});
```

It is also possible to add additional sources using the `addSource` method.

```ts
declare const websiteBucket: s3.IBucket;

const deployment = new s3deploy.BucketDeployment(this, 'DeployWebsite', {
sources: [s3deploy.Source.asset('./website-dist')],
destinationBucket: websiteBucket,
destinationKeyPrefix: 'web/static', // optional prefix in destination bucket
});

deployment.addSource(s3deploy.Source.asset('./another-asset'));
```

## Supported sources

The following source types are supported for bucket deployments:
Expand Down
52 changes: 41 additions & 11 deletions packages/@aws-cdk/aws-s3-deployment/lib/bucket-deployment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,8 @@ export class BucketDeployment extends Construct {
private readonly cr: cdk.CustomResource;
private _deployedBucket?: s3.IBucket;
private requestDestinationArn: boolean = false;
private readonly sources: SourceConfig[];
private readonly handlerRole: iam.IRole;

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

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

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

props.destinationBucket.grantReadWrite(handler);
if (props.accessControl) {
Expand All @@ -342,24 +345,35 @@ export class BucketDeployment extends Construct {
}));
}

// to avoid redundant stack updates, only include "SourceMarkers" if one of
// the sources actually has markers.
const hasMarkers = sources.some(source => source.markers);

// Markers are not replaced if zip sources are not extracted, so throw an error
// if extraction is not wanted and sources have markers.
if (hasMarkers && props.extract == false) {
throw new Error('Some sources are incompatible with extract=false; sources with deploy-time values (such as \'snsTopic.topicArn\') must be extracted.');
}
const _this = this;
this.node.addValidation({
validate(): string[] {
if (_this.sources.some(source => source.markers) && props.extract == false) {
return ['Some sources are incompatible with extract=false; sources with deploy-time values (such as \'snsTopic.topicArn\') must be extracted.'];
}
return [];
},
});

const crUniqueId = `CustomResource${this.renderUniqueId(props.memoryLimit, props.ephemeralStorageSize, props.vpc)}`;
this.cr = new cdk.CustomResource(this, crUniqueId, {
serviceToken: handler.functionArn,
resourceType: 'Custom::CDKBucketDeployment',
properties: {
SourceBucketNames: sources.map(source => source.bucket.bucketName),
SourceObjectKeys: sources.map(source => source.zipObjectKey),
SourceMarkers: hasMarkers ? sources.map(source => source.markers ?? {}) : undefined,
SourceBucketNames: cdk.Lazy.list({ produce: () => this.sources.map(source => source.bucket.bucketName) }),
SourceObjectKeys: cdk.Lazy.list({ produce: () => this.sources.map(source => source.zipObjectKey) }),
SourceMarkers: cdk.Lazy.any({
produce: () => {
return this.sources.reduce((acc, source) => {
if (source.markers) {
acc.push(source.markers);
}
return acc;
}, [] as Array<Record<string, any>>);
},
}, { omitEmptyArray: true }),
DestinationBucketName: props.destinationBucket.bucketName,
DestinationBucketKeyPrefix: props.destinationKeyPrefix,
RetainOnDelete: props.retainOnDelete,
Expand Down Expand Up @@ -465,6 +479,22 @@ export class BucketDeployment extends Construct {
return objectKeys;
}

/**
* Add an additional source to the bucket deployment
*
* @example
* declare const websiteBucket: s3.IBucket;
* const deployment = new s3deploy.BucketDeployment(this, 'Deployment', {
* sources: [s3deploy.Source.asset('./website-dist')],
* destinationBucket: websiteBucket,
* });
*
* deployment.addSource(s3deploy.Source.asset('./another-asset'));
*/
public addSource(source: ISource): void {
this.sources.push(source.bind(this, { handlerRole: this.handlerRole }));
}

private renderUniqueId(memoryLimit?: number, ephemeralStorageSize?: cdk.Size, vpc?: ec2.IVpc) {
let uuid = '';

Expand Down
34 changes: 29 additions & 5 deletions packages/@aws-cdk/aws-s3-deployment/test/bucket-deployment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -997,14 +997,15 @@ test('given a source with markers and extract is false, BucketDeployment throws
},
},
});
new s3deploy.BucketDeployment(stack, 'Deploy', {
sources: [file],
destinationBucket: bucket,
extract: false,
});

// THEN
expect(() => {
new s3deploy.BucketDeployment(stack, 'Deploy', {
sources: [file],
destinationBucket: bucket,
extract: false,
});
Template.fromStack(stack);
}).toThrow('Some sources are incompatible with extract=false; sources with deploy-time values (such as \'snsTopic.topicArn\') must be extracted.');
});

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

test('can add sources with addSource', () => {
const app = new cdk.App();
const stack = new cdk.Stack(app, 'Test');
const bucket = new s3.Bucket(stack, 'Bucket');
const deployment = new s3deploy.BucketDeployment(stack, 'Deploy', {
sources: [s3deploy.Source.data('my/path.txt', 'helloWorld')],
destinationBucket: bucket,
});
deployment.addSource(s3deploy.Source.data('my/other/path.txt', 'hello world'));

const result = app.synth();
const content = readDataFile(result, 'my/path.txt');
const content2 = readDataFile(result, 'my/other/path.txt');
expect(content).toStrictEqual('helloWorld');
expect(content2).toStrictEqual('hello world');
Template.fromStack(stack).hasResourceProperties('Custom::CDKBucketDeployment', {
SourceMarkers: [
{},
{},
],
});
});


function readDataFile(casm: cxapi.CloudAssembly, relativePath: string): string {
const assetDirs = readdirSync(casm.directory).filter(f => f.startsWith('asset.'));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,14 @@ const file1 = Source.data('file1.txt', 'boom');
const file2 = Source.data('path/to/file2.txt', `bam! ${bucket.bucketName}`);
const file3 = Source.jsonData('my/config.json', { website_url: bucket.bucketWebsiteUrl });

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

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

app.synth();
app.synth();

0 comments on commit fb189bd

Please sign in to comment.