diff --git a/packages/aws-cdk-lib/.eslintrc.js b/packages/aws-cdk-lib/.eslintrc.js index 7525de988518a..ca7a7961c60ee 100644 --- a/packages/aws-cdk-lib/.eslintrc.js +++ b/packages/aws-cdk-lib/.eslintrc.js @@ -26,6 +26,13 @@ const enableNoThrowDefaultErrorIn = [ 'aws-ssmincidents', 'aws-ssmquicksetup', 'aws-synthetics', + 'aws-s3-assets', + 'aws-s3-deployment', + 'aws-s3-notifications', + 'aws-s3express', + 'aws-s3objectlambda', + 'aws-s3outposts', + 'aws-s3tables', ]; baseConfig.overrides.push({ files: enableNoThrowDefaultErrorIn.map(m => `./${m}/lib/**`), diff --git a/packages/aws-cdk-lib/aws-s3-assets/lib/asset.ts b/packages/aws-cdk-lib/aws-s3-assets/lib/asset.ts index 6f07fffa6d9c0..945d45ae1206e 100644 --- a/packages/aws-cdk-lib/aws-s3-assets/lib/asset.ts +++ b/packages/aws-cdk-lib/aws-s3-assets/lib/asset.ts @@ -6,6 +6,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 { ValidationError } from '../../core/lib/errors'; import * as cxapi from '../../cx-api'; export interface AssetOptions extends CopyOptions, cdk.FileCopyOptions, cdk.AssetOptions { @@ -143,7 +144,7 @@ export class Asset extends Construct implements cdk.IAsset { super(scope, id); if (!props.path) { - throw new Error('Asset path cannot be empty'); + throw new ValidationError('Asset path cannot be empty', this); } this.isBundled = props.bundling != null; diff --git a/packages/aws-cdk-lib/aws-s3-assets/lib/compat.ts b/packages/aws-cdk-lib/aws-s3-assets/lib/compat.ts index b5eac2276ed94..35f10c0ba1e72 100644 --- a/packages/aws-cdk-lib/aws-s3-assets/lib/compat.ts +++ b/packages/aws-cdk-lib/aws-s3-assets/lib/compat.ts @@ -1,5 +1,6 @@ import { FollowMode } from '../../assets'; import { SymlinkFollowMode } from '../../core'; +import { UnscopedValidationError } from '../../core/lib/errors'; export function toSymlinkFollow(follow?: FollowMode): SymlinkFollowMode | undefined { if (!follow) { @@ -12,6 +13,6 @@ export function toSymlinkFollow(follow?: FollowMode): SymlinkFollowMode | undefi case FollowMode.BLOCK_EXTERNAL: return SymlinkFollowMode.BLOCK_EXTERNAL; case FollowMode.EXTERNAL: return SymlinkFollowMode.EXTERNAL; default: - throw new Error(`unknown follow mode: ${follow}`); + throw new UnscopedValidationError(`unknown follow mode: ${follow}`); } } diff --git a/packages/aws-cdk-lib/aws-s3-assets/test/custom-synthesis.test.ts b/packages/aws-cdk-lib/aws-s3-assets/test/custom-synthesis.test.ts index b78b52bc131ec..6074ea50b9b93 100644 --- a/packages/aws-cdk-lib/aws-s3-assets/test/custom-synthesis.test.ts +++ b/packages/aws-cdk-lib/aws-s3-assets/test/custom-synthesis.test.ts @@ -7,6 +7,7 @@ import * as path from 'path'; import { Template } from '../../assertions'; import { StackSynthesizer, FileAssetSource, FileAssetLocation, DockerImageAssetSource, DockerImageAssetLocation, ISynthesisSession, App, Stack, AssetManifestBuilder, CfnParameter, CfnResource } from '../../core'; +import { UnscopedValidationError } from '../../core/lib/errors'; import { AssetManifestArtifact } from '../../cx-api'; import { Asset } from '../lib'; @@ -84,7 +85,7 @@ class CustomSynthesizer extends StackSynthesizer { addDockerImageAsset(asset: DockerImageAssetSource): DockerImageAssetLocation { void(asset); - throw new Error('Docker images are not supported here'); + throw new UnscopedValidationError('Docker images are not supported here'); } synthesize(session: ISynthesisSession): void { diff --git a/packages/aws-cdk-lib/aws-s3-deployment/lib/bucket-deployment.ts b/packages/aws-cdk-lib/aws-s3-deployment/lib/bucket-deployment.ts index 4fb7d0b40cae6..9a8348c04a95a 100644 --- a/packages/aws-cdk-lib/aws-s3-deployment/lib/bucket-deployment.ts +++ b/packages/aws-cdk-lib/aws-s3-deployment/lib/bucket-deployment.ts @@ -11,6 +11,7 @@ import * as lambda from '../../aws-lambda'; import * as logs from '../../aws-logs'; import * as s3 from '../../aws-s3'; import * as cdk from '../../core'; +import { ValidationError } from '../../core/lib/errors'; import { BucketDeploymentSingletonFunction } from '../../custom-resource-handlers/dist/aws-s3-deployment/bucket-deployment-provider.generated'; import { AwsCliLayer } from '../../lambda-layer-awscli'; @@ -304,17 +305,17 @@ export class BucketDeployment extends Construct { if (props.distributionPaths) { if (!props.distribution) { - throw new Error('Distribution must be specified if distribution paths are specified'); + throw new ValidationError('Distribution must be specified if distribution paths are specified', this); } if (!cdk.Token.isUnresolved(props.distributionPaths)) { if (!props.distributionPaths.every(distributionPath => cdk.Token.isUnresolved(distributionPath) || distributionPath.startsWith('/'))) { - throw new Error('Distribution paths must start with "/"'); + throw new ValidationError('Distribution paths must start with "/"', this); } } } if (props.useEfs && !props.vpc) { - throw new Error('Vpc must be specified if useEfs is set'); + throw new ValidationError('Vpc must be specified if useEfs is set', this); } this.destinationBucket = props.destinationBucket; @@ -376,7 +377,7 @@ export class BucketDeployment extends Construct { }); const handlerRole = handler.role; - if (!handlerRole) { throw new Error('lambda.SingletonFunction should have created a Role'); } + if (!handlerRole) { throw new ValidationError('lambda.SingletonFunction should have created a Role', this); } this.handlerRole = handlerRole; this.sources = props.sources.map((source: ISource) => source.bind(this, { handlerRole: this.handlerRole })); @@ -455,7 +456,7 @@ export class BucketDeployment extends Construct { // '/this/is/a/random/key/prefix/that/is/a/lot/of/characters/do/we/think/that/it/will/ever/be/this/long?????' // better to throw an error here than wait for CloudFormation to fail if (!cdk.Token.isUnresolved(tagKey) && tagKey.length > 128) { - throw new Error('The BucketDeployment construct requires that the "destinationKeyPrefix" be <=104 characters.'); + throw new ValidationError('The BucketDeployment construct requires that the "destinationKeyPrefix" be <=104 characters.', this); } /* @@ -567,7 +568,7 @@ export class BucketDeployment extends Construct { // configurations since we have a singleton. if (memoryLimit) { if (cdk.Token.isUnresolved(memoryLimit)) { - throw new Error("Can't use tokens when specifying 'memoryLimit' since we use it to identify the singleton custom resource handler."); + throw new ValidationError("Can't use tokens when specifying 'memoryLimit' since we use it to identify the singleton custom resource handler.", this); } uuid += `-${memoryLimit.toString()}MiB`; @@ -578,7 +579,7 @@ export class BucketDeployment extends Construct { // configurations since we have a singleton. if (ephemeralStorageSize) { if (ephemeralStorageSize.isUnresolved()) { - throw new Error("Can't use tokens when specifying 'ephemeralStorageSize' since we use it to identify the singleton custom resource handler."); + throw new ValidationError("Can't use tokens when specifying 'ephemeralStorageSize' since we use it to identify the singleton custom resource handler.", this); } uuid += `-${ephemeralStorageSize.toMebibytes().toString()}MiB`; @@ -660,7 +661,7 @@ export class DeployTimeSubstitutedFile extends BucketDeployment { constructor(scope: Construct, id: string, props: DeployTimeSubstitutedFileProps) { if (!fs.existsSync(props.source)) { - throw new Error(`No file found at 'source' path ${props.source}`); + throw new ValidationError(`No file found at 'source' path ${props.source}`, scope); } // Makes substitutions on the file let fileData = fs.readFileSync(props.source, 'utf-8'); diff --git a/packages/aws-cdk-lib/aws-s3-deployment/lib/render-data.ts b/packages/aws-cdk-lib/aws-s3-deployment/lib/render-data.ts index 28f588d156d97..f04b0a9600cc0 100644 --- a/packages/aws-cdk-lib/aws-s3-deployment/lib/render-data.ts +++ b/packages/aws-cdk-lib/aws-s3-deployment/lib/render-data.ts @@ -1,5 +1,6 @@ import { Construct } from 'constructs'; import { Stack } from '../../core'; +import { ValidationError } from '../../core/lib/errors'; export interface Content { readonly text: string; @@ -22,7 +23,7 @@ export function renderData(scope: Construct, data: string): Content { } if (typeof(obj) !== 'object') { - throw new Error(`Unexpected: after resolve() data must either be a string or a CloudFormation intrinsic. Got: ${JSON.stringify(obj)}`); + throw new ValidationError(`Unexpected: after resolve() data must either be a string or a CloudFormation intrinsic. Got: ${JSON.stringify(obj)}`, scope); } let markerIndex = 0; @@ -35,7 +36,7 @@ export function renderData(scope: Construct, data: string): Content { const parts = fnJoin[1]; if (sep !== '') { - throw new Error(`Unexpected "Fn::Join", expecting separator to be an empty string but got "${sep}"`); + throw new ValidationError(`Unexpected "Fn::Join", expecting separator to be an empty string but got "${sep}"`, scope); } for (const part of parts) { @@ -49,13 +50,13 @@ export function renderData(scope: Construct, data: string): Content { continue; } - throw new Error(`Unexpected "Fn::Join" part, expecting string or object but got ${typeof (part)}`); + throw new ValidationError(`Unexpected "Fn::Join" part, expecting string or object but got ${typeof (part)}`, scope); } } else if (obj.Ref || obj['Fn::GetAtt'] || obj['Fn::Select']) { addMarker(obj); } else { - throw new Error('Unexpected: Expecting `resolve()` to return "Fn::Join", "Ref" or "Fn::GetAtt"'); + throw new ValidationError('Unexpected: Expecting `resolve()` to return "Fn::Join", "Ref" or "Fn::GetAtt"', scope); } function addMarker(part: Ref | GetAtt | FnSelect) { @@ -63,7 +64,7 @@ export function renderData(scope: Construct, data: string): Content { const acceptedCfnFns = ['Ref', 'Fn::GetAtt', 'Fn::Select']; if (keys.length !== 1 || !acceptedCfnFns.includes(keys[0])) { const stringifiedAcceptedCfnFns = acceptedCfnFns.map((fn) => `"${fn}"`).join(' or '); - throw new Error(`Invalid CloudFormation reference. Key must start with any of ${stringifiedAcceptedCfnFns}. Got ${JSON.stringify(part)}`); + throw new ValidationError(`Invalid CloudFormation reference. Key must start with any of ${stringifiedAcceptedCfnFns}. Got ${JSON.stringify(part)}`, scope); } const marker = `<>`; diff --git a/packages/aws-cdk-lib/aws-s3-deployment/lib/source.ts b/packages/aws-cdk-lib/aws-s3-deployment/lib/source.ts index c6815194aa843..857e0092371d7 100644 --- a/packages/aws-cdk-lib/aws-s3-deployment/lib/source.ts +++ b/packages/aws-cdk-lib/aws-s3-deployment/lib/source.ts @@ -6,6 +6,7 @@ import * as iam from '../../aws-iam'; import * as s3 from '../../aws-s3'; import * as s3_assets from '../../aws-s3-assets'; import { FileSystem, Stack } from '../../core'; +import { ValidationError } from '../../core/lib/errors'; /** * Source information. @@ -98,9 +99,9 @@ export class Source { */ public static bucket(bucket: s3.IBucket, zipObjectKey: string): ISource { return { - bind: (_: Construct, context?: DeploymentSourceContext) => { + bind: (scope: Construct, context?: DeploymentSourceContext) => { if (!context) { - throw new Error('To use a Source.bucket(), context must be provided'); + throw new ValidationError('To use a Source.bucket(), context must be provided', scope); } bucket.grantRead(context.handlerRole); @@ -121,7 +122,7 @@ export class Source { return { bind(scope: Construct, context?: DeploymentSourceContext): SourceConfig { if (!context) { - throw new Error('To use a Source.asset(), context must be provided'); + throw new ValidationError('To use a Source.asset(), context must be provided', scope); } let id = 1; @@ -133,7 +134,7 @@ export class Source { ...options, }); if (!asset.isZipArchive) { - throw new Error('Asset path must be either a .zip file or a directory'); + throw new ValidationError('Asset path must be either a .zip file or a directory', scope); } asset.grantRead(context.handlerRole); diff --git a/packages/aws-cdk-lib/aws-s3-deployment/test/bucket-deployment.test.ts b/packages/aws-cdk-lib/aws-s3-deployment/test/bucket-deployment.test.ts index 886102419b05f..8aa5bea01e1f1 100644 --- a/packages/aws-cdk-lib/aws-s3-deployment/test/bucket-deployment.test.ts +++ b/packages/aws-cdk-lib/aws-s3-deployment/test/bucket-deployment.test.ts @@ -9,6 +9,7 @@ import * as logs from '../../aws-logs'; import * as s3 from '../../aws-s3'; import * as sns from '../../aws-sns'; import * as cdk from '../../core'; +import { UnscopedValidationError } from '../../core/lib/errors'; import * as cxapi from '../../cx-api'; import * as s3deploy from '../lib'; @@ -1678,7 +1679,7 @@ function readDataFile(casm: cxapi.CloudAssembly, relativePath: string): string { } } - throw new Error(`File ${relativePath} not found in any of the assets of the assembly`); + throw new UnscopedValidationError(`File ${relativePath} not found in any of the assets of the assembly`); } test('DeployTimeSubstitutedFile allows custom role to be supplied', () => { diff --git a/packages/aws-cdk-lib/aws-s3-notifications/lib/lambda.ts b/packages/aws-cdk-lib/aws-s3-notifications/lib/lambda.ts index e6159ff8c22aa..45a7a44773f63 100644 --- a/packages/aws-cdk-lib/aws-s3-notifications/lib/lambda.ts +++ b/packages/aws-cdk-lib/aws-s3-notifications/lib/lambda.ts @@ -3,6 +3,7 @@ import * as iam from '../../aws-iam'; import * as lambda from '../../aws-lambda'; import * as s3 from '../../aws-s3'; import { CfnResource, Names, Stack } from '../../core'; +import { ValidationError } from '../../core/lib/errors'; /** * Use a Lambda function as a bucket notification destination @@ -11,12 +12,12 @@ export class LambdaDestination implements s3.IBucketNotificationDestination { constructor(private readonly fn: lambda.IFunction) { } - public bind(_scope: Construct, bucket: s3.IBucket): s3.BucketNotificationDestinationConfig { + public bind(scope: Construct, bucket: s3.IBucket): s3.BucketNotificationDestinationConfig { const permissionId = `AllowBucketNotificationsTo${Names.nodeUniqueId(this.fn.permissionsNode)}`; if (!(bucket instanceof Construct)) { - throw new Error(`LambdaDestination for function ${Names.nodeUniqueId(this.fn.permissionsNode)} can only be configured on a - bucket construct (Bucket ${bucket.bucketName})`); + throw new ValidationError(`LambdaDestination for function ${Names.nodeUniqueId(this.fn.permissionsNode)} can only be configured on a + bucket construct (Bucket ${bucket.bucketName})`, scope); } if (bucket.node.tryFindChild(permissionId) === undefined) {