Skip to content
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

feat(route53): throw ValidationError instead of untyped errors #33110

Merged
merged 2 commits into from
Jan 24, 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
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
Loading