Skip to content

Commit 9d6817f

Browse files
authored
feat(cfn-include): add 'loadNestedStack()' method (#10292)
Add a method that allows you to load a nested stack after the CfnInclude object has been instantiated. To make this consistent in nested stack handling, rename the 'nestedStacks' property to 'loadNestedStacks'. BREAKING CHANGE: the construction property 'nestedStacks' of class 'CfnInclude' has been renamed to 'loadNestedStacks' ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent cb30d20 commit 9d6817f

File tree

5 files changed

+100
-40
lines changed

5 files changed

+100
-40
lines changed

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

+10-1
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,7 @@ and the nested stack in your CDK application as follows:
254254
```typescript
255255
const parentTemplate = new inc.CfnInclude(this, 'ParentStack', {
256256
templateFile: 'path/to/my-parent-template.json',
257-
nestedStacks: {
257+
loadNestedStacks: {
258258
'ChildStack': {
259259
templateFile: 'path/to/my-nested-template.json',
260260
},
@@ -299,6 +299,15 @@ role.addToPolicy(new iam.PolicyStatement({
299299
}));
300300
```
301301

302+
You can also include the nested stack after the `CfnInclude` object was created,
303+
instead of doing it on construction:
304+
305+
```ts
306+
const includedChildStack = parentTemplate.loadNestedStack('ChildTemplate', {
307+
templateFile: 'path/to/my-nested-template.json',
308+
});
309+
```
310+
302311
## Vending CloudFormation templates as Constructs
303312

304313
In many cases, there are existing CloudFormation templates that are not entire applications,

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

+45-8
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,16 @@ export interface CfnIncludeProps {
3232
* Specifies the template files that define nested stacks that should be included.
3333
*
3434
* If your template specifies a stack that isn't included here, it won't be created as a NestedStack
35-
* resource, and it won't be accessible from {@link CfnInclude.getNestedStack}.
35+
* resource, and it won't be accessible from the {@link CfnInclude.getNestedStack} method
36+
* (but will still be accessible from the {@link CfnInclude.getResource} method).
3637
*
3738
* If you include a stack here with an ID that isn't in the template,
3839
* or is in the template but is not a nested stack,
3940
* template creation will fail and an error will be thrown.
4041
*
4142
* @default - no nested stacks will be included
4243
*/
43-
readonly nestedStacks?: { [stackName: string]: CfnIncludeProps };
44+
readonly loadNestedStacks?: { [stackName: string]: CfnIncludeProps };
4445

4546
/**
4647
* Specifies parameters to be replaced by the values in this mapping.
@@ -134,13 +135,13 @@ export class CfnInclude extends core.CfnElement {
134135
this.createRule(ruleName);
135136
}
136137

137-
this.nestedStacksToInclude = props.nestedStacks || {};
138+
this.nestedStacksToInclude = props.loadNestedStacks || {};
138139
// instantiate all resources as CDK L1 objects
139140
for (const logicalId of Object.keys(this.template.Resources || {})) {
140141
this.getOrCreateResource(logicalId);
141142
}
142143
// verify that all nestedStacks have been instantiated
143-
for (const nestedStackId of Object.keys(props.nestedStacks || {})) {
144+
for (const nestedStackId of Object.keys(props.loadNestedStacks || {})) {
144145
if (!(nestedStackId in this.resources)) {
145146
throw new Error(`Nested Stack with logical ID '${nestedStackId}' was not found in the template`);
146147
}
@@ -290,9 +291,10 @@ export class CfnInclude extends core.CfnElement {
290291
}
291292

292293
/**
293-
* Returns the NestedStack with name logicalId.
294-
* For a nested stack to be returned by this method, it must be specified in the {@link CfnIncludeProps.nestedStacks}
295-
* property.
294+
* Returns a loaded NestedStack with name logicalId.
295+
* For a nested stack to be returned by this method,
296+
* it must be specified either in the {@link CfnIncludeProps.loadNestedStacks} property,
297+
* or through the {@link loadNestedStack} method.
296298
*
297299
* @param logicalId the ID of the stack to retrieve, as it appears in the template
298300
*/
@@ -303,12 +305,47 @@ export class CfnInclude extends core.CfnElement {
303305
} else if (this.template.Resources[logicalId].Type !== 'AWS::CloudFormation::Stack') {
304306
throw new Error(`Resource with logical ID '${logicalId}' is not a CloudFormation Stack`);
305307
} else {
306-
throw new Error(`Nested Stack '${logicalId}' was not included in the nestedStacks property when including the parent template`);
308+
throw new Error(`Nested Stack '${logicalId}' was not included in the parent template. ` +
309+
'To retrieve an included nested stack, it must be specified either in the `loadNestedStacks` property, or through the `loadNestedStack` method');
307310
}
308311
}
309312
return this.nestedStacks[logicalId];
310313
}
311314

315+
/**
316+
* Includes a template for a child stack inside of this parent template.
317+
* A child with this logical ID must exist in the template,
318+
* and be of type AWS::CloudFormation::Stack.
319+
* This is equivalent to specifying the value in the {@link CfnIncludeProps.loadNestedStacks}
320+
* property on object construction.
321+
*
322+
* @param logicalId the ID of the stack to retrieve, as it appears in the template
323+
* @param nestedStackProps the properties of the included child Stack
324+
* @returns the same {@link IncludedNestedStack} object that {@link getNestedStack} returns for this logical ID
325+
*/
326+
public loadNestedStack(logicalId: string, nestedStackProps: CfnIncludeProps): IncludedNestedStack {
327+
if (logicalId in this.nestedStacks) {
328+
throw new Error(`Nested Stack '${logicalId}' was already included in its parent template`);
329+
}
330+
const cfnStack = this.resources[logicalId];
331+
if (!cfnStack) {
332+
throw new Error(`Nested Stack with logical ID '${logicalId}' was not found in the template`);
333+
}
334+
if (cfnStack instanceof core.CfnStack) {
335+
// delete the old CfnStack child - one will be created by the NestedStack object
336+
this.node.tryRemoveChild(logicalId);
337+
// remove the previously created CfnStack resource from the resources map
338+
delete this.resources[logicalId];
339+
// createNestedStack() (called by getOrCreateResource()) expects this to be filled
340+
this.nestedStacksToInclude[logicalId] = nestedStackProps;
341+
342+
this.getOrCreateResource(logicalId);
343+
return this.nestedStacks[logicalId];
344+
} else {
345+
throw new Error(`Nested Stack with logical ID '${logicalId}' is not an AWS::CloudFormation::Stack resource`);
346+
}
347+
}
348+
312349
/** @internal */
313350
public _toCloudFormation(): object {
314351
const ret: { [section: string]: any } = {};

packages/@aws-cdk/cloudformation-include/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -340,7 +340,7 @@
340340
},
341341
"awslint": {
342342
"exclude": [
343-
"props-no-cfn-types:@aws-cdk/cloudformation-include.CfnIncludeProps.nestedStacks"
343+
"props-no-cfn-types:@aws-cdk/cloudformation-include.CfnIncludeProps.loadNestedStacks"
344344
]
345345
},
346346
"stability": "experimental",

packages/@aws-cdk/cloudformation-include/test/integ.nested-stacks.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const stack = new core.Stack(app, 'ParentStack');
77

88
new inc.CfnInclude(stack, 'ParentStack', {
99
templateFile: 'test-templates/nested/parent-one-child.json',
10-
nestedStacks: {
10+
loadNestedStacks: {
1111
ChildStack: {
1212
templateFile: 'test-templates/nested/grandchild-import-stack.json',
1313
},

packages/@aws-cdk/cloudformation-include/test/nested-stacks.test.ts

+43-29
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ describe('CDK Include for nested stacks', () => {
1919
test('can ingest a template with one child', () => {
2020
const parentTemplate = new inc.CfnInclude(stack, 'ParentStack', {
2121
templateFile: testTemplateFilePath('parent-one-child.json'),
22-
nestedStacks: {
22+
loadNestedStacks: {
2323
'ChildStack': {
2424
templateFile: testTemplateFilePath('grandchild-import-stack.json'),
2525
},
@@ -35,7 +35,7 @@ describe('CDK Include for nested stacks', () => {
3535
test('can ingest a template with two children', () => {
3636
const parentTemplate = new inc.CfnInclude(stack, 'ParentStack', {
3737
templateFile: testTemplateFilePath('parent-two-children.json'),
38-
nestedStacks: {
38+
loadNestedStacks: {
3939
'ChildStack': {
4040
templateFile: testTemplateFilePath('grandchild-import-stack.json'),
4141
},
@@ -59,10 +59,10 @@ describe('CDK Include for nested stacks', () => {
5959
test('can ingest a template with one child and one grandchild', () => {
6060
const parentTemplate = new inc.CfnInclude(stack, 'ParentStack', {
6161
templateFile: testTemplateFilePath('parent-two-children.json'),
62-
nestedStacks: {
62+
loadNestedStacks: {
6363
'ChildStack': {
6464
templateFile: testTemplateFilePath('child-import-stack.json'),
65-
nestedStacks: {
65+
loadNestedStacks: {
6666
'GrandChildStack': {
6767
templateFile: testTemplateFilePath('grandchild-import-stack.json'),
6868
},
@@ -86,7 +86,7 @@ describe('CDK Include for nested stacks', () => {
8686
expect(() => {
8787
new inc.CfnInclude(stack, 'ParentStack', {
8888
templateFile: testTemplateFilePath('parent-two-children.json'),
89-
nestedStacks: {
89+
loadNestedStacks: {
9090
'FakeStack': {
9191
templateFile: testTemplateFilePath('child-import-stack.json'),
9292
},
@@ -99,7 +99,7 @@ describe('CDK Include for nested stacks', () => {
9999
expect(() => {
100100
new inc.CfnInclude(stack, 'ParentStack', {
101101
templateFile: testTemplateFilePath('child-import-stack.json'),
102-
nestedStacks: {
102+
loadNestedStacks: {
103103
'BucketImport': {
104104
templateFile: testTemplateFilePath('grandchild-import-stack.json'),
105105
},
@@ -112,7 +112,7 @@ describe('CDK Include for nested stacks', () => {
112112
expect(() => {
113113
new inc.CfnInclude(stack, 'ParentStack', {
114114
templateFile: testTemplateFilePath('parent-creation-policy.json'),
115-
nestedStacks: {
115+
loadNestedStacks: {
116116
'ChildStack': {
117117
templateFile: testTemplateFilePath('grandchild-import-stack.json'),
118118
},
@@ -125,7 +125,7 @@ describe('CDK Include for nested stacks', () => {
125125
expect(() => {
126126
new inc.CfnInclude(stack, 'ParentStack', {
127127
templateFile: testTemplateFilePath('parent-update-policy.json'),
128-
nestedStacks: {
128+
loadNestedStacks: {
129129
'ChildStack': {
130130
templateFile: testTemplateFilePath('grandchild-import-stack.json'),
131131
},
@@ -138,7 +138,7 @@ describe('CDK Include for nested stacks', () => {
138138
expect(() => {
139139
new inc.CfnInclude(stack, 'ParentStack', {
140140
templateFile: testTemplateFilePath('parent-invalid-condition.json'),
141-
nestedStacks: {
141+
loadNestedStacks: {
142142
'ChildStack': {
143143
templateFile: testTemplateFilePath('grandchild-import-stack.json'),
144144
},
@@ -151,7 +151,7 @@ describe('CDK Include for nested stacks', () => {
151151
expect(() => {
152152
new inc.CfnInclude(stack, 'ParentStack', {
153153
templateFile: testTemplateFilePath('parent-bad-depends-on.json'),
154-
nestedStacks: {
154+
loadNestedStacks: {
155155
'ChildStack': {
156156
templateFile: testTemplateFilePath('child-import-stack.json'),
157157
},
@@ -160,11 +160,11 @@ describe('CDK Include for nested stacks', () => {
160160
}).toThrow(/Resource 'ChildStack' depends on 'AFakeResource' that doesn't exist/);
161161
});
162162

163-
test('throws an exception when an ID was passed in nestedStacks that is a resource type not in the CloudFormation schema', () => {
163+
test('throws an exception when an ID was passed in loadNestedStacks that is a resource type not in the CloudFormation schema', () => {
164164
expect(() => {
165165
new inc.CfnInclude(stack, 'Template', {
166166
templateFile: testTemplateFilePath('custom-resource.json'),
167-
nestedStacks: {
167+
loadNestedStacks: {
168168
'CustomResource': {
169169
templateFile: testTemplateFilePath('whatever.json'),
170170
},
@@ -176,7 +176,7 @@ describe('CDK Include for nested stacks', () => {
176176
test('can modify resources in nested stacks', () => {
177177
const parent = new inc.CfnInclude(stack, 'ParentStack', {
178178
templateFile: testTemplateFilePath('child-import-stack.json'),
179-
nestedStacks: {
179+
loadNestedStacks: {
180180
'GrandChildStack': {
181181
templateFile: testTemplateFilePath('grandchild-import-stack.json'),
182182
},
@@ -194,7 +194,7 @@ describe('CDK Include for nested stacks', () => {
194194
test('can use a condition', () => {
195195
const parent = new inc.CfnInclude(stack, 'ParentStack', {
196196
templateFile: testTemplateFilePath('parent-valid-condition.json'),
197-
nestedStacks: {
197+
loadNestedStacks: {
198198
'ChildStack': {
199199
templateFile: testTemplateFilePath('grandchild-import-stack.json'),
200200
},
@@ -209,7 +209,7 @@ describe('CDK Include for nested stacks', () => {
209209
test('asset parameters generated in parent and child are identical', () => {
210210
new inc.CfnInclude(stack, 'ParentStack', {
211211
templateFile: testTemplateFilePath('parent-one-child.json'),
212-
nestedStacks: {
212+
loadNestedStacks: {
213213
'ChildStack': {
214214
templateFile: testTemplateFilePath('grandchild-import-stack.json'),
215215
},
@@ -279,7 +279,7 @@ describe('CDK Include for nested stacks', () => {
279279
});
280280
});
281281

282-
test('templates with nested stacks that were not provided in the nestedStacks property are left unmodified', () => {
282+
test('templates with nested stacks that were not provided in the loadNestedStacks property are left unmodified', () => {
283283
new inc.CfnInclude(stack, 'ParentStack', {
284284
templateFile: testTemplateFilePath('parent-two-children.json'),
285285
});
@@ -290,7 +290,7 @@ describe('CDK Include for nested stacks', () => {
290290
test('getNestedStack() throws an exception when getting a resource that does not exist in the template', () => {
291291
const parentTemplate = new inc.CfnInclude(stack, 'ParentStack', {
292292
templateFile: testTemplateFilePath('parent-two-children.json'),
293-
nestedStacks: {
293+
loadNestedStacks: {
294294
'ChildStack': {
295295
templateFile: testTemplateFilePath('child-import-stack.json'),
296296
},
@@ -305,7 +305,7 @@ describe('CDK Include for nested stacks', () => {
305305
test('getNestedStack() throws an exception when getting a resource that exists in the template, but is not a Stack', () => {
306306
const parentTemplate = new inc.CfnInclude(stack, 'ParentStack', {
307307
templateFile: testTemplateFilePath('parent-two-children.json'),
308-
nestedStacks: {
308+
loadNestedStacks: {
309309
'ChildStack': {
310310
templateFile: testTemplateFilePath('child-import-stack.json'),
311311
},
@@ -319,10 +319,10 @@ describe('CDK Include for nested stacks', () => {
319319
}).toThrow(/Resource with logical ID 'BucketImport' is not a CloudFormation Stack/);
320320
});
321321

322-
test('getNestedStack() throws an exception when getting a resource that exists in the template, but was not specified in the props', () => {
322+
test('getNestedStack() throws an exception when getting a nested stack that exists in the template, but was not specified in the props', () => {
323323
const parentTemplate = new inc.CfnInclude(stack, 'ParentStack', {
324324
templateFile: testTemplateFilePath('parent-two-children.json'),
325-
nestedStacks: {
325+
loadNestedStacks: {
326326
'ChildStack': {
327327
templateFile: testTemplateFilePath('child-import-stack.json'),
328328
},
@@ -331,13 +331,13 @@ describe('CDK Include for nested stacks', () => {
331331

332332
expect(() => {
333333
parentTemplate.getNestedStack('AnotherChildStack');
334-
}).toThrow(/Nested Stack 'AnotherChildStack' was not included in the nestedStacks property when including the parent template/);
334+
}).toThrow(/Nested Stack 'AnotherChildStack' was not included in the parent template/);
335335
});
336336

337337
test('correctly handles renaming of references across nested stacks', () => {
338338
const parentTemplate = new inc.CfnInclude(stack, 'ParentStack', {
339339
templateFile: testTemplateFilePath('cross-stack-refs.json'),
340-
nestedStacks: {
340+
loadNestedStacks: {
341341
'ChildStack': {
342342
templateFile: testTemplateFilePath('child-import-stack.json'),
343343
},
@@ -360,7 +360,7 @@ describe('CDK Include for nested stacks', () => {
360360
});
361361
});
362362

363-
test('returns the CfnStack object from getResource() for a nested stack that was not in the nestedStacks property', () => {
363+
test('returns the CfnStack object from getResource() for a nested stack that was not in the loadNestedStacks property', () => {
364364
const cfnTemplate = new inc.CfnInclude(stack, 'ParentStack', {
365365
templateFile: testTemplateFilePath('parent-two-children.json'),
366366
});
@@ -370,10 +370,10 @@ describe('CDK Include for nested stacks', () => {
370370
expect(childStack1).toBeInstanceOf(core.CfnStack);
371371
});
372372

373-
test('returns the CfnStack object from getResource() for a nested stack that was in the nestedStacks property', () => {
373+
test('returns the CfnStack object from getResource() for a nested stack that was in the loadNestedStacks property', () => {
374374
const cfnTemplate = new inc.CfnInclude(stack, 'ParentStack', {
375375
templateFile: testTemplateFilePath('parent-one-child.json'),
376-
nestedStacks: {
376+
loadNestedStacks: {
377377
'ChildStack': {
378378
templateFile: testTemplateFilePath('child-import-stack.json'),
379379
},
@@ -388,7 +388,7 @@ describe('CDK Include for nested stacks', () => {
388388
test("handles Metadata, DeletionPolicy, and UpdateReplacePolicy attributes of the nested stack's resource", () => {
389389
const cfnTemplate = new inc.CfnInclude(stack, 'ParentStack', {
390390
templateFile: testTemplateFilePath('parent-with-attributes.json'),
391-
nestedStacks: {
391+
loadNestedStacks: {
392392
'ChildStack': {
393393
templateFile: testTemplateFilePath('child-import-stack.json'),
394394
},
@@ -424,6 +424,20 @@ describe('CDK Include for nested stacks', () => {
424424
});
425425
});
426426

427+
test('can lazily include a single child nested stack', () => {
428+
const parentTemplate = new inc.CfnInclude(stack, 'ParentStack', {
429+
templateFile: testTemplateFilePath('parent-one-child.json'),
430+
});
431+
const includedChild = parentTemplate.loadNestedStack('ChildStack', {
432+
templateFile: testTemplateFilePath('child-no-bucket.json'),
433+
});
434+
435+
expect(includedChild.stack).toMatchTemplate(
436+
loadTestFileToJsObject('child-no-bucket.json'),
437+
);
438+
expect(includedChild.includedTemplate.getResource('GrandChildStack')).toBeDefined();
439+
});
440+
427441
describe('for a parent stack with children and grandchildren', () => {
428442
let assetStack: core.Stack;
429443
let parentTemplate: inc.CfnInclude;
@@ -442,10 +456,10 @@ describe('CDK Include for nested stacks', () => {
442456
assetStack = new core.Stack();
443457
parentTemplate = new inc.CfnInclude(assetStack, 'ParentStack', {
444458
templateFile: testTemplateFilePath('parent-one-child.json'),
445-
nestedStacks: {
459+
loadNestedStacks: {
446460
'ChildStack': {
447461
templateFile: testTemplateFilePath('child-no-bucket.json'),
448-
nestedStacks: {
462+
loadNestedStacks: {
449463
'GrandChildStack': {
450464
templateFile: testTemplateFilePath('grandchild-import-stack.json'),
451465
},
@@ -621,7 +635,7 @@ describe('CDK Include for nested stacks', () => {
621635
parentStack = new core.Stack();
622636
const parentTemplate = new inc.CfnInclude(parentStack, 'ParentStack', {
623637
templateFile: testTemplateFilePath('parent-two-parameters.json'),
624-
nestedStacks: {
638+
loadNestedStacks: {
625639
'ChildStack': {
626640
templateFile: testTemplateFilePath('child-two-parameters.json'),
627641
parameters: {

0 commit comments

Comments
 (0)