Skip to content

feat(custom-resources): throw ValidationError instead of untyped Errors #33392

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

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
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
13 changes: 13 additions & 0 deletions packages/aws-cdk-lib/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const enableNoThrowDefaultErrorIn = [
'aws-elasticloadbalancingv2-targets',
'aws-lambda',
'aws-rds',
'custom-resources',
'aws-s3',
'aws-sns',
'aws-sqs',
Expand Down Expand Up @@ -75,5 +76,17 @@ baseConfig.overrides.push({
rules: { "@cdklabs/no-throw-default-error": ['error'] },
});

// exceptions for no-throw-default-error
baseConfig.overrides.push({
rules: { "@cdklabs/no-throw-default-error": "off" },
files: [
// Test files
"./**/test/**",

// Lambda Runtime code in various locations
"./custom-resources/lib/provider-framework/runtime/**",
],
});


module.exports = baseConfig;
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,7 @@ export class AwsCustomResource extends 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`.');
throw new cdk.UnscopedValidationError(`\`${caller}\`` + ' cannot be called along with `ignoreErrorCodesMatching`.');
}
}
}
Expand All @@ -464,19 +464,19 @@ export class AwsCustomResource extends Construct implements iam.IGrantable {
super(scope, id);

if (!props.onCreate && !props.onUpdate && !props.onDelete) {
throw new Error('At least `onCreate`, `onUpdate` or `onDelete` must be specified.');
throw new cdk.ValidationError('At least `onCreate`, `onUpdate` or `onDelete` must be specified.', this);
}

if (!props.role && !props.policy) {
throw new Error('At least one of `policy` or `role` (or both) must be specified.');
throw new cdk.ValidationError('At least one of `policy` or `role` (or both) must be specified.', this);
}

if (props.onCreate && !props.onCreate.physicalResourceId) {
throw new Error("'physicalResourceId' must be specified for 'onCreate' call.");
throw new cdk.ValidationError("'physicalResourceId' must be specified for 'onCreate' call.", this);
}

if (!props.onCreate && props.onUpdate && !props.onUpdate.physicalResourceId) {
throw new Error("'physicalResourceId' must be specified for 'onUpdate' call when 'onCreate' is omitted.");
throw new cdk.ValidationError("'physicalResourceId' must be specified for 'onUpdate' call when 'onCreate' is omitted.", this);
}

for (const call of [props.onCreate, props.onUpdate, props.onDelete]) {
Expand All @@ -486,7 +486,7 @@ export class AwsCustomResource extends Construct implements iam.IGrantable {
}

if (includesPhysicalResourceIdRef(props.onCreate?.parameters)) {
throw new Error('`PhysicalResourceIdReference` must not be specified in `onCreate` parameters.');
throw new cdk.ValidationError('`PhysicalResourceIdReference` must not be specified in `onCreate` parameters.', this);
}

this.props = props;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import * as iam from '../../../aws-iam';
import * as kms from '../../../aws-kms';
import * as lambda from '../../../aws-lambda';
import * as logs from '../../../aws-logs';
import { Duration } from '../../../core';
import { Duration, ValidationError } from '../../../core';

const RUNTIME_HANDLER_PATH = path.join(__dirname, 'runtime');
const FRAMEWORK_HANDLER_TIMEOUT = Duration.minutes(15); // keep it simple for now
Expand Down Expand Up @@ -196,9 +196,9 @@ export class Provider extends Construct implements ICustomResourceProvider {
|| props.waiterStateMachineLogOptions
|| props.disableWaiterStateMachineLogging !== undefined
) {
throw new Error('"queryInterval", "totalTimeout", "waiterStateMachineLogOptions", and "disableWaiterStateMachineLogging" '
throw new ValidationError('"queryInterval", "totalTimeout", "waiterStateMachineLogOptions", and "disableWaiterStateMachineLogging" '
+ 'can only be configured if "isCompleteHandler" is specified. '
+ 'Otherwise, they have no meaning');
+ 'Otherwise, they have no meaning', this);
}
}

Expand All @@ -220,7 +220,7 @@ export class Provider extends Construct implements ICustomResourceProvider {
const isCompleteFunction = this.createFunction(consts.FRAMEWORK_IS_COMPLETE_HANDLER_NAME);
const timeoutFunction = this.createFunction(consts.FRAMEWORK_ON_TIMEOUT_HANDLER_NAME);

const retry = calculateRetryPolicy(props);
const retry = calculateRetryPolicy(this, props);
const waiterStateMachine = new WaiterStateMachine(this, 'waiter-state-machine', {
isCompleteHandler: isCompleteFunction,
timeoutHandler: timeoutFunction,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { Duration } from '../../../core';
import { Construct } from 'constructs';
import { Duration, ValidationError } from '../../../core';

const DEFAULT_TIMEOUT = Duration.minutes(30);
const DEFAULT_INTERVAL = Duration.seconds(5);

export function calculateRetryPolicy(props: { totalTimeout?: Duration; queryInterval?: Duration } = { }) {
export function calculateRetryPolicy(scope: Construct, props: { totalTimeout?: Duration; queryInterval?: Duration } = { }) {
const totalTimeout = props.totalTimeout || DEFAULT_TIMEOUT;
const interval = props.queryInterval || DEFAULT_INTERVAL;
const maxAttempts = totalTimeout.toSeconds() / interval.toSeconds();

if (Math.round(maxAttempts) !== maxAttempts) {
throw new Error(`Cannot determine retry count since totalTimeout=${totalTimeout.toSeconds()}s is not integrally dividable by queryInterval=${interval.toSeconds()}s`);
throw new ValidationError(`Cannot determine retry count since totalTimeout=${totalTimeout.toSeconds()}s is not integrally dividable by queryInterval=${interval.toSeconds()}s`, scope);
}

return {
Expand Down