Skip to content

feat(docdb): throw ValidationErrors instead of untyped Errors #33870

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Mar 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/aws-cdk-lib/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ const enableNoThrowDefaultErrorIn = [
'aws-codepipeline',
'aws-codepipeline-actions',
'aws-cognito',
'aws-docdb',
'aws-ecr',
'aws-elasticloadbalancing',
'aws-elasticloadbalancingv2',
Expand Down
34 changes: 17 additions & 17 deletions packages/aws-cdk-lib/aws-docdb/lib/cluster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import * as kms from '../../aws-kms';
import * as logs from '../../aws-logs';
import { CaCertificate } from '../../aws-rds';
import * as secretsmanager from '../../aws-secretsmanager';
import { CfnResource, Duration, RemovalPolicy, Resource, Token } from '../../core';
import { CfnResource, Duration, RemovalPolicy, Resource, Token, UnscopedValidationError, ValidationError } from '../../core';
import { addConstructMetadata, MethodMetadata } from '../../core/lib/metadata-resource';

const MIN_ENGINE_VERSION_FOR_IO_OPTIMIZED_STORAGE = 5;
Expand Down Expand Up @@ -355,35 +355,35 @@ export class DatabaseCluster extends DatabaseClusterBase {

public get instanceIdentifiers(): string[] {
if (!this._instanceIdentifiers) {
throw new Error('Cannot access `instanceIdentifiers` of an imported cluster without provided instanceIdentifiers');
throw new UnscopedValidationError('Cannot access `instanceIdentifiers` of an imported cluster without provided instanceIdentifiers');
}
return this._instanceIdentifiers;
}

public get clusterEndpoint(): Endpoint {
if (!this._clusterEndpoint) {
throw new Error('Cannot access `clusterEndpoint` of an imported cluster without an endpoint address and port');
throw new UnscopedValidationError('Cannot access `clusterEndpoint` of an imported cluster without an endpoint address and port');
}
return this._clusterEndpoint;
}

public get clusterReadEndpoint(): Endpoint {
if (!this._clusterReadEndpoint) {
throw new Error('Cannot access `clusterReadEndpoint` of an imported cluster without a readerEndpointAddress and port');
throw new UnscopedValidationError('Cannot access `clusterReadEndpoint` of an imported cluster without a readerEndpointAddress and port');
}
return this._clusterReadEndpoint;
}

public get instanceEndpoints(): Endpoint[] {
if (!this._instanceEndpoints) {
throw new Error('Cannot access `instanceEndpoints` of an imported cluster without instanceEndpointAddresses and port');
throw new UnscopedValidationError('Cannot access `instanceEndpoints` of an imported cluster without instanceEndpointAddresses and port');
}
return this._instanceEndpoints;
}

public get securityGroupId(): string {
if (!this._securityGroupId) {
throw new Error('Cannot access `securityGroupId` of an imported cluster without securityGroupId');
throw new UnscopedValidationError('Cannot access `securityGroupId` of an imported cluster without securityGroupId');
}
return this._securityGroupId;
}
Expand Down Expand Up @@ -479,7 +479,7 @@ export class DatabaseCluster extends DatabaseClusterBase {
// We cannot test whether the subnets are in different AZs, but at least we can test the amount.
// See https://docs.aws.amazon.com/documentdb/latest/developerguide/replication.html#replication.high-availability
if (subnetIds.length < 2) {
throw new Error(`Cluster requires at least 2 subnets, got ${subnetIds.length}`);
throw new ValidationError(`Cluster requires at least 2 subnets, got ${subnetIds.length}`, this);
}

const subnetGroup = new CfnDBSubnetGroup(this, 'Subnets', {
Expand All @@ -497,7 +497,7 @@ export class DatabaseCluster extends DatabaseClusterBase {
vpc: this.vpc,
});
// HACK: Use an escape-hatch to apply a consistent removal policy to the
// security group so we don't get errors when trying to delete the stack.
// security group so we don't get ValidationErrors when trying to delete the stack.
const securityGroupRemovalPolicy = this.getSecurityGroupRemovalPolicy(props);
(securityGroup.node.defaultChild as CfnResource).applyRemovalPolicy(securityGroupRemovalPolicy, {
applyToUpdateReplacePolicy: true,
Expand Down Expand Up @@ -529,20 +529,20 @@ export class DatabaseCluster extends DatabaseClusterBase {
const storageEncrypted = props.storageEncrypted ?? true;

if (props.kmsKey && !storageEncrypted) {
throw new Error('KMS key supplied but storageEncrypted is false');
throw new ValidationError('KMS key supplied but storageEncrypted is false', this);
}

const validEngineVersionRegex = /^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$/;
if (props.engineVersion !== undefined && !validEngineVersionRegex.test(props.engineVersion)) {
throw new Error(`Invalid engine version: '${props.engineVersion}'. Engine version must be in the format x.y.z`);
throw new ValidationError(`Invalid engine version: '${props.engineVersion}'. Engine version must be in the format x.y.z`, this);
}

if (
props.storageType === StorageType.IOPT1
&& props.engineVersion !== undefined
&& Number(props.engineVersion.split('.')[0]) < MIN_ENGINE_VERSION_FOR_IO_OPTIMIZED_STORAGE
) {
throw new Error(`I/O-optimized storage is supported starting with engine version 5.0.0, got '${props.engineVersion}'`);
throw new ValidationError(`I/O-optimized storage is supported starting with engine version 5.0.0, got '${props.engineVersion}'`, this);
}

// Create the DocDB cluster
Expand Down Expand Up @@ -594,7 +594,7 @@ export class DatabaseCluster extends DatabaseClusterBase {
// Create the instances
const instanceCount = props.instances ?? DatabaseCluster.DEFAULT_NUM_INSTANCES;
if (instanceCount < 1) {
throw new Error('At least one instance is required');
throw new ValidationError('At least one instance is required', this);
}

const instanceRemovalPolicy = this.getInstanceRemovalPolicy(props);
Expand Down Expand Up @@ -651,7 +651,7 @@ export class DatabaseCluster extends DatabaseClusterBase {

private getInstanceRemovalPolicy(props: DatabaseClusterProps) {
if (props.instanceRemovalPolicy === RemovalPolicy.SNAPSHOT) {
throw new Error('AWS::DocDB::DBInstance does not support the SNAPSHOT removal policy');
throw new ValidationError('AWS::DocDB::DBInstance does not support the SNAPSHOT removal policy', this);
}
if (props.instanceRemovalPolicy) return props.instanceRemovalPolicy;
return !props.removalPolicy || props.removalPolicy !== RemovalPolicy.SNAPSHOT ?
Expand All @@ -660,7 +660,7 @@ export class DatabaseCluster extends DatabaseClusterBase {

private getSecurityGroupRemovalPolicy(props: DatabaseClusterProps) {
if (props.securityGroupRemovalPolicy === RemovalPolicy.SNAPSHOT) {
throw new Error('AWS::EC2::SecurityGroup does not support the SNAPSHOT removal policy');
throw new ValidationError('AWS::EC2::SecurityGroup does not support the SNAPSHOT removal policy', this);
}
if (props.securityGroupRemovalPolicy) return props.securityGroupRemovalPolicy;
return !props.removalPolicy || props.removalPolicy !== RemovalPolicy.SNAPSHOT ?
Expand All @@ -676,13 +676,13 @@ export class DatabaseCluster extends DatabaseClusterBase {
@MethodMetadata()
public addRotationSingleUser(automaticallyAfter?: Duration): 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);
}

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, {
Expand All @@ -702,7 +702,7 @@ export class DatabaseCluster extends DatabaseClusterBase {
@MethodMetadata()
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);
}
return new secretsmanager.SecretRotation(this, id, {
secret: options.secret,
Expand Down
4 changes: 2 additions & 2 deletions packages/aws-cdk-lib/aws-docdb/lib/endpoint.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Token } from '../../core';
import { Token, UnscopedValidationError } from '../../core';

/**
* Connection endpoint of a database cluster or instance
Expand Down Expand Up @@ -52,7 +52,7 @@ export class Endpoint {
*/
constructor(address: string, port: number) {
if (!Token.isUnresolved(port) && !Endpoint.isValidPort(port)) {
throw new Error(`Port must be an integer between [${Endpoint.MIN_PORT}, ${Endpoint.MAX_PORT}] but got: ${port}`);
throw new UnscopedValidationError(`Port must be an integer between [${Endpoint.MIN_PORT}, ${Endpoint.MAX_PORT}] but got: ${port}`);
}

this.hostname = address;
Expand Down