Skip to content

Commit 0b2db62

Browse files
authored
feat(rds): throw ValidationError instead of untyped errors (#33042)
### Issue `aws-rds` 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 ed7d54d commit 0b2db62

11 files changed

+111
-101
lines changed

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ baseConfig.rules['import/no-extraneous-dependencies'] = [
1515

1616

1717
// no-throw-default-error
18-
const modules = ['aws-s3', 'aws-lambda'];
18+
const modules = ['aws-s3', 'aws-lambda', 'aws-rds'];
1919
baseConfig.overrides.push({
2020
files: modules.map(m => `./${m}/lib/**`),
2121
rules: { "@cdklabs/no-throw-default-error": ['error'] },

packages/aws-cdk-lib/aws-rds/lib/aurora-cluster-instance.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import * as ec2 from '../../aws-ec2';
1111
import { IRole } from '../../aws-iam';
1212
import * as kms from '../../aws-kms';
1313
import { IResource, Resource, Duration, RemovalPolicy, ArnFormat, FeatureFlags } from '../../core';
14+
import { ValidationError } from '../../core/lib/errors';
1415
import { AURORA_CLUSTER_CHANGE_SCOPE_OF_INSTANCE_PARAMETER_GROUP_WITH_EACH_PARAMETERS } from '../../cx-api';
1516

1617
/**
@@ -476,7 +477,7 @@ class AuroraClusterInstance extends Resource implements IAuroraClusterInstance {
476477
});
477478
this.tier = props.promotionTier ?? 2;
478479
if (this.tier > 15) {
479-
throw new Error('promotionTier must be between 0-15');
480+
throw new ValidationError('promotionTier must be between 0-15', this);
480481
}
481482

482483
const isOwnedResource = Resource.isOwnedResource(props.cluster);
@@ -499,7 +500,7 @@ class AuroraClusterInstance extends Resource implements IAuroraClusterInstance {
499500
const enablePerformanceInsights = props.enablePerformanceInsights
500501
|| props.performanceInsightRetention !== undefined || props.performanceInsightEncryptionKey !== undefined;
501502
if (enablePerformanceInsights && props.enablePerformanceInsights === false) {
502-
throw new Error('`enablePerformanceInsights` disabled, but `performanceInsightRetention` or `performanceInsightEncryptionKey` was set');
503+
throw new ValidationError('`enablePerformanceInsights` disabled, but `performanceInsightRetention` or `performanceInsightEncryptionKey` was set', this);
503504
}
504505

505506
this.performanceInsightsEnabled = enablePerformanceInsights;

packages/aws-cdk-lib/aws-rds/lib/cluster-engine.ts

+5-4
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { EngineVersion } from './engine-version';
44
import { IParameterGroup, ParameterGroup } from './parameter-group';
55
import * as iam from '../../aws-iam';
66
import * as secretsmanager from '../../aws-secretsmanager';
7+
import { ValidationError } from '../../core/lib/errors';
78

89
/**
910
* The extra options passed to the `IClusterEngine.bindToCluster` method.
@@ -1179,19 +1180,19 @@ class AuroraPostgresClusterEngine extends ClusterEngineBase {
11791180
// skip validation for unversioned as it might be supported/unsupported. we cannot reliably tell at compile-time
11801181
if (this.engineVersion?.fullVersion) {
11811182
if (options.s3ImportRole && !(config.features?.s3Import)) {
1182-
throw new Error(`s3Import is not supported for Postgres version: ${this.engineVersion.fullVersion}. Use a version that supports the s3Import feature.`);
1183+
throw new ValidationError(`s3Import is not supported for Postgres version: ${this.engineVersion.fullVersion}. Use a version that supports the s3Import feature.`, scope);
11831184
}
11841185
if (options.s3ExportRole && !(config.features?.s3Export)) {
1185-
throw new Error(`s3Export is not supported for Postgres version: ${this.engineVersion.fullVersion}. Use a version that supports the s3Export feature.`);
1186+
throw new ValidationError(`s3Export is not supported for Postgres version: ${this.engineVersion.fullVersion}. Use a version that supports the s3Export feature.`, scope);
11861187
}
11871188
}
11881189
return config;
11891190
}
11901191

11911192
protected defaultParameterGroup(scope: Construct): IParameterGroup | undefined {
11921193
if (!this.parameterGroupFamily) {
1193-
throw new Error('Could not create a new ParameterGroup for an unversioned aurora-postgresql cluster engine. ' +
1194-
'Please either use a versioned engine, or pass an explicit ParameterGroup when creating the cluster');
1194+
throw new ValidationError('Could not create a new ParameterGroup for an unversioned aurora-postgresql cluster engine. ' +
1195+
'Please either use a versioned engine, or pass an explicit ParameterGroup when creating the cluster', scope);
11951196
}
11961197
return ParameterGroup.fromParameterGroupName(scope, 'AuroraPostgreSqlDatabaseClusterEngineDefaultParameterGroup',
11971198
`default.${this.parameterGroupFamily}`);

packages/aws-cdk-lib/aws-rds/lib/cluster.ts

+40-39
Large diffs are not rendered by default.

packages/aws-cdk-lib/aws-rds/lib/instance-engine.ts

+5-4
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { EngineVersion } from './engine-version';
44
import { IOptionGroup, OptionGroup } from './option-group';
55
import * as iam from '../../aws-iam';
66
import * as secretsmanager from '../../aws-secretsmanager';
7+
import { ValidationError } from '../../core/lib/errors';
78

89
/**
910
* The options passed to `IInstanceEngine.bind`.
@@ -142,9 +143,9 @@ abstract class InstanceEngineBase implements IInstanceEngine {
142143
this.engineFamily = props.engineFamily;
143144
}
144145

145-
public bindToInstance(_scope: Construct, options: InstanceEngineBindOptions): InstanceEngineConfig {
146+
public bindToInstance(scope: Construct, options: InstanceEngineBindOptions): InstanceEngineConfig {
146147
if (options.timezone && !this.supportsTimezone) {
147-
throw new Error(`timezone property can not be configured for ${this.engineType}`);
148+
throw new ValidationError(`timezone property can not be configured for ${this.engineType}`, scope);
148149
}
149150
return {
150151
features: this.features,
@@ -621,7 +622,7 @@ class MariaDbInstanceEngine extends InstanceEngineBase {
621622

622623
public bindToInstance(scope: Construct, options: InstanceEngineBindOptions): InstanceEngineConfig {
623624
if (options.domain) {
624-
throw new Error(`domain property cannot be configured for ${this.engineType}`);
625+
throw new ValidationError(`domain property cannot be configured for ${this.engineType}`, scope);
625626
}
626627
return super.bindToInstance(scope, options);
627628
}
@@ -2828,7 +2829,7 @@ abstract class SqlServerInstanceEngineBase extends InstanceEngineBase {
28282829
const s3Role = options.s3ImportRole ?? options.s3ExportRole;
28292830
if (s3Role) {
28302831
if (options.s3ImportRole && options.s3ExportRole && options.s3ImportRole !== options.s3ExportRole) {
2831-
throw new Error('S3 import and export roles must be the same for SQL Server engines');
2832+
throw new ValidationError('S3 import and export roles must be the same for SQL Server engines', scope);
28322833
}
28332834

28342835
if (!optionGroup) {

packages/aws-cdk-lib/aws-rds/lib/instance.ts

+18-17
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import * as logs from '../../aws-logs';
1818
import * as s3 from '../../aws-s3';
1919
import * as secretsmanager from '../../aws-secretsmanager';
2020
import { ArnComponents, ArnFormat, Duration, FeatureFlags, IResource, Lazy, RemovalPolicy, Resource, Stack, Token, Tokenization } from '../../core';
21+
import { ValidationError } from '../../core/lib/errors';
2122
import * as cxapi from '../../cx-api';
2223

2324
/**
@@ -180,15 +181,15 @@ export abstract class DatabaseInstanceBase extends Resource implements IDatabase
180181

181182
public grantConnect(grantee: iam.IGrantable, dbUser?: string): iam.Grant {
182183
if (this.enableIamAuthentication === false) {
183-
throw new Error('Cannot grant connect when IAM authentication is disabled');
184+
throw new ValidationError('Cannot grant connect when IAM authentication is disabled', this);
184185
}
185186

186187
if (!this.instanceResourceId) {
187-
throw new Error('For imported Database Instances, instanceResourceId is required to grantConnect()');
188+
throw new ValidationError('For imported Database Instances, instanceResourceId is required to grantConnect()', this);
188189
}
189190

190191
if (!dbUser) {
191-
throw new Error('For imported Database Instances, the dbUser is required to grantConnect()');
192+
throw new ValidationError('For imported Database Instances, the dbUser is required to grantConnect()', this);
192193
}
193194

194195
this.enableIamAuthentication = true;
@@ -784,12 +785,12 @@ abstract class DatabaseInstanceNew extends DatabaseInstanceBase implements IData
784785

785786
this.vpc = props.vpc;
786787
if (props.vpcSubnets && props.vpcPlacement) {
787-
throw new Error('Only one of `vpcSubnets` or `vpcPlacement` can be specified');
788+
throw new ValidationError('Only one of `vpcSubnets` or `vpcPlacement` can be specified', this);
788789
}
789790
this.vpcPlacement = props.vpcSubnets ?? props.vpcPlacement;
790791

791792
if (props.multiAz === true && props.availabilityZone) {
792-
throw new Error('Requesting a specific availability zone is not valid for Multi-AZ instances');
793+
throw new ValidationError('Requesting a specific availability zone is not valid for Multi-AZ instances', this);
793794
}
794795

795796
const subnetGroup = props.subnetGroup ?? new SubnetGroup(this, 'SubnetGroup', {
@@ -820,12 +821,12 @@ abstract class DatabaseInstanceNew extends DatabaseInstanceBase implements IData
820821
const storageType = props.storageType ?? StorageType.GP2;
821822
const iops = defaultIops(storageType, props.iops);
822823
if (props.storageThroughput && storageType !== StorageType.GP3) {
823-
throw new Error(`The storage throughput can only be specified with GP3 storage type. Got ${storageType}.`);
824+
throw new ValidationError(`The storage throughput can only be specified with GP3 storage type. Got ${storageType}.`, this);
824825
}
825826
if (storageType === StorageType.GP3 && props.storageThroughput && iops
826827
&& !Token.isUnresolved(props.storageThroughput) && !Token.isUnresolved(iops)
827828
&& props.storageThroughput/iops > 0.25) {
828-
throw new Error(`The maximum ratio of storage throughput to IOPS is 0.25. Got ${props.storageThroughput/iops}.`);
829+
throw new ValidationError(`The maximum ratio of storage throughput to IOPS is 0.25. Got ${props.storageThroughput/iops}.`, this);
829830
}
830831

831832
this.cloudwatchLogGroups = {};
@@ -837,7 +838,7 @@ abstract class DatabaseInstanceNew extends DatabaseInstanceBase implements IData
837838
const enablePerformanceInsights = props.enablePerformanceInsights
838839
|| props.performanceInsightRetention !== undefined || props.performanceInsightEncryptionKey !== undefined;
839840
if (enablePerformanceInsights && props.enablePerformanceInsights === false) {
840-
throw new Error('`enablePerformanceInsights` disabled, but `performanceInsightRetention` or `performanceInsightEncryptionKey` was set');
841+
throw new ValidationError('`enablePerformanceInsights` disabled, but `performanceInsightRetention` or `performanceInsightEncryptionKey` was set', this);
841842
}
842843

843844
if (props.domain) {
@@ -1019,13 +1020,13 @@ abstract class DatabaseInstanceSource extends DatabaseInstanceNew implements IDa
10191020
const engineFeatures = engineConfig.features;
10201021
if (s3ImportRole) {
10211022
if (!engineFeatures?.s3Import) {
1022-
throw new Error(`Engine '${engineDescription(props.engine)}' does not support S3 import`);
1023+
throw new ValidationError(`Engine '${engineDescription(props.engine)}' does not support S3 import`, this);
10231024
}
10241025
instanceAssociatedRoles.push({ roleArn: s3ImportRole.roleArn, featureName: engineFeatures?.s3Import });
10251026
}
10261027
if (s3ExportRole) {
10271028
if (!engineFeatures?.s3Export) {
1028-
throw new Error(`Engine '${engineDescription(props.engine)}' does not support S3 export`);
1029+
throw new ValidationError(`Engine '${engineDescription(props.engine)}' does not support S3 export`, this);
10291030
}
10301031
// only add the export feature if it's different from the import feature
10311032
if (engineFeatures.s3Import !== engineFeatures?.s3Export) {
@@ -1036,7 +1037,7 @@ abstract class DatabaseInstanceSource extends DatabaseInstanceNew implements IDa
10361037
this.instanceType = props.instanceType ?? ec2.InstanceType.of(ec2.InstanceClass.M5, ec2.InstanceSize.LARGE);
10371038

10381039
if (props.parameterGroup && props.parameters) {
1039-
throw new Error('You cannot specify both parameterGroup and parameters');
1040+
throw new ValidationError('You cannot specify both parameterGroup and parameters', this);
10401041
}
10411042

10421043
const dbParameterGroupName = props.parameters
@@ -1069,13 +1070,13 @@ abstract class DatabaseInstanceSource extends DatabaseInstanceNew implements IDa
10691070
*/
10701071
public addRotationSingleUser(options: RotationSingleUserOptions = {}): secretsmanager.SecretRotation {
10711072
if (!this.secret) {
1072-
throw new Error('Cannot add single user rotation for an instance without secret.');
1073+
throw new ValidationError('Cannot add single user rotation for an instance without secret.', this);
10731074
}
10741075

10751076
const id = 'RotationSingleUser';
10761077
const existing = this.node.tryFindChild(id);
10771078
if (existing) {
1078-
throw new Error('A single user rotation was already added to this instance.');
1079+
throw new ValidationError('A single user rotation was already added to this instance.', this);
10791080
}
10801081

10811082
return new secretsmanager.SecretRotation(this, id, {
@@ -1092,7 +1093,7 @@ abstract class DatabaseInstanceSource extends DatabaseInstanceNew implements IDa
10921093
*/
10931094
public addRotationMultiUser(id: string, options: RotationMultiUserOptions): secretsmanager.SecretRotation {
10941095
if (!this.secret) {
1095-
throw new Error('Cannot add multi user rotation for an instance without secret.');
1096+
throw new ValidationError('Cannot add multi user rotation for an instance without secret.', this);
10961097
}
10971098

10981099
return new secretsmanager.SecretRotation(this, id, {
@@ -1115,7 +1116,7 @@ abstract class DatabaseInstanceSource extends DatabaseInstanceNew implements IDa
11151116
public grantConnect(grantee: iam.IGrantable, dbUser?: string): iam.Grant {
11161117
if (!dbUser) {
11171118
if (!this.secret) {
1118-
throw new Error('A secret or dbUser is required to grantConnect()');
1119+
throw new ValidationError('A secret or dbUser is required to grantConnect()', this);
11191120
}
11201121

11211122
dbUser = this.secret.secretValueFromJson('username').unsafeUnwrap();
@@ -1248,7 +1249,7 @@ export class DatabaseInstanceFromSnapshot extends DatabaseInstanceSource impleme
12481249
let secret = credentials?.secret;
12491250
if (!secret && credentials?.generatePassword) {
12501251
if (!credentials.username) {
1251-
throw new Error('`credentials` `username` must be specified when `generatePassword` is set to true');
1252+
throw new ValidationError('`credentials` `username` must be specified when `generatePassword` is set to true', this);
12521253
}
12531254

12541255
secret = new DatabaseSecret(this, 'Secret', {
@@ -1351,7 +1352,7 @@ export class DatabaseInstanceReadReplica extends DatabaseInstanceNew implements
13511352
if (props.sourceDatabaseInstance.engine
13521353
&& !props.sourceDatabaseInstance.engine.supportsReadReplicaBackups
13531354
&& props.backupRetention) {
1354-
throw new Error(`Cannot set 'backupRetention', as engine '${engineDescription(props.sourceDatabaseInstance.engine)}' does not support automatic backups for read replicas`);
1355+
throw new ValidationError(`Cannot set 'backupRetention', as engine '${engineDescription(props.sourceDatabaseInstance.engine)}' does not support automatic backups for read replicas`, this);
13551356
}
13561357

13571358
// The read replica instance always uses the same engine as the source instance

packages/aws-cdk-lib/aws-rds/lib/option-group.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { IInstanceEngine } from './instance-engine';
33
import { CfnOptionGroup } from './rds.generated';
44
import * as ec2 from '../../aws-ec2';
55
import { IResource, Lazy, Resource } from '../../core';
6+
import { ValidationError } from '../../core/lib/errors';
67

78
/**
89
* An option group
@@ -126,7 +127,7 @@ export class OptionGroup extends Resource implements IOptionGroup {
126127

127128
const majorEngineVersion = props.engine.engineVersion?.majorVersion;
128129
if (!majorEngineVersion) {
129-
throw new Error("OptionGroup cannot be used with an engine that doesn't specify a version");
130+
throw new ValidationError("OptionGroup cannot be used with an engine that doesn't specify a version", this);
130131
}
131132

132133
props.configurations.forEach(config => this.addConfiguration(config));
@@ -146,7 +147,7 @@ export class OptionGroup extends Resource implements IOptionGroup {
146147

147148
if (configuration.port) {
148149
if (!configuration.vpc) {
149-
throw new Error('`port` and `vpc` must be specified together.');
150+
throw new ValidationError('`port` and `vpc` must be specified together.', this);
150151
}
151152

152153
const securityGroups = configuration.securityGroups && configuration.securityGroups.length > 0

packages/aws-cdk-lib/aws-rds/lib/parameter-group.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Construct } from 'constructs';
22
import { IEngine } from './engine';
33
import { CfnDBClusterParameterGroup, CfnDBParameterGroup } from './rds.generated';
44
import { IResource, Lazy, RemovalPolicy, Resource } from '../../core';
5+
import { ValidationError } from '../../core/lib/errors';
56

67
/**
78
* Options for `IParameterGroup.bindToCluster`.
@@ -143,7 +144,7 @@ export class ParameterGroup extends Resource implements IParameterGroup {
143144

144145
const family = props.engine.parameterGroupFamily;
145146
if (!family) {
146-
throw new Error("ParameterGroup cannot be used with an engine that doesn't specify a version");
147+
throw new ValidationError("ParameterGroup cannot be used with an engine that doesn't specify a version", this);
147148
}
148149
this.family = family;
149150
this.description = props.description;

packages/aws-cdk-lib/aws-rds/lib/private/util.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as ec2 from '../../../aws-ec2';
33
import * as iam from '../../../aws-iam';
44
import * as s3 from '../../../aws-s3';
55
import { RemovalPolicy } from '../../../core';
6+
import { ValidationError } from '../../../core/lib/errors';
67
import { DatabaseSecret } from '../database-secret';
78
import { IEngine } from '../engine';
89
import { CommonRotationUserOptions, Credentials, SnapshotCredentials } from '../props';
@@ -43,7 +44,7 @@ export function setupS3ImportExport(
4344

4445
if (props.s3ImportBuckets && props.s3ImportBuckets.length > 0) {
4546
if (props.s3ImportRole) {
46-
throw new Error('Only one of s3ImportRole or s3ImportBuckets must be specified, not both.');
47+
throw new ValidationError('Only one of s3ImportRole or s3ImportBuckets must be specified, not both.', scope);
4748
}
4849

4950
s3ImportRole = (combineRoles && s3ExportRole) ? s3ExportRole : new iam.Role(scope, 'S3ImportRole', {
@@ -56,7 +57,7 @@ export function setupS3ImportExport(
5657

5758
if (props.s3ExportBuckets && props.s3ExportBuckets.length > 0) {
5859
if (props.s3ExportRole) {
59-
throw new Error('Only one of s3ExportRole or s3ExportBuckets must be specified, not both.');
60+
throw new ValidationError('Only one of s3ExportRole or s3ExportBuckets must be specified, not both.', scope);
6061
}
6162

6263
s3ExportRole = (combineRoles && s3ImportRole) ? s3ImportRole : new iam.Role(scope, 'S3ExportRole', {
@@ -117,7 +118,7 @@ export function renderSnapshotCredentials(scope: Construct, credentials?: Snapsh
117118
let secret = renderedCredentials?.secret;
118119
if (!secret && renderedCredentials?.generatePassword) {
119120
if (!renderedCredentials.username) {
120-
throw new Error('`snapshotCredentials` `username` must be specified when `generatePassword` is set to true');
121+
throw new ValidationError('`snapshotCredentials` `username` must be specified when `generatePassword` is set to true', scope);
121122
}
122123

123124
renderedCredentials = SnapshotCredentials.fromSecret(

0 commit comments

Comments
 (0)