Skip to content

Commit a038304

Browse files
authored
feat(cfn-include): add support for retrieving Mapping objects from the 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*
1 parent 4d044d0 commit a038304

File tree

7 files changed

+210
-10
lines changed

7 files changed

+210
-10
lines changed

packages/@aws-cdk/cloudformation-include/README.md

+18
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,24 @@ and any changes you make to it will be reflected in the resulting template:
163163
condition.expression = core.Fn.conditionEquals(1, 2);
164164
```
165165

166+
## Mappings
167+
168+
If your template uses [CloudFormation Mappings](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/mappings-section-structure.html),
169+
you can retrieve them from your template:
170+
171+
```typescript
172+
import * as core from '@aws-cdk/core';
173+
174+
const mapping: core.CfnMapping = cfnTemplate.getMapping('MyMapping');
175+
```
176+
177+
The `CfnMapping` object is mutable,
178+
and any changes you make to it will be reflected in the resulting template:
179+
180+
```typescript
181+
mapping.setValue('my-region', 'AMI', 'ami-04681a1dbd79675a5');
182+
```
183+
166184
## Outputs
167185

168186
If your template uses [CloudFormation Outputs](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/outputs-section-structure.html),

packages/@aws-cdk/cloudformation-include/lib/cfn-include.ts

+59-6
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ export class CfnInclude extends core.CfnElement {
6868
private readonly resources: { [logicalId: string]: core.CfnResource } = {};
6969
private readonly parameters: { [logicalId: string]: core.CfnParameter } = {};
7070
private readonly parametersToReplace: { [parameterName: string]: any };
71+
private readonly mappingsScope: core.Construct;
72+
private readonly mappings: { [mappingName: string]: core.CfnMapping } = {};
7173
private readonly outputs: { [logicalId: string]: core.CfnOutput } = {};
7274
private readonly nestedStacks: { [logicalId: string]: IncludedNestedStack } = {};
7375
private readonly nestedStacksToInclude: { [name: string]: CfnIncludeProps };
@@ -92,6 +94,12 @@ export class CfnInclude extends core.CfnElement {
9294
}
9395
}
9496

97+
// instantiate the Mappings
98+
this.mappingsScope = new core.Construct(this, '$Mappings');
99+
for (const mappingName of Object.keys(this.template.Mappings || {})) {
100+
this.createMapping(mappingName);
101+
}
102+
95103
// instantiate all parameters
96104
for (const logicalId of Object.keys(this.template.Parameters || {})) {
97105
this.createParameter(logicalId);
@@ -104,12 +112,10 @@ export class CfnInclude extends core.CfnElement {
104112
}
105113

106114
this.nestedStacksToInclude = props.nestedStacks || {};
107-
108115
// instantiate all resources as CDK L1 objects
109116
for (const logicalId of Object.keys(this.template.Resources || {})) {
110117
this.getOrCreateResource(logicalId);
111118
}
112-
113119
// verify that all nestedStacks have been instantiated
114120
for (const nestedStackId of Object.keys(props.nestedStacks || {})) {
115121
if (!(nestedStackId in this.resources)) {
@@ -118,7 +124,6 @@ export class CfnInclude extends core.CfnElement {
118124
}
119125

120126
const outputScope = new core.Construct(this, '$Ouputs');
121-
122127
for (const logicalId of Object.keys(this.template.Outputs || {})) {
123128
this.createOutput(logicalId, outputScope);
124129
}
@@ -168,7 +173,7 @@ export class CfnInclude extends core.CfnElement {
168173

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

192+
/**
193+
* Returns the CfnMapping object from the 'Mappings' section of the included template.
194+
* Any modifications performed on that object will be reflected in the resulting CDK template.
195+
*
196+
* If a Mapping with the given name is not present in the template,
197+
* an exception will be thrown.
198+
*
199+
* @param mappingName the name of the Mapping in the template to retrieve
200+
*/
201+
public getMapping(mappingName: string): core.CfnMapping {
202+
const ret = this.mappings[mappingName];
203+
if (!ret) {
204+
throw new Error(`Mapping with name '${mappingName}' was not found in the template`);
205+
}
206+
return ret;
207+
}
208+
187209
/**
188210
* Returns the CfnOutput object from the 'Outputs'
189-
* section of the included template
211+
* section of the included template.
190212
* Any modifications performed on that object will be reflected in the resulting CDK template.
191213
*
192214
* If an Output with the given name is not present in the template,
@@ -205,7 +227,9 @@ export class CfnInclude extends core.CfnElement {
205227
/**
206228
* Returns the NestedStack with name logicalId.
207229
* For a nested stack to be returned by this method, it must be specified in the {@link CfnIncludeProps.nestedStacks}
208-
* @param logicalId the ID of the stack to retrieve, as it appears in the template.
230+
* property.
231+
*
232+
* @param logicalId the ID of the stack to retrieve, as it appears in the template
209233
*/
210234
public getNestedStack(logicalId: string): IncludedNestedStack {
211235
if (!this.nestedStacks[logicalId]) {
@@ -236,6 +260,9 @@ export class CfnInclude extends core.CfnElement {
236260
findCondition(conditionName: string): core.CfnCondition | undefined {
237261
return self.conditions[conditionName];
238262
},
263+
findMapping(mappingName): core.CfnMapping | undefined {
264+
return self.mappings[mappingName];
265+
},
239266
};
240267
const cfnParser = new cfn_parse.CfnParser({
241268
finder,
@@ -244,6 +271,7 @@ export class CfnInclude extends core.CfnElement {
244271

245272
switch (section) {
246273
case 'Conditions':
274+
case 'Mappings':
247275
case 'Resources':
248276
case 'Parameters':
249277
case 'Outputs':
@@ -257,6 +285,22 @@ export class CfnInclude extends core.CfnElement {
257285
return ret;
258286
}
259287

288+
private createMapping(mappingName: string): void {
289+
const cfnParser = new cfn_parse.CfnParser({
290+
finder: {
291+
findCondition() { throw new Error('Referring to Conditions in Mapping definitions is not allowed'); },
292+
findMapping() { throw new Error('Referring to other Mappings in Mapping definitions is not allowed'); },
293+
findRefTarget() { throw new Error('Using Ref expressions in Mapping definitions is not allowed'); },
294+
findResource() { throw new Error('Using GetAtt expressions in Mapping definitions is not allowed'); },
295+
},
296+
});
297+
const cfnMapping = new core.CfnMapping(this.mappingsScope, mappingName, {
298+
mapping: cfnParser.parseValue(this.template.Mappings[mappingName]),
299+
});
300+
this.mappings[mappingName] = cfnMapping;
301+
cfnMapping.overrideLogicalId(mappingName);
302+
}
303+
260304
private createParameter(logicalId: string): void {
261305
if (logicalId in this.parametersToReplace) {
262306
return;
@@ -267,6 +311,7 @@ export class CfnInclude extends core.CfnElement {
267311
findResource() { throw new Error('Using GetAtt expressions in Parameter definitions is not allowed'); },
268312
findRefTarget() { throw new Error('Using Ref expressions in Parameter definitions is not allowed'); },
269313
findCondition() { throw new Error('Referring to Conditions in Parameter definitions is not allowed'); },
314+
findMapping() { throw new Error('Referring to Mappings in Parameter definitions is not allowed'); },
270315
},
271316
}).parseValue(this.template.Parameters[logicalId]);
272317
const cfnParameter = new core.CfnParameter(this, logicalId, {
@@ -300,6 +345,9 @@ export class CfnInclude extends core.CfnElement {
300345
findCondition(): undefined {
301346
return undefined;
302347
},
348+
findMapping(mappingName): core.CfnMapping | undefined {
349+
return self.mappings[mappingName];
350+
},
303351
},
304352
parameters: this.parametersToReplace,
305353
}).parseValue(this.template.Outputs[logicalId]);
@@ -341,6 +389,7 @@ export class CfnInclude extends core.CfnElement {
341389
? self.getOrCreateCondition(cName)
342390
: undefined;
343391
},
392+
findMapping() { throw new Error('Using FindInMap in Condition definitions is not allowed'); },
344393
},
345394
context: cfn_parse.CfnParsingContext.CONDITIONS,
346395
parameters: this.parametersToReplace,
@@ -381,6 +430,10 @@ export class CfnInclude extends core.CfnElement {
381430
return self.conditions[conditionName];
382431
},
383432

433+
findMapping(mappingName): core.CfnMapping | undefined {
434+
return self.mappings[mappingName];
435+
},
436+
384437
findResource(lId: string): core.CfnResource | undefined {
385438
if (!(lId in (self.template.Resources || {}))) {
386439
return undefined;

packages/@aws-cdk/cloudformation-include/test/invalid-templates.test.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -95,12 +95,18 @@ describe('CDK Include', () => {
9595
}).toThrow(/Resource used in GetAtt expression with logical ID: 'DoesNotExist' not found/);
9696
});
9797

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

104+
test("throws a validation exception when a Resource property references a Mapping that doesn't exist", () => {
105+
expect(() => {
106+
includeTestTemplate(stack, 'non-existent-mapping.json');
107+
}).toThrow(/Mapping used in FindInMap expression with name 'NonExistentMapping' was not found in the template/);
108+
});
109+
104110
test("throws a validation exception when Fn::Sub in string form uses a key that isn't in the template", () => {
105111
expect(() => {
106112
includeTestTemplate(stack, 'fn-sub-key-not-in-template-string.json');
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"Resources": {
3+
"Bucket": {
4+
"Type": "AWS::S3::Bucket",
5+
"Properties": {
6+
"BucketName": {
7+
"Fn::FindInMap": [
8+
"NonExistentMapping",
9+
{ "Ref": "AWS::Region" },
10+
"key1"
11+
]
12+
}
13+
}
14+
}
15+
}
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"Mappings": {
3+
"SomeMapping": {
4+
"region": {
5+
"key1": "value1"
6+
}
7+
}
8+
},
9+
"Resources": {
10+
"Bucket": {
11+
"Type": "AWS::S3::Bucket",
12+
"Properties": {
13+
"BucketName": {
14+
"Fn::FindInMap": [
15+
"SomeMapping",
16+
{ "Ref": "AWS::Region" },
17+
"key1"
18+
]
19+
}
20+
}
21+
}
22+
}
23+
}

packages/@aws-cdk/cloudformation-include/test/valid-templates.test.ts

+73-2
Original file line numberDiff line numberDiff line change
@@ -665,7 +665,78 @@ describe('CDK Include', () => {
665665
}).toThrow(/Output with logical ID 'FakeOutput' was not found in the template/);
666666
});
667667

668-
test('replaces references to parameters with the user-specified values in Resources, Conditions, Metadata, and Options', () => {
668+
test('can ingest a template that contains Mappings, and retrieve those Mappings', () => {
669+
const cfnTemplate = includeTestTemplate(stack, 'only-mapping-and-bucket.json');
670+
const someMapping = cfnTemplate.getMapping('SomeMapping');
671+
672+
someMapping.setValue('region', 'key2', 'value2');
673+
674+
expect(stack).toMatchTemplate({
675+
"Mappings": {
676+
"SomeMapping": {
677+
"region": {
678+
"key1": "value1",
679+
"key2": "value2",
680+
},
681+
},
682+
},
683+
"Resources": {
684+
"Bucket": {
685+
"Type": "AWS::S3::Bucket",
686+
"Properties": {
687+
"BucketName": {
688+
"Fn::FindInMap": [
689+
"SomeMapping",
690+
{ "Ref": "AWS::Region" },
691+
"key1",
692+
],
693+
},
694+
},
695+
},
696+
},
697+
});
698+
});
699+
700+
test("throws an exception when attempting to retrieve a Mapping that doesn't exist in the template", () => {
701+
const cfnTemplate = includeTestTemplate(stack, 'only-mapping-and-bucket.json');
702+
703+
expect(() => {
704+
cfnTemplate.getMapping('NonExistentMapping');
705+
}).toThrow(/Mapping with name 'NonExistentMapping' was not found in the template/);
706+
});
707+
708+
test('handles renaming Mapping references', () => {
709+
const cfnTemplate = includeTestTemplate(stack, 'only-mapping-and-bucket.json');
710+
const someMapping = cfnTemplate.getMapping('SomeMapping');
711+
712+
someMapping.overrideLogicalId('DifferentMapping');
713+
714+
expect(stack).toMatchTemplate({
715+
"Mappings": {
716+
"DifferentMapping": {
717+
"region": {
718+
"key1": "value1",
719+
},
720+
},
721+
},
722+
"Resources": {
723+
"Bucket": {
724+
"Type": "AWS::S3::Bucket",
725+
"Properties": {
726+
"BucketName": {
727+
"Fn::FindInMap": [
728+
"DifferentMapping",
729+
{ "Ref": "AWS::Region" },
730+
"key1",
731+
],
732+
},
733+
},
734+
},
735+
},
736+
});
737+
});
738+
739+
test('replaces references to parameters with the user-specified values in Resources, Conditions, Metadata, and Options sections', () => {
669740
includeTestTemplate(stack, 'parameter-references.json', {
670741
parameters: {
671742
'MyParam': 'my-s3-bucket',
@@ -712,7 +783,7 @@ describe('CDK Include', () => {
712783
});
713784
});
714785

715-
test('can replace parameters in Fn::Sub', () => {
786+
test('replaces parameters in Fn::Sub expressions', () => {
716787
includeTestTemplate(stack, 'fn-sub-parameters.json', {
717788
parameters: {
718789
'MyParam': 'my-s3-bucket',

packages/@aws-cdk/core/lib/cfn-parse.ts

+14-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { CfnCondition } from './cfn-condition';
22
import { CfnElement } from './cfn-element';
33
import { Fn } from './cfn-fn';
4+
import { CfnMapping } from './cfn-mapping';
45
import { Aws } from './cfn-pseudo';
56
import { CfnResource } from './cfn-resource';
67
import {
@@ -168,6 +169,13 @@ export interface ICfnFinder {
168169
*/
169170
findCondition(conditionName: string): CfnCondition | undefined;
170171

172+
/**
173+
* Return the Mapping with the given name from the template.
174+
* If there is no Mapping with that name in the template,
175+
* returns undefined.
176+
*/
177+
findMapping(mappingName: string): CfnMapping | undefined;
178+
171179
/**
172180
* Returns the element referenced using a Ref expression with the given name.
173181
* If there is no element with this name in the template,
@@ -452,7 +460,12 @@ export class CfnParser {
452460
}
453461
case 'Fn::FindInMap': {
454462
const value = this.parseValue(object[key]);
455-
return Fn.findInMap(value[0], value[1], value[2]);
463+
// the first argument to FindInMap is the mapping name
464+
const mapping = this.options.finder.findMapping(value[0]);
465+
if (!mapping) {
466+
throw new Error(`Mapping used in FindInMap expression with name '${value[0]}' was not found in the template`);
467+
}
468+
return Fn.findInMap(mapping.logicalId, value[1], value[2]);
456469
}
457470
case 'Fn::Select': {
458471
const value = this.parseValue(object[key]);

0 commit comments

Comments
 (0)