diff --git a/packages/aws-cdk-lib/.eslintrc.js b/packages/aws-cdk-lib/.eslintrc.js index f77ca351aac5d..3bfb1f9dedb7b 100644 --- a/packages/aws-cdk-lib/.eslintrc.js +++ b/packages/aws-cdk-lib/.eslintrc.js @@ -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'] }, diff --git a/packages/aws-cdk-lib/aws-lambda/lib/adot-layers.ts b/packages/aws-cdk-lib/aws-lambda/lib/adot-layers.ts index fd6257aa7b73a..91a3990d8e395 100644 --- a/packages/aws-cdk-lib/aws-lambda/lib/adot-layers.ts +++ b/packages/aws-cdk-lib/aws-lambda/lib/adot-layers.ts @@ -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'; @@ -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; diff --git a/packages/aws-cdk-lib/aws-lambda/lib/alias.ts b/packages/aws-cdk-lib/aws-lambda/lib/alias.ts index fbfa249286c99..ce5b2342e0dda 100644 --- a/packages/aws-cdk-lib/aws-lambda/lib/alias.ts +++ b/packages/aws-cdk-lib/aws-lambda/lib/alias.ts @@ -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 { /** @@ -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, @@ -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); } } @@ -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 }; diff --git a/packages/aws-cdk-lib/aws-lambda/lib/code-signing-config.ts b/packages/aws-cdk-lib/aws-lambda/lib/code-signing-config.ts index 573a198f16955..7230f49aa73bc 100644 --- a/packages/aws-cdk-lib/aws-lambda/lib/code-signing-config.ts +++ b/packages/aws-cdk-lib/aws-lambda/lib/code-signing-config.ts @@ -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. @@ -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::lambda:::code-signing-config:', got: '${codeSigningConfigArn}'`); + throw new ValidationError(`Code signing config ARN must be in the format 'arn::lambda:::code-signing-config:', got: '${codeSigningConfigArn}'`, scope); } const assertedCodeSigningProfileId = codeSigningProfileId; class Import extends Resource implements ICodeSigningConfig { diff --git a/packages/aws-cdk-lib/aws-lambda/lib/code.ts b/packages/aws-cdk-lib/aws-lambda/lib/code.ts index 83991d2d613bf..66e2a26532540 100644 --- a/packages/aws-cdk-lib/aws-lambda/lib/code.ts +++ b/packages/aws-cdk-lib/aws-lambda/lib/code.ts @@ -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. @@ -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]; @@ -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); @@ -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; @@ -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; @@ -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'); } } @@ -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 { @@ -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'; @@ -497,7 +498,7 @@ 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'); } } @@ -505,7 +506,7 @@ export class CfnParametersCode extends Code { 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'); } } } @@ -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 { @@ -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'; diff --git a/packages/aws-cdk-lib/aws-lambda/lib/event-invoke-config.ts b/packages/aws-cdk-lib/aws-lambda/lib/event-invoke-config.ts index 1393ce6328b1e..23af087bb9fe6 100644 --- a/packages/aws-cdk-lib/aws-lambda/lib/event-invoke-config.ts +++ b/packages/aws-cdk-lib/aws-lambda/lib/event-invoke-config.ts @@ -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. @@ -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', { diff --git a/packages/aws-cdk-lib/aws-lambda/lib/event-source-mapping.ts b/packages/aws-cdk-lib/aws-lambda/lib/event-source-mapping.ts index 2e28bc5b5cf43..1e5243d2dbf06 100644 --- a/packages/aws-cdk-lib/aws-lambda/lib/event-source-mapping.ts +++ b/packages/aws-cdk-lib/aws-lambda/lib/event-source-mapping.ts @@ -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 @@ -402,70 +403,70 @@ 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) { @@ -473,7 +474,7 @@ export class EventSourceMapping extends cdk.Resource implements IEventSourceMapp } 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); } /** @@ -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); } } } diff --git a/packages/aws-cdk-lib/aws-lambda/lib/function-base.ts b/packages/aws-cdk-lib/aws-lambda/lib/function-base.ts index 5cfdad63085c7..8a09a3e7a4932 100644 --- a/packages/aws-cdk-lib/aws-lambda/lib/function-base.ts +++ b/packages/aws-cdk-lib/aws-lambda/lib/function-base.ts @@ -14,6 +14,7 @@ import * as cloudwatch from '../../aws-cloudwatch'; import * as ec2 from '../../aws-ec2'; import * as iam from '../../aws-iam'; import { Annotations, ArnFormat, IResource, Resource, Token, Stack } from '../../core'; +import { ValidationError } from '../../core/lib/errors'; export interface IFunction extends IResource, ec2.IConnectable, iam.IGrantable { @@ -401,7 +402,7 @@ export abstract class FunctionBase extends Resource implements IFunction, ec2.IC public get connections(): ec2.Connections { if (!this._connections) { // eslint-disable-next-line max-len - throw new Error('Only VPC-associated Lambda Functions have security groups to manage. Supply the "vpc" parameter when creating the Lambda, or "securityGroupId" when importing it.'); + throw new ValidationError('Only VPC-associated Lambda Functions have security groups to manage. Supply the "vpc" parameter when creating the Lambda, or "securityGroupId" when importing it.', this); } return this._connections; } @@ -514,7 +515,7 @@ export abstract class FunctionBase extends Resource implements IFunction, ec2.IC public configureAsyncInvoke(options: EventInvokeConfigOptions): void { if (this.node.tryFindChild('EventInvokeConfig') !== undefined) { - throw new Error(`An EventInvokeConfig has already been configured for the function at ${this.node.path}`); + throw new ValidationError(`An EventInvokeConfig has already been configured for the function at ${this.node.path}`, this); } new EventInvokeConfig(this, 'EventInvokeConfig', { @@ -585,9 +586,9 @@ export abstract class FunctionBase extends Resource implements IFunction, ec2.IC const permissionNode = this._functionNode().tryFindChild(identifier); if (!permissionNode && !this._skipPermissions) { - throw new Error('Cannot modify permission to lambda function. Function is either imported or $LATEST version.\n' + throw new ValidationError('Cannot modify permission to lambda function. Function is either imported or $LATEST version.\n' + 'If the function is imported from the same account use `fromFunctionAttributes()` API with the `sameEnvironment` flag.\n' - + 'If the function is imported from a different account and already has the correct permissions use `fromFunctionAttributes()` API with the `skipPermissions` flag.'); + + 'If the function is imported from a different account and already has the correct permissions use `fromFunctionAttributes()` API with the `skipPermissions` flag.', this); } return { statementAdded: true, policyDependable: permissionNode }; }, @@ -653,8 +654,8 @@ export abstract class FunctionBase extends Resource implements IFunction, ec2.IC } } - throw new Error(`Invalid principal type for Lambda permission statement: ${principal.constructor.name}. ` + - 'Supported: AccountPrincipal, ArnPrincipal, ServicePrincipal, OrganizationPrincipal'); + throw new ValidationError(`Invalid principal type for Lambda permission statement: ${principal.constructor.name}. ` + + 'Supported: AccountPrincipal, ArnPrincipal, ServicePrincipal, OrganizationPrincipal', this); /** * Returns the value at the key if the object contains the key and nothing else. Otherwise, @@ -683,7 +684,7 @@ export abstract class FunctionBase extends Resource implements IFunction, ec2.IC // PrincipalOrgID cannot be combined with any other conditions if (principalOrgID && (sourceArn || sourceAccount)) { - throw new Error('PrincipalWithConditions had unsupported condition combinations for Lambda permission statement: principalOrgID cannot be set with other conditions.'); + throw new ValidationError('PrincipalWithConditions had unsupported condition combinations for Lambda permission statement: principalOrgID cannot be set with other conditions.', this); } return { @@ -725,8 +726,8 @@ export abstract class FunctionBase extends Resource implements IFunction, ec2.IC if (unsupportedConditions.length == 0) { return conditions; } else { - throw new Error(`PrincipalWithConditions had unsupported conditions for Lambda permission statement: ${JSON.stringify(unsupportedConditions)}. ` + - `Supported operator/condition pairs: ${JSON.stringify(supportedPrincipalConditions)}`); + throw new ValidationError(`PrincipalWithConditions had unsupported conditions for Lambda permission statement: ${JSON.stringify(unsupportedConditions)}. ` + + `Supported operator/condition pairs: ${JSON.stringify(supportedPrincipalConditions)}`, this); } } @@ -761,7 +762,7 @@ export abstract class QualifiedFunctionBase extends FunctionBase { public configureAsyncInvoke(options: EventInvokeConfigOptions): void { if (this.node.tryFindChild('EventInvokeConfig') !== undefined) { - throw new Error(`An EventInvokeConfig has already been configured for the qualified function at ${this.node.path}`); + throw new ValidationError(`An EventInvokeConfig has already been configured for the qualified function at ${this.node.path}`, this); } new EventInvokeConfig(this, 'EventInvokeConfig', { @@ -817,7 +818,7 @@ class LatestVersion extends FunctionBase implements IVersion { } public get edgeArn(): never { - throw new Error('$LATEST function version cannot be used for Lambda@Edge'); + throw new ValidationError('$LATEST function version cannot be used for Lambda@Edge', this); } public get resourceArnsForGrantInvoke() { diff --git a/packages/aws-cdk-lib/aws-lambda/lib/function-hash.ts b/packages/aws-cdk-lib/aws-lambda/lib/function-hash.ts index b45072d8d5668..fbc923920c89b 100644 --- a/packages/aws-cdk-lib/aws-lambda/lib/function-hash.ts +++ b/packages/aws-cdk-lib/aws-lambda/lib/function-hash.ts @@ -1,6 +1,7 @@ import { Function as LambdaFunction } from './function'; import { ILayerVersion } from './layers'; import { CfnResource, FeatureFlags, Stack, Token } from '../../core'; +import { ValidationError } from '../../core/lib/errors'; import { md5hash } from '../../core/lib/helpers-internal'; import { LAMBDA_RECOGNIZE_LAYER_VERSION, LAMBDA_RECOGNIZE_VERSION_PROPS } from '../../cx-api'; @@ -12,7 +13,7 @@ export function calculateFunctionHash(fn: LambdaFunction, additional: string = ' let stringifiedConfig; if (FeatureFlags.of(fn).isEnabled(LAMBDA_RECOGNIZE_VERSION_PROPS)) { - const updatedProps = sortFunctionProperties(filterUsefulKeys(properties)); + const updatedProps = sortFunctionProperties(filterUsefulKeys(properties, fn)); stringifiedConfig = JSON.stringify(updatedProps); } else { const sorted = sortFunctionProperties(properties); @@ -78,14 +79,14 @@ export const VERSION_LOCKED: { [key: string]: boolean } = { Tags: false, }; -function filterUsefulKeys(properties: any) { +function filterUsefulKeys(properties: any, fn: LambdaFunction) { const versionProps = { ...VERSION_LOCKED, ...LambdaFunction._VER_PROPS }; const unclassified = Object.entries(properties) .filter(([k, v]) => v != null && !Object.keys(versionProps).includes(k)) .map(([k, _]) => k); if (unclassified.length > 0) { - throw new Error(`The following properties are not recognized as version properties: [${unclassified}].` - + ' See the README of the aws-lambda module to learn more about this and to fix it.'); + throw new ValidationError(`The following properties are not recognized as version properties: [${unclassified}].` + + ' See the README of the aws-lambda module to learn more about this and to fix it.', fn); } const notLocked = Object.entries(versionProps).filter(([_, v]) => !v).map(([k, _]) => k); notLocked.forEach(p => delete properties[p]); @@ -202,7 +203,7 @@ function resolveSingleResourceProperties(stack: Stack, res: CfnResource): any { const resources = template.Resources; const resourceKeys = Object.keys(resources); if (resourceKeys.length !== 1) { - throw new Error(`Expected one rendered CloudFormation resource but found ${resourceKeys.length}`); + throw new ValidationError(`Expected one rendered CloudFormation resource but found ${resourceKeys.length}`, res); } const logicalId = resourceKeys[0]; return { properties: resources[logicalId].Properties, template, logicalId }; diff --git a/packages/aws-cdk-lib/aws-lambda/lib/function-url.ts b/packages/aws-cdk-lib/aws-lambda/lib/function-url.ts index 288c425232003..85393cbcf6e6b 100644 --- a/packages/aws-cdk-lib/aws-lambda/lib/function-url.ts +++ b/packages/aws-cdk-lib/aws-lambda/lib/function-url.ts @@ -5,6 +5,7 @@ import { IVersion } from './lambda-version'; import { CfnUrl } from './lambda.generated'; import * as iam from '../../aws-iam'; import { Duration, IResource, Resource } from '../../core'; +import { ValidationError } from '../../core/lib/errors'; /** * The auth types for a function url @@ -220,7 +221,7 @@ export class FunctionUrl extends Resource implements IFunctionUrl { super(scope, id); if (this.instanceOfVersion(props.function)) { - throw new Error('FunctionUrl cannot be used with a Version'); + throw new ValidationError('FunctionUrl cannot be used with a Version', this); } // If the target function is an alias, then it must be configured using the underlying function @@ -271,7 +272,7 @@ export class FunctionUrl extends Resource implements IFunctionUrl { private renderCors(cors: FunctionUrlCorsOptions): CfnUrl.CorsProperty { if (cors.maxAge && !cors.maxAge.isUnresolved() && cors.maxAge.toSeconds() > 86400) { - throw new Error(`FunctionUrl CORS maxAge should be less than or equal to 86400 secs (got ${cors.maxAge.toSeconds()})`); + throw new ValidationError(`FunctionUrl CORS maxAge should be less than or equal to 86400 secs (got ${cors.maxAge.toSeconds()})`, this); } return { diff --git a/packages/aws-cdk-lib/aws-lambda/lib/function.ts b/packages/aws-cdk-lib/aws-lambda/lib/function.ts index 68dc147ac5d3b..c8055d157d5c8 100644 --- a/packages/aws-cdk-lib/aws-lambda/lib/function.ts +++ b/packages/aws-cdk-lib/aws-lambda/lib/function.ts @@ -30,7 +30,7 @@ import * as logs from '../../aws-logs'; import * as sns from '../../aws-sns'; import * as sqs from '../../aws-sqs'; import { Annotations, ArnFormat, CfnResource, Duration, FeatureFlags, Fn, IAspect, Lazy, Names, Size, Stack, Token } from '../../core'; -import { ValidationError } from '../../core/lib/errors'; +import { UnscopedValidationError, ValidationError } from '../../core/lib/errors'; import { LAMBDA_RECOGNIZE_LAYER_VERSION } from '../../cx-api'; /** @@ -1753,20 +1753,20 @@ export function verifyCodeConfig(code: CodeConfig, props: FunctionProps) { const codeType = [code.inlineCode, code.s3Location, code.image]; if (codeType.filter(x => !!x).length !== 1) { - throw new Error('lambda.Code must specify exactly one of: "inlineCode", "s3Location", or "image"'); + throw new UnscopedValidationError('lambda.Code must specify exactly one of: "inlineCode", "s3Location", or "image"'); } if (!!code.image === (props.handler !== Handler.FROM_IMAGE)) { - throw new Error('handler must be `Handler.FROM_IMAGE` when using image asset for Lambda function'); + throw new UnscopedValidationError('handler must be `Handler.FROM_IMAGE` when using image asset for Lambda function'); } if (!!code.image === (props.runtime !== Runtime.FROM_IMAGE)) { - throw new Error('runtime must be `Runtime.FROM_IMAGE` when using image asset for Lambda function'); + throw new UnscopedValidationError('runtime must be `Runtime.FROM_IMAGE` when using image asset for Lambda function'); } // if this is inline code, check that the runtime supports if (code.inlineCode && !props.runtime.supportsInlineCode) { - throw new Error(`Inline source not allowed for ${props.runtime!.name}`); + throw new UnscopedValidationError(`Inline source not allowed for ${props.runtime!.name}`); } } diff --git a/packages/aws-cdk-lib/aws-lambda/lib/lambda-insights.ts b/packages/aws-cdk-lib/aws-lambda/lib/lambda-insights.ts index 518ba420b9077..de02d3e4a6cee 100644 --- a/packages/aws-cdk-lib/aws-lambda/lib/lambda-insights.ts +++ b/packages/aws-cdk-lib/aws-lambda/lib/lambda-insights.ts @@ -2,6 +2,7 @@ import { Construct, IConstruct } from 'constructs'; import { Architecture } from './architecture'; import { IFunction } from './function-base'; import { Lazy, Stack, Token } from '../../core'; +import { ValidationError } from '../../core/lib/errors'; import { FactName, RegionInfo } from '../../region-info'; /** @@ -112,16 +113,16 @@ export abstract class LambdaInsightsVersion { produce: (context) => getVersionArn(context.scope, insightsVersion), }); - public _bind(_scope: Construct, _function: IFunction): InsightsBindConfig { - const arch = _function.architecture?.name ?? Architecture.X86_64.name; + public _bind(scope: Construct, fn: IFunction): InsightsBindConfig { + const arch = fn.architecture?.name ?? Architecture.X86_64.name; // Check if insights version is valid. This should only happen if one of the public static readonly versions are set incorrectly // or if the version is not available for the Lambda Architecture const versionExists = RegionInfo.regions.some(regionInfo => regionInfo.cloudwatchLambdaInsightsArn(insightsVersion, arch)); if (!versionExists) { - throw new Error(`Insights version ${insightsVersion} does not exist.`); + throw new ValidationError(`Insights version ${insightsVersion} does not exist.`, fn); } return { - arn: getVersionArn(_scope, insightsVersion, arch), + arn: getVersionArn(scope, insightsVersion, arch), }; } } @@ -157,7 +158,7 @@ function getVersionArn(scope: IConstruct, insightsVersion: string, architecture? if (region !== undefined && !Token.isUnresolved(region)) { const arn = RegionInfo.get(region).cloudwatchLambdaInsightsArn(insightsVersion, arch); if (arn === undefined) { - throw new Error(`Insights version ${insightsVersion} is not supported in region ${region}`); + throw new ValidationError(`Insights version ${insightsVersion} is not supported in region ${region}`, scope); } return arn; } diff --git a/packages/aws-cdk-lib/aws-lambda/lib/lambda-version.ts b/packages/aws-cdk-lib/aws-lambda/lib/lambda-version.ts index f0ad496b72017..81ab1ae18ce56 100644 --- a/packages/aws-cdk-lib/aws-lambda/lib/lambda-version.ts +++ b/packages/aws-cdk-lib/aws-lambda/lib/lambda-version.ts @@ -8,6 +8,7 @@ import { CfnVersion } from './lambda.generated'; import { addAlias } from './util'; import * as cloudwatch from '../../aws-cloudwatch'; import { Fn, Lazy, RemovalPolicy, Token } from '../../core'; +import { ValidationError } from '../../core/lib/errors'; export interface IVersion extends IFunction { /** @@ -143,7 +144,7 @@ export class Version extends QualifiedFunctionBase implements IVersion { public get edgeArn(): string { if (version === '$LATEST') { - throw new Error('$LATEST function version cannot be used for Lambda@Edge'); + throw new ValidationError('$LATEST function version cannot be used for Lambda@Edge', this); } return this.functionArn; } @@ -170,7 +171,7 @@ export class Version extends QualifiedFunctionBase implements IVersion { public get edgeArn(): string { if (attrs.version === '$LATEST') { - throw new Error('$LATEST function version cannot be used for Lambda@Edge'); + throw new ValidationError('$LATEST function version cannot be used for Lambda@Edge', this); } return this.functionArn; } @@ -256,7 +257,7 @@ export class Version extends QualifiedFunctionBase implements IVersion { public get edgeArn(): string { // Validate first that this version can be used for Lambda@Edge if (this.version === '$LATEST') { - throw new Error('$LATEST function version cannot be used for Lambda@Edge'); + throw new ValidationError('$LATEST function version cannot be used for Lambda@Edge', this); } // Check compatibility at synthesis. It could be that the version was associated @@ -284,7 +285,7 @@ export class Version extends QualifiedFunctionBase implements IVersion { } 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 }; diff --git a/packages/aws-cdk-lib/aws-lambda/lib/layers.ts b/packages/aws-cdk-lib/aws-lambda/lib/layers.ts index 8fa00ec929949..a0590ed34a550 100644 --- a/packages/aws-cdk-lib/aws-lambda/lib/layers.ts +++ b/packages/aws-cdk-lib/aws-lambda/lib/layers.ts @@ -4,6 +4,7 @@ import { Code } from './code'; import { CfnLayerVersion, CfnLayerVersionPermission } from './lambda.generated'; import { Runtime } from './runtime'; import { IResource, RemovalPolicy, Resource } from '../../core'; +import { ValidationError } from '../../core/lib/errors'; /** * Non runtime options @@ -98,7 +99,7 @@ abstract class LayerVersionBase extends Resource implements ILayerVersion { public addPermission(id: string, permission: LayerVersionPermission) { if (permission.organizationId != null && permission.accountId !== '*') { - throw new Error(`OrganizationId can only be specified if AwsAccountId is '*', but it is ${permission.accountId}`); + throw new ValidationError(`OrganizationId can only be specified if AwsAccountId is '*', but it is ${permission.accountId}`, this); } new CfnLayerVersionPermission(this, id, { @@ -167,7 +168,7 @@ export class LayerVersion extends LayerVersionBase { */ public static fromLayerVersionAttributes(scope: Construct, id: string, attrs: LayerVersionAttributes): ILayerVersion { if (attrs.compatibleRuntimes && attrs.compatibleRuntimes.length === 0) { - throw new Error('Attempted to import a Lambda layer that supports no runtime!'); + throw new ValidationError('Attempted to import a Lambda layer that supports no runtime!', scope); } class Import extends LayerVersionBase { @@ -187,16 +188,16 @@ export class LayerVersion extends LayerVersionBase { }); if (props.compatibleRuntimes && props.compatibleRuntimes.length === 0) { - throw new Error('Attempted to define a Lambda layer that supports no runtime!'); + throw new ValidationError('Attempted to define a Lambda layer that supports no runtime!', this); } // Allow usage of the code in this context... const code = props.code.bind(this); if (code.inlineCode) { - throw new Error('Inline code is not supported for AWS Lambda layers'); + throw new ValidationError('Inline code is not supported for AWS Lambda layers', this); } if (!code.s3Location) { - throw new Error('Code must define an S3 location'); + throw new ValidationError('Code must define an S3 location', this); } const resource: CfnLayerVersion = new CfnLayerVersion(this, 'Resource', { diff --git a/packages/aws-cdk-lib/aws-lambda/lib/params-and-secrets-layers.ts b/packages/aws-cdk-lib/aws-lambda/lib/params-and-secrets-layers.ts index 38b5e1fca73b8..4f19085910b05 100644 --- a/packages/aws-cdk-lib/aws-lambda/lib/params-and-secrets-layers.ts +++ b/packages/aws-cdk-lib/aws-lambda/lib/params-and-secrets-layers.ts @@ -1,6 +1,7 @@ import { Construct, IConstruct } from 'constructs'; import { IFunction } from './function-base'; import { Token, Stack, Duration } from '../../core'; +import { ValidationError } from '../../core/lib/errors'; import { RegionInfo, FactName } from '../../region-info'; /** @@ -161,10 +162,10 @@ export abstract class ParamsAndSecretsLayerVersion { */ public static fromVersionArn(arn: string, options: ParamsAndSecretsOptions = {}): ParamsAndSecretsLayerVersion { return new (class extends ParamsAndSecretsLayerVersion { - public _bind(_scope: Construct, _fn: IFunction): ParamsAndSecretsBindConfig { + public _bind(_scope: Construct, fn: IFunction): ParamsAndSecretsBindConfig { return { arn, - environmentVars: this.environmentVariablesFromOptions, + environmentVars: this.environmentVariablesFromOptions(fn), }; } })(options); @@ -178,7 +179,7 @@ export abstract class ParamsAndSecretsLayerVersion { public _bind(scope: Construct, fn: IFunction): ParamsAndSecretsBindConfig { return { arn: this.getVersionArn(scope, version, fn.architecture.name), - environmentVars: this.environmentVariablesFromOptions, + environmentVars: this.environmentVariablesFromOptions(fn), }; } })(options); @@ -196,26 +197,26 @@ export abstract class ParamsAndSecretsLayerVersion { /** * Configure environment variables for Parameters and Secrets Extension based on configuration options */ - private get environmentVariablesFromOptions(): { [key: string]: any } { + private environmentVariablesFromOptions(scope: Construct): { [key: string]: any } { if (this.options.cacheSize !== undefined && (this.options.cacheSize < 0 || this.options.cacheSize > 1000)) { - throw new Error(`Cache size must be between 0 and 1000 inclusive - provided: ${this.options.cacheSize}`); + throw new ValidationError(`Cache size must be between 0 and 1000 inclusive - provided: ${this.options.cacheSize}`, scope); } if (this.options.httpPort !== undefined && (this.options.httpPort < 1 || this.options.httpPort > 65535)) { - throw new Error(`HTTP port must be between 1 and 65535 inclusive - provided: ${this.options.httpPort}`); + throw new ValidationError(`HTTP port must be between 1 and 65535 inclusive - provided: ${this.options.httpPort}`, scope); } // max connections has no maximum limit if (this.options.maxConnections !== undefined && this.options.maxConnections < 1) { - throw new Error(`Maximum connections must be at least 1 - provided: ${this.options.maxConnections}`); + throw new ValidationError(`Maximum connections must be at least 1 - provided: ${this.options.maxConnections}`, scope); } if (this.options.secretsManagerTtl !== undefined && this.options.secretsManagerTtl.toSeconds() > 300) { - throw new Error(`Maximum TTL for a cached secret is 300 seconds - provided: ${this.options.secretsManagerTtl.toSeconds()} seconds`); + throw new ValidationError(`Maximum TTL for a cached secret is 300 seconds - provided: ${this.options.secretsManagerTtl.toSeconds()} seconds`, scope); } if (this.options.parameterStoreTtl !== undefined && this.options.parameterStoreTtl.toSeconds() > 300) { - throw new Error(`Maximum TTL for a cached parameter is 300 seconds - provided: ${this.options.parameterStoreTtl.toSeconds()} seconds`); + throw new ValidationError(`Maximum TTL for a cached parameter is 300 seconds - provided: ${this.options.parameterStoreTtl.toSeconds()} seconds`, scope); } return { @@ -245,7 +246,7 @@ export abstract class ParamsAndSecretsLayerVersion { if (region !== undefined && !Token.isUnresolved(region)) { const layerArn = RegionInfo.get(region).paramsAndSecretsLambdaLayerArn(version, architecture); if (layerArn === undefined) { - throw new Error(`Parameters and Secrets Extension is not supported in region ${region} for ${architecture} architecture`); + throw new ValidationError(`Parameters and Secrets Extension is not supported in region ${region} for ${architecture} architecture`, scope); } return layerArn; } diff --git a/packages/aws-cdk-lib/aws-lambda/lib/private/scalable-function-attribute.ts b/packages/aws-cdk-lib/aws-lambda/lib/private/scalable-function-attribute.ts index 3595631e1182e..d4f6ddfe26943 100644 --- a/packages/aws-cdk-lib/aws-lambda/lib/private/scalable-function-attribute.ts +++ b/packages/aws-cdk-lib/aws-lambda/lib/private/scalable-function-attribute.ts @@ -1,6 +1,7 @@ import { Construct } from 'constructs'; import * as appscaling from '../../../aws-applicationautoscaling'; import { Token } from '../../../core'; +import { ValidationError } from '../../../core/lib/errors'; import { IScalableFunctionAttribute, UtilizationScalingOptions } from '../scalable-attribute-api'; /** @@ -18,7 +19,7 @@ export class ScalableFunctionAttribute extends appscaling.BaseScalableAttribute */ public scaleOnUtilization(options: UtilizationScalingOptions) { if ( !Token.isUnresolved(options.utilizationTarget) && (options.utilizationTarget < 0.1 || options.utilizationTarget > 0.9)) { - throw new Error(`Utilization Target should be between 0.1 and 0.9. Found ${options.utilizationTarget}.`); + throw new ValidationError(`Utilization Target should be between 0.1 and 0.9. Found ${options.utilizationTarget}.`, this); } super.doScaleToTrackMetric('Tracking', { targetValue: options.utilizationTarget,