Skip to content

Commit aea8f3b

Browse files
authored
feat(s3): throw ValidationError instead of untyped errors (#33109)
### Issue `aws-s3*` for #32569 ### Description of changes ValidationErrors everywhere ### Describe any new or updated permissions being added n/a ### Description of how you validated changes Existing tests. Exemptions granted as this is basically a refactor of existing code. ### Checklist - [x] My code adheres to the [CONTRIBUTING GUIDE](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and [DESIGN GUIDELINES](https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md) ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 7b9f6c8 commit aea8f3b

File tree

9 files changed

+39
-24
lines changed

9 files changed

+39
-24
lines changed

packages/aws-cdk-lib/.eslintrc.js

+7
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@ const enableNoThrowDefaultErrorIn = [
2626
'aws-ssmincidents',
2727
'aws-ssmquicksetup',
2828
'aws-synthetics',
29+
'aws-s3-assets',
30+
'aws-s3-deployment',
31+
'aws-s3-notifications',
32+
'aws-s3express',
33+
'aws-s3objectlambda',
34+
'aws-s3outposts',
35+
'aws-s3tables',
2936
];
3037
baseConfig.overrides.push({
3138
files: enableNoThrowDefaultErrorIn.map(m => `./${m}/lib/**`),

packages/aws-cdk-lib/aws-s3-assets/lib/asset.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import * as iam from '../../aws-iam';
66
import * as kms from '../../aws-kms';
77
import * as s3 from '../../aws-s3';
88
import * as cdk from '../../core';
9+
import { ValidationError } from '../../core/lib/errors';
910
import * as cxapi from '../../cx-api';
1011

1112
export interface AssetOptions extends CopyOptions, cdk.FileCopyOptions, cdk.AssetOptions {
@@ -143,7 +144,7 @@ export class Asset extends Construct implements cdk.IAsset {
143144
super(scope, id);
144145

145146
if (!props.path) {
146-
throw new Error('Asset path cannot be empty');
147+
throw new ValidationError('Asset path cannot be empty', this);
147148
}
148149

149150
this.isBundled = props.bundling != null;

packages/aws-cdk-lib/aws-s3-assets/lib/compat.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { FollowMode } from '../../assets';
22
import { SymlinkFollowMode } from '../../core';
3+
import { UnscopedValidationError } from '../../core/lib/errors';
34

45
export function toSymlinkFollow(follow?: FollowMode): SymlinkFollowMode | undefined {
56
if (!follow) {
@@ -12,6 +13,6 @@ export function toSymlinkFollow(follow?: FollowMode): SymlinkFollowMode | undefi
1213
case FollowMode.BLOCK_EXTERNAL: return SymlinkFollowMode.BLOCK_EXTERNAL;
1314
case FollowMode.EXTERNAL: return SymlinkFollowMode.EXTERNAL;
1415
default:
15-
throw new Error(`unknown follow mode: ${follow}`);
16+
throw new UnscopedValidationError(`unknown follow mode: ${follow}`);
1617
}
1718
}

packages/aws-cdk-lib/aws-s3-assets/test/custom-synthesis.test.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import * as path from 'path';
88
import { Template } from '../../assertions';
99
import { StackSynthesizer, FileAssetSource, FileAssetLocation, DockerImageAssetSource, DockerImageAssetLocation, ISynthesisSession, App, Stack, AssetManifestBuilder, CfnParameter, CfnResource } from '../../core';
10+
import { UnscopedValidationError } from '../../core/lib/errors';
1011
import { AssetManifestArtifact } from '../../cx-api';
1112
import { Asset } from '../lib';
1213

@@ -84,7 +85,7 @@ class CustomSynthesizer extends StackSynthesizer {
8485

8586
addDockerImageAsset(asset: DockerImageAssetSource): DockerImageAssetLocation {
8687
void(asset);
87-
throw new Error('Docker images are not supported here');
88+
throw new UnscopedValidationError('Docker images are not supported here');
8889
}
8990

9091
synthesize(session: ISynthesisSession): void {

packages/aws-cdk-lib/aws-s3-deployment/lib/bucket-deployment.ts

+9-8
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import * as lambda from '../../aws-lambda';
1111
import * as logs from '../../aws-logs';
1212
import * as s3 from '../../aws-s3';
1313
import * as cdk from '../../core';
14+
import { ValidationError } from '../../core/lib/errors';
1415
import { BucketDeploymentSingletonFunction } from '../../custom-resource-handlers/dist/aws-s3-deployment/bucket-deployment-provider.generated';
1516
import { AwsCliLayer } from '../../lambda-layer-awscli';
1617

@@ -304,17 +305,17 @@ export class BucketDeployment extends Construct {
304305

305306
if (props.distributionPaths) {
306307
if (!props.distribution) {
307-
throw new Error('Distribution must be specified if distribution paths are specified');
308+
throw new ValidationError('Distribution must be specified if distribution paths are specified', this);
308309
}
309310
if (!cdk.Token.isUnresolved(props.distributionPaths)) {
310311
if (!props.distributionPaths.every(distributionPath => cdk.Token.isUnresolved(distributionPath) || distributionPath.startsWith('/'))) {
311-
throw new Error('Distribution paths must start with "/"');
312+
throw new ValidationError('Distribution paths must start with "/"', this);
312313
}
313314
}
314315
}
315316

316317
if (props.useEfs && !props.vpc) {
317-
throw new Error('Vpc must be specified if useEfs is set');
318+
throw new ValidationError('Vpc must be specified if useEfs is set', this);
318319
}
319320

320321
this.destinationBucket = props.destinationBucket;
@@ -376,7 +377,7 @@ export class BucketDeployment extends Construct {
376377
});
377378

378379
const handlerRole = handler.role;
379-
if (!handlerRole) { throw new Error('lambda.SingletonFunction should have created a Role'); }
380+
if (!handlerRole) { throw new ValidationError('lambda.SingletonFunction should have created a Role', this); }
380381
this.handlerRole = handlerRole;
381382

382383
this.sources = props.sources.map((source: ISource) => source.bind(this, { handlerRole: this.handlerRole }));
@@ -455,7 +456,7 @@ export class BucketDeployment extends Construct {
455456
// '/this/is/a/random/key/prefix/that/is/a/lot/of/characters/do/we/think/that/it/will/ever/be/this/long?????'
456457
// better to throw an error here than wait for CloudFormation to fail
457458
if (!cdk.Token.isUnresolved(tagKey) && tagKey.length > 128) {
458-
throw new Error('The BucketDeployment construct requires that the "destinationKeyPrefix" be <=104 characters.');
459+
throw new ValidationError('The BucketDeployment construct requires that the "destinationKeyPrefix" be <=104 characters.', this);
459460
}
460461

461462
/*
@@ -567,7 +568,7 @@ export class BucketDeployment extends Construct {
567568
// configurations since we have a singleton.
568569
if (memoryLimit) {
569570
if (cdk.Token.isUnresolved(memoryLimit)) {
570-
throw new Error("Can't use tokens when specifying 'memoryLimit' since we use it to identify the singleton custom resource handler.");
571+
throw new ValidationError("Can't use tokens when specifying 'memoryLimit' since we use it to identify the singleton custom resource handler.", this);
571572
}
572573

573574
uuid += `-${memoryLimit.toString()}MiB`;
@@ -578,7 +579,7 @@ export class BucketDeployment extends Construct {
578579
// configurations since we have a singleton.
579580
if (ephemeralStorageSize) {
580581
if (ephemeralStorageSize.isUnresolved()) {
581-
throw new Error("Can't use tokens when specifying 'ephemeralStorageSize' since we use it to identify the singleton custom resource handler.");
582+
throw new ValidationError("Can't use tokens when specifying 'ephemeralStorageSize' since we use it to identify the singleton custom resource handler.", this);
582583
}
583584

584585
uuid += `-${ephemeralStorageSize.toMebibytes().toString()}MiB`;
@@ -660,7 +661,7 @@ export class DeployTimeSubstitutedFile extends BucketDeployment {
660661

661662
constructor(scope: Construct, id: string, props: DeployTimeSubstitutedFileProps) {
662663
if (!fs.existsSync(props.source)) {
663-
throw new Error(`No file found at 'source' path ${props.source}`);
664+
throw new ValidationError(`No file found at 'source' path ${props.source}`, scope);
664665
}
665666
// Makes substitutions on the file
666667
let fileData = fs.readFileSync(props.source, 'utf-8');

packages/aws-cdk-lib/aws-s3-deployment/lib/render-data.ts

+6-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Construct } from 'constructs';
22
import { Stack } from '../../core';
3+
import { ValidationError } from '../../core/lib/errors';
34

45
export interface Content {
56
readonly text: string;
@@ -22,7 +23,7 @@ export function renderData(scope: Construct, data: string): Content {
2223
}
2324

2425
if (typeof(obj) !== 'object') {
25-
throw new Error(`Unexpected: after resolve() data must either be a string or a CloudFormation intrinsic. Got: ${JSON.stringify(obj)}`);
26+
throw new ValidationError(`Unexpected: after resolve() data must either be a string or a CloudFormation intrinsic. Got: ${JSON.stringify(obj)}`, scope);
2627
}
2728

2829
let markerIndex = 0;
@@ -35,7 +36,7 @@ export function renderData(scope: Construct, data: string): Content {
3536
const parts = fnJoin[1];
3637

3738
if (sep !== '') {
38-
throw new Error(`Unexpected "Fn::Join", expecting separator to be an empty string but got "${sep}"`);
39+
throw new ValidationError(`Unexpected "Fn::Join", expecting separator to be an empty string but got "${sep}"`, scope);
3940
}
4041

4142
for (const part of parts) {
@@ -49,21 +50,21 @@ export function renderData(scope: Construct, data: string): Content {
4950
continue;
5051
}
5152

52-
throw new Error(`Unexpected "Fn::Join" part, expecting string or object but got ${typeof (part)}`);
53+
throw new ValidationError(`Unexpected "Fn::Join" part, expecting string or object but got ${typeof (part)}`, scope);
5354
}
5455

5556
} else if (obj.Ref || obj['Fn::GetAtt'] || obj['Fn::Select']) {
5657
addMarker(obj);
5758
} else {
58-
throw new Error('Unexpected: Expecting `resolve()` to return "Fn::Join", "Ref" or "Fn::GetAtt"');
59+
throw new ValidationError('Unexpected: Expecting `resolve()` to return "Fn::Join", "Ref" or "Fn::GetAtt"', scope);
5960
}
6061

6162
function addMarker(part: Ref | GetAtt | FnSelect) {
6263
const keys = Object.keys(part);
6364
const acceptedCfnFns = ['Ref', 'Fn::GetAtt', 'Fn::Select'];
6465
if (keys.length !== 1 || !acceptedCfnFns.includes(keys[0])) {
6566
const stringifiedAcceptedCfnFns = acceptedCfnFns.map((fn) => `"${fn}"`).join(' or ');
66-
throw new Error(`Invalid CloudFormation reference. Key must start with any of ${stringifiedAcceptedCfnFns}. Got ${JSON.stringify(part)}`);
67+
throw new ValidationError(`Invalid CloudFormation reference. Key must start with any of ${stringifiedAcceptedCfnFns}. Got ${JSON.stringify(part)}`, scope);
6768
}
6869

6970
const marker = `<<marker:0xbaba:${markerIndex++}>>`;

packages/aws-cdk-lib/aws-s3-deployment/lib/source.ts

+5-4
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import * as iam from '../../aws-iam';
66
import * as s3 from '../../aws-s3';
77
import * as s3_assets from '../../aws-s3-assets';
88
import { FileSystem, Stack } from '../../core';
9+
import { ValidationError } from '../../core/lib/errors';
910

1011
/**
1112
* Source information.
@@ -98,9 +99,9 @@ export class Source {
9899
*/
99100
public static bucket(bucket: s3.IBucket, zipObjectKey: string): ISource {
100101
return {
101-
bind: (_: Construct, context?: DeploymentSourceContext) => {
102+
bind: (scope: Construct, context?: DeploymentSourceContext) => {
102103
if (!context) {
103-
throw new Error('To use a Source.bucket(), context must be provided');
104+
throw new ValidationError('To use a Source.bucket(), context must be provided', scope);
104105
}
105106

106107
bucket.grantRead(context.handlerRole);
@@ -121,7 +122,7 @@ export class Source {
121122
return {
122123
bind(scope: Construct, context?: DeploymentSourceContext): SourceConfig {
123124
if (!context) {
124-
throw new Error('To use a Source.asset(), context must be provided');
125+
throw new ValidationError('To use a Source.asset(), context must be provided', scope);
125126
}
126127

127128
let id = 1;
@@ -133,7 +134,7 @@ export class Source {
133134
...options,
134135
});
135136
if (!asset.isZipArchive) {
136-
throw new Error('Asset path must be either a .zip file or a directory');
137+
throw new ValidationError('Asset path must be either a .zip file or a directory', scope);
137138
}
138139
asset.grantRead(context.handlerRole);
139140

packages/aws-cdk-lib/aws-s3-deployment/test/bucket-deployment.test.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import * as logs from '../../aws-logs';
99
import * as s3 from '../../aws-s3';
1010
import * as sns from '../../aws-sns';
1111
import * as cdk from '../../core';
12+
import { UnscopedValidationError } from '../../core/lib/errors';
1213
import * as cxapi from '../../cx-api';
1314
import * as s3deploy from '../lib';
1415

@@ -1678,7 +1679,7 @@ function readDataFile(casm: cxapi.CloudAssembly, relativePath: string): string {
16781679
}
16791680
}
16801681

1681-
throw new Error(`File ${relativePath} not found in any of the assets of the assembly`);
1682+
throw new UnscopedValidationError(`File ${relativePath} not found in any of the assets of the assembly`);
16821683
}
16831684

16841685
test('DeployTimeSubstitutedFile allows custom role to be supplied', () => {

packages/aws-cdk-lib/aws-s3-notifications/lib/lambda.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as iam from '../../aws-iam';
33
import * as lambda from '../../aws-lambda';
44
import * as s3 from '../../aws-s3';
55
import { CfnResource, Names, Stack } from '../../core';
6+
import { ValidationError } from '../../core/lib/errors';
67

78
/**
89
* Use a Lambda function as a bucket notification destination
@@ -11,12 +12,12 @@ export class LambdaDestination implements s3.IBucketNotificationDestination {
1112
constructor(private readonly fn: lambda.IFunction) {
1213
}
1314

14-
public bind(_scope: Construct, bucket: s3.IBucket): s3.BucketNotificationDestinationConfig {
15+
public bind(scope: Construct, bucket: s3.IBucket): s3.BucketNotificationDestinationConfig {
1516
const permissionId = `AllowBucketNotificationsTo${Names.nodeUniqueId(this.fn.permissionsNode)}`;
1617

1718
if (!(bucket instanceof Construct)) {
18-
throw new Error(`LambdaDestination for function ${Names.nodeUniqueId(this.fn.permissionsNode)} can only be configured on a
19-
bucket construct (Bucket ${bucket.bucketName})`);
19+
throw new ValidationError(`LambdaDestination for function ${Names.nodeUniqueId(this.fn.permissionsNode)} can only be configured on a
20+
bucket construct (Bucket ${bucket.bucketName})`, scope);
2021
}
2122

2223
if (bucket.node.tryFindChild(permissionId) === undefined) {

0 commit comments

Comments
 (0)