Skip to content

Commit

Permalink
feat(synthetics): throw ValidationError instead of untyped errors (a…
Browse files Browse the repository at this point in the history
…ws#33079)

### Issue 

`aws-synthetics` for aws#32569 

### Description of changes

ValidationErrors everywhere

### Describe any new or updated permissions being added

n/a

### Description of how you validated changes

Existing tests. Exemptions granted as this is basically a refactor of existing code.

### Checklist
- [x] My code adheres to the [CONTRIBUTING GUIDE](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and [DESIGN GUIDELINES](https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md)

----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
kaizencc authored Jan 23, 2025
1 parent 704ff0b commit e4703c1
Show file tree
Hide file tree
Showing 4 changed files with 30 additions and 26 deletions.
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

0 comments on commit e4703c1

Please sign in to comment.