Skip to content

feat(scheduler): throw ValidationErrors instead of untyped errors #34434

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

Merged
merged 2 commits into from
May 13, 2025
Merged
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
2 changes: 0 additions & 2 deletions packages/aws-cdk-lib/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@ const noThrowDefaultErrorNotYetSupported = [
'aws-lambda-destinations',
'aws-lambda-event-sources',
'aws-lambda-nodejs',
'aws-scheduler-targets',
'aws-scheduler',
'aws-secretsmanager',
'aws-servicecatalog',
'core',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ScheduleTargetBase, ScheduleTargetBaseProps } from './target';
import * as events from '../../aws-events';
import { IRole } from '../../aws-iam';
import { IScheduleTarget, ISchedule, ScheduleTargetInput, ScheduleTargetConfig } from '../../aws-scheduler';
import { UnscopedValidationError } from '../../core';

/**
* An entry to be sent to EventBridge
Expand Down Expand Up @@ -54,7 +55,7 @@ export class EventBridgePutEvents extends ScheduleTargetBase implements ISchedul
) {
super(props, entry.eventBus.eventBusArn);
if (this.props.input) {
throw new Error('ScheduleTargetBaseProps.input is not supported for EventBridgePutEvents. Please use entry.detail instead.');
throw new UnscopedValidationError('ScheduleTargetBaseProps.input is not supported for EventBridgePutEvents. Please use entry.detail instead.');
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ScheduleTargetBase, ScheduleTargetBaseProps } from './target';
import { IRole } from '../../aws-iam';
import * as kinesis from '../../aws-kinesis';
import { ISchedule, IScheduleTarget, ScheduleTargetConfig } from '../../aws-scheduler';
import { Token } from '../../core';
import { Token, ValidationError } from '../../core';

/**
* Properties for a Kinesis Data Streams Target
Expand All @@ -29,7 +29,7 @@ export class KinesisStreamPutRecord extends ScheduleTargetBase implements ISched
super(props, stream.streamArn);

if (!Token.isUnresolved(props.partitionKey) && (props.partitionKey.length < 1 || props.partitionKey.length > 256)) {
throw new Error(`partitionKey length must be between 1 and 256, got ${props.partitionKey.length}`);
throw new ValidationError(`partitionKey length must be between 1 and 256, got ${props.partitionKey.length}`, stream);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ScheduleTargetBase, ScheduleTargetBaseProps } from './target';
import { IRole } from '../../aws-iam';
import { IPipeline } from '../../aws-sagemaker';
import { ISchedule, IScheduleTarget, ScheduleTargetConfig } from '../../aws-scheduler';
import { ValidationError } from '../../core';

/**
* Properties for a pipeline parameter
Expand Down Expand Up @@ -45,7 +46,7 @@ export class SageMakerStartPipelineExecution extends ScheduleTargetBase implemen
super(props, pipeline.pipelineArn);

if (props.pipelineParameterList !== undefined && props.pipelineParameterList.length > 200) {
throw new Error(`pipelineParameterList length must be between 0 and 200, got ${props.pipelineParameterList.length}`);
throw new ValidationError(`pipelineParameterList length must be between 0 and 200, got ${props.pipelineParameterList.length}`, pipeline);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ScheduleTargetBase, ScheduleTargetBaseProps } from './target';
import { IRole } from '../../aws-iam';
import { ISchedule, IScheduleTarget, ScheduleTargetConfig } from '../../aws-scheduler';
import * as sqs from '../../aws-sqs';
import { Token } from '../../core';
import { Token, ValidationError } from '../../core';

/**
* Properties for a SQS Queue Target
Expand Down Expand Up @@ -35,16 +35,16 @@ export class SqsSendMessage extends ScheduleTargetBase implements IScheduleTarge

if (props.messageGroupId !== undefined) {
if (!Token.isUnresolved(props.messageGroupId) && (props.messageGroupId.length < 1 || props.messageGroupId.length > 128)) {
throw new Error(`messageGroupId length must be between 1 and 128, got ${props.messageGroupId.length}`);
throw new ValidationError(`messageGroupId length must be between 1 and 128, got ${props.messageGroupId.length}`, queue);
}
if (!queue.fifo) {
throw new Error('target must be a FIFO queue if messageGroupId is specified');
throw new ValidationError('target must be a FIFO queue if messageGroupId is specified', queue);
}
if (!(queue.node.defaultChild as sqs.CfnQueue).contentBasedDeduplication) {
throw new Error('contentBasedDeduplication must be true if the target is a FIFO queue');
throw new ValidationError('contentBasedDeduplication must be true if the target is a FIFO queue', queue);
}
} else if (queue.fifo) {
throw new Error('messageGroupId must be specified if the target is a FIFO queue');
throw new ValidationError('messageGroupId must be specified if the target is a FIFO queue', queue);
}
}

Expand Down
6 changes: 3 additions & 3 deletions packages/aws-cdk-lib/aws-scheduler-targets/lib/target.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as iam from '../../aws-iam';
import { ISchedule, ScheduleTargetConfig, ScheduleTargetInput } from '../../aws-scheduler';
import { CfnSchedule } from '../../aws-scheduler/lib/scheduler.generated';
import * as sqs from '../../aws-sqs';
import { Duration, PhysicalName, Stack, Token } from '../../core';
import { Duration, PhysicalName, Stack, Token, UnscopedValidationError } from '../../core';
import { md5hash } from '../../core/lib/helpers-internal';

/**
Expand Down Expand Up @@ -157,10 +157,10 @@ export abstract class ScheduleTargetBase {
if (maximumEventAge) {
maxAge = maximumEventAge.toSeconds({ integral: true });
if (maxAge > maxMaxAge) {
throw new Error('Maximum event age is 1 day');
throw new UnscopedValidationError('Maximum event age is 1 day');
}
if (maxAge < minMaxAge) {
throw new Error('Minimum event age is 1 minute');
throw new UnscopedValidationError('Minimum event age is 1 minute');
}
}
let maxAttempts = 185;
Expand Down
8 changes: 4 additions & 4 deletions packages/aws-cdk-lib/aws-scheduler-targets/lib/universal.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ScheduleTargetBase, ScheduleTargetBaseProps } from './target';
import { IRole, PolicyStatement } from '../../aws-iam';
import { IScheduleTarget } from '../../aws-scheduler';
import { Annotations, Aws, Token } from '../../core';
import { Annotations, Aws, Token, UnscopedValidationError } from '../../core';
import { awsSdkToIamAction } from '../../custom-resources/lib/helpers-internal';

/**
Expand Down Expand Up @@ -80,13 +80,13 @@ export class Universal extends ScheduleTargetBase implements IScheduleTarget {
const action = props.action;

if (!Token.isUnresolved(service) && service !== service.toLowerCase()) {
throw new Error(`API service must be lowercase, got: ${service}`);
throw new UnscopedValidationError(`API service must be lowercase, got: ${service}`);
}
if (!Token.isUnresolved(action) && !action.startsWith(action[0]?.toLowerCase())) {
throw new Error(`API action must be camelCase, got: ${action}`);
throw new UnscopedValidationError(`API action must be camelCase, got: ${action}`);
}
if (!Token.isUnresolved(action) && NOT_SUPPORTED_ACTION_PREFIX.some(prefix => action.startsWith(prefix))) {
throw new Error(`Read-only API action is not supported by EventBridge Scheduler: ${service}:${action}`);
throw new UnscopedValidationError(`Read-only API action is not supported by EventBridge Scheduler: ${service}:${action}`);
}

const arn = `arn:${Aws.PARTITION}:scheduler:::aws-sdk:${service}:${action}`;
Expand Down
4 changes: 2 additions & 2 deletions packages/aws-cdk-lib/aws-scheduler/lib/schedule-expression.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as events from '../../aws-events';
import { Duration, TimeZone } from '../../core';
import { Duration, TimeZone, UnscopedValidationError } from '../../core';

/**
* ScheduleExpression for EventBridge Schedule
Expand All @@ -22,7 +22,7 @@ export abstract class ScheduleExpression {
return new LiteralScheduleExpression(`at(${literal})`, timeZone ?? TimeZone.ETC_UTC);
} catch (e) {
if (e instanceof RangeError) {
throw new Error('Invalid date');
throw new UnscopedValidationError('Invalid date');
}
throw e;
}
Expand Down
12 changes: 6 additions & 6 deletions packages/aws-cdk-lib/aws-scheduler/lib/schedule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { CfnSchedule } from './scheduler.generated';
import { IScheduleTarget } from './target';
import * as cloudwatch from '../../aws-cloudwatch';
import * as kms from '../../aws-kms';
import { Arn, ArnFormat, Duration, IResource, Resource, Token } from '../../core';
import { Arn, ArnFormat, Duration, IResource, Resource, Token, UnscopedValidationError, ValidationError } from '../../core';
import { addConstructMetadata } from '../../core/lib/metadata-resource';

/**
Expand Down Expand Up @@ -46,7 +46,7 @@ export class TimeWindow {
*/
public static flexible(maxWindow: Duration): TimeWindow {
if (maxWindow.toMinutes() < 1 || maxWindow.toMinutes() > 1440) {
throw new Error(`The provided duration must be between 1 minute and 1440 minutes, got ${maxWindow.toMinutes()}`);
throw new UnscopedValidationError(`The provided duration must be between 1 minute and 1440 minutes, got ${maxWindow.toMinutes()}`);
}
return new TimeWindow('FLEXIBLE', maxWindow);
}
Expand Down Expand Up @@ -304,7 +304,7 @@ export class Schedule extends Resource implements ISchedule {

this.validateTimeFrame(props.start, props.end);
if (props.scheduleName && !Token.isUnresolved(props.scheduleName) && props.scheduleName.length > 64) {
throw new Error(`scheduleName cannot be longer than 64 characters, got: ${props.scheduleName.length}`);
throw new ValidationError(`scheduleName cannot be longer than 64 characters, got: ${props.scheduleName.length}`, this);
}

const resource = new CfnSchedule(this, 'Resource', {
Expand Down Expand Up @@ -349,10 +349,10 @@ export class Schedule extends Resource implements ISchedule {
};

if (policy.maximumEventAgeInSeconds && (policy.maximumEventAgeInSeconds < 60 || policy.maximumEventAgeInSeconds > 86400)) {
throw new Error(`maximumEventAgeInSeconds must be between 60 and 86400, got ${policy.maximumEventAgeInSeconds}`);
throw new ValidationError(`maximumEventAgeInSeconds must be between 60 and 86400, got ${policy.maximumEventAgeInSeconds}`, this);
}
if (policy.maximumRetryAttempts && (policy.maximumRetryAttempts < 0 || policy.maximumRetryAttempts > 185)) {
throw new Error(`maximumRetryAttempts must be between 0 and 185, got ${policy.maximumRetryAttempts}`);
throw new ValidationError(`maximumRetryAttempts must be between 0 and 185, got ${policy.maximumRetryAttempts}`, this);
}

const isEmptyPolicy = Object.values(policy).every(value => value === undefined);
Expand All @@ -361,7 +361,7 @@ export class Schedule extends Resource implements ISchedule {

private validateTimeFrame(start?: Date, end?: Date) {
if (start && end && start >= end) {
throw new Error(`start must precede end, got start: ${start.toISOString()}, end: ${end.toISOString()}`);
throw new ValidationError(`start must precede end, got start: ${start.toISOString()}, end: ${end.toISOString()}`, this);
}
}
}
Loading