Skip to content
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

feat(synthetics): throw ValidationError instead of untyped errors #33079

Merged
merged 2 commits into from
Jan 23, 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
1 change: 1 addition & 0 deletions packages/aws-cdk-lib/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const enableNoThrowDefaultErrorIn = [
'aws-ssmcontacts',
'aws-ssmincidents',
'aws-ssmquicksetup',
'aws-synthetics',
];
baseConfig.overrides.push({
files: enableNoThrowDefaultErrorIn.map(m => `./${m}/lib/**`),
Expand Down
31 changes: 16 additions & 15 deletions packages/aws-cdk-lib/aws-synthetics/lib/canary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import * as iam from '../../aws-iam';
import * as kms from '../../aws-kms';
import * as s3 from '../../aws-s3';
import * as cdk from '../../core';
import { UnscopedValidationError, ValidationError } from '../../core/lib/errors';
import { AutoDeleteUnderlyingResourcesProvider } from '../../custom-resource-handlers/dist/aws-synthetics/auto-delete-underlying-resources-provider.generated';

const AUTO_DELETE_UNDERLYING_RESOURCES_RESOURCE_TYPE = 'Custom::SyntheticsAutoDeleteUnderlyingResources';
Expand Down Expand Up @@ -427,7 +428,7 @@ export class Canary extends cdk.Resource implements ec2.IConnectable {
public get connections(): ec2.Connections {
if (!this._connections) {
// eslint-disable-next-line max-len
throw new Error('Only VPC-associated Canaries have security groups to manage. Supply the "vpc" parameter when creating the Canary.');
throw new ValidationError('Only VPC-associated Canaries have security groups to manage. Supply the "vpc" parameter when creating the Canary.', this);
}
return this._connections;
}
Expand Down Expand Up @@ -568,15 +569,15 @@ export class Canary extends cdk.Resource implements ec2.IConnectable {
];
if (oldRuntimes.includes(runtime)) {
if (!handler.match(/^[0-9A-Za-z_\\-]+\.handler*$/)) {
throw new Error(`Canary Handler must be specified as \'fileName.handler\' for legacy runtimes, received ${handler}`);
throw new ValidationError(`Canary Handler must be specified as \'fileName.handler\' for legacy runtimes, received ${handler}`, this);
}
} else {
if (!handler.match(/^([0-9a-zA-Z_-]+\/)*[0-9A-Za-z_\\-]+\.[A-Za-z_][A-Za-z0-9_]*$/)) {
throw new Error(`Canary Handler must be specified either as \'fileName.handler\', \'fileName.functionName\', or \'folder/fileName.functionName\', received ${handler}`);
throw new ValidationError(`Canary Handler must be specified either as \'fileName.handler\', \'fileName.functionName\', or \'folder/fileName.functionName\', received ${handler}`, this);
}
}
if (handler.length < 1 || handler.length > 128) {
throw new Error(`Canary Handler length must be between 1 and 128, received ${handler.length}`);
throw new ValidationError(`Canary Handler length must be between 1 and 128, received ${handler.length}`, this);
}
}

Expand All @@ -596,30 +597,30 @@ export class Canary extends cdk.Resource implements ec2.IConnectable {
(!cdk.Token.isUnresolved(props.runtime.name) && props.runtime.name.includes('playwright'))
)
) {
throw new Error(`You can only enable active tracing for canaries that use canary runtime version 'syn-nodejs-2.0' or later and are not using the Playwright runtime, got ${props.runtime.name}.`);
throw new ValidationError(`You can only enable active tracing for canaries that use canary runtime version 'syn-nodejs-2.0' or later and are not using the Playwright runtime, got ${props.runtime.name}.`, this);
}

let memoryInMb: number | undefined;
if (!cdk.Token.isUnresolved(props.memory) && props.memory !== undefined) {
memoryInMb = props.memory.toMebibytes();
if (memoryInMb % 64 !== 0) {
throw new Error(`\`memory\` must be a multiple of 64 MiB, got ${memoryInMb} MiB.`);
throw new ValidationError(`\`memory\` must be a multiple of 64 MiB, got ${memoryInMb} MiB.`, this);
}
if (memoryInMb < 960 || memoryInMb > 3008) {
throw new Error(`\`memory\` must be between 960 MiB and 3008 MiB, got ${memoryInMb} MiB.`);
throw new ValidationError(`\`memory\` must be between 960 MiB and 3008 MiB, got ${memoryInMb} MiB.`, this);
}
}

let timeoutInSeconds: number | undefined;
if (!cdk.Token.isUnresolved(props.timeout) && props.timeout !== undefined) {
const timeoutInMillis = props.timeout.toMilliseconds();
if (timeoutInMillis % 1000 !== 0) {
throw new Error(`\`timeout\` must be set as an integer representing seconds, got ${timeoutInMillis} milliseconds.`);
throw new ValidationError(`\`timeout\` must be set as an integer representing seconds, got ${timeoutInMillis} milliseconds.`, this);
}

timeoutInSeconds = props.timeout.toSeconds();
if (timeoutInSeconds < 3 || timeoutInSeconds > 840) {
throw new Error(`\`timeout\` must be between 3 seconds and 840 seconds, got ${timeoutInSeconds} seconds.`);
throw new ValidationError(`\`timeout\` must be between 3 seconds and 840 seconds, got ${timeoutInSeconds} seconds.`, this);
}
}

Expand All @@ -644,15 +645,15 @@ export class Canary extends cdk.Resource implements ec2.IConnectable {
private createVpcConfig(props: CanaryProps): CfnCanary.VPCConfigProperty | undefined {
if (!props.vpc) {
if (props.vpcSubnets != null || props.securityGroups != null) {
throw new Error("You must provide the 'vpc' prop when using VPC-related properties.");
throw new ValidationError("You must provide the 'vpc' prop when using VPC-related properties.", this);
}

return undefined;
}

const { subnetIds } = props.vpc.selectSubnets(props.vpcSubnets);
if (subnetIds.length < 1) {
throw new Error('No matching subnets found in the VPC.');
throw new ValidationError('No matching subnets found in the VPC.', this);
}

let securityGroups: ec2.ISecurityGroup[];
Expand Down Expand Up @@ -685,12 +686,12 @@ export class Canary extends cdk.Resource implements ec2.IConnectable {
props.artifactS3EncryptionMode === ArtifactsEncryptionMode.S3_MANAGED &&
props.artifactS3KmsKey
) {
throw new Error(`A customer-managed KMS key was provided, but the encryption mode is not set to SSE-KMS, got: ${props.artifactS3EncryptionMode}.`);
throw new ValidationError(`A customer-managed KMS key was provided, but the encryption mode is not set to SSE-KMS, got: ${props.artifactS3EncryptionMode}.`, this);
}

// Only check runtime family is Node.js because versions prior to `syn-nodejs-puppeteer-3.3` are deprecated and can no longer be configured.
if (!isNodeRuntime && props.artifactS3EncryptionMode) {
throw new Error(`Artifact encryption is only supported for canaries that use Synthetics runtime version \`syn-nodejs-puppeteer-3.3\` or later and the Playwright runtime, got ${props.runtime.name}.`);
throw new ValidationError(`Artifact encryption is only supported for canaries that use Synthetics runtime version \`syn-nodejs-puppeteer-3.3\` or later and the Playwright runtime, got ${props.runtime.name}.`, this);
}

const encryptionMode = props.artifactS3EncryptionMode ? props.artifactS3EncryptionMode :
Expand Down Expand Up @@ -752,9 +753,9 @@ const nameRegex: RegExp = /^[0-9a-z_\-]+$/;
*/
function validateName(name: string) {
if (name.length > 255) {
throw new Error(`Canary name is too large, must be between 1 and 255 characters, but is ${name.length} (got "${name}")`);
throw new UnscopedValidationError(`Canary name is too large, must be between 1 and 255 characters, but is ${name.length} (got "${name}")`);
}
if (!nameRegex.test(name)) {
throw new Error(`Canary name must be lowercase, numbers, hyphens, or underscores (got "${name}")`);
throw new UnscopedValidationError(`Canary name must be lowercase, numbers, hyphens, or underscores (got "${name}")`);
}
}
19 changes: 10 additions & 9 deletions packages/aws-cdk-lib/aws-synthetics/lib/code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { RuntimeFamily } from './runtime';
import * as s3 from '../../aws-s3';
import * as s3_assets from '../../aws-s3-assets';
import { Stage, Token } from '../../core';
import { UnscopedValidationError, ValidationError } from '../../core/lib/errors';

/**
* The code the canary should execute
Expand Down Expand Up @@ -92,7 +93,7 @@ export class AssetCode extends Code {
super();

if (!fs.existsSync(this.assetPath)) {
throw new Error(`${this.assetPath} is not a valid path`);
throw new UnscopedValidationError(`${this.assetPath} is not a valid path`);
}
}

Expand Down Expand Up @@ -129,7 +130,7 @@ export class AssetCode extends Code {
*/
private validateCanaryAsset(scope: Construct, handler: string, family: RuntimeFamily, runtimeName?: string) {
if (!this.asset) {
throw new Error("'validateCanaryAsset' must be called after 'this.asset' is instantiated");
throw new ValidationError("'validateCanaryAsset' must be called after 'this.asset' is instantiated", scope);
}

// Get the staged (or copied) asset path.
Expand All @@ -139,7 +140,7 @@ export class AssetCode extends Code {

if (path.extname(assetPath) !== '.zip') {
if (!fs.lstatSync(assetPath).isDirectory()) {
throw new Error(`Asset must be a .zip file or a directory (${this.assetPath})`);
throw new ValidationError(`Asset must be a .zip file or a directory (${this.assetPath})`, scope);
}

const filename = handler.split('.')[0];
Expand All @@ -151,16 +152,16 @@ export class AssetCode extends Code {
const hasValidExtension = playwrightValidExtensions.some(ext => fs.existsSync(path.join(assetPath, `${filename}${ext}`)));
// Requires asset directory to have the structure 'nodejs/node_modules' for puppeteer runtime.
if (family === RuntimeFamily.NODEJS && runtimeName.includes('puppeteer') && !fs.existsSync(path.join(assetPath, 'nodejs', 'node_modules', nodeFilename))) {
throw new Error(`The canary resource requires that the handler is present at "nodejs/node_modules/${nodeFilename}" but not found at ${this.assetPath} (https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Canaries_WritingCanary_Nodejs.html)`);
throw new ValidationError(`The canary resource requires that the handler is present at "nodejs/node_modules/${nodeFilename}" but not found at ${this.assetPath} (https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Canaries_WritingCanary_Nodejs.html)`, scope);
}
// Requires the canary handler file to have the extension '.js', '.mjs', or '.cjs' for the playwright runtime.
if (family === RuntimeFamily.NODEJS && runtimeName.includes('playwright') && !hasValidExtension) {
throw new Error(`The canary resource requires that the handler is present at one of the following extensions: ${playwrightValidExtensions.join(', ')} but not found at ${this.assetPath}`);
throw new ValidationError(`The canary resource requires that the handler is present at one of the following extensions: ${playwrightValidExtensions.join(', ')} but not found at ${this.assetPath}`, scope);
}
}
// Requires the asset directory to have the structure 'python/{canary-handler-name}.py' for the Python runtime.
if (family === RuntimeFamily.PYTHON && !fs.existsSync(path.join(assetPath, 'python', pythonFilename))) {
throw new Error(`The canary resource requires that the handler is present at "python/${pythonFilename}" but not found at ${this.assetPath} (https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Canaries_WritingCanary_Python.html)`);
throw new ValidationError(`The canary resource requires that the handler is present at "python/${pythonFilename}" but not found at ${this.assetPath} (https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Canaries_WritingCanary_Python.html)`, scope);
}
}
}
Expand All @@ -174,14 +175,14 @@ export class InlineCode extends Code {
super();

if (code.length === 0) {
throw new Error('Canary inline code cannot be empty');
throw new UnscopedValidationError('Canary inline code cannot be empty');
}
}

public bind(_scope: Construct, handler: string, _family: RuntimeFamily, _runtimeName?: string): CodeConfig {
public bind(scope: Construct, handler: string, _family: RuntimeFamily, _runtimeName?: string): CodeConfig {

if (handler !== 'index.handler') {
throw new Error(`The handler for inline code must be "index.handler" (got "${handler}")`);
throw new ValidationError(`The handler for inline code must be "index.handler" (got "${handler}")`, scope);
}

return {
Expand Down
5 changes: 3 additions & 2 deletions packages/aws-cdk-lib/aws-synthetics/lib/schedule.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Duration } from '../../core';
import { UnscopedValidationError } from '../../core/lib/errors';

/**
* Schedule for canary runs
Expand Down Expand Up @@ -30,7 +31,7 @@ export class Schedule {
public static rate(interval: Duration): Schedule {
const minutes = interval.toMinutes();
if (minutes > 60) {
throw new Error('Schedule duration must be between 1 and 60 minutes');
throw new UnscopedValidationError('Schedule duration must be between 1 and 60 minutes');
}
if (minutes === 0) {
return Schedule.once();
Expand All @@ -46,7 +47,7 @@ export class Schedule {
*/
public static cron(options: CronOptions): Schedule {
if (options.weekDay !== undefined && options.day !== undefined) {
throw new Error('Cannot supply both \'day\' and \'weekDay\', use at most one');
throw new UnscopedValidationError('Cannot supply both \'day\' and \'weekDay\', use at most one');
}

const minute = fallback(options.minute, '*');
Expand Down
Loading