Skip to content

Commit

Permalink
feat(cloudformation): add option to restrict data returned AwsCustomR…
Browse files Browse the repository at this point in the history
…esource

Specifying `outputPath` allows to restrict the data returned by the custom resource to a specific
path in the API response. This can be used to limit the data returned by the custom resource if
working with API calls that could potentially result in custom respone objects exceeding the hard
limit of 4096 bytes.

Closes aws#2825
  • Loading branch information
jogold committed Jun 13, 2019
1 parent 902636a commit 0aa35e0
Show file tree
Hide file tree
Showing 3 changed files with 82 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,26 @@ function fixBooleans(object: object) {
: v);
}

/**
* Filters the keys of an object.
*/
function filterKeys(object: object, pred: (key: string) => boolean) {
return Object.entries(object)
.reduce(
(acc, [k, v]) => pred(k)
? { ...acc, [k]: v }
: acc,
{}
);
}

export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent, context: AWSLambda.Context) {
try {
console.log(JSON.stringify(event));
console.log('AWS SDK VERSION: ' + (AWS as any).VERSION);

let physicalResourceId = (event as any).PhysicalResourceId;
let flatData: { [key: string]: string } = {};
let data: { [key: string]: string } = {};
const call: AwsSdkCall | undefined = event.ResourceProperties[event.RequestType];

Expand All @@ -47,18 +61,19 @@ export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent

try {
const response = await awsService[call.action](call.parameters && fixBooleans(call.parameters)).promise();
data = flatten(response);
flatData = flatten(response);
data = call.outputPath
? filterKeys(flatData, k => k.startsWith(call.outputPath!))
: flatData;
} catch (e) {
if (!call.catchErrorPattern || !new RegExp(call.catchErrorPattern).test(e.code)) {
throw e;
}
}

if (call.physicalResourceIdPath) {
physicalResourceId = data[call.physicalResourceIdPath];
} else {
physicalResourceId = call.physicalResourceId!;
}
physicalResourceId = call.physicalResourceIdPath
? flatData[call.physicalResourceIdPath]
: call.physicalResourceId;
}

await respond('SUCCESS', 'OK', physicalResourceId, data);
Expand Down
13 changes: 13 additions & 0 deletions packages/@aws-cdk/aws-cloudformation/lib/aws-custom-resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,18 @@ export interface AwsSdkCall {
* @default use latest available API version
*/
readonly apiVersion?: string;

/**
* Restrict the data returned by the custom resource to a specific path in
* the API response. Use this to limit the data returned by the custom
* resource if working with API calls that could potentially result in custom
* response objects exceeding the hard limit of 4096 bytes.
*
* Example for ECS / updateService: 'service.deploymentConfiguration.maximumPercent'
*
* @default return all data
*/
readonly outputPath?: string;
}

export interface AwsCustomResourceProps {
Expand Down Expand Up @@ -158,6 +170,7 @@ export class AwsCustomResource extends cdk.Construct {

/**
* Returns response data for the AWS SDK call.
*
* Example for S3 / listBucket : 'Buckets.0.Name'
*
* @param dataPath the path to the data
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -223,5 +223,52 @@ export = {
test.equal(request.isDone(), true);

test.done();
}
},

async 'restrict output path'(test: Test) {
const listObjectsFake = sinon.fake.resolves({
Contents: [
{
Key: 'first-key',
ETag: 'first-key-etag'
},
{
Key: 'second-key',
ETag: 'second-key-etag',
}
]
} as SDK.S3.ListObjectsOutput);

AWS.mock('S3', 'listObjects', listObjectsFake);

const event: AWSLambda.CloudFormationCustomResourceCreateEvent = {
...eventCommon,
RequestType: 'Create',
ResourceProperties: {
ServiceToken: 'token',
Create: {
service: 'S3',
action: 'listObjects',
parameters: {
Bucket: 'my-bucket'
},
physicalResourceId: 'id',
outputPath: 'Contents.0'
} as AwsSdkCall
}
};

const request = createRequest(body =>
body.Status === 'SUCCESS' &&
body.PhysicalResourceId === 'id' &&
body.Data!['Contents.0.Key'] === 'first-key' &&
body.Data!['Contents.1.Key'] === undefined
);

await handler(event, {} as AWSLambda.Context);

test.equal(request.isDone(), true);

test.done();
},
};

0 comments on commit 0aa35e0

Please sign in to comment.