Skip to content

Commit

Permalink
feat(lambda): throw ValidationError instead of untyped errors (#33033)
Browse files Browse the repository at this point in the history
### Issue 

`aws-lambda` for #32569 

### Description of changes

Updated thrown errors.

### 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
mrgrain authored Jan 21, 2025
1 parent 61e876b commit a928748
Show file tree
Hide file tree
Showing 16 changed files with 105 additions and 91 deletions.
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 {
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

0 comments on commit a928748

Please sign in to comment.