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(lambda): throw ValidationError instead of untyped errors #33033

Merged
merged 1 commit into from
Jan 21, 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: 1 addition & 1 deletion packages/aws-cdk-lib/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ baseConfig.rules['import/no-extraneous-dependencies'] = [


// no-throw-default-error
const modules = ['aws-s3'];
const modules = ['aws-s3', 'aws-lambda'];
baseConfig.overrides.push({
files: modules.map(m => `./${m}/lib/**`),
rules: { "@cdklabs/no-throw-default-error": ['error'] },
Expand Down
5 changes: 3 additions & 2 deletions packages/aws-cdk-lib/aws-lambda/lib/adot-layers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { IConstruct } from 'constructs';
import { Architecture } from './architecture';
import { IFunction } from './function-base';
import { ValidationError } from '../../core/lib/errors';
import { Stack } from '../../core/lib/stack';
import { Token } from '../../core/lib/token';
import { RegionInfo } from '../../region-info';
Expand Down Expand Up @@ -68,8 +69,8 @@ function getLayerArn(scope: IConstruct, type: string, version: string, architect
if (region !== undefined && !Token.isUnresolved(region)) {
const arn = RegionInfo.get(region).adotLambdaLayerArn(type, version, architecture);
if (arn === undefined) {
throw new Error(
`Could not find the ARN information for the ADOT Lambda Layer of type ${type} and version ${version} in ${region}`,
throw new ValidationError(
`Could not find the ARN information for the ADOT Lambda Layer of type ${type} and version ${version} in ${region}`, scope,
);
}
return arn;
Expand Down
9 changes: 5 additions & 4 deletions packages/aws-cdk-lib/aws-lambda/lib/alias.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import * as appscaling from '../../aws-applicationautoscaling';
import * as cloudwatch from '../../aws-cloudwatch';
import * as iam from '../../aws-iam';
import { ArnFormat } from '../../core';
import { ValidationError } from '../../core/lib/errors';

export interface IAlias extends IFunction {
/**
Expand Down Expand Up @@ -223,7 +224,7 @@ export class Alias extends QualifiedFunctionBase implements IAlias {
*/
public addAutoScaling(options: AutoScalingOptions): IScalableFunctionAttribute {
if (this.scalableAlias) {
throw new Error('AutoScaling already enabled for this alias');
throw new ValidationError('AutoScaling already enabled for this alias', this);
}
return this.scalableAlias = new ScalableFunctionAttribute(this, 'AliasScaling', {
minCapacity: options.minCapacity ?? 1,
Expand Down Expand Up @@ -262,12 +263,12 @@ export class Alias extends QualifiedFunctionBase implements IAlias {
*/
private validateAdditionalWeights(weights: VersionWeight[]) {
const total = weights.map(w => {
if (w.weight < 0 || w.weight > 1) { throw new Error(`Additional version weight must be between 0 and 1, got: ${w.weight}`); }
if (w.weight < 0 || w.weight > 1) { throw new ValidationError(`Additional version weight must be between 0 and 1, got: ${w.weight}`, this); }
return w.weight;
}).reduce((a, x) => a + x);

if (total > 1) {
throw new Error(`Sum of additional version weights must not exceed 1, got: ${total}`);
throw new ValidationError(`Sum of additional version weights must not exceed 1, got: ${total}`, this);
}
}

Expand All @@ -282,7 +283,7 @@ export class Alias extends QualifiedFunctionBase implements IAlias {
}

if (props.provisionedConcurrentExecutions <= 0) {
throw new Error('provisionedConcurrentExecutions must have value greater than or equal to 1');
throw new ValidationError('provisionedConcurrentExecutions must have value greater than or equal to 1', this);
}

return { provisionedConcurrentExecutions: props.provisionedConcurrentExecutions };
Expand Down
5 changes: 3 additions & 2 deletions packages/aws-cdk-lib/aws-lambda/lib/code-signing-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Construct } from 'constructs';
import { CfnCodeSigningConfig } from './lambda.generated';
import { ISigningProfile } from '../../aws-signer';
import { ArnFormat, IResource, Resource, Stack } from '../../core';
import { ValidationError } from '../../core/lib/errors';

/**
* Code signing configuration policy for deployment validation failure.
Expand Down Expand Up @@ -78,10 +79,10 @@ export class CodeSigningConfig extends Resource implements ICodeSigningConfig {
* @param id The construct's name.
* @param codeSigningConfigArn The ARN of code signing config.
*/
public static fromCodeSigningConfigArn( scope: Construct, id: string, codeSigningConfigArn: string): ICodeSigningConfig {
public static fromCodeSigningConfigArn(scope: Construct, id: string, codeSigningConfigArn: string): ICodeSigningConfig {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

argh! should be a linter rule

const codeSigningProfileId = Stack.of(scope).splitArn(codeSigningConfigArn, ArnFormat.SLASH_RESOURCE_NAME).resourceName;
if (!codeSigningProfileId) {
throw new Error(`Code signing config ARN must be in the format 'arn:<partition>:lambda:<region>:<account>:code-signing-config:<codeSigningConfigArn>', got: '${codeSigningConfigArn}'`);
throw new ValidationError(`Code signing config ARN must be in the format 'arn:<partition>:lambda:<region>:<account>:code-signing-config:<codeSigningConfigArn>', got: '${codeSigningConfigArn}'`, scope);
}
const assertedCodeSigningProfileId = codeSigningProfileId;
class Import extends Resource implements ICodeSigningConfig {
Expand Down
31 changes: 16 additions & 15 deletions packages/aws-cdk-lib/aws-lambda/lib/code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { IKey } from '../../aws-kms';
import * as s3 from '../../aws-s3';
import * as s3_assets from '../../aws-s3-assets';
import * as cdk from '../../core';
import { UnscopedValidationError, ValidationError } from '../../core/lib/errors';

/**
* Represents the Lambda Handler Code.
Expand Down Expand Up @@ -83,7 +84,7 @@ export abstract class Code {
options?: CustomCommandOptions,
): AssetCode {
if (command.length === 0) {
throw new Error('command must contain at least one argument. For example, ["node", "buildFile.js"].');
throw new UnscopedValidationError('command must contain at least one argument. For example, ["node", "buildFile.js"].');
}

const cmd = command[0];
Expand All @@ -94,10 +95,10 @@ export abstract class Code {
: spawnSync(cmd, commandArguments, options.commandOptions);

if (proc.error) {
throw new Error(`Failed to execute custom command: ${proc.error}`);
throw new UnscopedValidationError(`Failed to execute custom command: ${proc.error}`);
}
if (proc.status !== 0) {
throw new Error(`${command.join(' ')} exited with status: ${proc.status}\n\nstdout: ${proc.stdout?.toString().trim()}\n\nstderr: ${proc.stderr?.toString().trim()}`);
throw new UnscopedValidationError(`${command.join(' ')} exited with status: ${proc.status}\n\nstdout: ${proc.stdout?.toString().trim()}\n\nstderr: ${proc.stderr?.toString().trim()}`);
}

return new AssetCode(output, options);
Expand Down Expand Up @@ -275,7 +276,7 @@ export class S3Code extends Code {
super();

if (!bucket.bucketName) {
throw new Error('bucketName is undefined for the provided bucket');
throw new ValidationError('bucketName is undefined for the provided bucket', bucket);
}

this.bucketName = bucket.bucketName;
Expand Down Expand Up @@ -303,7 +304,7 @@ export class S3CodeV2 extends Code {
constructor(bucket: s3.IBucket, private key: string, private options?: BucketOptions) {
super();
if (!bucket.bucketName) {
throw new Error('bucketName is undefined for the provided bucket');
throw new ValidationError('bucketName is undefined for the provided bucket', bucket);
}

this.bucketName = bucket.bucketName;
Expand Down Expand Up @@ -332,7 +333,7 @@ export class InlineCode extends Code {
super();

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

Expand Down Expand Up @@ -366,12 +367,12 @@ export class AssetCode extends Code {
...this.options,
});
} else if (cdk.Stack.of(this.asset) !== cdk.Stack.of(scope)) {
throw new Error(`Asset is already associated with another stack '${cdk.Stack.of(this.asset).stackName}'. ` +
'Create a new Code instance for every stack.');
throw new ValidationError(`Asset is already associated with another stack '${cdk.Stack.of(this.asset).stackName}'. ` +
'Create a new Code instance for every stack.', scope);
}

if (!this.asset.isZipArchive) {
throw new Error(`Asset must be a .zip file or a directory (${this.path})`);
throw new ValidationError(`Asset must be a .zip file or a directory (${this.path})`, scope);
}

return {
Expand All @@ -385,7 +386,7 @@ export class AssetCode extends Code {

public bindToResource(resource: cdk.CfnResource, options: ResourceBindOptions = { }) {
if (!this.asset) {
throw new Error('bindToResource() must be called after bind()');
throw new ValidationError('bindToResource() must be called after bind()', resource);
}

const resourceProperty = options.resourceProperty || 'Code';
Expand Down Expand Up @@ -497,15 +498,15 @@ export class CfnParametersCode extends Code {
if (this._bucketNameParam) {
return this._bucketNameParam.logicalId;
} else {
throw new Error('Pass CfnParametersCode to a Lambda Function before accessing the bucketNameParam property');
throw new UnscopedValidationError('Pass CfnParametersCode to a Lambda Function before accessing the bucketNameParam property');
}
}

public get objectKeyParam(): string {
if (this._objectKeyParam) {
return this._objectKeyParam.logicalId;
} else {
throw new Error('Pass CfnParametersCode to a Lambda Function before accessing the objectKeyParam property');
throw new UnscopedValidationError('Pass CfnParametersCode to a Lambda Function before accessing the objectKeyParam property');
}
}
}
Expand Down Expand Up @@ -628,8 +629,8 @@ export class AssetImageCode extends Code {
});
this.asset.repository.grantPull(new iam.ServicePrincipal('lambda.amazonaws.com'));
} else if (cdk.Stack.of(this.asset) !== cdk.Stack.of(scope)) {
throw new Error(`Asset is already associated with another stack '${cdk.Stack.of(this.asset).stackName}'. ` +
'Create a new Code instance for every stack.');
throw new ValidationError(`Asset is already associated with another stack '${cdk.Stack.of(this.asset).stackName}'. ` +
'Create a new Code instance for every stack.', scope);
}

return {
Expand All @@ -644,7 +645,7 @@ export class AssetImageCode extends Code {

public bindToResource(resource: cdk.CfnResource, options: ResourceBindOptions = { }) {
if (!this.asset) {
throw new Error('bindToResource() must be called after bind()');
throw new ValidationError('bindToResource() must be called after bind()', resource);
}

const resourceProperty = options.resourceProperty || 'Code.ImageUri';
Expand Down
5 changes: 3 additions & 2 deletions packages/aws-cdk-lib/aws-lambda/lib/event-invoke-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { DestinationType, IDestination } from './destination';
import { IFunction } from './function-base';
import { CfnEventInvokeConfig } from './lambda.generated';
import { Duration, Resource } from '../../core';
import { ValidationError } from '../../core/lib/errors';

/**
* Options to add an EventInvokeConfig to a function.
Expand Down Expand Up @@ -74,11 +75,11 @@ export class EventInvokeConfig extends Resource {
super(scope, id);

if (props.maxEventAge && (props.maxEventAge.toSeconds() < 60 || props.maxEventAge.toSeconds() > 21600)) {
throw new Error('`maximumEventAge` must represent a `Duration` that is between 60 and 21600 seconds.');
throw new ValidationError('`maximumEventAge` must represent a `Duration` that is between 60 and 21600 seconds.', this);
}

if (props.retryAttempts && (props.retryAttempts < 0 || props.retryAttempts > 2)) {
throw new Error('`retryAttempts` must be between 0 and 2.');
throw new ValidationError('`retryAttempts` must be between 0 and 2.', this);
}

new CfnEventInvokeConfig(this, 'Resource', {
Expand Down
35 changes: 18 additions & 17 deletions packages/aws-cdk-lib/aws-lambda/lib/event-source-mapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { CfnEventSourceMapping } from './lambda.generated';
import * as iam from '../../aws-iam';
import { IKey } from '../../aws-kms';
import * as cdk from '../../core';
import { ValidationError } from '../../core/lib/errors';

/**
* The type of authentication protocol or the VPC components for your event source's SourceAccessConfiguration
Expand Down Expand Up @@ -402,78 +403,78 @@ export class EventSourceMapping extends cdk.Resource implements IEventSourceMapp
super(scope, id);

if (props.eventSourceArn == undefined && props.kafkaBootstrapServers == undefined) {
throw new Error('Either eventSourceArn or kafkaBootstrapServers must be set');
throw new ValidationError('Either eventSourceArn or kafkaBootstrapServers must be set', this);
}

if (props.eventSourceArn !== undefined && props.kafkaBootstrapServers !== undefined) {
throw new Error('eventSourceArn and kafkaBootstrapServers are mutually exclusive');
throw new ValidationError('eventSourceArn and kafkaBootstrapServers are mutually exclusive', this);
}

if (props.provisionedPollerConfig) {
const { minimumPollers, maximumPollers } = props.provisionedPollerConfig;
if (minimumPollers != undefined) {
if (minimumPollers < 1 || minimumPollers > 200) {
throw new Error('Minimum provisioned pollers must be between 1 and 200 inclusive');
throw new ValidationError('Minimum provisioned pollers must be between 1 and 200 inclusive', this);
}
}
if (maximumPollers != undefined) {
if (maximumPollers < 1 || maximumPollers > 2000) {
throw new Error('Maximum provisioned pollers must be between 1 and 2000 inclusive');
throw new ValidationError('Maximum provisioned pollers must be between 1 and 2000 inclusive', this);
}
}
if (minimumPollers != undefined && maximumPollers != undefined) {
if (minimumPollers > maximumPollers) {
throw new Error('Minimum provisioned pollers must be less than or equal to maximum provisioned pollers');
throw new ValidationError('Minimum provisioned pollers must be less than or equal to maximum provisioned pollers', this);
}
}
}

if (props.kafkaBootstrapServers && (props.kafkaBootstrapServers?.length < 1)) {
throw new Error('kafkaBootStrapServers must not be empty if set');
throw new ValidationError('kafkaBootStrapServers must not be empty if set', this);
}

if (props.maxBatchingWindow && props.maxBatchingWindow.toSeconds() > 300) {
throw new Error(`maxBatchingWindow cannot be over 300 seconds, got ${props.maxBatchingWindow.toSeconds()}`);
throw new ValidationError(`maxBatchingWindow cannot be over 300 seconds, got ${props.maxBatchingWindow.toSeconds()}`, this);
}

if (props.maxConcurrency && !cdk.Token.isUnresolved(props.maxConcurrency) && (props.maxConcurrency < 2 || props.maxConcurrency > 1000)) {
throw new Error('maxConcurrency must be between 2 and 1000 concurrent instances');
throw new ValidationError('maxConcurrency must be between 2 and 1000 concurrent instances', this);
}

if (props.maxRecordAge && (props.maxRecordAge.toSeconds() < 60 || props.maxRecordAge.toDays({ integral: false }) > 7)) {
throw new Error('maxRecordAge must be between 60 seconds and 7 days inclusive');
throw new ValidationError('maxRecordAge must be between 60 seconds and 7 days inclusive', this);
}

props.retryAttempts !== undefined && cdk.withResolved(props.retryAttempts, (attempts) => {
if (attempts < 0 || attempts > 10000) {
throw new Error(`retryAttempts must be between 0 and 10000 inclusive, got ${attempts}`);
throw new ValidationError(`retryAttempts must be between 0 and 10000 inclusive, got ${attempts}`, this);
}
});

props.parallelizationFactor !== undefined && cdk.withResolved(props.parallelizationFactor, (factor) => {
if (factor < 1 || factor > 10) {
throw new Error(`parallelizationFactor must be between 1 and 10 inclusive, got ${factor}`);
throw new ValidationError(`parallelizationFactor must be between 1 and 10 inclusive, got ${factor}`, this);
}
});

if (props.tumblingWindow && !cdk.Token.isUnresolved(props.tumblingWindow) && props.tumblingWindow.toSeconds() > 900) {
throw new Error(`tumblingWindow cannot be over 900 seconds, got ${props.tumblingWindow.toSeconds()}`);
throw new ValidationError(`tumblingWindow cannot be over 900 seconds, got ${props.tumblingWindow.toSeconds()}`, this);
}

if (props.startingPosition === StartingPosition.AT_TIMESTAMP && !props.startingPositionTimestamp) {
throw new Error('startingPositionTimestamp must be provided when startingPosition is AT_TIMESTAMP');
throw new ValidationError('startingPositionTimestamp must be provided when startingPosition is AT_TIMESTAMP', this);
}

if (props.startingPosition !== StartingPosition.AT_TIMESTAMP && props.startingPositionTimestamp) {
throw new Error('startingPositionTimestamp can only be used when startingPosition is AT_TIMESTAMP');
throw new ValidationError('startingPositionTimestamp can only be used when startingPosition is AT_TIMESTAMP', this);
}

if (props.kafkaConsumerGroupId) {
this.validateKafkaConsumerGroupIdOrThrow(props.kafkaConsumerGroupId);
}

if (props.filterEncryption !== undefined && props.filters == undefined) {
throw new Error('filter criteria must be provided to enable setting filter criteria encryption');
throw new ValidationError('filter criteria must be provided to enable setting filter criteria encryption', this);
}

/**
Expand Down Expand Up @@ -540,13 +541,13 @@ export class EventSourceMapping extends cdk.Resource implements IEventSourceMapp
}

if (kafkaConsumerGroupId.length > 200 || kafkaConsumerGroupId.length < 1) {
throw new Error('kafkaConsumerGroupId must be a valid string between 1 and 200 characters');
throw new ValidationError('kafkaConsumerGroupId must be a valid string between 1 and 200 characters', this);
}

const regex = new RegExp(/[a-zA-Z0-9-\/*:_+=.@-]*/);
const patternMatch = regex.exec(kafkaConsumerGroupId);
if (patternMatch === null || patternMatch[0] !== kafkaConsumerGroupId) {
throw new Error('kafkaConsumerGroupId contains invalid characters. Allowed values are "[a-zA-Z0-9-\/*:_+=.@-]"');
throw new ValidationError('kafkaConsumerGroupId contains invalid characters. Allowed values are "[a-zA-Z0-9-\/*:_+=.@-]"', this);
}
}
}
Expand Down
Loading
Loading