Skip to content

Commit

Permalink
feat(cfn-include): add support for retrieving Mapping objects from th…
Browse files Browse the repository at this point in the history
…e template (#9777)

Fixes #9711

----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
skinny85 committed Aug 19, 2020
1 parent 4d044d0 commit a038304
Show file tree
Hide file tree
Showing 7 changed files with 210 additions and 10 deletions.
18 changes: 18 additions & 0 deletions packages/@aws-cdk/cloudformation-include/README.md
Expand Up @@ -163,6 +163,24 @@ and any changes you make to it will be reflected in the resulting template:
condition.expression = core.Fn.conditionEquals(1, 2);
```

## Mappings

If your template uses [CloudFormation Mappings](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/mappings-section-structure.html),
you can retrieve them from your template:

```typescript
import * as core from '@aws-cdk/core';

const mapping: core.CfnMapping = cfnTemplate.getMapping('MyMapping');
```

The `CfnMapping` object is mutable,
and any changes you make to it will be reflected in the resulting template:

```typescript
mapping.setValue('my-region', 'AMI', 'ami-04681a1dbd79675a5');
```

## Outputs

If your template uses [CloudFormation Outputs](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/outputs-section-structure.html),
Expand Down
65 changes: 59 additions & 6 deletions packages/@aws-cdk/cloudformation-include/lib/cfn-include.ts
Expand Up @@ -68,6 +68,8 @@ export class CfnInclude extends core.CfnElement {
private readonly resources: { [logicalId: string]: core.CfnResource } = {};
private readonly parameters: { [logicalId: string]: core.CfnParameter } = {};
private readonly parametersToReplace: { [parameterName: string]: any };
private readonly mappingsScope: core.Construct;
private readonly mappings: { [mappingName: string]: core.CfnMapping } = {};
private readonly outputs: { [logicalId: string]: core.CfnOutput } = {};
private readonly nestedStacks: { [logicalId: string]: IncludedNestedStack } = {};
private readonly nestedStacksToInclude: { [name: string]: CfnIncludeProps };
Expand All @@ -92,6 +94,12 @@ export class CfnInclude extends core.CfnElement {
}
}

// instantiate the Mappings
this.mappingsScope = new core.Construct(this, '$Mappings');
for (const mappingName of Object.keys(this.template.Mappings || {})) {
this.createMapping(mappingName);
}

// instantiate all parameters
for (const logicalId of Object.keys(this.template.Parameters || {})) {
this.createParameter(logicalId);
Expand All @@ -104,12 +112,10 @@ export class CfnInclude extends core.CfnElement {
}

this.nestedStacksToInclude = props.nestedStacks || {};

// instantiate all resources as CDK L1 objects
for (const logicalId of Object.keys(this.template.Resources || {})) {
this.getOrCreateResource(logicalId);
}

// verify that all nestedStacks have been instantiated
for (const nestedStackId of Object.keys(props.nestedStacks || {})) {
if (!(nestedStackId in this.resources)) {
Expand All @@ -118,7 +124,6 @@ export class CfnInclude extends core.CfnElement {
}

const outputScope = new core.Construct(this, '$Ouputs');

for (const logicalId of Object.keys(this.template.Outputs || {})) {
this.createOutput(logicalId, outputScope);
}
Expand Down Expand Up @@ -168,7 +173,7 @@ export class CfnInclude extends core.CfnElement {

/**
* Returns the CfnParameter object from the 'Parameters'
* section of the included template
* section of the included template.
* Any modifications performed on that object will be reflected in the resulting CDK template.
*
* If a Parameter with the given name is not present in the template,
Expand All @@ -184,9 +189,26 @@ export class CfnInclude extends core.CfnElement {
return ret;
}

/**
* Returns the CfnMapping object from the 'Mappings' section of the included template.
* Any modifications performed on that object will be reflected in the resulting CDK template.
*
* If a Mapping with the given name is not present in the template,
* an exception will be thrown.
*
* @param mappingName the name of the Mapping in the template to retrieve
*/
public getMapping(mappingName: string): core.CfnMapping {
const ret = this.mappings[mappingName];
if (!ret) {
throw new Error(`Mapping with name '${mappingName}' was not found in the template`);
}
return ret;
}

/**
* Returns the CfnOutput object from the 'Outputs'
* section of the included template
* section of the included template.
* Any modifications performed on that object will be reflected in the resulting CDK template.
*
* If an Output with the given name is not present in the template,
Expand All @@ -205,7 +227,9 @@ export class CfnInclude extends core.CfnElement {
/**
* Returns the NestedStack with name logicalId.
* For a nested stack to be returned by this method, it must be specified in the {@link CfnIncludeProps.nestedStacks}
* @param logicalId the ID of the stack to retrieve, as it appears in the template.
* property.
*
* @param logicalId the ID of the stack to retrieve, as it appears in the template
*/
public getNestedStack(logicalId: string): IncludedNestedStack {
if (!this.nestedStacks[logicalId]) {
Expand Down Expand Up @@ -236,6 +260,9 @@ export class CfnInclude extends core.CfnElement {
findCondition(conditionName: string): core.CfnCondition | undefined {
return self.conditions[conditionName];
},
findMapping(mappingName): core.CfnMapping | undefined {
return self.mappings[mappingName];
},
};
const cfnParser = new cfn_parse.CfnParser({
finder,
Expand All @@ -244,6 +271,7 @@ export class CfnInclude extends core.CfnElement {

switch (section) {
case 'Conditions':
case 'Mappings':
case 'Resources':
case 'Parameters':
case 'Outputs':
Expand All @@ -257,6 +285,22 @@ export class CfnInclude extends core.CfnElement {
return ret;
}

private createMapping(mappingName: string): void {
const cfnParser = new cfn_parse.CfnParser({
finder: {
findCondition() { throw new Error('Referring to Conditions in Mapping definitions is not allowed'); },
findMapping() { throw new Error('Referring to other Mappings in Mapping definitions is not allowed'); },
findRefTarget() { throw new Error('Using Ref expressions in Mapping definitions is not allowed'); },
findResource() { throw new Error('Using GetAtt expressions in Mapping definitions is not allowed'); },
},
});
const cfnMapping = new core.CfnMapping(this.mappingsScope, mappingName, {
mapping: cfnParser.parseValue(this.template.Mappings[mappingName]),
});
this.mappings[mappingName] = cfnMapping;
cfnMapping.overrideLogicalId(mappingName);
}

private createParameter(logicalId: string): void {
if (logicalId in this.parametersToReplace) {
return;
Expand All @@ -267,6 +311,7 @@ export class CfnInclude extends core.CfnElement {
findResource() { throw new Error('Using GetAtt expressions in Parameter definitions is not allowed'); },
findRefTarget() { throw new Error('Using Ref expressions in Parameter definitions is not allowed'); },
findCondition() { throw new Error('Referring to Conditions in Parameter definitions is not allowed'); },
findMapping() { throw new Error('Referring to Mappings in Parameter definitions is not allowed'); },
},
}).parseValue(this.template.Parameters[logicalId]);
const cfnParameter = new core.CfnParameter(this, logicalId, {
Expand Down Expand Up @@ -300,6 +345,9 @@ export class CfnInclude extends core.CfnElement {
findCondition(): undefined {
return undefined;
},
findMapping(mappingName): core.CfnMapping | undefined {
return self.mappings[mappingName];
},
},
parameters: this.parametersToReplace,
}).parseValue(this.template.Outputs[logicalId]);
Expand Down Expand Up @@ -341,6 +389,7 @@ export class CfnInclude extends core.CfnElement {
? self.getOrCreateCondition(cName)
: undefined;
},
findMapping() { throw new Error('Using FindInMap in Condition definitions is not allowed'); },
},
context: cfn_parse.CfnParsingContext.CONDITIONS,
parameters: this.parametersToReplace,
Expand Down Expand Up @@ -381,6 +430,10 @@ export class CfnInclude extends core.CfnElement {
return self.conditions[conditionName];
},

findMapping(mappingName): core.CfnMapping | undefined {
return self.mappings[mappingName];
},

findResource(lId: string): core.CfnResource | undefined {
if (!(lId in (self.template.Resources || {}))) {
return undefined;
Expand Down
Expand Up @@ -95,12 +95,18 @@ describe('CDK Include', () => {
}).toThrow(/Resource used in GetAtt expression with logical ID: 'DoesNotExist' not found/);
});

test("throws a validation exception when an output references a condition that doesn't exist", () => {
test("throws a validation exception when an Output references a Condition that doesn't exist", () => {
expect(() => {
includeTestTemplate(stack, 'output-referencing-nonexistant-condition.json');
}).toThrow(/Output with name 'SomeOutput' refers to a Condition with name 'NonexistantCondition' which was not found in this template/);
});

test("throws a validation exception when a Resource property references a Mapping that doesn't exist", () => {
expect(() => {
includeTestTemplate(stack, 'non-existent-mapping.json');
}).toThrow(/Mapping used in FindInMap expression with name 'NonExistentMapping' was not found in the template/);
});

test("throws a validation exception when Fn::Sub in string form uses a key that isn't in the template", () => {
expect(() => {
includeTestTemplate(stack, 'fn-sub-key-not-in-template-string.json');
Expand Down
@@ -0,0 +1,16 @@
{
"Resources": {
"Bucket": {
"Type": "AWS::S3::Bucket",
"Properties": {
"BucketName": {
"Fn::FindInMap": [
"NonExistentMapping",
{ "Ref": "AWS::Region" },
"key1"
]
}
}
}
}
}
@@ -0,0 +1,23 @@
{
"Mappings": {
"SomeMapping": {
"region": {
"key1": "value1"
}
}
},
"Resources": {
"Bucket": {
"Type": "AWS::S3::Bucket",
"Properties": {
"BucketName": {
"Fn::FindInMap": [
"SomeMapping",
{ "Ref": "AWS::Region" },
"key1"
]
}
}
}
}
}
Expand Up @@ -665,7 +665,78 @@ describe('CDK Include', () => {
}).toThrow(/Output with logical ID 'FakeOutput' was not found in the template/);
});

test('replaces references to parameters with the user-specified values in Resources, Conditions, Metadata, and Options', () => {
test('can ingest a template that contains Mappings, and retrieve those Mappings', () => {
const cfnTemplate = includeTestTemplate(stack, 'only-mapping-and-bucket.json');
const someMapping = cfnTemplate.getMapping('SomeMapping');

someMapping.setValue('region', 'key2', 'value2');

expect(stack).toMatchTemplate({
"Mappings": {
"SomeMapping": {
"region": {
"key1": "value1",
"key2": "value2",
},
},
},
"Resources": {
"Bucket": {
"Type": "AWS::S3::Bucket",
"Properties": {
"BucketName": {
"Fn::FindInMap": [
"SomeMapping",
{ "Ref": "AWS::Region" },
"key1",
],
},
},
},
},
});
});

test("throws an exception when attempting to retrieve a Mapping that doesn't exist in the template", () => {
const cfnTemplate = includeTestTemplate(stack, 'only-mapping-and-bucket.json');

expect(() => {
cfnTemplate.getMapping('NonExistentMapping');
}).toThrow(/Mapping with name 'NonExistentMapping' was not found in the template/);
});

test('handles renaming Mapping references', () => {
const cfnTemplate = includeTestTemplate(stack, 'only-mapping-and-bucket.json');
const someMapping = cfnTemplate.getMapping('SomeMapping');

someMapping.overrideLogicalId('DifferentMapping');

expect(stack).toMatchTemplate({
"Mappings": {
"DifferentMapping": {
"region": {
"key1": "value1",
},
},
},
"Resources": {
"Bucket": {
"Type": "AWS::S3::Bucket",
"Properties": {
"BucketName": {
"Fn::FindInMap": [
"DifferentMapping",
{ "Ref": "AWS::Region" },
"key1",
],
},
},
},
},
});
});

test('replaces references to parameters with the user-specified values in Resources, Conditions, Metadata, and Options sections', () => {
includeTestTemplate(stack, 'parameter-references.json', {
parameters: {
'MyParam': 'my-s3-bucket',
Expand Down Expand Up @@ -712,7 +783,7 @@ describe('CDK Include', () => {
});
});

test('can replace parameters in Fn::Sub', () => {
test('replaces parameters in Fn::Sub expressions', () => {
includeTestTemplate(stack, 'fn-sub-parameters.json', {
parameters: {
'MyParam': 'my-s3-bucket',
Expand Down
15 changes: 14 additions & 1 deletion packages/@aws-cdk/core/lib/cfn-parse.ts
@@ -1,6 +1,7 @@
import { CfnCondition } from './cfn-condition';
import { CfnElement } from './cfn-element';
import { Fn } from './cfn-fn';
import { CfnMapping } from './cfn-mapping';
import { Aws } from './cfn-pseudo';
import { CfnResource } from './cfn-resource';
import {
Expand Down Expand Up @@ -168,6 +169,13 @@ export interface ICfnFinder {
*/
findCondition(conditionName: string): CfnCondition | undefined;

/**
* Return the Mapping with the given name from the template.
* If there is no Mapping with that name in the template,
* returns undefined.
*/
findMapping(mappingName: string): CfnMapping | undefined;

/**
* Returns the element referenced using a Ref expression with the given name.
* If there is no element with this name in the template,
Expand Down Expand Up @@ -452,7 +460,12 @@ export class CfnParser {
}
case 'Fn::FindInMap': {
const value = this.parseValue(object[key]);
return Fn.findInMap(value[0], value[1], value[2]);
// the first argument to FindInMap is the mapping name
const mapping = this.options.finder.findMapping(value[0]);
if (!mapping) {
throw new Error(`Mapping used in FindInMap expression with name '${value[0]}' was not found in the template`);
}
return Fn.findInMap(mapping.logicalId, value[1], value[2]);
}
case 'Fn::Select': {
const value = this.parseValue(object[key]);
Expand Down

0 comments on commit a038304

Please sign in to comment.