Skip to content

Commit

Permalink
feat(custom-resources): restrict output of AwsCustomResource to list …
Browse files Browse the repository at this point in the history
…of paths

It was already possible to restrict to a single path. Add option to
restrict to multiple paths and deprecate the single path option.

Also document this option.

See aws#2825 (comment)
  • Loading branch information
jogold committed Apr 8, 2021
1 parent 8782e67 commit 1ed69e0
Show file tree
Hide file tree
Showing 4 changed files with 110 additions and 4 deletions.
24 changes: 24 additions & 0 deletions packages/@aws-cdk/custom-resources/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,30 @@ new AwsCustomResource(this, 'Customized', {
})
```

### Restricting the output of the Custom Resource

CloudFormation imposes a hard limit of 4096 bytes for custom resources response
objects. If your API call returns an object that exceeds this limit, you can restrict
the data returned by the custom resource to specific paths in the API response:

```ts
new AwsCustomResource(stack, 'ListObjects', {
onCreate: {
service: 's3',
action: 'listObjectsV2',
parameters: {
Bucket: 'my-bucket',
},
physicalResourceId: PhysicalResourceId.of('id'),
outputPaths: ['Contents.0.Key', 'Contents.1.Key'], // Output only the two first keys
},
policy: AwsCustomResourcePolicy.fromSdkCalls({ resources: AwsCustomResourcePolicy.ANY_RESOURCE }),
});
```

Note that even if you restrict the output of your custom resource you can still use any
path in `PhysicalResourceId.fromResponse()`.

### Custom Resource Examples

#### Verify a domain with SES
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,22 @@ export interface AwsSdkCall {
* Example for ECS / updateService: 'service.deploymentConfiguration.maximumPercent'
*
* @default - return all data
*
* @deprecated use outputPaths instead
*/
readonly outputPath?: string;

/**
* Restrict the data returned by the custom resource to specific paths 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 outputPaths?: string[];
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,9 +128,19 @@ export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent
region: awsService.config.region, // For test purposes: check if region was correctly passed.
...flatten(response),
};
data = call.outputPath
? filterKeys(flatData, k => k.startsWith(call.outputPath!))
: flatData;

let outputPaths: string[] | undefined;
if (call.outputPath) {
outputPaths = [call.outputPath];
} else if (call.outputPaths) {
outputPaths = call.outputPaths;
}

if (outputPaths) {
data = filterKeys(flatData, startsWithOneOf(outputPaths));
} else {
data = flatData;
}
} catch (e) {
if (!call.ignoreErrorCodesMatching || !new RegExp(call.ignoreErrorCodesMatching).test(e.code)) {
throw e;
Expand Down Expand Up @@ -188,4 +198,15 @@ export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent
function decodeCall(call: string | undefined) {
if (!call) { return undefined; }
return JSON.parse(call);
}
}

function startsWithOneOf(searchStrings: string[]): (string: string) => boolean {
return function(string: string): boolean {
for (const searchString of searchStrings) {
if (string.startsWith(searchString)) {
return true;
}
}
return false;
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,53 @@ test('restrict output path', async () => {
expect(request.isDone()).toBeTruthy();
});

test('restrict output paths', async () => {
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: JSON.stringify({
service: 'S3',
action: 'listObjects',
parameters: {
Bucket: 'my-bucket',
},
physicalResourceId: PhysicalResourceId.of('id'),
outputPaths: ['Contents.0.Key', 'Contents.1.Key'],
} as AwsSdkCall),
},
};

const request = createRequest(body =>
body.Status === 'SUCCESS' &&
body.PhysicalResourceId === 'id' &&
JSON.stringify(body.Data) === JSON.stringify({
'Contents.0.Key': 'first-key',
'Contents.1.Key': 'second-key',
}),
);

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

expect(request.isDone()).toBeTruthy();
});

test('can specify apiVersion and region', async () => {
const publishFake = sinon.fake.resolves({});

Expand Down

0 comments on commit 1ed69e0

Please sign in to comment.