Skip to content

Commit

Permalink
feat(route53): throw ValidationError instead of untyped errors (#33110
Browse files Browse the repository at this point in the history
)

### Issue 

`aws-route53*` 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*
  • Loading branch information
kaizencc authored Jan 24, 2025
1 parent 34ae997 commit 5e0f16d
Show file tree
Hide file tree
Showing 10 changed files with 48 additions and 32 deletions.
7 changes: 7 additions & 0 deletions packages/aws-cdk-lib/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ const enableNoThrowDefaultErrorIn = [
'aws-ssmquicksetup',
'aws-apigatewayv2-authorizers',
'aws-synthetics',
'aws-route53',
'aws-route53-patterns',
'aws-route53-targets',
'aws-route53profiles',
'aws-route53recoverycontrol',
'aws-route53recoveryreadiness',
'aws-route53resolver',
'aws-s3-assets',
'aws-s3-deployment',
'aws-s3-notifications',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ARecord, AaaaRecord, IHostedZone, RecordTarget } from '../../aws-route5
import { CloudFrontTarget } from '../../aws-route53-targets';
import { BlockPublicAccess, Bucket, RedirectProtocol } from '../../aws-s3';
import { ArnFormat, RemovalPolicy, Stack, Token, FeatureFlags } from '../../core';
import { ValidationError } from '../../core/lib/errors';
import { md5hash } from '../../core/lib/helpers-internal';
import { ROUTE53_PATTERNS_USE_CERTIFICATE } from '../../cx-api';

Expand Down Expand Up @@ -61,7 +62,7 @@ export class HttpsRedirect extends Construct {
if (props.certificate) {
const certificateRegion = Stack.of(this).splitArn(props.certificate.certificateArn, ArnFormat.SLASH_RESOURCE_NAME).region;
if (!Token.isUnresolved(certificateRegion) && certificateRegion !== 'us-east-1') {
throw new Error(`The certificate must be in the us-east-1 region and the certificate you provided is in ${certificateRegion}.`);
throw new ValidationError(`The certificate must be in the us-east-1 region and the certificate you provided is in ${certificateRegion}.`, this);
}
}
const redirectCert = props.certificate ?? this.createCertificate(domainNames, props.zone);
Expand Down Expand Up @@ -123,10 +124,10 @@ export class HttpsRedirect extends Construct {
const stack = Stack.of(this);
const parent = stack.node.scope;
if (!parent) {
throw new Error(`Stack ${stack.stackId} must be created in the scope of an App or Stage`);
throw new ValidationError(`Stack ${stack.stackId} must be created in the scope of an App or Stage`, this);
}
if (Token.isUnresolved(stack.region)) {
throw new Error(`When ${ROUTE53_PATTERNS_USE_CERTIFICATE} is enabled, a region must be defined on the Stack`);
throw new ValidationError(`When ${ROUTE53_PATTERNS_USE_CERTIFICATE} is enabled, a region must be defined on the Stack`, this);
}
if (stack.region !== 'us-east-1') {
const stackId = `certificate-redirect-stack-${stack.node.addr}`;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as apig from '../../aws-apigateway';
import * as route53 from '../../aws-route53';
import { ValidationError } from '../../core/lib/errors';

/**
* Defines an API Gateway domain name as the alias target.
Expand Down Expand Up @@ -28,7 +29,7 @@ export class ApiGatewayDomain implements route53.IAliasRecordTarget {
export class ApiGateway extends ApiGatewayDomain {
constructor(api: apig.RestApiBase) {
if (!api.domainName) {
throw new Error('API does not define a default domain name');
throw new ValidationError('API does not define a default domain name', api);
}

super(api.domainName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { IAliasRecordTargetProps } from './shared';
import * as route53 from '../../aws-route53';
import * as s3 from '../../aws-s3';
import { Stack, Token } from '../../core';
import { ValidationError } from '../../core/lib/errors';
import { RegionInfo } from '../../region-info';

/**
Expand All @@ -10,21 +11,21 @@ import { RegionInfo } from '../../region-info';
export class BucketWebsiteTarget implements route53.IAliasRecordTarget {
constructor(private readonly bucket: s3.IBucket, private readonly props?: IAliasRecordTargetProps) {}

public bind(_record: route53.IRecordSet, _zone?: route53.IHostedZone): route53.AliasRecordTargetConfig {
public bind(record: route53.IRecordSet, _zone?: route53.IHostedZone): route53.AliasRecordTargetConfig {
const { region } = Stack.of(this.bucket.stack);

if (Token.isUnresolved(region)) {
throw new Error([
throw new ValidationError([
'Cannot use an S3 record alias in region-agnostic stacks.',
'You must specify a specific region when you define the stack',
'(see https://docs.aws.amazon.com/cdk/latest/guide/environments.html)',
].join(' '));
].join(' '), record);
}

const { s3StaticWebsiteHostedZoneId: hostedZoneId, s3StaticWebsiteEndpoint: dnsName } = RegionInfo.get(region);

if (!hostedZoneId || !dnsName) {
throw new Error(`Bucket website target is not supported for the "${region}" region`);
throw new ValidationError(`Bucket website target is not supported for the "${region}" region`, record);
}

return { hostedZoneId, dnsName, evaluateTargetHealth: this.props?.evaluateTargetHealth };
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { IAliasRecordTargetProps } from './shared';
import * as route53 from '../../aws-route53';
import * as cdk from '../../core';
import { ValidationError } from '../../core/lib/errors';
import { RegionInfo } from '../../region-info';

/**
Expand All @@ -13,9 +14,9 @@ import { RegionInfo } from '../../region-info';
export class ElasticBeanstalkEnvironmentEndpointTarget implements route53.IAliasRecordTarget {
constructor( private readonly environmentEndpoint: string, private readonly props?: IAliasRecordTargetProps) {}

public bind(_record: route53.IRecordSet, _zone?: route53.IHostedZone): route53.AliasRecordTargetConfig {
public bind(record: route53.IRecordSet, _zone?: route53.IHostedZone): route53.AliasRecordTargetConfig {
if (cdk.Token.isUnresolved(this.environmentEndpoint)) {
throw new Error('Cannot use an EBS alias as `environmentEndpoint`. You must find your EBS environment endpoint via the AWS console. See the Elastic Beanstalk developer guide: https://docs.aws.amazon.com/elasticbeanstalk/latest/dg/customdomains.html');
throw new ValidationError('Cannot use an EBS alias as `environmentEndpoint`. You must find your EBS environment endpoint via the AWS console. See the Elastic Beanstalk developer guide: https://docs.aws.amazon.com/elasticbeanstalk/latest/dg/customdomains.html', record);
}

const dnsName = this.environmentEndpoint;
Expand All @@ -25,7 +26,7 @@ export class ElasticBeanstalkEnvironmentEndpointTarget implements route53.IAlias
const { ebsEnvEndpointHostedZoneId: hostedZoneId } = RegionInfo.get(region);

if (!hostedZoneId || !dnsName) {
throw new Error(`Elastic Beanstalk environment target is not supported for the "${region}" region.`);
throw new ValidationError(`Elastic Beanstalk environment target is not supported for the "${region}" region.`, record);
}

return {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as route53 from '../../aws-route53';
import { ValidationError } from '../../core/lib/errors';

/**
* Use another Route 53 record as an alias record target
Expand All @@ -7,9 +8,9 @@ export class Route53RecordTarget implements route53.IAliasRecordTarget {
constructor(private readonly record: route53.IRecordSet) {
}

public bind(_record: route53.IRecordSet, zone?: route53.IHostedZone): route53.AliasRecordTargetConfig {
public bind(record: route53.IRecordSet, zone?: route53.IHostedZone): route53.AliasRecordTargetConfig {
if (!zone) { // zone introduced as optional to avoid a breaking change
throw new Error('Cannot bind to record without a zone');
throw new ValidationError('Cannot bind to record without a zone', record);
}
return {
dnsName: this.record.domainName,
Expand Down
8 changes: 5 additions & 3 deletions packages/aws-cdk-lib/aws-route53/lib/geo-location.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { UnscopedValidationError } from '../../core/lib/errors';

/**
* Routing based on geographical location.
*/
Expand Down Expand Up @@ -49,21 +51,21 @@ export class GeoLocation {
private static validateCountry(country: string) {
if (!GeoLocation.COUNTRY_REGEX.test(country)) {
// eslint-disable-next-line max-len
throw new Error(`Invalid country format for country: ${country}, country should be two-letter and uppercase country ISO 3166-1-alpha-2 code`);
throw new UnscopedValidationError(`Invalid country format for country: ${country}, country should be two-letter and uppercase country ISO 3166-1-alpha-2 code`);
}
}

private static validateCountryForSubdivision(country: string) {
if (!GeoLocation.COUNTRY_FOR_SUBDIVISION_REGEX.test(country)) {
// eslint-disable-next-line max-len
throw new Error(`Invalid country for subdivisions geolocation: ${country}, only UA (Ukraine) and US (United states) are supported`);
throw new UnscopedValidationError(`Invalid country for subdivisions geolocation: ${country}, only UA (Ukraine) and US (United states) are supported`);
}
}

private static validateSubDivision(subDivision: string) {
if (!GeoLocation.SUBDIVISION_REGEX.test(subDivision)) {
// eslint-disable-next-line max-len
throw new Error(`Invalid subdivision format for subdivision: ${subDivision}, subdivision should be alphanumeric and between 1 and 3 characters`);
throw new UnscopedValidationError(`Invalid subdivision format for subdivision: ${subDivision}, subdivision should be alphanumeric and between 1 and 3 characters`);
}
}

Expand Down
13 changes: 7 additions & 6 deletions packages/aws-cdk-lib/aws-route53/lib/hosted-zone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import * as iam from '../../aws-iam';
import * as kms from '../../aws-kms';
import * as cxschema from '../../cloud-assembly-schema';
import { ContextProvider, Duration, Lazy, Resource, Stack } from '../../core';
import { ValidationError } from '../../core/lib/errors';

/**
* Common properties to create a Route 53 hosted zone
Expand Down Expand Up @@ -105,7 +106,7 @@ export class HostedZone extends Resource implements IHostedZone {
class Import extends Resource implements IHostedZone {
public readonly hostedZoneId = hostedZoneId;
public get zoneName(): string {
throw new Error('Cannot reference `zoneName` when using `HostedZone.fromHostedZoneId()`. A construct consuming this hosted zone may be trying to reference its `zoneName`. If this is the case, use `fromHostedZoneAttributes()` or `fromLookup()` instead.');
throw new ValidationError('Cannot reference `zoneName` when using `HostedZone.fromHostedZoneId()`. A construct consuming this hosted zone may be trying to reference its `zoneName`. If this is the case, use `fromHostedZoneAttributes()` or `fromLookup()` instead.', this);
}
public get hostedZoneArn(): string {
return makeHostedZoneArn(this, this.hostedZoneId);
Expand Down Expand Up @@ -152,7 +153,7 @@ export class HostedZone extends Resource implements IHostedZone {
*/
public static fromLookup(scope: Construct, id: string, query: HostedZoneProviderProps): IHostedZone {
if (!query.domainName) {
throw new Error('Cannot use undefined value for attribute `domainName`');
throw new ValidationError('Cannot use undefined value for attribute `domainName`', scope);
}

const DEFAULT_HOSTED_ZONE: HostedZoneContextResponse = {
Expand Down Expand Up @@ -242,7 +243,7 @@ export class HostedZone extends Resource implements IHostedZone {
*/
public enableDnssec(options: ZoneSigningOptions): IKeySigningKey {
if (this.keySigningKey) {
throw new Error('DNSSEC is already enabled for this hosted zone');
throw new ValidationError('DNSSEC is already enabled for this hosted zone', this);
}
this.keySigningKey = new KeySigningKey(this, 'KeySigningKey', {
hostedZone: this,
Expand Down Expand Up @@ -323,7 +324,7 @@ export class PublicHostedZone extends HostedZone implements IPublicHostedZone {
public static fromPublicHostedZoneId(scope: Construct, id: string, publicHostedZoneId: string): IPublicHostedZone {
class Import extends Resource implements IPublicHostedZone {
public readonly hostedZoneId = publicHostedZoneId;
public get zoneName(): string { throw new Error('Cannot reference `zoneName` when using `PublicHostedZone.fromPublicHostedZoneId()`. A construct consuming this hosted zone may be trying to reference its `zoneName`. If this is the case, use `fromPublicHostedZoneAttributes()` instead'); }
public get zoneName(): string { throw new ValidationError('Cannot reference `zoneName` when using `PublicHostedZone.fromPublicHostedZoneId()`. A construct consuming this hosted zone may be trying to reference its `zoneName`. If this is the case, use `fromPublicHostedZoneAttributes()` instead', this); }
public get hostedZoneArn(): string {
return makeHostedZoneArn(this, this.hostedZoneId);
}
Expand Down Expand Up @@ -404,7 +405,7 @@ export class PublicHostedZone extends HostedZone implements IPublicHostedZone {
}

public addVpc(_vpc: ec2.IVpc) {
throw new Error('Cannot associate public hosted zones with a VPC');
throw new ValidationError('Cannot associate public hosted zones with a VPC', this);
}

/**
Expand Down Expand Up @@ -483,7 +484,7 @@ export class PrivateHostedZone extends HostedZone implements IPrivateHostedZone
public static fromPrivateHostedZoneId(scope: Construct, id: string, privateHostedZoneId: string): IPrivateHostedZone {
class Import extends Resource implements IPrivateHostedZone {
public readonly hostedZoneId = privateHostedZoneId;
public get zoneName(): string { throw new Error('Cannot reference `zoneName` when using `PrivateHostedZone.fromPrivateHostedZoneId()`. A construct consuming this hosted zone may be trying to reference its `zoneName`'); }
public get zoneName(): string { throw new ValidationError('Cannot reference `zoneName` when using `PrivateHostedZone.fromPrivateHostedZoneId()`. A construct consuming this hosted zone may be trying to reference its `zoneName`', this); }
public get hostedZoneArn(): string {
return makeHostedZoneArn(this, this.hostedZoneId);
}
Expand Down
17 changes: 9 additions & 8 deletions packages/aws-cdk-lib/aws-route53/lib/record-set.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { CfnRecordSet } from './route53.generated';
import { determineFullyQualifiedDomainName } from './util';
import * as iam from '../../aws-iam';
import { CustomResource, Duration, IResource, Names, RemovalPolicy, Resource, Token } from '../../core';
import { ValidationError } from '../../core/lib/errors';
import { CrossAccountZoneDelegationProvider } from '../../custom-resource-handlers/dist/aws-route53/cross-account-zone-delegation-provider.generated';
import { DeleteExistingRecordSetProvider } from '../../custom-resource-handlers/dist/aws-route53/delete-existing-record-set-provider.generated';

Expand Down Expand Up @@ -340,16 +341,16 @@ export class RecordSet extends Resource implements IRecordSet {
super(scope, id);

if (props.weight && !Token.isUnresolved(props.weight) && (props.weight < 0 || props.weight > 255)) {
throw new Error(`weight must be between 0 and 255 inclusive, got: ${props.weight}`);
throw new ValidationError(`weight must be between 0 and 255 inclusive, got: ${props.weight}`, this);
}
if (props.setIdentifier && (props.setIdentifier.length < 1 || props.setIdentifier.length > 128)) {
throw new Error(`setIdentifier must be between 1 and 128 characters long, got: ${props.setIdentifier.length}`);
throw new ValidationError(`setIdentifier must be between 1 and 128 characters long, got: ${props.setIdentifier.length}`, this);
}
if (props.setIdentifier && props.weight === undefined && !props.geoLocation && !props.region && !props.multiValueAnswer) {
throw new Error('setIdentifier can only be specified for non-simple routing policies');
throw new ValidationError('setIdentifier can only be specified for non-simple routing policies', this);
}
if (props.multiValueAnswer && props.target.aliasTarget) {
throw new Error('multiValueAnswer cannot be specified for alias record');
throw new ValidationError('multiValueAnswer cannot be specified for alias record', this);
}

const nonSimpleRoutingPolicies = [
Expand All @@ -359,7 +360,7 @@ export class RecordSet extends Resource implements IRecordSet {
props.multiValueAnswer,
].filter((variable) => variable !== undefined).length;
if (nonSimpleRoutingPolicies > 1) {
throw new Error('Only one of region, weight, multiValueAnswer or geoLocation can be defined');
throw new ValidationError('Only one of region, weight, multiValueAnswer or geoLocation can be defined', this);
}

this.geoLocation = props.geoLocation;
Expand Down Expand Up @@ -546,9 +547,9 @@ class ARecordAsAliasTarget implements IAliasRecordTarget {
constructor(private readonly aRrecordAttrs: ARecordAttrs) {
}

public bind(_record: IRecordSet, _zone?: IHostedZone | undefined): AliasRecordTargetConfig {
if (!_zone) {
throw new Error('Cannot bind to record without a zone');
public bind(record: IRecordSet, zone?: IHostedZone | undefined): AliasRecordTargetConfig {
if (!zone) {
throw new ValidationError('Cannot bind to record without a zone', record);
}
return {
dnsName: this.aRrecordAttrs.targetDNS,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Construct } from 'constructs';
import { IVpcEndpointService } from '../../aws-ec2';
import { Fn, Names, Stack } from '../../core';
import { ValidationError } from '../../core/lib/errors';
import { md5hash } from '../../core/lib/helpers-internal';
import { AwsCustomResource, AwsCustomResourcePolicy, PhysicalResourceId } from '../../custom-resources';
import { IPublicHostedZone, TxtRecord } from '../lib';
Expand Down Expand Up @@ -80,8 +81,7 @@ export class VpcEndpointServiceDomainName extends Construct {
const serviceUniqueId = Names.nodeUniqueId(props.endpointService.node);
if (serviceUniqueId in VpcEndpointServiceDomainName.endpointServicesMap) {
const endpoint = VpcEndpointServiceDomainName.endpointServicesMap[serviceUniqueId];
throw new Error(
`Cannot create a VpcEndpointServiceDomainName for service ${serviceUniqueId}, another VpcEndpointServiceDomainName (${endpoint}) is already associated with it`);
throw new ValidationError(`Cannot create a VpcEndpointServiceDomainName for service ${serviceUniqueId}, another VpcEndpointServiceDomainName (${endpoint}) is already associated with it`, this);
}
}

Expand Down

0 comments on commit 5e0f16d

Please sign in to comment.