From f974531d41b828bdfb5052e96bba6b660922a991 Mon Sep 17 00:00:00 2001 From: Romain Marcadier-Muller Date: Thu, 28 Feb 2019 09:50:41 +0100 Subject: [PATCH] feat(route53): Convenience API for creating zone delegations (#1853) When creating delegation relationship between two `PublicHostedZone`s, one can now use `zone.addDelegation(otherZone)` instead of manually creating the `ZoneDelegationRecord` insteance. This reduces the risk of passing the incorrect name server, or hosting the record on the wrong end of the relationship (DNS is hard!) Additionally, fixes a bug in which it was not possible to create a zone delegation record using a `IHostedZone.hostedZoneNameServers` property as the array was mapped, which caused the `Fn::GetAtt` stringified list token to become corrupted. Fixes #1847 --- .../@aws-cdk/aws-route53/lib/hosted-zone.ts | 36 +++++++++++++++++++ .../lib/records/zone-delegation.ts | 28 +++++---------- .../test/integ.route53.expected.json | 25 ++++++++++++- .../aws-route53/test/integ.route53.ts | 4 +++ .../@aws-cdk/aws-route53/test/test.route53.ts | 22 +++++++++++- 5 files changed, 94 insertions(+), 21 deletions(-) diff --git a/packages/@aws-cdk/aws-route53/lib/hosted-zone.ts b/packages/@aws-cdk/aws-route53/lib/hosted-zone.ts index 4b6c621845fa5..1cbc9d2d12adb 100644 --- a/packages/@aws-cdk/aws-route53/lib/hosted-zone.ts +++ b/packages/@aws-cdk/aws-route53/lib/hosted-zone.ts @@ -1,6 +1,7 @@ import ec2 = require('@aws-cdk/aws-ec2'); import cdk = require('@aws-cdk/cdk'); import { HostedZoneImportProps, IHostedZone } from './hosted-zone-ref'; +import { ZoneDelegationRecord } from './records'; import { CfnHostedZone } from './route53.generated'; import { validateZoneName } from './util'; @@ -112,6 +113,41 @@ export class PublicHostedZone extends HostedZone { public addVpc(_vpc: ec2.IVpcNetwork) { throw new Error('Cannot associate public hosted zones with a VPC'); } + + /** + * Adds a delegation from this zone to a designated zone. + * + * @param delegate the zone being delegated to. + * @param opts options for creating the DNS record, if any. + */ + public addDelegation(delegate: PublicHostedZone, opts: ZoneDelegationOptions = {}): void { + new ZoneDelegationRecord(this, `${this.zoneName} -> ${delegate.zoneName}`, { + zone: this, + delegatedZoneName: delegate.zoneName, + nameServers: delegate.hostedZoneNameServers!, // PublicHostedZones always have name servers! + comment: opts.comment, + ttl: opts.ttl, + }); + } +} + +/** + * Options available when creating a delegation relationship from one PublicHostedZone to another. + */ +export interface ZoneDelegationOptions { + /** + * A comment to add on the DNS record created to incorporate the delegation. + * + * @default none + */ + comment?: string; + + /** + * The TTL (Time To Live) of the DNS delegation record in DNS caches. + * + * @default 172800 + */ + ttl?: number; } export interface PrivateHostedZoneProps extends CommonHostedZoneProps { diff --git a/packages/@aws-cdk/aws-route53/lib/records/zone-delegation.ts b/packages/@aws-cdk/aws-route53/lib/records/zone-delegation.ts index d29d84dd7dba3..82f3764dc35b2 100644 --- a/packages/@aws-cdk/aws-route53/lib/records/zone-delegation.ts +++ b/packages/@aws-cdk/aws-route53/lib/records/zone-delegation.ts @@ -1,9 +1,10 @@ -import { Construct } from '@aws-cdk/cdk'; +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 { +export interface ZoneDelegationRecordProps extends ZoneDelegationOptions { /** * The zone in which this delegate is defined. */ @@ -17,30 +18,19 @@ export interface ZoneDelegationRecordProps { * The name servers to report in the delegation records. */ nameServers: string[]; - - /** - * The TTL of the zone delegation records. - * - * @default 172800 seconds. - */ - ttl?: number; - - /** - * Any comments that you want to include about the zone delegation records. - * - * @default no comment. - */ - comment?: string; } /** * A record to delegate further lookups to a different set of name servers */ -export class ZoneDelegationRecord extends Construct { - constructor(scope: Construct, id: string, props: ZoneDelegationRecordProps) { +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.unresolved(props.nameServers) + ? props.nameServers // Can't map a string-array token! + : props.nameServers.map(ns => (cdk.unresolved(ns) || ns.endsWith('.')) ? ns : `${ns}.`); new CfnRecordSet(this, 'Resource', { hostedZoneId: props.zone.hostedZoneId, @@ -48,7 +38,7 @@ export class ZoneDelegationRecord extends Construct { type: 'NS', ttl: ttl.toString(), comment: props.comment, - resourceRecords: props.nameServers.map(ns => ns.endsWith('.') ? ns : `${ns}.`) + resourceRecords, }); } } 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 fd1a019f9ae10..9cde321023c0f 100644 --- a/packages/@aws-cdk/aws-route53/test/integ.route53.expected.json +++ b/packages/@aws-cdk/aws-route53/test/integ.route53.expected.json @@ -532,6 +532,29 @@ "Name": "cdk.test." } }, + "PublicZonecdktestsubcdktest83558650": { + "Type": "AWS::Route53::RecordSet", + "Properties": { + "Name": "sub.cdk.test.", + "Type": "NS", + "HostedZoneId": { + "Ref": "PublicZone2E1C4E34" + }, + "ResourceRecords": { + "Fn::GetAtt": [ + "PublicSubZoneDBD26A0A", + "NameServers" + ] + }, + "TTL": "172800" + } + }, + "PublicSubZoneDBD26A0A": { + "Type": "AWS::Route53::HostedZone", + "Properties": { + "Name": "sub.cdk.test." + } + }, "CNAMEC70A2D52": { "Type": "AWS::Route53::RecordSet", "Properties": { @@ -559,4 +582,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 99b204971f5bf..4109fc0b61713 100644 --- a/packages/@aws-cdk/aws-route53/test/integ.route53.ts +++ b/packages/@aws-cdk/aws-route53/test/integ.route53.ts @@ -15,6 +15,10 @@ const privateZone = new PrivateHostedZone(stack, 'PrivateZone', { const publicZone = new PublicHostedZone(stack, 'PublicZone', { zoneName: 'cdk.test' }); +const publicSubZone = new PublicHostedZone(stack, 'PublicSubZone', { + zoneName: 'sub.cdk.test' +}); +publicZone.addDelegation(publicSubZone); new TxtRecord(privateZone, 'TXT', { zone: privateZone, diff --git a/packages/@aws-cdk/aws-route53/test/test.route53.ts b/packages/@aws-cdk/aws-route53/test/test.route53.ts index 27a26cc19ce6e..34ff48aa292c3 100644 --- a/packages/@aws-cdk/aws-route53/test/test.route53.ts +++ b/packages/@aws-cdk/aws-route53/test/test.route53.ts @@ -192,7 +192,27 @@ export = { // THEN test.throws(() => zone.addVpc(vpc), /Cannot associate public hosted zones with a VPC/); test.done(); - } + }, + + 'setting up zone delegation'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const zone = new PublicHostedZone(stack, 'TopZone', { zoneName: 'top.test' }); + const delegate = new PublicHostedZone(stack, 'SubZone', { zoneName: 'sub.top.test' }); + + // WHEN + zone.addDelegation(delegate, { ttl: 1337 }); + + // THEN + expect(stack).to(haveResource('AWS::Route53::RecordSet', { + Type: 'NS', + Name: 'sub.top.test.', + HostedZoneId: zone.node.resolve(zone.hostedZoneId), + ResourceRecords: zone.node.resolve(delegate.hostedZoneNameServers), + TTL: '1337', + })); + test.done(); + }, }; class TestApp {