From 0b2db62850913a1af5b0018aff7c71fad6a7714f Mon Sep 17 00:00:00 2001 From: Kaizen Conroy <36202692+kaizencc@users.noreply.github.com> Date: Wed, 22 Jan 2025 05:57:05 -0500 Subject: [PATCH] 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* --- packages/aws-cdk-lib/.eslintrc.js | 2 +- .../aws-rds/lib/aurora-cluster-instance.ts | 5 +- .../aws-cdk-lib/aws-rds/lib/cluster-engine.ts | 9 ++- packages/aws-cdk-lib/aws-rds/lib/cluster.ts | 79 ++++++++++--------- .../aws-rds/lib/instance-engine.ts | 9 ++- packages/aws-cdk-lib/aws-rds/lib/instance.ts | 35 ++++---- .../aws-cdk-lib/aws-rds/lib/option-group.ts | 5 +- .../aws-rds/lib/parameter-group.ts | 3 +- .../aws-cdk-lib/aws-rds/lib/private/util.ts | 7 +- packages/aws-cdk-lib/aws-rds/lib/proxy.ts | 25 +++--- .../aws-rds/lib/serverless-cluster.ts | 33 ++++---- 11 files changed, 111 insertions(+), 101 deletions(-) diff --git a/packages/aws-cdk-lib/.eslintrc.js b/packages/aws-cdk-lib/.eslintrc.js index 3bfb1f9dedb7b..ccfede12dae77 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', 'aws-lambda']; +const modules = ['aws-s3', 'aws-lambda', 'aws-rds']; baseConfig.overrides.push({ files: modules.map(m => `./${m}/lib/**`), rules: { "@cdklabs/no-throw-default-error": ['error'] }, diff --git a/packages/aws-cdk-lib/aws-rds/lib/aurora-cluster-instance.ts b/packages/aws-cdk-lib/aws-rds/lib/aurora-cluster-instance.ts index 184a3530acb5d..8ce1ff7a80540 100644 --- a/packages/aws-cdk-lib/aws-rds/lib/aurora-cluster-instance.ts +++ b/packages/aws-cdk-lib/aws-rds/lib/aurora-cluster-instance.ts @@ -11,6 +11,7 @@ import * as ec2 from '../../aws-ec2'; import { IRole } from '../../aws-iam'; import * as kms from '../../aws-kms'; import { IResource, Resource, Duration, RemovalPolicy, ArnFormat, FeatureFlags } from '../../core'; +import { ValidationError } from '../../core/lib/errors'; import { AURORA_CLUSTER_CHANGE_SCOPE_OF_INSTANCE_PARAMETER_GROUP_WITH_EACH_PARAMETERS } from '../../cx-api'; /** @@ -476,7 +477,7 @@ class AuroraClusterInstance extends Resource implements IAuroraClusterInstance { }); this.tier = props.promotionTier ?? 2; if (this.tier > 15) { - throw new Error('promotionTier must be between 0-15'); + throw new ValidationError('promotionTier must be between 0-15', this); } const isOwnedResource = Resource.isOwnedResource(props.cluster); @@ -499,7 +500,7 @@ class AuroraClusterInstance extends Resource implements IAuroraClusterInstance { const enablePerformanceInsights = props.enablePerformanceInsights || props.performanceInsightRetention !== undefined || props.performanceInsightEncryptionKey !== undefined; if (enablePerformanceInsights && props.enablePerformanceInsights === false) { - throw new Error('`enablePerformanceInsights` disabled, but `performanceInsightRetention` or `performanceInsightEncryptionKey` was set'); + throw new ValidationError('`enablePerformanceInsights` disabled, but `performanceInsightRetention` or `performanceInsightEncryptionKey` was set', this); } this.performanceInsightsEnabled = enablePerformanceInsights; diff --git a/packages/aws-cdk-lib/aws-rds/lib/cluster-engine.ts b/packages/aws-cdk-lib/aws-rds/lib/cluster-engine.ts index 9c183910e1c61..285bc661844d7 100644 --- a/packages/aws-cdk-lib/aws-rds/lib/cluster-engine.ts +++ b/packages/aws-cdk-lib/aws-rds/lib/cluster-engine.ts @@ -4,6 +4,7 @@ import { EngineVersion } from './engine-version'; import { IParameterGroup, ParameterGroup } from './parameter-group'; import * as iam from '../../aws-iam'; import * as secretsmanager from '../../aws-secretsmanager'; +import { ValidationError } from '../../core/lib/errors'; /** * The extra options passed to the `IClusterEngine.bindToCluster` method. @@ -1179,10 +1180,10 @@ class AuroraPostgresClusterEngine extends ClusterEngineBase { // skip validation for unversioned as it might be supported/unsupported. we cannot reliably tell at compile-time if (this.engineVersion?.fullVersion) { if (options.s3ImportRole && !(config.features?.s3Import)) { - throw new Error(`s3Import is not supported for Postgres version: ${this.engineVersion.fullVersion}. Use a version that supports the s3Import feature.`); + throw new ValidationError(`s3Import is not supported for Postgres version: ${this.engineVersion.fullVersion}. Use a version that supports the s3Import feature.`, scope); } if (options.s3ExportRole && !(config.features?.s3Export)) { - throw new Error(`s3Export is not supported for Postgres version: ${this.engineVersion.fullVersion}. Use a version that supports the s3Export feature.`); + throw new ValidationError(`s3Export is not supported for Postgres version: ${this.engineVersion.fullVersion}. Use a version that supports the s3Export feature.`, scope); } } return config; @@ -1190,8 +1191,8 @@ class AuroraPostgresClusterEngine extends ClusterEngineBase { protected defaultParameterGroup(scope: Construct): IParameterGroup | undefined { if (!this.parameterGroupFamily) { - throw new Error('Could not create a new ParameterGroup for an unversioned aurora-postgresql cluster engine. ' + - 'Please either use a versioned engine, or pass an explicit ParameterGroup when creating the cluster'); + throw new ValidationError('Could not create a new ParameterGroup for an unversioned aurora-postgresql cluster engine. ' + + 'Please either use a versioned engine, or pass an explicit ParameterGroup when creating the cluster', scope); } return ParameterGroup.fromParameterGroupName(scope, 'AuroraPostgreSqlDatabaseClusterEngineDefaultParameterGroup', `default.${this.parameterGroupFamily}`); diff --git a/packages/aws-cdk-lib/aws-rds/lib/cluster.ts b/packages/aws-cdk-lib/aws-rds/lib/cluster.ts index 7284a8c42b1e3..c7261601c8e6a 100644 --- a/packages/aws-cdk-lib/aws-rds/lib/cluster.ts +++ b/packages/aws-cdk-lib/aws-rds/lib/cluster.ts @@ -20,6 +20,7 @@ import * as logs from '../../aws-logs'; import * as s3 from '../../aws-s3'; import * as secretsmanager from '../../aws-secretsmanager'; import { Annotations, ArnFormat, Duration, FeatureFlags, Lazy, RemovalPolicy, Resource, Stack, Token, TokenComparison } from '../../core'; +import { ValidationError } from '../../core/lib/errors'; import * as cxapi from '../../cx-api'; /** @@ -637,7 +638,7 @@ export abstract class DatabaseClusterBase extends Resource implements IDatabaseC */ public grantDataApiAccess(grantee: iam.IGrantable): iam.Grant { if (this.enableDataApi === false) { - throw new Error('Cannot grant Data API access when the Data API is disabled'); + throw new ValidationError('Cannot grant Data API access when the Data API is disabled', this); } this.enableDataApi = true; @@ -731,14 +732,14 @@ abstract class DatabaseClusterNew extends DatabaseClusterBase { super(scope, id); if (props.clusterScalabilityType !== undefined && props.clusterScailabilityType !== undefined) { - throw new Error('You cannot specify both clusterScalabilityType and clusterScailabilityType (deprecated). Use clusterScalabilityType.'); + throw new ValidationError('You cannot specify both clusterScalabilityType and clusterScailabilityType (deprecated). Use clusterScalabilityType.', this); } if ((props.vpc && props.instanceProps?.vpc) || (!props.vpc && !props.instanceProps?.vpc)) { - throw new Error('Provide either vpc or instanceProps.vpc, but not both'); + throw new ValidationError('Provide either vpc or instanceProps.vpc, but not both', this); } if ((props.vpcSubnets && props.instanceProps?.vpcSubnets)) { - throw new Error('Provide either vpcSubnets or instanceProps.vpcSubnets, but not both'); + throw new ValidationError('Provide either vpcSubnets or instanceProps.vpcSubnets, but not both', this); } this.vpc = props.instanceProps?.vpc ?? props.vpc!; this.vpcSubnets = props.instanceProps?.vpcSubnets ?? props.vpcSubnets; @@ -779,7 +780,7 @@ abstract class DatabaseClusterNew extends DatabaseClusterBase { let { s3ImportRole, s3ExportRole } = setupS3ImportExport(this, props, combineRoles); if (props.parameterGroup && props.parameters) { - throw new Error('You cannot specify both parameterGroup and parameters'); + throw new ValidationError('You cannot specify both parameterGroup and parameters', this); } const parameterGroup = props.parameterGroup ?? ( props.parameters @@ -832,30 +833,30 @@ abstract class DatabaseClusterNew extends DatabaseClusterBase { const enablePerformanceInsights = props.enablePerformanceInsights || props.performanceInsightRetention !== undefined || props.performanceInsightEncryptionKey !== undefined; if (enablePerformanceInsights && props.enablePerformanceInsights === false) { - throw new Error('`enablePerformanceInsights` disabled, but `performanceInsightRetention` or `performanceInsightEncryptionKey` was set'); + throw new ValidationError('`enablePerformanceInsights` disabled, but `performanceInsightRetention` or `performanceInsightEncryptionKey` was set', this); } if (props.clusterScalabilityType === ClusterScalabilityType.LIMITLESS || props.clusterScailabilityType === ClusterScailabilityType.LIMITLESS) { if (!props.enablePerformanceInsights) { - throw new Error('Performance Insights must be enabled for Aurora Limitless Database.'); + throw new ValidationError('Performance Insights must be enabled for Aurora Limitless Database.', this); } if (!props.performanceInsightRetention || props.performanceInsightRetention < PerformanceInsightRetention.MONTHS_1) { - throw new Error('Performance Insights retention period must be set at least 31 days for Aurora Limitless Database.'); + throw new ValidationError('Performance Insights retention period must be set at least 31 days for Aurora Limitless Database.', this); } if (!props.monitoringInterval || !props.enableClusterLevelEnhancedMonitoring) { - throw new Error('Cluster level enhanced monitoring must be set for Aurora Limitless Database. Please set \'monitoringInterval\' and enable \'enableClusterLevelEnhancedMonitoring\'.'); + throw new ValidationError('Cluster level enhanced monitoring must be set for Aurora Limitless Database. Please set \'monitoringInterval\' and enable \'enableClusterLevelEnhancedMonitoring\'.', this); } if (props.writer || props.readers) { - throw new Error('Aurora Limitless Database does not support readers or writer instances.'); + throw new ValidationError('Aurora Limitless Database does not support readers or writer instances.', this); } if (!props.engine.engineVersion?.fullVersion?.endsWith('limitless')) { - throw new Error(`Aurora Limitless Database requires an engine version that supports it, got ${props.engine.engineVersion?.fullVersion}`); + throw new ValidationError(`Aurora Limitless Database requires an engine version that supports it, got ${props.engine.engineVersion?.fullVersion}`, this); } if (props.storageType !== DBClusterStorageType.AURORA_IOPT1) { - throw new Error(`Aurora Limitless Database requires I/O optimized storage type, got: ${props.storageType}`); + throw new ValidationError(`Aurora Limitless Database requires I/O optimized storage type, got: ${props.storageType}`, this); } if (props.cloudwatchLogsExports === undefined || props.cloudwatchLogsExports.length === 0) { - throw new Error('Aurora Limitless Database requires CloudWatch Logs exports to be set.'); + throw new ValidationError('Aurora Limitless Database requires CloudWatch Logs exports to be set.', this); } } @@ -877,10 +878,10 @@ abstract class DatabaseClusterNew extends DatabaseClusterBase { } if (props.enableClusterLevelEnhancedMonitoring && !props.monitoringInterval) { - throw new Error('`monitoringInterval` must be set when `enableClusterLevelEnhancedMonitoring` is true.'); + throw new ValidationError('`monitoringInterval` must be set when `enableClusterLevelEnhancedMonitoring` is true.', this); } if (props.monitoringInterval && [0, 1, 5, 10, 15, 30, 60].indexOf(props.monitoringInterval.toSeconds()) === -1) { - throw new Error(`'monitoringInterval' must be one of 0, 1, 5, 10, 15, 30, or 60 seconds, got: ${props.monitoringInterval.toSeconds()} seconds.`); + throw new ValidationError(`'monitoringInterval' must be one of 0, 1, 5, 10, 15, 30, or 60 seconds, got: ${props.monitoringInterval.toSeconds()} seconds.`, this); } this.newCfnProps = { @@ -1108,21 +1109,21 @@ abstract class DatabaseClusterNew extends DatabaseClusterBase { private validateServerlessScalingConfig(): void { if (this.serverlessV2MaxCapacity > 256 || this.serverlessV2MaxCapacity < 1) { - throw new Error('serverlessV2MaxCapacity must be >= 1 & <= 256'); + throw new ValidationError('serverlessV2MaxCapacity must be >= 1 & <= 256', this); } if (this.serverlessV2MinCapacity > 256 || this.serverlessV2MinCapacity < 0) { - throw new Error('serverlessV2MinCapacity must be >= 0 & <= 256'); + throw new ValidationError('serverlessV2MinCapacity must be >= 0 & <= 256', this); } if (this.serverlessV2MaxCapacity < this.serverlessV2MinCapacity) { - throw new Error('serverlessV2MaxCapacity must be greater than serverlessV2MinCapacity'); + throw new ValidationError('serverlessV2MaxCapacity must be greater than serverlessV2MinCapacity', this); } const regexp = new RegExp(/^[0-9]+\.?5?$/); if (!regexp.test(this.serverlessV2MaxCapacity.toString()) || !regexp.test(this.serverlessV2MinCapacity.toString())) { - throw new Error('serverlessV2MinCapacity & serverlessV2MaxCapacity must be in 0.5 step increments, received '+ - `min: ${this.serverlessV2MaxCapacity}, max: ${this.serverlessV2MaxCapacity}`); + throw new ValidationError('serverlessV2MinCapacity & serverlessV2MaxCapacity must be in 0.5 step increments, received '+ + `min: ${this.serverlessV2MaxCapacity}, max: ${this.serverlessV2MaxCapacity}`, this); } } @@ -1133,13 +1134,13 @@ abstract class DatabaseClusterNew extends DatabaseClusterBase { */ public addRotationSingleUser(options: RotationSingleUserOptions = {}): secretsmanager.SecretRotation { if (!this.secret) { - throw new Error('Cannot add a single user rotation for a cluster without a secret.'); + throw new ValidationError('Cannot add a single user rotation for a cluster without a secret.', this); } const id = 'RotationSingleUser'; const existing = this.node.tryFindChild(id); if (existing) { - throw new Error('A single user rotation was already added to this cluster.'); + throw new ValidationError('A single user rotation was already added to this cluster.', this); } return new secretsmanager.SecretRotation(this, id, { @@ -1157,7 +1158,7 @@ abstract class DatabaseClusterNew extends DatabaseClusterBase { */ public addRotationMultiUser(id: string, options: RotationMultiUserOptions): secretsmanager.SecretRotation { if (!this.secret) { - throw new Error('Cannot add a multi user rotation for a cluster without a secret.'); + throw new ValidationError('Cannot add a multi user rotation for a cluster without a secret.', this); } return new secretsmanager.SecretRotation(this, id, { @@ -1214,35 +1215,35 @@ class ImportedDatabaseCluster extends DatabaseClusterBase implements IDatabaseCl public get clusterResourceIdentifier() { if (!this._clusterResourceIdentifier) { - throw new Error('Cannot access `clusterResourceIdentifier` of an imported cluster without a clusterResourceIdentifier'); + throw new ValidationError('Cannot access `clusterResourceIdentifier` of an imported cluster without a clusterResourceIdentifier', this); } return this._clusterResourceIdentifier; } public get clusterEndpoint() { if (!this._clusterEndpoint) { - throw new Error('Cannot access `clusterEndpoint` of an imported cluster without an endpoint address and port'); + throw new ValidationError('Cannot access `clusterEndpoint` of an imported cluster without an endpoint address and port', this); } return this._clusterEndpoint; } public get clusterReadEndpoint() { if (!this._clusterReadEndpoint) { - throw new Error('Cannot access `clusterReadEndpoint` of an imported cluster without a readerEndpointAddress and port'); + throw new ValidationError('Cannot access `clusterReadEndpoint` of an imported cluster without a readerEndpointAddress and port', this); } return this._clusterReadEndpoint; } public get instanceIdentifiers() { if (!this._instanceIdentifiers) { - throw new Error('Cannot access `instanceIdentifiers` of an imported cluster without provided instanceIdentifiers'); + throw new ValidationError('Cannot access `instanceIdentifiers` of an imported cluster without provided instanceIdentifiers', this); } return this._instanceIdentifiers; } public get instanceEndpoints() { if (!this._instanceEndpoints) { - throw new Error('Cannot access `instanceEndpoints` of an imported cluster without instanceEndpointAddresses and port'); + throw new ValidationError('Cannot access `instanceEndpoints` of an imported cluster without instanceEndpointAddresses and port', this); } return this._instanceEndpoints; } @@ -1322,11 +1323,11 @@ export class DatabaseCluster extends DatabaseClusterNew { // create the instances for only standard aurora clusters if (props.clusterScalabilityType !== ClusterScalabilityType.LIMITLESS && props.clusterScailabilityType !== ClusterScailabilityType.LIMITLESS) { if ((props.writer || props.readers) && (props.instances || props.instanceProps)) { - throw new Error('Cannot provide writer or readers if instances or instanceProps are provided'); + throw new ValidationError('Cannot provide writer or readers if instances or instanceProps are provided', this); } if (!props.instanceProps && !props.writer) { - throw new Error('writer must be provided'); + throw new ValidationError('writer must be provided', this); } const createdInstances = props.writer ? this._createInstances(props) : legacyCreateInstances(this, props, this.subnetGroup); @@ -1518,7 +1519,7 @@ export class DatabaseClusterFromSnapshot extends DatabaseClusterNew { setLogRetention(this, props); if ((props.writer || props.readers) && (props.instances || props.instanceProps)) { - throw new Error('Cannot provide clusterInstances if instances or instanceProps are provided'); + throw new ValidationError('Cannot provide clusterInstances if instances or instanceProps are provided', this); } const createdInstances = props.writer ? this._createInstances(props) : legacyCreateInstances(this, props, this.subnetGroup); this.instanceIdentifiers = createdInstances.instanceIdentifiers; @@ -1534,7 +1535,7 @@ function setLogRetention(cluster: DatabaseClusterNew, props: DatabaseClusterBase if (props.cloudwatchLogsExports) { const unsupportedLogTypes = props.cloudwatchLogsExports.filter(logType => !props.engine.supportedLogTypes.includes(logType)); if (unsupportedLogTypes.length > 0) { - throw new Error(`Unsupported logs for the current engine type: ${unsupportedLogTypes.join(',')}`); + throw new ValidationError(`Unsupported logs for the current engine type: ${unsupportedLogTypes.join(',')}`, cluster); } if (props.cloudwatchLogsRetention) { @@ -1566,10 +1567,10 @@ function legacyCreateInstances(cluster: DatabaseClusterNew, props: DatabaseClust const instanceCount = props.instances != null ? props.instances : 2; const instanceUpdateBehaviour = props.instanceUpdateBehaviour ?? InstanceUpdateBehaviour.BULK; if (Token.isUnresolved(instanceCount)) { - throw new Error('The number of instances an RDS Cluster consists of cannot be provided as a deploy-time only value!'); + throw new ValidationError('The number of instances an RDS Cluster consists of cannot be provided as a deploy-time only value!', cluster); } if (instanceCount < 1) { - throw new Error('At least one instance is required'); + throw new ValidationError('At least one instance is required', cluster); } const instanceIdentifiers: string[] = []; @@ -1583,7 +1584,7 @@ function legacyCreateInstances(cluster: DatabaseClusterNew, props: DatabaseClust const enablePerformanceInsights = instanceProps.enablePerformanceInsights || instanceProps.performanceInsightRetention !== undefined || instanceProps.performanceInsightEncryptionKey !== undefined; if (enablePerformanceInsights && instanceProps.enablePerformanceInsights === false) { - throw new Error('`enablePerformanceInsights` disabled, but `performanceInsightRetention` or `performanceInsightEncryptionKey` was set'); + throw new ValidationError('`enablePerformanceInsights` disabled, but `performanceInsightRetention` or `performanceInsightEncryptionKey` was set', cluster); } const performanceInsightRetention = enablePerformanceInsights ? (instanceProps.performanceInsightRetention || PerformanceInsightRetention.DEFAULT) @@ -1600,7 +1601,7 @@ function legacyCreateInstances(cluster: DatabaseClusterNew, props: DatabaseClust const instanceType = instanceProps.instanceType ?? ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.MEDIUM); if (instanceProps.parameterGroup && instanceProps.parameters) { - throw new Error('You cannot specify both parameterGroup and parameters'); + throw new ValidationError('You cannot specify both parameterGroup and parameters', cluster); } const instanceParameterGroup = instanceProps.parameterGroup ?? ( @@ -1708,7 +1709,7 @@ function validatePerformanceInsightsSettings( instance.performanceInsightRetention && instance.performanceInsightRetention !== cluster.performanceInsightRetention ) { - throw new Error(`\`performanceInsightRetention\` for each instance must be the same as the one at cluster level, got ${target}: ${instance.performanceInsightRetention}, cluster: ${cluster.performanceInsightRetention}`); + throw new ValidationError(`\`performanceInsightRetention\` for each instance must be the same as the one at cluster level, got ${target}: ${instance.performanceInsightRetention}, cluster: ${cluster.performanceInsightRetention}`, cluster); } // If `performanceInsightEncryptionKey` is enabled on the cluster, the same parameter for each instance must be @@ -1719,11 +1720,11 @@ function validatePerformanceInsightsSettings( const compared = Token.compareStrings(clusterKeyArn, instanceKeyArn); if (compared === TokenComparison.DIFFERENT) { - throw new Error(`\`performanceInsightEncryptionKey\` for each instance must be the same as the one at cluster level, got ${target}: '${instance.performanceInsightEncryptionKey.keyArn}', cluster: '${cluster.performanceInsightEncryptionKey.keyArn}'`); + throw new ValidationError(`\`performanceInsightEncryptionKey\` for each instance must be the same as the one at cluster level, got ${target}: '${instance.performanceInsightEncryptionKey.keyArn}', cluster: '${cluster.performanceInsightEncryptionKey.keyArn}'`, cluster); } // Even if both of cluster and instance keys are unresolved, check if they are the same token. if (compared === TokenComparison.BOTH_UNRESOLVED && clusterKeyArn !== instanceKeyArn) { - throw new Error('`performanceInsightEncryptionKey` for each instance must be the same as the one at cluster level'); + throw new ValidationError('`performanceInsightEncryptionKey` for each instance must be the same as the one at cluster level', cluster); } } } diff --git a/packages/aws-cdk-lib/aws-rds/lib/instance-engine.ts b/packages/aws-cdk-lib/aws-rds/lib/instance-engine.ts index 7fd956629630d..0bf7010718a43 100644 --- a/packages/aws-cdk-lib/aws-rds/lib/instance-engine.ts +++ b/packages/aws-cdk-lib/aws-rds/lib/instance-engine.ts @@ -4,6 +4,7 @@ import { EngineVersion } from './engine-version'; import { IOptionGroup, OptionGroup } from './option-group'; import * as iam from '../../aws-iam'; import * as secretsmanager from '../../aws-secretsmanager'; +import { ValidationError } from '../../core/lib/errors'; /** * The options passed to `IInstanceEngine.bind`. @@ -142,9 +143,9 @@ abstract class InstanceEngineBase implements IInstanceEngine { this.engineFamily = props.engineFamily; } - public bindToInstance(_scope: Construct, options: InstanceEngineBindOptions): InstanceEngineConfig { + public bindToInstance(scope: Construct, options: InstanceEngineBindOptions): InstanceEngineConfig { if (options.timezone && !this.supportsTimezone) { - throw new Error(`timezone property can not be configured for ${this.engineType}`); + throw new ValidationError(`timezone property can not be configured for ${this.engineType}`, scope); } return { features: this.features, @@ -621,7 +622,7 @@ class MariaDbInstanceEngine extends InstanceEngineBase { public bindToInstance(scope: Construct, options: InstanceEngineBindOptions): InstanceEngineConfig { if (options.domain) { - throw new Error(`domain property cannot be configured for ${this.engineType}`); + throw new ValidationError(`domain property cannot be configured for ${this.engineType}`, scope); } return super.bindToInstance(scope, options); } @@ -2828,7 +2829,7 @@ abstract class SqlServerInstanceEngineBase extends InstanceEngineBase { const s3Role = options.s3ImportRole ?? options.s3ExportRole; if (s3Role) { if (options.s3ImportRole && options.s3ExportRole && options.s3ImportRole !== options.s3ExportRole) { - throw new Error('S3 import and export roles must be the same for SQL Server engines'); + throw new ValidationError('S3 import and export roles must be the same for SQL Server engines', scope); } if (!optionGroup) { diff --git a/packages/aws-cdk-lib/aws-rds/lib/instance.ts b/packages/aws-cdk-lib/aws-rds/lib/instance.ts index cfd41de9e680c..6b7a0dfb87981 100644 --- a/packages/aws-cdk-lib/aws-rds/lib/instance.ts +++ b/packages/aws-cdk-lib/aws-rds/lib/instance.ts @@ -18,6 +18,7 @@ import * as logs from '../../aws-logs'; import * as s3 from '../../aws-s3'; import * as secretsmanager from '../../aws-secretsmanager'; import { ArnComponents, ArnFormat, Duration, FeatureFlags, IResource, Lazy, RemovalPolicy, Resource, Stack, Token, Tokenization } from '../../core'; +import { ValidationError } from '../../core/lib/errors'; import * as cxapi from '../../cx-api'; /** @@ -180,15 +181,15 @@ export abstract class DatabaseInstanceBase extends Resource implements IDatabase public grantConnect(grantee: iam.IGrantable, dbUser?: string): iam.Grant { if (this.enableIamAuthentication === false) { - throw new Error('Cannot grant connect when IAM authentication is disabled'); + throw new ValidationError('Cannot grant connect when IAM authentication is disabled', this); } if (!this.instanceResourceId) { - throw new Error('For imported Database Instances, instanceResourceId is required to grantConnect()'); + throw new ValidationError('For imported Database Instances, instanceResourceId is required to grantConnect()', this); } if (!dbUser) { - throw new Error('For imported Database Instances, the dbUser is required to grantConnect()'); + throw new ValidationError('For imported Database Instances, the dbUser is required to grantConnect()', this); } this.enableIamAuthentication = true; @@ -784,12 +785,12 @@ abstract class DatabaseInstanceNew extends DatabaseInstanceBase implements IData this.vpc = props.vpc; if (props.vpcSubnets && props.vpcPlacement) { - throw new Error('Only one of `vpcSubnets` or `vpcPlacement` can be specified'); + throw new ValidationError('Only one of `vpcSubnets` or `vpcPlacement` can be specified', this); } this.vpcPlacement = props.vpcSubnets ?? props.vpcPlacement; if (props.multiAz === true && props.availabilityZone) { - throw new Error('Requesting a specific availability zone is not valid for Multi-AZ instances'); + throw new ValidationError('Requesting a specific availability zone is not valid for Multi-AZ instances', this); } const subnetGroup = props.subnetGroup ?? new SubnetGroup(this, 'SubnetGroup', { @@ -820,12 +821,12 @@ abstract class DatabaseInstanceNew extends DatabaseInstanceBase implements IData const storageType = props.storageType ?? StorageType.GP2; const iops = defaultIops(storageType, props.iops); if (props.storageThroughput && storageType !== StorageType.GP3) { - throw new Error(`The storage throughput can only be specified with GP3 storage type. Got ${storageType}.`); + throw new ValidationError(`The storage throughput can only be specified with GP3 storage type. Got ${storageType}.`, this); } if (storageType === StorageType.GP3 && props.storageThroughput && iops && !Token.isUnresolved(props.storageThroughput) && !Token.isUnresolved(iops) && props.storageThroughput/iops > 0.25) { - throw new Error(`The maximum ratio of storage throughput to IOPS is 0.25. Got ${props.storageThroughput/iops}.`); + throw new ValidationError(`The maximum ratio of storage throughput to IOPS is 0.25. Got ${props.storageThroughput/iops}.`, this); } this.cloudwatchLogGroups = {}; @@ -837,7 +838,7 @@ abstract class DatabaseInstanceNew extends DatabaseInstanceBase implements IData const enablePerformanceInsights = props.enablePerformanceInsights || props.performanceInsightRetention !== undefined || props.performanceInsightEncryptionKey !== undefined; if (enablePerformanceInsights && props.enablePerformanceInsights === false) { - throw new Error('`enablePerformanceInsights` disabled, but `performanceInsightRetention` or `performanceInsightEncryptionKey` was set'); + throw new ValidationError('`enablePerformanceInsights` disabled, but `performanceInsightRetention` or `performanceInsightEncryptionKey` was set', this); } if (props.domain) { @@ -1019,13 +1020,13 @@ abstract class DatabaseInstanceSource extends DatabaseInstanceNew implements IDa const engineFeatures = engineConfig.features; if (s3ImportRole) { if (!engineFeatures?.s3Import) { - throw new Error(`Engine '${engineDescription(props.engine)}' does not support S3 import`); + throw new ValidationError(`Engine '${engineDescription(props.engine)}' does not support S3 import`, this); } instanceAssociatedRoles.push({ roleArn: s3ImportRole.roleArn, featureName: engineFeatures?.s3Import }); } if (s3ExportRole) { if (!engineFeatures?.s3Export) { - throw new Error(`Engine '${engineDescription(props.engine)}' does not support S3 export`); + throw new ValidationError(`Engine '${engineDescription(props.engine)}' does not support S3 export`, this); } // only add the export feature if it's different from the import feature if (engineFeatures.s3Import !== engineFeatures?.s3Export) { @@ -1036,7 +1037,7 @@ abstract class DatabaseInstanceSource extends DatabaseInstanceNew implements IDa this.instanceType = props.instanceType ?? ec2.InstanceType.of(ec2.InstanceClass.M5, ec2.InstanceSize.LARGE); if (props.parameterGroup && props.parameters) { - throw new Error('You cannot specify both parameterGroup and parameters'); + throw new ValidationError('You cannot specify both parameterGroup and parameters', this); } const dbParameterGroupName = props.parameters @@ -1069,13 +1070,13 @@ abstract class DatabaseInstanceSource extends DatabaseInstanceNew implements IDa */ public addRotationSingleUser(options: RotationSingleUserOptions = {}): secretsmanager.SecretRotation { if (!this.secret) { - throw new Error('Cannot add single user rotation for an instance without secret.'); + throw new ValidationError('Cannot add single user rotation for an instance without secret.', this); } const id = 'RotationSingleUser'; const existing = this.node.tryFindChild(id); if (existing) { - throw new Error('A single user rotation was already added to this instance.'); + throw new ValidationError('A single user rotation was already added to this instance.', this); } return new secretsmanager.SecretRotation(this, id, { @@ -1092,7 +1093,7 @@ abstract class DatabaseInstanceSource extends DatabaseInstanceNew implements IDa */ public addRotationMultiUser(id: string, options: RotationMultiUserOptions): secretsmanager.SecretRotation { if (!this.secret) { - throw new Error('Cannot add multi user rotation for an instance without secret.'); + throw new ValidationError('Cannot add multi user rotation for an instance without secret.', this); } return new secretsmanager.SecretRotation(this, id, { @@ -1115,7 +1116,7 @@ abstract class DatabaseInstanceSource extends DatabaseInstanceNew implements IDa public grantConnect(grantee: iam.IGrantable, dbUser?: string): iam.Grant { if (!dbUser) { if (!this.secret) { - throw new Error('A secret or dbUser is required to grantConnect()'); + throw new ValidationError('A secret or dbUser is required to grantConnect()', this); } dbUser = this.secret.secretValueFromJson('username').unsafeUnwrap(); @@ -1248,7 +1249,7 @@ export class DatabaseInstanceFromSnapshot extends DatabaseInstanceSource impleme let secret = credentials?.secret; if (!secret && credentials?.generatePassword) { if (!credentials.username) { - throw new Error('`credentials` `username` must be specified when `generatePassword` is set to true'); + throw new ValidationError('`credentials` `username` must be specified when `generatePassword` is set to true', this); } secret = new DatabaseSecret(this, 'Secret', { @@ -1351,7 +1352,7 @@ export class DatabaseInstanceReadReplica extends DatabaseInstanceNew implements if (props.sourceDatabaseInstance.engine && !props.sourceDatabaseInstance.engine.supportsReadReplicaBackups && props.backupRetention) { - throw new Error(`Cannot set 'backupRetention', as engine '${engineDescription(props.sourceDatabaseInstance.engine)}' does not support automatic backups for read replicas`); + throw new ValidationError(`Cannot set 'backupRetention', as engine '${engineDescription(props.sourceDatabaseInstance.engine)}' does not support automatic backups for read replicas`, this); } // The read replica instance always uses the same engine as the source instance diff --git a/packages/aws-cdk-lib/aws-rds/lib/option-group.ts b/packages/aws-cdk-lib/aws-rds/lib/option-group.ts index 8b4ca531e804a..9b45f16d3049f 100644 --- a/packages/aws-cdk-lib/aws-rds/lib/option-group.ts +++ b/packages/aws-cdk-lib/aws-rds/lib/option-group.ts @@ -3,6 +3,7 @@ import { IInstanceEngine } from './instance-engine'; import { CfnOptionGroup } from './rds.generated'; import * as ec2 from '../../aws-ec2'; import { IResource, Lazy, Resource } from '../../core'; +import { ValidationError } from '../../core/lib/errors'; /** * An option group @@ -126,7 +127,7 @@ export class OptionGroup extends Resource implements IOptionGroup { const majorEngineVersion = props.engine.engineVersion?.majorVersion; if (!majorEngineVersion) { - throw new Error("OptionGroup cannot be used with an engine that doesn't specify a version"); + throw new ValidationError("OptionGroup cannot be used with an engine that doesn't specify a version", this); } props.configurations.forEach(config => this.addConfiguration(config)); @@ -146,7 +147,7 @@ export class OptionGroup extends Resource implements IOptionGroup { if (configuration.port) { if (!configuration.vpc) { - throw new Error('`port` and `vpc` must be specified together.'); + throw new ValidationError('`port` and `vpc` must be specified together.', this); } const securityGroups = configuration.securityGroups && configuration.securityGroups.length > 0 diff --git a/packages/aws-cdk-lib/aws-rds/lib/parameter-group.ts b/packages/aws-cdk-lib/aws-rds/lib/parameter-group.ts index ef1f43aac56b1..b8763044b90f6 100644 --- a/packages/aws-cdk-lib/aws-rds/lib/parameter-group.ts +++ b/packages/aws-cdk-lib/aws-rds/lib/parameter-group.ts @@ -2,6 +2,7 @@ import { Construct } from 'constructs'; import { IEngine } from './engine'; import { CfnDBClusterParameterGroup, CfnDBParameterGroup } from './rds.generated'; import { IResource, Lazy, RemovalPolicy, Resource } from '../../core'; +import { ValidationError } from '../../core/lib/errors'; /** * Options for `IParameterGroup.bindToCluster`. @@ -143,7 +144,7 @@ export class ParameterGroup extends Resource implements IParameterGroup { const family = props.engine.parameterGroupFamily; if (!family) { - throw new Error("ParameterGroup cannot be used with an engine that doesn't specify a version"); + throw new ValidationError("ParameterGroup cannot be used with an engine that doesn't specify a version", this); } this.family = family; this.description = props.description; diff --git a/packages/aws-cdk-lib/aws-rds/lib/private/util.ts b/packages/aws-cdk-lib/aws-rds/lib/private/util.ts index 30f16b9b30855..e2d524df371fb 100644 --- a/packages/aws-cdk-lib/aws-rds/lib/private/util.ts +++ b/packages/aws-cdk-lib/aws-rds/lib/private/util.ts @@ -3,6 +3,7 @@ import * as ec2 from '../../../aws-ec2'; import * as iam from '../../../aws-iam'; import * as s3 from '../../../aws-s3'; import { RemovalPolicy } from '../../../core'; +import { ValidationError } from '../../../core/lib/errors'; import { DatabaseSecret } from '../database-secret'; import { IEngine } from '../engine'; import { CommonRotationUserOptions, Credentials, SnapshotCredentials } from '../props'; @@ -43,7 +44,7 @@ export function setupS3ImportExport( if (props.s3ImportBuckets && props.s3ImportBuckets.length > 0) { if (props.s3ImportRole) { - throw new Error('Only one of s3ImportRole or s3ImportBuckets must be specified, not both.'); + throw new ValidationError('Only one of s3ImportRole or s3ImportBuckets must be specified, not both.', scope); } s3ImportRole = (combineRoles && s3ExportRole) ? s3ExportRole : new iam.Role(scope, 'S3ImportRole', { @@ -56,7 +57,7 @@ export function setupS3ImportExport( if (props.s3ExportBuckets && props.s3ExportBuckets.length > 0) { if (props.s3ExportRole) { - throw new Error('Only one of s3ExportRole or s3ExportBuckets must be specified, not both.'); + throw new ValidationError('Only one of s3ExportRole or s3ExportBuckets must be specified, not both.', scope); } s3ExportRole = (combineRoles && s3ImportRole) ? s3ImportRole : new iam.Role(scope, 'S3ExportRole', { @@ -117,7 +118,7 @@ export function renderSnapshotCredentials(scope: Construct, credentials?: Snapsh let secret = renderedCredentials?.secret; if (!secret && renderedCredentials?.generatePassword) { if (!renderedCredentials.username) { - throw new Error('`snapshotCredentials` `username` must be specified when `generatePassword` is set to true'); + throw new ValidationError('`snapshotCredentials` `username` must be specified when `generatePassword` is set to true', scope); } renderedCredentials = SnapshotCredentials.fromSecret( diff --git a/packages/aws-cdk-lib/aws-rds/lib/proxy.ts b/packages/aws-cdk-lib/aws-rds/lib/proxy.ts index 6c8ffe2fb1695..a3af2c9bf100d 100644 --- a/packages/aws-cdk-lib/aws-rds/lib/proxy.ts +++ b/packages/aws-cdk-lib/aws-rds/lib/proxy.ts @@ -8,6 +8,7 @@ import * as ec2 from '../../aws-ec2'; import * as iam from '../../aws-iam'; import * as secretsmanager from '../../aws-secretsmanager'; import * as cdk from '../../core'; +import { ValidationError } from '../../core/lib/errors'; import * as cxapi from '../../cx-api'; /** @@ -98,14 +99,14 @@ export class ProxyTarget { if (!engine) { const errorResource = this.dbCluster ?? this.dbInstance; - throw new Error(`Could not determine engine for proxy target '${errorResource?.node.path}'. ` + - 'Please provide it explicitly when importing the resource'); + throw new ValidationError(`Could not determine engine for proxy target '${errorResource?.node.path}'. ` + + 'Please provide it explicitly when importing the resource', proxy); } const engineFamily = engine.engineFamily; if (!engineFamily) { - throw new Error('RDS proxies require an engine family to be specified on the database cluster or instance. ' + - `No family specified for engine '${engineDescription(engine)}'`); + throw new ValidationError('RDS proxies require an engine family to be specified on the database cluster or instance. ' + + `No family specified for engine '${engineDescription(engine)}'`, proxy); } // allow connecting to the Cluster/Instance from the Proxy @@ -374,7 +375,7 @@ abstract class DatabaseProxyBase extends cdk.Resource implements IDatabaseProxy public grantConnect(grantee: iam.IGrantable, dbUser?: string): iam.Grant { if (!dbUser) { - throw new Error('For imported Database Proxies, the dbUser is required in grantConnect()'); + throw new ValidationError('For imported Database Proxies, the dbUser is required in grantConnect()', this); } const scopeStack = cdk.Stack.of(this); const proxyGeneratedId = scopeStack.splitArn(this.dbProxyArn, cdk.ArnFormat.COLON_RESOURCE_NAME).resourceName; @@ -474,7 +475,7 @@ export class DatabaseProxy extends DatabaseProxyBase const bindResult = props.proxyTarget.bind(this); if (props.secrets.length < 1) { - throw new Error('One or more secrets are required.'); + throw new ValidationError('One or more secrets are required.', this); } this.secrets = props.secrets; @@ -515,7 +516,7 @@ export class DatabaseProxy extends DatabaseProxyBase } if (!!dbInstanceIdentifiers && !!dbClusterIdentifiers) { - throw new Error('Cannot specify both dbInstanceIdentifiers and dbClusterIdentifiers'); + throw new ValidationError('Cannot specify both dbInstanceIdentifiers and dbClusterIdentifiers', this); } const proxyTargetGroup = new CfnDBProxyTargetGroup(this, 'ProxyTargetGroup', { @@ -565,7 +566,7 @@ export class DatabaseProxy extends DatabaseProxyBase public grantConnect(grantee: iam.IGrantable, dbUser?: string): iam.Grant { if (!dbUser) { if (this.secrets.length > 1) { - throw new Error('When the Proxy contains multiple Secrets, you must pass a dbUser explicitly to grantConnect()'); + throw new ValidationError('When the Proxy contains multiple Secrets, you must pass a dbUser explicitly to grantConnect()', this); } // 'username' is the field RDS uses here, // see https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/rds-proxy.html#rds-proxy-secrets-arns @@ -577,16 +578,16 @@ export class DatabaseProxy extends DatabaseProxyBase private validateClientPasswordAuthType(engineFamily: string, clientPasswordAuthType?: ClientPasswordAuthType) { if (!clientPasswordAuthType || cdk.Token.isUnresolved(clientPasswordAuthType)) return; if (clientPasswordAuthType === ClientPasswordAuthType.MYSQL_NATIVE_PASSWORD && engineFamily !== 'MYSQL') { - throw new Error(`${ClientPasswordAuthType.MYSQL_NATIVE_PASSWORD} client password authentication type requires MYSQL engineFamily, got ${engineFamily}`); + throw new ValidationError(`${ClientPasswordAuthType.MYSQL_NATIVE_PASSWORD} client password authentication type requires MYSQL engineFamily, got ${engineFamily}`, this); } if (clientPasswordAuthType === ClientPasswordAuthType.POSTGRES_SCRAM_SHA_256 && engineFamily !== 'POSTGRESQL') { - throw new Error(`${ClientPasswordAuthType.POSTGRES_SCRAM_SHA_256} client password authentication type requires POSTGRESQL engineFamily, got ${engineFamily}`); + throw new ValidationError(`${ClientPasswordAuthType.POSTGRES_SCRAM_SHA_256} client password authentication type requires POSTGRESQL engineFamily, got ${engineFamily}`, this); } if (clientPasswordAuthType === ClientPasswordAuthType.POSTGRES_MD5 && engineFamily !== 'POSTGRESQL') { - throw new Error(`${ClientPasswordAuthType.POSTGRES_MD5} client password authentication type requires POSTGRESQL engineFamily, got ${engineFamily}`); + throw new ValidationError(`${ClientPasswordAuthType.POSTGRES_MD5} client password authentication type requires POSTGRESQL engineFamily, got ${engineFamily}`, this); } if (clientPasswordAuthType === ClientPasswordAuthType.SQL_SERVER_AUTHENTICATION && engineFamily !== 'SQLSERVER') { - throw new Error(`${ClientPasswordAuthType.SQL_SERVER_AUTHENTICATION} client password authentication type requires SQLSERVER engineFamily, got ${engineFamily}`); + throw new ValidationError(`${ClientPasswordAuthType.SQL_SERVER_AUTHENTICATION} client password authentication type requires SQLSERVER engineFamily, got ${engineFamily}`, this); } } } diff --git a/packages/aws-cdk-lib/aws-rds/lib/serverless-cluster.ts b/packages/aws-cdk-lib/aws-rds/lib/serverless-cluster.ts index 572b0bb2fc3c2..f63b19abdfbd8 100644 --- a/packages/aws-cdk-lib/aws-rds/lib/serverless-cluster.ts +++ b/packages/aws-cdk-lib/aws-rds/lib/serverless-cluster.ts @@ -13,6 +13,7 @@ import * as iam from '../../aws-iam'; import * as kms from '../../aws-kms'; import * as secretsmanager from '../../aws-secretsmanager'; import { Resource, Duration, Token, Annotations, RemovalPolicy, IResource, Stack, Lazy, FeatureFlags, ArnFormat } from '../../core'; +import { ValidationError } from '../../core/lib/errors'; import * as cxapi from '../../cx-api'; /** @@ -361,7 +362,7 @@ abstract class ServerlessClusterBase extends Resource implements IServerlessClus */ public grantDataApiAccess(grantee: iam.IGrantable): iam.Grant { if (this.enableDataApi === false) { - throw new Error('Cannot grant Data API access when the Data API is disabled'); + throw new ValidationError('Cannot grant Data API access when the Data API is disabled', this); } this.enableDataApi = true; @@ -402,13 +403,13 @@ abstract class ServerlessClusterNew extends ServerlessClusterBase { if (props.vpc === undefined) { if (props.vpcSubnets !== undefined) { - throw new Error('A VPC is required to use vpcSubnets in ServerlessCluster. Please add a VPC or remove vpcSubnets'); + throw new ValidationError('A VPC is required to use vpcSubnets in ServerlessCluster. Please add a VPC or remove vpcSubnets', this); } if (props.subnetGroup !== undefined) { - throw new Error('A VPC is required to use subnetGroup in ServerlessCluster. Please add a VPC or remove subnetGroup'); + throw new ValidationError('A VPC is required to use subnetGroup in ServerlessCluster. Please add a VPC or remove subnetGroup', this); } if (props.securityGroups !== undefined) { - throw new Error('A VPC is required to use securityGroups in ServerlessCluster. Please add a VPC or remove securityGroups'); + throw new ValidationError('A VPC is required to use securityGroups in ServerlessCluster. Please add a VPC or remove securityGroups', this); } } @@ -440,7 +441,7 @@ abstract class ServerlessClusterNew extends ServerlessClusterBase { if (props.backupRetention) { const backupRetentionDays = props.backupRetention.toDays(); if (backupRetentionDays < 1 || backupRetentionDays > 35) { - throw new Error(`backup retention period must be between 1 and 35 days. received: ${backupRetentionDays}`); + throw new ValidationError(`backup retention period must be between 1 and 35 days. received: ${backupRetentionDays}`, this); } } @@ -484,16 +485,16 @@ abstract class ServerlessClusterNew extends ServerlessClusterBase { const timeout = options.timeout?.toSeconds(); if (minCapacity && maxCapacity && minCapacity > maxCapacity) { - throw new Error('maximum capacity must be greater than or equal to minimum capacity.'); + throw new ValidationError('maximum capacity must be greater than or equal to minimum capacity.', this); } const secondsToAutoPause = options.autoPause?.toSeconds(); if (secondsToAutoPause && (secondsToAutoPause < 300 || secondsToAutoPause > 86400)) { - throw new Error('auto pause time must be between 5 minutes and 1 day.'); + throw new ValidationError('auto pause time must be between 5 minutes and 1 day.', this); } if (timeout && (timeout < 60 || timeout > 600)) { - throw new Error(`timeout must be between 60 and 600 seconds, but got ${timeout} seconds.`); + throw new ValidationError(`timeout must be between 60 and 600 seconds, but got ${timeout} seconds.`, this); } return { @@ -595,17 +596,17 @@ export class ServerlessCluster extends ServerlessClusterNew { */ public addRotationSingleUser(options: RotationSingleUserOptions = {}): secretsmanager.SecretRotation { if (!this.secret) { - throw new Error('Cannot add single user rotation for a cluster without secret.'); + throw new ValidationError('Cannot add single user rotation for a cluster without secret.', this); } if (this.vpc === undefined) { - throw new Error('Cannot add single user rotation for a cluster without VPC.'); + throw new ValidationError('Cannot add single user rotation for a cluster without VPC.', this); } const id = 'RotationSingleUser'; const existing = this.node.tryFindChild(id); if (existing) { - throw new Error('A single user rotation was already added to this cluster.'); + throw new ValidationError('A single user rotation was already added to this cluster.', this); } return new secretsmanager.SecretRotation(this, id, { @@ -622,11 +623,11 @@ export class ServerlessCluster extends ServerlessClusterNew { */ public addRotationMultiUser(id: string, options: RotationMultiUserOptions): secretsmanager.SecretRotation { if (!this.secret) { - throw new Error('Cannot add multi user rotation for a cluster without secret.'); + throw new ValidationError('Cannot add multi user rotation for a cluster without secret.', this); } if (this.vpc === undefined) { - throw new Error('Cannot add multi user rotation for a cluster without VPC.'); + throw new ValidationError('Cannot add multi user rotation for a cluster without VPC.', this); } return new secretsmanager.SecretRotation(this, id, { @@ -673,14 +674,14 @@ class ImportedServerlessCluster extends ServerlessClusterBase implements IServer public get clusterEndpoint() { if (!this._clusterEndpoint) { - throw new Error('Cannot access `clusterEndpoint` of an imported cluster without an endpoint address and port'); + throw new ValidationError('Cannot access `clusterEndpoint` of an imported cluster without an endpoint address and port', this); } return this._clusterEndpoint; } public get clusterReadEndpoint() { if (!this._clusterReadEndpoint) { - throw new Error('Cannot access `clusterReadEndpoint` of an imported cluster without a readerEndpointAddress and port'); + throw new ValidationError('Cannot access `clusterReadEndpoint` of an imported cluster without a readerEndpointAddress and port', this); } return this._clusterReadEndpoint; } @@ -728,7 +729,7 @@ export class ServerlessClusterFromSnapshot extends ServerlessClusterNew { let secret = credentials?.secret; if (!secret && credentials?.generatePassword) { if (!credentials.username) { - throw new Error('`credentials` `username` must be specified when `generatePassword` is set to true'); + throw new ValidationError('`credentials` `username` must be specified when `generatePassword` is set to true', this); } secret = new DatabaseSecret(this, 'Secret', {