From 9da5d98ddab8db0e1235984bc936f94d640d1ced Mon Sep 17 00:00:00 2001 From: Ayush Goyal Date: Sat, 10 Apr 2021 12:17:18 +0530 Subject: [PATCH 1/2] feat(route53): add support for parentHostedZoneName for CrossAccountZoneDelegationRecord --- packages/@aws-cdk/aws-route53/README.md | 31 +++++-- .../index.ts | 20 ++++- .../@aws-cdk/aws-route53/lib/hosted-zone.ts | 16 ++++ .../@aws-cdk/aws-route53/lib/record-set.ts | 20 ++++- .../index.test.ts | 69 +++++++++++++++- .../aws-route53/test/hosted-zone.test.ts | 24 ++++++ ...ross-account-zone-delegation.expected.json | 63 +++++++++++--- .../integ.cross-account-zone-delegation.ts | 17 +++- .../aws-route53/test/record-set.test.ts | 82 ++++++++++++++++++- 9 files changed, 315 insertions(+), 27 deletions(-) diff --git a/packages/@aws-cdk/aws-route53/README.md b/packages/@aws-cdk/aws-route53/README.md index 0213665ff2b65..1dedae8faa1fa 100644 --- a/packages/@aws-cdk/aws-route53/README.md +++ b/packages/@aws-cdk/aws-route53/README.md @@ -125,26 +125,45 @@ 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. -To add a NS record to a HostedZone in different account +To add a NS record to a HostedZone in different account you can do the following: + +In the account containing the parent hosted zone: ```ts import * as route53 from '@aws-cdk/aws-route53'; -// In the account containing the HostedZone const parentZone = new route53.PublicHostedZone(this, 'HostedZone', { zoneName: 'someexample.com', - crossAccountZoneDelegationPrincipal: new iam.AccountPrincipal('12345678901') + crossAccountZoneDelegationPrincipal: new iam.AccountPrincipal('12345678901'), + crossAccountZoneDelegationRoleName: 'MyDelegationRole', }); +``` + +In the account containing the child zone to be delegated: + +```ts +import * as iam from '@aws-cdk/aws-iam'; +import * as route53 from '@aws-cdk/aws-route53'; -// In this account const subZone = new route53.PublicHostedZone(this, 'SubZone', { zoneName: 'sub.someexample.com' }); +// import the delegation role by constructing the roleArn +const delegationRoleArn = Stack.of(this).formatArn({ + region: '', // IAM is global in each partition + service: 'iam', + account: 'parent-account-id', + resource: 'role', + resourceName: 'MyDelegationRole', +}); +const delegationRole = iam.Role.fromRoleArn(this, 'DelegationRole', delegationRoleArn); + +// create the record new route53.CrossAccountZoneDelegationRecord(this, 'delegate', { delegatedZone: subZone, - parentHostedZoneId: parentZone.hostedZoneId, - delegationRole: parentZone.crossAccountDelegationRole + parentHostedZoneName: 'someexample.com', // or you can use parentHostedZoneId + delegationRole: delegationRole }); ``` diff --git a/packages/@aws-cdk/aws-route53/lib/cross-account-zone-delegation-handler/index.ts b/packages/@aws-cdk/aws-route53/lib/cross-account-zone-delegation-handler/index.ts index 3c711d283d7e5..3a7e87f402fd1 100644 --- a/packages/@aws-cdk/aws-route53/lib/cross-account-zone-delegation-handler/index.ts +++ b/packages/@aws-cdk/aws-route53/lib/cross-account-zone-delegation-handler/index.ts @@ -3,7 +3,8 @@ import { Credentials, Route53, STS } from 'aws-sdk'; interface ResourceProperties { AssumeRoleArn: string, - ParentZoneId: string, + ParentZoneName?: string, + ParentZoneId?: string, DelegatedZoneName: string, DelegatedZoneNameServers: string[], TTL: number, @@ -22,13 +23,15 @@ export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent } async function cfnEventHandler(props: ResourceProperties, isDeleteEvent: boolean) { - const { AssumeRoleArn, ParentZoneId, DelegatedZoneName, DelegatedZoneNameServers, TTL } = props; + const { AssumeRoleArn, ParentZoneId, ParentZoneName, DelegatedZoneName, DelegatedZoneNameServers, TTL } = props; const credentials = await getCrossAccountCredentials(AssumeRoleArn); const route53 = new Route53({ credentials }); + const parentZoneId = ParentZoneId ?? await getHostedZoneIdByName(ParentZoneName!, route53); + await route53.changeResourceRecordSets({ - HostedZoneId: ParentZoneId, + HostedZoneId: parentZoneId, ChangeBatch: { Changes: [{ Action: isDeleteEvent ? 'DELETE' : 'UPSERT', @@ -64,3 +67,14 @@ async function getCrossAccountCredentials(roleArn: string): Promise sessionToken: assumedCredentials.SessionToken, }); } + +async function getHostedZoneIdByName(name: string, route53: Route53): Promise { + const zones = await route53.listHostedZonesByName({ DNSName: name }).promise(); + const matchedZones = zones.HostedZones.filter(zone => zone.Name === `${name}.`); + + if (matchedZones.length !== 1) { + throw Error(`Expected one hosted zone to match the given name but found ${matchedZones.length}`); + } + + return matchedZones[0].Id; +} diff --git a/packages/@aws-cdk/aws-route53/lib/hosted-zone.ts b/packages/@aws-cdk/aws-route53/lib/hosted-zone.ts index 1ca835cb258d9..e426d619fe044 100644 --- a/packages/@aws-cdk/aws-route53/lib/hosted-zone.ts +++ b/packages/@aws-cdk/aws-route53/lib/hosted-zone.ts @@ -198,6 +198,13 @@ export interface PublicHostedZoneProps extends CommonHostedZoneProps { * @default - No delegation configuration */ readonly crossAccountZoneDelegationPrincipal?: iam.IPrincipal; + + /** + * The name of the role created for cross account delegation + * + * @default - A role name is generated automatically + */ + readonly crossAccountZoneDelegationRoleName?: string; } /** @@ -244,8 +251,13 @@ export class PublicHostedZone extends HostedZone implements IPublicHostedZone { }); } + if (!props.crossAccountZoneDelegationPrincipal && props.crossAccountZoneDelegationRoleName) { + throw Error('crossAccountZoneDelegationRoleName property is not supported without crossAccountZoneDelegationPrincipal'); + } + if (props.crossAccountZoneDelegationPrincipal) { this.crossAccountZoneDelegationRole = new iam.Role(this, 'CrossAccountZoneDelegationRole', { + roleName: props.crossAccountZoneDelegationRoleName, assumedBy: props.crossAccountZoneDelegationPrincipal, inlinePolicies: { delegation: new iam.PolicyDocument({ @@ -254,6 +266,10 @@ export class PublicHostedZone extends HostedZone implements IPublicHostedZone { actions: ['route53:ChangeResourceRecordSets'], resources: [this.hostedZoneArn], }), + new iam.PolicyStatement({ + actions: ['route53:ListHostedZonesByName'], + resources: ['*'], + }), ], }), }, diff --git a/packages/@aws-cdk/aws-route53/lib/record-set.ts b/packages/@aws-cdk/aws-route53/lib/record-set.ts index b880100ea60a4..57f93931c2097 100644 --- a/packages/@aws-cdk/aws-route53/lib/record-set.ts +++ b/packages/@aws-cdk/aws-route53/lib/record-set.ts @@ -602,10 +602,19 @@ export interface CrossAccountZoneDelegationRecordProps { */ readonly delegatedZone: IHostedZone; + /** + * The hosted zone name in the parent account + * + * @default - no zone name + */ + readonly parentHostedZoneName?: string; + /** * The hosted zone id in the parent account + * + * @default - no zone id */ - readonly parentHostedZoneId: string; + readonly parentHostedZoneId?: string; /** * The delegation role in the parent account @@ -627,6 +636,14 @@ export class CrossAccountZoneDelegationRecord extends CoreConstruct { constructor(scope: Construct, id: string, props: CrossAccountZoneDelegationRecordProps) { super(scope, id); + if (!props.parentHostedZoneName && !props.parentHostedZoneId) { + throw Error('At least one of parentHostedZoneName or parentHostedZoneId is required'); + } + + if (props.parentHostedZoneName && props.parentHostedZoneId) { + throw Error('Only one of parentHostedZoneName and parentHostedZoneId is supported'); + } + const serviceToken = CustomResourceProvider.getOrCreate(this, CROSS_ACCOUNT_ZONE_DELEGATION_RESOURCE_TYPE, { codeDirectory: path.join(__dirname, 'cross-account-zone-delegation-handler'), runtime: CustomResourceProviderRuntime.NODEJS_12_X, @@ -638,6 +655,7 @@ export class CrossAccountZoneDelegationRecord extends CoreConstruct { serviceToken, properties: { AssumeRoleArn: props.delegationRole.roleArn, + ParentZoneName: props.parentHostedZoneName, ParentZoneId: props.parentHostedZoneId, DelegatedZoneName: props.delegatedZone.zoneName, DelegatedZoneNameServers: props.delegatedZone.hostedZoneNameServers!, diff --git a/packages/@aws-cdk/aws-route53/test/cross-account-zone-delegation-handler/index.test.ts b/packages/@aws-cdk/aws-route53/test/cross-account-zone-delegation-handler/index.test.ts index 8c8dcafcbd9c7..47cd460bef8a3 100644 --- a/packages/@aws-cdk/aws-route53/test/cross-account-zone-delegation-handler/index.test.ts +++ b/packages/@aws-cdk/aws-route53/test/cross-account-zone-delegation-handler/index.test.ts @@ -6,6 +6,7 @@ const mockStsClient = { }; const mockRoute53Client = { changeResourceRecordSets: jest.fn().mockReturnThis(), + listHostedZonesByName: jest.fn().mockReturnThis(), promise: jest.fn(), }; @@ -97,7 +98,72 @@ test('calls create resouce record set with DELETE for Delete event', async () => }); }); -function getCfnEvent(event?: Partial): Partial { +test('calls listHostedZonesByName to get zoneId if ParentZoneId is not provided', async () => { + // GIVEN + const parentZoneName = 'some.zone'; + const parentZoneId = 'zone-id'; + + mockStsClient.promise.mockResolvedValueOnce({ Credentials: { AccessKeyId: 'K', SecretAccessKey: 'S', SessionToken: 'T' } }); + mockRoute53Client.promise.mockResolvedValueOnce({ HostedZones: [{ Name: `${parentZoneName}.`, Id: parentZoneId }] }); + mockRoute53Client.promise.mockResolvedValueOnce({}); + + // WHEN + const event = getCfnEvent({}, { + ParentZoneId: undefined, + ParentZoneName: parentZoneName, + }); + await invokeHandler(event); + + // THEN + expect(mockRoute53Client.listHostedZonesByName).toHaveBeenCalledTimes(1); + expect(mockRoute53Client.listHostedZonesByName).toHaveBeenCalledWith({ DNSName: parentZoneName }); + + expect(mockRoute53Client.changeResourceRecordSets).toHaveBeenCalledTimes(1); + expect(mockRoute53Client.changeResourceRecordSets).toHaveBeenCalledWith({ + HostedZoneId: parentZoneId, + ChangeBatch: { + Changes: [{ + Action: 'UPSERT', + ResourceRecordSet: { + Name: 'recordName', + Type: 'NS', + TTL: 172800, + ResourceRecords: [{ Value: 'one' }, { Value: 'two' }], + }, + }], + }, + }); +}); + +test('throws if more than one HostedZones are returnd for the provided ParentHostedZone', async () => { + // GIVEN + const parentZoneName = 'some.zone'; + const parentZoneId = 'zone-id'; + + mockStsClient.promise.mockResolvedValueOnce({ Credentials: { AccessKeyId: 'K', SecretAccessKey: 'S', SessionToken: 'T' } }); + mockRoute53Client.promise.mockResolvedValueOnce({ + HostedZones: [ + { Name: `${parentZoneName}.`, Id: parentZoneId }, + { Name: `${parentZoneName}.`, Id: parentZoneId }, + ], + }); + + // WHEN + const event = getCfnEvent({}, { + ParentZoneId: undefined, + ParentZoneName: parentZoneName, + }); + + // THEN + await expect(invokeHandler(event)).rejects.toThrow(/Expected one hosted zone to match the given name but found 2/); + expect(mockRoute53Client.listHostedZonesByName).toHaveBeenCalledTimes(1); + expect(mockRoute53Client.listHostedZonesByName).toHaveBeenCalledWith({ DNSName: parentZoneName }); +}); + +function getCfnEvent( + event?: Partial, + resourceProps?: any, +): Partial { return { RequestType: 'Create', ResourceProperties: { @@ -107,6 +173,7 @@ function getCfnEvent(event?: Partial { + new PublicHostedZone(stack, 'HostedZone', { + zoneName: 'testZone', + crossAccountZoneDelegationRoleName: 'myrole', + }); + }, /crossAccountZoneDelegationRoleName property is not supported without crossAccountZoneDelegationPrincipal/); + + test.done(); + }, }); diff --git a/packages/@aws-cdk/aws-route53/test/integ.cross-account-zone-delegation.expected.json b/packages/@aws-cdk/aws-route53/test/integ.cross-account-zone-delegation.expected.json index 919a54f8b5051..e615527b1db4e 100644 --- a/packages/@aws-cdk/aws-route53/test/integ.cross-account-zone-delegation.expected.json +++ b/packages/@aws-cdk/aws-route53/test/integ.cross-account-zone-delegation.expected.json @@ -58,6 +58,11 @@ ] ] } + }, + { + "Action": "route53:ListHostedZonesByName", + "Effect": "Allow", + "Resource": "*" } ], "Version": "2012-10-17" @@ -67,13 +72,13 @@ ] } }, - "ChildHostedZone4B14AC71": { + "ChildHostedZoneWithZoneId729259E6": { "Type": "AWS::Route53::HostedZone", "Properties": { "Name": "sub.myzone.com." } }, - "DelegationCrossAccountZoneDelegationCustomResourceFADC27F0": { + "DelegationWithZoneIdCrossAccountZoneDelegationCustomResourceFFD766E7": { "Type": "Custom::CrossAccountZoneDelegation", "Properties": { "ServiceToken": { @@ -94,7 +99,7 @@ "DelegatedZoneName": "sub.myzone.com", "DelegatedZoneNameServers": { "Fn::GetAtt": [ - "ChildHostedZone4B14AC71", + "ChildHostedZoneWithZoneId729259E6", "NameServers" ] }, @@ -150,7 +155,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters3c971020239d152fff59dc3bdbabbbc6d3d9140574e45fd4eb7313ced117113aS3Bucket8B462894" + "Ref": "AssetParameters6f9976bda7bdc2299a222bcc607302200193ba5e1b77c22cc2bdca44414774acS3Bucket2194A765" }, "S3Key": { "Fn::Join": [ @@ -163,7 +168,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters3c971020239d152fff59dc3bdbabbbc6d3d9140574e45fd4eb7313ced117113aS3VersionKeyFDEC5E1D" + "Ref": "AssetParameters6f9976bda7bdc2299a222bcc607302200193ba5e1b77c22cc2bdca44414774acS3VersionKey860C328E" } ] } @@ -176,7 +181,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters3c971020239d152fff59dc3bdbabbbc6d3d9140574e45fd4eb7313ced117113aS3VersionKeyFDEC5E1D" + "Ref": "AssetParameters6f9976bda7bdc2299a222bcc607302200193ba5e1b77c22cc2bdca44414774acS3VersionKey860C328E" } ] } @@ -200,20 +205,54 @@ "DependsOn": [ "CustomCrossAccountZoneDelegationCustomResourceProviderRoleED64687B" ] + }, + "ChildHostedZoneWithZoneNameBC2C15F6": { + "Type": "AWS::Route53::HostedZone", + "Properties": { + "Name": "anothersub.myzone.com." + } + }, + "DelegationWithZoneNameCrossAccountZoneDelegationCustomResourceA1A1C94A": { + "Type": "Custom::CrossAccountZoneDelegation", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "CustomCrossAccountZoneDelegationCustomResourceProviderHandler44A84265", + "Arn" + ] + }, + "AssumeRoleArn": { + "Fn::GetAtt": [ + "ParentHostedZoneCrossAccountZoneDelegationRole95B1C36E", + "Arn" + ] + }, + "ParentZoneName": "myzone.com", + "DelegatedZoneName": "anothersub.myzone.com", + "DelegatedZoneNameServers": { + "Fn::GetAtt": [ + "ChildHostedZoneWithZoneNameBC2C15F6", + "NameServers" + ] + }, + "TTL": 172800 + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" } }, "Parameters": { - "AssetParameters3c971020239d152fff59dc3bdbabbbc6d3d9140574e45fd4eb7313ced117113aS3Bucket8B462894": { + "AssetParameters6f9976bda7bdc2299a222bcc607302200193ba5e1b77c22cc2bdca44414774acS3Bucket2194A765": { "Type": "String", - "Description": "S3 bucket for asset \"3c971020239d152fff59dc3bdbabbbc6d3d9140574e45fd4eb7313ced117113a\"" + "Description": "S3 bucket for asset \"6f9976bda7bdc2299a222bcc607302200193ba5e1b77c22cc2bdca44414774ac\"" }, - "AssetParameters3c971020239d152fff59dc3bdbabbbc6d3d9140574e45fd4eb7313ced117113aS3VersionKeyFDEC5E1D": { + "AssetParameters6f9976bda7bdc2299a222bcc607302200193ba5e1b77c22cc2bdca44414774acS3VersionKey860C328E": { "Type": "String", - "Description": "S3 key for asset version \"3c971020239d152fff59dc3bdbabbbc6d3d9140574e45fd4eb7313ced117113a\"" + "Description": "S3 key for asset version \"6f9976bda7bdc2299a222bcc607302200193ba5e1b77c22cc2bdca44414774ac\"" }, - "AssetParameters3c971020239d152fff59dc3bdbabbbc6d3d9140574e45fd4eb7313ced117113aArtifactHash4F367D8C": { + "AssetParameters6f9976bda7bdc2299a222bcc607302200193ba5e1b77c22cc2bdca44414774acArtifactHash2F5ADA6C": { "Type": "String", - "Description": "Artifact hash for asset \"3c971020239d152fff59dc3bdbabbbc6d3d9140574e45fd4eb7313ced117113a\"" + "Description": "Artifact hash for asset \"6f9976bda7bdc2299a222bcc607302200193ba5e1b77c22cc2bdca44414774ac\"" } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-route53/test/integ.cross-account-zone-delegation.ts b/packages/@aws-cdk/aws-route53/test/integ.cross-account-zone-delegation.ts index 75f9e86152eb0..8ec89ab3ec343 100644 --- a/packages/@aws-cdk/aws-route53/test/integ.cross-account-zone-delegation.ts +++ b/packages/@aws-cdk/aws-route53/test/integ.cross-account-zone-delegation.ts @@ -11,13 +11,24 @@ const parentZone = new PublicHostedZone(stack, 'ParentHostedZone', { crossAccountZoneDelegationPrincipal: new iam.AccountPrincipal(cdk.Aws.ACCOUNT_ID), }); -const childZone = new PublicHostedZone(stack, 'ChildHostedZone', { +// with zoneId +const childZoneWithZoneId = new PublicHostedZone(stack, 'ChildHostedZoneWithZoneId', { zoneName: 'sub.myzone.com', }); -new CrossAccountZoneDelegationRecord(stack, 'Delegation', { - delegatedZone: childZone, +new CrossAccountZoneDelegationRecord(stack, 'DelegationWithZoneId', { + delegatedZone: childZoneWithZoneId, parentHostedZoneId: parentZone.hostedZoneId, delegationRole: parentZone.crossAccountZoneDelegationRole!, }); +// with zoneName +const childZoneWithZoneName = new PublicHostedZone(stack, 'ChildHostedZoneWithZoneName', { + zoneName: 'anothersub.myzone.com', +}); +new CrossAccountZoneDelegationRecord(stack, 'DelegationWithZoneName', { + delegatedZone: childZoneWithZoneName, + parentHostedZoneName: 'myzone.com', + delegationRole: parentZone.crossAccountZoneDelegationRole!, +}); + app.synth(); diff --git a/packages/@aws-cdk/aws-route53/test/record-set.test.ts b/packages/@aws-cdk/aws-route53/test/record-set.test.ts index 136f88a27d426..a9d5446185f66 100644 --- a/packages/@aws-cdk/aws-route53/test/record-set.test.ts +++ b/packages/@aws-cdk/aws-route53/test/record-set.test.ts @@ -570,7 +570,7 @@ nodeunitShim({ test.done(); }, - 'Cross account zone delegation record'(test: Test) { + 'Cross account zone delegation record with parentHostedZoneId'(test: Test) { // GIVEN const stack = new Stack(); const parentZone = new route53.PublicHostedZone(stack, 'ParentHostedZone', { @@ -617,4 +617,84 @@ nodeunitShim({ })); test.done(); }, + + 'Cross account zone delegation record with parentHostedZoneName'(test: Test) { + // GIVEN + const stack = new Stack(); + const parentZone = new route53.PublicHostedZone(stack, 'ParentHostedZone', { + zoneName: 'myzone.com', + crossAccountZoneDelegationPrincipal: new iam.AccountPrincipal('123456789012'), + }); + + // WHEN + const childZone = new route53.PublicHostedZone(stack, 'ChildHostedZone', { + zoneName: 'sub.myzone.com', + }); + new route53.CrossAccountZoneDelegationRecord(stack, 'Delegation', { + delegatedZone: childZone, + parentHostedZoneName: 'myzone.com', + delegationRole: parentZone.crossAccountZoneDelegationRole!, + ttl: Duration.seconds(60), + }); + + // THEN + expect(stack).to(haveResource('Custom::CrossAccountZoneDelegation', { + ServiceToken: { + 'Fn::GetAtt': [ + 'CustomCrossAccountZoneDelegationCustomResourceProviderHandler44A84265', + 'Arn', + ], + }, + AssumeRoleArn: { + 'Fn::GetAtt': [ + 'ParentHostedZoneCrossAccountZoneDelegationRole95B1C36E', + 'Arn', + ], + }, + ParentZoneName: 'myzone.com', + DelegatedZoneName: 'sub.myzone.com', + DelegatedZoneNameServers: { + 'Fn::GetAtt': [ + 'ChildHostedZone4B14AC71', + 'NameServers', + ], + }, + TTL: 60, + })); + test.done(); + }, + + 'Cross account zone delegation record throws when parent id and name both/nither are supplied'(test: Test) { + // GIVEN + const stack = new Stack(); + const parentZone = new route53.PublicHostedZone(stack, 'ParentHostedZone', { + zoneName: 'myzone.com', + crossAccountZoneDelegationPrincipal: new iam.AccountPrincipal('123456789012'), + }); + + // THEN + const childZone = new route53.PublicHostedZone(stack, 'ChildHostedZone', { + zoneName: 'sub.myzone.com', + }); + + test.throws(() => { + new route53.CrossAccountZoneDelegationRecord(stack, 'Delegation1', { + delegatedZone: childZone, + delegationRole: parentZone.crossAccountZoneDelegationRole!, + ttl: Duration.seconds(60), + }); + }, /At least one of parentHostedZoneName or parentHostedZoneId is required/); + + test.throws(() => { + new route53.CrossAccountZoneDelegationRecord(stack, 'Delegation2', { + delegatedZone: childZone, + parentHostedZoneId: parentZone.hostedZoneId, + parentHostedZoneName: parentZone.zoneName, + delegationRole: parentZone.crossAccountZoneDelegationRole!, + ttl: Duration.seconds(60), + }); + }, /Only one of parentHostedZoneName and parentHostedZoneId is supported/); + + test.done(); + }, }); From d9d0ce117a6e644c2d543bdadbcfaf70a36e0fa1 Mon Sep 17 00:00:00 2001 From: Ayush Goyal Date: Tue, 27 Apr 2021 15:40:16 +0530 Subject: [PATCH 2/2] add a check --- packages/@aws-cdk/aws-route53/README.md | 2 +- .../index.ts | 4 ++++ .../index.test.ts | 16 ++++++++++++++-- ...cross-account-zone-delegation.expected.json | 18 +++++++++--------- 4 files changed, 28 insertions(+), 12 deletions(-) diff --git a/packages/@aws-cdk/aws-route53/README.md b/packages/@aws-cdk/aws-route53/README.md index 1dedae8faa1fa..1c4affa83e3a0 100644 --- a/packages/@aws-cdk/aws-route53/README.md +++ b/packages/@aws-cdk/aws-route53/README.md @@ -163,7 +163,7 @@ const delegationRole = iam.Role.fromRoleArn(this, 'DelegationRole', delegationRo new route53.CrossAccountZoneDelegationRecord(this, 'delegate', { delegatedZone: subZone, parentHostedZoneName: 'someexample.com', // or you can use parentHostedZoneId - delegationRole: delegationRole + delegationRole, }); ``` diff --git a/packages/@aws-cdk/aws-route53/lib/cross-account-zone-delegation-handler/index.ts b/packages/@aws-cdk/aws-route53/lib/cross-account-zone-delegation-handler/index.ts index 3a7e87f402fd1..62c3e957d1ff4 100644 --- a/packages/@aws-cdk/aws-route53/lib/cross-account-zone-delegation-handler/index.ts +++ b/packages/@aws-cdk/aws-route53/lib/cross-account-zone-delegation-handler/index.ts @@ -25,6 +25,10 @@ export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent async function cfnEventHandler(props: ResourceProperties, isDeleteEvent: boolean) { const { AssumeRoleArn, ParentZoneId, ParentZoneName, DelegatedZoneName, DelegatedZoneNameServers, TTL } = props; + if (!ParentZoneId && !ParentZoneName) { + throw Error('One of ParentZoneId or ParentZoneName must be specified'); + } + const credentials = await getCrossAccountCredentials(AssumeRoleArn); const route53 = new Route53({ credentials }); diff --git a/packages/@aws-cdk/aws-route53/test/cross-account-zone-delegation-handler/index.test.ts b/packages/@aws-cdk/aws-route53/test/cross-account-zone-delegation-handler/index.test.ts index 47cd460bef8a3..04f7a54b5f1a1 100644 --- a/packages/@aws-cdk/aws-route53/test/cross-account-zone-delegation-handler/index.test.ts +++ b/packages/@aws-cdk/aws-route53/test/cross-account-zone-delegation-handler/index.test.ts @@ -21,12 +21,24 @@ jest.mock('aws-sdk', () => { beforeEach(() => { mockStsClient.assumeRole.mockReturnThis(); mockRoute53Client.changeResourceRecordSets.mockReturnThis(); + mockRoute53Client.listHostedZonesByName.mockReturnThis(); }); afterEach(() => { jest.clearAllMocks(); }); +test('throws error if both ParentZoneId and ParentZoneName are not provided', async () => { + // WHEN + const event = getCfnEvent({}, { + ParentZoneId: undefined, + ParentZoneName: undefined, + }); + + // THEN + await expect(invokeHandler(event)).rejects.toThrow(/One of ParentZoneId or ParentZoneName must be specified/); +}); + test('throws error if getting credentials fails', async () => { // GIVEN mockStsClient.promise.mockResolvedValueOnce({ Credentials: undefined }); @@ -44,7 +56,7 @@ test('throws error if getting credentials fails', async () => { }); }); -test('calls create resouce record set with Upsert for Create event', async () => { +test('calls create resource record set with Upsert for Create event', async () => { // GIVEN mockStsClient.promise.mockResolvedValueOnce({ Credentials: { AccessKeyId: 'K', SecretAccessKey: 'S', SessionToken: 'T' } }); mockRoute53Client.promise.mockResolvedValueOnce({}); @@ -71,7 +83,7 @@ test('calls create resouce record set with Upsert for Create event', async () => }); }); -test('calls create resouce record set with DELETE for Delete event', async () => { +test('calls create resource record set with DELETE for Delete event', async () => { // GIVEN mockStsClient.promise.mockResolvedValueOnce({ Credentials: { AccessKeyId: 'K', SecretAccessKey: 'S', SessionToken: 'T' } }); mockRoute53Client.promise.mockResolvedValueOnce({}); diff --git a/packages/@aws-cdk/aws-route53/test/integ.cross-account-zone-delegation.expected.json b/packages/@aws-cdk/aws-route53/test/integ.cross-account-zone-delegation.expected.json index e615527b1db4e..281fc984d0756 100644 --- a/packages/@aws-cdk/aws-route53/test/integ.cross-account-zone-delegation.expected.json +++ b/packages/@aws-cdk/aws-route53/test/integ.cross-account-zone-delegation.expected.json @@ -155,7 +155,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters6f9976bda7bdc2299a222bcc607302200193ba5e1b77c22cc2bdca44414774acS3Bucket2194A765" + "Ref": "AssetParametersd17df4f90e07a972e8f7b00dddbae8e3eba45a212226d2b714dcd28dded69602S3Bucket200D9216" }, "S3Key": { "Fn::Join": [ @@ -168,7 +168,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters6f9976bda7bdc2299a222bcc607302200193ba5e1b77c22cc2bdca44414774acS3VersionKey860C328E" + "Ref": "AssetParametersd17df4f90e07a972e8f7b00dddbae8e3eba45a212226d2b714dcd28dded69602S3VersionKey0E5C26F0" } ] } @@ -181,7 +181,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters6f9976bda7bdc2299a222bcc607302200193ba5e1b77c22cc2bdca44414774acS3VersionKey860C328E" + "Ref": "AssetParametersd17df4f90e07a972e8f7b00dddbae8e3eba45a212226d2b714dcd28dded69602S3VersionKey0E5C26F0" } ] } @@ -242,17 +242,17 @@ } }, "Parameters": { - "AssetParameters6f9976bda7bdc2299a222bcc607302200193ba5e1b77c22cc2bdca44414774acS3Bucket2194A765": { + "AssetParametersd17df4f90e07a972e8f7b00dddbae8e3eba45a212226d2b714dcd28dded69602S3Bucket200D9216": { "Type": "String", - "Description": "S3 bucket for asset \"6f9976bda7bdc2299a222bcc607302200193ba5e1b77c22cc2bdca44414774ac\"" + "Description": "S3 bucket for asset \"d17df4f90e07a972e8f7b00dddbae8e3eba45a212226d2b714dcd28dded69602\"" }, - "AssetParameters6f9976bda7bdc2299a222bcc607302200193ba5e1b77c22cc2bdca44414774acS3VersionKey860C328E": { + "AssetParametersd17df4f90e07a972e8f7b00dddbae8e3eba45a212226d2b714dcd28dded69602S3VersionKey0E5C26F0": { "Type": "String", - "Description": "S3 key for asset version \"6f9976bda7bdc2299a222bcc607302200193ba5e1b77c22cc2bdca44414774ac\"" + "Description": "S3 key for asset version \"d17df4f90e07a972e8f7b00dddbae8e3eba45a212226d2b714dcd28dded69602\"" }, - "AssetParameters6f9976bda7bdc2299a222bcc607302200193ba5e1b77c22cc2bdca44414774acArtifactHash2F5ADA6C": { + "AssetParametersd17df4f90e07a972e8f7b00dddbae8e3eba45a212226d2b714dcd28dded69602ArtifactHash37FB4D0C": { "Type": "String", - "Description": "Artifact hash for asset \"6f9976bda7bdc2299a222bcc607302200193ba5e1b77c22cc2bdca44414774ac\"" + "Description": "Artifact hash for asset \"d17df4f90e07a972e8f7b00dddbae8e3eba45a212226d2b714dcd28dded69602\"" } } } \ No newline at end of file