From 696f53f8ee426a7a7958d0807c7ad89b72f587ae Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Fri, 7 Jun 2019 10:06:17 +0200 Subject: [PATCH] feat(route53): improve constructs for basic records (#2741) Constructs for records (CNAME, TXT, etc.) now extend the `RecordSet` construct and offer better typed properties interfaces. Add constructs for A, AAAA, CAA, MX and SRV records. Add support for multiple values in basic records. Make `recordName` optional with default to zone root. Add a "security" `CaaAmazonRecord` construct to easily restrict certificate authorities allowed to issue certificates for a domain to Amazon only. BREAKING CHANGE: `recordValue: string` prop in `route53.TxtRecord` changed to `values: string[]` * `recordValue` prop in `route53.CnameRecord` renamed to `domainName` * `route53.AliasRecord` has been removed, use `route53.ARecord` or `route53.AaaaRecord` with the `target` prop. --- .../fargate/load-balanced-fargate-service.ts | 6 +- .../lib/cloudfront-target.ts | 2 +- .../lib/load-balancer-target.ts | 2 +- .../test/cloudfront-target.test.ts | 6 +- .../test/integ.alb-alias-target.ts | 4 +- .../test/integ.cloudfront-alias-target.ts | 4 +- .../test/load-balancer-target.test.ts | 4 +- packages/@aws-cdk/aws-route53/README.md | 54 ++- .../aws-route53/lib/alias-record-target.ts | 4 +- .../@aws-cdk/aws-route53/lib/hosted-zone.ts | 24 +- packages/@aws-cdk/aws-route53/lib/index.ts | 2 +- .../@aws-cdk/aws-route53/lib/record-set.ts | 447 +++++++++++++++++ .../@aws-cdk/aws-route53/lib/records/_util.ts | 29 -- .../@aws-cdk/aws-route53/lib/records/alias.ts | 51 -- .../@aws-cdk/aws-route53/lib/records/cname.ts | 47 -- .../@aws-cdk/aws-route53/lib/records/index.ts | 4 - .../@aws-cdk/aws-route53/lib/records/txt.ts | 49 -- .../lib/records/zone-delegation.ts | 44 -- packages/@aws-cdk/aws-route53/lib/util.ts | 30 ++ .../test/integ.route53.expected.json | 342 ++----------- .../aws-route53/test/integ.route53.ts | 21 +- .../aws-route53/test/test.alias-record.ts | 79 --- .../aws-route53/test/test.cname-record.ts | 67 --- .../aws-route53/test/test.record-set.ts | 458 ++++++++++++++++++ .../@aws-cdk/aws-route53/test/test.route53.ts | 23 +- .../aws-route53/test/test.txt-record.ts | 50 -- .../test/test.zone-delegation-record.ts | 54 --- 27 files changed, 1087 insertions(+), 820 deletions(-) create mode 100644 packages/@aws-cdk/aws-route53/lib/record-set.ts delete mode 100644 packages/@aws-cdk/aws-route53/lib/records/_util.ts delete mode 100644 packages/@aws-cdk/aws-route53/lib/records/alias.ts delete mode 100644 packages/@aws-cdk/aws-route53/lib/records/cname.ts delete mode 100644 packages/@aws-cdk/aws-route53/lib/records/index.ts delete mode 100644 packages/@aws-cdk/aws-route53/lib/records/txt.ts delete mode 100644 packages/@aws-cdk/aws-route53/lib/records/zone-delegation.ts delete mode 100644 packages/@aws-cdk/aws-route53/test/test.alias-record.ts delete mode 100644 packages/@aws-cdk/aws-route53/test/test.cname-record.ts create mode 100644 packages/@aws-cdk/aws-route53/test/test.record-set.ts delete mode 100644 packages/@aws-cdk/aws-route53/test/test.txt-record.ts delete mode 100644 packages/@aws-cdk/aws-route53/test/test.zone-delegation-record.ts diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/load-balanced-fargate-service.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/load-balanced-fargate-service.ts index 0b2ddba2634f2..6129c57a2f7b9 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/load-balanced-fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/load-balanced-fargate-service.ts @@ -1,5 +1,5 @@ import ecs = require('@aws-cdk/aws-ecs'); -import { AliasRecord, IHostedZone } from '@aws-cdk/aws-route53'; +import { AddressRecordTarget, ARecord, IHostedZone } from '@aws-cdk/aws-route53'; import targets = require('@aws-cdk/aws-route53-targets'); import cdk = require('@aws-cdk/cdk'); import { LoadBalancedServiceBase, LoadBalancedServiceBaseProps } from '../base/load-balanced-service-base'; @@ -120,10 +120,10 @@ export class LoadBalancedFargateService extends LoadBalancedServiceBase { throw new Error('A Route53 hosted domain zone name is required to configure the specified domain name'); } - new AliasRecord(this, "DNS", { + new ARecord(this, "DNS", { zone: props.domainZone, recordName: props.domainName, - target: new targets.LoadBalancerTarget(this.loadBalancer), + target: AddressRecordTarget.fromAlias(new targets.LoadBalancerTarget(this.loadBalancer)), }); } } diff --git a/packages/@aws-cdk/aws-route53-targets/lib/cloudfront-target.ts b/packages/@aws-cdk/aws-route53-targets/lib/cloudfront-target.ts index ceda2b8c7920e..f81b0db8cb665 100644 --- a/packages/@aws-cdk/aws-route53-targets/lib/cloudfront-target.ts +++ b/packages/@aws-cdk/aws-route53-targets/lib/cloudfront-target.ts @@ -14,7 +14,7 @@ export class CloudFrontTarget implements route53.IAliasRecordTarget { constructor(private readonly distribution: cloudfront.CloudFrontWebDistribution) { } - public bind(_record: route53.IAliasRecord): route53.AliasRecordTargetConfig { + public bind(_record: route53.IRecordSet): route53.AliasRecordTargetConfig { return { hostedZoneId: CLOUDFRONT_ZONE_ID, dnsName: this.distribution.domainName diff --git a/packages/@aws-cdk/aws-route53-targets/lib/load-balancer-target.ts b/packages/@aws-cdk/aws-route53-targets/lib/load-balancer-target.ts index 77b2d09817dc9..a621115cb5ab4 100644 --- a/packages/@aws-cdk/aws-route53-targets/lib/load-balancer-target.ts +++ b/packages/@aws-cdk/aws-route53-targets/lib/load-balancer-target.ts @@ -8,7 +8,7 @@ export class LoadBalancerTarget implements route53.IAliasRecordTarget { constructor(private readonly loadBalancer: elbv2.ILoadBalancerV2) { } - public bind(_record: route53.IAliasRecord): route53.AliasRecordTargetConfig { + public bind(_record: route53.IRecordSet): route53.AliasRecordTargetConfig { return { hostedZoneId: this.loadBalancer.loadBalancerCanonicalHostedZoneId, dnsName: this.loadBalancer.loadBalancerDnsName diff --git a/packages/@aws-cdk/aws-route53-targets/test/cloudfront-target.test.ts b/packages/@aws-cdk/aws-route53-targets/test/cloudfront-target.test.ts index d62ed2a4e1eb5..cb7b33b1d4ced 100644 --- a/packages/@aws-cdk/aws-route53-targets/test/cloudfront-target.test.ts +++ b/packages/@aws-cdk/aws-route53-targets/test/cloudfront-target.test.ts @@ -24,10 +24,10 @@ test('use CloudFront as record target', () => { // WHEN const zone = new route53.PublicHostedZone(stack, 'HostedZone', { zoneName: 'test.public' }); - new route53.AliasRecord(zone, 'Alias', { + new route53.ARecord(zone, 'Alias', { zone, recordName: '_foo', - target: new targets.CloudFrontTarget(distribution) + target: route53.AddressRecordTarget.fromAlias(new targets.CloudFrontTarget(distribution)) }); // THEN @@ -37,4 +37,4 @@ test('use CloudFront as record target', () => { HostedZoneId: "Z2FDTNDATAQYW2" }, }); -}); \ No newline at end of file +}); diff --git a/packages/@aws-cdk/aws-route53-targets/test/integ.alb-alias-target.ts b/packages/@aws-cdk/aws-route53-targets/test/integ.alb-alias-target.ts index b909c7f6c99fe..a9e17e7b44353 100644 --- a/packages/@aws-cdk/aws-route53-targets/test/integ.alb-alias-target.ts +++ b/packages/@aws-cdk/aws-route53-targets/test/integ.alb-alias-target.ts @@ -19,10 +19,10 @@ const lb = new elbv2.ApplicationLoadBalancer(stack, 'LB', { const zone = new route53.PublicHostedZone(stack, 'HostedZone', { zoneName: 'test.public' }); -new route53.AliasRecord(zone, 'Alias', { +new route53.ARecord(zone, 'Alias', { zone, recordName: '_foo', - target: new targets.LoadBalancerTarget(lb) + target: route53.AddressRecordTarget.fromAlias(new targets.LoadBalancerTarget(lb)) }); app.synth(); diff --git a/packages/@aws-cdk/aws-route53-targets/test/integ.cloudfront-alias-target.ts b/packages/@aws-cdk/aws-route53-targets/test/integ.cloudfront-alias-target.ts index 2d659f62aa9f0..dcbe7c404b394 100644 --- a/packages/@aws-cdk/aws-route53-targets/test/integ.cloudfront-alias-target.ts +++ b/packages/@aws-cdk/aws-route53-targets/test/integ.cloudfront-alias-target.ts @@ -25,10 +25,10 @@ const distribution = new cloudfront.CloudFrontWebDistribution(stack, 'MyDistribu ] }); -new route53.AliasRecord(zone, 'Alias', { +new route53.ARecord(zone, 'Alias', { zone, recordName: '_foo', - target: new targets.CloudFrontTarget(distribution) + target: route53.AddressRecordTarget.fromAlias(new targets.CloudFrontTarget(distribution)) }); app.synth(); diff --git a/packages/@aws-cdk/aws-route53-targets/test/load-balancer-target.test.ts b/packages/@aws-cdk/aws-route53-targets/test/load-balancer-target.test.ts index 623cb7bbe5023..c78062120398d 100644 --- a/packages/@aws-cdk/aws-route53-targets/test/load-balancer-target.test.ts +++ b/packages/@aws-cdk/aws-route53-targets/test/load-balancer-target.test.ts @@ -19,10 +19,10 @@ test('use ALB as record target', () => { const zone = new route53.PublicHostedZone(stack, 'HostedZone', { zoneName: 'test.public' }); // WHEN - new route53.AliasRecord(zone, 'Alias', { + new route53.ARecord(zone, 'Alias', { zone, recordName: '_foo', - target: new targets.LoadBalancerTarget(lb) + target: route53.AddressRecordTarget.fromAlias(new targets.LoadBalancerTarget(lb)) }); // THEN diff --git a/packages/@aws-cdk/aws-route53/README.md b/packages/@aws-cdk/aws-route53/README.md index 6386a11f14be6..a455c562012ed 100644 --- a/packages/@aws-cdk/aws-route53/README.md +++ b/packages/@aws-cdk/aws-route53/README.md @@ -18,7 +18,7 @@ To add a public hosted zone: import route53 = require('@aws-cdk/aws-route53'); new route53.PublicHostedZone(this, 'HostedZone', { - zoneName: 'fully.qualified.domain.com' + zoneName: 'fully.qualified.domain.com' }); ``` @@ -33,8 +33,8 @@ import route53 = require('@aws-cdk/aws-route53'); const vpc = new ec2.VpcNetwork(this, 'VPC'); const zone = new route53.PrivateHostedZone(this, 'HostedZone', { - zoneName: 'fully.qualified.domain.com', - vpc // At least one VPC has to be added to a Private Hosted Zone. + zoneName: 'fully.qualified.domain.com', + vpc // At least one VPC has to be added to a Private Hosted Zone. }); ``` @@ -46,15 +46,45 @@ To add a TXT record to your zone: ```ts import route53 = require('@aws-cdk/aws-route53'); -new route53.TxtRecord(zone, 'TXTRecord', { - recordName: '_foo', // If the name ends with a ".", it will be used as-is; - // if it ends with a "." followed by the zone name, a trailing "." will be added automatically; - // otherwise, a ".", the zone name, and a trailing "." will be added automatically. - recordValue: 'Bar!', // Will be quoted for you, and " will be escaped automatically. - ttl: 90, // Optional - default is 1800 +new route53.TxtRecord(this, 'TXTRecord', { + zone: myZone, + recordName: '_foo', // If the name ends with a ".", it will be used as-is; + // if it ends with a "." followed by the zone name, a trailing "." will be added automatically; + // otherwise, a ".", the zone name, and a trailing "." will be added automatically. + // Defaults to zone root if not specified. + values: [ // Will be quoted for you, and " will be escaped automatically. + 'Bar!', + 'Baz?' + ], + ttl: 90, // Optional - default is 1800 }); ``` +To add a A record to your zone: +```ts +import route53 = require('@aws-cdk/aws-route53'); + +new route53.ARecord(this, 'ARecord', { + zone: myZone, + target: route53.AddressRecordTarget.fromIpAddresses('1.2.3.4', '5.6.7.8') +}) +``` + +To add a AAAA record pointing to a CloudFront distribution: +```ts +import route53 = require('@aws-cdk/aws-route53'); +import targets = require('@aws-cdk/aws-route53-targets'); + +new route53.AaaaRecord(this, 'Alias', { + zone: myZone, + target: route53.AddressRecordTarget.fromAlias(new targets.CloudFrontTarget(distribution)) +}) +``` + +Constructs are available for A, AAAA, CAA, CNAME, MX, NS, SRV and TXT records. + +Use the `CaaAmazonRecord` construct to easily restrict certificate authorities +allowed to issue certificates for a domain to Amazon only. ### Adding records to existing hosted zones @@ -62,8 +92,8 @@ If you know the ID and Name of a Hosted Zone, you can import it directly: ```ts const zone = HostedZone.import(this, 'MyZone', { - zoneName: 'example.com', - hostedZoneId: 'ZOJJZC49E0EPZ', + zoneName: 'example.com', + hostedZoneId: 'ZOJJZC49E0EPZ', }); ``` @@ -72,6 +102,6 @@ to discover and import it: ```ts const zone = new HostedZoneProvider(this, { - domainName: 'example.com' + domainName: 'example.com' }).findAndImport(this, 'MyZone'); ``` diff --git a/packages/@aws-cdk/aws-route53/lib/alias-record-target.ts b/packages/@aws-cdk/aws-route53/lib/alias-record-target.ts index a0023eb038c98..e86be1a07a3c8 100644 --- a/packages/@aws-cdk/aws-route53/lib/alias-record-target.ts +++ b/packages/@aws-cdk/aws-route53/lib/alias-record-target.ts @@ -1,4 +1,4 @@ -import { IAliasRecord } from "./records/alias"; +import { IRecordSet } from "./record-set"; /** * Classes that are valid alias record targets, like CloudFront distributions and load @@ -8,7 +8,7 @@ export interface IAliasRecordTarget { /** * Return hosted zone ID and DNS name, usable for Route53 alias targets */ - bind(record: IAliasRecord): AliasRecordTargetConfig; + bind(record: IRecordSet): AliasRecordTargetConfig; } /** diff --git a/packages/@aws-cdk/aws-route53/lib/hosted-zone.ts b/packages/@aws-cdk/aws-route53/lib/hosted-zone.ts index af99ddb1efbb0..e89494dc02e70 100644 --- a/packages/@aws-cdk/aws-route53/lib/hosted-zone.ts +++ b/packages/@aws-cdk/aws-route53/lib/hosted-zone.ts @@ -1,7 +1,7 @@ import ec2 = require('@aws-cdk/aws-ec2'); import { Construct, Resource, Token } from '@aws-cdk/cdk'; import { HostedZoneAttributes, IHostedZone } from './hosted-zone-ref'; -import { ZoneDelegationRecord } from './records'; +import { CaaAmazonRecord, ZoneDelegationRecord } from './record-set'; import { CfnHostedZone } from './route53.generated'; import { validateZoneName } from './util'; @@ -107,7 +107,19 @@ export class HostedZone extends Resource implements IHostedZone { } } -export interface PublicHostedZoneProps extends CommonHostedZoneProps { } +/** + * Construction properties for a PublicHostedZone. + */ +export interface PublicHostedZoneProps extends CommonHostedZoneProps { + /** + * Whether to create a CAA record to restrict certificate authorities allowed + * to issue certificates for this domain to Amazon only. + * + * @default false + */ + readonly caaAmazon?: boolean; +} + export interface IPublicHostedZone extends IHostedZone { } /** @@ -127,6 +139,12 @@ export class PublicHostedZone extends HostedZone implements IPublicHostedZone { constructor(scope: Construct, id: string, props: PublicHostedZoneProps) { super(scope, id, props); + + if (props.caaAmazon) { + new CaaAmazonRecord(this, 'CaaAmazon', { + zone: this + }); + } } public addVpc(_vpc: ec2.IVpc) { @@ -142,7 +160,7 @@ export class PublicHostedZone extends HostedZone implements IPublicHostedZone { public addDelegation(delegate: IPublicHostedZone, opts: ZoneDelegationOptions = {}): void { new ZoneDelegationRecord(this, `${this.zoneName} -> ${delegate.zoneName}`, { zone: this, - delegatedZoneName: delegate.zoneName, + recordName: delegate.zoneName, nameServers: delegate.hostedZoneNameServers!, // PublicHostedZones always have name servers! comment: opts.comment, ttl: opts.ttl, diff --git a/packages/@aws-cdk/aws-route53/lib/index.ts b/packages/@aws-cdk/aws-route53/lib/index.ts index 1abc508d84901..9a6ed0a853679 100644 --- a/packages/@aws-cdk/aws-route53/lib/index.ts +++ b/packages/@aws-cdk/aws-route53/lib/index.ts @@ -1,7 +1,7 @@ export * from './hosted-zone'; export * from './hosted-zone-provider'; export * from './hosted-zone-ref'; -export * from './records'; +export * from './record-set'; export * from './alias-record-target'; // AWS::Route53 CloudFormation Resources: diff --git a/packages/@aws-cdk/aws-route53/lib/record-set.ts b/packages/@aws-cdk/aws-route53/lib/record-set.ts new file mode 100644 index 0000000000000..929ef4ec28192 --- /dev/null +++ b/packages/@aws-cdk/aws-route53/lib/record-set.ts @@ -0,0 +1,447 @@ +import { Construct, IResource, Resource, Token } from '@aws-cdk/cdk'; +import { IAliasRecordTarget } from './alias-record-target'; +import { IHostedZone } from './hosted-zone-ref'; +import { CfnRecordSet } from './route53.generated'; +import { determineFullyQualifiedDomainName } from './util'; + +/** + * A record set + */ +export interface IRecordSet extends IResource { + /** + * The domain name of the record + */ + readonly domainName: string; +} + +/** + * The record type. + */ +export enum RecordType { + A = 'A', + AAAA = 'AAAA', + CAA = 'CAA', + CNAME = 'CNAME', + MX = 'MX', + NAPTR = 'NAPTR', + NS = 'NS', + PTR = 'PTR', + SOA = 'SOA', + SPF = 'SPF', + SRV = 'SRV', + TXT = 'TXT' +} + +/** + * Options for a RecordSet. + */ +export interface RecordSetOptions { + /** + * The hosted zone in which to define the new record. + */ + readonly zone: IHostedZone; + + /** + * The domain name for this record. + * + * @default zone root + */ + readonly recordName?: string; + + /** + * The resource record cache time to live (TTL) in seconds. + * + * @default 1800 seconds + */ + readonly ttl?: number; + + /** + * A comment to add on the record. + * + * @default no comment + */ + readonly comment?: string; +} + +/** + * Type union for a record that accepts multiple types of target. + */ +export class RecordTarget { + /** + * Use string values as target. + */ + public static fromValues(...values: string[]) { + return new RecordTarget(values); + } + + /** + * Use an alias as target. + */ + public static fromAlias(aliasTarget: IAliasRecordTarget) { + return new RecordTarget(undefined, aliasTarget); + } + + protected constructor(public readonly values?: string[], public readonly aliasTarget?: IAliasRecordTarget) { + } +} + +/** + * Construction properties for a RecordSet. + */ +export interface RecordSetProps extends RecordSetOptions { + /** + * The record type. + */ + readonly recordType: RecordType; + + /** + * The target for this record, either `RecordTarget.fromValues()` or + * `RecordTarget.fromAlias()`. + */ + readonly target: RecordTarget; +} + +/** + * A record set. + */ +export class RecordSet extends Resource implements IRecordSet { + public readonly domainName: string; + + constructor(scope: Construct, id: string, props: RecordSetProps) { + super(scope, id); + + const ttl = props.target.aliasTarget ? undefined : (props.ttl || 1800).toString(); + + const recordSet = new CfnRecordSet(this, 'Resource', { + hostedZoneId: props.zone.hostedZoneId, + name: determineFullyQualifiedDomainName(props.recordName || props.zone.zoneName, props.zone), + type: props.recordType, + resourceRecords: props.target.values, + aliasTarget: props.target.aliasTarget && props.target.aliasTarget.bind(this), + ttl, + comment: props.comment + }); + + this.domainName = recordSet.ref; + } +} + +/** + * + */ +export class AddressRecordTarget extends RecordTarget { + /** + * Use ip adresses as target. + */ + public static fromIpAddresses(...ipAddresses: string[]) { + return RecordTarget.fromValues(...ipAddresses); + } +} + +/** + * Construction properties for a ARecord. + */ +export interface ARecordProps extends RecordSetOptions { + /** + * The target. + */ + readonly target: AddressRecordTarget; +} + +/** + * A DNS A record + * + * @resource AWS::Route53::RecordSet + */ +export class ARecord extends RecordSet { + constructor(scope: Construct, id: string, props: ARecordProps) { + super(scope, id, { + ...props, + recordType: RecordType.A, + target: props.target, + }); + } +} + +/** + * Construction properties for a AaaaRecord. + */ +export interface AaaaRecordProps extends RecordSetOptions, ARecordProps {} + +/** + * A DNS AAAA record + * + * @resource AWS::Route53::RecordSet + */ +export class AaaaRecord extends RecordSet { + constructor(scope: Construct, id: string, props: AaaaRecordProps) { + super(scope, id, { + ...props, + recordType: RecordType.AAAA, + target: props.target, + }); + } +} + +/** + * Construction properties for a CnameRecord. + */ +export interface CnameRecordProps extends RecordSetOptions { + /** + * The domain name. + */ + readonly domainName: string; +} + +/** + * A DNS CNAME record + * + * @resource AWS::Route53::RecordSet + */ +export class CnameRecord extends RecordSet { + constructor(scope: Construct, id: string, props: CnameRecordProps) { + super(scope, id, { + ...props, + recordType: RecordType.CNAME, + target: RecordTarget.fromValues(props.domainName) + }); + } +} + +/** + * Construction properties for a TxtRecord. + */ +export interface TxtRecordProps extends RecordSetOptions { + /** + * The text values. + */ + readonly values: string[]; +} + +/** + * A DNS TXT record + * + * @resource AWS::Route53::RecordSet + */ +export class TxtRecord extends RecordSet { + constructor(scope: Construct, id: string, props: TxtRecordProps) { + super(scope, id, { + ...props, + recordType: RecordType.TXT, + target: RecordTarget.fromValues(...props.values.map(v => JSON.stringify(v))), + }); + } +} + +/** + * Properties for a SRV record value. + */ +export interface SrvRecordValue { + /** + * The priority. + */ + readonly priority: number; + + /** + * The weight. + */ + readonly weight: number; + + /** + * The port. + */ + readonly port: number; + + /** + * The server host name. + */ + readonly hostName: string; +} +/** + * Construction properties for a SrvRecord. + */ +export interface SrvRecordProps extends RecordSetOptions { + /** + * The values. + */ + readonly values: SrvRecordValue[]; +} + +/** + * A DNS SRV record + * + * @resource AWS::Route53::RecordSet + */ +export class SrvRecord extends RecordSet { + constructor(scope: Construct, id: string, props: SrvRecordProps) { + super(scope, id, { + ...props, + recordType: RecordType.SRV, + target: RecordTarget.fromValues(...props.values.map(v => `${v.priority} ${v.weight} ${v.port} ${v.hostName}`)), + }); + } +} + +/** + * The CAA tag. + */ +export enum CaaTag { + /** + * Explicity authorizes a single certificate authority to issue a + * certificate (any type) for the hostname. + */ + ISSUE = 'issue', + + /** + * Explicity authorizes a single certificate authority to issue a + * wildcard certificate (and only wildcard) for the hostname. + */ + ISSUEWILD = 'issuewild', + + /** + * Specifies a URL to which a certificate authority may report policy + * violations. + */ + IODEF = 'iodef', +} + +/** + * Properties for a CAA record value. + */ +export interface CaaRecordValue { + /** + * The flag. + */ + readonly flag: number; + + /** + * The tag. + */ + readonly tag: CaaTag; + + /** + * The value associated with the tag. + */ + readonly value: string; +} + +/** + * Construction properties for a CaaRecord. + */ +export interface CaaRecordProps extends RecordSetOptions { + /** + * The values. + */ + readonly values: CaaRecordValue[]; +} + +/** + * A DNS CAA record + * + * @resource AWS::Route53::RecordSet + */ +export class CaaRecord extends RecordSet { + constructor(scope: Construct, id: string, props: CaaRecordProps) { + super(scope, id, { + ...props, + recordType: RecordType.CAA, + target: RecordTarget.fromValues(...props.values.map(v => `${v.flag} ${v.tag} "${v.value}"`)), + }); + } +} + +/** + * Construction properties for a CaaAmazonRecord. + */ +export interface CaaAmazonRecordProps extends RecordSetOptions {} + +/** + * A DNS Amazon CAA record. + * + * A CAA record to restrict certificate authorities allowed + * to issue certificates for a domain to Amazon only. + * + * @resource AWS::Route53::RecordSet + */ +export class CaaAmazonRecord extends CaaRecord { + constructor(scope: Construct, id: string, props: CaaAmazonRecordProps) { + super(scope, id, { + ...props, + values: [ + { + flag: 0, + tag: CaaTag.ISSUE, + value: 'amazon.com' + } + ], + recordName: props.zone.zoneName + }); + } +} + +/** + * Properties for a MX record value. + */ +export interface MxRecordValue { + /** + * The priority. + */ + readonly priority: number; + + /** + * The mail server host name. + */ + readonly hostName: string; +} + +/** + * Construction properties for a MxRecord. + */ +export interface MxRecordProps extends RecordSetOptions { + /** + * The values. + */ + readonly values: MxRecordValue[]; +} + +/** + * A DNS MX record + * + * @resource AWS::Route53::RecordSet + */ +export class MxRecord extends RecordSet { + constructor(scope: Construct, id: string, props: MxRecordProps) { + super(scope, id, { + ...props, + recordType: RecordType.MX, + target: RecordTarget.fromValues(...props.values.map(v => `${v.priority} ${v.hostName}`)) + }); + } +} + +/** + * Construction properties for a ZoneDelegationRecord + */ +export interface ZoneDelegationRecordProps extends RecordSetOptions { + /** + * The name servers to report in the delegation records. + */ + readonly nameServers: string[]; +} + +/** + * A record to delegate further lookups to a different set of name servers. + */ +export class ZoneDelegationRecord extends RecordSet { + constructor(scope: Construct, id: string, props: ZoneDelegationRecordProps) { + super(scope, id, { + ...props, + recordType: RecordType.NS, + target: RecordTarget.fromValues(...Token.isToken(props.nameServers) + ? props.nameServers // Can't map a string-array token! + : props.nameServers.map(ns => (Token.isToken(ns) || ns.endsWith('.')) ? ns : `${ns}.`) + ), + ttl: props.ttl || 172_800 + }); + } +} diff --git a/packages/@aws-cdk/aws-route53/lib/records/_util.ts b/packages/@aws-cdk/aws-route53/lib/records/_util.ts deleted file mode 100644 index ba503721f258c..0000000000000 --- a/packages/@aws-cdk/aws-route53/lib/records/_util.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { IHostedZone } from '../hosted-zone-ref'; - -/** - * Route53 requires the record names are specified as fully qualified names, but this - * forces lots of redundant work on the user (repeating the zone name over and over). - * This function allows the user to be lazier and offers a nicer experience, by - * qualifying relative names appropriately: - * - * @param providedName the user-specified name of the record. - * @param zoneName the fully-qualified name of the zone the record will be created in. - * - * @returns - */ -export function determineFullyQualifiedDomainName(providedName: string, hostedZone: IHostedZone): string { - if (providedName.endsWith('.')) { - return providedName; - } - - const suffix = `.${hostedZone.zoneName}`; - if (providedName.endsWith(suffix) || providedName === hostedZone.zoneName) { - return `${providedName}.`; - } - - return `${providedName}${suffix}.`; -} diff --git a/packages/@aws-cdk/aws-route53/lib/records/alias.ts b/packages/@aws-cdk/aws-route53/lib/records/alias.ts deleted file mode 100644 index a933116e1fb14..0000000000000 --- a/packages/@aws-cdk/aws-route53/lib/records/alias.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { Construct, IConstruct } from '@aws-cdk/cdk'; -import { IAliasRecordTarget } from '../alias-record-target'; -import { IHostedZone } from '../hosted-zone-ref'; -import { CfnRecordSet } from '../route53.generated'; -import { determineFullyQualifiedDomainName } from './_util'; - -export interface AliasRecordProps { - /** - * The zone in which this alias should be defined. - */ - readonly zone: IHostedZone; - /** - * Name for the record. This can be the FQDN for the record (foo.example.com) or - * a subdomain of the parent hosted zone (foo, with example.com as the hosted zone). - */ - readonly recordName: string; - /** - * Target for the alias record - */ - readonly target: IAliasRecordTarget; -} - -/** - * An alias record - */ -export interface IAliasRecord extends IConstruct { - /** - * The domain name of the record - */ - readonly domainName: string; -} - -/** - * Define a new Route53 alias record - */ -export class AliasRecord extends Construct implements IAliasRecord { - public readonly domainName: string; - - constructor(scope: Construct, id: string, props: AliasRecordProps) { - super(scope, id); - - const record = new CfnRecordSet(this, 'Resource', { - hostedZoneId: props.zone.hostedZoneId, - name: determineFullyQualifiedDomainName(props.recordName, props.zone), - type: 'A', // ipv4 - aliasTarget: props.target.bind(this) - }); - - this.domainName = record.ref; - } -} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-route53/lib/records/cname.ts b/packages/@aws-cdk/aws-route53/lib/records/cname.ts deleted file mode 100644 index 55e9a7bd3f32a..0000000000000 --- a/packages/@aws-cdk/aws-route53/lib/records/cname.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Construct } from '@aws-cdk/cdk'; -import { IHostedZone } from '../hosted-zone-ref'; -import { CfnRecordSet } from '../route53.generated'; -import { determineFullyQualifiedDomainName } from './_util'; - -export interface CnameRecordProps { - /** - * The hosted zone in which to define the new TXT record. - */ - readonly zone: IHostedZone; - - /** - * The domain name for this record set. - */ - readonly recordName: string; - - /** - * The value for this record set. - */ - readonly recordValue: string; - - /** - * The resource record cache time to live (TTL) in seconds. - * - * @default 1800 seconds - */ - readonly ttl?: number; -} - -/** - * A DNS CNAME record - */ -export class CnameRecord extends Construct { - constructor(scope: Construct, id: string, props: CnameRecordProps) { - super(scope, id); - - const ttl = props.ttl === undefined ? 1800 : props.ttl; - - new CfnRecordSet(this, 'Resource', { - hostedZoneId: props.zone.hostedZoneId, - name: determineFullyQualifiedDomainName(props.recordName, props.zone), - type: 'CNAME', - resourceRecords: [ props.recordValue ], - ttl: ttl.toString(), - }); - } -} diff --git a/packages/@aws-cdk/aws-route53/lib/records/index.ts b/packages/@aws-cdk/aws-route53/lib/records/index.ts deleted file mode 100644 index 1643af3bdaadb..0000000000000 --- a/packages/@aws-cdk/aws-route53/lib/records/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './alias'; -export * from './txt'; -export * from './cname'; -export * from './zone-delegation'; diff --git a/packages/@aws-cdk/aws-route53/lib/records/txt.ts b/packages/@aws-cdk/aws-route53/lib/records/txt.ts deleted file mode 100644 index 64bd769848640..0000000000000 --- a/packages/@aws-cdk/aws-route53/lib/records/txt.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { Construct } from '@aws-cdk/cdk'; -import { IHostedZone } from '../hosted-zone-ref'; -import { CfnRecordSet } from '../route53.generated'; -import { determineFullyQualifiedDomainName } from './_util'; - -export interface TxtRecordProps { - /** - * The hosted zone in which to define the new TXT record. - */ - readonly zone: IHostedZone; - - /** - * The domain name for this record set. - */ - readonly recordName: string; - - /** - * The value for this record set. - */ - readonly recordValue: string; - - /** - * The resource record cache time to live (TTL) in seconds. - * - * @default 1800 seconds - */ - readonly ttl?: number; -} - -/** - * A DNS TXT record - */ -export class TxtRecord extends Construct { - constructor(scope: Construct, id: string, props: TxtRecordProps) { - super(scope, id); - - // JSON.stringify conveniently wraps strings in " and escapes ". - const recordValue = JSON.stringify(props.recordValue); - const ttl = props.ttl === undefined ? 1800 : props.ttl; - - new CfnRecordSet(this, 'Resource', { - hostedZoneId: props.zone.hostedZoneId, - name: determineFullyQualifiedDomainName(props.recordName, props.zone), - type: 'TXT', - resourceRecords: [recordValue], - ttl: ttl.toString() - }); - } -} diff --git a/packages/@aws-cdk/aws-route53/lib/records/zone-delegation.ts b/packages/@aws-cdk/aws-route53/lib/records/zone-delegation.ts deleted file mode 100644 index 175e1c921aceb..0000000000000 --- a/packages/@aws-cdk/aws-route53/lib/records/zone-delegation.ts +++ /dev/null @@ -1,44 +0,0 @@ -import cdk = require('@aws-cdk/cdk'); -import { ZoneDelegationOptions } from '../hosted-zone'; -import { IHostedZone } from '../hosted-zone-ref'; -import { CfnRecordSet } from '../route53.generated'; -import { determineFullyQualifiedDomainName } from './_util'; - -export interface ZoneDelegationRecordProps extends ZoneDelegationOptions { - /** - * The zone in which this delegate is defined. - */ - readonly zone: IHostedZone; - /** - * The name of the zone that delegation is made to. - */ - readonly delegatedZoneName: string; - - /** - * The name servers to report in the delegation records. - */ - readonly nameServers: string[]; -} - -/** - * A record to delegate further lookups to a different set of name servers - */ -export class ZoneDelegationRecord extends cdk.Construct { - constructor(scope: cdk.Construct, id: string, props: ZoneDelegationRecordProps) { - super(scope, id); - - const ttl = props.ttl === undefined ? 172_800 : props.ttl; - const resourceRecords = cdk.Token.isToken(props.nameServers) - ? props.nameServers // Can't map a string-array token! - : props.nameServers.map(ns => (cdk.Token.isToken(ns) || ns.endsWith('.')) ? ns : `${ns}.`); - - new CfnRecordSet(this, 'Resource', { - hostedZoneId: props.zone.hostedZoneId, - name: determineFullyQualifiedDomainName(props.delegatedZoneName, props.zone), - type: 'NS', - ttl: ttl.toString(), - comment: props.comment, - resourceRecords, - }); - } -} diff --git a/packages/@aws-cdk/aws-route53/lib/util.ts b/packages/@aws-cdk/aws-route53/lib/util.ts index 2483b74f3a4ec..358f39f2da7ce 100644 --- a/packages/@aws-cdk/aws-route53/lib/util.ts +++ b/packages/@aws-cdk/aws-route53/lib/util.ts @@ -1,3 +1,5 @@ +import { IHostedZone } from './hosted-zone-ref'; + /** * Validates a zone name is valid by Route53 specifc naming rules, * and that there is no trailing dot in the name. @@ -26,3 +28,31 @@ class ValidationError extends Error { super(message); } } + +/** + * Route53 requires the record names are specified as fully qualified names, but this + * forces lots of redundant work on the user (repeating the zone name over and over). + * This function allows the user to be lazier and offers a nicer experience, by + * qualifying relative names appropriately: + * + * @param providedName the user-specified name of the record. + * @param zoneName the fully-qualified name of the zone the record will be created in. + * + * @returns + */ +export function determineFullyQualifiedDomainName(providedName: string, hostedZone: IHostedZone): string { + if (providedName.endsWith('.')) { + return providedName; + } + + const suffix = `.${hostedZone.zoneName}`; + if (providedName.endsWith(suffix) || providedName === hostedZone.zoneName) { + return `${providedName}.`; + } + + return `${providedName}${suffix}.`; +} diff --git a/packages/@aws-cdk/aws-route53/test/integ.route53.expected.json b/packages/@aws-cdk/aws-route53/test/integ.route53.expected.json index 9cde321023c0f..4a1797a096e06 100644 --- a/packages/@aws-cdk/aws-route53/test/integ.route53.expected.json +++ b/packages/@aws-cdk/aws-route53/test/integ.route53.expected.json @@ -18,7 +18,7 @@ "VPCPublicSubnet1SubnetB4246D30": { "Type": "AWS::EC2::Subnet", "Properties": { - "CidrBlock": "10.0.0.0/19", + "CidrBlock": "10.0.0.0/17", "VpcId": { "Ref": "VPCB9E5F0B4" }, @@ -106,192 +106,10 @@ ] } }, - "VPCPublicSubnet2Subnet74179F39": { - "Type": "AWS::EC2::Subnet", - "Properties": { - "CidrBlock": "10.0.32.0/19", - "VpcId": { - "Ref": "VPCB9E5F0B4" - }, - "AvailabilityZone": "test-region-1b", - "MapPublicIpOnLaunch": true, - "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-route53-integ/VPC/PublicSubnet2" - }, - { - "Key": "aws-cdk:subnet-name", - "Value": "Public" - }, - { - "Key": "aws-cdk:subnet-type", - "Value": "Public" - } - ] - } - }, - "VPCPublicSubnet2RouteTable6F1A15F1": { - "Type": "AWS::EC2::RouteTable", - "Properties": { - "VpcId": { - "Ref": "VPCB9E5F0B4" - }, - "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-route53-integ/VPC/PublicSubnet2" - } - ] - } - }, - "VPCPublicSubnet2RouteTableAssociation5A808732": { - "Type": "AWS::EC2::SubnetRouteTableAssociation", - "Properties": { - "RouteTableId": { - "Ref": "VPCPublicSubnet2RouteTable6F1A15F1" - }, - "SubnetId": { - "Ref": "VPCPublicSubnet2Subnet74179F39" - } - } - }, - "VPCPublicSubnet2DefaultRouteB7481BBA": { - "Type": "AWS::EC2::Route", - "Properties": { - "RouteTableId": { - "Ref": "VPCPublicSubnet2RouteTable6F1A15F1" - }, - "DestinationCidrBlock": "0.0.0.0/0", - "GatewayId": { - "Ref": "VPCIGWB7E252D3" - } - }, - "DependsOn": [ - "VPCVPCGW99B986DC" - ] - }, - "VPCPublicSubnet2EIP4947BC00": { - "Type": "AWS::EC2::EIP", - "Properties": { - "Domain": "vpc" - } - }, - "VPCPublicSubnet2NATGateway3C070193": { - "Type": "AWS::EC2::NatGateway", - "Properties": { - "AllocationId": { - "Fn::GetAtt": [ - "VPCPublicSubnet2EIP4947BC00", - "AllocationId" - ] - }, - "SubnetId": { - "Ref": "VPCPublicSubnet2Subnet74179F39" - }, - "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-route53-integ/VPC/PublicSubnet2" - } - ] - } - }, - "VPCPublicSubnet3Subnet631C5E25": { - "Type": "AWS::EC2::Subnet", - "Properties": { - "CidrBlock": "10.0.64.0/19", - "VpcId": { - "Ref": "VPCB9E5F0B4" - }, - "AvailabilityZone": "test-region-1c", - "MapPublicIpOnLaunch": true, - "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-route53-integ/VPC/PublicSubnet3" - }, - { - "Key": "aws-cdk:subnet-name", - "Value": "Public" - }, - { - "Key": "aws-cdk:subnet-type", - "Value": "Public" - } - ] - } - }, - "VPCPublicSubnet3RouteTable98AE0E14": { - "Type": "AWS::EC2::RouteTable", - "Properties": { - "VpcId": { - "Ref": "VPCB9E5F0B4" - }, - "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-route53-integ/VPC/PublicSubnet3" - } - ] - } - }, - "VPCPublicSubnet3RouteTableAssociation427FE0C6": { - "Type": "AWS::EC2::SubnetRouteTableAssociation", - "Properties": { - "RouteTableId": { - "Ref": "VPCPublicSubnet3RouteTable98AE0E14" - }, - "SubnetId": { - "Ref": "VPCPublicSubnet3Subnet631C5E25" - } - } - }, - "VPCPublicSubnet3DefaultRouteA0D29D46": { - "Type": "AWS::EC2::Route", - "Properties": { - "RouteTableId": { - "Ref": "VPCPublicSubnet3RouteTable98AE0E14" - }, - "DestinationCidrBlock": "0.0.0.0/0", - "GatewayId": { - "Ref": "VPCIGWB7E252D3" - } - }, - "DependsOn": [ - "VPCVPCGW99B986DC" - ] - }, - "VPCPublicSubnet3EIPAD4BC883": { - "Type": "AWS::EC2::EIP", - "Properties": { - "Domain": "vpc" - } - }, - "VPCPublicSubnet3NATGatewayD3048F5C": { - "Type": "AWS::EC2::NatGateway", - "Properties": { - "AllocationId": { - "Fn::GetAtt": [ - "VPCPublicSubnet3EIPAD4BC883", - "AllocationId" - ] - }, - "SubnetId": { - "Ref": "VPCPublicSubnet3Subnet631C5E25" - }, - "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-route53-integ/VPC/PublicSubnet3" - } - ] - } - }, "VPCPrivateSubnet1Subnet8BCA10E0": { "Type": "AWS::EC2::Subnet", "Properties": { - "CidrBlock": "10.0.96.0/19", + "CidrBlock": "10.0.128.0/17", "VpcId": { "Ref": "VPCB9E5F0B4" }, @@ -350,130 +168,6 @@ } } }, - "VPCPrivateSubnet2SubnetCFCDAA7A": { - "Type": "AWS::EC2::Subnet", - "Properties": { - "CidrBlock": "10.0.128.0/19", - "VpcId": { - "Ref": "VPCB9E5F0B4" - }, - "AvailabilityZone": "test-region-1b", - "MapPublicIpOnLaunch": false, - "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-route53-integ/VPC/PrivateSubnet2" - }, - { - "Key": "aws-cdk:subnet-name", - "Value": "Private" - }, - { - "Key": "aws-cdk:subnet-type", - "Value": "Private" - } - ] - } - }, - "VPCPrivateSubnet2RouteTable0A19E10E": { - "Type": "AWS::EC2::RouteTable", - "Properties": { - "VpcId": { - "Ref": "VPCB9E5F0B4" - }, - "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-route53-integ/VPC/PrivateSubnet2" - } - ] - } - }, - "VPCPrivateSubnet2RouteTableAssociation0C73D413": { - "Type": "AWS::EC2::SubnetRouteTableAssociation", - "Properties": { - "RouteTableId": { - "Ref": "VPCPrivateSubnet2RouteTable0A19E10E" - }, - "SubnetId": { - "Ref": "VPCPrivateSubnet2SubnetCFCDAA7A" - } - } - }, - "VPCPrivateSubnet2DefaultRouteF4F5CFD2": { - "Type": "AWS::EC2::Route", - "Properties": { - "RouteTableId": { - "Ref": "VPCPrivateSubnet2RouteTable0A19E10E" - }, - "DestinationCidrBlock": "0.0.0.0/0", - "NatGatewayId": { - "Ref": "VPCPublicSubnet2NATGateway3C070193" - } - } - }, - "VPCPrivateSubnet3Subnet3EDCD457": { - "Type": "AWS::EC2::Subnet", - "Properties": { - "CidrBlock": "10.0.160.0/19", - "VpcId": { - "Ref": "VPCB9E5F0B4" - }, - "AvailabilityZone": "test-region-1c", - "MapPublicIpOnLaunch": false, - "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-route53-integ/VPC/PrivateSubnet3" - }, - { - "Key": "aws-cdk:subnet-name", - "Value": "Private" - }, - { - "Key": "aws-cdk:subnet-type", - "Value": "Private" - } - ] - } - }, - "VPCPrivateSubnet3RouteTable192186F8": { - "Type": "AWS::EC2::RouteTable", - "Properties": { - "VpcId": { - "Ref": "VPCB9E5F0B4" - }, - "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-route53-integ/VPC/PrivateSubnet3" - } - ] - } - }, - "VPCPrivateSubnet3RouteTableAssociationC28D144E": { - "Type": "AWS::EC2::SubnetRouteTableAssociation", - "Properties": { - "RouteTableId": { - "Ref": "VPCPrivateSubnet3RouteTable192186F8" - }, - "SubnetId": { - "Ref": "VPCPrivateSubnet3Subnet3EDCD457" - } - } - }, - "VPCPrivateSubnet3DefaultRoute27F311AE": { - "Type": "AWS::EC2::Route", - "Properties": { - "RouteTableId": { - "Ref": "VPCPrivateSubnet3RouteTable192186F8" - }, - "DestinationCidrBlock": "0.0.0.0/0", - "NatGatewayId": { - "Ref": "VPCPublicSubnet3NATGatewayD3048F5C" - } - } - }, "VPCIGWB7E252D3": { "Type": "AWS::EC2::InternetGateway", "Properties": { @@ -521,7 +215,8 @@ "Ref": "PrivateZone27242E85" }, "ResourceRecords": [ - "\"Bar!\"" + "\"Bar!\"", + "\"Baz?\"" ], "TTL": "60" } @@ -568,6 +263,35 @@ ], "TTL": "1800" } + }, + "ACCC8ACD5": { + "Type": "AWS::Route53::RecordSet", + "Properties": { + "Name": "test.cdk.local.", + "Type": "A", + "HostedZoneId": { + "Ref": "PrivateZone27242E85" + }, + "ResourceRecords": [ + "1.2.3.4", + "5.6.7.8" + ], + "TTL": "1800" + } + }, + "CaaAmazon40DF725F": { + "Type": "AWS::Route53::RecordSet", + "Properties": { + "Name": "cdk.test.", + "Type": "CAA", + "HostedZoneId": { + "Ref": "PublicZone2E1C4E34" + }, + "ResourceRecords": [ + "0 issue \"amazon.com\"" + ], + "TTL": "1800" + } } }, "Outputs": { diff --git a/packages/@aws-cdk/aws-route53/test/integ.route53.ts b/packages/@aws-cdk/aws-route53/test/integ.route53.ts index 1c916099623fd..544c84e09b80c 100644 --- a/packages/@aws-cdk/aws-route53/test/integ.route53.ts +++ b/packages/@aws-cdk/aws-route53/test/integ.route53.ts @@ -1,12 +1,12 @@ import ec2 = require('@aws-cdk/aws-ec2'); import cdk = require('@aws-cdk/cdk'); -import { CnameRecord, PrivateHostedZone, PublicHostedZone, TxtRecord } from '../lib'; +import { AddressRecordTarget, ARecord, CaaAmazonRecord, CnameRecord, PrivateHostedZone, PublicHostedZone, TxtRecord } from '../lib'; const app = new cdk.App(); const stack = new cdk.Stack(app, 'aws-cdk-route53-integ'); -const vpc = new ec2.Vpc(stack, 'VPC'); +const vpc = new ec2.Vpc(stack, 'VPC', { maxAZs: 1 }); const privateZone = new PrivateHostedZone(stack, 'PrivateZone', { zoneName: 'cdk.local', vpc @@ -23,14 +23,27 @@ publicZone.addDelegation(publicSubZone); new TxtRecord(privateZone, 'TXT', { zone: privateZone, recordName: '_foo', - recordValue: 'Bar!', + values: [ + 'Bar!', + 'Baz?' + ], ttl: 60 }); new CnameRecord(stack, 'CNAME', { zone: privateZone, recordName: 'www', - recordValue: 'server' + domainName: 'server' +}); + +new ARecord(stack, 'A', { + zone: privateZone, + recordName: 'test', + target: AddressRecordTarget.fromIpAddresses('1.2.3.4', '5.6.7.8') +}); + +new CaaAmazonRecord(stack, 'CaaAmazon', { + zone: publicZone }); new cdk.CfnOutput(stack, 'PrivateZoneId', { value: privateZone.hostedZoneId }); diff --git a/packages/@aws-cdk/aws-route53/test/test.alias-record.ts b/packages/@aws-cdk/aws-route53/test/test.alias-record.ts deleted file mode 100644 index 412f0f271d17d..0000000000000 --- a/packages/@aws-cdk/aws-route53/test/test.alias-record.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { expect, haveResource } from '@aws-cdk/assert'; -import { Stack } from '@aws-cdk/cdk'; -import { Test } from 'nodeunit'; -import { AliasRecord, IAliasRecordTarget, PublicHostedZone } from '../lib'; - -export = { - 'test alias record'(test: Test) { - // GIVEN - const stack = new Stack(); - const zone = new PublicHostedZone(stack, 'HostedZone', { zoneName: 'test.public' }); - - const target: IAliasRecordTarget = { - bind: () => { - return { - hostedZoneId: 'Z2P70J7EXAMPLE', - dnsName: 'foo.example.com' - }; - } - }; - - // WHEN - new AliasRecord(zone, 'Alias', { - zone, - recordName: '_foo', - target - }); - - // THEN - stack contains a record set - expect(stack).to(haveResource('AWS::Route53::RecordSet', { - Name: '_foo.test.public.', - HostedZoneId: { - Ref: 'HostedZoneDB99F866' - }, - Type: 'A', - AliasTarget: { - HostedZoneId: 'Z2P70J7EXAMPLE', - DNSName: 'foo.example.com', - } - })); - - test.done(); - }, - 'test alias record on zone root'(test: Test) { - // GIVEN - const stack = new Stack(); - const zone = new PublicHostedZone(stack, 'HostedZone', { zoneName: 'test.public' }); - - const target: IAliasRecordTarget = { - bind: () => { - return { - hostedZoneId: 'Z2P70J7EXAMPLE', - dnsName: 'foo.example.com' - }; - } - }; - - // WHEN - new AliasRecord(zone, 'Alias', { - zone, - recordName: 'test.public', - target - }); - - // THEN - stack contains a record set - expect(stack).to(haveResource('AWS::Route53::RecordSet', { - Name: 'test.public.', - HostedZoneId: { - Ref: 'HostedZoneDB99F866' - }, - Type: 'A', - AliasTarget: { - HostedZoneId: 'Z2P70J7EXAMPLE', - DNSName: 'foo.example.com', - } - })); - - test.done(); - } -}; diff --git a/packages/@aws-cdk/aws-route53/test/test.cname-record.ts b/packages/@aws-cdk/aws-route53/test/test.cname-record.ts deleted file mode 100644 index 92d1b84b478fe..0000000000000 --- a/packages/@aws-cdk/aws-route53/test/test.cname-record.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { expect, haveResource } from '@aws-cdk/assert'; -import { Stack } from '@aws-cdk/cdk'; -import { Test } from 'nodeunit'; -import route53 = require('../lib'); - -export = { - 'with default ttl'(test: Test) { - // GIVEN - const stack = new Stack(); - - // WHEN - const zone = new route53.HostedZone(stack, 'HostedZone', { - zoneName: 'myzone' - }); - - new route53.CnameRecord(stack, 'MyCname', { - zone, - recordName: 'www', - recordValue: 'zzz', - }); - - // THEN - expect(stack).to(haveResource('AWS::Route53::RecordSet', { - Name: "www.myzone.", - Type: "CNAME", - HostedZoneId: { - Ref: "HostedZoneDB99F866" - }, - ResourceRecords: [ - "zzz" - ], - TTL: "1800" - })); - test.done(); - }, - - 'with custom ttl'(test: Test) { - // GIVEN - const stack = new Stack(); - - // WHEN - const zone = new route53.HostedZone(stack, 'HostedZone', { - zoneName: 'myzone' - }); - - new route53.CnameRecord(stack, 'MyCname', { - zone, - recordName: 'aa', - recordValue: 'bbb', - ttl: 6077, - }); - - // THEN - expect(stack).to(haveResource('AWS::Route53::RecordSet', { - Name: "aa.myzone.", - Type: "CNAME", - HostedZoneId: { - Ref: "HostedZoneDB99F866" - }, - ResourceRecords: [ - "bbb" - ], - TTL: "6077" - })); - test.done(); - } -}; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-route53/test/test.record-set.ts b/packages/@aws-cdk/aws-route53/test/test.record-set.ts new file mode 100644 index 0000000000000..8dca376245ab5 --- /dev/null +++ b/packages/@aws-cdk/aws-route53/test/test.record-set.ts @@ -0,0 +1,458 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import { Stack } from '@aws-cdk/cdk'; +import { Test } from 'nodeunit'; +import route53 = require('../lib'); + +export = { + 'with default ttl'(test: Test) { + // GIVEN + const stack = new Stack(); + + const zone = new route53.HostedZone(stack, 'HostedZone', { + zoneName: 'myzone' + }); + + // WHEN + new route53.RecordSet(stack, 'Basic', { + zone, + recordName: 'www', + recordType: route53.RecordType.CNAME, + target: route53.RecordTarget.fromValues('zzz') + }); + + // THEN + expect(stack).to(haveResource('AWS::Route53::RecordSet', { + Name: "www.myzone.", + Type: "CNAME", + HostedZoneId: { + Ref: "HostedZoneDB99F866" + }, + ResourceRecords: [ + "zzz" + ], + TTL: "1800" + })); + test.done(); + }, + + 'with custom ttl'(test: Test) { + // GIVEN + const stack = new Stack(); + + const zone = new route53.HostedZone(stack, 'HostedZone', { + zoneName: 'myzone' + }); + + // WHEN + new route53.RecordSet(stack, 'Basic', { + zone, + recordName: 'aa', + recordType: route53.RecordType.CNAME, + target: route53.RecordTarget.fromValues('bbb'), + ttl: 6077 + }); + + // THEN + expect(stack).to(haveResource('AWS::Route53::RecordSet', { + Name: "aa.myzone.", + Type: "CNAME", + HostedZoneId: { + Ref: "HostedZoneDB99F866" + }, + ResourceRecords: [ + "bbb" + ], + TTL: "6077" + })); + test.done(); + }, + + 'defaults to zone root'(test: Test) { + // GIVEN + const stack = new Stack(); + + const zone = new route53.HostedZone(stack, 'HostedZone', { + zoneName: 'myzone' + }); + + // WHEN + new route53.RecordSet(stack, 'Basic', { + zone, + recordType: route53.RecordType.A, + target: route53.RecordTarget.fromValues('1.2.3.4'), + }); + + // THEN + expect(stack).to(haveResource('AWS::Route53::RecordSet', { + Name: "myzone.", + Type: "A", + HostedZoneId: { + Ref: "HostedZoneDB99F866" + }, + ResourceRecords: [ + "1.2.3.4" + ], + })); + test.done(); + }, + + 'A record with ip addresses'(test: Test) { + // GIVEN + const stack = new Stack(); + + const zone = new route53.HostedZone(stack, 'HostedZone', { + zoneName: 'myzone' + }); + + // WHEN + new route53.ARecord(stack, 'A', { + zone, + recordName: 'www', + target: route53.AddressRecordTarget.fromIpAddresses('1.2.3.4', '5.6.7.8'), + }); + + // THEN + expect(stack).to(haveResource('AWS::Route53::RecordSet', { + Name: "www.myzone.", + Type: "A", + HostedZoneId: { + Ref: "HostedZoneDB99F866" + }, + ResourceRecords: [ + "1.2.3.4", + "5.6.7.8" + ], + TTL: "1800" + })); + test.done(); + }, + + 'A record with alias'(test: Test) { + // GIVEN + const stack = new Stack(); + + const zone = new route53.HostedZone(stack, 'HostedZone', { + zoneName: 'myzone' + }); + + const target: route53.IAliasRecordTarget = { + bind: () => { + return { + hostedZoneId: 'Z2P70J7EXAMPLE', + dnsName: 'foo.example.com' + }; + } + }; + + // WHEN + new route53.ARecord(zone, 'Alias', { + zone, + recordName: '_foo', + target: route53.RecordTarget.fromAlias(target) + }); + + // THEN + expect(stack).to(haveResource('AWS::Route53::RecordSet', { + Name: '_foo.myzone.', + HostedZoneId: { + Ref: 'HostedZoneDB99F866' + }, + Type: 'A', + AliasTarget: { + HostedZoneId: 'Z2P70J7EXAMPLE', + DNSName: 'foo.example.com', + } + })); + + test.done(); + }, + + 'AAAA record with ip addresses'(test: Test) { + // GIVEN + const stack = new Stack(); + + const zone = new route53.HostedZone(stack, 'HostedZone', { + zoneName: 'myzone' + }); + + // WHEN + new route53.AaaaRecord(stack, 'AAAA', { + zone, + recordName: 'www', + target: route53.AddressRecordTarget.fromIpAddresses('2001:0db8:85a3:0000:0000:8a2e:0370:7334'), + }); + + // THEN + expect(stack).to(haveResource('AWS::Route53::RecordSet', { + Name: "www.myzone.", + Type: "AAAA", + HostedZoneId: { + Ref: "HostedZoneDB99F866" + }, + ResourceRecords: [ + "2001:0db8:85a3:0000:0000:8a2e:0370:7334" + ], + TTL: "1800" + })); + test.done(); + }, + + 'AAAA record with alias on zone root'(test: Test) { + // GIVEN + const stack = new Stack(); + const zone = new route53.HostedZone(stack, 'HostedZone', { + zoneName: 'myzone' + }); + + const target: route53.IAliasRecordTarget = { + bind: () => { + return { + hostedZoneId: 'Z2P70J7EXAMPLE', + dnsName: 'foo.example.com' + }; + } + }; + + // WHEN + new route53.AaaaRecord(zone, 'Alias', { + zone, + target: route53.RecordTarget.fromAlias(target) + }); + + // THEN + expect(stack).to(haveResource('AWS::Route53::RecordSet', { + Name: 'myzone.', + HostedZoneId: { + Ref: 'HostedZoneDB99F866' + }, + Type: 'AAAA', + AliasTarget: { + HostedZoneId: 'Z2P70J7EXAMPLE', + DNSName: 'foo.example.com', + } + })); + + test.done(); + }, + + 'CNAME record'(test: Test) { + // GIVEN + const stack = new Stack(); + + const zone = new route53.HostedZone(stack, 'HostedZone', { + zoneName: 'myzone' + }); + + // WHEN + new route53.CnameRecord(stack, 'CNAME', { + zone, + recordName: 'www', + domainName: 'hello', + }); + + // THEN + expect(stack).to(haveResource('AWS::Route53::RecordSet', { + Name: "www.myzone.", + Type: "CNAME", + HostedZoneId: { + Ref: "HostedZoneDB99F866" + }, + ResourceRecords: [ + "hello" + ], + TTL: "1800" + })); + test.done(); + }, + + 'TXT record'(test: Test) { + // GIVEN + const stack = new Stack(); + + const zone = new route53.HostedZone(stack, 'HostedZone', { + zoneName: 'myzone' + }); + + // WHEN + new route53.TxtRecord(stack, 'TXT', { + zone, + recordName: 'www', + values: ['should be enclosed with double quotes'] + }); + + // THEN + expect(stack).to(haveResource('AWS::Route53::RecordSet', { + Name: "www.myzone.", + Type: "TXT", + HostedZoneId: { + Ref: "HostedZoneDB99F866" + }, + ResourceRecords: [ + '"should be enclosed with double quotes"' + ], + TTL: "1800" + })); + test.done(); + }, + + 'SRV record'(test: Test) { + // GIVEN + const stack = new Stack(); + + const zone = new route53.HostedZone(stack, 'HostedZone', { + zoneName: 'myzone' + }); + + // WHEN + new route53.SrvRecord(stack, 'SRV', { + zone, + recordName: 'www', + values: [{ + hostName: 'aws.com', + port: 8080, + priority: 10, + weight: 5 + }] + }); + + // THEN + expect(stack).to(haveResource('AWS::Route53::RecordSet', { + Name: "www.myzone.", + Type: "SRV", + HostedZoneId: { + Ref: "HostedZoneDB99F866" + }, + ResourceRecords: [ + '10 5 8080 aws.com' + ], + TTL: "1800" + })); + test.done(); + }, + + 'CAA record'(test: Test) { + // GIVEN + const stack = new Stack(); + + const zone = new route53.HostedZone(stack, 'HostedZone', { + zoneName: 'myzone' + }); + + // WHEN + new route53.CaaRecord(stack, 'CAA', { + zone, + recordName: 'www', + values: [{ + flag: 0, + tag: route53.CaaTag.ISSUE, + value: 'ssl.com' + }] + }); + + // THEN + expect(stack).to(haveResource('AWS::Route53::RecordSet', { + Name: "www.myzone.", + Type: "CAA", + HostedZoneId: { + Ref: "HostedZoneDB99F866" + }, + ResourceRecords: [ + '0 issue "ssl.com"' + ], + TTL: "1800" + })); + test.done(); + }, + + 'CAA Amazon record'(test: Test) { + // GIVEN + const stack = new Stack(); + + const zone = new route53.HostedZone(stack, 'HostedZone', { + zoneName: 'myzone' + }); + + // WHEN + new route53.CaaAmazonRecord(stack, 'CAAAmazon', { + zone, + recordName: 'www', // should have no effect + }); + + // THEN + expect(stack).to(haveResource('AWS::Route53::RecordSet', { + Name: "myzone.", + Type: "CAA", + HostedZoneId: { + Ref: "HostedZoneDB99F866" + }, + ResourceRecords: [ + '0 issue "amazon.com"' + ], + TTL: "1800" + })); + test.done(); + }, + + 'MX record'(test: Test) { + // GIVEN + const stack = new Stack(); + + const zone = new route53.HostedZone(stack, 'HostedZone', { + zoneName: 'myzone' + }); + + // WHEN + new route53.MxRecord(stack, 'MX', { + zone, + recordName: 'mail', + values: [{ + hostName: 'workmail.aws', + priority: 10 + }] + }); + + // THEN + expect(stack).to(haveResource('AWS::Route53::RecordSet', { + Name: "mail.myzone.", + Type: "MX", + HostedZoneId: { + Ref: "HostedZoneDB99F866" + }, + ResourceRecords: [ + '10 workmail.aws' + ], + TTL: "1800" + })); + test.done(); + }, + + 'Zone delegation record'(test: Test) { + // GIVEN + const stack = new Stack(); + + const zone = new route53.HostedZone(stack, 'HostedZone', { + zoneName: 'myzone' + }); + + // WHEN + new route53.ZoneDelegationRecord(stack, 'NS', { + zone, + recordName: 'foo', + nameServers: ['ns-1777.awsdns-30.co.uk'] + }); + + // THEN + expect(stack).to(haveResource('AWS::Route53::RecordSet', { + Name: "foo.myzone.", + Type: "NS", + HostedZoneId: { + Ref: "HostedZoneDB99F866" + }, + ResourceRecords: [ + "ns-1777.awsdns-30.co.uk." + ], + TTL: "172800" + })); + test.done(); + } +}; diff --git a/packages/@aws-cdk/aws-route53/test/test.route53.ts b/packages/@aws-cdk/aws-route53/test/test.route53.ts index 6e1dd3286abe3..e5cac2f6a1879 100644 --- a/packages/@aws-cdk/aws-route53/test/test.route53.ts +++ b/packages/@aws-cdk/aws-route53/test/test.route53.ts @@ -80,7 +80,7 @@ export = { new TxtRecord(importedZone as any, 'Record', { zone: importedZone, recordName: 'lookHere', - recordValue: 'SeeThere' + values: ['SeeThere'] }); expect(stack2).to(haveResource("AWS::Route53::RecordSet", { @@ -194,6 +194,27 @@ export = { })); test.done(); }, + + 'public hosted zone wiht caaAmazon set to true'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new PublicHostedZone(stack, 'MyHostedZone', { + zoneName: 'protected.com', + caaAmazon: true + }); + + // THEN + expect(stack).to(haveResource('AWS::Route53::RecordSet', { + Type: 'CAA', + Name: 'protected.com.', + ResourceRecords: [ + '0 issue "amazon.com"' + ] + })); + test.done(); + } }; class TestApp { diff --git a/packages/@aws-cdk/aws-route53/test/test.txt-record.ts b/packages/@aws-cdk/aws-route53/test/test.txt-record.ts deleted file mode 100644 index c213b798813c2..0000000000000 --- a/packages/@aws-cdk/aws-route53/test/test.txt-record.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { exactlyMatchTemplate, expect } from '@aws-cdk/assert'; -import { App, Stack } from '@aws-cdk/cdk'; -import { Test } from 'nodeunit'; -import { PublicHostedZone, TxtRecord } from '../lib'; - -export = { - 'TXT records': { - TXT(test: Test) { - const app = new TestApp(); - const zone = new PublicHostedZone(app.stack, 'HostedZone', { zoneName: 'test.public' }); - new TxtRecord(zone, 'TXT', { zone, recordName: '_foo', recordValue: 'Bar!' }); - expect(app.stack).to(exactlyMatchTemplate({ - Resources: { - HostedZoneDB99F866: { - Type: 'AWS::Route53::HostedZone', - Properties: { - Name: 'test.public.' - } - }, - HostedZoneTXT69C29760: { - Type: 'AWS::Route53::RecordSet', - Properties: { - HostedZoneId: { - Ref: 'HostedZoneDB99F866' - }, - Name: '_foo.test.public.', - ResourceRecords: ['"Bar!"'], - Type: 'TXT', - TTL: '1800' - } - } - } - })); - test.done(); - } - } -}; - -class TestApp { - public readonly stack: Stack; - private readonly app = new App(); - - constructor() { - const account = '123456789012'; - const region = 'bermuda-triangle'; - this.app.node.setContext(`availability-zones:${account}:${region}`, - [`${region}-1a`]); - this.stack = new Stack(this.app, 'MyStack', { env: { account, region } }); - } -} diff --git a/packages/@aws-cdk/aws-route53/test/test.zone-delegation-record.ts b/packages/@aws-cdk/aws-route53/test/test.zone-delegation-record.ts deleted file mode 100644 index 03d05b2161c74..0000000000000 --- a/packages/@aws-cdk/aws-route53/test/test.zone-delegation-record.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { exactlyMatchTemplate, expect } from '@aws-cdk/assert'; -import { App, Stack } from '@aws-cdk/cdk'; -import { Test } from 'nodeunit'; -import { PublicHostedZone, ZoneDelegationRecord } from '../lib'; - -export = { - 'Zone Delegation records': { - NS(test: Test) { - const app = new TestApp(); - const zone = new PublicHostedZone(app.stack, 'HostedZone', { zoneName: 'test.public' }); - new ZoneDelegationRecord(zone, 'NS', { - zone, - delegatedZoneName: 'foo', - nameServers: ['ns-1777.awsdns-30.co.uk'] - }); - expect(app.stack).to(exactlyMatchTemplate({ - Resources: { - HostedZoneDB99F866: { - Type: 'AWS::Route53::HostedZone', - Properties: { - Name: 'test.public.' - } - }, - HostedZoneNS1BB87CC3: { - Type: 'AWS::Route53::RecordSet', - Properties: { - HostedZoneId: { - Ref: 'HostedZoneDB99F866' - }, - Name: 'foo.test.public.', - ResourceRecords: ['ns-1777.awsdns-30.co.uk.'], - Type: 'NS', - TTL: '172800' - } - } - } - })); - test.done(); - } - } -}; - -class TestApp { - public readonly stack: Stack; - private readonly app = new App(); - - constructor() { - const account = '123456789012'; - const region = 'bermuda-triangle'; - this.app.node.setContext(`availability-zones:${account}:${region}`, - [`${region}-1a`]); - this.stack = new Stack(this.app, 'MyStack', { env: { account, region } }); - } -}