Skip to content

Commit 93313dd

Browse files
authored
feat(ec2): throw ValidationErrors instead of untyped Errors (#34127)
### Issue # (if applicable) Relates to #32569 ### Reason for this change Untyped Errors are not recommended. ### Description of changes Change Error to ValidationError / UnscopedValidationError ### Describe any new or updated permissions being added None ### Description of how you validated changes Existing tests. Exemptions granted as this is 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 c4fd9fd commit 93313dd

33 files changed

+227
-207
lines changed

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

+1
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ const enableNoThrowDefaultErrorIn = [
5050
'aws-config',
5151
'aws-docdb',
5252
'aws-dynamodb',
53+
'aws-ec2',
5354
'aws-ecr',
5455
'aws-ecr-assets',
5556
'aws-efs',

packages/aws-cdk-lib/aws-ec2/lib/bastion-host.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { ISecurityGroup } from './security-group';
1010
import { BlockDevice } from './volume';
1111
import { IVpc, SubnetSelection } from './vpc';
1212
import { IPrincipal, IRole, PolicyStatement } from '../../aws-iam';
13-
import { CfnOutput, FeatureFlags, Resource, Stack } from '../../core';
13+
import { CfnOutput, FeatureFlags, Resource, Stack, UnscopedValidationError } from '../../core';
1414
import { addConstructMetadata, MethodMetadata } from '../../core/lib/metadata-resource';
1515
import { BASTION_HOST_USE_AMAZON_LINUX_2023_BY_DEFAULT } from '../../cx-api';
1616

@@ -246,7 +246,7 @@ export class BastionHostLinux extends Resource implements IInstance {
246246
return AmazonLinuxCpuType.X86_64;
247247
}
248248

249-
throw new Error(`Unsupported instance architecture '${architecture}'`);
249+
throw new UnscopedValidationError(`Unsupported instance architecture '${architecture}'`);
250250
}
251251

252252
/**

packages/aws-cdk-lib/aws-ec2/lib/cfn-init-elements.ts

+13-13
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { InitBindOptions, InitElementConfig, InitElementType, InitPlatform } fro
33
import * as iam from '../../aws-iam';
44
import * as s3 from '../../aws-s3';
55
import * as s3_assets from '../../aws-s3-assets';
6-
import { Duration } from '../../core';
6+
import { Duration, UnscopedValidationError, ValidationError } from '../../core';
77
import { md5hash } from '../../core/lib/helpers-internal';
88

99
/**
@@ -220,7 +220,7 @@ export class InitCommand extends InitElement {
220220
*/
221221
public static argvCommand(argv: string[], options: InitCommandOptions = {}): InitCommand {
222222
if (argv.length === 0) {
223-
throw new Error('Cannot define argvCommand with an empty arguments');
223+
throw new UnscopedValidationError('Cannot define argvCommand with an empty arguments');
224224
}
225225
return new InitCommand(argv, options);
226226
}
@@ -236,7 +236,7 @@ export class InitCommand extends InitElement {
236236
const commandKey = this.options.key || `${options.index}`.padStart(3, '0'); // 001, 005, etc.
237237

238238
if (options.platform !== InitPlatform.WINDOWS && this.options.waitAfterCompletion !== undefined) {
239-
throw new Error(`Command '${this.command}': 'waitAfterCompletion' is only valid for Windows systems.`);
239+
throw new ValidationError(`Command '${this.command}': 'waitAfterCompletion' is only valid for Windows systems.`, options.scope);
240240
}
241241

242242
for (const handle of this.options.serviceRestartHandles ?? []) {
@@ -325,7 +325,7 @@ export abstract class InitFile extends InitElement {
325325
*/
326326
public static fromString(fileName: string, content: string, options: InitFileOptions = {}): InitFile {
327327
if (!content) {
328-
throw new Error(`InitFile ${fileName}: cannot create empty file. Please supply at least one character of content.`);
328+
throw new UnscopedValidationError(`InitFile ${fileName}: cannot create empty file. Please supply at least one character of content.`);
329329
}
330330
return new class extends InitFile {
331331
protected _doBind(bindOptions: InitBindOptions) {
@@ -345,7 +345,7 @@ export abstract class InitFile extends InitElement {
345345
public static symlink(fileName: string, target: string, options: InitFileOptions = {}): InitFile {
346346
const { mode, ...otherOptions } = options;
347347
if (mode && mode.slice(0, 3) !== '120') {
348-
throw new Error('File mode for symlinks must begin with 120XXX');
348+
throw new UnscopedValidationError('File mode for symlinks must begin with 120XXX');
349349
}
350350
return InitFile.fromString(fileName, target, { mode: (mode || '120644'), ...otherOptions });
351351
}
@@ -489,7 +489,7 @@ export abstract class InitFile extends InitElement {
489489
protected _standardConfig(fileOptions: InitFileOptions, platform: InitPlatform, contentVars: Record<string, any>): Record<string, any> {
490490
if (platform === InitPlatform.WINDOWS) {
491491
if (fileOptions.group || fileOptions.owner || fileOptions.mode) {
492-
throw new Error('Owner, group, and mode options not supported for Windows.');
492+
throw new UnscopedValidationError('Owner, group, and mode options not supported for Windows.');
493493
}
494494
return {
495495
[this.fileName]: { ...contentVars },
@@ -529,7 +529,7 @@ export class InitGroup extends InitElement {
529529
/** @internal */
530530
public _bind(options: InitBindOptions): InitElementConfig {
531531
if (options.platform === InitPlatform.WINDOWS) {
532-
throw new Error('Init groups are not supported on Windows');
532+
throw new UnscopedValidationError('Init groups are not supported on Windows');
533533
}
534534

535535
return {
@@ -593,7 +593,7 @@ export class InitUser extends InitElement {
593593
/** @internal */
594594
public _bind(options: InitBindOptions): InitElementConfig {
595595
if (options.platform === InitPlatform.WINDOWS) {
596-
throw new Error('Init users are not supported on Windows');
596+
throw new UnscopedValidationError('Init users are not supported on Windows');
597597
}
598598

599599
return {
@@ -712,14 +712,14 @@ export class InitPackage extends InitElement {
712712
public _bind(options: InitBindOptions): InitElementConfig {
713713
if ((this.type === 'msi') !== (options.platform === InitPlatform.WINDOWS)) {
714714
if (this.type === 'msi') {
715-
throw new Error('MSI installers are only supported on Windows systems.');
715+
throw new UnscopedValidationError('MSI installers are only supported on Windows systems.');
716716
} else {
717-
throw new Error('Windows only supports the MSI package type');
717+
throw new UnscopedValidationError('Windows only supports the MSI package type');
718718
}
719719
}
720720

721721
if (!this.packageName && !['rpm', 'msi'].includes(this.type)) {
722-
throw new Error('Package name must be specified for all package types besides RPM and MSI.');
722+
throw new UnscopedValidationError('Package name must be specified for all package types besides RPM and MSI.');
723723
}
724724

725725
const packageName = this.packageName || `${options.index}`.padStart(3, '0');
@@ -826,11 +826,11 @@ export class InitService extends InitElement {
826826
*/
827827
public static systemdConfigFile(serviceName: string, options: SystemdConfigFileOptions): InitFile {
828828
if (!options.command.startsWith('/')) {
829-
throw new Error(`SystemD executables must use an absolute path, got '${options.command}'`);
829+
throw new UnscopedValidationError(`SystemD executables must use an absolute path, got '${options.command}'`);
830830
}
831831

832832
if (options.environmentFiles?.some(file => !file.startsWith('/'))) {
833-
throw new Error('SystemD environment files must use absolute paths');
833+
throw new UnscopedValidationError('SystemD environment files must use absolute paths');
834834
}
835835

836836
const lines = [

packages/aws-cdk-lib/aws-ec2/lib/cfn-init.ts

+8-8
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { OperatingSystemType } from './machine-image';
55
import { InitBindOptions, InitElementConfig, InitElementType, InitPlatform } from './private/cfn-init-internal';
66
import { UserData } from './user-data';
77
import * as iam from '../../aws-iam';
8-
import { Aws, CfnResource } from '../../core';
8+
import { Aws, CfnResource, UnscopedValidationError, ValidationError } from '../../core';
99

1010
/**
1111
* A CloudFormation-init configuration
@@ -50,7 +50,7 @@ export class CloudFormationInit {
5050
*/
5151
public addConfig(configName: string, config: InitConfig) {
5252
if (this._configs[configName]) {
53-
throw new Error(`CloudFormationInit already contains a config named '${configName}'`);
53+
throw new UnscopedValidationError(`CloudFormationInit already contains a config named '${configName}'`);
5454
}
5555
this._configs[configName] = config;
5656
}
@@ -62,12 +62,12 @@ export class CloudFormationInit {
6262
*/
6363
public addConfigSet(configSetName: string, configNames: string[] = []) {
6464
if (this._configSets[configSetName]) {
65-
throw new Error(`CloudFormationInit already contains a configSet named '${configSetName}'`);
65+
throw new UnscopedValidationError(`CloudFormationInit already contains a configSet named '${configSetName}'`);
6666
}
6767

6868
const unk = configNames.filter(c => !this._configs[c]);
6969
if (unk.length > 0) {
70-
throw new Error(`Unknown configs referenced in definition of '${configSetName}': ${unk}`);
70+
throw new UnscopedValidationError(`Unknown configs referenced in definition of '${configSetName}': ${unk}`);
7171
}
7272

7373
this._configSets[configSetName] = [...configNames];
@@ -91,13 +91,13 @@ export class CloudFormationInit {
9191
*/
9292
public attach(attachedResource: CfnResource, attachOptions: AttachInitOptions) {
9393
if (attachOptions.platform === OperatingSystemType.UNKNOWN) {
94-
throw new Error('Cannot attach CloudFormationInit to an unknown OS type');
94+
throw new ValidationError('Cannot attach CloudFormationInit to an unknown OS type', attachedResource);
9595
}
9696

9797
const CFN_INIT_METADATA_KEY = 'AWS::CloudFormation::Init';
9898

9999
if (attachedResource.getMetadata(CFN_INIT_METADATA_KEY) !== undefined) {
100-
throw new Error(`Cannot bind CfnInit: resource '${attachedResource.node.path}' already has '${CFN_INIT_METADATA_KEY}' attached`);
100+
throw new ValidationError(`Cannot bind CfnInit: resource '${attachedResource.node.path}' already has '${CFN_INIT_METADATA_KEY}' attached`, attachedResource);
101101
}
102102

103103
// Note: This will not reflect mutations made after attaching.
@@ -270,7 +270,7 @@ export class InitConfig {
270270
return InitPlatform.WINDOWS;
271271
}
272272
default: {
273-
throw new Error('Cannot attach CloudFormationInit to an unknown OS type');
273+
throw new UnscopedValidationError('Cannot attach CloudFormationInit to an unknown OS type');
274274
}
275275
}
276276
}
@@ -308,7 +308,7 @@ function deepMerge(target?: Record<string, any>, src?: Record<string, any>) {
308308

309309
if (Array.isArray(value)) {
310310
if (target[key] && !Array.isArray(target[key])) {
311-
throw new Error(`Trying to merge array [${value}] into a non-array '${target[key]}'`);
311+
throw new UnscopedValidationError(`Trying to merge array [${value}] into a non-array '${target[key]}'`);
312312
}
313313
if (key === 'command') { // don't deduplicate command arguments
314314
target[key] = new Array(

packages/aws-cdk-lib/aws-ec2/lib/cidr-splits.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { UnscopedValidationError } from '../../core';
2+
13
/**
24
* Return the splits necessary to allocate the given sequence of cidrs in the given order
35
*
@@ -43,7 +45,7 @@ export function calculateCidrSplits(rootNetmask: number, netmasks: number[]): Ci
4345
}
4446

4547
if (offset > Math.pow(2, 32 - rootNetmask)) {
46-
throw new Error(`IP space of size /${rootNetmask} not big enough to allocate subnets of sizes ${netmasks.map(x => `/${x}`)}`);
48+
throw new UnscopedValidationError(`IP space of size /${rootNetmask} not big enough to allocate subnets of sizes ${netmasks.map(x => `/${x}`)}`);
4749
}
4850

4951
return ret;

packages/aws-cdk-lib/aws-ec2/lib/client-vpn-authorization-rule.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Construct } from 'constructs';
22
import { IClientVpnEndpoint } from './client-vpn-endpoint-types';
33
import { CfnClientVpnAuthorizationRule } from './ec2.generated';
4-
import { Resource } from '../../core';
4+
import { Resource, ValidationError } from '../../core';
55
import { addConstructMetadata } from '../../core/lib/metadata-resource';
66

77
/**
@@ -54,14 +54,16 @@ export interface ClientVpnAuthorizationRuleProps extends ClientVpnAuthorizationR
5454
export class ClientVpnAuthorizationRule extends Resource {
5555
constructor(scope: Construct, id: string, props: ClientVpnAuthorizationRuleProps) {
5656
if (!props.clientVpnEndoint && !props.clientVpnEndpoint) {
57-
throw new Error(
57+
throw new ValidationError(
5858
'ClientVpnAuthorizationRule: either clientVpnEndpoint or clientVpnEndoint (deprecated) must be specified',
59+
scope,
5960
);
6061
}
6162
if (props.clientVpnEndoint && props.clientVpnEndpoint) {
62-
throw new Error(
63+
throw new ValidationError(
6364
'ClientVpnAuthorizationRule: either clientVpnEndpoint or clientVpnEndoint (deprecated) must be specified' +
6465
', but not both',
66+
scope,
6567
);
6668
}
6769
const clientVpnEndpoint = props.clientVpnEndoint || props.clientVpnEndpoint;

packages/aws-cdk-lib/aws-ec2/lib/client-vpn-endpoint.ts

+8-8
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { ISecurityGroup, SecurityGroup } from './security-group';
99
import { IVpc, SubnetSelection } from './vpc';
1010
import { ISamlProvider } from '../../aws-iam';
1111
import * as logs from '../../aws-logs';
12-
import { CfnOutput, Resource, Token } from '../../core';
12+
import { CfnOutput, Resource, Token, UnscopedValidationError, ValidationError } from '../../core';
1313
import { addConstructMetadata, MethodMetadata } from '../../core/lib/metadata-resource';
1414

1515
/**
@@ -300,28 +300,28 @@ export class ClientVpnEndpoint extends Resource implements IClientVpnEndpoint {
300300
const clientCidr = new CidrBlock(props.cidr);
301301
const vpcCidr = new CidrBlock(props.vpc.vpcCidrBlock);
302302
if (vpcCidr.containsCidr(clientCidr)) {
303-
throw new Error('The client CIDR cannot overlap with the local CIDR of the VPC');
303+
throw new ValidationError('The client CIDR cannot overlap with the local CIDR of the VPC', this);
304304
}
305305
}
306306

307307
if (props.dnsServers && props.dnsServers.length > 2) {
308-
throw new Error('A client VPN endpoint can have up to two DNS servers');
308+
throw new ValidationError('A client VPN endpoint can have up to two DNS servers', this);
309309
}
310310

311311
if (props.logging == false && (props.logGroup || props.logStream)) {
312-
throw new Error('Cannot specify `logGroup` or `logStream` when logging is disabled');
312+
throw new ValidationError('Cannot specify `logGroup` or `logStream` when logging is disabled', this);
313313
}
314314

315315
if (props.clientConnectionHandler
316316
&& !Token.isUnresolved(props.clientConnectionHandler.functionName)
317317
&& !props.clientConnectionHandler.functionName.startsWith('AWSClientVPN-')) {
318-
throw new Error('The name of the Lambda function must begin with the `AWSClientVPN-` prefix');
318+
throw new ValidationError('The name of the Lambda function must begin with the `AWSClientVPN-` prefix', this);
319319
}
320320

321321
if (props.clientLoginBanner
322322
&& !Token.isUnresolved(props.clientLoginBanner)
323323
&& props.clientLoginBanner.length > 1400) {
324-
throw new Error(`The maximum length for the client login banner is 1400, got ${props.clientLoginBanner.length}`);
324+
throw new ValidationError(`The maximum length for the client login banner is 1400, got ${props.clientLoginBanner.length}`, this);
325325
}
326326

327327
const logging = props.logging ?? true;
@@ -379,7 +379,7 @@ export class ClientVpnEndpoint extends Resource implements IClientVpnEndpoint {
379379
const subnetIds = props.vpc.selectSubnets(props.vpcSubnets).subnetIds;
380380

381381
if (Token.isUnresolved(subnetIds)) {
382-
throw new Error('Cannot associate subnets when VPC are imported from parameters or exports containing lists of subnet IDs.');
382+
throw new ValidationError('Cannot associate subnets when VPC are imported from parameters or exports containing lists of subnet IDs.', this);
383383
}
384384

385385
for (const [idx, subnetId] of Object.entries(subnetIds)) {
@@ -439,7 +439,7 @@ function renderAuthenticationOptions(
439439
}
440440

441441
if (authenticationOptions.length === 0) {
442-
throw new Error('A client VPN endpoint must use at least one authentication option');
442+
throw new UnscopedValidationError('A client VPN endpoint must use at least one authentication option');
443443
}
444444
return authenticationOptions;
445445
}

packages/aws-cdk-lib/aws-ec2/lib/client-vpn-route.ts

+5-4
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Construct } from 'constructs';
22
import { IClientVpnEndpoint } from './client-vpn-endpoint-types';
33
import { CfnClientVpnRoute } from './ec2.generated';
44
import { ISubnet } from './vpc';
5-
import { Resource } from '../../core';
5+
import { Resource, ValidationError } from '../../core';
66
import { addConstructMetadata } from '../../core/lib/metadata-resource';
77

88
/**
@@ -83,14 +83,15 @@ export interface ClientVpnRouteProps extends ClientVpnRouteOptions {
8383
export class ClientVpnRoute extends Resource {
8484
constructor(scope: Construct, id: string, props: ClientVpnRouteProps) {
8585
if (!props.clientVpnEndoint && !props.clientVpnEndpoint) {
86-
throw new Error(
87-
'ClientVpnRoute: either clientVpnEndpoint or clientVpnEndoint (deprecated) must be specified',
86+
throw new ValidationError(
87+
'ClientVpnRoute: either clientVpnEndpoint or clientVpnEndoint (deprecated) must be specified', scope,
8888
);
8989
}
9090
if (props.clientVpnEndoint && props.clientVpnEndpoint) {
91-
throw new Error(
91+
throw new ValidationError(
9292
'ClientVpnRoute: either clientVpnEndpoint or clientVpnEndoint (deprecated) must be specified' +
9393
', but not both',
94+
scope,
9495
);
9596
}
9697
const clientVpnEndpoint = props.clientVpnEndoint || props.clientVpnEndpoint;

packages/aws-cdk-lib/aws-ec2/lib/connections.ts

+6-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { IPeer, Peer } from './peer';
22
import { Port } from './port';
33
import { ISecurityGroup } from './security-group';
4+
import { UnscopedValidationError } from '../../core';
45

56
/**
67
* The goal of this module is to make possible to write statements like this:
@@ -201,7 +202,7 @@ export class Connections implements IConnectable {
201202
*/
202203
public allowDefaultPortFrom(other: IConnectable, description?: string) {
203204
if (!this.defaultPort) {
204-
throw new Error('Cannot call allowDefaultPortFrom(): this resource has no default port');
205+
throw new UnscopedValidationError('Cannot call allowDefaultPortFrom(): this resource has no default port');
205206
}
206207
this.allowFrom(other, this.defaultPort, description);
207208
}
@@ -211,7 +212,7 @@ export class Connections implements IConnectable {
211212
*/
212213
public allowDefaultPortInternally(description?: string) {
213214
if (!this.defaultPort) {
214-
throw new Error('Cannot call allowDefaultPortInternally(): this resource has no default port');
215+
throw new UnscopedValidationError('Cannot call allowDefaultPortInternally(): this resource has no default port');
215216
}
216217
this.allowInternally(this.defaultPort, description);
217218
}
@@ -221,7 +222,7 @@ export class Connections implements IConnectable {
221222
*/
222223
public allowDefaultPortFromAnyIpv4(description?: string) {
223224
if (!this.defaultPort) {
224-
throw new Error('Cannot call allowDefaultPortFromAnyIpv4(): this resource has no default port');
225+
throw new UnscopedValidationError('Cannot call allowDefaultPortFromAnyIpv4(): this resource has no default port');
225226
}
226227
this.allowFromAnyIpv4(this.defaultPort, description);
227228
}
@@ -231,7 +232,7 @@ export class Connections implements IConnectable {
231232
*/
232233
public allowToDefaultPort(other: IConnectable, description?: string) {
233234
if (other.connections.defaultPort === undefined) {
234-
throw new Error('Cannot call allowToDefaultPort(): other resource has no default port');
235+
throw new UnscopedValidationError('Cannot call allowToDefaultPort(): other resource has no default port');
235236
}
236237

237238
this.allowTo(other, other.connections.defaultPort, description);
@@ -244,7 +245,7 @@ export class Connections implements IConnectable {
244245
*/
245246
public allowDefaultPortTo(other: IConnectable, description?: string) {
246247
if (!this.defaultPort) {
247-
throw new Error('Cannot call allowDefaultPortTo(): this resource has no default port');
248+
throw new UnscopedValidationError('Cannot call allowDefaultPortTo(): this resource has no default port');
248249
}
249250
this.allowTo(other, this.defaultPort, description);
250251
}

0 commit comments

Comments
 (0)