Skip to content

Commit

Permalink
Add support for Route53 records (#204)
Browse files Browse the repository at this point in the history
This PR adds support for Route53 records (`AWS::Route53::RecordSet`)
which is not currently supported by CCAPI.

I've also added integration tests for the Route53 constructs.

re #183, closes #177
  • Loading branch information
corymhall authored Nov 8, 2024
1 parent 8263027 commit 129e930
Show file tree
Hide file tree
Showing 8 changed files with 313 additions and 4 deletions.
14 changes: 14 additions & 0 deletions integration/examples_nodejs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,20 @@ func TestEc2(t *testing.T) {
integration.ProgramTest(t, &test)
}

func TestRoute53(t *testing.T) {
test := getJSBaseOptions(t).
With(integration.ProgramTestOptions{
Dir: filepath.Join(getCwd(t), "route53"),
Config: map[string]string{
// This test has to be run in us-east-1 for DNSSEC
"aws:region": "us-east-1",
"aws-native:region": "us-east-1",
},
})

integration.ProgramTest(t, &test)
}

func TestKms(t *testing.T) {
test := getJSBaseOptions(t).
With(integration.ProgramTestOptions{
Expand Down
3 changes: 3 additions & 0 deletions integration/route53/Pulumi.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
name: pulumi-aws-route53
runtime: nodejs
description: route53 integration test
84 changes: 84 additions & 0 deletions integration/route53/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as route53 from 'aws-cdk-lib/aws-route53';
import * as pulumicdk from '@pulumi/cdk';
import { Duration } from 'aws-cdk-lib/core';
import { aws_elasticloadbalancingv2, aws_kms, aws_route53_targets } from 'aws-cdk-lib';

class Route53Stack extends pulumicdk.Stack {
constructor(app: pulumicdk.App, id: string, options?: pulumicdk.StackOptions) {
super(app, id, options);
const kmsKey = new aws_kms.Key(this, 'Key', {
keySpec: aws_kms.KeySpec.ECC_NIST_P256,
keyUsage: aws_kms.KeyUsage.SIGN_VERIFY,
pendingWindow: Duration.days(7),
});
const zone = new route53.HostedZone(this, 'HostedZone', {
zoneName: 'pulumi-cdk.com',
});
zone.enableDnssec({
kmsKey,
});

new route53.TxtRecord(this, 'TxtRecord', {
zone,
values: ['somevalue'],
recordName: 'cdk-txt',
});

new route53.TxtRecord(this, 'TxtRecord2', {
zone,
values: ['hello'.repeat(52)],
recordName: 'cdk-txt-2',
});

new route53.CnameRecord(this, 'Cname', {
zone,
recordName: 'cdk-cname',
domainName: 'pulumi.com',
ttl: Duration.minutes(1),
});

new route53.ARecord(this, 'ARecord', {
zone,
recordName: 'cdk-a',
target: route53.RecordTarget.fromIpAddresses('1.2.3.4'),
geoLocation: route53.GeoLocation.continent(route53.Continent.NORTH_AMERICA),
});

const vpc = new ec2.Vpc(this, 'Vpc', {
maxAzs: 2,
ipAddresses: ec2.IpAddresses.cidr('10.0.0.0/16'),
natGateways: 0,
subnetConfiguration: [
{
name: 'Isolated',
subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
},
],
});
const privateZone = new route53.PrivateHostedZone(this, 'PrivateHostedZone', {
zoneName: 'pulumi-cdk-private.com',
vpc,
});
const nlb = new aws_elasticloadbalancingv2.NetworkLoadBalancer(this, 'NLB', {
vpc,
vpcSubnets: vpc.selectSubnets({ subnetType: ec2.SubnetType.PRIVATE_ISOLATED }),
});
new route53.AaaaRecord(this, 'AaaaRecord1', {
recordName: 'nlb',
zone: privateZone,
target: route53.RecordTarget.fromAlias(new aws_route53_targets.LoadBalancerTarget(nlb)),
weight: 1,
});
new route53.AaaaRecord(this, 'AaaaRecord2', {
recordName: 'nlb',
zone: privateZone,
target: route53.RecordTarget.fromAlias(new aws_route53_targets.LoadBalancerTarget(nlb)),
weight: 2,
});
}
}

new pulumicdk.App('app', (scope: pulumicdk.App) => {
new Route53Stack(scope, 'teststack');
});
15 changes: 15 additions & 0 deletions integration/route53/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "pulumi-aws-cdk",
"devDependencies": {
"@types/node": "^10.0.0"
},
"dependencies": {
"@pulumi/aws": "^6.0.0",
"@pulumi/aws-native": "^1.5.0",
"@pulumi/cdk": "^0.5.0",
"@pulumi/pulumi": "^3.0.0",
"aws-cdk-lib": "2.149.0",
"constructs": "10.3.0",
"esbuild": "^0.24.0"
}
}
18 changes: 18 additions & 0 deletions integration/route53/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"compilerOptions": {
"strict": true,
"outDir": "bin",
"target": "es2019",
"module": "commonjs",
"moduleResolution": "node",
"sourceMap": true,
"experimentalDecorators": true,
"pretty": true,
"noFallthroughCasesInSwitch": true,
"noImplicitReturns": true,
"forceConsistentCasingInFileNames": true
},
"include": [
"./*.ts"
]
}
59 changes: 59 additions & 0 deletions src/aws-resource-mappings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,65 @@ export function mapToAwsResource(
return resources;
}

case 'AWS::Route53::RecordSet': {
let records: string[] = props.resourceRecords;
if (props.type === 'TXT') {
// CDK has special handling for TXT records that conflicts with the Terraform provider's handling.
// 1. CDK wraps the value in double quotes, which Terraform does as well. We need to remove the quotes that
// CDK adds otherwise we get double quotes
// 2. CDK splits the value into multiple records if it exceeds 255 characters. Terraform does not do this so we need to.
//
// (user) (cdk) (terraform)
// e.g. "hello...hello" => '"hello...""hello"' ["hello...", "hello"]
records = records.flatMap((r) => r.split('""').flatMap((record) => record.replace(/"/g, '')));
}
return new aws.route53.Record(
logicalId,
{
zoneId: props.hostedZoneId,
name: props.name,
type: props.type,
records,
ttl: props.ttl,
aliases: props.aliasTarget
? [
{
name: props.aliasTarget.dnsName,
zoneId: props.aliasTarget.hostedZoneId,
evaluateTargetHealth: props.aliasTarget.evaluateTargetHealth ?? false,
},
]
: undefined,
healthCheckId: props.healthCheckId,
setIdentifier: props.setIdentifier,
cidrRoutingPolicy: props.cidrRoutingConfig,
failoverRoutingPolicies: props.failover ? [{ type: props.failover }] : undefined,
weightedRoutingPolicies: props.weight ? [{ weight: props.weight }] : undefined,
geoproximityRoutingPolicy: props.geoProximityLocation
? {
bias: props.geoProximityLocation.bias,
awsRegion: props.geoProximityLocation.awsRegion,
localZoneGroup: props.geoProximityLocation.localZoneGroup,
coordinates: props.geoProximityLocation.coordinates
? [props.geoProximityLocation.coordinates]
: undefined,
}
: undefined,
geolocationRoutingPolicies: props.geoLocation
? [
{
country: props.geoLocation.countryCode,
continent: props.geoLocation.continentCode,
subdivision: props.geoLocation.subdivisionCode,
},
]
: undefined,
multivalueAnswerRoutingPolicy: props.multiValueAnswer,
},
options,
);
}

default:
return undefined;
}
Expand Down
98 changes: 97 additions & 1 deletion tests/aws-resource-mappings.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { CfnResource, Stack } from 'aws-cdk-lib/core';
import { mapToAwsResource } from '../src/aws-resource-mappings';
import * as route53 from 'aws-cdk-lib/aws-route53';
import * as aws from '@pulumi/aws';

jest.mock('@pulumi/aws', () => {
Expand Down Expand Up @@ -33,6 +33,11 @@ jest.mock('@pulumi/aws', () => {
return {};
}),
},
route53: {
Record: jest.fn().mockImplementation(() => {
return {};
}),
},
};
});

Expand Down Expand Up @@ -270,4 +275,95 @@ describe('AWS Resource Mappings', () => {
{},
);
});

test('maps route53.Record', () => {
// GIVEN
const cfnType = 'AWS::Route53::RecordSet';
const logicalId = 'my-resource';
const cfnProps = {
HostedZoneId: 'zone-id',
Name: 'example.com',
Type: 'A',
TTL: 900,
ResourceRecords: ['192.0.2.99'],
AliasTarget: {
DNSName: 'example.com',
HostedZoneId: 'zone-id',
EvaluateTargetHealth: true,
},
HealthCheckId: 'health-check-id',
SetIdentifier: 'set-identifier',
CidrRoutingConfig: {
CollectionId: 'collection-id',
LocationName: 'location-name',
},
Failover: 'PRIMARY',
Weight: 1,
GeoProximityLocation: {
Bias: 'bias',
AWSRegion: 'region',
LocalZoneGroup: 'group',
Coordinates: {
Latitude: 0,
Longitude: 0,
},
},
GeoLocation: {
ContinentCode: 'code',
CountryCode: 'code',
SubdivisionCode: 'code',
},
MultiValueAnswer: true,
};

// WHEN
mapToAwsResource(logicalId, cfnType, cfnProps, {});

// THEN
expect(aws.route53.Record).toHaveBeenCalledWith(
logicalId,
expect.objectContaining({
zoneId: 'zone-id',
name: 'example.com',
type: 'A',
records: ['192.0.2.99'],
ttl: 900,
aliases: [
{
name: 'example.com',
zoneId: 'zone-id',
evaluateTargetHealth: true,
},
],
healthCheckId: 'health-check-id',
setIdentifier: 'set-identifier',
cidrRoutingPolicy: {
collectionId: 'collection-id',
locationName: 'location-name',
},
failoverRoutingPolicies: [{ type: 'PRIMARY' }],
weightedRoutingPolicies: [{ weight: 1 }],
geoproximityRoutingPolicy: {
bias: 'bias',
awsRegion: 'region',
localZoneGroup: 'group',
coordinates: [
{
latitude: 0,
longitude: 0,
},
],
},
geolocationRoutingPolicies: [
{
continent: 'code',
country: 'code',
subdivision: 'code',
},
],
multivalueAnswerRoutingPolicy: true,
}),
{},
);
});
});
26 changes: 23 additions & 3 deletions tests/cdk-resource.test.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
import * as route53 from 'aws-cdk-lib/aws-route53';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import { TableArgs } from '@pulumi/aws-native/dynamodb';
import { Key } from 'aws-cdk-lib/aws-kms';
import { setMocks, testApp } from './mocks';
import { MockResourceArgs } from '@pulumi/pulumi/runtime';
import { Construct } from 'constructs';

let resources: MockResourceArgs[] = [];
beforeAll(() => {
resources = [];
setMocks(resources);
});

describe('CDK Construct tests', () => {
// DynamoDB table was previously mapped to the `aws` provider
// otherwise this level of testing wouldn't be necessary.
// We also don't need to do this type of testing for _every_ resource
test('dynamodb table', async () => {
const resources: MockResourceArgs[] = [];
setMocks(resources);

await testApp((scope: Construct) => {
const key = Key.fromKeyArn(scope, 'key', 'arn:aws:kms:us-west-2:123456789012:key/abcdefg');
const table = new dynamodb.Table(scope, 'Table', {
Expand Down Expand Up @@ -89,4 +93,20 @@ describe('CDK Construct tests', () => {
],
} as TableArgs);
});

test('route53 long text records are split', async () => {
await testApp((scope: Construct) => {
const zone = new route53.PublicHostedZone(scope, 'HostedZone', {
zoneName: 'pulumi-cdk.com',
});
new route53.TxtRecord(scope, 'TxtRecord2', {
zone,
values: ['hello'.repeat(52)],
recordName: 'cdk-txt-2',
});
});
const txt = resources.find((res) => res.type === 'aws:route53/record:Record');
expect(txt).toBeDefined();
expect(txt?.inputs.records).toEqual(['hello'.repeat(51), 'hello']);
});
});

0 comments on commit 129e930

Please sign in to comment.