diff --git a/packages/@aws-cdk/aws-route53/README.md b/packages/@aws-cdk/aws-route53/README.md index 6476432160805..51262f30e865f 100644 --- a/packages/@aws-cdk/aws-route53/README.md +++ b/packages/@aws-cdk/aws-route53/README.md @@ -1,4 +1,4 @@ -## AWS Route53 Constuct Library +## AWS Route53 Construct Library To add a public hosted zone: @@ -34,7 +34,7 @@ To add a TXT record to your zone: ```ts import route53 = require('@aws-cdk/aws-route53'); -new route53.TXTRecord(zone, 'TXTRecord', { +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. diff --git a/packages/@aws-cdk/aws-route53/lib/hosted-zone-ref.ts b/packages/@aws-cdk/aws-route53/lib/hosted-zone-ref.ts index a007f97db19d2..33a7b0e295fb9 100644 --- a/packages/@aws-cdk/aws-route53/lib/hosted-zone-ref.ts +++ b/packages/@aws-cdk/aws-route53/lib/hosted-zone-ref.ts @@ -5,7 +5,7 @@ import cdk = require('@aws-cdk/cdk'); */ export interface IHostedZone extends cdk.IConstruct { /** - * ID of this hosted zone + * ID of this hosted zone, such as "Z23ABC4XYZL05B" */ readonly hostedZoneId: string; @@ -14,6 +14,14 @@ export interface IHostedZone extends cdk.IConstruct { */ readonly zoneName: string; + /** + * Returns the set of name servers for the specific hosted zone. For example: + * ns1.example.com. + * + * This attribute will be undefined for private hosted zones or hosted zones imported from another stack. + */ + readonly hostedZoneNameServers?: string[]; + /** * Export the hosted zone */ diff --git a/packages/@aws-cdk/aws-route53/lib/hosted-zone.ts b/packages/@aws-cdk/aws-route53/lib/hosted-zone.ts index a4b7c1e865ef2..fb05148c2b7e9 100644 --- a/packages/@aws-cdk/aws-route53/lib/hosted-zone.ts +++ b/packages/@aws-cdk/aws-route53/lib/hosted-zone.ts @@ -1,89 +1,125 @@ import ec2 = require('@aws-cdk/aws-ec2'); import cdk = require('@aws-cdk/cdk'); import { HostedZoneImportProps, IHostedZone } from './hosted-zone-ref'; -import { CfnHostedZone, HostedZoneNameServers } from './route53.generated'; +import { CfnHostedZone } from './route53.generated'; import { validateZoneName } from './util'; -/** - * Properties of a new hosted zone - */ -export interface PublicHostedZoneProps { +export interface CommonHostedZoneProps { /** - * The fully qualified domain name for the hosted zone + * The name of the domain. For resource record types that include a domain + * name, specify a fully qualified domain name. */ zoneName: string; /** * Any comments that you want to include about the hosted zone. * - * @default no comment + * @default none */ comment?: string; /** * The Amazon Resource Name (ARN) for the log group that you want Amazon Route 53 to send query logs to. * - * @default no DNS query logging + * @default disabled */ queryLogsLogGroupArn?: string; } -export abstract class HostedZone extends cdk.Construct implements IHostedZone { - public static import(scope: cdk.Construct, id: string, props: HostedZoneImportProps): IHostedZone { - return new ImportedHostedZone(scope, id, props); - } - - public abstract readonly hostedZoneId: string; - public abstract readonly zoneName: string; - - public export(): HostedZoneImportProps { - return { - hostedZoneId: new cdk.Output(this, 'HostedZoneId', { value: this.hostedZoneId }).makeImportValue().toString(), - zoneName: this.zoneName, - }; - } -} - /** - * Create a Route53 public hosted zone. + * Properties of a new hosted zone */ -export class PublicHostedZone extends HostedZone { +export interface HostedZoneProps extends CommonHostedZoneProps { /** - * Identifier of this hosted zone + * A VPC that you want to associate with this hosted zone. When you specify + * this property, a private hosted zone will be created. + * + * You can associate additional VPCs to this private zone using `addVpc(vpc)`. + * + * @default public (no VPCs associated) */ - public readonly hostedZoneId: string; + vpcs?: ec2.IVpcNetwork[]; +} +export class HostedZone extends cdk.Construct implements IHostedZone { /** - * Fully qualified domain name for the hosted zone + * Imports a hosted zone from another stack. */ + public static import(scope: cdk.Construct, id: string, props: HostedZoneImportProps): IHostedZone { + return new ImportedHostedZone(scope, id, props); + } + + public readonly hostedZoneId: string; public readonly zoneName: string; + public readonly hostedZoneNameServers?: string[]; /** - * Nameservers for this public hosted zone + * VPCs to which this hosted zone will be added */ - public readonly nameServers: HostedZoneNameServers; + protected readonly vpcs = new Array(); - constructor(scope: cdk.Construct, id: string, props: PublicHostedZoneProps) { + constructor(scope: cdk.Construct, id: string, props: HostedZoneProps) { super(scope, id); validateZoneName(props.zoneName); const hostedZone = new CfnHostedZone(this, 'Resource', { - ...determineHostedZoneProps(props) + name: props.zoneName + '.', + hostedZoneConfig: props.comment ? { comment: props.comment } : undefined, + queryLoggingConfig: props.queryLogsLogGroupArn ? { cloudWatchLogsLogGroupArn: props.queryLogsLogGroupArn } : undefined, + vpcs: new cdk.Token(() => this.vpcs.length === 0 ? undefined : this.vpcs) }); this.hostedZoneId = hostedZone.ref; - this.nameServers = hostedZone.hostedZoneNameServers; + this.hostedZoneNameServers = hostedZone.hostedZoneNameServers.toList(); this.zoneName = props.zoneName; + + for (const vpc of props.vpcs || []) { + this.addVpc(vpc); + } + } + + public export(): HostedZoneImportProps { + return { + hostedZoneId: new cdk.Output(this, 'HostedZoneId', { value: this.hostedZoneId }).makeImportValue(), + zoneName: this.zoneName, + }; + } + + /** + * Add another VPC to this private hosted zone. + * + * @param vpc the other VPC to add. + */ + public addVpc(vpc: ec2.IVpcNetwork) { + this.vpcs.push({ vpcId: vpc.vpcId, vpcRegion: new cdk.AwsRegion() }); } } +// tslint:disable-next-line:no-empty-interface +export interface PublicHostedZoneProps extends CommonHostedZoneProps { + +} + /** - * Properties for a private hosted zone. + * Create a Route53 public hosted zone. */ -export interface PrivateHostedZoneProps extends PublicHostedZoneProps { +export class PublicHostedZone extends HostedZone { + constructor(scope: cdk.Construct, id: string, props: PublicHostedZoneProps) { + super(scope, id, props); + } + + public addVpc(_vpc: ec2.IVpcNetwork) { + throw new Error('Cannot associate public hosted zones with a VPC'); + } +} + +export interface PrivateHostedZoneProps extends CommonHostedZoneProps { /** - * One VPC that you want to associate with this hosted zone. + * A VPC that you want to associate with this hosted zone. + * + * Private hosted zones must be associated with at least one VPC. You can + * associated additional VPCs using `addVpc(vpc)`. */ vpc: ec2.IVpcNetwork; } @@ -95,57 +131,11 @@ export interface PrivateHostedZoneProps extends PublicHostedZoneProps { * for the VPC you're configuring for private hosted zones. */ export class PrivateHostedZone extends HostedZone { - /** - * Identifier of this hosted zone - */ - public readonly hostedZoneId: string; - - /** - * Fully qualified domain name for the hosted zone - */ - public readonly zoneName: string; - - /** - * VPCs to which this hosted zone will be added - */ - private readonly vpcs: CfnHostedZone.VPCProperty[] = []; - constructor(scope: cdk.Construct, id: string, props: PrivateHostedZoneProps) { - super(scope, id); - - validateZoneName(props.zoneName); - - const hostedZone = new CfnHostedZone(this, 'Resource', { - vpcs: new cdk.Token(() => this.vpcs ? this.vpcs : undefined), - ...determineHostedZoneProps(props) - }); - - this.hostedZoneId = hostedZone.ref; - this.zoneName = props.zoneName; + super(scope, id, props); this.addVpc(props.vpc); } - - /** - * Add another VPC to this private hosted zone. - * - * @param vpc the other VPC to add. - */ - public addVpc(vpc: ec2.IVpcNetwork) { - this.vpcs.push(toVpcProperty(vpc)); - } -} - -function toVpcProperty(vpc: ec2.IVpcNetwork): CfnHostedZone.VPCProperty { - return { vpcId: vpc.vpcId, vpcRegion: new cdk.AwsRegion() }; -} - -function determineHostedZoneProps(props: PublicHostedZoneProps) { - const name = props.zoneName + '.'; - const hostedZoneConfig = props.comment ? { comment: props.comment } : undefined; - const queryLoggingConfig = props.queryLogsLogGroupArn ? { cloudWatchLogsLogGroupArn: props.queryLogsLogGroupArn } : undefined; - - return { name, hostedZoneConfig, queryLoggingConfig }; } /** @@ -153,7 +143,6 @@ function determineHostedZoneProps(props: PublicHostedZoneProps) { */ class ImportedHostedZone extends cdk.Construct implements IHostedZone { public readonly hostedZoneId: string; - public readonly zoneName: string; constructor(scope: cdk.Construct, name: string, private readonly props: HostedZoneImportProps) { diff --git a/packages/@aws-cdk/aws-route53/lib/records/cname.ts b/packages/@aws-cdk/aws-route53/lib/records/cname.ts new file mode 100644 index 0000000000000..e41bda2f515d8 --- /dev/null +++ b/packages/@aws-cdk/aws-route53/lib/records/cname.ts @@ -0,0 +1,47 @@ +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. + */ + zone: IHostedZone; + + /** + * The domain name for this record set. + */ + recordName: string; + + /** + * The value for this record set. + */ + recordValue: string; + + /** + * The resource record cache time to live (TTL) in seconds. + * + * @default 1800 seconds + */ + 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 index 655c84bf4ca70..1643af3bdaadb 100644 --- a/packages/@aws-cdk/aws-route53/lib/records/index.ts +++ b/packages/@aws-cdk/aws-route53/lib/records/index.ts @@ -1,3 +1,4 @@ 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 index b227e277e3dde..a4cafcbcea50b 100644 --- a/packages/@aws-cdk/aws-route53/lib/records/txt.ts +++ b/packages/@aws-cdk/aws-route53/lib/records/txt.ts @@ -3,19 +3,35 @@ import { IHostedZone } from '../hosted-zone-ref'; import { CfnRecordSet } from '../route53.generated'; import { determineFullyQualifiedDomainName } from './_util'; -export interface TXTRecordProps { +export interface TxtRecordProps { + /** + * The hosted zone in which to define the new TXT record. + */ zone: IHostedZone; + + /** + * The domain name for this record set. + */ recordName: string; + + /** + * The value for this record set. + */ recordValue: string; - /** @default 1800 seconds */ + + /** + * The resource record cache time to live (TTL) in seconds. + * + * @default 1800 seconds + */ ttl?: number; } /** * A DNS TXT record */ -export class TXTRecord extends Construct { - constructor(scope: Construct, id: string, props: TXTRecordProps) { +export class TxtRecord extends Construct { + constructor(scope: Construct, id: string, props: TxtRecordProps) { super(scope, id); // JSON.stringify conveniently wraps strings in " and escapes ". diff --git a/packages/@aws-cdk/aws-route53/package.json b/packages/@aws-cdk/aws-route53/package.json index 34482a57ed11a..ff4a7c64b0d4d 100644 --- a/packages/@aws-cdk/aws-route53/package.json +++ b/packages/@aws-cdk/aws-route53/package.json @@ -73,11 +73,5 @@ }, "engines": { "node": ">= 8.10.0" - }, - "awslint": { - "exclude": [ - "resource-attribute:@aws-cdk/aws-route53.IHostedZone.hostedZoneNameServers", - "resource-props:@aws-cdk/aws-route53.HostedZoneProps" - ] } } 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 a35b1b55fcfd2..ee65a6bd2ae21 100644 --- a/packages/@aws-cdk/aws-route53/test/integ.route53.expected.json +++ b/packages/@aws-cdk/aws-route53/test/integ.route53.expected.json @@ -67,9 +67,6 @@ }, "VPCPublicSubnet1DefaultRoute91CEF279": { "Type": "AWS::EC2::Route", - "DependsOn": [ - "VPCVPCGW99B986DC" - ], "Properties": { "RouteTableId": { "Ref": "VPCPublicSubnet1RouteTableFEE4B781" @@ -78,7 +75,10 @@ "GatewayId": { "Ref": "VPCIGWB7E252D3" } - } + }, + "DependsOn": [ + "VPCVPCGW99B986DC" + ] }, "VPCPublicSubnet1EIP6AD938E8": { "Type": "AWS::EC2::EIP", @@ -158,9 +158,6 @@ }, "VPCPublicSubnet2DefaultRouteB7481BBA": { "Type": "AWS::EC2::Route", - "DependsOn": [ - "VPCVPCGW99B986DC" - ], "Properties": { "RouteTableId": { "Ref": "VPCPublicSubnet2RouteTable6F1A15F1" @@ -169,7 +166,10 @@ "GatewayId": { "Ref": "VPCIGWB7E252D3" } - } + }, + "DependsOn": [ + "VPCVPCGW99B986DC" + ] }, "VPCPublicSubnet2EIP4947BC00": { "Type": "AWS::EC2::EIP", @@ -249,9 +249,6 @@ }, "VPCPublicSubnet3DefaultRouteA0D29D46": { "Type": "AWS::EC2::Route", - "DependsOn": [ - "VPCVPCGW99B986DC" - ], "Properties": { "RouteTableId": { "Ref": "VPCPublicSubnet3RouteTable98AE0E14" @@ -260,7 +257,10 @@ "GatewayId": { "Ref": "VPCIGWB7E252D3" } - } + }, + "DependsOn": [ + "VPCVPCGW99B986DC" + ] }, "VPCPublicSubnet3EIPAD4BC883": { "Type": "AWS::EC2::EIP", @@ -531,6 +531,20 @@ "Properties": { "Name": "cdk.test." } + }, + "CNAMEC70A2D52": { + "Type": "AWS::Route53::RecordSet", + "Properties": { + "Name": "www.cdk.local.", + "Type": "CNAME", + "HostedZoneId": { + "Ref": "PrivateZone27242E85" + }, + "ResourceRecords": [ + "server" + ], + "TTL": "1800" + } } }, "Outputs": { @@ -551,4 +565,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-route53/test/integ.route53.ts b/packages/@aws-cdk/aws-route53/test/integ.route53.ts index f276d80e26aaa..99b204971f5bf 100644 --- a/packages/@aws-cdk/aws-route53/test/integ.route53.ts +++ b/packages/@aws-cdk/aws-route53/test/integ.route53.ts @@ -1,6 +1,6 @@ import ec2 = require('@aws-cdk/aws-ec2'); import cdk = require('@aws-cdk/cdk'); -import { PrivateHostedZone, PublicHostedZone, TXTRecord } from '../lib'; +import { CnameRecord, PrivateHostedZone, PublicHostedZone, TxtRecord } from '../lib'; const app = new cdk.App(); @@ -16,13 +16,19 @@ const publicZone = new PublicHostedZone(stack, 'PublicZone', { zoneName: 'cdk.test' }); -new TXTRecord(privateZone, 'TXT', { +new TxtRecord(privateZone, 'TXT', { zone: privateZone, recordName: '_foo', recordValue: 'Bar!', ttl: 60 }); +new CnameRecord(stack, 'CNAME', { + zone: privateZone, + recordName: 'www', + recordValue: 'server' +}); + new cdk.Output(stack, 'PrivateZoneId', { value: privateZone.hostedZoneId }); new cdk.Output(stack, 'PublicZoneId', { value: publicZone.hostedZoneId }); diff --git a/packages/@aws-cdk/aws-route53/test/test.cname-record.ts b/packages/@aws-cdk/aws-route53/test/test.cname-record.ts new file mode 100644 index 0000000000000..92d1b84b478fe --- /dev/null +++ b/packages/@aws-cdk/aws-route53/test/test.cname-record.ts @@ -0,0 +1,67 @@ +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.route53.ts b/packages/@aws-cdk/aws-route53/test/test.route53.ts index 3a18465c1ff03..87898bcf30603 100644 --- a/packages/@aws-cdk/aws-route53/test/test.route53.ts +++ b/packages/@aws-cdk/aws-route53/test/test.route53.ts @@ -2,7 +2,7 @@ import { beASupersetOfTemplate, exactlyMatchTemplate, expect, haveResource } fro import ec2 = require('@aws-cdk/aws-ec2'); import cdk = require('@aws-cdk/cdk'); import { Test } from 'nodeunit'; -import { HostedZone, PrivateHostedZone, PublicHostedZone, TXTRecord } from '../lib'; +import { HostedZone, PrivateHostedZone, PublicHostedZone, TxtRecord } from '../lib'; export = { 'default properties': { @@ -79,7 +79,7 @@ export = { const zoneRef = zone.export(); const importedZone = HostedZone.import(stack2, 'Imported', zoneRef); - new TXTRecord(importedZone as any, 'Record', { + new TxtRecord(importedZone as any, 'Record', { zone: importedZone, recordName: 'lookHere', recordValue: 'SeeThere' @@ -109,6 +109,88 @@ export = { Type: "TXT" })); + test.done(); + }, + + 'adds period to name if not provided'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new HostedZone(stack, 'MyHostedZone', { + zoneName: 'zonename' + }); + + // THEN + expect(stack).to(haveResource('AWS::Route53::HostedZone', { + Name: 'zonename.' + })); + test.done(); + }, + + 'fails if zone name ends with a trailing dot'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // THEN + test.throws(() => new HostedZone(stack, 'MyHostedZone', { zoneName: 'zonename.' }), /zone name must not end with a trailing dot/); + test.done(); + }, + + 'a hosted zone can be assiciated with a VPC either upon creation or using "addVpc"'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc1 = new ec2.VpcNetwork(stack, 'VPC1'); + const vpc2 = new ec2.VpcNetwork(stack, 'VPC2'); + const vpc3 = new ec2.VpcNetwork(stack, 'VPC3'); + + // WHEN + const zone = new HostedZone(stack, 'MyHostedZone', { + zoneName: 'zonename', + vpcs: [ vpc1, vpc2 ] + }); + zone.addVpc(vpc3); + + // THEN + expect(stack).to(haveResource('AWS::Route53::HostedZone', { + VPCs: [ + { + VPCId: { + Ref: "VPC17DE2CF87" + }, + VPCRegion: { + Ref: "AWS::Region" + } + }, + { + VPCId: { + Ref: "VPC2C1F0E711" + }, + VPCRegion: { + Ref: "AWS::Region" + } + }, + { + VPCId: { + Ref: "VPC3CB5FCDA8" + }, + VPCRegion: { + Ref: "AWS::Region" + } + } + ] + })); + test.done(); + }, + + 'public zone cannot be associated with a vpc (runtime error)'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const zone = new PublicHostedZone(stack, 'MyHostedZone', { zoneName: 'zonename' }); + const vpc = new ec2.VpcNetwork(stack, 'VPC'); + + // THEN + test.throws(() => zone.addVpc(vpc), /Cannot associate public hosted zones with a VPC/); test.done(); } }; diff --git a/packages/@aws-cdk/aws-route53/test/test.txt-record.ts b/packages/@aws-cdk/aws-route53/test/test.txt-record.ts index 47222bb6406b7..c2f797a037d4b 100644 --- a/packages/@aws-cdk/aws-route53/test/test.txt-record.ts +++ b/packages/@aws-cdk/aws-route53/test/test.txt-record.ts @@ -1,14 +1,14 @@ import { exactlyMatchTemplate, expect } from '@aws-cdk/assert'; import { App, Stack } from '@aws-cdk/cdk'; import { Test } from 'nodeunit'; -import { PublicHostedZone, TXTRecord } from '../lib'; +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!' }); + new TxtRecord(zone, 'TXT', { zone, recordName: '_foo', recordValue: 'Bar!' }); expect(app.synthesizeTemplate()).to(exactlyMatchTemplate({ Resources: { HostedZoneDB99F866: {