Skip to content

Commit

Permalink
feat(custom-resources): rename catchErrorPattern to `ignoreErrorCod…
Browse files Browse the repository at this point in the history
…esMatching` (#6553)

In the spirit of being "Explicit and Clear*, renaming `catchErrorPattern` to `ignoreErrorCodesMatching` since it better describes the meaning of this property.

In addition, the following validations were added:

- `ignoreErrorCodesMatching` cannot be used with `PhysicalResourceId.fromResponse` since the response might not exist.
- `ignoreErrorCodesMatching` cannot be used with `getData` or `getDataString` since the resource might not have any attributes due to the error catching.

Relates to #5873 

BREAKING CHANGE: `catchErrorPattern` was renamed to `ignoreErrorCodesMatching`. In addition, a few synth time validations were added when using this property. See [Error Handling](https://github.com/aws/aws-cdk/tree/master/packages/%40aws-cdk/custom-resources#error-handling-1) for details.
  • Loading branch information
iliapolo authored and Elad Ben-Israel committed Mar 9, 2020
1 parent 96a0537 commit 421a148
Show file tree
Hide file tree
Showing 6 changed files with 147 additions and 12 deletions.
13 changes: 13 additions & 0 deletions packages/@aws-cdk/custom-resources/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,19 @@ const awsCustom2 = new AwsCustomResource(this, 'API2', {
})
```

### Error Handling

Every error produced by the API call is treated as is and will cause a "FAILED" response to be submitted to CloudFormation.
You can ignore some errors by specifying the `ignoreErrorCodesMatching` property, which accepts a regular expression that is
tested against the `code` property of the response. If matched, a "SUCCESS" response is submitted.
Note that in such a case, the call response data and the `Data` key submitted to CloudFormation would both be an empty JSON object.
Since a successful resource provisioning might or might not produce outputs, this presents us with some limitations:

- `PhysicalResourceId.fromResponse` - Since the call response data might be empty, we cannot use it to extract the physical id.
- `getData` and `getDataString` - Since the `Data` key is empty, the resource will not have any attributes, and therefore, invoking these functions will result in an error.

In both the cases, you will get a synth time error if you attempt to use it in conjunction with `ignoreErrorCodesMatching`.

### Examples

#### Verify a domain with SES
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export interface AwsSdkCall {
*
* @default - do not catch errors
*/
readonly catchErrorPattern?: string;
readonly ignoreErrorCodesMatching?: string;

/**
* API version to use for the service
Expand Down Expand Up @@ -245,9 +245,21 @@ export interface AwsCustomResourceProps {
*
*/
export class AwsCustomResource extends cdk.Construct implements iam.IGrantable {

private static breakIgnoreErrorsCircuit(sdkCalls: Array<AwsSdkCall | undefined>, caller: string) {

for (const call of sdkCalls) {
if (call?.ignoreErrorCodesMatching) {
throw new Error(`\`${caller}\`` + ' cannot be called along with `ignoreErrorCodesMatching`.');
}
}

}

public readonly grantPrincipal: iam.IPrincipal;

private readonly customResource: CustomResource;
private readonly props: AwsCustomResourceProps;

// 'props' cannot be optional, even though all its properties are optional.
// this is because at least one sdk call must be provided.
Expand All @@ -264,6 +276,14 @@ export class AwsCustomResource extends cdk.Construct implements iam.IGrantable {
}
}

for (const call of [props.onCreate, props.onUpdate, props.onDelete]) {
if (call?.physicalResourceId?.responsePath) {
AwsCustomResource.breakIgnoreErrorsCircuit([call], "PhysicalResourceId.fromResponse");
}
}

this.props = props;

const provider = new lambda.SingletonFunction(this, 'Provider', {
code: lambda.Code.fromAsset(path.join(__dirname, 'runtime')),
runtime: lambda.Runtime.NODEJS_12_X,
Expand Down Expand Up @@ -313,9 +333,14 @@ export class AwsCustomResource extends cdk.Construct implements iam.IGrantable {
* Use `Token.asXxx` to encode the returned `Reference` as a specific type or
* use the convenience `getDataString` for string attributes.
*
* Note that you cannot use this method if `ignoreErrorCodesMatching`
* is configured for any of the SDK calls. This is because in such a case,
* the response data might not exist, and will cause a CloudFormation deploy time error.
*
* @param dataPath the path to the data
*/
public getData(dataPath: string) {
AwsCustomResource.breakIgnoreErrorsCircuit([this.props.onCreate, this.props.onUpdate], "getData");
return this.customResource.getAtt(dataPath);
}

Expand All @@ -324,11 +349,17 @@ export class AwsCustomResource extends cdk.Construct implements iam.IGrantable {
*
* Example for S3 / listBucket : 'Buckets.0.Name'
*
* Note that you cannot use this method if `ignoreErrorCodesMatching`
* is configured for any of the SDK calls. This is because in such a case,
* the response data might not exist, and will cause a CloudFormation deploy time error.
*
* @param dataPath the path to the data
*/
public getDataString(dataPath: string): string {
AwsCustomResource.breakIgnoreErrorsCircuit([this.props.onCreate, this.props.onUpdate], "getDataString");
return this.customResource.getAttString(dataPath);
}

}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent
? filterKeys(flatData, k => k.startsWith(call.outputPath!))
: flatData;
} catch (e) {
if (!call.catchErrorPattern || !new RegExp(call.catchErrorPattern).test(e.code)) {
if (!call.ignoreErrorCodesMatching || !new RegExp(call.ignoreErrorCodesMatching).test(e.code)) {
throw e;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ test('catch errors', async () => {
Bucket: 'my-bucket'
},
physicalResourceId: PhysicalResourceId.of('physicalResourceId'),
catchErrorPattern: 'NoSuchBucket'
ignoreErrorCodesMatching: 'NoSuchBucket'
} as AwsSdkCall
}
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,97 @@ test('getData', () => {
});
});

test('fails when getData is used with `ignoreErrorCodesMatching`', () => {

const stack = new cdk.Stack();

const resource = new AwsCustomResource(stack, 'AwsSdk', {
onUpdate: {
service: 'CloudWatchLogs',
action: 'putRetentionPolicy',
parameters: {
logGroupName: '/aws/lambda/loggroup',
retentionInDays: 90
},
ignoreErrorCodesMatching: ".*",
physicalResourceId: PhysicalResourceId.of("Id")
},
policy: AwsCustomResourcePolicy.fromSdkCalls({resources: AwsCustomResourcePolicy.ANY_RESOURCE}),
});

expect(() => resource.getData("ShouldFail")).toThrow(/`getData`.+`ignoreErrorCodesMatching`/);

});

test('fails when getDataString is used with `ignoreErrorCodesMatching`', () => {

const stack = new cdk.Stack();

const resource = new AwsCustomResource(stack, 'AwsSdk', {
onUpdate: {
service: 'CloudWatchLogs',
action: 'putRetentionPolicy',
parameters: {
logGroupName: '/aws/lambda/loggroup',
retentionInDays: 90
},
ignoreErrorCodesMatching: ".*",
physicalResourceId: PhysicalResourceId.of("Id"),
},
policy: AwsCustomResourcePolicy.fromSdkCalls({resources: AwsCustomResourcePolicy.ANY_RESOURCE}),
});

expect(() => resource.getDataString("ShouldFail")).toThrow(/`getDataString`.+`ignoreErrorCodesMatching`/);

});

test('fail when `PhysicalResourceId.fromResponse` is used with `ignoreErrorCodesMatching', () => {

const stack = new cdk.Stack();
expect(() => new AwsCustomResource(stack, 'AwsSdkOnUpdate', {
onUpdate: {
service: 'CloudWatchLogs',
action: 'putRetentionPolicy',
parameters: {
logGroupName: '/aws/lambda/loggroup',
retentionInDays: 90
},
ignoreErrorCodesMatching: ".*",
physicalResourceId: PhysicalResourceId.fromResponse("Response")
},
policy: AwsCustomResourcePolicy.fromSdkCalls({resources: AwsCustomResourcePolicy.ANY_RESOURCE}),
})).toThrow(/`PhysicalResourceId.fromResponse`.+`ignoreErrorCodesMatching`/);

expect(() => new AwsCustomResource(stack, 'AwsSdkOnCreate', {
onCreate: {
service: 'CloudWatchLogs',
action: 'putRetentionPolicy',
parameters: {
logGroupName: '/aws/lambda/loggroup',
retentionInDays: 90
},
ignoreErrorCodesMatching: ".*",
physicalResourceId: PhysicalResourceId.fromResponse("Response")
},
policy: AwsCustomResourcePolicy.fromSdkCalls({resources: AwsCustomResourcePolicy.ANY_RESOURCE}),
})).toThrow(/`PhysicalResourceId.fromResponse`.+`ignoreErrorCodesMatching`/);

expect(() => new AwsCustomResource(stack, 'AwsSdkOnDelete', {
onDelete: {
service: 'CloudWatchLogs',
action: 'putRetentionPolicy',
parameters: {
logGroupName: '/aws/lambda/loggroup',
retentionInDays: 90
},
ignoreErrorCodesMatching: ".*",
physicalResourceId: PhysicalResourceId.fromResponse("Response")
},
policy: AwsCustomResourcePolicy.fromSdkCalls({resources: AwsCustomResourcePolicy.ANY_RESOURCE}),
})).toThrow(/`PhysicalResourceId.fromResponse`.+`ignoreErrorCodesMatching`/);

});

test('getDataString', () => {
// GIVEN
const stack = new cdk.Stack();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@
"Properties": {
"Code": {
"S3Bucket": {
"Ref": "AssetParameters6bf0eac2bfd1c5dcd41b3cc53f24814f9dba9cce0cb7fe2e34d0ded661481748S3BucketFC283D1B"
"Ref": "AssetParameters4317eb4c8ccce73f91d75018d965c243c175c64101884e0f202d4a280e23313fS3Bucket71E9DB75"
},
"S3Key": {
"Fn::Join": [
Expand All @@ -126,7 +126,7 @@
"Fn::Split": [
"||",
{
"Ref": "AssetParameters6bf0eac2bfd1c5dcd41b3cc53f24814f9dba9cce0cb7fe2e34d0ded661481748S3VersionKey7E916B81"
"Ref": "AssetParameters4317eb4c8ccce73f91d75018d965c243c175c64101884e0f202d4a280e23313fS3VersionKeyADF76D6F"
}
]
}
Expand All @@ -139,7 +139,7 @@
"Fn::Split": [
"||",
{
"Ref": "AssetParameters6bf0eac2bfd1c5dcd41b3cc53f24814f9dba9cce0cb7fe2e34d0ded661481748S3VersionKey7E916B81"
"Ref": "AssetParameters4317eb4c8ccce73f91d75018d965c243c175c64101884e0f202d4a280e23313fS3VersionKeyADF76D6F"
}
]
}
Expand Down Expand Up @@ -242,17 +242,17 @@
}
},
"Parameters": {
"AssetParameters6bf0eac2bfd1c5dcd41b3cc53f24814f9dba9cce0cb7fe2e34d0ded661481748S3BucketFC283D1B": {
"AssetParameters4317eb4c8ccce73f91d75018d965c243c175c64101884e0f202d4a280e23313fS3Bucket71E9DB75": {
"Type": "String",
"Description": "S3 bucket for asset \"6bf0eac2bfd1c5dcd41b3cc53f24814f9dba9cce0cb7fe2e34d0ded661481748\""
"Description": "S3 bucket for asset \"4317eb4c8ccce73f91d75018d965c243c175c64101884e0f202d4a280e23313f\""
},
"AssetParameters6bf0eac2bfd1c5dcd41b3cc53f24814f9dba9cce0cb7fe2e34d0ded661481748S3VersionKey7E916B81": {
"AssetParameters4317eb4c8ccce73f91d75018d965c243c175c64101884e0f202d4a280e23313fS3VersionKeyADF76D6F": {
"Type": "String",
"Description": "S3 key for asset version \"6bf0eac2bfd1c5dcd41b3cc53f24814f9dba9cce0cb7fe2e34d0ded661481748\""
"Description": "S3 key for asset version \"4317eb4c8ccce73f91d75018d965c243c175c64101884e0f202d4a280e23313f\""
},
"AssetParameters6bf0eac2bfd1c5dcd41b3cc53f24814f9dba9cce0cb7fe2e34d0ded661481748ArtifactHashB96EE827": {
"AssetParameters4317eb4c8ccce73f91d75018d965c243c175c64101884e0f202d4a280e23313fArtifactHash7FE12709": {
"Type": "String",
"Description": "Artifact hash for asset \"6bf0eac2bfd1c5dcd41b3cc53f24814f9dba9cce0cb7fe2e34d0ded661481748\""
"Description": "Artifact hash for asset \"4317eb4c8ccce73f91d75018d965c243c175c64101884e0f202d4a280e23313f\""
}
},
"Outputs": {
Expand Down

0 comments on commit 421a148

Please sign in to comment.