Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(s3-deployment): deploy data with deploy-time values #18659

Merged
merged 19 commits into from
Feb 3, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
59 changes: 59 additions & 0 deletions packages/@aws-cdk/aws-s3-deployment/lib/content.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Stack } from '@aws-cdk/core';

// keep this import separate from other imports to reduce chance for merge conflicts with v2-main
// eslint-disable-next-line no-duplicate-imports, import/order
import { Construct } from '@aws-cdk/core';

export interface Content {
readonly text: string;
readonly markers: Record<string, any>;
}

export function renderContent(scope: Construct, content: string): Content {
const obj = Stack.of(scope).resolve(content);
if (typeof obj === 'string') {
return { text: obj, markers: {} };
}

if (!obj['Fn::Join']) {
throw new Error('Unexpected resolved value. Expecting Fn::Join');
}

const fnJoin: FnJoin = obj['Fn::Join'];
if (fnJoin[0] !== '') {
throw new Error('Unexpected join, expecting separator to be ""');
}

const markers: Record<string, FnJoinPart> = {};
const result = new Array<string>();
let markerIndex = 0;

for (const part of fnJoin[1]) {
if (typeof(part) === 'string') {
result.push(part);
continue;
}

if (typeof(part) === 'object') {
const keys: string[] = Object.keys(part);
if (keys.length !== 1) {
throw new Error('Invalid object');
}

if ('Ref' in part || 'Fn::GetAtt' in part) {
const marker = `<<marker:0xbaba:${markerIndex++}>>`;
result.push(marker);
markers[marker] = part;
} else {
throw new Error('Invalid object');
}
}
}

return { text: result.join(''), markers };
}

type FnJoin = [string, FnJoinPart[]];
type FnJoinPart = string | Ref | GetAtt;
type Ref = { Ref: string };
type GetAtt = { 'Fn::GetAtt': [string, string] };
35 changes: 35 additions & 0 deletions packages/@aws-cdk/aws-s3-deployment/lib/source.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import * as fs from 'fs';
import { join, dirname } from 'path';
import * as iam from '@aws-cdk/aws-iam';
import * as s3 from '@aws-cdk/aws-s3';
import * as s3_assets from '@aws-cdk/aws-s3-assets';
import { FileSystem } from '@aws-cdk/core';
import { renderContent } from './content';

// keep this import separate from other imports to reduce chance for merge conflicts with v2-main
// eslint-disable-next-line no-duplicate-imports, import/order
Expand All @@ -19,6 +23,11 @@ export interface SourceConfig {
* An S3 object key in the source bucket that points to a zip file.
*/
readonly zipObjectKey: string;

/**
* A set of markers to substitute in the source content.
*/
readonly markers?: Record<string, any>;
}

/**
Expand Down Expand Up @@ -110,5 +119,31 @@ export class Source {
};
}

/**
* Deploys a file with the specified textual contents into the bucket. The
* content can include deploy-time values that will get resolved only during
* deployment.
*
* @param objectKey The S3 object key to use for this file.
* @param content The contents
*/
public static content(objectKey: string, content: string): ISource {
return {
bind: (scope: Construct, context?: DeploymentSourceContext) => {
const workdir = FileSystem.mkdtemp('s3-deployment');
const outputPath = join(workdir, objectKey);
const rendered = renderContent(scope, content);
fs.mkdirSync(dirname(outputPath), { recursive: true });
fs.writeFileSync(outputPath, rendered.text);
const asset = this.asset(workdir).bind(scope, context);
return {
bucket: asset.bucket,
zipObjectKey: asset.zipObjectKey,
markers: rendered.markers,
};
},
};
}

private constructor() { }
}
17 changes: 17 additions & 0 deletions packages/@aws-cdk/aws-s3-deployment/test/bucket-deployment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1056,3 +1056,20 @@ test('bucket has multiple deployments', () => {
],
});
});

test('Source.content() can be used to create a file with contents', () => {
const app = new cdk.App();
const stack = new cdk.Stack(app, 'Test');
const bucket = new s3.Bucket(stack, 'Bucket');

const source = s3deploy.Source.content('my/path.txt', 'hello, world');

new s3deploy.BucketDeployment(stack, 'DeployWithVpc3', {
sources: [source],
destinationBucket: bucket,
destinationKeyPrefix: '/x/z',
});

const result = app.synth();
expect(result.stacks[0].assets).toStrictEqual([]);
});
72 changes: 72 additions & 0 deletions packages/@aws-cdk/aws-s3-deployment/test/content.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import * as lambda from '@aws-cdk/aws-lambda';
import * as s3 from '@aws-cdk/aws-s3';
import { Stack } from '@aws-cdk/core';
import { Source } from '../lib';
import { renderContent } from '../lib/content';

test('simple string', () => {
const stack = new Stack();
expect(renderContent(stack, 'foo')).toStrictEqual({
markers: {},
text: 'foo',
});
});

test('string with a "Ref" token', () => {
eladb marked this conversation as resolved.
Show resolved Hide resolved
const stack = new Stack();
const bucket = new s3.Bucket(stack, 'Bucket');

expect(renderContent(stack, `foo-${bucket.bucketName}`)).toStrictEqual({
text: 'foo-<<marker:0xbaba:0>>',
markers: { '<<marker:0xbaba:0>>': { Ref: 'Bucket83908E77' } },
});
});

test('string with a "Fn::GetAtt" token', () => {
const stack = new Stack();
const bucket = new s3.Bucket(stack, 'Bucket');

expect(renderContent(stack, `foo-${bucket.bucketArn}`)).toStrictEqual({
text: 'foo-<<marker:0xbaba:0>>',
markers: { '<<marker:0xbaba:0>>': { 'Fn::GetAtt': ['Bucket83908E77', 'Arn'] } },
});
});

test('multiple markers', () => {
const stack = new Stack();
const bucket = new s3.Bucket(stack, 'Bucket');

expect(renderContent(stack, `boom-${bucket.bucketName}-bam-${bucket.bucketArn}`)).toStrictEqual({
text: 'boom-<<marker:0xbaba:0>>-bam-<<marker:0xbaba:1>>',
markers: {
'<<marker:0xbaba:0>>': { Ref: 'Bucket83908E77' },
'<<marker:0xbaba:1>>': { 'Fn::GetAtt': ['Bucket83908E77', 'Arn'] },
},
});
});

test('json-encoded string', () => {
const stack = new Stack();
const bucket = new s3.Bucket(stack, 'Bucket');
const json = {
BucketArn: bucket.bucketArn,
BucketName: bucket.bucketName,
};

expect(renderContent(stack, JSON.stringify(json))).toStrictEqual({
text: JSON.stringify({ BucketArn: '<<marker:0xbaba:0>>', BucketName: '<<marker:0xbaba:1>>' }),
markers: {
'<<marker:0xbaba:0>>': { 'Fn::GetAtt': ['Bucket83908E77', 'Arn'] },
'<<marker:0xbaba:1>>': { Ref: 'Bucket83908E77' },
},
});
});

test('markers are returned in the source config', () => {
const stack = new Stack();
const handler = new lambda.Function(stack, 'Handler', { runtime: lambda.Runtime.NODEJS_14_X, code: lambda.Code.fromInline('foo'), handler: 'index.handler' });
const actual = Source.content('file1.txt', `boom-${stack.account}`).bind(stack, { handlerRole: handler.role! });
expect(actual.markers).toStrictEqual({
'<<marker:0xbaba:0>>': { Ref: 'AWS::AccountId' },
});
});
Loading