From 5af3c5c593997c9e38859c3ce8dd779193189250 Mon Sep 17 00:00:00 2001 From: Kaizen Conroy Date: Tue, 21 Jan 2025 18:19:25 -0500 Subject: [PATCH 1/4] feat(rds); throw `ValidationError` instead of untyped errors --- .../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 ++++---- 10 files changed, 110 insertions(+), 100 deletions(-) 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..8ec165cca1f3f 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 @@ -12,6 +12,7 @@ import { IRole } from '../../aws-iam'; import * as kms from '../../aws-kms'; import { IResource, Resource, Duration, RemovalPolicy, ArnFormat, FeatureFlags } from '../../core'; import { AURORA_CLUSTER_CHANGE_SCOPE_OF_INSTANCE_PARAMETER_GROUP_WITH_EACH_PARAMETERS } from '../../cx-api'; +import { ValidationError } from '../../core/lib/errors'; /** * Options for binding the instance to the cluster @@ -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..97fccf1d9b67f 100644 --- a/packages/aws-cdk-lib/aws-rds/lib/cluster.ts +++ b/packages/aws-cdk-lib/aws-rds/lib/cluster.ts @@ -21,6 +21,7 @@ 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 * as cxapi from '../../cx-api'; +import { ValidationError } from '../../core/lib/errors'; /** * Common properties for a new database cluster or cluster from snapshot. @@ -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..1ed1431e43b7e 100644 --- a/packages/aws-cdk-lib/aws-rds/lib/instance.ts +++ b/packages/aws-cdk-lib/aws-rds/lib/instance.ts @@ -19,6 +19,7 @@ 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 * as cxapi from '../../cx-api'; +import { ValidationError } from '../../core/lib/errors'; /** * A database instance @@ -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..a337b41c7cac4 100644 --- a/packages/aws-cdk-lib/aws-rds/lib/private/util.ts +++ b/packages/aws-cdk-lib/aws-rds/lib/private/util.ts @@ -6,6 +6,7 @@ import { RemovalPolicy } from '../../../core'; import { DatabaseSecret } from '../database-secret'; import { IEngine } from '../engine'; import { CommonRotationUserOptions, Credentials, SnapshotCredentials } from '../props'; +import { ValidationError } from '../../../core/lib/errors'; /** * The default set of characters we exclude from generated passwords for database users. @@ -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..5b0b866c60472 100644 --- a/packages/aws-cdk-lib/aws-rds/lib/proxy.ts +++ b/packages/aws-cdk-lib/aws-rds/lib/proxy.ts @@ -9,6 +9,7 @@ import * as iam from '../../aws-iam'; import * as secretsmanager from '../../aws-secretsmanager'; import * as cdk from '../../core'; import * as cxapi from '../../cx-api'; +import { ValidationError } from '../../core/lib/errors'; /** * Client password authentication type used by a proxy to log in as a specific database user. @@ -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', this); } 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)}'`, this); } // 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..d15eaf07d8574 100644 --- a/packages/aws-cdk-lib/aws-rds/lib/serverless-cluster.ts +++ b/packages/aws-cdk-lib/aws-rds/lib/serverless-cluster.ts @@ -14,6 +14,7 @@ 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 * as cxapi from '../../cx-api'; +import { ValidationError } from '../../core/lib/errors'; /** * Interface representing a serverless database cluster. @@ -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', { From ff535e04b9f1a753d71891d241dce9bd399b6865 Mon Sep 17 00:00:00 2001 From: Kaizen Conroy Date: Tue, 21 Jan 2025 18:22:25 -0500 Subject: [PATCH 2/4] lint --- packages/aws-cdk-lib/aws-rds/lib/aurora-cluster-instance.ts | 2 +- packages/aws-cdk-lib/aws-rds/lib/cluster.ts | 2 +- packages/aws-cdk-lib/aws-rds/lib/instance.ts | 2 +- packages/aws-cdk-lib/aws-rds/lib/private/util.ts | 2 +- packages/aws-cdk-lib/aws-rds/lib/proxy.ts | 6 +++--- packages/aws-cdk-lib/aws-rds/lib/serverless-cluster.ts | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) 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 8ec165cca1f3f..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,8 +11,8 @@ 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 { AURORA_CLUSTER_CHANGE_SCOPE_OF_INSTANCE_PARAMETER_GROUP_WITH_EACH_PARAMETERS } from '../../cx-api'; import { ValidationError } from '../../core/lib/errors'; +import { AURORA_CLUSTER_CHANGE_SCOPE_OF_INSTANCE_PARAMETER_GROUP_WITH_EACH_PARAMETERS } from '../../cx-api'; /** * Options for binding the instance to the cluster diff --git a/packages/aws-cdk-lib/aws-rds/lib/cluster.ts b/packages/aws-cdk-lib/aws-rds/lib/cluster.ts index 97fccf1d9b67f..c7261601c8e6a 100644 --- a/packages/aws-cdk-lib/aws-rds/lib/cluster.ts +++ b/packages/aws-cdk-lib/aws-rds/lib/cluster.ts @@ -20,8 +20,8 @@ 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 * as cxapi from '../../cx-api'; import { ValidationError } from '../../core/lib/errors'; +import * as cxapi from '../../cx-api'; /** * Common properties for a new database cluster or cluster from snapshot. diff --git a/packages/aws-cdk-lib/aws-rds/lib/instance.ts b/packages/aws-cdk-lib/aws-rds/lib/instance.ts index 1ed1431e43b7e..6b7a0dfb87981 100644 --- a/packages/aws-cdk-lib/aws-rds/lib/instance.ts +++ b/packages/aws-cdk-lib/aws-rds/lib/instance.ts @@ -18,8 +18,8 @@ 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 * as cxapi from '../../cx-api'; import { ValidationError } from '../../core/lib/errors'; +import * as cxapi from '../../cx-api'; /** * A database instance 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 a337b41c7cac4..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,10 +3,10 @@ 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'; -import { ValidationError } from '../../../core/lib/errors'; /** * The default set of characters we exclude from generated passwords for database users. diff --git a/packages/aws-cdk-lib/aws-rds/lib/proxy.ts b/packages/aws-cdk-lib/aws-rds/lib/proxy.ts index 5b0b866c60472..a3af2c9bf100d 100644 --- a/packages/aws-cdk-lib/aws-rds/lib/proxy.ts +++ b/packages/aws-cdk-lib/aws-rds/lib/proxy.ts @@ -8,8 +8,8 @@ import * as ec2 from '../../aws-ec2'; import * as iam from '../../aws-iam'; import * as secretsmanager from '../../aws-secretsmanager'; import * as cdk from '../../core'; -import * as cxapi from '../../cx-api'; import { ValidationError } from '../../core/lib/errors'; +import * as cxapi from '../../cx-api'; /** * Client password authentication type used by a proxy to log in as a specific database user. @@ -100,13 +100,13 @@ export class ProxyTarget { if (!engine) { const errorResource = this.dbCluster ?? this.dbInstance; throw new ValidationError(`Could not determine engine for proxy target '${errorResource?.node.path}'. ` + - 'Please provide it explicitly when importing the resource', this); + 'Please provide it explicitly when importing the resource', proxy); } const engineFamily = engine.engineFamily; if (!engineFamily) { 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)}'`, this); + `No family specified for engine '${engineDescription(engine)}'`, proxy); } // allow connecting to the Cluster/Instance from the Proxy 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 d15eaf07d8574..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,8 +13,8 @@ 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 * as cxapi from '../../cx-api'; import { ValidationError } from '../../core/lib/errors'; +import * as cxapi from '../../cx-api'; /** * Interface representing a serverless database cluster. From 1ad42085ef0e0c55508b39e7897c91087f66fe23 Mon Sep 17 00:00:00 2001 From: Kaizen Conroy Date: Tue, 21 Jan 2025 18:31:04 -0500 Subject: [PATCH 3/4] eslint --- packages/aws-cdk-lib/.eslintrc.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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'] }, From 88d74c064ad6fea7386ab767b2d6b57d9d3244da Mon Sep 17 00:00:00 2001 From: Kaizen Conroy Date: Tue, 21 Jan 2025 18:41:37 -0500 Subject: [PATCH 4/4] feat(sns): throw `ValidationError` instead of untyped errors --- packages/aws-cdk-lib/.eslintrc.js | 2 +- .../aws-sns/lib/subscription-filter.ts | 8 ++-- .../aws-cdk-lib/aws-sns/lib/subscription.ts | 40 ++++++++++--------- .../aws-cdk-lib/aws-sns/lib/topic-base.ts | 3 +- packages/aws-cdk-lib/aws-sns/lib/topic.ts | 23 ++++++----- 5 files changed, 41 insertions(+), 35 deletions(-) diff --git a/packages/aws-cdk-lib/.eslintrc.js b/packages/aws-cdk-lib/.eslintrc.js index ccfede12dae77..39021406fb36c 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', 'aws-rds']; +const modules = ['aws-s3', 'aws-lambda', 'aws-rds', 'aws-sns']; baseConfig.overrides.push({ files: modules.map(m => `./${m}/lib/**`), rules: { "@cdklabs/no-throw-default-error": ['error'] }, diff --git a/packages/aws-cdk-lib/aws-sns/lib/subscription-filter.ts b/packages/aws-cdk-lib/aws-sns/lib/subscription-filter.ts index fce85aa23fc32..81067364c5e64 100644 --- a/packages/aws-cdk-lib/aws-sns/lib/subscription-filter.ts +++ b/packages/aws-cdk-lib/aws-sns/lib/subscription-filter.ts @@ -1,3 +1,5 @@ +import { UnscopedValidationError } from '../../core/lib/errors'; + /** * Conditions that can be applied to string attributes. */ @@ -131,10 +133,10 @@ export class SubscriptionFilter { const conditions = new Array(); if (stringConditions.whitelist && stringConditions.allowlist) { - throw new Error('`whitelist` is deprecated; please use `allowlist` instead'); + throw new UnscopedValidationError('`whitelist` is deprecated; please use `allowlist` instead'); } if (stringConditions.blacklist && stringConditions.denylist) { - throw new Error('`blacklist` is deprecated; please use `denylist` instead'); + throw new UnscopedValidationError('`blacklist` is deprecated; please use `denylist` instead'); } const allowlist = stringConditions.allowlist ?? stringConditions.whitelist; const denylist = stringConditions.denylist ?? stringConditions.blacklist; @@ -165,7 +167,7 @@ export class SubscriptionFilter { const conditions = new Array(); if (numericConditions.whitelist && numericConditions.allowlist) { - throw new Error('`whitelist` is deprecated; please use `allowlist` instead'); + throw new UnscopedValidationError('`whitelist` is deprecated; please use `allowlist` instead'); } const allowlist = numericConditions.allowlist ?? numericConditions.whitelist; diff --git a/packages/aws-cdk-lib/aws-sns/lib/subscription.ts b/packages/aws-cdk-lib/aws-sns/lib/subscription.ts index 2b6cc1c073bc7..1ea11da82c5fb 100644 --- a/packages/aws-cdk-lib/aws-sns/lib/subscription.ts +++ b/packages/aws-cdk-lib/aws-sns/lib/subscription.ts @@ -6,6 +6,7 @@ import { ITopic } from './topic-base'; import { PolicyStatement, ServicePrincipal } from '../../aws-iam'; import { IQueue } from '../../aws-sqs'; import { Resource } from '../../core'; +import { ValidationError } from '../../core/lib/errors'; /** * Options for creating a new subscription @@ -114,12 +115,12 @@ export class Subscription extends Resource { SubscriptionProtocol.FIREHOSE, ] .indexOf(props.protocol) < 0) { - throw new Error('Raw message delivery can only be enabled for HTTP, HTTPS, SQS, and Firehose subscriptions.'); + throw new ValidationError('Raw message delivery can only be enabled for HTTP, HTTPS, SQS, and Firehose subscriptions.', this); } if (props.filterPolicy) { if (Object.keys(props.filterPolicy).length > 5) { - throw new Error('A filter policy can have a maximum of 5 attribute names.'); + throw new ValidationError('A filter policy can have a maximum of 5 attribute names.', this); } this.filterPolicy = Object.entries(props.filterPolicy) @@ -131,22 +132,22 @@ export class Subscription extends Resource { let total = 1; Object.values(this.filterPolicy).forEach(filter => { total *= filter.length; }); if (total > 150) { - throw new Error(`The total combination of values (${total}) must not exceed 150.`); + throw new ValidationError(`The total combination of values (${total}) must not exceed 150.`, this); } } else if (props.filterPolicyWithMessageBody) { if (Object.keys(props.filterPolicyWithMessageBody).length > 5) { - throw new Error('A filter policy can have a maximum of 5 attribute names.'); + throw new ValidationError('A filter policy can have a maximum of 5 attribute names.', this); } this.filterPolicyWithMessageBody = props.filterPolicyWithMessageBody; } if (props.protocol === SubscriptionProtocol.FIREHOSE && !props.subscriptionRoleArn) { - throw new Error('Subscription role arn is required field for subscriptions with a firehose protocol.'); + throw new ValidationError('Subscription role arn is required field for subscriptions with a firehose protocol.', this); } // Format filter policy const filterPolicy = this.filterPolicyWithMessageBody - ? buildFilterPolicyWithMessageBody(this.filterPolicyWithMessageBody) + ? buildFilterPolicyWithMessageBody(this, this.filterPolicyWithMessageBody) : this.filterPolicy; this.deadLetterQueue = this.buildDeadLetterQueue(props); @@ -167,7 +168,7 @@ export class Subscription extends Resource { private renderDeliveryPolicy(deliveryPolicy: DeliveryPolicy, protocol: SubscriptionProtocol): any { if (![SubscriptionProtocol.HTTP, SubscriptionProtocol.HTTPS].includes(protocol)) { - throw new Error(`Delivery policy is only supported for HTTP and HTTPS subscriptions, got: ${protocol}`); + throw new ValidationError(`Delivery policy is only supported for HTTP and HTTPS subscriptions, got: ${protocol}`, this); } const { healthyRetryPolicy, throttlePolicy } = deliveryPolicy; if (healthyRetryPolicy) { @@ -176,45 +177,45 @@ export class Subscription extends Resource { const maxDelayTarget = healthyRetryPolicy.maxDelayTarget; if (minDelayTarget !== undefined) { if (minDelayTarget.toMilliseconds() % 1000 !== 0) { - throw new Error(`minDelayTarget must be a whole number of seconds, got: ${minDelayTarget}`); + throw new ValidationError(`minDelayTarget must be a whole number of seconds, got: ${minDelayTarget}`, this); } const minDelayTargetSecs = minDelayTarget.toSeconds(); if (minDelayTargetSecs < 1 || minDelayTargetSecs > delayTargetLimitSecs) { - throw new Error(`minDelayTarget must be between 1 and ${delayTargetLimitSecs} seconds inclusive, got: ${minDelayTargetSecs}s`); + throw new ValidationError(`minDelayTarget must be between 1 and ${delayTargetLimitSecs} seconds inclusive, got: ${minDelayTargetSecs}s`, this); } } if (maxDelayTarget !== undefined) { if (maxDelayTarget.toMilliseconds() % 1000 !== 0) { - throw new Error(`maxDelayTarget must be a whole number of seconds, got: ${maxDelayTarget}`); + throw new ValidationError(`maxDelayTarget must be a whole number of seconds, got: ${maxDelayTarget}`, this); } const maxDelayTargetSecs = maxDelayTarget.toSeconds(); if (maxDelayTargetSecs < 1 || maxDelayTargetSecs > delayTargetLimitSecs) { - throw new Error(`maxDelayTarget must be between 1 and ${delayTargetLimitSecs} seconds inclusive, got: ${maxDelayTargetSecs}s`); + throw new ValidationError(`maxDelayTarget must be between 1 and ${delayTargetLimitSecs} seconds inclusive, got: ${maxDelayTargetSecs}s`, this); } if ((minDelayTarget !== undefined) && minDelayTarget.toSeconds() > maxDelayTargetSecs) { - throw new Error('minDelayTarget must not exceed maxDelayTarget'); + throw new ValidationError('minDelayTarget must not exceed maxDelayTarget', this); } } const numRetriesLimit = 100; if (healthyRetryPolicy.numRetries && (healthyRetryPolicy.numRetries < 0 || healthyRetryPolicy.numRetries > numRetriesLimit)) { - throw new Error(`numRetries must be between 0 and ${numRetriesLimit} inclusive, got: ${healthyRetryPolicy.numRetries}`); + throw new ValidationError(`numRetries must be between 0 and ${numRetriesLimit} inclusive, got: ${healthyRetryPolicy.numRetries}`, this); } const { numNoDelayRetries, numMinDelayRetries, numMaxDelayRetries } = healthyRetryPolicy; if (numNoDelayRetries && (numNoDelayRetries < 0 || !Number.isInteger(numNoDelayRetries))) { - throw new Error(`numNoDelayRetries must be an integer zero or greater, got: ${numNoDelayRetries}`); + throw new ValidationError(`numNoDelayRetries must be an integer zero or greater, got: ${numNoDelayRetries}`, this); } if (numMinDelayRetries && (numMinDelayRetries < 0 || !Number.isInteger(numMinDelayRetries))) { - throw new Error(`numMinDelayRetries must be an integer zero or greater, got: ${numMinDelayRetries}`); + throw new ValidationError(`numMinDelayRetries must be an integer zero or greater, got: ${numMinDelayRetries}`, this); } if (numMaxDelayRetries && (numMaxDelayRetries < 0 || !Number.isInteger(numMaxDelayRetries))) { - throw new Error(`numMaxDelayRetries must be an integer zero or greater, got: ${numMaxDelayRetries}`); + throw new ValidationError(`numMaxDelayRetries must be an integer zero or greater, got: ${numMaxDelayRetries}`, this); } } if (throttlePolicy) { const maxReceivesPerSecond = throttlePolicy.maxReceivesPerSecond; if (maxReceivesPerSecond !== undefined && (maxReceivesPerSecond < 1 || !Number.isInteger(maxReceivesPerSecond))) { - throw new Error(`maxReceivesPerSecond must be an integer greater than zero, got: ${maxReceivesPerSecond}`); + throw new ValidationError(`maxReceivesPerSecond must be an integer greater than zero, got: ${maxReceivesPerSecond}`, this); } } return { @@ -320,6 +321,7 @@ export enum SubscriptionProtocol { } function buildFilterPolicyWithMessageBody( + scope: Construct, inputObject: { [key: string]: FilterOrPolicy }, depth = 1, totalCombinationValues = [1], @@ -328,7 +330,7 @@ function buildFilterPolicyWithMessageBody( for (const [key, filterOrPolicy] of Object.entries(inputObject)) { if (filterOrPolicy.isPolicy()) { - result[key] = buildFilterPolicyWithMessageBody(filterOrPolicy.policyDoc, depth + 1, totalCombinationValues); + result[key] = buildFilterPolicyWithMessageBody(scope, filterOrPolicy.policyDoc, depth + 1, totalCombinationValues); } else if (filterOrPolicy.isFilter()) { const filter = filterOrPolicy.filterDoc.conditions; result[key] = filter; @@ -338,7 +340,7 @@ function buildFilterPolicyWithMessageBody( // https://docs.aws.amazon.com/sns/latest/dg/subscription-filter-policy-constraints.html if (totalCombinationValues[0] > 150) { - throw new Error(`The total combination of values (${totalCombinationValues}) must not exceed 150.`); + throw new ValidationError(`The total combination of values (${totalCombinationValues}) must not exceed 150.`, scope); } return result; diff --git a/packages/aws-cdk-lib/aws-sns/lib/topic-base.ts b/packages/aws-cdk-lib/aws-sns/lib/topic-base.ts index 4a50c87c570bb..6abbc295bb86d 100644 --- a/packages/aws-cdk-lib/aws-sns/lib/topic-base.ts +++ b/packages/aws-cdk-lib/aws-sns/lib/topic-base.ts @@ -6,6 +6,7 @@ import { Subscription } from './subscription'; import * as notifications from '../../aws-codestarnotifications'; import * as iam from '../../aws-iam'; import { IResource, Resource, ResourceProps, Token } from '../../core'; +import { ValidationError } from '../../core/lib/errors'; /** * Represents an SNS topic @@ -111,7 +112,7 @@ export abstract class TopicBase extends Resource implements ITopic { // We use the subscriber's id as the construct id. There's no meaning // to subscribing the same subscriber twice on the same topic. if (scope.node.tryFindChild(id)) { - throw new Error(`A subscription with id "${id}" already exists under the scope ${scope.node.path}`); + throw new ValidationError(`A subscription with id "${id}" already exists under the scope ${scope.node.path}`, scope); } const subscription = new Subscription(scope, id, { diff --git a/packages/aws-cdk-lib/aws-sns/lib/topic.ts b/packages/aws-cdk-lib/aws-sns/lib/topic.ts index f78b32bf8cfe3..2df476a870b27 100644 --- a/packages/aws-cdk-lib/aws-sns/lib/topic.ts +++ b/packages/aws-cdk-lib/aws-sns/lib/topic.ts @@ -4,6 +4,7 @@ import { ITopic, TopicBase } from './topic-base'; import { IRole } from '../../aws-iam'; import { IKey } from '../../aws-kms'; import { ArnFormat, Lazy, Names, Stack, Token } from '../../core'; +import { ValidationError } from '../../core/lib/errors'; /** * Properties for a new SNS topic @@ -226,7 +227,7 @@ export class Topic extends TopicBase { const fifo = topicName.endsWith('.fifo'); if (attrs.contentBasedDeduplication && !fifo) { - throw new Error('Cannot import topic; contentBasedDeduplication is only available for FIFO SNS topics.'); + throw new ValidationError('Cannot import topic; contentBasedDeduplication is only available for FIFO SNS topics.', scope); } class Import extends TopicBase { @@ -259,17 +260,17 @@ export class Topic extends TopicBase { this.enforceSSL = props.enforceSSL; if (props.contentBasedDeduplication && !props.fifo) { - throw new Error('Content based deduplication can only be enabled for FIFO SNS topics.'); + throw new ValidationError('Content based deduplication can only be enabled for FIFO SNS topics.', this); } if (props.messageRetentionPeriodInDays && !props.fifo) { - throw new Error('`messageRetentionPeriodInDays` is only valid for FIFO SNS topics.'); + throw new ValidationError('`messageRetentionPeriodInDays` is only valid for FIFO SNS topics.', this); } if ( props.messageRetentionPeriodInDays !== undefined && !Token.isUnresolved(props.messageRetentionPeriodInDays) && (!Number.isInteger(props.messageRetentionPeriodInDays) || props.messageRetentionPeriodInDays > 365 || props.messageRetentionPeriodInDays < 1) ) { - throw new Error('`messageRetentionPeriodInDays` must be an integer between 1 and 365'); + throw new ValidationError('`messageRetentionPeriodInDays` must be an integer between 1 and 365', this); } if (props.loggingConfigs) { @@ -296,11 +297,11 @@ export class Topic extends TopicBase { props.signatureVersion !== '1' && props.signatureVersion !== '2' ) { - throw new Error(`signatureVersion must be "1" or "2", received: "${props.signatureVersion}"`); + throw new ValidationError(`signatureVersion must be "1" or "2", received: "${props.signatureVersion}"`, this); } if (props.displayName && !Token.isUnresolved(props.displayName) && props.displayName.length > 100) { - throw new Error(`displayName must be less than or equal to 100 characters, got ${props.displayName.length}`); + throw new ValidationError(`displayName must be less than or equal to 100 characters, got ${props.displayName.length}`, this); } const resource = new CfnTopic(this, 'Resource', { @@ -327,13 +328,11 @@ export class Topic extends TopicBase { } private renderLoggingConfigs(): CfnTopic.LoggingConfigProperty[] { - return this.loggingConfigs.map(renderLoggingConfig); - - function renderLoggingConfig(spec: LoggingConfig): CfnTopic.LoggingConfigProperty { + const renderLoggingConfig = (spec: LoggingConfig): CfnTopic.LoggingConfigProperty => { if (spec.successFeedbackSampleRate !== undefined) { const rate = spec.successFeedbackSampleRate; if (!Number.isInteger(rate) || rate < 0 || rate > 100) { - throw new Error('Success feedback sample rate must be an integer between 0 and 100'); + throw new ValidationError('Success feedback sample rate must be an integer between 0 and 100', this); } } return { @@ -342,7 +341,9 @@ export class Topic extends TopicBase { successFeedbackRoleArn: spec.successFeedbackRole?.roleArn, successFeedbackSampleRate: spec.successFeedbackSampleRate?.toString(), }; - } + }; + + return this.loggingConfigs.map(renderLoggingConfig); } /**