Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hosted Zone Context Provider #425

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 30 additions & 2 deletions packages/@aws-cdk/cdk/lib/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Construct } from './core/construct';

const AVAILABILITY_ZONES_PROVIDER = 'availability-zones';
const SSM_PARAMETER_PROVIDER = 'ssm';
const HOSTED_ZONE_PROVIDER = 'hosted-zone';

/**
* Base class for the model side of context providers
Expand Down Expand Up @@ -157,13 +158,40 @@ export class SSMParameterProvider {
}
}

export interface HostedZoneFilter {
name: string;
privateZone?: boolean;
vpcId?: string;
}

/**
* Context provider that will lookup the Hosted Zone ID for the given arguments
*/
export class HostedZoneProvider {
private provider: ContextProvider;

constructor(context: Construct) {
this.provider = new ContextProvider(context);
}

/**
* Return the hosted zone meeting the filter
*/
public getZone(filter: HostedZoneFilter): string {
const scope = this.provider.accountRegionScope('HostedZoneProvider');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this intended to be "hosted-zone"?

const args: string[] = [filter.name];
if (filter.privateZone) { args.push(`${filter.privateZone}`); }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was this intended as a test against undefined or a test against true?

if (filter.vpcId) { args.push(filter.vpcId); }
return this.provider.getStringValue(HOSTED_ZONE_PROVIDER, scope, args, 'dummy');
}
}

function formatMissingScopeError(provider: string, args: string[]) {
let s = `Cannot determine scope for context provider ${provider}`;
if (args.length > 0) {
s += JSON.stringify(args);
}
s += '.';
s += '\n';
s += '.\n';
s += 'This usually happens when AWS credentials are not available and the default account/region cannot be determined.';
return s;
}
24 changes: 23 additions & 1 deletion packages/@aws-cdk/cdk/test/test.context.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import cxapi = require('@aws-cdk/cx-api');
import { Test } from 'nodeunit';
import { App, AvailabilityZoneProvider, Construct, MetadataEntry, resolve, SSMParameterProvider, Stack } from '../lib';
import { App, AvailabilityZoneProvider, Construct, HostedZoneProvider, MetadataEntry, resolve, SSMParameterProvider, Stack } from '../lib';

export = {
'AvailabilityZoneProvider returns a list with dummy values if the context is not available'(test: Test) {
Expand Down Expand Up @@ -53,22 +53,44 @@ export = {
test.done();
},

'HostedZoneProvider will return context values if availble'(test: Test) {
const stack = new Stack(undefined, 'TestStack', { env: { account: '12345', region: 'us-east-1' } });
const filter = {name: 'test.com'};
new HostedZoneProvider(stack).getZone(filter);
const key = expectedContextKey(stack);

stack.setContext(key, 'HOSTEDZONEID');

const zone = resolve(new HostedZoneProvider(stack).getZone(filter));
test.deepEqual(zone, 'HOSTEDZONEID');

test.done();
},

'Return default values if "env" is undefined to facilitate unit tests, but also expect metadata to include "error" messages'(test: Test) {
const app = new App();
const stack = new Stack(app, 'test-stack');

const child = new Construct(stack, 'ChildConstruct');

const hostedChild = new Construct(stack, 'HostedChildConstruct');

test.deepEqual(new AvailabilityZoneProvider(stack).availabilityZones, [ 'dummy1a', 'dummy1b', 'dummy1c' ]);
test.deepEqual(new SSMParameterProvider(child).getString('foo'), 'dummy');

test.deepEqual(new HostedZoneProvider(hostedChild).getZone({name: 'test.com'}), 'dummy');

const output = app.synthesizeStack(stack.name);

const azError: MetadataEntry | undefined = output.metadata['/test-stack'].find(x => x.type === cxapi.ERROR_METADATA_KEY);
const ssmError: MetadataEntry | undefined = output.metadata['/test-stack/ChildConstruct'].find(x => x.type === cxapi.ERROR_METADATA_KEY);

const hostedError: MetadataEntry | undefined = output.metadata['/test-stack/HostedChildConstruct']
.find(x => x.type === cxapi.ERROR_METADATA_KEY);

test.ok(azError && (azError.data as string).includes('Cannot determine scope for context provider availability-zones.'));
test.ok(ssmError && (ssmError.data as string).includes('Cannot determine scope for context provider ssm["foo"].'));
test.ok(hostedError && (hostedError.data as string).includes('Cannot determine scope for context provider hosted-zone'));

test.done();
},
Expand Down
10 changes: 9 additions & 1 deletion packages/aws-cdk/lib/api/util/sdk.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Environment} from '@aws-cdk/cx-api';
import { CloudFormation, config, CredentialProviderChain, EC2, S3, SSM, STS } from 'aws-sdk';
import { CloudFormation, config, CredentialProviderChain, EC2, Route53, S3, SSM, STS } from 'aws-sdk';
import { debug } from '../../logging';
import { PluginHost } from '../../plugin';
import { CredentialProviderSource, Mode } from '../aws-auth/credentials';
Expand Down Expand Up @@ -58,6 +58,14 @@ export class SDK {
});
}

public async route53(awsAccountId: string | undefined, region: string | undefined, mode: Mode): Promise<Route53> {
return new Route53({
region,
credentialProvider: await this.getCredentialProvider(awsAccountId, mode),
customUserAgent: this.userAgent
});
}

public defaultRegion() {
return config.region;
}
Expand Down
62 changes: 60 additions & 2 deletions packages/aws-cdk/lib/contextplugins.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { MissingContext } from '@aws-cdk/cx-api';
import { Mode, SDK } from './api';
import { Route53 } from 'aws-sdk';
import { debug } from './logging';
import { Settings } from './settings';

Expand Down Expand Up @@ -48,12 +49,69 @@ export class SSMContextProviderPlugin implements ContextProviderPlugin {
}
}

/**
* Plugin to read arbitrary SSM parameter names
*/
export class HostedZoneContextProviderPlugin implements ContextProviderPlugin {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An instance of this needs to be added to availableContextProviders in cdk.ts as well.

constructor(private readonly aws: SDK) {
}

public async getValue(scope: string[], args: string[]) {
const [account, region] = scope;
const domainName = args[0];
const privateZone: boolean = args[1] ? args[1] === 'true' : false;
const vpcId = args[2];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

args[2] might not exist.

debug(`Reading hosted zone ${account}:${region}:${domainName}`);

const r53 = await this.aws.route53(account, region, Mode.ForReading);
const response = await r53.listHostedZonesByName({ DNSName: domainName }).promise();
if (!response.HostedZones) {
throw new Error(`Hosted Zone not availble in account ${account}, region ${region}: ${domainName}`);
}
const candidateZones = this.filterZones(r53, response.HostedZones, privateZone, vpcId);
if(candidateZones.length > 1) {
const filter = `dns:${domainName}, privateZone:${privateZone}, vpcId:${vpcId}`;
throw new Error(`Found more than one matching HostedZone ${candidateZones} for ${filter}`);
}
return candidateZones[0];

}

private filterZones(r53: Route53, zones: Route53.HostedZone[], privateZone: boolean, vpcId: string | undefined): Route53.HostedZone[]{
let candidates: Route53.HostedZone[] = [];
if(privateZone) {
candidates = zones.filter(zone => zone.Config && zone.Config.PrivateZone);
} else {
candidates = zones.filter(zone => !zone.Config || !zone.Config.PrivateZone);
}
if(vpcId) {
const vpcZones: Route53.HostedZone[] = [];
for(const zone of candidates) {
r53.getHostedZone({Id: zone.Id}, (err, data) => {
if(err) {
throw new Error(err.message);
}
if(!data.VPCs) {
debug(`Expected VPC for private zone but no VPC found ${zone.Id}`)
return;
}
if(data.VPCs.map(vpc => vpc.VPCId).includes(vpcId)) {
vpcZones.push(zone);
}
});
}
return vpcZones;
}
return candidates;
}
}

/**
* Iterate over the list of missing context values and invoke the appropriate providers from the map to retrieve them
*/
export async function provideContextValues(missingValues: { [key: string]: MissingContext },
projectConfig: Settings,
availableContextProviders: ProviderMap) {
projectConfig: Settings,
availableContextProviders: ProviderMap) {
for (const key of Object.keys(missingValues)) {
const query = missingValues[key];

Expand Down